From d58fb67c52f23ead3480d3e5eae3637dfe4cecfc Mon Sep 17 00:00:00 2001 From: Jofkos Date: Mon, 10 Jan 2022 09:14:36 +0100 Subject: [PATCH 1/2] Merged PR fabioz/PyDev.Debugger#156, updated it with fixes PR ipython/ipython#12804 and PR ipython/ipython#12807 --- pydev_ipython/inputhook.py | 36 ++---- pydev_ipython/inputhookmac.py | 209 ++++++++++++++++++++++++++++++++++ 2 files changed, 219 insertions(+), 26 deletions(-) create mode 100644 pydev_ipython/inputhookmac.py diff --git a/pydev_ipython/inputhook.py b/pydev_ipython/inputhook.py index fe088466f..9cd8efa9b 100644 --- a/pydev_ipython/inputhook.py +++ b/pydev_ipython/inputhook.py @@ -38,12 +38,10 @@ # Utilities #----------------------------------------------------------------------------- - def ignore_CTRL_C(): """Ignore CTRL+C (not implemented).""" pass - def allow_CTRL_C(): """Take CTRL+C into account (not implemented).""" pass @@ -299,6 +297,7 @@ def disable_tk(self): """ self.clear_inputhook() + def enable_glut(self, app=None): """ Enable event loop integration with GLUT. @@ -330,14 +329,13 @@ def enable_glut(self, app=None): glut_idle, inputhook_glut if GUI_GLUT not in self._apps: - argv = getattr(sys, 'argv', []) - glut.glutInit(argv) + glut.glutInit(sys.argv) glut.glutInitDisplayMode(glut_display_mode) # This is specific to freeglut if bool(glut.glutSetOption): glut.glutSetOption(glut.GLUT_ACTION_ON_WINDOW_CLOSE, glut.GLUT_ACTION_GLUTMAINLOOP_RETURNS) - glut.glutCreateWindow(argv[0] if len(argv) > 0 else '') + glut.glutCreateWindow(sys.argv[0]) glut.glutReshapeWindow(1, 1) glut.glutHideWindow() glut.glutWMCloseFunc(glut_close) @@ -351,6 +349,7 @@ def enable_glut(self, app=None): self._current_gui = GUI_GLUT self._apps[GUI_GLUT] = True + def disable_glut(self): """Disable event loop integration with glut. @@ -424,25 +423,12 @@ def disable_gtk3(self): def enable_mac(self, app=None): """ Enable event loop integration with MacOSX. - We call function pyplot.pause, which updates and displays active - figure during pause. It's not MacOSX-specific, but it enables to - avoid inputhooks in native MacOSX backend. - Also we shouldn't import pyplot, until user does it. Cause it's - possible to choose backend before importing pyplot for the first - time only. - """ + Uses native inputhooks for MacOSX that significantly improve + performance and responsiveness. - def inputhook_mac(app=None): - if self.pyplot_imported: - pyplot = sys.modules['matplotlib.pyplot'] - try: - pyplot.pause(0.01) - except: - pass - else: - if 'matplotlib.pyplot' in sys.modules: - self.pyplot_imported = True + """ + from pydev_ipython.inputhookmac import inputhook_mac self.set_inputhook(inputhook_mac) self._current_gui = GUI_OSX @@ -453,7 +439,6 @@ def current_gui(self): """Return a string indicating the currently active GUI or None.""" return self._current_gui - inputhook_manager = InputHookManager() enable_wx = inputhook_manager.enable_wx @@ -487,7 +472,6 @@ def current_gui(self): get_return_control_callback = inputhook_manager.get_return_control_callback get_inputhook = inputhook_manager.get_inputhook - # Convenience function to switch amongst them def enable_gui(gui=None, app=None): """Switch amongst GUI input hooks by name. @@ -535,11 +519,10 @@ def enable_gui(gui=None, app=None): if gui is None or gui == '': gui_hook = clear_inputhook else: - e = "Invalid GUI request %r, valid ones are:%s" % (gui, list(guis.keys())) + e = "Invalid GUI request %r, valid ones are:%s" % (gui, guis.keys()) raise ValueError(e) return gui_hook(app) - __all__ = [ "GUI_WX", "GUI_QT", @@ -553,6 +536,7 @@ def enable_gui(gui=None, app=None): "GUI_GTK3", "GUI_NONE", + "ignore_CTRL_C", "allow_CTRL_C", diff --git a/pydev_ipython/inputhookmac.py b/pydev_ipython/inputhookmac.py new file mode 100644 index 000000000..8c824b18d --- /dev/null +++ b/pydev_ipython/inputhookmac.py @@ -0,0 +1,209 @@ +"""Inputhook for OS X + +Calls NSApp / CoreFoundation APIs via ctypes. +""" + +import os +from pydev_ipython.inputhook import stdin_ready +import time +from _pydev_imps._pydev_saved_modules import threading as _threading_ + +# obj-c boilerplate from appnope, used under BSD 2-clause + +import ctypes +import ctypes.util + +objc = ctypes.cdll.LoadLibrary(ctypes.util.find_library('objc')) + +void_p = ctypes.c_void_p + +objc.objc_getClass.restype = void_p +objc.sel_registerName.restype = void_p +objc.objc_msgSend.restype = void_p +objc.objc_msgSend.argtypes = [void_p, void_p] + +msg = objc.objc_msgSend + +ccounter = True + +def _utf8(s): + """ensure utf8 bytes""" + if not isinstance(s, bytes): + s = s.encode('utf8') + return s + +def n(name): + """create a selector name (for ObjC methods)""" + return objc.sel_registerName(_utf8(name)) + +def C(classname): + """get an ObjC Class by name""" + return objc.objc_getClass(_utf8(classname)) + +# end obj-c boilerplate from appnope + +# CoreFoundation C-API calls we will use: +CoreFoundation = ctypes.cdll.LoadLibrary(ctypes.util.find_library('CoreFoundation')) + +CFFileDescriptorCreate = CoreFoundation.CFFileDescriptorCreate +CFFileDescriptorCreate.restype = void_p +CFFileDescriptorCreate.argtypes = [void_p, ctypes.c_int, ctypes.c_bool, void_p, void_p] + +CFFileDescriptorGetNativeDescriptor = CoreFoundation.CFFileDescriptorGetNativeDescriptor +CFFileDescriptorGetNativeDescriptor.restype = ctypes.c_int +CFFileDescriptorGetNativeDescriptor.argtypes = [void_p] + +CFFileDescriptorEnableCallBacks = CoreFoundation.CFFileDescriptorEnableCallBacks +CFFileDescriptorEnableCallBacks.restype = None +CFFileDescriptorEnableCallBacks.argtypes = [void_p, ctypes.c_ulong] + +CFFileDescriptorCreateRunLoopSource = CoreFoundation.CFFileDescriptorCreateRunLoopSource +CFFileDescriptorCreateRunLoopSource.restype = void_p +CFFileDescriptorCreateRunLoopSource.argtypes = [void_p, void_p, void_p] + +CFRunLoopGetCurrent = CoreFoundation.CFRunLoopGetCurrent +CFRunLoopGetCurrent.restype = void_p + +CFRunLoopAddSource = CoreFoundation.CFRunLoopAddSource +CFRunLoopAddSource.restype = None +CFRunLoopAddSource.argtypes = [void_p, void_p, void_p] + +CFRelease = CoreFoundation.CFRelease +CFRelease.restype = None +CFRelease.argtypes = [void_p] + +CFFileDescriptorInvalidate = CoreFoundation.CFFileDescriptorInvalidate +CFFileDescriptorInvalidate.restype = None +CFFileDescriptorInvalidate.argtypes = [void_p] + +# From CFFileDescriptor.h +kCFFileDescriptorReadCallBack = 1 +kCFRunLoopCommonModes = void_p.in_dll(CoreFoundation, 'kCFRunLoopCommonModes') + + +def _NSApp(): + """Return the global NSApplication instance (NSApp)""" + objc.objc_msgSend.argtypes = [void_p, void_p] + return msg(C('NSApplication'), n('sharedApplication')) + + +def _wake(NSApp): + """Wake the Application""" + objc.objc_msgSend.argtypes = [ + void_p, + void_p, + void_p, + void_p, + void_p, + void_p, + void_p, + void_p, + void_p, + void_p, + void_p, + ] + event = msg( + C("NSEvent"), + n( + "otherEventWithType:location:modifierFlags:" + "timestamp:windowNumber:context:subtype:data1:data2:" + ), + 15, # Type + 0, # location + 0, # flags + 0, # timestamp + 0, # window + None, # context + 0, # subtype + 0, # data1 + 0, # data2 + ) + objc.objc_msgSend.argtypes = [void_p, void_p, void_p, void_p] + msg(NSApp, n('postEvent:atStart:'), void_p(event), True) + + +_triggered = _threading_.Event() + +def _input_callback(fdref, flags, info): + """Callback to fire when there's input to be read""" + + _triggered.set() + CFFileDescriptorInvalidate(fdref) + CFRelease(fdref) + NSApp = _NSApp() + objc.objc_msgSend.argtypes = [void_p, void_p, void_p] + msg(NSApp, n('stop:'), NSApp) + _wake(NSApp) + +_c_callback_func_type = ctypes.CFUNCTYPE(None, void_p, void_p, void_p) +_c_input_callback = _c_callback_func_type(_input_callback) + +def _stop_on_read(fd): + """Register callback to stop eventloop when there's data on fd""" + + _triggered.clear() + fdref = CFFileDescriptorCreate(None, fd, False, _c_input_callback, None) + CFFileDescriptorEnableCallBacks(fdref, kCFFileDescriptorReadCallBack) + source = CFFileDescriptorCreateRunLoopSource(None, fdref, 0) + loop = CFRunLoopGetCurrent() + CFRunLoopAddSource(loop, source, kCFRunLoopCommonModes) + CFRelease(source) + + +class Timer(_threading_.Thread): + def __init__(self, callback=None, interval=0.1): + super().__init__() + self.callback = callback + self.interval = interval + self._stopev = _threading_.Event() + + def run(self, *args, **kwargs): + if callable(self.callback): + while not self._stopev.is_set(): + time.sleep(self.interval) + self.callback(self._stopev) + + +class FHSingleton(object): + """Implements a singleton resource manager for pipes. Avoids opening and + closing pipes during event loops. + """ + _instance = None + def __new__(cls): + if cls._instance is None: + cls._instance = super().__new__(cls) + cls.rh, cls.wh = os.pipe() + else: + # Clears the character written to trigger callback in the last + # loop. + os.read(cls.rh, 1) + + return cls._instance + + +def inputhook_mac(): + fh = FHSingleton() + + # stop_cb is used to cleanly terminate loop when last figure window is + # closed. + stop_cb = _threading_.Event() + def inputhook_cb(stop): + if stop_cb.is_set() or stdin_ready(): + os.write(fh.wh, b'x') + stop.set() + + + t = Timer(callback=inputhook_cb) + t.start() + NSApp = _NSApp() + objc.objc_msgSend.argtypes = [void_p, void_p] + _stop_on_read(fh.rh) + msg(NSApp, n('run')) + if not _triggered.is_set(): + # app closed without firing callback, + # probably due to last window being closed. + # Run the loop manually in this case, + # since there may be events still to process (#9734) + CoreFoundation.CFRunLoopRun() + stop_cb.set() + t.join() From 099e9f57cbf7b6ae1caccca670b5233aa35cb5ca Mon Sep 17 00:00:00 2001 From: Jofkos Date: Tue, 3 May 2022 16:42:33 +0200 Subject: [PATCH 2/2] Update inputhook.py to reflect current version --- pydev_ipython/inputhook.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/pydev_ipython/inputhook.py b/pydev_ipython/inputhook.py index 9cd8efa9b..af186a2e2 100644 --- a/pydev_ipython/inputhook.py +++ b/pydev_ipython/inputhook.py @@ -38,10 +38,12 @@ # Utilities #----------------------------------------------------------------------------- + def ignore_CTRL_C(): """Ignore CTRL+C (not implemented).""" pass + def allow_CTRL_C(): """Take CTRL+C into account (not implemented).""" pass @@ -297,7 +299,6 @@ def disable_tk(self): """ self.clear_inputhook() - def enable_glut(self, app=None): """ Enable event loop integration with GLUT. @@ -329,13 +330,14 @@ def enable_glut(self, app=None): glut_idle, inputhook_glut if GUI_GLUT not in self._apps: - glut.glutInit(sys.argv) + argv = getattr(sys, 'argv', []) + glut.glutInit(argv) glut.glutInitDisplayMode(glut_display_mode) # This is specific to freeglut if bool(glut.glutSetOption): glut.glutSetOption(glut.GLUT_ACTION_ON_WINDOW_CLOSE, glut.GLUT_ACTION_GLUTMAINLOOP_RETURNS) - glut.glutCreateWindow(sys.argv[0]) + glut.glutCreateWindow(argv[0] if len(argv) > 0 else '') glut.glutReshapeWindow(1, 1) glut.glutHideWindow() glut.glutWMCloseFunc(glut_close) @@ -349,7 +351,6 @@ def enable_glut(self, app=None): self._current_gui = GUI_GLUT self._apps[GUI_GLUT] = True - def disable_glut(self): """Disable event loop integration with glut. @@ -439,6 +440,7 @@ def current_gui(self): """Return a string indicating the currently active GUI or None.""" return self._current_gui + inputhook_manager = InputHookManager() enable_wx = inputhook_manager.enable_wx @@ -472,6 +474,7 @@ def current_gui(self): get_return_control_callback = inputhook_manager.get_return_control_callback get_inputhook = inputhook_manager.get_inputhook + # Convenience function to switch amongst them def enable_gui(gui=None, app=None): """Switch amongst GUI input hooks by name. @@ -519,10 +522,11 @@ def enable_gui(gui=None, app=None): if gui is None or gui == '': gui_hook = clear_inputhook else: - e = "Invalid GUI request %r, valid ones are:%s" % (gui, guis.keys()) + e = "Invalid GUI request %r, valid ones are:%s" % (gui, list(guis.keys())) raise ValueError(e) return gui_hook(app) + __all__ = [ "GUI_WX", "GUI_QT", @@ -536,7 +540,6 @@ def enable_gui(gui=None, app=None): "GUI_GTK3", "GUI_NONE", - "ignore_CTRL_C", "allow_CTRL_C",