Skip to content

Commit

Permalink
Merged PR fabioz#156, updated it with fixes PR ipython/ipython#12804
Browse files Browse the repository at this point in the history
  • Loading branch information
Jofkos committed Jan 10, 2022
1 parent 0f8c02a commit d58fb67
Show file tree
Hide file tree
Showing 2 changed files with 219 additions and 26 deletions.
36 changes: 10 additions & 26 deletions pydev_ipython/inputhook.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -299,6 +297,7 @@ def disable_tk(self):
"""
self.clear_inputhook()


def enable_glut(self, app=None):
""" Enable event loop integration with GLUT.
Expand Down Expand Up @@ -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)
Expand All @@ -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.
Expand Down Expand Up @@ -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

Expand All @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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",
Expand All @@ -553,6 +536,7 @@ def enable_gui(gui=None, app=None):
"GUI_GTK3",
"GUI_NONE",


"ignore_CTRL_C",
"allow_CTRL_C",

Expand Down
209 changes: 209 additions & 0 deletions pydev_ipython/inputhookmac.py
Original file line number Diff line number Diff line change
@@ -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()

0 comments on commit d58fb67

Please sign in to comment.