diff --git a/hotkey.py b/hotkey.py index bb26f5ab..a185c822 100644 --- a/hotkey.py +++ b/hotkey.py @@ -32,10 +32,10 @@ if platform == 'darwin': # noqa: C901 # 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'→', + 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', + 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'⌧', } @@ -122,6 +122,7 @@ if platform == 'darwin': # noqa: C901 # 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(), @@ -203,7 +204,7 @@ if platform == 'darwin': # noqa: C901 (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 + if keycode == 0x1b: # Esc = retain previous self.acquire_state = HotkeyMgr.ACQUIRE_INACTIVE return False @@ -254,7 +255,7 @@ if platform == 'darwin': # noqa: C901 text += HotkeyMgr.DISPLAY[keycode] elif keycode < 0x20: # control keys - text += chr(keycode+0x40) + text += chr(keycode + 0x40) elif keycode < 0xf700: # key char text += chr(keycode).upper() @@ -281,78 +282,117 @@ elif platform == 'win32': import winsound from ctypes.wintypes import DWORD, HWND, LONG, LPWSTR, MSG, ULONG, WORD - RegisterHotKey = ctypes.windll.user32.RegisterHotKey - UnregisterHotKey = ctypes.windll.user32.UnregisterHotKey - MOD_ALT = 0x0001 - MOD_CONTROL = 0x0002 - MOD_SHIFT = 0x0004 - MOD_WIN = 0x0008 + RegisterHotKey = ctypes.windll.user32.RegisterHotKey + UnregisterHotKey = ctypes.windll.user32.UnregisterHotKey + MOD_ALT = 0x0001 + MOD_CONTROL = 0x0002 + MOD_SHIFT = 0x0004 + MOD_WIN = 0x0008 MOD_NOREPEAT = 0x4000 - GetMessage = ctypes.windll.user32.GetMessageW - TranslateMessage = ctypes.windll.user32.TranslateMessage - DispatchMessage = ctypes.windll.user32.DispatchMessageW + GetMessage = ctypes.windll.user32.GetMessageW + TranslateMessage = ctypes.windll.user32.TranslateMessage + DispatchMessage = ctypes.windll.user32.DispatchMessageW PostThreadMessage = ctypes.windll.user32.PostThreadMessageW - WM_QUIT = 0x0012 - WM_HOTKEY = 0x0312 - WM_APP = 0x8000 - WM_SND_GOOD = WM_APP + 1 - WM_SND_BAD = WM_APP + 2 + WM_QUIT = 0x0012 + WM_HOTKEY = 0x0312 + WM_APP = 0x8000 + WM_SND_GOOD = WM_APP + 1 + WM_SND_BAD = WM_APP + 2 - GetKeyState = ctypes.windll.user32.GetKeyState - MapVirtualKey = ctypes.windll.user32.MapVirtualKeyW - VK_BACK = 0x08 - VK_CLEAR = 0x0c - VK_RETURN = 0x0d - VK_SHIFT = 0x10 - VK_CONTROL = 0x11 - VK_MENU = 0x12 - VK_CAPITAL = 0x14 - VK_MODECHANGE= 0x1f - VK_ESCAPE = 0x1b - VK_SPACE = 0x20 - VK_DELETE = 0x2e - VK_LWIN = 0x5b - VK_RWIN = 0x5c - VK_NUMPAD0 = 0x60 - VK_DIVIDE = 0x6f - VK_F1 = 0x70 - VK_F24 = 0x87 + GetKeyState = ctypes.windll.user32.GetKeyState + MapVirtualKey = ctypes.windll.user32.MapVirtualKeyW + VK_BACK = 0x08 + VK_CLEAR = 0x0c + VK_RETURN = 0x0d + VK_SHIFT = 0x10 + VK_CONTROL = 0x11 + VK_MENU = 0x12 + VK_CAPITAL = 0x14 + VK_MODECHANGE = 0x1f + VK_ESCAPE = 0x1b + VK_SPACE = 0x20 + VK_DELETE = 0x2e + VK_LWIN = 0x5b + VK_RWIN = 0x5c + VK_NUMPAD0 = 0x60 + VK_DIVIDE = 0x6f + VK_F1 = 0x70 + VK_F24 = 0x87 VK_OEM_MINUS = 0xbd - VK_NUMLOCK = 0x90 - VK_SCROLL = 0x91 - VK_PROCESSKEY= 0xe5 + VK_NUMLOCK = 0x90 + VK_SCROLL = 0x91 + VK_PROCESSKEY = 0xe5 VK_OEM_CLEAR = 0xfe - GetForegroundWindow = ctypes.windll.user32.GetForegroundWindow - GetWindowText = ctypes.windll.user32.GetWindowTextW + GetWindowText = ctypes.windll.user32.GetWindowTextW GetWindowText.argtypes = [HWND, LPWSTR, ctypes.c_int] GetWindowTextLength = ctypes.windll.user32.GetWindowTextLengthW - def WindowTitle(h): + def window_title(h) -> str: + """ + Determine the title for a window. + + :param h: Window handle. + :return: Window title. + """ if h: - l = GetWindowTextLength(h) + 1 - buf = ctypes.create_unicode_buffer(l) - if GetWindowText(h, buf, l): + title_length = GetWindowTextLength(h) + 1 + buf = ctypes.create_unicode_buffer(title_length) + if GetWindowText(h, buf, title_length): return buf.value + return '' - class MOUSEINPUT(ctypes.Structure): - _fields_ = [('dx', LONG), ('dy', LONG), ('mouseData', DWORD), ('dwFlags', DWORD), ('time', DWORD), ('dwExtraInfo', ctypes.POINTER(ULONG))] + """Mouse Input structure.""" + + _fields_ = [ + ('dx', LONG), + ('dy', LONG), + ('mouseData', DWORD), + ('dwFlags', DWORD), + ('time', DWORD), + ('dwExtraInfo', ctypes.POINTER(ULONG)) + ] class KEYBDINPUT(ctypes.Structure): - _fields_ = [('wVk', WORD), ('wScan', WORD), ('dwFlags', DWORD), ('time', DWORD), ('dwExtraInfo', ctypes.POINTER(ULONG))] + """Keyboard Input structure.""" + + _fields_ = [ + ('wVk', WORD), + ('wScan', WORD), + ('dwFlags', DWORD), + ('time', DWORD), + ('dwExtraInfo', ctypes.POINTER(ULONG)) + ] class HARDWAREINPUT(ctypes.Structure): - _fields_ = [('uMsg', DWORD), ('wParamL', WORD), ('wParamH', WORD)] + """Hardware Input structure.""" - class INPUT_union(ctypes.Union): - _fields_ = [('mi', MOUSEINPUT), ('ki', KEYBDINPUT), ('hi', HARDWAREINPUT)] + _fields_ = [ + ('uMsg', DWORD), + ('wParamL', WORD), + ('wParamH', WORD) + ] + + class INPUTUNION(ctypes.Union): + """Input union.""" + + _fields_ = [ + ('mi', MOUSEINPUT), + ('ki', KEYBDINPUT), + ('hi', HARDWAREINPUT) + ] class INPUT(ctypes.Structure): - _fields_ = [('type', DWORD), ('union', INPUT_union)] + """Input structure.""" + + _fields_ = [ + ('type', DWORD), + ('union', INPUTUNION) + ] SendInput = ctypes.windll.user32.SendInput SendInput.argtypes = [ctypes.c_uint, ctypes.POINTER(INPUT), ctypes.c_int] @@ -361,19 +401,20 @@ elif platform == 'win32': INPUT_KEYBOARD = 1 INPUT_HARDWARE = 2 - class HotkeyMgr: + """Hot key management.""" # https://msdn.microsoft.com/en-us/library/windows/desktop/dd375731%28v=vs.85%29.aspx # Limit ourselves to symbols in Windows 7 Segoe UI - DISPLAY = { 0x03: 'Break', 0x08: 'Bksp', 0x09: u'↹', 0x0c: 'Clear', 0x0d: u'↵', 0x13: 'Pause', - 0x14: u'Ⓐ', 0x1b: 'Esc', - 0x20: u'⏘', 0x21: 'PgUp', 0x22: 'PgDn', 0x23: 'End', 0x24: 'Home', - 0x25: u'←', 0x26: u'↑', 0x27: u'→', 0x28: u'↓', - 0x2c: 'PrtScn', 0x2d: 'Ins', 0x2e: 'Del', 0x2f: 'Help', - 0x5d: u'▤', 0x5f: u'☾', - 0x90: u'➀', 0x91: 'ScrLk', - 0xa6: u'⇦', 0xa7: u'⇨', 0xa9: u'⊗', 0xab: u'☆', 0xac: u'⌂', 0xb4: u'✉', + DISPLAY = { + 0x03: 'Break', 0x08: 'Bksp', 0x09: u'↹', 0x0c: 'Clear', 0x0d: u'↵', 0x13: 'Pause', + 0x14: u'Ⓐ', 0x1b: 'Esc', + 0x20: u'⏘', 0x21: 'PgUp', 0x22: 'PgDn', 0x23: 'End', 0x24: 'Home', + 0x25: u'←', 0x26: u'↑', 0x27: u'→', 0x28: u'↓', + 0x2c: 'PrtScn', 0x2d: 'Ins', 0x2e: 'Del', 0x2f: 'Help', + 0x5d: u'▤', 0x5f: u'☾', + 0x90: u'➀', 0x91: 'ScrLk', + 0xa6: u'⇦', 0xa7: u'⇨', 0xa9: u'⊗', 0xab: u'☆', 0xac: u'⌂', 0xb4: u'✉', } def __init__(self): @@ -385,7 +426,7 @@ elif platform == 'win32': self.snd_bad = sb.read() atexit.register(self.unregister) - def register(self, root, keycode, modifiers): + def register(self, root: tk.Tk, keycode, modifiers) -> None: """Register the hotkey handler.""" self.root = root @@ -395,13 +436,17 @@ elif platform == 'win32': if keycode or modifiers: logger.debug('Creating thread worker...') - self.thread = threading.Thread(target=self.worker, name=f'Hotkey "{keycode}:{modifiers}"', args=(keycode, modifiers)) + self.thread = threading.Thread( + target=self.worker, + name=f'Hotkey "{keycode}:{modifiers}"', + args=(keycode, modifiers) + ) self.thread.daemon = True logger.debug('Starting thread worker...') self.thread.start() logger.debug('Done.') - def unregister(self): + def unregister(self) -> None: """Unregister the hotkey handling.""" thread = self.thread @@ -418,7 +463,7 @@ elif platform == 'win32': logger.debug('Done.') - def worker(self, keycode, modifiers): + def worker(self, keycode, modifiers) -> None: # noqa: CCR001 """Handle hotkeys.""" logger.debug('Begin...') # Hotkey must be registered by the thread that handles it @@ -427,7 +472,7 @@ elif platform == 'win32': self.thread = None return - fake = INPUT(INPUT_KEYBOARD, INPUT_union(ki=KEYBDINPUT(keycode, keycode, 0, 0, None))) + fake = INPUT(INPUT_KEYBOARD, INPUTUNION(ki=KEYBDINPUT(keycode, keycode, 0, 0, None))) msg = MSG() logger.debug('Entering GetMessage() loop...') @@ -438,7 +483,7 @@ elif platform == 'win32': if ( config.get_int('hotkey_always') - or WindowTitle(GetForegroundWindow()).startswith('Elite - Dangerous') + or window_title(GetForegroundWindow()).startswith('Elite - Dangerous') ): if not config.shutting_down: logger.debug('Sending event <>') @@ -458,7 +503,7 @@ elif platform == 'win32': elif msg.message == WM_SND_BAD: logger.debug('WM_SND_BAD') - winsound.PlaySound(self.snd_bad, winsound.SND_MEMORY) # synchronous + winsound.PlaySound(self.snd_bad, winsound.SND_MEMORY) # synchronous else: logger.debug('Something else') @@ -470,86 +515,135 @@ elif platform == 'win32': self.thread = None logger.debug('Done.') - def acquire_start(self): + def acquire_start(self) -> None: + """Start acquiring hotkey state via polling.""" pass - def acquire_stop(self): + def acquire_stop(self) -> None: + """Stop acquiring hotkey state.""" pass - def fromevent(self, event): - # event.state is a pain - it shows the state of the modifiers *before* a modifier key was pressed. - # event.state *does* differentiate between left and right Ctrl and Alt and between Return and Enter - # by putting KF_EXTENDED in bit 18, but RegisterHotKey doesn't differentiate. - modifiers = ((GetKeyState(VK_MENU) & 0x8000) and MOD_ALT) | ((GetKeyState(VK_CONTROL) & 0x8000) and MOD_CONTROL) | ((GetKeyState(VK_SHIFT) & 0x8000) and MOD_SHIFT) | ((GetKeyState(VK_LWIN) & 0x8000) and MOD_WIN) | ((GetKeyState(VK_RWIN) & 0x8000) and MOD_WIN) + def fromevent(self, event) -> Optional[Union[bool, Tuple]]: # noqa: CCR001 + """ + Return configuration (keycode, modifiers) or None=clear or False=retain previous. + + event.state is a pain - it shows the state of the modifiers *before* a modifier key was pressed. + event.state *does* differentiate between left and right Ctrl and Alt and between Return and Enter + by putting KF_EXTENDED in bit 18, but RegisterHotKey doesn't differentiate. + + :param event: tk event ? + :return: False to retain previous, None to not use, else (keycode, modifiers) + """ + modifiers = ((GetKeyState(VK_MENU) & 0x8000) and MOD_ALT) \ + | ((GetKeyState(VK_CONTROL) & 0x8000) and MOD_CONTROL) \ + | ((GetKeyState(VK_SHIFT) & 0x8000) and MOD_SHIFT) \ + | ((GetKeyState(VK_LWIN) & 0x8000) and MOD_WIN) \ + | ((GetKeyState(VK_RWIN) & 0x8000) and MOD_WIN) keycode = event.keycode - if keycode in [ VK_SHIFT, VK_CONTROL, VK_MENU, VK_LWIN, VK_RWIN ]: + if keycode in [VK_SHIFT, VK_CONTROL, VK_MENU, VK_LWIN, VK_RWIN]: return (0, modifiers) + if not modifiers: - if keycode == VK_ESCAPE: # Esc = retain previous + if keycode == VK_ESCAPE: # Esc = retain previous return False - elif keycode in [ VK_BACK, VK_DELETE, VK_CLEAR, VK_OEM_CLEAR ]: # BkSp, Del, Clear = clear hotkey + + elif keycode in [VK_BACK, VK_DELETE, VK_CLEAR, VK_OEM_CLEAR]: # BkSp, Del, Clear = clear hotkey return None - elif keycode in [ VK_RETURN, VK_SPACE, VK_OEM_MINUS] or ord('A') <= keycode <= ord('Z'): # don't allow keys needed for typing in System Map + + elif keycode in [VK_RETURN, VK_SPACE, VK_OEM_MINUS] or ord('A') <= keycode <= ord( + 'Z'): # don't allow keys needed for typing in System Map winsound.MessageBeep() return None - elif keycode in [ VK_NUMLOCK, VK_SCROLL, VK_PROCESSKEY ] or VK_CAPITAL <= keycode <= VK_MODECHANGE: # ignore unmodified mode switch keys + + elif (keycode in [VK_NUMLOCK, VK_SCROLL, VK_PROCESSKEY] + or VK_CAPITAL <= keycode <= VK_MODECHANGE): # ignore unmodified mode switch keys return (0, modifiers) # See if the keycode is usable and available - if RegisterHotKey(None, 2, modifiers|MOD_NOREPEAT, keycode): + if RegisterHotKey(None, 2, modifiers | MOD_NOREPEAT, keycode): UnregisterHotKey(None, 2) return (keycode, modifiers) + else: winsound.MessageBeep() return None - def display(self, 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 & MOD_WIN: text += u'❖+' - if modifiers & MOD_CONTROL: text += u'Ctrl+' - if modifiers & MOD_ALT: text += u'Alt+' - if modifiers & MOD_SHIFT: text += u'⇧+' - if VK_NUMPAD0 <= keycode <= VK_DIVIDE: text += u'№' + if modifiers & MOD_WIN: + text += u'❖+' + + if modifiers & MOD_CONTROL: + text += u'Ctrl+' + + if modifiers & MOD_ALT: + text += u'Alt+' + + if modifiers & MOD_SHIFT: + text += u'⇧+' + + if VK_NUMPAD0 <= keycode <= VK_DIVIDE: + text += u'№' if not keycode: pass + elif VK_F1 <= keycode <= VK_F24: - text += 'F%d' % (keycode + 1 - VK_F1) - elif keycode in HotkeyMgr.DISPLAY: # specials + text += f'F{keycode + 1 - VK_F1}' + + elif keycode in HotkeyMgr.DISPLAY: # specials text += HotkeyMgr.DISPLAY[keycode] + else: - c = MapVirtualKey(keycode, 2) # printable ? - if not c: # oops not printable + c = MapVirtualKey(keycode, 2) # printable ? + if not c: # oops not printable text += u'⁈' - elif c < 0x20: # control keys - text += chr(c+0x40) + + elif c < 0x20: # control keys + text += chr(c + 0x40) + else: text += chr(c).upper() + return text - def play_good(self): + def play_good(self) -> None: + """Play the 'good' sound.""" if self.thread: PostThreadMessage(self.thread.ident, WM_SND_GOOD, 0, 0) - def play_bad(self): + def play_bad(self) -> None: + """Play the 'bad' sound.""" if self.thread: PostThreadMessage(self.thread.ident, WM_SND_BAD, 0, 0) -else: # Linux +else: # Linux class HotkeyMgr: + """Hot key management.""" - def register(self, root, keycode, modifiers): + def register(self, root, keycode, modifiers) -> None: + """Register the hotkey handler.""" pass - def unregister(self): + def unregister(self) -> None: + """Unregister the hotkey handling.""" pass - def play_good(self): + def play_good(self) -> None: + """Play the 'good' sound.""" pass - def play_bad(self): + def play_bad(self) -> None: + """Play the 'bad' sound.""" pass # singleton