summaryrefslogtreecommitdiff
path: root/qutebrowser/keyinput
diff options
context:
space:
mode:
authorMarc Jauvin <marc.jauvin@gmail.com>2018-03-16 14:28:36 -0400
committerMarc Jauvin <marc.jauvin@gmail.com>2018-03-16 14:28:36 -0400
commitb7159d780a69daf104da939438938d262cd86000 (patch)
treeab9dc382617174d366dae51cda89186cfbd2bcb9 /qutebrowser/keyinput
parentc9f6cd507b55dabe4d4d8f7841955837a634ff20 (diff)
parentf7074b80d0a68eec6fdfd13f2f82acc94ff2951e (diff)
downloadqutebrowser-b7159d780a69daf104da939438938d262cd86000.tar.gz
qutebrowser-b7159d780a69daf104da939438938d262cd86000.zip
Merge 'origin/master' into tab-input-mode
Diffstat (limited to 'qutebrowser/keyinput')
-rw-r--r--qutebrowser/keyinput/basekeyparser.py299
-rw-r--r--qutebrowser/keyinput/keyparser.py77
-rw-r--r--qutebrowser/keyinput/keyutils.py558
-rw-r--r--qutebrowser/keyinput/modeman.py75
-rw-r--r--qutebrowser/keyinput/modeparsers.py201
5 files changed, 837 insertions, 373 deletions
diff --git a/qutebrowser/keyinput/basekeyparser.py b/qutebrowser/keyinput/basekeyparser.py
index 89120f922..f0f2c6f28 100644
--- a/qutebrowser/keyinput/basekeyparser.py
+++ b/qutebrowser/keyinput/basekeyparser.py
@@ -19,14 +19,12 @@
"""Base class for vim-like key sequence parser."""
-import enum
-import re
-import unicodedata
-
from PyQt5.QtCore import pyqtSignal, QObject
+from PyQt5.QtGui import QKeySequence
from qutebrowser.config import config
from qutebrowser.utils import usertypes, log, utils
+from qutebrowser.keyinput import keyutils
class BaseKeyParser(QObject):
@@ -43,24 +41,16 @@ class BaseKeyParser(QObject):
definitive: Keychain matches exactly.
none: No more matches possible.
- Types: type of a key binding.
- chain: execute() was called via a chain-like key binding
- special: execute() was called via a special key binding
-
do_log: Whether to log keypresses or not.
passthrough: Whether unbound keys should be passed through with this
handler.
Attributes:
bindings: Bound key bindings
- special_bindings: Bound special bindings (<Foo>).
_win_id: The window ID this keyparser is associated with.
- _warn_on_keychains: Whether a warning should be logged when binding
- keychains in a section which does not support them.
- _keystring: The currently entered key sequence
+ _sequence: The currently entered key sequence
_modename: The name of the input mode associated with this keyparser.
_supports_count: Whether count is supported
- _supports_chains: Whether keychains are supported
Signals:
keystring_updated: Emitted when the keystring is updated.
@@ -76,27 +66,18 @@ class BaseKeyParser(QObject):
do_log = True
passthrough = False
- Match = enum.Enum('Match', ['partial', 'definitive', 'other', 'none'])
- Type = enum.Enum('Type', ['chain', 'special'])
-
- def __init__(self, win_id, parent=None, supports_count=None,
- supports_chains=False):
+ def __init__(self, win_id, parent=None, supports_count=True):
super().__init__(parent)
self._win_id = win_id
self._modename = None
- self._keystring = ''
- if supports_count is None:
- supports_count = supports_chains
+ self._sequence = keyutils.KeySequence()
+ self._count = ''
self._supports_count = supports_count
- self._supports_chains = supports_chains
- self._warn_on_keychains = True
self.bindings = {}
- self.special_bindings = {}
config.instance.changed.connect(self._on_config_changed)
def __repr__(self):
- return utils.get_repr(self, supports_count=self._supports_count,
- supports_chains=self._supports_chains)
+ return utils.get_repr(self, supports_count=self._supports_count)
def _debug_log(self, message):
"""Log a message to the debug log if logging is active.
@@ -107,62 +88,66 @@ class BaseKeyParser(QObject):
if self.do_log:
log.keyboard.debug(message)
- def _handle_special_key(self, e):
- """Handle a new keypress with special keys (<Foo>).
-
- Return True if the keypress has been handled, and False if not.
-
- Args:
- e: the KeyPressEvent from Qt.
-
- Return:
- True if event has been handled, False otherwise.
- """
- binding = utils.keyevent_to_string(e)
- if binding is None:
- self._debug_log("Ignoring only-modifier keyeevent.")
- return False
-
- if binding not in self.special_bindings:
- key_mappings = config.val.bindings.key_mappings
- try:
- binding = key_mappings['<{}>'.format(binding)][1:-1]
- except KeyError:
- pass
-
- try:
- cmdstr = self.special_bindings[binding]
- except KeyError:
- self._debug_log("No special binding found for {}.".format(binding))
- return False
- count, _command = self._split_count(self._keystring)
- self.execute(cmdstr, self.Type.special, count)
- self.clear_keystring()
- return True
-
- def _split_count(self, keystring):
- """Get count and command from the current keystring.
+ def _match_key(self, sequence):
+ """Try to match a given keystring with any bound keychain.
Args:
- keystring: The key string to split.
+ sequence: The command string to find.
Return:
- A (count, command) tuple.
+ A tuple (matchtype, binding).
+ matchtype: Match.definitive, Match.partial or Match.none.
+ binding: - None with Match.partial/Match.none.
+ - The found binding with Match.definitive.
"""
- if self._supports_count:
- (countstr, cmd_input) = re.fullmatch(r'(\d*)(.*)',
- keystring).groups()
- count = int(countstr) if countstr else None
- if count == 0 and not cmd_input:
- cmd_input = keystring
- count = None
- else:
- cmd_input = keystring
- count = None
- return count, cmd_input
-
- def _handle_single_key(self, e):
- """Handle a new keypress with a single key (no modifiers).
+ assert sequence
+ assert not isinstance(sequence, str)
+ result = QKeySequence.NoMatch
+
+ for seq, cmd in self.bindings.items():
+ assert not isinstance(seq, str), seq
+ match = sequence.matches(seq)
+ if match == QKeySequence.ExactMatch:
+ return match, cmd
+ elif match == QKeySequence.PartialMatch:
+ result = QKeySequence.PartialMatch
+
+ return result, None
+
+ def _match_without_modifiers(self, sequence):
+ """Try to match a key with optional modifiers stripped."""
+ self._debug_log("Trying match without modifiers")
+ sequence = sequence.strip_modifiers()
+ match, binding = self._match_key(sequence)
+ return match, binding, sequence
+
+ def _match_key_mapping(self, sequence):
+ """Try to match a key in bindings.key_mappings."""
+ self._debug_log("Trying match with key_mappings")
+ 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)
+ sequence = mapped
+ return match, binding, sequence
+ return QKeySequence.NoMatch, None, sequence
+
+ def _match_count(self, sequence, dry_run):
+ """Try to match a key as count."""
+ txt = str(sequence[-1]) # To account for sequences changed above.
+ if (txt.isdigit() and self._supports_count and
+ not (not self._count and txt == '0')):
+ self._debug_log("Trying match as count")
+ assert len(txt) == 1, txt
+ if not dry_run:
+ self._count += txt
+ self.keystring_updated.emit(self._count + str(self._sequence))
+ return True
+ return False
+
+ def handle(self, e, *, dry_run=False):
+ """Handle a new keypress.
Separate the keypress into count/command, then check if it matches
any possible command, and either run the command, ignore it, or
@@ -170,109 +155,62 @@ class BaseKeyParser(QObject):
Args:
e: the KeyPressEvent from Qt.
+ dry_run: Don't actually execute anything, only check whether there
+ would be a match.
Return:
- A self.Match member.
+ A QKeySequence match.
"""
- txt = e.text()
key = e.key()
- self._debug_log("Got key: 0x{:x} / text: '{}'".format(key, txt))
+ txt = str(keyutils.KeyInfo.from_event(e))
+ self._debug_log("Got key: 0x{:x} / modifiers: 0x{:x} / text: '{}' / "
+ "dry_run {}".format(key, int(e.modifiers()), txt,
+ dry_run))
- if len(txt) == 1:
- category = unicodedata.category(txt)
- is_control_char = (category == 'Cc')
- else:
- is_control_char = False
-
- if (not txt) or is_control_char:
- self._debug_log("Ignoring, no text char")
- return self.Match.none
-
- count, cmd_input = self._split_count(self._keystring + txt)
- match, binding = self._match_key(cmd_input)
- if match == self.Match.none:
- mappings = config.val.bindings.key_mappings
- mapped = mappings.get(txt, None)
- if mapped is not None:
- txt = mapped
- count, cmd_input = self._split_count(self._keystring + txt)
- match, binding = self._match_key(cmd_input)
-
- self._keystring += txt
- if match == self.Match.definitive:
+ if keyutils.is_modifier_key(key):
+ self._debug_log("Ignoring, only modifier")
+ return QKeySequence.NoMatch
+
+ try:
+ sequence = self._sequence.append_event(e)
+ except keyutils.KeyParseError as ex:
+ self._debug_log("{} Aborting keychain.".format(ex))
+ self.clear_keystring()
+ return QKeySequence.NoMatch
+
+ match, binding = self._match_key(sequence)
+ if match == QKeySequence.NoMatch:
+ match, binding, sequence = self._match_without_modifiers(sequence)
+ if match == QKeySequence.NoMatch:
+ match, binding, sequence = self._match_key_mapping(sequence)
+ if match == QKeySequence.NoMatch:
+ was_count = self._match_count(sequence, dry_run)
+ if was_count:
+ return QKeySequence.ExactMatch
+
+ if dry_run:
+ return match
+
+ self._sequence = sequence
+
+ if match == QKeySequence.ExactMatch:
self._debug_log("Definitive match for '{}'.".format(
- self._keystring))
+ sequence))
+ count = int(self._count) if self._count else None
self.clear_keystring()
- self.execute(binding, self.Type.chain, count)
- elif match == self.Match.partial:
+ self.execute(binding, count)
+ elif match == QKeySequence.PartialMatch:
self._debug_log("No match for '{}' (added {})".format(
- self._keystring, txt))
- elif match == self.Match.none:
+ sequence, txt))
+ self.keystring_updated.emit(self._count + str(sequence))
+ elif match == QKeySequence.NoMatch:
self._debug_log("Giving up with '{}', no matches".format(
- self._keystring))
+ sequence))
self.clear_keystring()
- elif match == self.Match.other:
- pass
else:
raise utils.Unreachable("Invalid match value {!r}".format(match))
- return match
-
- def _match_key(self, cmd_input):
- """Try to match a given keystring with any bound keychain.
-
- Args:
- cmd_input: The command string to find.
-
- Return:
- A tuple (matchtype, binding).
- matchtype: Match.definitive, Match.partial or Match.none.
- binding: - None with Match.partial/Match.none.
- - The found binding with Match.definitive.
- """
- if not cmd_input:
- # Only a count, no command yet, but we handled it
- return (self.Match.other, None)
- # A (cmd_input, binding) tuple (k, v of bindings) or None.
- definitive_match = None
- partial_match = False
- # Check definitive match
- try:
- definitive_match = (cmd_input, self.bindings[cmd_input])
- except KeyError:
- pass
- # Check partial match
- for binding in self.bindings:
- if definitive_match is not None and binding == definitive_match[0]:
- # We already matched that one
- continue
- elif binding.startswith(cmd_input):
- partial_match = True
- break
- if definitive_match is not None:
- return (self.Match.definitive, definitive_match[1])
- elif partial_match:
- return (self.Match.partial, None)
- else:
- return (self.Match.none, None)
-
- def handle(self, e):
- """Handle a new keypress and call the respective handlers.
-
- Args:
- e: the KeyPressEvent from Qt
-
- Return:
- True if the event was handled, False otherwise.
- """
- handled = self._handle_special_key(e)
- if handled or not self._supports_chains:
- return handled
- match = self._handle_single_key(e)
- # don't emit twice if the keystring was cleared in self.clear_keystring
- if self._keystring:
- self.keystring_updated.emit(self._keystring)
- return match != self.Match.none
+ return match
@config.change_filter('bindings')
def _on_config_changed(self):
@@ -295,37 +233,26 @@ class BaseKeyParser(QObject):
else:
self._modename = modename
self.bindings = {}
- self.special_bindings = {}
for key, cmd in config.key_instance.get_bindings_for(modename).items():
+ assert not isinstance(key, str), key
assert cmd
- self._parse_key_command(modename, key, cmd)
-
- def _parse_key_command(self, modename, key, cmd):
- """Parse the keys and their command and store them in the object."""
- if utils.is_special_key(key):
- self.special_bindings[key[1:-1]] = cmd
- elif self._supports_chains:
self.bindings[key] = cmd
- elif self._warn_on_keychains:
- log.keyboard.warning("Ignoring keychain '{}' in mode '{}' because "
- "keychains are not supported there."
- .format(key, modename))
- def execute(self, cmdstr, keytype, count=None):
+ def execute(self, cmdstr, count=None):
"""Handle a completed keychain.
Args:
cmdstr: The command to execute as a string.
- keytype: Type.chain or Type.special
count: The count if given.
"""
raise NotImplementedError
def clear_keystring(self):
"""Clear the currently entered key sequence."""
- if self._keystring:
- self._debug_log("discarding keystring '{}'.".format(
- self._keystring))
- self._keystring = ''
- self.keystring_updated.emit(self._keystring)
+ if self._sequence:
+ self._debug_log("Clearing keystring (was: {}).".format(
+ self._sequence))
+ self._sequence = keyutils.KeySequence()
+ self._count = ''
+ self.keystring_updated.emit('')
diff --git a/qutebrowser/keyinput/keyparser.py b/qutebrowser/keyinput/keyparser.py
deleted file mode 100644
index 8ae27412e..000000000
--- a/qutebrowser/keyinput/keyparser.py
+++ /dev/null
@@ -1,77 +0,0 @@
-# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
-
-# Copyright 2014-2018 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
-#
-# This file is part of qutebrowser.
-#
-# qutebrowser is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# qutebrowser is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
-
-"""Advanced keyparsers."""
-
-import traceback
-
-from qutebrowser.keyinput.basekeyparser import BaseKeyParser
-from qutebrowser.utils import message, utils
-from qutebrowser.commands import runners, cmdexc
-
-
-class CommandKeyParser(BaseKeyParser):
-
- """KeyChainParser for command bindings.
-
- Attributes:
- _commandrunner: CommandRunner instance.
- """
-
- def __init__(self, win_id, parent=None, supports_count=None,
- supports_chains=False):
- super().__init__(win_id, parent, supports_count, supports_chains)
- self._commandrunner = runners.CommandRunner(win_id)
-
- def execute(self, cmdstr, _keytype, count=None):
- try:
- self._commandrunner.run(cmdstr, count)
- except cmdexc.Error as e:
- message.error(str(e), stack=traceback.format_exc())
-
-
-class PassthroughKeyParser(CommandKeyParser):
-
- """KeyChainParser which passes through normal keys.
-
- Used for insert/passthrough modes.
-
- Attributes:
- _mode: The mode this keyparser is for.
- """
-
- do_log = False
- passthrough = True
-
- def __init__(self, win_id, mode, parent=None, warn=True):
- """Constructor.
-
- Args:
- mode: The mode this keyparser is for.
- parent: Qt parent.
- warn: Whether to warn if an ignored key was bound.
- """
- super().__init__(win_id, parent, supports_chains=False)
- self._warn_on_keychains = warn
- self._read_config(mode)
- self._mode = mode
-
- def __repr__(self):
- return utils.get_repr(self, mode=self._mode,
- warn=self._warn_on_keychains)
diff --git a/qutebrowser/keyinput/keyutils.py b/qutebrowser/keyinput/keyutils.py
new file mode 100644
index 000000000..1f34fcae0
--- /dev/null
+++ b/qutebrowser/keyinput/keyutils.py
@@ -0,0 +1,558 @@
+# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
+
+# Copyright 2014-2018 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
+#
+# This file is part of qutebrowser.
+#
+# qutebrowser is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# qutebrowser is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
+
+"""Our own QKeySequence-like class and related utilities."""
+
+import itertools
+
+import attr
+from PyQt5.QtCore import Qt, QEvent
+from PyQt5.QtGui import QKeySequence, QKeyEvent
+
+from qutebrowser.utils import utils
+
+
+# Map Qt::Key values to their Qt::KeyboardModifier value.
+_MODIFIER_MAP = {
+ Qt.Key_Shift: Qt.ShiftModifier,
+ Qt.Key_Control: Qt.ControlModifier,
+ Qt.Key_Alt: Qt.AltModifier,
+ Qt.Key_Meta: Qt.MetaModifier,
+ Qt.Key_Mode_switch: Qt.GroupSwitchModifier,
+}
+
+
+def _assert_plain_key(key):
+ """Make sure this is a key without KeyboardModifiers mixed in."""
+ assert not key & Qt.KeyboardModifierMask, hex(key)
+
+
+def _assert_plain_modifier(key):
+ """Make sure this is a modifier without a key mixed in."""
+ assert not key & ~Qt.KeyboardModifierMask, hex(key)
+
+
+def _is_printable(key):
+ _assert_plain_key(key)
+ return key <= 0xff and key not in [Qt.Key_Space, 0x0]
+
+
+def is_special(key, modifiers):
+ """Check whether this key requires special key syntax."""
+ _assert_plain_key(key)
+ _assert_plain_modifier(modifiers)
+ return not (_is_printable(key) and
+ modifiers in [Qt.ShiftModifier, Qt.NoModifier])
+
+
+def is_modifier_key(key):
+ """Test whether the given key is a modifier.
+
+ This only considers keys which are part of Qt::KeyboardModifiers, i.e.
+ which would interrupt a key chain like "yY" when handled.
+ """
+ _assert_plain_key(key)
+ return key in _MODIFIER_MAP
+
+
+def _check_valid_utf8(s, data):
+ """Make sure the given string is valid UTF-8.
+
+ Makes sure there are no chars where Qt did fall back to weird UTF-16
+ surrogates.
+ """
+ try:
+ s.encode('utf-8')
+ except UnicodeEncodeError as e: # pragma: no cover
+ raise ValueError("Invalid encoding in 0x{:x} -> {}: {}"
+ .format(data, s, e))
+
+
+def _key_to_string(key):
+ """Convert a Qt::Key member to a meaningful name.
+
+ Args:
+ key: A Qt::Key member.
+
+ Return:
+ A name of the key as a string.
+ """
+ _assert_plain_key(key)
+ special_names_str = {
+ # Some keys handled in a weird way by QKeySequence::toString.
+ # See https://bugreports.qt.io/browse/QTBUG-40030
+ # Most are unlikely to be ever needed, but you never know ;)
+ # For dead/combining keys, we return the corresponding non-combining
+ # key, as that's easier to add to the config.
+
+ 'Super_L': 'Super L',
+ 'Super_R': 'Super R',
+ 'Hyper_L': 'Hyper L',
+ 'Hyper_R': 'Hyper R',
+ 'Direction_L': 'Direction L',
+ 'Direction_R': 'Direction R',
+
+ 'Shift': 'Shift',
+ 'Control': 'Control',
+ 'Meta': 'Meta',
+ 'Alt': 'Alt',
+
+ 'AltGr': 'AltGr',
+ 'Multi_key': 'Multi key',
+ 'SingleCandidate': 'Single Candidate',
+ 'Mode_switch': 'Mode switch',
+ 'Dead_Grave': '`',
+ 'Dead_Acute': '´',
+ 'Dead_Circumflex': '^',
+ 'Dead_Tilde': '~',
+ 'Dead_Macron': '¯',
+ 'Dead_Breve': '˘',
+ 'Dead_Abovedot': '˙',
+ 'Dead_Diaeresis': '¨',
+ 'Dead_Abovering': '˚',
+ 'Dead_Doubleacute': '˝',
+ 'Dead_Caron': 'ˇ',
+ 'Dead_Cedilla': '¸',
+ 'Dead_Ogonek': '˛',
+ 'Dead_Iota': 'Iota',
+ 'Dead_Voiced_Sound': 'Voiced Sound',
+ 'Dead_Semivoiced_Sound': 'Semivoiced Sound',
+ 'Dead_Belowdot': 'Belowdot',
+ 'Dead_Hook': 'Hook',
+ 'Dead_Horn': 'Horn',
+
+ 'Memo': 'Memo',
+ 'ToDoList': 'To Do List',
+ 'Calendar': 'Calendar',
+ 'ContrastAdjust': 'Contrast Adjust',
+ 'LaunchG': 'Launch (G)',
+ 'LaunchH': 'Launch (H)',
+
+ 'MediaLast': 'Media Last',
+
+ 'unknown': 'Unknown',
+
+ # For some keys, we just want a different name
+ 'Escape': 'Escape',
+ }
+ # We now build our real special_names dict from the string mapping above.
+ # The reason we don't do this directly is that certain Qt versions don't
+ # have all the keys, so we want to ignore AttributeErrors.
+ special_names = {}
+ for k, v in special_names_str.items():
+ try:
+ special_names[getattr(Qt, 'Key_' + k)] = v
+ except AttributeError:
+ pass
+ special_names[0x0] = 'nil'
+
+ if key in special_names:
+ return special_names[key]
+
+ result = QKeySequence(key).toString()
+ _check_valid_utf8(result, key)
+ return result
+
+
+def _modifiers_to_string(modifiers):
+ """Convert the given Qt::KeyboardModifiers to a string.
+
+ Handles Qt.GroupSwitchModifier because Qt doesn't handle that as a
+ modifier.
+ """
+ _assert_plain_modifier(modifiers)
+ if modifiers & Qt.GroupSwitchModifier:
+ modifiers &= ~Qt.GroupSwitchModifier
+ result = 'AltGr+'
+ else:
+ result = ''
+
+ result += QKeySequence(modifiers).toString()
+
+ _check_valid_utf8(result, modifiers)
+ return result
+
+
+class KeyParseError(Exception):
+
+ """Raised by _parse_single_key/parse_keystring on parse errors."""
+
+ def __init__(self, keystr, error):
+ if keystr is None:
+ msg = "Could not parse keystring: {}".format(error)
+ else:
+ msg = "Could not parse {!r}: {}".format(keystr, error)
+ super().__init__(msg)
+
+
+def _parse_keystring(keystr):
+ key = ''
+ special = False
+ for c in keystr:
+ if c == '>':
+ if special:
+ yield _parse_special_key(key)
+ key = ''
+ special = False
+ else:
+ yield '>'
+ assert not key, key
+ elif c == '<':
+ special = True
+ elif special:
+ key += c
+ else:
+ yield _parse_single_key(c)
+ if special:
+ yield '<'
+ for c in key:
+ yield _parse_single_key(c)
+
+
+def _parse_special_key(keystr):
+ """Normalize a keystring like Ctrl-Q to a keystring like Ctrl+Q.
+
+ Args:
+ keystr: The key combination as a string.
+
+ Return:
+ The normalized keystring.
+ """
+ keystr = keystr.lower()
+ replacements = (
+ ('control', 'ctrl'),
+ ('windows', 'meta'),
+ ('mod1', 'alt'),
+ ('mod4', 'meta'),
+ ('less', '<'),
+ ('greater', '>'),
+ )
+ for (orig, repl) in replacements:
+ keystr = keystr.replace(orig, repl)
+
+ for mod in ['ctrl', 'meta', 'alt', 'shift', 'num']:
+ keystr = keystr.replace(mod + '-', mod + '+')
+ return keystr
+
+
+def _parse_single_key(keystr):
+ """Get a keystring for QKeySequence for a single key."""
+ return 'Shift+' + keystr if keystr.isupper() else keystr
+
+
+@attr.s
+class KeyInfo:
+
+ """A key with optional modifiers.
+
+ Attributes:
+ key: A Qt::Key member.
+ modifiers: A Qt::KeyboardModifiers enum value.
+ """
+
+ key = attr.ib()
+ modifiers = attr.ib()
+
+ @classmethod
+ def from_event(cls, e):
+ return cls(e.key(), e.modifiers())
+
+ def __str__(self):
+ """Convert this KeyInfo to a meaningful name.
+
+ Return:
+ A name of the key (combination) as a string.
+ """
+ key_string = _key_to_string(self.key)
+ modifiers = int(self.modifiers)
+
+ if self.key in _MODIFIER_MAP:
+ # Don't return e.g. <Shift+Shift>
+ modifiers &= ~_MODIFIER_MAP[self.key]
+ elif _is_printable(self.key):
+ # "normal" binding
+ if not key_string: # pragma: no cover
+ raise ValueError("Got empty string for key 0x{:x}!"
+ .format(self.key))
+
+ assert len(key_string) == 1, key_string
+ if self.modifiers == Qt.ShiftModifier:
+ assert not is_special(self.key, self.modifiers)
+ return key_string.upper()
+ elif self.modifiers == Qt.NoModifier:
+ assert not is_special(self.key, self.modifiers)
+ return key_string.lower()
+ else:
+ # Use special binding syntax, but <Ctrl-a> instead of <Ctrl-A>
+ key_string = key_string.lower()
+
+ # "special" binding
+ assert is_special(self.key, self.modifiers)
+ modifier_string = _modifiers_to_string(modifiers)
+ return '<{}{}>'.format(modifier_string, key_string)
+
+ def text(self):
+ """Get the text which would be displayed when pressing this key."""
+ control = {
+ Qt.Key_Space: ' ',
+ Qt.Key_Tab: '\t',
+ Qt.Key_Backspace: '\b',
+ Qt.Key_Return: '\r',
+ Qt.Key_Enter: '\r',
+ Qt.Key_Escape: '\x1b',
+ }
+
+ if self.key in control:
+ return control[self.key]
+ elif not _is_printable(self.key):
+ return ''
+
+ text = QKeySequence(self.key).toString()
+ if not self.modifiers & Qt.ShiftModifier:
+ text = text.lower()
+ return text
+
+ def to_event(self, typ=QEvent.KeyPress):
+ """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:
+
+ """A sequence of key presses.
+
+ This internally uses chained QKeySequence objects and exposes a nicer
+ interface over it.
+
+ NOTE: While private members of this class are in theory mutable, they must
+ not be mutated in order to ensure consistent hashing.
+
+ Attributes:
+ _sequences: A list of QKeySequence
+
+ Class attributes:
+ _MAX_LEN: The maximum amount of keys in a QKeySequence.
+ """
+
+ _MAX_LEN = 4
+
+ def __init__(self, *keys):
+ self._sequences = []
+ for sub in utils.chunk(keys, self._MAX_LEN):
+ sequence = QKeySequence(*sub)
+ self._sequences.append(sequence)
+ if keys:
+ assert self
+ self._validate()
+
+ def __str__(self):
+ parts = []
+ for info in self:
+ parts.append(str(info))
+ return ''.join(parts)
+
+ def __iter__(self):
+ """Iterate over KeyInfo objects."""
+ for key_and_modifiers in self._iter_keys():
+ key = int(key_and_modifiers) & ~Qt.KeyboardModifierMask
+ modifiers = Qt.KeyboardModifiers(int(key_and_modifiers) &
+ Qt.KeyboardModifierMask)
+ yield KeyInfo(key=key, modifiers=modifiers)
+
+ def __repr__(self):
+ return utils.get_repr(self, keys=str(self))
+
+ def __lt__(self, other):
+ # pylint: disable=protected-access
+ return self._sequences < other._sequences
+
+ def __gt__(self, other):
+ # pylint: disable=protected-access
+ return self._sequences > other._sequences
+
+ def __le__(self, other):
+ # pylint: disable=protected-access
+ return self._sequences <= other._sequences
+
+ def __ge__(self, other):
+ # pylint: disable=protected-access
+ return self._sequences >= other._sequences
+
+ def __eq__(self, other):
+ # pylint: disable=protected-access
+ return self._sequences == other._sequences
+
+ def __ne__(self, other):
+ # pylint: disable=protected-access
+ return self._sequences != other._sequences
+
+ def __hash__(self):
+ return hash(tuple(self._sequences))
+
+ def __len__(self):
+ return sum(len(seq) for seq in self._sequences)
+
+ def __bool__(self):
+ return bool(self._sequences)
+
+ def __getitem__(self, item):
+ if isinstance(item, slice):
+ keys = list(self._iter_keys())
+ return self.__class__(*keys[item])
+ else:
+ infos = list(self)
+ return infos[item]
+
+ def _iter_keys(self):
+ return itertools.chain.from_iterable(self._sequences)
+
+ def _validate(self, keystr=None):
+ for info in self:
+ if info.key < Qt.Key_Space or info.key >= Qt.Key_unknown:
+ raise KeyParseError(keystr, "Got invalid key!")
+
+ for seq in self._sequences:
+ if not seq:
+ raise KeyParseError(keystr, "Got invalid key!")
+
+ def matches(self, other):
+ """Check whether the given KeySequence matches with this one.
+
+ We store multiple QKeySequences with <= 4 keys each, so we need to
+ match those pair-wise, and account for an unequal amount of sequences
+ as well.
+ """
+ # pylint: disable=protected-access
+
+ if len(self._sequences) > len(other._sequences):
+ # If we entered more sequences than there are in the config,
+ # there's no way there can be a match.
+ return QKeySequence.NoMatch
+
+ for entered, configured in zip(self._sequences, other._sequences):
+ # If we get NoMatch/PartialMatch in a sequence, we can abort there.
+ match = entered.matches(configured)
+ if match != QKeySequence.ExactMatch:
+ return match
+
+ # We checked all common sequences and they had an ExactMatch.
+ #
+ # If there's still more sequences configured than entered, that's a
+ # PartialMatch, as more keypresses can still follow and new sequences
+ # will appear which we didn't check above.
+ #
+ # If there's the same amount of sequences configured and entered,
+ # that's an EqualMatch.
+ if len(self._sequences) == len(other._sequences):
+ return QKeySequence.ExactMatch
+ elif len(self._sequences) < len(other._sequences):
+ return QKeySequence.PartialMatch
+ else:
+ raise utils.Unreachable("self={!r} other={!r}".format(self, other))
+
+ def append_event(self, ev):
+ """Create a new KeySequence object with the given QKeyEvent added."""
+ key = ev.key()
+ modifiers = ev.modifiers()
+
+ _assert_plain_key(key)
+ _assert_plain_modifier(modifiers)
+
+ if key == 0x0:
+ raise KeyParseError(None, "Got nil key!")
+
+ # We always remove Qt.GroupSwitchModifier because QKeySequence has no
+ # way to mention that in a binding anyways...
+ modifiers &= ~Qt.GroupSwitchModifier
+
+ # We change Qt.Key_Backtab to Key_Tab here because nobody would
+ # configure "Shift-Backtab" in their config.
+ if modifiers & Qt.ShiftModifier and key == Qt.Key_Backtab:
+ key = Qt.Key_Tab
+
+ # We don't care about a shift modifier with symbols (Shift-: should
+ # match a : binding even though we typed it with a shift on an
+ # US-keyboard)
+ #
+ # However, we *do* care about Shift being involved if we got an
+ # upper-case letter, as Shift-A should match a Shift-A binding, but not
+ # an "a" binding.
+ #
+ # In addition, Shift also *is* relevant when other modifiers are
+ # involved. Shift-Ctrl-X should not be equivalent to Ctrl-X.
+ if (modifiers == Qt.ShiftModifier and
+ _is_printable(ev.key()) and
+ not ev.text().isupper()):
+ modifiers = Qt.KeyboardModifiers()
+
+ # On macOS, swap Ctrl and Meta back
+ # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-51293
+ if utils.is_mac:
+ if modifiers & Qt.ControlModifier and modifiers & Qt.MetaModifier:
+ pass
+ elif modifiers & Qt.ControlModifier:
+ modifiers &= ~Qt.ControlModifier
+ modifiers |= Qt.MetaModifier
+ elif modifiers & Qt.MetaModifier:
+ modifiers &= ~Qt.MetaModifier
+ modifiers |= Qt.ControlModifier
+
+ keys = list(self._iter_keys())
+ keys.append(key | int(modifiers))
+
+ return self.__class__(*keys)
+
+ def strip_modifiers(self):
+ """Strip optional modifiers from keys."""
+ modifiers = Qt.KeypadModifier
+ keys = [key & ~modifiers for key in self._iter_keys()]
+ 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 <Ctrl-x> or xyz and return a KeySequence."""
+ # pylint: disable=protected-access
+ new = cls()
+ strings = list(_parse_keystring(keystr))
+ for sub in utils.chunk(strings, cls._MAX_LEN):
+ sequence = QKeySequence(', '.join(sub))
+ new._sequences.append(sequence)
+
+ if keystr:
+ assert new, keystr
+
+ # pylint: disable=protected-access
+ new._validate(keystr)
+ return new
diff --git a/qutebrowser/keyinput/modeman.py b/qutebrowser/keyinput/modeman.py
index 4e9d78fb0..ffe780333 100644
--- a/qutebrowser/keyinput/modeman.py
+++ b/qutebrowser/keyinput/modeman.py
@@ -25,7 +25,7 @@ import attr
from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QObject, QEvent
from PyQt5.QtWidgets import QApplication
-from qutebrowser.keyinput import modeparsers, keyparser
+from qutebrowser.keyinput import modeparsers
from qutebrowser.config import config
from qutebrowser.commands import cmdexc, cmdutils
from qutebrowser.utils import usertypes, log, objreg, utils
@@ -68,24 +68,30 @@ def init(win_id, parent):
modeman = ModeManager(win_id, parent)
objreg.register('mode-manager', modeman, scope='window', window=win_id)
keyparsers = {
- KM.normal: modeparsers.NormalKeyParser(win_id, modeman),
- KM.hint: modeparsers.HintKeyParser(win_id, modeman),
- KM.insert: keyparser.PassthroughKeyParser(win_id, 'insert', modeman),
- KM.passthrough: keyparser.PassthroughKeyParser(win_id, 'passthrough',
- modeman),
- KM.command: keyparser.PassthroughKeyParser(win_id, 'command', modeman),
- KM.prompt: keyparser.PassthroughKeyParser(win_id, 'prompt', modeman,
- warn=False),
- KM.yesno: modeparsers.PromptKeyParser(win_id, modeman),
- KM.caret: modeparsers.CaretKeyParser(win_id, modeman),
- KM.set_mark: modeparsers.RegisterKeyParser(win_id, KM.set_mark,
- modeman),
- KM.jump_mark: modeparsers.RegisterKeyParser(win_id, KM.jump_mark,
- modeman),
- KM.record_macro: modeparsers.RegisterKeyParser(win_id, KM.record_macro,
- modeman),
- KM.run_macro: modeparsers.RegisterKeyParser(win_id, KM.run_macro,
- modeman),
+ KM.normal:
+ modeparsers.NormalKeyParser(win_id, modeman),
+ KM.hint:
+ modeparsers.HintKeyParser(win_id, modeman),
+ KM.insert:
+ modeparsers.PassthroughKeyParser(win_id, 'insert', modeman),
+ KM.passthrough:
+ modeparsers.PassthroughKeyParser(win_id, 'passthrough', modeman),
+ KM.command:
+ modeparsers.PassthroughKeyParser(win_id, 'command', modeman),
+ KM.prompt:
+ modeparsers.PassthroughKeyParser(win_id, 'prompt', modeman),
+ KM.yesno:
+ modeparsers.PromptKeyParser(win_id, modeman),
+ KM.caret:
+ modeparsers.CaretKeyParser(win_id, modeman),
+ KM.set_mark:
+ modeparsers.RegisterKeyParser(win_id, KM.set_mark, modeman),
+ KM.jump_mark:
+ modeparsers.RegisterKeyParser(win_id, KM.jump_mark, modeman),
+ KM.record_macro:
+ modeparsers.RegisterKeyParser(win_id, KM.record_macro, modeman),
+ KM.run_macro:
+ modeparsers.RegisterKeyParser(win_id, KM.run_macro, modeman),
}
objreg.register('keyparsers', keyparsers, scope='window', window=win_id)
modeman.destroyed.connect(
@@ -149,11 +155,12 @@ class ModeManager(QObject):
def __repr__(self):
return utils.get_repr(self, mode=self.mode)
- def _eventFilter_keypress(self, event):
+ def _handle_keypress(self, event, *, dry_run=False):
"""Handle filtering of KeyPress events.
Args:
event: The KeyPress to examine.
+ dry_run: Don't actually handle the key, only filter it.
Return:
True if event should be filtered, False otherwise.
@@ -163,7 +170,7 @@ class ModeManager(QObject):
if curmode != usertypes.KeyMode.insert:
log.modes.debug("got keypress in mode {} - delegating to "
"{}".format(curmode, utils.qualname(parser)))
- handled = parser.handle(event)
+ match = parser.handle(event, dry_run=dry_run)
is_non_alnum = (
event.modifiers() not in [Qt.NoModifier, Qt.ShiftModifier] or
@@ -171,7 +178,7 @@ class ModeManager(QObject):
forward_unbound_keys = config.val.input.forward_unbound_keys
- if handled:
+ if match:
filter_this = True
elif (parser.passthrough or forward_unbound_keys == 'all' or
(forward_unbound_keys == 'auto' and is_non_alnum)):
@@ -179,20 +186,20 @@ class ModeManager(QObject):
else:
filter_this = True
- if not filter_this:
+ if not filter_this and not dry_run:
self._releaseevents_to_pass.add(KeyEvent.from_event(event))
if curmode != usertypes.KeyMode.insert:
focus_widget = QApplication.instance().focusWidget()
- log.modes.debug("handled: {}, forward_unbound_keys: {}, "
- "passthrough: {}, is_non_alnum: {} --> "
- "filter: {} (focused: {!r})".format(
- handled, forward_unbound_keys,
- parser.passthrough, is_non_alnum, filter_this,
- focus_widget))
+ log.modes.debug("match: {}, forward_unbound_keys: {}, "
+ "passthrough: {}, is_non_alnum: {}, dry_run: {} "
+ "--> filter: {} (focused: {!r})".format(
+ match, forward_unbound_keys,
+ parser.passthrough, is_non_alnum, dry_run,
+ filter_this, focus_widget))
return filter_this
- def _eventFilter_keyrelease(self, event):
+ def _handle_keyrelease(self, event):
"""Handle filtering of KeyRelease events.
Args:
@@ -315,7 +322,7 @@ class ModeManager(QObject):
raise ValueError("Can't leave normal mode!")
self.leave(self.mode, 'leave current')
- def eventFilter(self, event):
+ def handle_event(self, event):
"""Filter all events based on the currently set mode.
Also calls the real keypress handler.
@@ -331,8 +338,10 @@ class ModeManager(QObject):
return False
handlers = {
- QEvent.KeyPress: self._eventFilter_keypress,
- QEvent.KeyRelease: self._eventFilter_keyrelease,
+ QEvent.KeyPress: self._handle_keypress,
+ QEvent.KeyRelease: self._handle_keyrelease,
+ QEvent.ShortcutOverride:
+ functools.partial(self._handle_keypress, dry_run=True),
}
handler = handlers[event.type()]
return handler(event)
diff --git a/qutebrowser/keyinput/modeparsers.py b/qutebrowser/keyinput/modeparsers.py
index b739d38a1..270590fff 100644
--- a/qutebrowser/keyinput/modeparsers.py
+++ b/qutebrowser/keyinput/modeparsers.py
@@ -27,10 +27,11 @@ import traceback
import enum
from PyQt5.QtCore import pyqtSlot, Qt
+from PyQt5.QtGui import QKeySequence
-from qutebrowser.commands import cmdexc
+from qutebrowser.commands import runners, cmdexc
from qutebrowser.config import config
-from qutebrowser.keyinput import keyparser
+from qutebrowser.keyinput import basekeyparser, keyutils
from qutebrowser.utils import usertypes, log, message, objreg, utils
@@ -38,7 +39,26 @@ STARTCHARS = ":/?"
LastPress = enum.Enum('LastPress', ['none', 'filtertext', 'keystring'])
-class NormalKeyParser(keyparser.CommandKeyParser):
+class CommandKeyParser(basekeyparser.BaseKeyParser):
+
+ """KeyChainParser for command bindings.
+
+ Attributes:
+ _commandrunner: CommandRunner instance.
+ """
+
+ def __init__(self, win_id, parent=None, supports_count=None):
+ super().__init__(win_id, parent, supports_count)
+ self._commandrunner = runners.CommandRunner(win_id)
+
+ def execute(self, cmdstr, count=None):
+ try:
+ self._commandrunner.run(cmdstr, count)
+ except cmdexc.Error as e:
+ message.error(str(e), stack=traceback.format_exc())
+
+
+class NormalKeyParser(CommandKeyParser):
"""KeyParser for normal mode with added STARTCHARS detection and more.
@@ -47,8 +67,7 @@ class NormalKeyParser(keyparser.CommandKeyParser):
"""
def __init__(self, win_id, parent=None):
- super().__init__(win_id, parent, supports_count=True,
- supports_chains=True)
+ super().__init__(win_id, parent, supports_count=True)
self._read_config('normal')
self._partial_timer = usertypes.Timer(self, 'partial-match')
self._partial_timer.setSingleShot(True)
@@ -59,11 +78,13 @@ class NormalKeyParser(keyparser.CommandKeyParser):
def __repr__(self):
return utils.get_repr(self)
- def _handle_single_key(self, e):
- """Override _handle_single_key to abort if the key is a startchar.
+ def handle(self, e, *, dry_run=False):
+ """Override to abort if the key is a startchar.
Args:
e: the KeyPressEvent from Qt.
+ dry_run: Don't actually execute anything, only check whether there
+ would be a match.
Return:
A self.Match member.
@@ -72,9 +93,11 @@ class NormalKeyParser(keyparser.CommandKeyParser):
if self._inhibited:
self._debug_log("Ignoring key '{}', because the normal mode is "
"currently inhibited.".format(txt))
- return self.Match.none
- match = super()._handle_single_key(e)
- if match == self.Match.partial:
+ return QKeySequence.NoMatch
+
+ match = super().handle(e, dry_run=dry_run)
+
+ if match == QKeySequence.PartialMatch and not dry_run:
timeout = config.val.input.partial_timeout
if timeout != 0:
self._partial_timer.setInterval(timeout)
@@ -96,9 +119,9 @@ class NormalKeyParser(keyparser.CommandKeyParser):
def _clear_partial_match(self):
"""Clear a partial keystring after a timeout."""
self._debug_log("Clearing partial keystring {}".format(
- self._keystring))
- self._keystring = ''
- self.keystring_updated.emit(self._keystring)
+ self._sequence))
+ self._sequence = keyutils.KeySequence()
+ self.keystring_updated.emit(str(self._sequence))
@pyqtSlot()
def _clear_inhibited(self):
@@ -123,22 +146,48 @@ class NormalKeyParser(keyparser.CommandKeyParser):
pass
-class PromptKeyParser(keyparser.CommandKeyParser):
+class PassthroughKeyParser(CommandKeyParser):
+
+ """KeyChainParser which passes through normal keys.
+
+ Used for insert/passthrough modes.
+
+ Attributes:
+ _mode: The mode this keyparser is for.
+ """
+
+ do_log = False
+ passthrough = True
+
+ def __init__(self, win_id, mode, parent=None):
+ """Constructor.
+
+ Args:
+ mode: The mode this keyparser is for.
+ parent: Qt parent.
+ warn: Whether to warn if an ignored key was bound.
+ """
+ super().__init__(win_id, parent)
+ self._read_config(mode)
+ self._mode = mode
+
+ def __repr__(self):
+ return utils.get_repr(self, mode=self._mode)
+
+
+class PromptKeyParser(CommandKeyParser):
"""KeyParser for yes/no prompts."""
def __init__(self, win_id, parent=None):
- super().__init__(win_id, parent, supports_count=False,
- supports_chains=True)
- # We don't want an extra section for this in the config, so we just
- # abuse the prompt section.
- self._read_config('prompt')
+ super().__init__(win_id, parent, supports_count=False)
+ self._read_config('yesno')
def __repr__(self):
return utils.get_repr(self)
-class HintKeyParser(keyparser.CommandKeyParser):
+class HintKeyParser(CommandKeyParser):
"""KeyChainParser for hints.
@@ -148,15 +197,14 @@ class HintKeyParser(keyparser.CommandKeyParser):
"""
def __init__(self, win_id, parent=None):
- super().__init__(win_id, parent, supports_count=False,
- supports_chains=True)
+ super().__init__(win_id, parent, supports_count=False)
self._filtertext = ''
self._last_press = LastPress.none
self._read_config('hint')
self.keystring_updated.connect(self.on_keystring_updated)
- def _handle_special_key(self, e):
- """Override _handle_special_key to handle string filtering.
+ def _handle_filter_key(self, e):
+ """Handle keys for string filtering.
Return True if the keypress has been handled, and False if not.
@@ -164,78 +212,75 @@ class HintKeyParser(keyparser.CommandKeyParser):
e: the KeyPressEvent from Qt.
Return:
- True if event has been handled, False otherwise.
+ A QKeySequence match.
"""
- log.keyboard.debug("Got special key 0x{:x} text {}".format(
+ log.keyboard.debug("Got filter key 0x{:x} text {}".format(
e.key(), e.text()))
hintmanager = objreg.get('hintmanager', scope='tab',
window=self._win_id, tab='current')
if e.key() == Qt.Key_Backspace:
log.keyboard.debug("Got backspace, mode {}, filtertext '{}', "
- "keystring '{}'".format(self._last_press,
- self._filtertext,
- self._keystring))
+ "sequence '{}'".format(self._last_press,
+ self._filtertext,
+ self._sequence))
if self._last_press == LastPress.filtertext and self._filtertext:
self._filtertext = self._filtertext[:-1]
hintmanager.filter_hints(self._filtertext)
- return True
- elif self._last_press == LastPress.keystring and self._keystring:
- self._keystring = self._keystring[:-1]
- self.keystring_updated.emit(self._keystring)
- if not self._keystring and self._filtertext:
+ return QKeySequence.ExactMatch
+ elif self._last_press == LastPress.keystring and self._sequence:
+ self._sequence = self._sequence[:-1]
+ self.keystring_updated.emit(str(self._sequence))
+ if not self._sequence and self._filtertext:
# Switch back to hint filtering mode (this can happen only
# in numeric mode after the number has been deleted).
hintmanager.filter_hints(self._filtertext)
self._last_press = LastPress.filtertext
- return True
+ return QKeySequence.ExactMatch
else:
- return super()._handle_special_key(e)
+ return QKeySequence.NoMatch
elif hintmanager.current_mode() != 'number':
- return super()._handle_special_key(e)
+ return QKeySequence.NoMatch
elif not e.text():
- return super()._handle_special_key(e)
+ return QKeySequence.NoMatch
else:
self._filtertext += e.text()
hintmanager.filter_hints(self._filtertext)
self._last_press = LastPress.filtertext
- return True
+ return QKeySequence.ExactMatch
- def handle(self, e):
+ def handle(self, e, *, dry_run=False):
"""Handle a new keypress and call the respective handlers.
Args:
e: the KeyPressEvent from Qt
+ dry_run: Don't actually execute anything, only check whether there
+ would be a match.
Returns:
True if the match has been handled, False otherwise.
"""
- match = self._handle_single_key(e)
- if match == self.Match.partial:
- self.keystring_updated.emit(self._keystring)
+ dry_run_match = super().handle(e, dry_run=True)
+ if dry_run:
+ return dry_run_match
+
+ if keyutils.is_special(e.key(), e.modifiers()):
+ log.keyboard.debug("Got special key, clearing keychain")
+ self.clear_keystring()
+
+ assert not dry_run
+ match = super().handle(e)
+
+ if match == QKeySequence.PartialMatch:
self._last_press = LastPress.keystring
- return True
- elif match == self.Match.definitive:
+ elif match == QKeySequence.ExactMatch:
self._last_press = LastPress.none
- return True
- elif match == self.Match.other:
- return None
- elif match == self.Match.none:
+ elif match == QKeySequence.NoMatch:
# We couldn't find a keychain so we check if it's a special key.
- return self._handle_special_key(e)
+ return self._handle_filter_key(e)
else:
raise ValueError("Got invalid match type {}!".format(match))
- def execute(self, cmdstr, keytype, count=None):
- """Handle a completed keychain."""
- if not isinstance(keytype, self.Type):
- raise TypeError("Type {} is no Type member!".format(keytype))
- if keytype == self.Type.chain:
- hintmanager = objreg.get('hintmanager', scope='tab',
- window=self._win_id, tab='current')
- hintmanager.handle_partial_key(cmdstr)
- else:
- # execute as command
- super().execute(cmdstr, keytype, count)
+ return match
def update_bindings(self, strings, preserve_filter=False):
"""Update bindings when the hint strings changed.
@@ -245,7 +290,9 @@ class HintKeyParser(keyparser.CommandKeyParser):
preserve_filter: Whether to keep the current value of
`self._filtertext`.
"""
- self.bindings = {s: s for s in strings}
+ self._read_config()
+ self.bindings.update({keyutils.KeySequence.parse(s):
+ 'follow-hint -s ' + s for s in strings})
if not preserve_filter:
self._filtertext = ''
@@ -257,19 +304,18 @@ class HintKeyParser(keyparser.CommandKeyParser):
hintmanager.handle_partial_key(keystr)
-class CaretKeyParser(keyparser.CommandKeyParser):
+class CaretKeyParser(CommandKeyParser):
"""KeyParser for caret mode."""
passthrough = True
def __init__(self, win_id, parent=None):
- super().__init__(win_id, parent, supports_count=True,
- supports_chains=True)
+ super().__init__(win_id, parent, supports_count=True)
self._read_config('caret')
-class RegisterKeyParser(keyparser.CommandKeyParser):
+class RegisterKeyParser(CommandKeyParser):
"""KeyParser for modes that record a register key.
@@ -279,28 +325,30 @@ class RegisterKeyParser(keyparser.CommandKeyParser):
"""
def __init__(self, win_id, mode, parent=None):
- super().__init__(win_id, parent, supports_count=False,
- supports_chains=False)
+ super().__init__(win_id, parent, supports_count=False)
self._mode = mode
self._read_config('register')
- def handle(self, e):
+ def handle(self, e, *, dry_run=False):
"""Override handle to always match the next key and use the register.
Args:
e: the KeyPressEvent from Qt.
+ dry_run: Don't actually execute anything, only check whether there
+ would be a match.
Return:
True if event has been handled, False otherwise.
"""
- if super().handle(e):
- return True
+ match = super().handle(e, dry_run=dry_run)
+ if match or dry_run:
+ return match
- key = e.text()
-
- if key == '' or utils.keyevent_to_string(e) is None:
+ if keyutils.is_special(e.key(), e.modifiers()):
# this is not a proper register key, let it pass and keep going
- return False
+ return QKeySequence.NoMatch
+
+ key = e.text()
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=self._win_id)
@@ -322,5 +370,4 @@ class RegisterKeyParser(keyparser.CommandKeyParser):
message.error(str(err), stack=traceback.format_exc())
self.request_leave.emit(self._mode, "valid register key", True)
-
- return True
+ return QKeySequence.ExactMatch