|
"""Eventloop hook for OS X |
|
|
|
Calls NSApp / CoreFoundation APIs via ctypes. |
|
""" |
|
|
|
|
|
|
|
|
|
import ctypes |
|
import ctypes.util |
|
from threading import Event |
|
|
|
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 |
|
|
|
msg = objc.objc_msgSend |
|
|
|
|
|
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)) |
|
|
|
|
|
|
|
|
|
|
|
CoreFoundation = ctypes.cdll.LoadLibrary( |
|
ctypes.util.find_library("CoreFoundation") |
|
) |
|
|
|
CFAbsoluteTimeGetCurrent = CoreFoundation.CFAbsoluteTimeGetCurrent |
|
CFAbsoluteTimeGetCurrent.restype = ctypes.c_double |
|
|
|
CFRunLoopGetCurrent = CoreFoundation.CFRunLoopGetCurrent |
|
CFRunLoopGetCurrent.restype = void_p |
|
|
|
CFRunLoopGetMain = CoreFoundation.CFRunLoopGetMain |
|
CFRunLoopGetMain.restype = void_p |
|
|
|
CFRunLoopStop = CoreFoundation.CFRunLoopStop |
|
CFRunLoopStop.restype = None |
|
CFRunLoopStop.argtypes = [void_p] |
|
|
|
CFRunLoopTimerCreate = CoreFoundation.CFRunLoopTimerCreate |
|
CFRunLoopTimerCreate.restype = void_p |
|
CFRunLoopTimerCreate.argtypes = [ |
|
void_p, |
|
ctypes.c_double, |
|
ctypes.c_double, |
|
ctypes.c_int, |
|
ctypes.c_int, |
|
void_p, |
|
void_p, |
|
] |
|
|
|
CFRunLoopAddTimer = CoreFoundation.CFRunLoopAddTimer |
|
CFRunLoopAddTimer.restype = None |
|
CFRunLoopAddTimer.argtypes = [void_p, void_p, void_p] |
|
|
|
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, |
|
0, |
|
0, |
|
0, |
|
0, |
|
None, |
|
0, |
|
0, |
|
0, |
|
) |
|
objc.objc_msgSend.argtypes = [void_p, void_p, void_p, void_p] |
|
msg(NSApp, n("postEvent:atStart:"), void_p(event), True) |
|
|
|
|
|
_triggered = Event() |
|
|
|
|
|
def stop(timer=None, loop=None): |
|
"""Callback to fire when there's input to be read""" |
|
_triggered.set() |
|
NSApp = _NSApp() |
|
|
|
|
|
objc.objc_msgSend.argtypes = [void_p, void_p] |
|
if msg(NSApp, n("isRunning")): |
|
objc.objc_msgSend.argtypes = [void_p, void_p, void_p] |
|
msg(NSApp, n("stop:"), NSApp) |
|
_wake(NSApp) |
|
else: |
|
CFRunLoopStop(CFRunLoopGetCurrent()) |
|
|
|
|
|
_c_callback_func_type = ctypes.CFUNCTYPE(None, void_p, void_p) |
|
_c_stop_callback = _c_callback_func_type(stop) |
|
|
|
|
|
def _stop_after(delay): |
|
"""Register callback to stop eventloop after a delay""" |
|
timer = CFRunLoopTimerCreate( |
|
None, |
|
CFAbsoluteTimeGetCurrent() + delay, |
|
0, |
|
0, |
|
0, |
|
_c_stop_callback, |
|
None, |
|
) |
|
CFRunLoopAddTimer( |
|
CFRunLoopGetMain(), |
|
timer, |
|
kCFRunLoopCommonModes, |
|
) |
|
|
|
|
|
def mainloop(duration=1): |
|
"""run the Cocoa eventloop for the specified duration (seconds)""" |
|
|
|
_triggered.clear() |
|
NSApp = _NSApp() |
|
_stop_after(duration) |
|
objc.objc_msgSend.argtypes = [void_p, void_p] |
|
msg(NSApp, n("run")) |
|
if not _triggered.is_set(): |
|
|
|
|
|
|
|
|
|
CoreFoundation.CFRunLoopRun() |
|
|