_eventloop_macos.py 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. """Eventloop hook for OS X
  2. Calls NSApp / CoreFoundation APIs via ctypes.
  3. """
  4. # cribbed heavily from IPython.terminal.pt_inputhooks.osx
  5. # obj-c boilerplate from appnope, used under BSD 2-clause
  6. import ctypes
  7. import ctypes.util
  8. from threading import Event
  9. objc = ctypes.cdll.LoadLibrary(ctypes.util.find_library("objc")) # type:ignore[arg-type]
  10. void_p = ctypes.c_void_p
  11. objc.objc_getClass.restype = void_p
  12. objc.sel_registerName.restype = void_p
  13. objc.objc_msgSend.restype = void_p
  14. msg = objc.objc_msgSend
  15. def _utf8(s):
  16. """ensure utf8 bytes"""
  17. if not isinstance(s, bytes):
  18. s = s.encode("utf8")
  19. return s
  20. def n(name):
  21. """create a selector name (for ObjC methods)"""
  22. return objc.sel_registerName(_utf8(name))
  23. def C(classname):
  24. """get an ObjC Class by name"""
  25. return objc.objc_getClass(_utf8(classname))
  26. # end obj-c boilerplate from appnope
  27. # CoreFoundation C-API calls we will use:
  28. CoreFoundation = ctypes.cdll.LoadLibrary(
  29. ctypes.util.find_library("CoreFoundation") # type:ignore[arg-type]
  30. )
  31. CFAbsoluteTimeGetCurrent = CoreFoundation.CFAbsoluteTimeGetCurrent
  32. CFAbsoluteTimeGetCurrent.restype = ctypes.c_double
  33. CFRunLoopGetCurrent = CoreFoundation.CFRunLoopGetCurrent
  34. CFRunLoopGetCurrent.restype = void_p
  35. CFRunLoopGetMain = CoreFoundation.CFRunLoopGetMain
  36. CFRunLoopGetMain.restype = void_p
  37. CFRunLoopStop = CoreFoundation.CFRunLoopStop
  38. CFRunLoopStop.restype = None
  39. CFRunLoopStop.argtypes = [void_p]
  40. CFRunLoopTimerCreate = CoreFoundation.CFRunLoopTimerCreate
  41. CFRunLoopTimerCreate.restype = void_p
  42. CFRunLoopTimerCreate.argtypes = [
  43. void_p, # allocator (NULL)
  44. ctypes.c_double, # fireDate
  45. ctypes.c_double, # interval
  46. ctypes.c_int, # flags (0)
  47. ctypes.c_int, # order (0)
  48. void_p, # callout
  49. void_p, # context
  50. ]
  51. CFRunLoopAddTimer = CoreFoundation.CFRunLoopAddTimer
  52. CFRunLoopAddTimer.restype = None
  53. CFRunLoopAddTimer.argtypes = [void_p, void_p, void_p]
  54. kCFRunLoopCommonModes = void_p.in_dll(CoreFoundation, "kCFRunLoopCommonModes")
  55. def _NSApp():
  56. """Return the global NSApplication instance (NSApp)"""
  57. objc.objc_msgSend.argtypes = [void_p, void_p]
  58. return msg(C("NSApplication"), n("sharedApplication"))
  59. def _wake(NSApp):
  60. """Wake the Application"""
  61. objc.objc_msgSend.argtypes = [
  62. void_p,
  63. void_p,
  64. void_p,
  65. void_p,
  66. void_p,
  67. void_p,
  68. void_p,
  69. void_p,
  70. void_p,
  71. void_p,
  72. void_p,
  73. ]
  74. event = msg(
  75. C("NSEvent"),
  76. n(
  77. "otherEventWithType:location:modifierFlags:"
  78. "timestamp:windowNumber:context:subtype:data1:data2:"
  79. ),
  80. 15, # Type
  81. 0, # location
  82. 0, # flags
  83. 0, # timestamp
  84. 0, # window
  85. None, # context
  86. 0, # subtype
  87. 0, # data1
  88. 0, # data2
  89. )
  90. objc.objc_msgSend.argtypes = [void_p, void_p, void_p, void_p]
  91. msg(NSApp, n("postEvent:atStart:"), void_p(event), True)
  92. _triggered = Event()
  93. def stop(timer=None, loop=None):
  94. """Callback to fire when there's input to be read"""
  95. _triggered.set()
  96. NSApp = _NSApp()
  97. # if NSApp is not running, stop CFRunLoop directly,
  98. # otherwise stop and wake NSApp
  99. objc.objc_msgSend.argtypes = [void_p, void_p]
  100. if msg(NSApp, n("isRunning")):
  101. objc.objc_msgSend.argtypes = [void_p, void_p, void_p]
  102. msg(NSApp, n("stop:"), NSApp)
  103. _wake(NSApp)
  104. else:
  105. CFRunLoopStop(CFRunLoopGetCurrent())
  106. _c_callback_func_type = ctypes.CFUNCTYPE(None, void_p, void_p)
  107. _c_stop_callback = _c_callback_func_type(stop)
  108. def _stop_after(delay):
  109. """Register callback to stop eventloop after a delay"""
  110. timer = CFRunLoopTimerCreate(
  111. None, # allocator
  112. CFAbsoluteTimeGetCurrent() + delay, # fireDate
  113. 0, # interval
  114. 0, # flags
  115. 0, # order
  116. _c_stop_callback,
  117. None,
  118. )
  119. CFRunLoopAddTimer(
  120. CFRunLoopGetMain(),
  121. timer,
  122. kCFRunLoopCommonModes,
  123. )
  124. def mainloop(duration=1):
  125. """run the Cocoa eventloop for the specified duration (seconds)"""
  126. _triggered.clear()
  127. NSApp = _NSApp()
  128. _stop_after(duration)
  129. objc.objc_msgSend.argtypes = [void_p, void_p]
  130. msg(NSApp, n("run"))
  131. if not _triggered.is_set():
  132. # app closed without firing callback,
  133. # probably due to last window being closed.
  134. # Run the loop manually in this case,
  135. # since there may be events still to process (ipython/ipython#9734)
  136. CoreFoundation.CFRunLoopRun()