diff --git a/qutebrowser/keyinput/basekeyparser.py b/qutebrowser/keyinput/basekeyparser.py index 33b49501d..6ebb0b464 100644 --- a/qutebrowser/keyinput/basekeyparser.py +++ b/qutebrowser/keyinput/basekeyparser.py @@ -155,9 +155,8 @@ class BaseKeyParser(QObject): match, binding = self._match_key(sequence) if match == QKeySequence.NoMatch: - mappings = config.val.bindings.key_mappings - mapped = mappings.get(sequence, None) - if mapped is not None: + mapped = sequence.with_mappings(config.val.bindings.key_mappings) + if sequence != mapped: self._debug_log("Mapped {} -> {}".format( sequence, mapped)) match, binding = self._match_key(mapped) diff --git a/qutebrowser/keyinput/keyutils.py b/qutebrowser/keyinput/keyutils.py index 91d6a36a3..dfe4fb3ff 100644 --- a/qutebrowser/keyinput/keyutils.py +++ b/qutebrowser/keyinput/keyutils.py @@ -321,6 +321,10 @@ class KeyInfo: """Get a QKeyEvent from this KeyInfo.""" return QKeyEvent(typ, self.key, self.modifiers, self.text()) + def to_int(self): + """Get the key as an integer (with key/modifiers).""" + return int(self.key) | int(self.modifiers) + class KeySequence: @@ -495,6 +499,18 @@ class KeySequence: return self.__class__(*keys) + def with_mappings(self, mappings): + """Get a new KeySequence with the given mappings applied.""" + keys = [] + for key in self._iter_keys(): + key_seq = KeySequence(key) + if key_seq in mappings: + new_seq = mappings[key_seq] + assert len(new_seq) == 1 + key = new_seq[0].to_int() + keys.append(key) + return self.__class__(*keys) + @classmethod def parse(cls, keystr): """Parse a keystring like or xyz and return a KeySequence.""" diff --git a/tests/unit/keyinput/test_basekeyparser.py b/tests/unit/keyinput/test_basekeyparser.py index 662b1f82a..581d0b676 100644 --- a/tests/unit/keyinput/test_basekeyparser.py +++ b/tests/unit/keyinput/test_basekeyparser.py @@ -212,6 +212,14 @@ class TestHandle: handle_text(Qt.Key_B) assert not keyparser.execute.called + def test_mapping_in_key_chain(self, config_stub, handle_text, keyparser): + """A mapping should work even as part of a keychain.""" + config_stub.val.bindings.commands = {'normal': + {'aa': 'message-info aa'}} + keyparser._read_config('normal') + handle_text(Qt.Key_A, Qt.Key_X) + keyparser.execute.assert_called_once_with('message-info aa', None) + def test_binding_with_shift(self, keyparser, fake_keyevent): """Simulate a binding which involves shift.""" for key, modifiers in [(Qt.Key_Y, Qt.NoModifier), diff --git a/tests/unit/keyinput/test_keyutils.py b/tests/unit/keyinput/test_keyutils.py index 11dd41b1f..0557b3c3f 100644 --- a/tests/unit/keyinput/test_keyutils.py +++ b/tests/unit/keyinput/test_keyutils.py @@ -377,6 +377,12 @@ class TestKeySequence: with pytest.raises(keyutils.KeyParseError): seq.append_event(event) + def test_with_mappings(self): + seq = keyutils.KeySequence.parse('foobar') + mappings = {keyutils.KeySequence('b'): keyutils.KeySequence('t')} + seq2 = seq.with_mappings(mappings) + assert seq2 == keyutils.KeySequence.parse('footar') + @pytest.mark.parametrize('keystr, expected', [ ('', keyutils.KeySequence(Qt.ControlModifier | Qt.AltModifier | Qt.Key_Y)), @@ -443,6 +449,11 @@ def test_key_info_to_event(): assert ev.text() == 'A' +def test_key_info_to_int(): + info = keyutils.KeyInfo(Qt.Key_A, Qt.ShiftModifier) + assert info.to_int() == Qt.Key_A | Qt.ShiftModifier + + @pytest.mark.parametrize('key, printable', [ (Qt.Key_Control, False), (Qt.Key_Escape, False),