mirror of
https://github.com/EDCD/EDMarketConnector.git
synced 2025-04-13 07:47:14 +03:00
Tuples are (slightly) more efficient for comparing if x in y. Not that it'll really matter at this scale, but it's technically better and simple to implement. Applying to all files except theme.py, because theme.py is scary.
277 lines
10 KiB
Python
277 lines
10 KiB
Python
"""darwin/macOS implementation of hotkey.AbstractHotkeyMgr."""
|
|
from __future__ import annotations
|
|
|
|
import pathlib
|
|
import sys
|
|
import tkinter as tk
|
|
from typing import Callable
|
|
assert sys.platform == 'darwin'
|
|
|
|
import objc
|
|
from AppKit import (
|
|
NSAlternateKeyMask, NSApplication, NSBeep, NSClearLineFunctionKey, NSCommandKeyMask, NSControlKeyMask,
|
|
NSDeleteFunctionKey, NSDeviceIndependentModifierFlagsMask, NSEvent, NSF1FunctionKey, NSF35FunctionKey,
|
|
NSFlagsChanged, NSKeyDown, NSKeyDownMask, NSKeyUp, NSNumericPadKeyMask, NSShiftKeyMask, NSSound, NSWorkspace
|
|
)
|
|
|
|
from config import config
|
|
from EDMCLogging import get_main_logger
|
|
from hotkey import AbstractHotkeyMgr
|
|
|
|
logger = get_main_logger()
|
|
|
|
|
|
class MacHotkeyMgr(AbstractHotkeyMgr):
|
|
"""Hot key management."""
|
|
|
|
POLL = 250
|
|
# https://developer.apple.com/library/mac/documentation/Cocoa/Reference/ApplicationKit/Classes/NSEvent_Class/#//apple_ref/doc/constant_group/Function_Key_Unicodes
|
|
DISPLAY = {
|
|
0x03: u'⌅', 0x09: u'⇥', 0xd: u'↩', 0x19: u'⇤', 0x1b: u'esc', 0x20: u'⏘', 0x7f: u'⌫',
|
|
0xf700: u'↑', 0xf701: u'↓', 0xf702: u'←', 0xf703: u'→',
|
|
0xf727: u'Ins',
|
|
0xf728: u'⌦', 0xf729: u'↖', 0xf72a: u'Fn', 0xf72b: u'↘',
|
|
0xf72c: u'⇞', 0xf72d: u'⇟', 0xf72e: u'PrtScr', 0xf72f: u'ScrollLock',
|
|
0xf730: u'Pause', 0xf731: u'SysReq', 0xf732: u'Break', 0xf733: u'Reset',
|
|
0xf739: u'⌧',
|
|
}
|
|
(ACQUIRE_INACTIVE, ACQUIRE_ACTIVE, ACQUIRE_NEW) = range(3)
|
|
|
|
def __init__(self):
|
|
self.MODIFIERMASK = NSShiftKeyMask | NSControlKeyMask | NSAlternateKeyMask | NSCommandKeyMask \
|
|
| NSNumericPadKeyMask
|
|
self.root: tk.Tk
|
|
|
|
self.keycode = 0
|
|
self.modifiers = 0
|
|
self.activated = False
|
|
self.observer = None
|
|
|
|
self.acquire_key = 0
|
|
self.acquire_state = MacHotkeyMgr.ACQUIRE_INACTIVE
|
|
|
|
self.tkProcessKeyEvent_old: Callable
|
|
|
|
self.snd_good = NSSound.alloc().initWithContentsOfFile_byReference_(
|
|
pathlib.Path(config.respath_path) / 'snd_good.wav', False
|
|
)
|
|
self.snd_bad = NSSound.alloc().initWithContentsOfFile_byReference_(
|
|
pathlib.Path(config.respath_path) / 'snd_bad.wav', False
|
|
)
|
|
|
|
def register(self, root: tk.Tk, keycode: int, modifiers: int) -> None:
|
|
"""
|
|
Register current hotkey for monitoring.
|
|
|
|
:param root: parent window.
|
|
:param keycode: Key to monitor.
|
|
:param modifiers: Any modifiers to take into account.
|
|
"""
|
|
self.root = root
|
|
self.keycode = keycode
|
|
self.modifiers = modifiers
|
|
self.activated = False
|
|
|
|
if keycode:
|
|
if not self.observer:
|
|
self.root.after_idle(self._observe)
|
|
self.root.after(MacHotkeyMgr.POLL, self._poll)
|
|
|
|
# Monkey-patch tk (tkMacOSXKeyEvent.c)
|
|
if not callable(self.tkProcessKeyEvent_old):
|
|
sel = b'tkProcessKeyEvent:'
|
|
cls = NSApplication.sharedApplication().class__() # type: ignore
|
|
self.tkProcessKeyEvent_old = NSApplication.sharedApplication().methodForSelector_(sel) # type: ignore
|
|
newmethod = objc.selector( # type: ignore
|
|
self.tkProcessKeyEvent,
|
|
selector=self.tkProcessKeyEvent_old.selector,
|
|
signature=self.tkProcessKeyEvent_old.signature
|
|
)
|
|
objc.classAddMethod(cls, sel, newmethod) # type: ignore
|
|
|
|
def tkProcessKeyEvent(self, cls, the_event): # noqa: N802
|
|
"""
|
|
Monkey-patch tk (tkMacOSXKeyEvent.c).
|
|
|
|
- workaround crash on OSX 10.9 & 10.10 on seeing a composing character
|
|
- notice when modifier key state changes
|
|
- keep a copy of NSEvent.charactersIgnoringModifiers, which is what we need for the hotkey
|
|
|
|
(Would like to use a decorator but need to ensure the application is created before this is installed)
|
|
:param cls: ???
|
|
:param the_event: tk event
|
|
:return: ???
|
|
"""
|
|
if self.acquire_state:
|
|
if the_event.type() == NSFlagsChanged:
|
|
self.acquire_key = the_event.modifierFlags() & NSDeviceIndependentModifierFlagsMask
|
|
self.acquire_state = MacHotkeyMgr.ACQUIRE_NEW
|
|
# suppress the event by not chaining the old function
|
|
return the_event
|
|
|
|
if the_event.type() in (NSKeyDown, NSKeyUp):
|
|
c = the_event.charactersIgnoringModifiers()
|
|
self.acquire_key = (c and ord(c[0]) or 0) | \
|
|
(the_event.modifierFlags() & NSDeviceIndependentModifierFlagsMask)
|
|
self.acquire_state = MacHotkeyMgr.ACQUIRE_NEW
|
|
# suppress the event by not chaining the old function
|
|
return the_event
|
|
|
|
# replace empty characters with charactersIgnoringModifiers to avoid crash
|
|
elif the_event.type() in (NSKeyDown, NSKeyUp) and not the_event.characters():
|
|
the_event = NSEvent.keyEventWithType_location_modifierFlags_timestamp_windowNumber_context_characters_charactersIgnoringModifiers_isARepeat_keyCode_( # noqa: E501
|
|
# noqa: E501
|
|
the_event.type(),
|
|
the_event.locationInWindow(),
|
|
the_event.modifierFlags(),
|
|
the_event.timestamp(),
|
|
the_event.windowNumber(),
|
|
the_event.context(),
|
|
the_event.charactersIgnoringModifiers(),
|
|
the_event.charactersIgnoringModifiers(),
|
|
the_event.isARepeat(),
|
|
the_event.keyCode()
|
|
)
|
|
return self.tkProcessKeyEvent_old(cls, the_event)
|
|
|
|
def _observe(self):
|
|
# Must be called after root.mainloop() so that the app's message loop has been created
|
|
self.observer = NSEvent.addGlobalMonitorForEventsMatchingMask_handler_(NSKeyDownMask, self._handler)
|
|
|
|
def _poll(self):
|
|
if config.shutting_down:
|
|
return
|
|
|
|
# No way of signalling to Tkinter from within the callback handler block that doesn't
|
|
# cause Python to crash, so poll.
|
|
if self.activated:
|
|
self.activated = False
|
|
self.root.event_generate('<<Invoke>>', when="tail")
|
|
|
|
if self.keycode or self.modifiers:
|
|
self.root.after(MacHotkeyMgr.POLL, self._poll)
|
|
|
|
def unregister(self) -> None:
|
|
"""Remove hotkey registration."""
|
|
self.keycode = 0
|
|
self.modifiers = 0
|
|
|
|
@objc.callbackFor(NSEvent.addGlobalMonitorForEventsMatchingMask_handler_)
|
|
def _handler(self, event) -> None:
|
|
# use event.charactersIgnoringModifiers to handle composing characters like Alt-e
|
|
if (
|
|
(event.modifierFlags() & self.MODIFIERMASK) == self.modifiers
|
|
and ord(event.charactersIgnoringModifiers()[0]) == self.keycode
|
|
):
|
|
if config.get_int('hotkey_always'):
|
|
self.activated = True
|
|
|
|
else: # Only trigger if game client is front process
|
|
front = NSWorkspace.sharedWorkspace().frontmostApplication()
|
|
if front and front.bundleIdentifier() == 'uk.co.frontier.EliteDangerous':
|
|
self.activated = True
|
|
|
|
def acquire_start(self) -> None:
|
|
"""Start acquiring hotkey state via polling."""
|
|
self.acquire_state = MacHotkeyMgr.ACQUIRE_ACTIVE
|
|
self.root.after_idle(self._acquire_poll)
|
|
|
|
def acquire_stop(self) -> None:
|
|
"""Stop acquiring hotkey state."""
|
|
self.acquire_state = MacHotkeyMgr.ACQUIRE_INACTIVE
|
|
|
|
def _acquire_poll(self) -> None:
|
|
"""Perform a poll of current hotkey state."""
|
|
if config.shutting_down:
|
|
return
|
|
|
|
# No way of signalling to Tkinter from within the monkey-patched event handler that doesn't
|
|
# cause Python to crash, so poll.
|
|
if self.acquire_state:
|
|
if self.acquire_state == MacHotkeyMgr.ACQUIRE_NEW:
|
|
# Abuse tkEvent's keycode field to hold our acquired key & modifier
|
|
self.root.event_generate('<KeyPress>', keycode=self.acquire_key)
|
|
self.acquire_state = MacHotkeyMgr.ACQUIRE_ACTIVE
|
|
self.root.after(50, self._acquire_poll)
|
|
|
|
def fromevent(self, event) -> bool | tuple | None:
|
|
"""
|
|
Return configuration (keycode, modifiers) or None=clear or False=retain previous.
|
|
|
|
:param event: tk event ?
|
|
:return: False to retain previous, None to not use, else (keycode, modifiers)
|
|
"""
|
|
(keycode, modifiers) = (event.keycode & 0xffff, event.keycode & 0xffff0000) # Set by _acquire_poll()
|
|
if (
|
|
keycode
|
|
and not (modifiers & (NSShiftKeyMask | NSControlKeyMask | NSAlternateKeyMask | NSCommandKeyMask))
|
|
):
|
|
if keycode == 0x1b: # Esc = retain previous
|
|
self.acquire_state = MacHotkeyMgr.ACQUIRE_INACTIVE
|
|
return False
|
|
|
|
# BkSp, Del, Clear = clear hotkey
|
|
if keycode in (0x7f, ord(NSDeleteFunctionKey), ord(NSClearLineFunctionKey)):
|
|
self.acquire_state = MacHotkeyMgr.ACQUIRE_INACTIVE
|
|
return None
|
|
|
|
# don't allow keys needed for typing in System Map
|
|
if keycode in (0x13, 0x20, 0x2d) or 0x61 <= keycode <= 0x7a:
|
|
NSBeep()
|
|
self.acquire_state = MacHotkeyMgr.ACQUIRE_INACTIVE
|
|
return None
|
|
|
|
return keycode, modifiers
|
|
|
|
def display(self, keycode, modifiers) -> str:
|
|
"""
|
|
Return displayable form of given hotkey + modifiers.
|
|
|
|
:param keycode:
|
|
:param modifiers:
|
|
:return: string form
|
|
"""
|
|
text = ''
|
|
if modifiers & NSControlKeyMask:
|
|
text += u'⌃'
|
|
|
|
if modifiers & NSAlternateKeyMask:
|
|
text += u'⌥'
|
|
|
|
if modifiers & NSShiftKeyMask:
|
|
text += u'⇧'
|
|
|
|
if modifiers & NSCommandKeyMask:
|
|
text += u'⌘'
|
|
|
|
if (modifiers & NSNumericPadKeyMask) and keycode <= 0x7f:
|
|
text += u'№'
|
|
|
|
if not keycode:
|
|
pass
|
|
|
|
elif ord(NSF1FunctionKey) <= keycode <= ord(NSF35FunctionKey):
|
|
text += f'F{keycode + 1 - ord(NSF1FunctionKey)}'
|
|
|
|
elif keycode in MacHotkeyMgr.DISPLAY: # specials
|
|
text += MacHotkeyMgr.DISPLAY[keycode]
|
|
|
|
elif keycode < 0x20: # control keys
|
|
text += chr(keycode + 0x40)
|
|
|
|
elif keycode < 0xf700: # key char
|
|
text += chr(keycode).upper()
|
|
|
|
else:
|
|
text += u'⁈'
|
|
|
|
return text
|
|
|
|
def play_good(self):
|
|
"""Play the 'good' sound."""
|
|
self.snd_good.play()
|
|
|
|
def play_bad(self):
|
|
"""Play the 'bad' sound."""
|
|
self.snd_bad.play()
|