wx.py 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. """Enable wxPython to be used interactively in prompt_toolkit
  2. """
  3. import sys
  4. import signal
  5. import time
  6. from timeit import default_timer as clock
  7. import wx
  8. def ignore_keyboardinterrupts(func):
  9. """Decorator which causes KeyboardInterrupt exceptions to be ignored during
  10. execution of the decorated function.
  11. This is used by the inputhook functions to handle the event where the user
  12. presses CTRL+C while IPython is idle, and the inputhook loop is running. In
  13. this case, we want to ignore interrupts.
  14. """
  15. def wrapper(*args, **kwargs):
  16. try:
  17. func(*args, **kwargs)
  18. except KeyboardInterrupt:
  19. pass
  20. return wrapper
  21. @ignore_keyboardinterrupts
  22. def inputhook_wx1(context):
  23. """Run the wx event loop by processing pending events only.
  24. This approach seems to work, but its performance is not great as it
  25. relies on having PyOS_InputHook called regularly.
  26. """
  27. app = wx.GetApp()
  28. if app is not None:
  29. assert wx.Thread_IsMain()
  30. # Make a temporary event loop and process system events until
  31. # there are no more waiting, then allow idle events (which
  32. # will also deal with pending or posted wx events.)
  33. evtloop = wx.EventLoop()
  34. ea = wx.EventLoopActivator(evtloop)
  35. while evtloop.Pending():
  36. evtloop.Dispatch()
  37. app.ProcessIdle()
  38. del ea
  39. return 0
  40. class EventLoopTimer(wx.Timer):
  41. def __init__(self, func):
  42. self.func = func
  43. wx.Timer.__init__(self)
  44. def Notify(self):
  45. self.func()
  46. class EventLoopRunner(object):
  47. def Run(self, time, input_is_ready):
  48. self.input_is_ready = input_is_ready
  49. self.evtloop = wx.EventLoop()
  50. self.timer = EventLoopTimer(self.check_stdin)
  51. self.timer.Start(time)
  52. self.evtloop.Run()
  53. def check_stdin(self):
  54. if self.input_is_ready():
  55. self.timer.Stop()
  56. self.evtloop.Exit()
  57. @ignore_keyboardinterrupts
  58. def inputhook_wx2(context):
  59. """Run the wx event loop, polling for stdin.
  60. This version runs the wx eventloop for an undetermined amount of time,
  61. during which it periodically checks to see if anything is ready on
  62. stdin. If anything is ready on stdin, the event loop exits.
  63. The argument to elr.Run controls how often the event loop looks at stdin.
  64. This determines the responsiveness at the keyboard. A setting of 1000
  65. enables a user to type at most 1 char per second. I have found that a
  66. setting of 10 gives good keyboard response. We can shorten it further,
  67. but eventually performance would suffer from calling select/kbhit too
  68. often.
  69. """
  70. app = wx.GetApp()
  71. if app is not None:
  72. assert wx.Thread_IsMain()
  73. elr = EventLoopRunner()
  74. # As this time is made shorter, keyboard response improves, but idle
  75. # CPU load goes up. 10 ms seems like a good compromise.
  76. elr.Run(time=10, # CHANGE time here to control polling interval
  77. input_is_ready=context.input_is_ready)
  78. return 0
  79. @ignore_keyboardinterrupts
  80. def inputhook_wx3(context):
  81. """Run the wx event loop by processing pending events only.
  82. This is like inputhook_wx1, but it keeps processing pending events
  83. until stdin is ready. After processing all pending events, a call to
  84. time.sleep is inserted. This is needed, otherwise, CPU usage is at 100%.
  85. This sleep time should be tuned though for best performance.
  86. """
  87. app = wx.GetApp()
  88. if app is not None:
  89. assert wx.Thread_IsMain()
  90. # The import of wx on Linux sets the handler for signal.SIGINT
  91. # to 0. This is a bug in wx or gtk. We fix by just setting it
  92. # back to the Python default.
  93. if not callable(signal.getsignal(signal.SIGINT)):
  94. signal.signal(signal.SIGINT, signal.default_int_handler)
  95. evtloop = wx.EventLoop()
  96. ea = wx.EventLoopActivator(evtloop)
  97. t = clock()
  98. while not context.input_is_ready():
  99. while evtloop.Pending():
  100. t = clock()
  101. evtloop.Dispatch()
  102. app.ProcessIdle()
  103. # We need to sleep at this point to keep the idle CPU load
  104. # low. However, if sleep to long, GUI response is poor. As
  105. # a compromise, we watch how often GUI events are being processed
  106. # and switch between a short and long sleep time. Here are some
  107. # stats useful in helping to tune this.
  108. # time CPU load
  109. # 0.001 13%
  110. # 0.005 3%
  111. # 0.01 1.5%
  112. # 0.05 0.5%
  113. used_time = clock() - t
  114. if used_time > 10.0:
  115. # print('Sleep for 1 s') # dbg
  116. time.sleep(1.0)
  117. elif used_time > 0.1:
  118. # Few GUI events coming in, so we can sleep longer
  119. # print('Sleep for 0.05 s') # dbg
  120. time.sleep(0.05)
  121. else:
  122. # Many GUI events coming in, so sleep only very little
  123. time.sleep(0.001)
  124. del ea
  125. return 0
  126. @ignore_keyboardinterrupts
  127. def inputhook_wxphoenix(context):
  128. """Run the wx event loop until the user provides more input.
  129. This input hook is suitable for use with wxPython >= 4 (a.k.a. Phoenix).
  130. It uses the same approach to that used in
  131. ipykernel.eventloops.loop_wx. The wx.MainLoop is executed, and a wx.Timer
  132. is used to periodically poll the context for input. As soon as input is
  133. ready, the wx.MainLoop is stopped.
  134. """
  135. app = wx.GetApp()
  136. if app is None:
  137. return
  138. if context.input_is_ready():
  139. return
  140. assert wx.IsMainThread()
  141. # Wx uses milliseconds
  142. poll_interval = 100
  143. # Use a wx.Timer to periodically check whether input is ready - as soon as
  144. # it is, we exit the main loop
  145. timer = wx.Timer()
  146. def poll(ev):
  147. if context.input_is_ready():
  148. timer.Stop()
  149. app.ExitMainLoop()
  150. timer.Start(poll_interval)
  151. timer.Bind(wx.EVT_TIMER, poll)
  152. # The import of wx on Linux sets the handler for signal.SIGINT to 0. This
  153. # is a bug in wx or gtk. We fix by just setting it back to the Python
  154. # default.
  155. if not callable(signal.getsignal(signal.SIGINT)):
  156. signal.signal(signal.SIGINT, signal.default_int_handler)
  157. # The SetExitOnFrameDelete call allows us to run the wx mainloop without
  158. # having a frame open.
  159. app.SetExitOnFrameDelete(False)
  160. app.MainLoop()
  161. # Get the major wx version number to figure out what input hook we should use.
  162. major_version = 3
  163. try:
  164. major_version = int(wx.__version__[0])
  165. except Exception:
  166. pass
  167. # Use the phoenix hook on all platforms for wxpython >= 4
  168. if major_version >= 4:
  169. inputhook = inputhook_wxphoenix
  170. # On OSX, evtloop.Pending() always returns True, regardless of there being
  171. # any events pending. As such we can't use implementations 1 or 3 of the
  172. # inputhook as those depend on a pending/dispatch loop.
  173. elif sys.platform == 'darwin':
  174. inputhook = inputhook_wx2
  175. else:
  176. inputhook = inputhook_wx3