1
0
mirror of https://github.com/EDCD/EDMarketConnector.git synced 2025-06-18 16:03:12 +03:00

hotkey: Basic conversion to Abstract class, and implementation per platform

This commit is contained in:
Athanasius 2021-04-07 17:16:54 +01:00
parent 8aa0337ff4
commit e0a3f1ad53

109
hotkey.py
View File

@ -1,9 +1,11 @@
"""Handle keyboard input for manual update triggering.""" """Handle keyboard input for manual update triggering."""
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import abc
import pathlib import pathlib
import sys
import tkinter as tk import tkinter as tk
from sys import platform from abc import abstractmethod
from typing import Optional, Tuple, Union from typing import Optional, Tuple, Union
from config import config from config import config
@ -11,7 +13,32 @@ from EDMCLogging import get_main_logger
logger = get_main_logger() logger = get_main_logger()
if platform == 'darwin': # noqa: C901
class AbstractHotkeyMgr(abc.abstractmethod):
"""Abstract root class of all platforms specific HotKeyMgr."""
@abstractmethod
def register(self, root, keycode, modifiers) -> None:
"""Register the hotkey handler."""
pass
@abstractmethod
def unregister(self) -> None:
"""Unregister the hotkey handling."""
pass
@abstractmethod
def play_good(self) -> None:
"""Play the 'good' sound."""
pass
@abstractmethod
def play_bad(self) -> None:
"""Play the 'bad' sound."""
pass
if sys.platform == 'darwin':
import objc import objc
from AppKit import ( from AppKit import (
@ -20,7 +47,8 @@ if platform == 'darwin': # noqa: C901
NSFlagsChanged, NSKeyDown, NSKeyDownMask, NSKeyUp, NSNumericPadKeyMask, NSShiftKeyMask, NSSound, NSWorkspace NSFlagsChanged, NSKeyDown, NSKeyDownMask, NSKeyUp, NSNumericPadKeyMask, NSShiftKeyMask, NSSound, NSWorkspace
) )
class HotkeyMgr:
class MacHotkeyMgr(AbstractHotkeyMgr):
"""Hot key management.""" """Hot key management."""
MODIFIERMASK = NSShiftKeyMask | NSControlKeyMask | NSAlternateKeyMask | NSCommandKeyMask | NSNumericPadKeyMask MODIFIERMASK = NSShiftKeyMask | NSControlKeyMask | NSAlternateKeyMask | NSCommandKeyMask | NSNumericPadKeyMask
@ -46,7 +74,7 @@ if platform == 'darwin': # noqa: C901
self.observer = None self.observer = None
self.acquire_key = 0 self.acquire_key = 0
self.acquire_state = HotkeyMgr.ACQUIRE_INACTIVE self.acquire_state = MacHotkeyMgr.ACQUIRE_INACTIVE
self.tkProcessKeyEvent_old = None self.tkProcessKeyEvent_old = None
@ -73,7 +101,7 @@ if platform == 'darwin': # noqa: C901
if keycode: if keycode:
if not self.observer: if not self.observer:
self.root.after_idle(self._observe) self.root.after_idle(self._observe)
self.root.after(HotkeyMgr.POLL, self._poll) self.root.after(MacHotkeyMgr.POLL, self._poll)
# Monkey-patch tk (tkMacOSXKeyEvent.c) # Monkey-patch tk (tkMacOSXKeyEvent.c)
if not self.tkProcessKeyEvent_old: if not self.tkProcessKeyEvent_old:
@ -103,7 +131,7 @@ if platform == 'darwin': # noqa: C901
if self.acquire_state: if self.acquire_state:
if the_event.type() == NSFlagsChanged: if the_event.type() == NSFlagsChanged:
self.acquire_key = the_event.modifierFlags() & NSDeviceIndependentModifierFlagsMask self.acquire_key = the_event.modifierFlags() & NSDeviceIndependentModifierFlagsMask
self.acquire_state = HotkeyMgr.ACQUIRE_NEW self.acquire_state = MacHotkeyMgr.ACQUIRE_NEW
# suppress the event by not chaining the old function # suppress the event by not chaining the old function
return the_event return the_event
@ -111,7 +139,7 @@ if platform == 'darwin': # noqa: C901
c = the_event.charactersIgnoringModifiers() c = the_event.charactersIgnoringModifiers()
self.acquire_key = (c and ord(c[0]) or 0) | \ self.acquire_key = (c and ord(c[0]) or 0) | \
(the_event.modifierFlags() & NSDeviceIndependentModifierFlagsMask) (the_event.modifierFlags() & NSDeviceIndependentModifierFlagsMask)
self.acquire_state = HotkeyMgr.ACQUIRE_NEW self.acquire_state = MacHotkeyMgr.ACQUIRE_NEW
# suppress the event by not chaining the old function # suppress the event by not chaining the old function
return the_event return the_event
@ -147,7 +175,7 @@ if platform == 'darwin': # noqa: C901
self.root.event_generate('<<Invoke>>', when="tail") self.root.event_generate('<<Invoke>>', when="tail")
if self.keycode or self.modifiers: if self.keycode or self.modifiers:
self.root.after(HotkeyMgr.POLL, self._poll) self.root.after(MacHotkeyMgr.POLL, self._poll)
def unregister(self) -> None: def unregister(self) -> None:
"""Remove hotkey registration.""" """Remove hotkey registration."""
@ -157,7 +185,7 @@ if platform == 'darwin': # noqa: C901
@objc.callbackFor(NSEvent.addGlobalMonitorForEventsMatchingMask_handler_) @objc.callbackFor(NSEvent.addGlobalMonitorForEventsMatchingMask_handler_)
def _handler(self, event) -> None: def _handler(self, event) -> None:
# use event.charactersIgnoringModifiers to handle composing characters like Alt-e # use event.charactersIgnoringModifiers to handle composing characters like Alt-e
if ((event.modifierFlags() & HotkeyMgr.MODIFIERMASK) == self.modifiers if ((event.modifierFlags() & MacHotkeyMgr.MODIFIERMASK) == self.modifiers
and ord(event.charactersIgnoringModifiers()[0]) == self.keycode): and ord(event.charactersIgnoringModifiers()[0]) == self.keycode):
if config.get_int('hotkey_always'): if config.get_int('hotkey_always'):
self.activated = True self.activated = True
@ -169,12 +197,12 @@ if platform == 'darwin': # noqa: C901
def acquire_start(self) -> None: def acquire_start(self) -> None:
"""Start acquiring hotkey state via polling.""" """Start acquiring hotkey state via polling."""
self.acquire_state = HotkeyMgr.ACQUIRE_ACTIVE self.acquire_state = MacHotkeyMgr.ACQUIRE_ACTIVE
self.root.after_idle(self._acquire_poll) self.root.after_idle(self._acquire_poll)
def acquire_stop(self) -> None: def acquire_stop(self) -> None:
"""Stop acquiring hotkey state.""" """Stop acquiring hotkey state."""
self.acquire_state = HotkeyMgr.ACQUIRE_INACTIVE self.acquire_state = MacHotkeyMgr.ACQUIRE_INACTIVE
def _acquire_poll(self) -> None: def _acquire_poll(self) -> None:
"""Perform a poll of current hotkey state.""" """Perform a poll of current hotkey state."""
@ -184,10 +212,10 @@ if platform == 'darwin': # noqa: C901
# No way of signalling to Tkinter from within the monkey-patched event handler that doesn't # No way of signalling to Tkinter from within the monkey-patched event handler that doesn't
# cause Python to crash, so poll. # cause Python to crash, so poll.
if self.acquire_state: if self.acquire_state:
if self.acquire_state == HotkeyMgr.ACQUIRE_NEW: if self.acquire_state == MacHotkeyMgr.ACQUIRE_NEW:
# Abuse tkEvent's keycode field to hold our acquired key & modifier # Abuse tkEvent's keycode field to hold our acquired key & modifier
self.root.event_generate('<KeyPress>', keycode=self.acquire_key) self.root.event_generate('<KeyPress>', keycode=self.acquire_key)
self.acquire_state = HotkeyMgr.ACQUIRE_ACTIVE self.acquire_state = MacHotkeyMgr.ACQUIRE_ACTIVE
self.root.after(50, self._acquire_poll) self.root.after(50, self._acquire_poll)
def fromevent(self, event) -> Optional[Union[bool, Tuple]]: def fromevent(self, event) -> Optional[Union[bool, Tuple]]:
@ -201,18 +229,18 @@ if platform == 'darwin': # noqa: C901
if (keycode if (keycode
and not (modifiers & (NSShiftKeyMask | NSControlKeyMask | NSAlternateKeyMask | NSCommandKeyMask))): and not (modifiers & (NSShiftKeyMask | NSControlKeyMask | NSAlternateKeyMask | NSCommandKeyMask))):
if keycode == 0x1b: # Esc = retain previous if keycode == 0x1b: # Esc = retain previous
self.acquire_state = HotkeyMgr.ACQUIRE_INACTIVE self.acquire_state = MacHotkeyMgr.ACQUIRE_INACTIVE
return False return False
# BkSp, Del, Clear = clear hotkey # BkSp, Del, Clear = clear hotkey
elif keycode in [0x7f, ord(NSDeleteFunctionKey), ord(NSClearLineFunctionKey)]: elif keycode in [0x7f, ord(NSDeleteFunctionKey), ord(NSClearLineFunctionKey)]:
self.acquire_state = HotkeyMgr.ACQUIRE_INACTIVE self.acquire_state = MacHotkeyMgr.ACQUIRE_INACTIVE
return None return None
# don't allow keys needed for typing in System Map # don't allow keys needed for typing in System Map
elif keycode in [0x13, 0x20, 0x2d] or 0x61 <= keycode <= 0x7a: elif keycode in [0x13, 0x20, 0x2d] or 0x61 <= keycode <= 0x7a:
NSBeep() NSBeep()
self.acquire_state = HotkeyMgr.ACQUIRE_INACTIVE self.acquire_state = MacHotkeyMgr.ACQUIRE_INACTIVE
return None return None
return (keycode, modifiers) return (keycode, modifiers)
@ -247,8 +275,8 @@ if platform == 'darwin': # noqa: C901
elif ord(NSF1FunctionKey) <= keycode <= ord(NSF35FunctionKey): elif ord(NSF1FunctionKey) <= keycode <= ord(NSF35FunctionKey):
text += f'F{keycode + 1 - ord(NSF1FunctionKey)}' text += f'F{keycode + 1 - ord(NSF1FunctionKey)}'
elif keycode in HotkeyMgr.DISPLAY: # specials elif keycode in MacHotkeyMgr.DISPLAY: # specials
text += HotkeyMgr.DISPLAY[keycode] text += MacHotkeyMgr.DISPLAY[keycode]
elif keycode < 0x20: # control keys elif keycode < 0x20: # control keys
text += chr(keycode + 0x40) text += chr(keycode + 0x40)
@ -270,7 +298,7 @@ if platform == 'darwin': # noqa: C901
self.snd_bad.play() self.snd_bad.play()
elif platform == 'win32': if sys.platform == 'win32':
import atexit import atexit
import ctypes import ctypes
@ -397,7 +425,8 @@ elif platform == 'win32':
INPUT_KEYBOARD = 1 INPUT_KEYBOARD = 1
INPUT_HARDWARE = 2 INPUT_HARDWARE = 2
class HotkeyMgr:
class WindowsHotkeyMgr(AbstractHotkeyMgr):
"""Hot key management.""" """Hot key management."""
# https://msdn.microsoft.com/en-us/library/windows/desktop/dd375731%28v=vs.85%29.aspx # https://msdn.microsoft.com/en-us/library/windows/desktop/dd375731%28v=vs.85%29.aspx
@ -595,8 +624,8 @@ elif platform == 'win32':
elif VK_F1 <= keycode <= VK_F24: elif VK_F1 <= keycode <= VK_F24:
text += f'F{keycode + 1 - VK_F1}' text += f'F{keycode + 1 - VK_F1}'
elif keycode in HotkeyMgr.DISPLAY: # specials elif keycode in WindowsHotkeyMgr.DISPLAY: # specials
text += HotkeyMgr.DISPLAY[keycode] text += WindowsHotkeyMgr.DISPLAY[keycode]
else: else:
c = MapVirtualKey(keycode, 2) # printable ? c = MapVirtualKey(keycode, 2) # printable ?
@ -621,26 +650,34 @@ elif platform == 'win32':
if self.thread: if self.thread:
PostThreadMessage(self.thread.ident, WM_SND_BAD, 0, 0) PostThreadMessage(self.thread.ident, WM_SND_BAD, 0, 0)
else: # Linux
class HotkeyMgr: class LinuxHotKeyMgr(AbstractHotkeyMgr):
"""Hot key management.""" """Hot key management."""
def register(self, root, keycode, modifiers) -> None:
"""Register the hotkey handler."""
pass pass
def unregister(self) -> None:
"""Unregister the hotkey handling."""
pass
def play_good(self) -> None: def get_hotkeymgr(*args, **kwargs) -> AbstractHotkeyMgr:
"""Play the 'good' sound.""" """
pass Determine platform-specific HotkeyMgr.
:param args:
:param kwargs:
:return: Appropriate class instance.
:raises ValueError: If unsupported platform.
"""
if sys.platform == 'darwin':
return MacHotkeyMgr(*args, **kwargs)
elif sys.platform == 'win32':
return WindowsHotkeyMgr(*args, **kwargs)
elif sys.platform == 'linux':
return LinuxHotKeyMgr(*args, **kwargs)
else:
raise ValueError(f'Unknown platform: {sys.platform}')
def play_bad(self) -> None:
"""Play the 'bad' sound."""
pass
# singleton # singleton
hotkeymgr = HotkeyMgr() hotkeymgr = get_hotkeymgr()