# ---------------------------------------------------------------------------- # pyglet # Copyright (c) 2006-2008 Alex Holkner # Copyright (c) 2008-2022 pyglet contributors # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions # are met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in # the documentation and/or other materials provided with the # distribution. # * Neither the name of pyglet nor the names of its # contributors may be used to endorse or promote products # derived from this software without specific prior written # permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. # ---------------------------------------------------------------------------- from pyglet.window import key, mouse from pyglet.libs.darwin.quartzkey import keymap, charmap from pyglet.libs.darwin import cocoapy from .pyglet_textview import PygletTextView NSTrackingArea = cocoapy.ObjCClass('NSTrackingArea') # Event data helper functions. def getMouseDelta(nsevent): dx = nsevent.deltaX() dy = -nsevent.deltaY() return dx, dy def getMousePosition(self, nsevent): in_window = nsevent.locationInWindow() in_window = self.convertPoint_fromView_(in_window, None) x = int(in_window.x) y = int(in_window.y) # Must record mouse position for BaseWindow.draw_mouse_cursor to work. self._window._mouse_x = x self._window._mouse_y = y return x, y def getModifiers(nsevent): modifiers = 0 modifierFlags = nsevent.modifierFlags() if modifierFlags & cocoapy.NSAlphaShiftKeyMask: modifiers |= key.MOD_CAPSLOCK if modifierFlags & cocoapy.NSShiftKeyMask: modifiers |= key.MOD_SHIFT if modifierFlags & cocoapy.NSControlKeyMask: modifiers |= key.MOD_CTRL if modifierFlags & cocoapy.NSAlternateKeyMask: modifiers |= key.MOD_ALT modifiers |= key.MOD_OPTION if modifierFlags & cocoapy.NSCommandKeyMask: modifiers |= key.MOD_COMMAND if modifierFlags & cocoapy.NSFunctionKeyMask: modifiers |= key.MOD_FUNCTION return modifiers def getSymbol(nsevent): symbol = keymap.get(nsevent.keyCode(), None) if symbol is not None: return symbol chars = cocoapy.cfstring_to_string(nsevent.charactersIgnoringModifiers()) if chars: return charmap.get(chars[0].upper(), None) return None class PygletView_Implementation: PygletView = cocoapy.ObjCSubclass('NSView', 'PygletView') @PygletView.method(b'@'+cocoapy.NSRectEncoding+cocoapy.PyObjectEncoding) def initWithFrame_cocoaWindow_(self, frame, window): # The tracking area is used to get mouseEntered, mouseExited, and cursorUpdate # events so that we can custom set the mouse cursor within the view. self._tracking_area = None self = cocoapy.ObjCInstance(cocoapy.send_super(self, 'initWithFrame:', frame, argtypes=[cocoapy.NSRect])) if not self: return None # CocoaWindow object. self._window = window self.updateTrackingAreas() # Create an instance of PygletTextView to handle text events. # We must do this because NSOpenGLView doesn't conform to the # NSTextInputClient protocol by default, and the insertText: method will # not do the right thing with respect to translating key sequences like # "Option-e", "e" if the protocol isn't implemented. So the easiest # thing to do is to subclass NSTextView which *does* implement the # protocol and let it handle text input. self._textview = PygletTextView.alloc().initWithCocoaWindow_(window) # Add text view to the responder chain. self.addSubview_(self._textview) return self @PygletView.method('v') def dealloc(self): self._window = None # cocoapy.end_message(self.objc_self, 'removeFromSuperviewWithoutNeedingDisplay') self._textview.release() self._textview = None self._tracking_area.release() self._tracking_area = None cocoapy.send_super(self, 'dealloc') @PygletView.method('v') def updateTrackingAreas(self): # This method is called automatically whenever the tracking areas need to be # recreated, for example when window resizes. if self._tracking_area: self.removeTrackingArea_(self._tracking_area) self._tracking_area.release() self._tracking_area = None tracking_options = cocoapy.NSTrackingMouseEnteredAndExited | cocoapy.NSTrackingActiveInActiveApp | cocoapy.NSTrackingCursorUpdate frame = self.frame() self._tracking_area = NSTrackingArea.alloc().initWithRect_options_owner_userInfo_( frame, # rect tracking_options, # options self, # owner None) # userInfo self.addTrackingArea_(self._tracking_area) @PygletView.method('B') def canBecomeKeyView(self): return True @PygletView.method('B') def isOpaque(self): return True ## Event responders. # This method is called whenever the view changes size. @PygletView.method(b'v'+cocoapy.NSSizeEncoding) def setFrameSize_(self, size): cocoapy.send_super(self, 'setFrameSize:', size, superclass_name='NSView', argtypes=[cocoapy.NSSize]) # This method is called when view is first installed as the # contentView of window. Don't do anything on first call. # This also helps ensure correct window creation event ordering. if not self._window.context.canvas: return width, height = int(size.width), int(size.height) self._window.switch_to() self._window.context.update_geometry() self._window._width, self._window._height = width, height self._window.dispatch_event("on_resize", width, height) self._window.dispatch_event("on_expose") # Can't get app.event_loop.enter_blocking() working with Cocoa, because # when mouse clicks on the window's resize control, Cocoa enters into a # mini-event loop that only responds to mouseDragged and mouseUp events. # This means that using NSTimer to call idle() won't work. Our kludge # is to override NSWindow's nextEventMatchingMask_etc method and call # idle() from there. if self.inLiveResize(): from pyglet import app if app.event_loop is not None: app.event_loop.idle() @PygletView.method('v@') def pygletKeyDown_(self, nsevent): symbol = getSymbol(nsevent) modifiers = getModifiers(nsevent) self._window.dispatch_event('on_key_press', symbol, modifiers) @PygletView.method('v@') def pygletKeyUp_(self, nsevent): symbol = getSymbol(nsevent) modifiers = getModifiers(nsevent) self._window.dispatch_event('on_key_release', symbol, modifiers) @PygletView.method('v@') def pygletFlagsChanged_(self, nsevent): # Handles on_key_press and on_key_release events for modifier keys. # Note that capslock is handled differently than other keys; it acts # as a toggle, so on_key_release is only sent when it's turned off. # TODO: Move these constants somewhere else. # Undocumented left/right modifier masks found by experimentation: NSLeftShiftKeyMask = 1 << 1 NSRightShiftKeyMask = 1 << 2 NSLeftControlKeyMask = 1 << 0 NSRightControlKeyMask = 1 << 13 NSLeftAlternateKeyMask = 1 << 5 NSRightAlternateKeyMask = 1 << 6 NSLeftCommandKeyMask = 1 << 3 NSRightCommandKeyMask = 1 << 4 maskForKey = {key.LSHIFT: NSLeftShiftKeyMask, key.RSHIFT: NSRightShiftKeyMask, key.LCTRL: NSLeftControlKeyMask, key.RCTRL: NSRightControlKeyMask, key.LOPTION: NSLeftAlternateKeyMask, key.ROPTION: NSRightAlternateKeyMask, key.LCOMMAND: NSLeftCommandKeyMask, key.RCOMMAND: NSRightCommandKeyMask, key.CAPSLOCK: cocoapy.NSAlphaShiftKeyMask, key.FUNCTION: cocoapy.NSFunctionKeyMask} symbol = keymap.get(nsevent.keyCode(), None) # Ignore this event if symbol is not a modifier key. We must check this # because e.g., we receive a flagsChanged message when using CMD-tab to # switch applications, with symbol == "a" when command key is released. if symbol is None or symbol not in maskForKey: return modifiers = getModifiers(nsevent) modifierFlags = nsevent.modifierFlags() if symbol and modifierFlags & maskForKey[symbol]: self._window.dispatch_event('on_key_press', symbol, modifiers) else: self._window.dispatch_event('on_key_release', symbol, modifiers) # Overriding this method helps prevent system beeps for unhandled events. @PygletView.method('B@') def performKeyEquivalent_(self, nsevent): # Let arrow keys and certain function keys pass through the responder # chain so that the textview can handle on_text_motion events. modifierFlags = nsevent.modifierFlags() if modifierFlags & cocoapy.NSNumericPadKeyMask: return False if modifierFlags & cocoapy.NSFunctionKeyMask: ch = cocoapy.cfstring_to_string(nsevent.charactersIgnoringModifiers()) if ch in (cocoapy.NSHomeFunctionKey, cocoapy.NSEndFunctionKey, cocoapy.NSPageUpFunctionKey, cocoapy.NSPageDownFunctionKey): return False # Send the key equivalent to the main menu to perform menu items. NSApp = cocoapy.ObjCClass('NSApplication').sharedApplication() NSApp.mainMenu().performKeyEquivalent_(nsevent) # Indicate that we've handled the event so system won't beep. return True @PygletView.method('v@') def mouseMoved_(self, nsevent): if self._window._mouse_ignore_motion: self._window._mouse_ignore_motion = False return # Don't send on_mouse_motion events if we're not inside the content rectangle. if not self._window._mouse_in_window: return x, y = getMousePosition(self, nsevent) dx, dy = getMouseDelta(nsevent) self._window.dispatch_event('on_mouse_motion', x, y, dx, dy) @PygletView.method('v@') def scrollWheel_(self, nsevent): x, y = getMousePosition(self, nsevent) scroll_x, scroll_y = getMouseDelta(nsevent) self._window.dispatch_event('on_mouse_scroll', x, y, scroll_x, scroll_y) @PygletView.method('v@') def mouseDown_(self, nsevent): x, y = getMousePosition(self, nsevent) buttons = mouse.LEFT modifiers = getModifiers(nsevent) self._window.dispatch_event('on_mouse_press', x, y, buttons, modifiers) @PygletView.method('v@') def mouseDragged_(self, nsevent): x, y = getMousePosition(self, nsevent) dx, dy = getMouseDelta(nsevent) buttons = mouse.LEFT modifiers = getModifiers(nsevent) self._window.dispatch_event('on_mouse_drag', x, y, dx, dy, buttons, modifiers) @PygletView.method('v@') def mouseUp_(self, nsevent): x, y = getMousePosition(self, nsevent) buttons = mouse.LEFT modifiers = getModifiers(nsevent) self._window.dispatch_event('on_mouse_release', x, y, buttons, modifiers) @PygletView.method('v@') def rightMouseDown_(self, nsevent): x, y = getMousePosition(self, nsevent) buttons = mouse.RIGHT modifiers = getModifiers(nsevent) self._window.dispatch_event('on_mouse_press', x, y, buttons, modifiers) @PygletView.method('v@') def rightMouseDragged_(self, nsevent): x, y = getMousePosition(self, nsevent) dx, dy = getMouseDelta(nsevent) buttons = mouse.RIGHT modifiers = getModifiers(nsevent) self._window.dispatch_event('on_mouse_drag', x, y, dx, dy, buttons, modifiers) @PygletView.method('v@') def rightMouseUp_(self, nsevent): x, y = getMousePosition(self, nsevent) buttons = mouse.RIGHT modifiers = getModifiers(nsevent) self._window.dispatch_event('on_mouse_release', x, y, buttons, modifiers) @PygletView.method('v@') def otherMouseDown_(self, nsevent): x, y = getMousePosition(self, nsevent) buttons = mouse.MIDDLE modifiers = getModifiers(nsevent) self._window.dispatch_event('on_mouse_press', x, y, buttons, modifiers) @PygletView.method('v@') def otherMouseDragged_(self, nsevent): x, y = getMousePosition(self, nsevent) dx, dy = getMouseDelta(nsevent) buttons = mouse.MIDDLE modifiers = getModifiers(nsevent) self._window.dispatch_event('on_mouse_drag', x, y, dx, dy, buttons, modifiers) @PygletView.method('v@') def otherMouseUp_(self, nsevent): x, y = getMousePosition(self, nsevent) buttons = mouse.MIDDLE modifiers = getModifiers(nsevent) self._window.dispatch_event('on_mouse_release', x, y, buttons, modifiers) @PygletView.method('v@') def mouseEntered_(self, nsevent): x, y = getMousePosition(self, nsevent) self._window._mouse_in_window = True # Don't call self._window.set_mouse_platform_visible() from here. # Better to do it from cursorUpdate: self._window.dispatch_event('on_mouse_enter', x, y) @PygletView.method('v@') def mouseExited_(self, nsevent): x, y = getMousePosition(self, nsevent) self._window._mouse_in_window = False if not self._window._mouse_exclusive: self._window.set_mouse_platform_visible() self._window.dispatch_event('on_mouse_leave', x, y) @PygletView.method('v@') def cursorUpdate_(self, nsevent): # Called when mouse cursor enters view. Unlike mouseEntered:, # this method will be called if the view appears underneath a # motionless mouse cursor, as can happen during window creation, # or when switching into fullscreen mode. # BUG: If the mouse enters the window via the resize control at the # the bottom right corner, the resize control will set the cursor # to the default arrow and screw up our cursor tracking. self._window._mouse_in_window = True if not self._window._mouse_exclusive: self._window.set_mouse_platform_visible() PygletView = cocoapy.ObjCClass('PygletView')