Source code for tagit.bindings

"""Configurable keybindings.

Part of the tagit module.
A copy of the license is provided with the project.
Author: Matthias Baumgartner, 2016

"""
# IMPORTS
from collections import defaultdict

# INNER-MODULE IMPORTS
from basics import fst, difference, string_types

# CONSTANTS

# EXPORTS
__all__ = ('Binding', )

## CODE ##

[docs]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.settings.trace("bindings", "browser", "page_prev", Binding.simple(Binding.PGUP, None, Binding.ALL))) >>> # ESC or CTRL + SHIFT + a >>> Binding.check(evt, Binding.multi((Binding.ESC, ), (97, (Binding.CTRL, Binding.SHIFT), Binding.REST)))) """ # Modifiers SHIFT = 'shift' CTRL = 'ctrl' ALT = 'alt' # Modifier specials ALL = 'all' REST = 'rest' # Special keys BACKSPACE = 8 ENTER = 13 ESC = 27 SPACEBAR = 32 SLASH = 47 DEL = 127 UP = 273 DOWN = 274 RIGHT = 275 LEFT = 276 HOME = 278 END = 279 PGUP = 280 PGDOWN = 281 @staticmethod
[docs] def simple(code, inclusive=None, exclusive=None): """Create a binding constraint.""" # Handle None if inclusive is None: inclusive = [] if exclusive is None: exclusive = [] # Handle strings if isinstance(inclusive, string_types): inclusive = (inclusive, ) if isinstance(exclusive, string_types): exclusive = (exclusive, ) # Build constraint return ((code, inclusive, exclusive), )
@staticmethod
[docs] def multi(*args): """Return binding for multiple constraints.""" return [fst(Binding.simple(*arg)) for arg in args]
@staticmethod
[docs] def check(((code, scankey), modifiers), constraint): """Return True if *evt* matches the *constraint*. """ all_ = [Binding.CTRL, Binding.SHIFT, Binding.ALT] for key, inclusive, exclusive in constraint: if key in (code, scankey): # 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 = difference(all_, exclusive) if 'rest' in exclusive: exclusive = difference(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 printable(cfg_binds): def recurse(binds): parts = [] for key, val in binds.iteritems(): if isinstance(val, dict): # subdict! parts += recurse(val) elif isinstance(val, list): # Binding! evt = key.replace('_', ' ') for code, inclusive, exclusive in val: combo = '' + ' + '.join(map(lambda s: s.upper(), inclusive)) combo += len(inclusive) > 0 and ' + ' or '' combo += Binding.translate(code) parts.append((combo, evt)) return parts printable = [] for client, binds in cfg_binds.iteritems(): # browser, filter, ... if isinstance(binds, dict): # saveguard against title parts = recurse(binds) printable.append((client.title(), parts)) return printable @staticmethod def translate(code): if 97 <= code and code <= 122: return chr(code) return defaultdict(lambda: code, { Binding.BACKSPACE : 'BACKSPACE' , Binding.ENTER : 'ENTER' , Binding.ESC : 'ESC' , Binding.SPACEBAR : 'SPACEBAR' , Binding.SLASH : '/' , Binding.DEL : 'DEL' , Binding.UP : 'UP' , Binding.DOWN : 'DOWN' , Binding.RIGHT : 'RIGHT' , Binding.LEFT : 'LEFT' , Binding.HOME : 'HOME' , Binding.END : 'END' , Binding.PGUP : 'PGUP' , Binding.PGDOWN : 'PGDN' , 286: 'F5' })[code] ## EOF ##