247 lines
8.8 KiB
Python
247 lines
8.8 KiB
Python
import signal
|
|
from pyglet import app
|
|
from pyglet.app.base import PlatformEventLoop, EventLoop
|
|
from pyglet.libs.darwin import cocoapy, AutoReleasePool, ObjCSubclass, PyObjectEncoding, ObjCInstance, send_super, \
|
|
ObjCClass, get_selector
|
|
|
|
NSApplication = cocoapy.ObjCClass('NSApplication')
|
|
NSMenu = cocoapy.ObjCClass('NSMenu')
|
|
NSMenuItem = cocoapy.ObjCClass('NSMenuItem')
|
|
NSDate = cocoapy.ObjCClass('NSDate')
|
|
NSEvent = cocoapy.ObjCClass('NSEvent')
|
|
NSUserDefaults = cocoapy.ObjCClass('NSUserDefaults')
|
|
NSTimer = cocoapy.ObjCClass('NSTimer')
|
|
|
|
|
|
|
|
def add_menu_item(menu, title, action, key):
|
|
with AutoReleasePool():
|
|
title = cocoapy.CFSTR(title)
|
|
action = cocoapy.get_selector(action)
|
|
key = cocoapy.CFSTR(key)
|
|
menuItem = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_(
|
|
title, action, key)
|
|
menu.addItem_(menuItem)
|
|
|
|
# cleanup
|
|
menuItem.release()
|
|
|
|
|
|
def create_menu():
|
|
with AutoReleasePool():
|
|
appMenu = NSMenu.alloc().init()
|
|
|
|
# Hide still doesn't work!?
|
|
add_menu_item(appMenu, 'Hide!', 'hide:', 'h')
|
|
appMenu.addItem_(NSMenuItem.separatorItem())
|
|
add_menu_item(appMenu, 'Quit!', 'terminate:', 'q')
|
|
|
|
menubar = NSMenu.alloc().init()
|
|
appMenuItem = NSMenuItem.alloc().init()
|
|
appMenuItem.setSubmenu_(appMenu)
|
|
menubar.addItem_(appMenuItem)
|
|
NSApp = NSApplication.sharedApplication()
|
|
NSApp.setMainMenu_(menubar)
|
|
|
|
# cleanup
|
|
appMenu.release()
|
|
menubar.release()
|
|
appMenuItem.release()
|
|
|
|
|
|
class _AppDelegate_Implementation:
|
|
_AppDelegate = ObjCSubclass('NSObject', '_AppDelegate')
|
|
|
|
@_AppDelegate.method(b'@' + PyObjectEncoding)
|
|
def init(self, pyglet_loop):
|
|
objc = ObjCInstance(send_super(self, 'init'))
|
|
self._pyglet_loop = pyglet_loop
|
|
return objc # objc is self
|
|
|
|
@_AppDelegate.method('v')
|
|
def updatePyglet_(self):
|
|
self._pyglet_loop.nsapp_step()
|
|
|
|
@_AppDelegate.method('v@')
|
|
def applicationWillTerminate_(self, notification):
|
|
self._pyglet_loop.is_running = False
|
|
self._pyglet_loop.has_exit = True
|
|
|
|
@_AppDelegate.method('v@')
|
|
def applicationDidFinishLaunching_(self, notification):
|
|
self._pyglet_loop._finished_launching = True
|
|
|
|
_AppDelegate = ObjCClass('_AppDelegate') # the actual class
|
|
|
|
class CocoaAlternateEventLoop(EventLoop):
|
|
"""This is an alternate loop developed mainly for ARM64 variants of macOS.
|
|
nextEventMatchingMask_untilDate_inMode_dequeue_ is very broken with ctypes calls. Events eventually stop
|
|
working properly after X returns. This event loop differs in that it uses the built-in NSApplication event
|
|
loop. We tie our schedule into it via timer.
|
|
"""
|
|
def run(self, interval=1/60):
|
|
if not interval:
|
|
self.clock.schedule(self._redraw_windows)
|
|
else:
|
|
self.clock.schedule_interval(self._redraw_windows, interval)
|
|
|
|
self.has_exit = False
|
|
|
|
from pyglet.window import Window
|
|
Window._enable_event_queue = False
|
|
|
|
# Dispatch pending events
|
|
for window in app.windows:
|
|
window.switch_to()
|
|
window.dispatch_pending_events()
|
|
|
|
self.platform_event_loop = app.platform_event_loop
|
|
|
|
self.dispatch_event('on_enter')
|
|
self.is_running = True
|
|
self.platform_event_loop.nsapp_start(interval)
|
|
|
|
def exit(self):
|
|
"""Safely exit the event loop at the end of the current iteration.
|
|
|
|
This method is a thread-safe equivalent for setting
|
|
:py:attr:`has_exit` to ``True``. All waiting threads will be
|
|
interrupted (see :py:meth:`sleep`).
|
|
"""
|
|
self.has_exit = True
|
|
self.platform_event_loop.notify()
|
|
|
|
self.is_running = False
|
|
self.dispatch_event('on_exit')
|
|
|
|
self.platform_event_loop.nsapp_stop()
|
|
|
|
class CocoaPlatformEventLoop(PlatformEventLoop):
|
|
|
|
def __init__(self):
|
|
super(CocoaPlatformEventLoop, self).__init__()
|
|
with AutoReleasePool():
|
|
# Prepare the default application.
|
|
self.NSApp = NSApplication.sharedApplication()
|
|
if self.NSApp.isRunning():
|
|
# Application was already started by GUI library (e.g. wxPython).
|
|
return
|
|
if not self.NSApp.mainMenu():
|
|
create_menu()
|
|
self.NSApp.setActivationPolicy_(cocoapy.NSApplicationActivationPolicyRegular)
|
|
# Prevent Lion / Mountain Lion from automatically saving application state.
|
|
# If we don't do this, new windows will not display on 10.8 after finishLaunching
|
|
# has been called.
|
|
defaults = NSUserDefaults.standardUserDefaults()
|
|
ignoreState = cocoapy.CFSTR("ApplePersistenceIgnoreState")
|
|
if not defaults.objectForKey_(ignoreState):
|
|
defaults.setBool_forKey_(True, ignoreState)
|
|
|
|
holdEnabled = cocoapy.CFSTR("ApplePressAndHoldEnabled")
|
|
if not defaults.objectForKey_(holdEnabled):
|
|
defaults.setBool_forKey_(False, holdEnabled)
|
|
|
|
self._finished_launching = False
|
|
|
|
def start(self):
|
|
with AutoReleasePool():
|
|
if not self.NSApp.isRunning() and not self._finished_launching:
|
|
# finishLaunching should be called only once. However isRunning will not
|
|
# guard this, as we are not using the normal event loop.
|
|
self.NSApp.finishLaunching()
|
|
self.NSApp.activateIgnoringOtherApps_(True)
|
|
self._finished_launching = True
|
|
|
|
def nsapp_start(self, interval):
|
|
"""Used only for CocoaAlternateEventLoop"""
|
|
from pyglet.app import event_loop
|
|
self._event_loop = event_loop
|
|
|
|
def term_received(*args):
|
|
if self.timer:
|
|
self.timer.invalidate()
|
|
self.timer = None
|
|
|
|
self.nsapp_stop()
|
|
|
|
# Force NSApp to close if Python receives sig events.
|
|
signal.signal(signal.SIGINT, term_received)
|
|
signal.signal(signal.SIGTERM, term_received)
|
|
|
|
self.appdelegate = _AppDelegate.alloc().init(self)
|
|
self.NSApp.setDelegate_(self.appdelegate)
|
|
|
|
self.timer = NSTimer.scheduledTimerWithTimeInterval_target_selector_userInfo_repeats_(
|
|
interval, # Clamped internally to 0.0001 (including 0)
|
|
self.appdelegate,
|
|
get_selector('updatePyglet:'),
|
|
False,
|
|
True
|
|
)
|
|
|
|
self.NSApp.run()
|
|
|
|
def nsapp_step(self):
|
|
"""Used only for CocoaAlternateEventLoop"""
|
|
self._event_loop.idle()
|
|
self.dispatch_posted_events()
|
|
|
|
def nsapp_stop(self):
|
|
"""Used only for CocoaAlternateEventLoop"""
|
|
self.NSApp.terminate_(None)
|
|
|
|
def step(self, timeout=None):
|
|
with AutoReleasePool():
|
|
self.dispatch_posted_events()
|
|
|
|
# Determine the timeout date.
|
|
if timeout is None:
|
|
# Using distantFuture as untilDate means that nextEventMatchingMask
|
|
# will wait until the next event comes along.
|
|
timeout_date = NSDate.distantFuture()
|
|
elif timeout == 0.0:
|
|
timeout_date = NSDate.distantPast()
|
|
else:
|
|
timeout_date = NSDate.dateWithTimeIntervalSinceNow_(timeout)
|
|
|
|
# Retrieve the next event (if any). We wait for an event to show up
|
|
# and then process it, or if timeout_date expires we simply return.
|
|
# We only process one event per call of step().
|
|
self._is_running.set()
|
|
event = self.NSApp.nextEventMatchingMask_untilDate_inMode_dequeue_(
|
|
cocoapy.NSAnyEventMask, timeout_date, cocoapy.NSDefaultRunLoopMode, True)
|
|
|
|
# Dispatch the event (if any).
|
|
if event is not None:
|
|
event_type = event.type()
|
|
if event_type != cocoapy.NSApplicationDefined:
|
|
self.NSApp.sendEvent_(event)
|
|
|
|
self.NSApp.updateWindows()
|
|
did_time_out = False
|
|
else:
|
|
did_time_out = True
|
|
|
|
self._is_running.clear()
|
|
|
|
return did_time_out
|
|
|
|
def stop(self):
|
|
pass
|
|
|
|
def notify(self):
|
|
with AutoReleasePool():
|
|
notifyEvent = NSEvent.otherEventWithType_location_modifierFlags_timestamp_windowNumber_context_subtype_data1_data2_(
|
|
cocoapy.NSApplicationDefined, # type
|
|
cocoapy.NSPoint(0.0, 0.0), # location
|
|
0, # modifierFlags
|
|
0, # timestamp
|
|
0, # windowNumber
|
|
None, # graphicsContext
|
|
0, # subtype
|
|
0, # data1
|
|
0, # data2
|
|
)
|
|
|
|
self.NSApp.postEvent_atStart_(notifyEvent, False)
|