"""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 ##