Difficult-Rocket/pyglet/window/cocoa/pyglet_view.py
2021-03-24 23:37:10 +08:00

385 lines
15 KiB
Python

# ----------------------------------------------------------------------------
# pyglet
# Copyright (c) 2006-2008 Alex Holkner
# Copyright (c) 2008-2020 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
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.
PygletTextView = cocoapy.ObjCClass('PygletTextView')
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.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._is_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._is_mouse_exclusive:
self._window.set_mouse_platform_visible()
PygletView = cocoapy.ObjCClass('PygletView')