Skip to content

Commit d58fb67

Browse files
committed
Merged PR fabioz#156, updated it with fixes PR ipython/ipython#12804 and PR ipython/ipython#12807
1 parent 0f8c02a commit d58fb67

File tree

2 files changed

+219
-26
lines changed

2 files changed

+219
-26
lines changed

pydev_ipython/inputhook.py

Lines changed: 10 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,10 @@
3838
# Utilities
3939
#-----------------------------------------------------------------------------
4040

41-
4241
def ignore_CTRL_C():
4342
"""Ignore CTRL+C (not implemented)."""
4443
pass
4544

46-
4745
def allow_CTRL_C():
4846
"""Take CTRL+C into account (not implemented)."""
4947
pass
@@ -299,6 +297,7 @@ def disable_tk(self):
299297
"""
300298
self.clear_inputhook()
301299

300+
302301
def enable_glut(self, app=None):
303302
""" Enable event loop integration with GLUT.
304303
@@ -330,14 +329,13 @@ def enable_glut(self, app=None):
330329
glut_idle, inputhook_glut
331330

332331
if GUI_GLUT not in self._apps:
333-
argv = getattr(sys, 'argv', [])
334-
glut.glutInit(argv)
332+
glut.glutInit(sys.argv)
335333
glut.glutInitDisplayMode(glut_display_mode)
336334
# This is specific to freeglut
337335
if bool(glut.glutSetOption):
338336
glut.glutSetOption(glut.GLUT_ACTION_ON_WINDOW_CLOSE,
339337
glut.GLUT_ACTION_GLUTMAINLOOP_RETURNS)
340-
glut.glutCreateWindow(argv[0] if len(argv) > 0 else '')
338+
glut.glutCreateWindow(sys.argv[0])
341339
glut.glutReshapeWindow(1, 1)
342340
glut.glutHideWindow()
343341
glut.glutWMCloseFunc(glut_close)
@@ -351,6 +349,7 @@ def enable_glut(self, app=None):
351349
self._current_gui = GUI_GLUT
352350
self._apps[GUI_GLUT] = True
353351

352+
354353
def disable_glut(self):
355354
"""Disable event loop integration with glut.
356355
@@ -424,25 +423,12 @@ def disable_gtk3(self):
424423
def enable_mac(self, app=None):
425424
""" Enable event loop integration with MacOSX.
426425
427-
We call function pyplot.pause, which updates and displays active
428-
figure during pause. It's not MacOSX-specific, but it enables to
429-
avoid inputhooks in native MacOSX backend.
430-
Also we shouldn't import pyplot, until user does it. Cause it's
431-
possible to choose backend before importing pyplot for the first
432-
time only.
433-
"""
426+
Uses native inputhooks for MacOSX that significantly improve
427+
performance and responsiveness.
434428
435-
def inputhook_mac(app=None):
436-
if self.pyplot_imported:
437-
pyplot = sys.modules['matplotlib.pyplot']
438-
try:
439-
pyplot.pause(0.01)
440-
except:
441-
pass
442-
else:
443-
if 'matplotlib.pyplot' in sys.modules:
444-
self.pyplot_imported = True
429+
"""
445430

431+
from pydev_ipython.inputhookmac import inputhook_mac
446432
self.set_inputhook(inputhook_mac)
447433
self._current_gui = GUI_OSX
448434

@@ -453,7 +439,6 @@ def current_gui(self):
453439
"""Return a string indicating the currently active GUI or None."""
454440
return self._current_gui
455441

456-
457442
inputhook_manager = InputHookManager()
458443

459444
enable_wx = inputhook_manager.enable_wx
@@ -487,7 +472,6 @@ def current_gui(self):
487472
get_return_control_callback = inputhook_manager.get_return_control_callback
488473
get_inputhook = inputhook_manager.get_inputhook
489474

490-
491475
# Convenience function to switch amongst them
492476
def enable_gui(gui=None, app=None):
493477
"""Switch amongst GUI input hooks by name.
@@ -535,11 +519,10 @@ def enable_gui(gui=None, app=None):
535519
if gui is None or gui == '':
536520
gui_hook = clear_inputhook
537521
else:
538-
e = "Invalid GUI request %r, valid ones are:%s" % (gui, list(guis.keys()))
522+
e = "Invalid GUI request %r, valid ones are:%s" % (gui, guis.keys())
539523
raise ValueError(e)
540524
return gui_hook(app)
541525

542-
543526
__all__ = [
544527
"GUI_WX",
545528
"GUI_QT",
@@ -553,6 +536,7 @@ def enable_gui(gui=None, app=None):
553536
"GUI_GTK3",
554537
"GUI_NONE",
555538

539+
556540
"ignore_CTRL_C",
557541
"allow_CTRL_C",
558542

pydev_ipython/inputhookmac.py

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
"""Inputhook for OS X
2+
3+
Calls NSApp / CoreFoundation APIs via ctypes.
4+
"""
5+
6+
import os
7+
from pydev_ipython.inputhook import stdin_ready
8+
import time
9+
from _pydev_imps._pydev_saved_modules import threading as _threading_
10+
11+
# obj-c boilerplate from appnope, used under BSD 2-clause
12+
13+
import ctypes
14+
import ctypes.util
15+
16+
objc = ctypes.cdll.LoadLibrary(ctypes.util.find_library('objc'))
17+
18+
void_p = ctypes.c_void_p
19+
20+
objc.objc_getClass.restype = void_p
21+
objc.sel_registerName.restype = void_p
22+
objc.objc_msgSend.restype = void_p
23+
objc.objc_msgSend.argtypes = [void_p, void_p]
24+
25+
msg = objc.objc_msgSend
26+
27+
ccounter = True
28+
29+
def _utf8(s):
30+
"""ensure utf8 bytes"""
31+
if not isinstance(s, bytes):
32+
s = s.encode('utf8')
33+
return s
34+
35+
def n(name):
36+
"""create a selector name (for ObjC methods)"""
37+
return objc.sel_registerName(_utf8(name))
38+
39+
def C(classname):
40+
"""get an ObjC Class by name"""
41+
return objc.objc_getClass(_utf8(classname))
42+
43+
# end obj-c boilerplate from appnope
44+
45+
# CoreFoundation C-API calls we will use:
46+
CoreFoundation = ctypes.cdll.LoadLibrary(ctypes.util.find_library('CoreFoundation'))
47+
48+
CFFileDescriptorCreate = CoreFoundation.CFFileDescriptorCreate
49+
CFFileDescriptorCreate.restype = void_p
50+
CFFileDescriptorCreate.argtypes = [void_p, ctypes.c_int, ctypes.c_bool, void_p, void_p]
51+
52+
CFFileDescriptorGetNativeDescriptor = CoreFoundation.CFFileDescriptorGetNativeDescriptor
53+
CFFileDescriptorGetNativeDescriptor.restype = ctypes.c_int
54+
CFFileDescriptorGetNativeDescriptor.argtypes = [void_p]
55+
56+
CFFileDescriptorEnableCallBacks = CoreFoundation.CFFileDescriptorEnableCallBacks
57+
CFFileDescriptorEnableCallBacks.restype = None
58+
CFFileDescriptorEnableCallBacks.argtypes = [void_p, ctypes.c_ulong]
59+
60+
CFFileDescriptorCreateRunLoopSource = CoreFoundation.CFFileDescriptorCreateRunLoopSource
61+
CFFileDescriptorCreateRunLoopSource.restype = void_p
62+
CFFileDescriptorCreateRunLoopSource.argtypes = [void_p, void_p, void_p]
63+
64+
CFRunLoopGetCurrent = CoreFoundation.CFRunLoopGetCurrent
65+
CFRunLoopGetCurrent.restype = void_p
66+
67+
CFRunLoopAddSource = CoreFoundation.CFRunLoopAddSource
68+
CFRunLoopAddSource.restype = None
69+
CFRunLoopAddSource.argtypes = [void_p, void_p, void_p]
70+
71+
CFRelease = CoreFoundation.CFRelease
72+
CFRelease.restype = None
73+
CFRelease.argtypes = [void_p]
74+
75+
CFFileDescriptorInvalidate = CoreFoundation.CFFileDescriptorInvalidate
76+
CFFileDescriptorInvalidate.restype = None
77+
CFFileDescriptorInvalidate.argtypes = [void_p]
78+
79+
# From CFFileDescriptor.h
80+
kCFFileDescriptorReadCallBack = 1
81+
kCFRunLoopCommonModes = void_p.in_dll(CoreFoundation, 'kCFRunLoopCommonModes')
82+
83+
84+
def _NSApp():
85+
"""Return the global NSApplication instance (NSApp)"""
86+
objc.objc_msgSend.argtypes = [void_p, void_p]
87+
return msg(C('NSApplication'), n('sharedApplication'))
88+
89+
90+
def _wake(NSApp):
91+
"""Wake the Application"""
92+
objc.objc_msgSend.argtypes = [
93+
void_p,
94+
void_p,
95+
void_p,
96+
void_p,
97+
void_p,
98+
void_p,
99+
void_p,
100+
void_p,
101+
void_p,
102+
void_p,
103+
void_p,
104+
]
105+
event = msg(
106+
C("NSEvent"),
107+
n(
108+
"otherEventWithType:location:modifierFlags:"
109+
"timestamp:windowNumber:context:subtype:data1:data2:"
110+
),
111+
15, # Type
112+
0, # location
113+
0, # flags
114+
0, # timestamp
115+
0, # window
116+
None, # context
117+
0, # subtype
118+
0, # data1
119+
0, # data2
120+
)
121+
objc.objc_msgSend.argtypes = [void_p, void_p, void_p, void_p]
122+
msg(NSApp, n('postEvent:atStart:'), void_p(event), True)
123+
124+
125+
_triggered = _threading_.Event()
126+
127+
def _input_callback(fdref, flags, info):
128+
"""Callback to fire when there's input to be read"""
129+
130+
_triggered.set()
131+
CFFileDescriptorInvalidate(fdref)
132+
CFRelease(fdref)
133+
NSApp = _NSApp()
134+
objc.objc_msgSend.argtypes = [void_p, void_p, void_p]
135+
msg(NSApp, n('stop:'), NSApp)
136+
_wake(NSApp)
137+
138+
_c_callback_func_type = ctypes.CFUNCTYPE(None, void_p, void_p, void_p)
139+
_c_input_callback = _c_callback_func_type(_input_callback)
140+
141+
def _stop_on_read(fd):
142+
"""Register callback to stop eventloop when there's data on fd"""
143+
144+
_triggered.clear()
145+
fdref = CFFileDescriptorCreate(None, fd, False, _c_input_callback, None)
146+
CFFileDescriptorEnableCallBacks(fdref, kCFFileDescriptorReadCallBack)
147+
source = CFFileDescriptorCreateRunLoopSource(None, fdref, 0)
148+
loop = CFRunLoopGetCurrent()
149+
CFRunLoopAddSource(loop, source, kCFRunLoopCommonModes)
150+
CFRelease(source)
151+
152+
153+
class Timer(_threading_.Thread):
154+
def __init__(self, callback=None, interval=0.1):
155+
super().__init__()
156+
self.callback = callback
157+
self.interval = interval
158+
self._stopev = _threading_.Event()
159+
160+
def run(self, *args, **kwargs):
161+
if callable(self.callback):
162+
while not self._stopev.is_set():
163+
time.sleep(self.interval)
164+
self.callback(self._stopev)
165+
166+
167+
class FHSingleton(object):
168+
"""Implements a singleton resource manager for pipes. Avoids opening and
169+
closing pipes during event loops.
170+
"""
171+
_instance = None
172+
def __new__(cls):
173+
if cls._instance is None:
174+
cls._instance = super().__new__(cls)
175+
cls.rh, cls.wh = os.pipe()
176+
else:
177+
# Clears the character written to trigger callback in the last
178+
# loop.
179+
os.read(cls.rh, 1)
180+
181+
return cls._instance
182+
183+
184+
def inputhook_mac():
185+
fh = FHSingleton()
186+
187+
# stop_cb is used to cleanly terminate loop when last figure window is
188+
# closed.
189+
stop_cb = _threading_.Event()
190+
def inputhook_cb(stop):
191+
if stop_cb.is_set() or stdin_ready():
192+
os.write(fh.wh, b'x')
193+
stop.set()
194+
195+
196+
t = Timer(callback=inputhook_cb)
197+
t.start()
198+
NSApp = _NSApp()
199+
objc.objc_msgSend.argtypes = [void_p, void_p]
200+
_stop_on_read(fh.rh)
201+
msg(NSApp, n('run'))
202+
if not _triggered.is_set():
203+
# app closed without firing callback,
204+
# probably due to last window being closed.
205+
# Run the loop manually in this case,
206+
# since there may be events still to process (#9734)
207+
CoreFoundation.CFRunLoopRun()
208+
stop_cb.set()
209+
t.join()

0 commit comments

Comments
 (0)