From e06b59199b84ce92285396832d9cb3eb884022dc 2019-11-12 22:30:22
From: Matthias Bussonnier <bussonniermatthias@gmail.com>
Date: 2019-11-12 22:30:22
Subject: [PATCH] Merge pull request #11911 from pauldmccarthy/enh/wxphoenix_eventloop

Input hook for wxpython phoenix
---

diff --git a/IPython/terminal/pt_inputhooks/wx.py b/IPython/terminal/pt_inputhooks/wx.py
index e6b4f57..618f092 100644
--- a/IPython/terminal/pt_inputhooks/wx.py
+++ b/IPython/terminal/pt_inputhooks/wx.py
@@ -8,30 +8,45 @@ from timeit import default_timer as clock
 import wx
 
 
+def ignore_keyboardinterrupts(func):
+    """Decorator which causes KeyboardInterrupt exceptions to be ignored during
+    execution of the decorated function.
+
+    This is used by the inputhook functions to handle the event where the user
+    presses CTRL+C while IPython is idle, and the inputhook loop is running. In
+    this case, we want to ignore interrupts.
+    """
+    def wrapper(*args, **kwargs):
+        try:
+            func(*args, **kwargs)
+        except KeyboardInterrupt:
+            pass
+    return wrapper
+
+
+@ignore_keyboardinterrupts
 def inputhook_wx1(context):
     """Run the wx event loop by processing pending events only.
 
     This approach seems to work, but its performance is not great as it
     relies on having PyOS_InputHook called regularly.
     """
-    try:
-        app = wx.GetApp()
-        if app is not None:
-            assert wx.Thread_IsMain()
-
-            # Make a temporary event loop and process system events until
-            # there are no more waiting, then allow idle events (which
-            # will also deal with pending or posted wx events.)
-            evtloop = wx.EventLoop()
-            ea = wx.EventLoopActivator(evtloop)
-            while evtloop.Pending():
-                evtloop.Dispatch()
-            app.ProcessIdle()
-            del ea
-    except KeyboardInterrupt:
-        pass
+    app = wx.GetApp()
+    if app is not None:
+        assert wx.Thread_IsMain()
+
+        # Make a temporary event loop and process system events until
+        # there are no more waiting, then allow idle events (which
+        # will also deal with pending or posted wx events.)
+        evtloop = wx.EventLoop()
+        ea = wx.EventLoopActivator(evtloop)
+        while evtloop.Pending():
+            evtloop.Dispatch()
+        app.ProcessIdle()
+        del ea
     return 0
 
+
 class EventLoopTimer(wx.Timer):
 
     def __init__(self, func):
@@ -41,6 +56,7 @@ class EventLoopTimer(wx.Timer):
     def Notify(self):
         self.func()
 
+
 class EventLoopRunner(object):
 
     def Run(self, time, input_is_ready):
@@ -55,6 +71,8 @@ class EventLoopRunner(object):
             self.timer.Stop()
             self.evtloop.Exit()
 
+
+@ignore_keyboardinterrupts
 def inputhook_wx2(context):
     """Run the wx event loop, polling for stdin.
 
@@ -69,19 +87,18 @@ def inputhook_wx2(context):
     but eventually performance would suffer from calling select/kbhit too
     often.
     """
-    try:
-        app = wx.GetApp()
-        if app is not None:
-            assert wx.Thread_IsMain()
-            elr = EventLoopRunner()
-            # As this time is made shorter, keyboard response improves, but idle
-            # CPU load goes up.  10 ms seems like a good compromise.
-            elr.Run(time=10,  # CHANGE time here to control polling interval
-                    input_is_ready=context.input_is_ready)
-    except KeyboardInterrupt:
-        pass
+    app = wx.GetApp()
+    if app is not None:
+        assert wx.Thread_IsMain()
+        elr = EventLoopRunner()
+        # As this time is made shorter, keyboard response improves, but idle
+        # CPU load goes up.  10 ms seems like a good compromise.
+        elr.Run(time=10,  # CHANGE time here to control polling interval
+                input_is_ready=context.input_is_ready)
     return 0
 
+
+@ignore_keyboardinterrupts
 def inputhook_wx3(context):
     """Run the wx event loop by processing pending events only.
 
@@ -90,58 +107,111 @@ def inputhook_wx3(context):
     time.sleep is inserted.  This is needed, otherwise, CPU usage is at 100%.
     This sleep time should be tuned though for best performance.
     """
-    # We need to protect against a user pressing Control-C when IPython is
-    # idle and this is running. We trap KeyboardInterrupt and pass.
-    try:
-        app = wx.GetApp()
-        if app is not None:
-            assert wx.Thread_IsMain()
-
-            # The import of wx on Linux sets the handler for signal.SIGINT
-            # to 0.  This is a bug in wx or gtk.  We fix by just setting it
-            # back to the Python default.
-            if not callable(signal.getsignal(signal.SIGINT)):
-                signal.signal(signal.SIGINT, signal.default_int_handler)
-
-            evtloop = wx.EventLoop()
-            ea = wx.EventLoopActivator(evtloop)
-            t = clock()
-            while not context.input_is_ready():
-                while evtloop.Pending():
-                    t = clock()
-                    evtloop.Dispatch()
-                app.ProcessIdle()
-                # We need to sleep at this point to keep the idle CPU load
-                # low.  However, if sleep to long, GUI response is poor.  As
-                # a compromise, we watch how often GUI events are being processed
-                # and switch between a short and long sleep time.  Here are some
-                # stats useful in helping to tune this.
-                # time    CPU load
-                # 0.001   13%
-                # 0.005   3%
-                # 0.01    1.5%
-                # 0.05    0.5%
-                used_time = clock() - t
-                if used_time > 10.0:
-                    # print 'Sleep for 1 s'  # dbg
-                    time.sleep(1.0)
-                elif used_time > 0.1:
-                    # Few GUI events coming in, so we can sleep longer
-                    # print 'Sleep for 0.05 s'  # dbg
-                    time.sleep(0.05)
-                else:
-                    # Many GUI events coming in, so sleep only very little
-                    time.sleep(0.001)
-            del ea
-    except KeyboardInterrupt:
-        pass
+    app = wx.GetApp()
+    if app is not None:
+        assert wx.Thread_IsMain()
+
+        # The import of wx on Linux sets the handler for signal.SIGINT
+        # to 0.  This is a bug in wx or gtk.  We fix by just setting it
+        # back to the Python default.
+        if not callable(signal.getsignal(signal.SIGINT)):
+            signal.signal(signal.SIGINT, signal.default_int_handler)
+
+        evtloop = wx.EventLoop()
+        ea = wx.EventLoopActivator(evtloop)
+        t = clock()
+        while not context.input_is_ready():
+            while evtloop.Pending():
+                t = clock()
+                evtloop.Dispatch()
+            app.ProcessIdle()
+            # We need to sleep at this point to keep the idle CPU load
+            # low.  However, if sleep to long, GUI response is poor.  As
+            # a compromise, we watch how often GUI events are being processed
+            # and switch between a short and long sleep time.  Here are some
+            # stats useful in helping to tune this.
+            # time    CPU load
+            # 0.001   13%
+            # 0.005   3%
+            # 0.01    1.5%
+            # 0.05    0.5%
+            used_time = clock() - t
+            if used_time > 10.0:
+                # print 'Sleep for 1 s'  # dbg
+                time.sleep(1.0)
+            elif used_time > 0.1:
+                # Few GUI events coming in, so we can sleep longer
+                # print 'Sleep for 0.05 s'  # dbg
+                time.sleep(0.05)
+            else:
+                # Many GUI events coming in, so sleep only very little
+                time.sleep(0.001)
+        del ea
     return 0
 
-if sys.platform == 'darwin':
-    # On OSX, evtloop.Pending() always returns True, regardless of there being
-    # any events pending. As such we can't use implementations 1 or 3 of the
-    # inputhook as those depend on a pending/dispatch loop.
+
+@ignore_keyboardinterrupts
+def inputhook_wxphoenix(context):
+    """Run the wx event loop until the user provides more input.
+
+    This input hook is suitable for use with wxPython >= 4 (a.k.a. Phoenix).
+
+    It uses the same approach to that used in
+    ipykernel.eventloops.loop_wx. The wx.MainLoop is executed, and a wx.Timer
+    is used to periodically poll the context for input. As soon as input is
+    ready, the wx.MainLoop is stopped.
+    """
+
+    app = wx.GetApp()
+
+    if app is None:
+        return
+
+    if context.input_is_ready():
+        return
+
+    assert wx.IsMainThread()
+
+    # Wx uses milliseconds
+    poll_interval = 100
+
+    # Use a wx.Timer to periodically check whether input is ready - as soon as
+    # it is, we exit the main loop
+    def poll(ev):
+        if context.input_is_ready():
+            app.ExitMainLoop()
+
+    timer = wx.Timer()
+    timer.Start(poll_interval)
+    timer.Bind(wx.EVT_TIMER, poll)
+
+    # The import of wx on Linux sets the handler for signal.SIGINT to 0.  This
+    # is a bug in wx or gtk.  We fix by just setting it back to the Python
+    # default.
+    if not callable(signal.getsignal(signal.SIGINT)):
+        signal.signal(signal.SIGINT, signal.default_int_handler)
+
+    # The SetExitOnFrameDelete call allows us to run the wx mainloop without
+    # having a frame open.
+    app.SetExitOnFrameDelete(False)
+    app.MainLoop()
+
+
+# Get the major wx version number to figure out what input hook we should use.
+major_version = 3
+
+try:
+    major_version = int(wx.__version__[0])
+except Exception:
+    pass
+
+# Use the phoenix hook on all platforms for wxpython >= 4
+if major_version >= 4:
+    inputhook = inputhook_wxphoenix
+# On OSX, evtloop.Pending() always returns True, regardless of there being
+# any events pending. As such we can't use implementations 1 or 3 of the
+# inputhook as those depend on a pending/dispatch loop.
+elif sys.platform == 'darwin':
     inputhook = inputhook_wx2
 else:
-    # This is our default implementation
     inputhook = inputhook_wx3