1
0
mirror of https://github.com/EDCD/EDMarketConnector.git synced 2025-04-12 15:27:14 +03:00
EDMarketConnector/hotkey.py
2015-09-12 04:03:12 +01:00

382 lines
17 KiB
Python

# -*- coding: utf-8 -*-
from os.path import dirname, join, normpath
import sys
from sys import platform
if platform == 'darwin':
import threading
import objc
from AppKit import NSApplication, NSWorkspace, NSBeep, NSSound, NSEvent, NSKeyDown, NSKeyUp, NSFlagsChanged, NSKeyDownMask, NSFlagsChangedMask, NSShiftKeyMask, NSControlKeyMask, NSAlternateKeyMask, NSCommandKeyMask, NSNumericPadKeyMask, NSDeviceIndependentModifierFlagsMask, NSF1FunctionKey, NSF35FunctionKey, NSDeleteFunctionKey, NSClearLineFunctionKey
class HotkeyMgr:
MODIFIERMASK = NSShiftKeyMask|NSControlKeyMask|NSAlternateKeyMask|NSCommandKeyMask|NSNumericPadKeyMask
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.root = None
self.keycode = 0
self.modifiers = 0
self.activated = False
self.observer = None
self.acquire_key = 0
self.acquire_state = HotkeyMgr.ACQUIRE_INACTIVE
self.tkProcessKeyEvent_old = None
if getattr(sys, 'frozen', False):
respath = normpath(join(dirname(sys.executable), os.pardir, 'Resources'))
elif __file__:
respath = dirname(__file__)
else:
respath = '.'
self.snd_good = NSSound.alloc().initWithContentsOfFile_byReference_(join(respath, 'snd_good.wav'), False)
self.snd_bad = NSSound.alloc().initWithContentsOfFile_byReference_(join(respath, 'snd_bad.wav'), False)
def register(self, root, keycode, modifiers):
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(HotkeyMgr.POLL, self._poll)
# Monkey-patch tk (tkMacOSXKeyEvent.c)
if not self.tkProcessKeyEvent_old:
sel = 'tkProcessKeyEvent:'
cls = NSApplication.sharedApplication().class__()
self.tkProcessKeyEvent_old = NSApplication.sharedApplication().methodForSelector_(sel)
newmethod = objc.selector(self.tkProcessKeyEvent, selector = self.tkProcessKeyEvent_old.selector, signature = self.tkProcessKeyEvent_old.signature)
objc.classAddMethod(cls, sel, newmethod)
# Monkey-patch tk (tkMacOSXKeyEvent.c) to:
# - 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)
def tkProcessKeyEvent(self, cls, theEvent):
if self.acquire_state:
if theEvent.type() == NSFlagsChanged:
self.acquire_key = theEvent.modifierFlags() & NSDeviceIndependentModifierFlagsMask
self.acquire_state = HotkeyMgr.ACQUIRE_NEW
# suppress the event by not chaining the old function
return theEvent
elif theEvent.type() in (NSKeyDown, NSKeyUp):
c = theEvent.charactersIgnoringModifiers()
self.acquire_key = (c and ord(c[0]) or 0) | (theEvent.modifierFlags() & NSDeviceIndependentModifierFlagsMask)
self.acquire_state = HotkeyMgr.ACQUIRE_NEW
# suppress the event by not chaining the old function
return theEvent
# replace empty characters with charactersIgnoringModifiers to avoid crash
elif theEvent.type() in (NSKeyDown, NSKeyUp) and not theEvent.characters():
theEvent = NSEvent.keyEventWithType_location_modifierFlags_timestamp_windowNumber_context_characters_charactersIgnoringModifiers_isARepeat_keyCode_(theEvent.type(), theEvent.locationInWindow(), theEvent.modifierFlags(), theEvent.timestamp(), theEvent.windowNumber(), theEvent.context(), theEvent.charactersIgnoringModifiers(), theEvent.charactersIgnoringModifiers(), theEvent.isARepeat(), theEvent.keyCode())
return self.tkProcessKeyEvent_old(cls, theEvent)
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):
# 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(HotkeyMgr.POLL, self._poll)
def unregister(self):
self.keycode = None
self.modifiers = None
@objc.callbackFor(NSEvent.addGlobalMonitorForEventsMatchingMask_handler_)
def _handler(self, event):
# use event.charactersIgnoringModifiers to handle composing characters like Alt-e
if (event.modifierFlags() & HotkeyMgr.MODIFIERMASK) == self.modifiers and ord(event.charactersIgnoringModifiers()[0]) == self.keycode:
# Only trigger if game client is front process
active = [x for x in NSWorkspace.sharedWorkspace().runningApplications() if x.isActive()]
if active and active[0] and active[0].bundleIdentifier() == 'uk.co.frontier.EliteDangerous':
self.activated = True
def acquire_start(self):
self.acquire_state = HotkeyMgr.ACQUIRE_ACTIVE
self.root.after_idle(self._acquire_poll)
def acquire_stop(self):
self.acquire_state = HotkeyMgr.ACQUIRE_INACTIVE
def _acquire_poll(self):
# 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 == HotkeyMgr.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 = HotkeyMgr.ACQUIRE_ACTIVE
self.root.after(50, self._acquire_poll)
def fromevent(self, event):
# Return configuration (keycode, modifiers) or None=clear or False=retain previous
(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 = HotkeyMgr.ACQUIRE_INACTIVE
return False
elif keycode in [0x7f, NSDeleteFunctionKey, NSClearLineFunctionKey]: # BkSp, Del, Clear = clear hotkey
self.acquire_state = HotkeyMgr.ACQUIRE_INACTIVE
return None
elif keycode in [0x13, 0x20, 0x2d] or 0x61 <= keycode <= 0x7a: # don't allow keys needed for typing in System Map
NSBeep()
self.acquire_state = HotkeyMgr.ACQUIRE_INACTIVE
return None
return (keycode, modifiers)
def display(self, keycode, modifiers):
# Return displayable 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 NSF1FunctionKey <= keycode <= NSF35FunctionKey:
text += 'F%d' % (keycode + 1 - NSF1FunctionKey)
elif keycode in HotkeyMgr.DISPLAY: # specials
text += HotkeyMgr.DISPLAY[keycode]
elif keycode < 0x20: # control keys
text += unichr(keycode+0x40)
elif keycode < 0xf700: # key char
text += unichr(keycode).upper()
else:
text += u''
return text
def play_good(self):
self.snd_good.play()
def play_bad(self):
self.snd_bad.play()
elif platform == 'win32':
import ctypes
from ctypes.wintypes import *
import threading
import winsound
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
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
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_OEM_CLEAR = 0xfe
GetForegroundWindow = ctypes.windll.user32.GetForegroundWindow
GetWindowText = ctypes.windll.user32.GetWindowTextW
GetWindowText.argtypes = [HWND, LPWSTR, ctypes.c_int]
GetWindowTextLength = ctypes.windll.user32.GetWindowTextLengthW
class HotkeyMgr:
# 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'',
}
def __init__(self):
self.root = None
self.thread = None
if getattr(sys, 'frozen', False):
respath = dirname(sys.executable)
elif __file__:
respath = dirname(__file__)
else:
respath = '.'
self.snd_good = open(join(respath, 'snd_good.wav'), 'rb').read()
self.snd_bad = open(join(respath, 'snd_bad.wav'), 'rb').read()
def register(self, root, keycode, modifiers):
self.root = root
if self.thread:
self.unregister()
if keycode or modifiers:
self.thread = threading.Thread(target = self.worker, name = 'Hotkey "%x:%x"' % (keycode,modifiers), args = (keycode,modifiers))
self.thread.daemon = True
self.thread.start()
def unregister(self):
if self.thread:
PostThreadMessage(self.thread.ident, WM_QUIT, 0, 0)
self.thread = None
def worker(self, keycode, modifiers):
# Hotkey must be registered by the thread that handles it
if not RegisterHotKey(None, 1, modifiers|MOD_NOREPEAT, keycode):
self.thread = None
return
msg = MSG()
while GetMessage(ctypes.byref(msg), None, 0, 0) != 0:
if msg.message == WM_HOTKEY:
h = GetForegroundWindow()
if h:
l = GetWindowTextLength(h) + 1
buf = ctypes.create_unicode_buffer(l)
if GetWindowText(h, buf, l) and buf.value.startswith('Elite - Dangerous'):
self.root.event_generate('<<Invoke>>', when="tail")
elif msg.message == WM_SND_GOOD:
winsound.PlaySound(self.snd_good, winsound.SND_MEMORY) # synchronous
elif msg.message == WM_SND_BAD:
winsound.PlaySound(self.snd_bad, winsound.SND_MEMORY) # synchronous
else:
TranslateMessage(ctypes.byref(msg))
DispatchMessage(ctypes.byref(msg))
UnregisterHotKey(None, 1)
self.thread = None
def acquire_start(self):
pass
def acquire_stop(self):
pass
def fromevent(self, event):
# event.state is a pain - it shows the state of the modifiers *before* a modifier key was pressed
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 ]:
return (0, modifiers)
if not modifiers:
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
return None
elif keycode in [ VK_RETURN, VK_SPACE, VK_OEM_MINUS] or 0x41 <= keycode <= 0x5a: # 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
return (0, modifiers)
# See if the keycode is usable and available
if RegisterHotKey(None, 2, modifiers|MOD_NOREPEAT, keycode):
UnregisterHotKey(None, 2)
return (keycode, modifiers)
else:
winsound.MessageBeep()
return None
def display(self, keycode, modifiers):
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 not keycode:
pass
elif VK_F1 <= keycode <= VK_F24:
text += 'F%d' % (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
text += u''
elif c < 0x20: # control keys
text += unichr(c+0x40)
else:
text += unichr(c).upper()
return text
def play_good(self):
if self.thread:
PostThreadMessage(self.thread.ident, WM_SND_GOOD, 0, 0)
def play_bad(self):
if self.thread:
PostThreadMessage(self.thread.ident, WM_SND_BAD, 0, 0)
else: # Linux
class HotkeyMgr:
def register(self, keycode, modifiers):
pass
def unregister(self):
pass
# singleton
hotkeymgr = HotkeyMgr()