From 1ec03462c8725b3a59b497e270192b469237a1c3 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 19 Nov 2015 07:35:14 +0100 Subject: [PATCH] Add a utils.parse_keystring. --- qutebrowser/utils/utils.py | 94 ++++++++++++++++++++++++++++++++++ tests/unit/utils/test_utils.py | 40 +++++++++++++++ 2 files changed, 134 insertions(+) diff --git a/qutebrowser/utils/utils.py b/qutebrowser/utils/utils.py index 01723c704..607c97268 100644 --- a/qutebrowser/utils/utils.py +++ b/qutebrowser/utils/utils.py @@ -391,6 +391,100 @@ def keyevent_to_string(e): return '+'.join(parts) +class KeyInfo: + + """Stores information about a key, like used in a QKeyEvent. + + Attributes: + key: Qt::Key + modifiers: Qt::KeyboardModifiers + text: str + """ + + def __init__(self, key, modifiers, text): + self.key = key + self.modifiers = modifiers + self.text = text + + def __repr__(self): + # Meh, dependency cycle... + from qutebrowser.utils.debug import qenum_key + if self.modifiers is None: + modifiers = None + else: + #modifiers = qflags_key(Qt, self.modifiers) + modifiers = hex(int(self.modifiers)) + return get_repr(self, constructor=True, key=qenum_key(Qt, self.key), + modifiers=modifiers, text=self.text) + + def __eq__(self, other): + return (self.key == other.key and self.modifiers == other.modifiers and + self.text == other.text) + + +class KeyParseError(Exception): + + """Raised by _parse_single_key/parse_keystring on parse errors.""" + + def __init__(self, keystr, error): + super().__init__("Could not parse {!r}: {}".format(keystr, error)) + + +def _parse_single_key(keystr): + """Convert a single key string to a (Qt.Key, Qt.Modifiers, text) tuple.""" + if keystr.startswith('<') and keystr.endswith('>'): + # Special key + keystr = keystr[1:-1] + elif len(keystr) == 1: + # vim-like key + pass + else: + raise KeyParseError(keystr, "Expecting either a single key or a " + " like keybinding.") + + seq = QKeySequence(normalize_keystr(keystr), QKeySequence.PortableText) + if len(seq) != 1: + raise KeyParseError(keystr, "Got {} keys instead of 1.".format( + len(seq))) + result = seq[0] + + if result == Qt.Key_unknown: + raise KeyParseError(keystr, "Got unknown key.") + + modifier_mask = int(Qt.ShiftModifier | Qt.ControlModifier | + Qt.AltModifier | Qt.MetaModifier | Qt.KeypadModifier | + Qt.GroupSwitchModifier) + assert Qt.Key_unknown & ~modifier_mask == Qt.Key_unknown + + modifiers = result & modifier_mask + key = result & ~modifier_mask + + if len(keystr) == 1 and keystr.isupper(): + modifiers |= Qt.ShiftModifier + + assert key != 0, key + key = Qt.Key(key) + modifiers = Qt.KeyboardModifiers(modifiers) + + # Let's hope this is accurate... + if len(keystr) == 1 and not modifiers: + text = keystr + elif len(keystr) == 1 and modifiers == Qt.ShiftModifier: + text = keystr.upper() + else: + text = '' + + return KeyInfo(key, modifiers, text) + + +def parse_keystring(keystr): + """Parse a keystring like or xyz and return a KeyInfo list.""" + if keystr.startswith('<') and keystr.endswith('>'): + return [_parse_single_key(keystr)] + else: + return [_parse_single_key(char) for char in keystr] + + def normalize_keystr(keystr): """Normalize a keystring like Ctrl-Q to a keystring like Ctrl+Q. diff --git a/tests/unit/utils/test_utils.py b/tests/unit/utils/test_utils.py index fa635e1af..499d86ef2 100644 --- a/tests/unit/utils/test_utils.py +++ b/tests/unit/utils/test_utils.py @@ -500,6 +500,46 @@ class TestKeyEventToString: assert utils.keyevent_to_string(evt) == 'Meta+A' +@pytest.mark.parametrize('keystr, expected', [ + ('', utils.KeyInfo(Qt.Key_X, Qt.ControlModifier, '')), + ('', utils.KeyInfo(Qt.Key_X, Qt.MetaModifier, '')), + ('', + utils.KeyInfo(Qt.Key_Y, Qt.ControlModifier | Qt.AltModifier, '')), + ('x', utils.KeyInfo(Qt.Key_X, Qt.NoModifier, 'x')), + ('X', utils.KeyInfo(Qt.Key_X, Qt.ShiftModifier, 'X')), + ('', utils.KeyInfo(Qt.Key_Escape, Qt.NoModifier, '')), + + ('foobar', utils.KeyParseError), + ('x, y', utils.KeyParseError), + ('xyz', utils.KeyParseError), + ('Escape', utils.KeyParseError), + (', ', utils.KeyParseError), +]) +def test_parse_single_key(keystr, expected): + if expected is utils.KeyParseError: + with pytest.raises(utils.KeyParseError): + utils._parse_single_key(keystr) + else: + assert utils._parse_single_key(keystr) == expected + + + +@pytest.mark.parametrize('keystr, expected', [ + ('', [utils.KeyInfo(Qt.Key_X, Qt.ControlModifier, '')]), + ('x', [utils.KeyInfo(Qt.Key_X, Qt.NoModifier, 'x')]), + ('xy', [utils.KeyInfo(Qt.Key_X, Qt.NoModifier, 'x'), + utils.KeyInfo(Qt.Key_Y, Qt.NoModifier, 'y')]), + + ('', utils.KeyParseError), +]) +def test_parse_keystring(keystr, expected): + if expected is utils.KeyParseError: + with pytest.raises(utils.KeyParseError): + utils.parse_keystring(keystr) + else: + assert utils.parse_keystring(keystr) == expected + + @pytest.mark.parametrize('orig, repl', [ ('Control+x', 'ctrl+x'), ('Windows+x', 'meta+x'),