"""Configurable keybindings. Part of the tagit module. A copy of the license is provided with the project. Author: Matthias Baumgartner, 2022 """ # standard imports import typing # tagit imports from tagit.utils import errors # exports __all__: typing.Sequence[str] = ( 'Binding', ) ## code ## class Binding(object): """Handle keybindings. A keybinding is a set of three constraints: * Key code * Inclusive modifiers * Exclusive modifiers Inclusive modifiers must be present, exclusive ones must not be present. Modifiers occuring in neither of the two lists are ignored. Modifiers are always lowercase strings. Additionally to SHIFT, CTRL and ALT, the modifiers "all" and "rest" can be used. "all" is a shortcut for all of the modifiers known. "rest" means all modifiers not consumed by the other list yet. "rest" can therefore only occur in at most one of the lists. Usage example: >>> # From settings, with PGUP w/o modifiers as default >>> Binding.check(evt, self.cfg("bindings", "browser", "page_prev", ... default=Binding.simple(Binding.PGUP, None, Binding.mALL))) >>> # ESC or CTRL + SHIFT + a >>> Binding.check(evt, Binding.multi((Binding.ESC, ), ... (97, (Binding.mCTRL, Binding.mSHIFT), Binding.mREST)))) """ # Modifiers mSHIFT = 'shift' mCTRL = 'ctrl' mALT = 'alt' mCMD = 'cmd' mALTGR = 'altgr' mNUMLOCK = 'numlock' mCAPSLOCK = 'capslock' # Modifier specials mALL = 'all' mREST = 'rest' # Special keys BACKSPACE = 8 TAB = 9 ENTER = 13 ESC = 27 SPACEBAR = 32 DEL = 127 UP = 273 DOWN = 274 RIGHT = 275 LEFT = 276 INSERT = 277 HOME = 278 END = 279 PGUP = 280 PGDN = 281 F1 = 282 F2 = 283 F3 = 284 F4 = 285 F5 = 286 F6 = 287 F7 = 288 F8 = 289 F9 = 290 F10 = 291 F11 = 292 F12 = 293 CAPSLOCK = 301 RIGHT_SHIFT = 303 LEFT_SHIFT = 304 LEFT_CTRL = 305 RIGHT_CTRL = 306 ALTGR = 307 ALT = 308 CMD = 309 @staticmethod def simple(code, inclusive=None, exclusive=None): """Create a binding constraint.""" # handle strings inclusive = (inclusive, ) if isinstance(inclusive, str) else inclusive exclusive = (exclusive, ) if isinstance(exclusive, str) else exclusive # handle None, ensure tuple inclusive = tuple(inclusive) if inclusive is not None else tuple() exclusive = tuple(exclusive) if exclusive is not None else tuple() # handle code code = Binding.str_to_key(code.lower()) if isinstance(code, str) else code if code is None: raise errors.ProgrammingError('invalid key code') # build constraint return [(code, inclusive, exclusive)] @staticmethod def multi(*args): """Return binding for multiple constraints.""" return [Binding.simple(*arg)[0] for arg in args] @staticmethod def from_string(string): mods = (Binding.mSHIFT, Binding.mCTRL, Binding.mALT, Binding.mCMD, Binding.mALTGR, Binding.mNUMLOCK, Binding.mCAPSLOCK) bindings = [] for kcombo in (itm.strip() for itm in string.split(';')): strokes = [key.lower().strip() for key in kcombo.split('+')] # modifiers; ignore lock modifiers inc = [key for key in strokes if key in mods] inc = [key for key in inc if key not in (Binding.mNUMLOCK, Binding.mCAPSLOCK)] # key code = [key for key in strokes if key not in mods] if len(code) != 1: raise errors.ProgrammingError('there must be exactly one key code in a keybinding') code = Binding.str_to_key(code[0]) if code is None: raise errors.ProgrammingError('invalid key code') bindings.append((code, tuple(inc), (Binding.mREST, ))) return bindings @staticmethod def to_string(constraints): values = [] for code, inc, exc in constraints: values.append( ' + '.join([m.upper() for m in inc] + [Binding.key_to_str(code)])) return '; '.join(values) @staticmethod def check(stroke, constraint): """Return True if *evt* matches the *constraint*.""" code, char, modifiers = stroke all_ = {Binding.mSHIFT, Binding.mCTRL, Binding.mALT, Binding.mCMD, Binding.mALTGR} for key, inclusive, exclusive in constraint: inclusive, exclusive = set(inclusive), set(exclusive) if key in (code, char): # Otherwise, we don't have to process the modifiers # Handle specials if 'all' in inclusive: inclusive = all_ if 'all' in exclusive: exclusive = all_ if 'rest' in inclusive: inclusive = all_ - exclusive if 'rest' in exclusive: exclusive = all_ - inclusive if (all([mod in modifiers for mod in inclusive]) and all([mod not in modifiers for mod in exclusive])): # Code and modifiers match return True # No matching constraint found return False @staticmethod def key_to_str(code, default='?'): if isinstance(code, str): return code if 32 <= code and code <= 226 and code != 127: return chr(code) return { Binding.BACKSPACE : 'BACKSPACE', Binding.TAB : 'TAB', Binding.ENTER : 'ENTER', Binding.ESC : 'ESC', Binding.SPACEBAR : 'SPACEBAR', Binding.DEL : 'DEL', Binding.UP : 'UP', Binding.DOWN : 'DOWN', Binding.RIGHT : 'RIGHT', Binding.LEFT : 'LEFT', Binding.INSERT : 'INSERT', Binding.HOME : 'HOME', Binding.END : 'END', Binding.PGUP : 'PGUP', Binding.PGDN : 'PGDN', Binding.F1 : 'F1', Binding.F2 : 'F2', Binding.F3 : 'F3', Binding.F4 : 'F4', Binding.F5 : 'F5', Binding.F6 : 'F6', Binding.F7 : 'F7', Binding.F8 : 'F8', Binding.F9 : 'F9', Binding.F10 : 'F10', Binding.F11 : 'F11', Binding.F12 : 'F12', Binding.CAPSLOCK : 'CAPSLOCK', Binding.RIGHT_SHIFT : 'RIGHT_SHIFT', Binding.LEFT_SHIFT : 'LEFT_SHIFT', Binding.LEFT_CTRL : 'LEFT_CTRL', Binding.RIGHT_CTRL : 'RIGHT_CTRL', Binding.ALTGR : 'ALTGR', Binding.ALT : 'ALT', Binding.CMD : 'CMD', }.get(code, default) @staticmethod def str_to_key(char, default=None): if isinstance(char, int): return char try: # check if ascii code = ord(char) if 32 <= code and code <= 226: return code except TypeError: pass return { 'BACKSPACE' : Binding.BACKSPACE, 'TAB' : Binding.TAB, 'ENTER' : Binding.ENTER, 'ESC' : Binding.ESC, 'SPACEBAR' : Binding.SPACEBAR, 'DEL' : Binding.DEL, 'UP' : Binding.UP, 'DOWN' : Binding.DOWN, 'RIGHT' : Binding.RIGHT, 'LEFT' : Binding.LEFT, 'INSERT' : Binding.INSERT, 'HOME' : Binding.HOME, 'END' : Binding.END, 'PGUP' : Binding.PGUP, 'PGDN' : Binding.PGDN, 'F1' : Binding.F1, 'F2' : Binding.F2, 'F3' : Binding.F3, 'F4' : Binding.F4, 'F5' : Binding.F5, 'F6' : Binding.F6, 'F7' : Binding.F7, 'F8' : Binding.F8, 'F9' : Binding.F9, 'F10' : Binding.F10, 'F11' : Binding.F11, 'F12' : Binding.F12, 'CAPSLOCK' : Binding.CAPSLOCK, 'RIGHT_SHIFT' : Binding.RIGHT_SHIFT, 'LEFT_SHIFT' : Binding.LEFT_SHIFT, 'LEFT_CTRL' : Binding.LEFT_CTRL, 'RIGHT_CTRL' : Binding.RIGHT_CTRL, 'ALTGR' : Binding.ALTGR, 'ALT' : Binding.ALT, 'CMD' : Binding.CMD, }.get(char, default) ## EOF ##