From ddcb5445a29a794dad1d1518e5fda0a26f5b6352 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 28 Dec 2017 20:31:42 +0100 Subject: [PATCH 001/135] Initial refactoring for new key parsing --- qutebrowser/config/configtypes.py | 7 ++- qutebrowser/keyinput/basekeyparser.py | 80 +++++++++++---------------- qutebrowser/utils/utils.py | 22 ++++++-- 3 files changed, 53 insertions(+), 56 deletions(-) diff --git a/qutebrowser/config/configtypes.py b/qutebrowser/config/configtypes.py index 71c32f59e..e7e96cc44 100644 --- a/qutebrowser/config/configtypes.py +++ b/qutebrowser/config/configtypes.py @@ -1651,6 +1651,7 @@ class Key(BaseType): self._basic_py_validation(value, str) if not value: return None - if utils.is_special_key(value): - value = '<{}>'.format(utils.normalize_keystr(value[1:-1])) - return value + #if utils.is_special_key(value): + # value = '<{}>'.format(utils.normalize_keystr(value[1:-1])) + #return value + return utils.parse_keystring(value) diff --git a/qutebrowser/keyinput/basekeyparser.py b/qutebrowser/keyinput/basekeyparser.py index b435ac52c..ef902c82a 100644 --- a/qutebrowser/keyinput/basekeyparser.py +++ b/qutebrowser/keyinput/basekeyparser.py @@ -24,6 +24,7 @@ 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 @@ -53,7 +54,6 @@ class BaseKeyParser(QObject): Attributes: bindings: Bound key bindings - special_bindings: Bound special bindings (). _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. @@ -76,7 +76,6 @@ 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, @@ -91,7 +90,6 @@ class BaseKeyParser(QObject): 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): @@ -118,6 +116,7 @@ class BaseKeyParser(QObject): Return: True if event has been handled, False otherwise. """ + # FIXME remove? binding = utils.keyevent_to_string(e) if binding is None: self._debug_log("Ignoring only-modifier keyeevent.") @@ -161,8 +160,8 @@ class BaseKeyParser(QObject): count = None return count, cmd_input - def _handle_single_key(self, e): - """Handle a new keypress with a single key (no modifiers). + def _handle_key(self, e): + """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 @@ -186,11 +185,11 @@ class BaseKeyParser(QObject): if (not txt) or is_control_char: self._debug_log("Ignoring, no text char") - return self.Match.none + return QKeySequence.NoMatch count, cmd_input = self._split_count(self._keystring + txt) match, binding = self._match_key(cmd_input) - if match == self.Match.none: + if match == QKeySequence.NoMatch: mappings = config.val.bindings.key_mappings mapped = mappings.get(txt, None) if mapped is not None: @@ -199,19 +198,19 @@ class BaseKeyParser(QObject): match, binding = self._match_key(cmd_input) self._keystring += txt - if match == self.Match.definitive: + if match == QKeySequence.ExactMatch: self._debug_log("Definitive match for '{}'.".format( self._keystring)) self.clear_keystring() self.execute(binding, self.Type.chain, count) - elif match == self.Match.partial: + elif match == QKeySequence.PartialMatch: self._debug_log("No match for '{}' (added {})".format( self._keystring, txt)) - elif match == self.Match.none: + elif match == QKeySequence.NoMatch: self._debug_log("Giving up with '{}', no matches".format( self._keystring)) self.clear_keystring() - elif match == self.Match.other: + elif match is None: pass else: raise utils.Unreachable("Invalid match value {!r}".format(match)) @@ -231,29 +230,14 @@ class BaseKeyParser(QObject): """ 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) + return (None, None) + + for seq, cmd in self.bindings.items(): + match = seq.matches(utils.parse_keystring(cmd_input)) + if match != QKeySequence.NoMatch: + return (match, cmd) + + return (QKeySequence.NoMatch, None) def handle(self, e): """Handle a new keypress and call the respective handlers. @@ -264,15 +248,17 @@ class BaseKeyParser(QObject): Return: True if the event was handled, False otherwise. """ - handled = self._handle_special_key(e) + match = self._handle_key(e) + + # FIXME + # if handled or not self._supports_chains: + # return handled - 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 != QKeySequence.NoMatch @config.change_filter('bindings') def _on_config_changed(self): @@ -295,22 +281,18 @@ 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 cmd - self._parse_key_command(modename, key, cmd) + self.bindings[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)) + # FIXME + # 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): """Handle a completed keychain. diff --git a/qutebrowser/utils/utils.py b/qutebrowser/utils/utils.py index f29db578e..eb6ebe901 100644 --- a/qutebrowser/utils/utils.py +++ b/qutebrowser/utils/utils.py @@ -511,12 +511,26 @@ def _parse_single_key(keystr): return KeyInfo(key, modifiers, text) +def _parse_keystring(keystr): + key = '' + special = False + for c in keystr: + if c == '>': + yield normalize_keystr(key) + key = '' + special = False + elif c == '<': + special = True + elif special: + key += c + else: + yield 'Shift+' + c if c.isupper() else c + + def parse_keystring(keystr): """Parse a keystring like or xyz and return a KeyInfo list.""" - if is_special_key(keystr): - return [_parse_single_key(keystr)] - else: - return [_parse_single_key(char) for char in keystr] + s = ', '.join(_parse_keystring(keystr)) + return QKeySequence(s) def normalize_keystr(keystr): From a8aaf01ff0fe2597c856c8efa5a192f640c42e6d Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 28 Dec 2017 23:03:59 +0100 Subject: [PATCH 002/135] Fix some more stuff (and break some :D) --- qutebrowser/config/config.py | 16 +++++++++------- qutebrowser/config/configcommands.py | 8 ++++---- qutebrowser/keyinput/basekeyparser.py | 22 +++++++++++++--------- qutebrowser/keyinput/keyparser.py | 3 ++- qutebrowser/misc/keyhintwidget.py | 1 - qutebrowser/misc/miscwidgets.py | 2 -- qutebrowser/utils/utils.py | 17 ++++++++++++++++- 7 files changed, 44 insertions(+), 25 deletions(-) diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py index c170d0705..1f910f549 100644 --- a/qutebrowser/config/config.py +++ b/qutebrowser/config/config.py @@ -136,9 +136,10 @@ class KeyConfig: """Make sure the given mode exists and normalize the key.""" if mode not in configdata.DATA['bindings.default'].default: raise configexc.KeybindingError("Invalid mode {}!".format(mode)) - if utils.is_special_key(key): - # , , and should be considered equivalent - return utils.normalize_keystr(key) + # FIXME needed? + # if utils.is_special_key(key): + # # , , and should be considered equivalent + # return utils.normalize_keystr(key) return key def get_bindings_for(self, mode): @@ -160,10 +161,11 @@ class KeyConfig: cmd = cmd.strip() cmd_to_keys.setdefault(cmd, []) # put special bindings last - if utils.is_special_key(key): - cmd_to_keys[cmd].append(key) - else: - cmd_to_keys[cmd].insert(0, key) + # FIXME update + # if utils.is_special_key(key): + # cmd_to_keys[cmd].append(key) + # else: + cmd_to_keys[cmd].insert(0, key.toString()) return cmd_to_keys def get_command(self, key, mode, default=False): diff --git a/qutebrowser/config/configcommands.py b/qutebrowser/config/configcommands.py index 7d9adb475..711fe861c 100644 --- a/qutebrowser/config/configcommands.py +++ b/qutebrowser/config/configcommands.py @@ -117,10 +117,10 @@ class ConfigCommands: return # No --default -> print binding - if utils.is_special_key(key): - # self._keyconfig.get_command does this, but we also need it - # normalized for the output below - key = utils.normalize_keystr(key) + #if utils.is_special_key(key): + # # self._keyconfig.get_command does this, but we also need it + # # normalized for the output below + # key = utils.normalize_keystr(key) with self._handle_config_error(): cmd = self._keyconfig.get_command(key, mode) if cmd is None: diff --git a/qutebrowser/keyinput/basekeyparser.py b/qutebrowser/keyinput/basekeyparser.py index ef902c82a..95149df3e 100644 --- a/qutebrowser/keyinput/basekeyparser.py +++ b/qutebrowser/keyinput/basekeyparser.py @@ -173,20 +173,24 @@ class BaseKeyParser(QObject): Return: A self.Match member. """ - txt = e.text() key = e.key() + txt = utils.keyevent_to_string(e) self._debug_log("Got key: 0x{:x} / text: '{}'".format(key, txt)) - 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: + if txt is None: self._debug_log("Ignoring, no text char") return QKeySequence.NoMatch + # 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 QKeySequence.NoMatch + count, cmd_input = self._split_count(self._keystring + txt) match, binding = self._match_key(cmd_input) if match == QKeySequence.NoMatch: @@ -233,7 +237,7 @@ class BaseKeyParser(QObject): return (None, None) for seq, cmd in self.bindings.items(): - match = seq.matches(utils.parse_keystring(cmd_input)) + match = seq.matches(cmd_input) if match != QKeySequence.NoMatch: return (match, cmd) diff --git a/qutebrowser/keyinput/keyparser.py b/qutebrowser/keyinput/keyparser.py index 8fcb53035..3df8ab193 100644 --- a/qutebrowser/keyinput/keyparser.py +++ b/qutebrowser/keyinput/keyparser.py @@ -56,7 +56,8 @@ class PassthroughKeyParser(CommandKeyParser): _mode: The mode this keyparser is for. """ - do_log = False + # FIXME + # do_log = False passthrough = True def __init__(self, win_id, mode, parent=None, warn=True): diff --git a/qutebrowser/misc/keyhintwidget.py b/qutebrowser/misc/keyhintwidget.py index 46d1d3f24..600095e0f 100644 --- a/qutebrowser/misc/keyhintwidget.py +++ b/qutebrowser/misc/keyhintwidget.py @@ -106,7 +106,6 @@ class KeyHintView(QLabel): bindings_dict = config.key_instance.get_bindings_for(modename) bindings = [(k, v) for (k, v) in sorted(bindings_dict.items()) if k.startswith(prefix) and - not utils.is_special_key(k) and not blacklisted(k) and (takes_count(v) or not countstr)] diff --git a/qutebrowser/misc/miscwidgets.py b/qutebrowser/misc/miscwidgets.py index 5d3e9b5ca..c52600bb1 100644 --- a/qutebrowser/misc/miscwidgets.py +++ b/qutebrowser/misc/miscwidgets.py @@ -293,8 +293,6 @@ class FullscreenNotification(QLabel): bindings = all_bindings.get('fullscreen --leave') if bindings: key = bindings[0] - if utils.is_special_key(key): - key = key.strip('<>').capitalize() self.setText("Press {} to exit fullscreen.".format(key)) else: self.setText("Page is now fullscreen.") diff --git a/qutebrowser/utils/utils.py b/qutebrowser/utils/utils.py index eb6ebe901..29d9c6671 100644 --- a/qutebrowser/utils/utils.py +++ b/qutebrowser/utils/utils.py @@ -33,6 +33,7 @@ import functools import contextlib import socket import shlex +import unicodedata import attr from PyQt5.QtCore import Qt, QUrl @@ -418,10 +419,22 @@ def keyevent_to_string(e): return None mod = e.modifiers() parts = [] + for (mask, s) in modmask2str.items(): if mod & mask and s not in parts: parts.append(s) - parts.append(key_to_string(e.key())) + + key_string = key_to_string(e.key()) + if len(key_string) == 1: + category = unicodedata.category(key_string) + is_control_char = (category == 'Cc') + else: + is_control_char = False + + if e.modifiers() == Qt.ShiftModifier and not is_control_char: + parts = [] + + parts.append(key_string) return normalize_keystr('+'.join(parts)) @@ -466,6 +479,8 @@ def is_special_key(keystr): def _parse_single_key(keystr): """Convert a single key string to a (Qt.Key, Qt.Modifiers, text) tuple.""" + # FIXME remove + if is_special_key(keystr): # Special key keystr = keystr[1:-1] From 55803afbd2da0e4a268c9fc214e95fdb82d9498c Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 28 Dec 2017 23:21:01 +0100 Subject: [PATCH 003/135] Fix matching --- qutebrowser/keyinput/basekeyparser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutebrowser/keyinput/basekeyparser.py b/qutebrowser/keyinput/basekeyparser.py index 95149df3e..bd52dec6a 100644 --- a/qutebrowser/keyinput/basekeyparser.py +++ b/qutebrowser/keyinput/basekeyparser.py @@ -237,7 +237,7 @@ class BaseKeyParser(QObject): return (None, None) for seq, cmd in self.bindings.items(): - match = seq.matches(cmd_input) + match = cmd_input.matches(seq) if match != QKeySequence.NoMatch: return (match, cmd) From 26fdc129d33cd3af9d68798971e9a7803f78c453 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 28 Dec 2017 23:23:38 +0100 Subject: [PATCH 004/135] Split off counts --- qutebrowser/keyinput/basekeyparser.py | 41 ++++++++------------------- 1 file changed, 12 insertions(+), 29 deletions(-) diff --git a/qutebrowser/keyinput/basekeyparser.py b/qutebrowser/keyinput/basekeyparser.py index bd52dec6a..67dea1a15 100644 --- a/qutebrowser/keyinput/basekeyparser.py +++ b/qutebrowser/keyinput/basekeyparser.py @@ -84,6 +84,7 @@ class BaseKeyParser(QObject): self._win_id = win_id self._modename = None self._keystring = '' + self._count = '' if supports_count is None: supports_count = supports_chains self._supports_count = supports_count @@ -139,27 +140,6 @@ class BaseKeyParser(QObject): self.clear_keystring() return True - def _split_count(self, keystring): - """Get count and command from the current keystring. - - Args: - keystring: The key string to split. - - Return: - A (count, command) tuple. - """ - 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_key(self, e): """Handle a new keypress. @@ -191,14 +171,19 @@ class BaseKeyParser(QObject): # self._debug_log("Ignoring, no text char") # return QKeySequence.NoMatch - count, cmd_input = self._split_count(self._keystring + txt) + if txt.isdigit(): + assert len(txt) == 1, txt + self._count += txt + return None + + cmd_input = self._keystring + txt match, binding = self._match_key(cmd_input) if match == QKeySequence.NoMatch: 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) + cmd_input = self._keystring + txt match, binding = self._match_key(cmd_input) self._keystring += txt @@ -206,6 +191,7 @@ class BaseKeyParser(QObject): self._debug_log("Definitive match for '{}'.".format( self._keystring)) self.clear_keystring() + count = int(self._count) if self._count else None self.execute(binding, self.Type.chain, count) elif match == QKeySequence.PartialMatch: self._debug_log("No match for '{}' (added {})".format( @@ -214,8 +200,6 @@ class BaseKeyParser(QObject): self._debug_log("Giving up with '{}', no matches".format( self._keystring)) self.clear_keystring() - elif match is None: - pass else: raise utils.Unreachable("Invalid match value {!r}".format(match)) return match @@ -232,9 +216,7 @@ class BaseKeyParser(QObject): 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 (None, None) + assert cmd_input for seq, cmd in self.bindings.items(): match = cmd_input.matches(seq) @@ -260,7 +242,7 @@ class BaseKeyParser(QObject): # don't emit twice if the keystring was cleared in self.clear_keystring if self._keystring: - self.keystring_updated.emit(self._keystring) + self.keystring_updated.emit(self._count + self._keystring) return match != QKeySequence.NoMatch @@ -314,4 +296,5 @@ class BaseKeyParser(QObject): self._debug_log("discarding keystring '{}'.".format( self._keystring)) self._keystring = '' + self._count = '' self.keystring_updated.emit(self._keystring) From 8478a1ea3d0019db8d6c2e9baf461366a8941f36 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 28 Dec 2017 23:33:06 +0100 Subject: [PATCH 005/135] Remove _handle_special_key --- qutebrowser/keyinput/basekeyparser.py | 34 --------------------------- 1 file changed, 34 deletions(-) diff --git a/qutebrowser/keyinput/basekeyparser.py b/qutebrowser/keyinput/basekeyparser.py index 67dea1a15..6220e2e33 100644 --- a/qutebrowser/keyinput/basekeyparser.py +++ b/qutebrowser/keyinput/basekeyparser.py @@ -106,40 +106,6 @@ class BaseKeyParser(QObject): if self.do_log: log.keyboard.debug(message) - def _handle_special_key(self, e): - """Handle a new keypress with special keys (). - - 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. - """ - # FIXME remove? - 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 _handle_key(self, e): """Handle a new keypress. From a565b77bf0ec60c7bde7aecc9ddebfbe0d27be51 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 28 Dec 2017 23:47:23 +0100 Subject: [PATCH 006/135] Switch from string to QKeySequence --- qutebrowser/keyinput/basekeyparser.py | 44 ++++++++++++++------------- qutebrowser/misc/keyhintwidget.py | 3 +- 2 files changed, 25 insertions(+), 22 deletions(-) diff --git a/qutebrowser/keyinput/basekeyparser.py b/qutebrowser/keyinput/basekeyparser.py index 6220e2e33..33a1f808a 100644 --- a/qutebrowser/keyinput/basekeyparser.py +++ b/qutebrowser/keyinput/basekeyparser.py @@ -57,7 +57,7 @@ class BaseKeyParser(QObject): _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 @@ -83,7 +83,7 @@ class BaseKeyParser(QObject): super().__init__(parent) self._win_id = win_id self._modename = None - self._keystring = '' + self._sequence = QKeySequence() self._count = '' if supports_count is None: supports_count = supports_chains @@ -142,39 +142,41 @@ class BaseKeyParser(QObject): self._count += txt return None - cmd_input = self._keystring + txt - match, binding = self._match_key(cmd_input) + sequence = QKeySequence(*self._sequence, e.modifiers() | e.key()) + match, binding = self._match_key(sequence) if match == QKeySequence.NoMatch: mappings = config.val.bindings.key_mappings mapped = mappings.get(txt, None) if mapped is not None: + # FIXME + raise Exception txt = mapped - cmd_input = self._keystring + txt - match, binding = self._match_key(cmd_input) + sequence = QKeySequence(*self._sequence, e.modifiers() | e.key()) + match, binding = self._match_key(sequence) - self._keystring += txt + self._sequence = QKeySequence(*self._sequence, e.modifiers() | e.key()) if match == QKeySequence.ExactMatch: self._debug_log("Definitive match for '{}'.".format( - self._keystring)) - self.clear_keystring() + self._sequence.toString())) count = int(self._count) if self._count else None + self.clear_keystring() self.execute(binding, self.Type.chain, count) elif match == QKeySequence.PartialMatch: self._debug_log("No match for '{}' (added {})".format( - self._keystring, txt)) + self._sequence.toString(), txt)) elif match == QKeySequence.NoMatch: self._debug_log("Giving up with '{}', no matches".format( - self._keystring)) + self._sequence.toString())) self.clear_keystring() else: raise utils.Unreachable("Invalid match value {!r}".format(match)) return match - def _match_key(self, cmd_input): + def _match_key(self, sequence): """Try to match a given keystring with any bound keychain. Args: - cmd_input: The command string to find. + sequence: The command string to find. Return: A tuple (matchtype, binding). @@ -182,10 +184,10 @@ class BaseKeyParser(QObject): binding: - None with Match.partial/Match.none. - The found binding with Match.definitive. """ - assert cmd_input + assert sequence for seq, cmd in self.bindings.items(): - match = cmd_input.matches(seq) + match = sequence.matches(seq) if match != QKeySequence.NoMatch: return (match, cmd) @@ -207,8 +209,8 @@ class BaseKeyParser(QObject): # return handled # don't emit twice if the keystring was cleared in self.clear_keystring - if self._keystring: - self.keystring_updated.emit(self._count + self._keystring) + if self._sequence: + self.keystring_updated.emit(self._count + self._sequence.toString()) return match != QKeySequence.NoMatch @@ -258,9 +260,9 @@ class BaseKeyParser(QObject): def clear_keystring(self): """Clear the currently entered key sequence.""" - if self._keystring: + if self._sequence: self._debug_log("discarding keystring '{}'.".format( - self._keystring)) - self._keystring = '' + self._sequence.toString())) + self._sequence = QKeySequence() self._count = '' - self.keystring_updated.emit(self._keystring) + self.keystring_updated.emit('') diff --git a/qutebrowser/misc/keyhintwidget.py b/qutebrowser/misc/keyhintwidget.py index 600095e0f..d70ddf23d 100644 --- a/qutebrowser/misc/keyhintwidget.py +++ b/qutebrowser/misc/keyhintwidget.py @@ -30,6 +30,7 @@ import re from PyQt5.QtWidgets import QLabel, QSizePolicy from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt +from PyQt5.QtGui import QKeySequence from qutebrowser.config import config from qutebrowser.utils import utils, usertypes @@ -105,7 +106,7 @@ class KeyHintView(QLabel): bindings_dict = config.key_instance.get_bindings_for(modename) bindings = [(k, v) for (k, v) in sorted(bindings_dict.items()) - if k.startswith(prefix) and + if k.matches(QKeySequence(prefix)) and # FIXME not blacklisted(k) and (takes_count(v) or not countstr)] From 600919a23ad9a811eeb1d3aaa43f596ad832c543 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 29 Dec 2017 00:53:37 +0100 Subject: [PATCH 007/135] Add a custom KeySequence class --- qutebrowser/keyinput/basekeyparser.py | 21 +++++----- qutebrowser/keyinput/sequence.py | 58 +++++++++++++++++++++++++++ qutebrowser/misc/keyhintwidget.py | 3 +- qutebrowser/utils/utils.py | 5 ++- 4 files changed, 74 insertions(+), 13 deletions(-) create mode 100644 qutebrowser/keyinput/sequence.py diff --git a/qutebrowser/keyinput/basekeyparser.py b/qutebrowser/keyinput/basekeyparser.py index 33a1f808a..14a7ccc6f 100644 --- a/qutebrowser/keyinput/basekeyparser.py +++ b/qutebrowser/keyinput/basekeyparser.py @@ -28,6 +28,7 @@ from PyQt5.QtGui import QKeySequence from qutebrowser.config import config from qutebrowser.utils import usertypes, log, utils +from qutebrowser.keyinput import sequence class BaseKeyParser(QObject): @@ -83,7 +84,7 @@ class BaseKeyParser(QObject): super().__init__(parent) self._win_id = win_id self._modename = None - self._sequence = QKeySequence() + self._sequence = sequence.KeySequence() self._count = '' if supports_count is None: supports_count = supports_chains @@ -142,7 +143,7 @@ class BaseKeyParser(QObject): self._count += txt return None - sequence = QKeySequence(*self._sequence, e.modifiers() | e.key()) + sequence = self._sequence.append_event(e) match, binding = self._match_key(sequence) if match == QKeySequence.NoMatch: mappings = config.val.bindings.key_mappings @@ -151,22 +152,22 @@ class BaseKeyParser(QObject): # FIXME raise Exception txt = mapped - sequence = QKeySequence(*self._sequence, e.modifiers() | e.key()) + sequence = self._sequence.append_event(e) match, binding = self._match_key(sequence) - self._sequence = QKeySequence(*self._sequence, e.modifiers() | e.key()) + self._sequence = self._sequence.append_event(e) if match == QKeySequence.ExactMatch: self._debug_log("Definitive match for '{}'.".format( - self._sequence.toString())) + self._sequence)) count = int(self._count) if self._count else None self.clear_keystring() self.execute(binding, self.Type.chain, count) elif match == QKeySequence.PartialMatch: self._debug_log("No match for '{}' (added {})".format( - self._sequence.toString(), txt)) + self._sequence, txt)) elif match == QKeySequence.NoMatch: self._debug_log("Giving up with '{}', no matches".format( - self._sequence.toString())) + self._sequence)) self.clear_keystring() else: raise utils.Unreachable("Invalid match value {!r}".format(match)) @@ -210,7 +211,7 @@ class BaseKeyParser(QObject): # don't emit twice if the keystring was cleared in self.clear_keystring if self._sequence: - self.keystring_updated.emit(self._count + self._sequence.toString()) + self.keystring_updated.emit(self._count + str(self._sequence)) return match != QKeySequence.NoMatch @@ -262,7 +263,7 @@ class BaseKeyParser(QObject): """Clear the currently entered key sequence.""" if self._sequence: self._debug_log("discarding keystring '{}'.".format( - self._sequence.toString())) - self._sequence = QKeySequence() + self._sequence)) + self._sequence = sequence.KeySequence() self._count = '' self.keystring_updated.emit('') diff --git a/qutebrowser/keyinput/sequence.py b/qutebrowser/keyinput/sequence.py new file mode 100644 index 000000000..7db4f76a7 --- /dev/null +++ b/qutebrowser/keyinput/sequence.py @@ -0,0 +1,58 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2014-2017 Florian Bruhin (The Compiler) +# +# 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 . + +"""Our own QKeySequence-like class and related utilities.""" + +from PyQt5.QtGui import QKeySequence + +from qutebrowser.utils import utils + + +class KeySequence: + + def __init__(self, *args): + self._sequence = QKeySequence(*args) + + def __str__(self): + return self._sequence.toString() + + def __repr__(self): + return utils.get_repr(self, keys=str(self)) + + def __lt__(self, other): + return self._sequence < other._sequence + + def __gt__(self, other): + return self._sequence > other._sequence + + def __eq__(self, other): + return self._sequence == other._sequence + + def __ne__(self, other): + return self._sequence != other._sequence + + def __hash__(self): + return hash(self._sequence) + + def matches(self, other): + # pylint: disable=protected-access + return self._sequence.matches(other._sequence) + + def append_event(self, ev): + return self.__class__(*self._sequence, ev.modifiers() | ev.key()) diff --git a/qutebrowser/misc/keyhintwidget.py b/qutebrowser/misc/keyhintwidget.py index d70ddf23d..d4ed43141 100644 --- a/qutebrowser/misc/keyhintwidget.py +++ b/qutebrowser/misc/keyhintwidget.py @@ -35,6 +35,7 @@ from PyQt5.QtGui import QKeySequence from qutebrowser.config import config from qutebrowser.utils import utils, usertypes from qutebrowser.commands import cmdutils +from qutebrowser.keyinput import sequence class KeyHintView(QLabel): @@ -106,7 +107,7 @@ class KeyHintView(QLabel): bindings_dict = config.key_instance.get_bindings_for(modename) bindings = [(k, v) for (k, v) in sorted(bindings_dict.items()) - if k.matches(QKeySequence(prefix)) and # FIXME + if k.matches(sequence.KeySequence(prefix)) and # FIXME not blacklisted(k) and (takes_count(v) or not countstr)] diff --git a/qutebrowser/utils/utils.py b/qutebrowser/utils/utils.py index 29d9c6671..a11c67bc4 100644 --- a/qutebrowser/utils/utils.py +++ b/qutebrowser/utils/utils.py @@ -50,6 +50,7 @@ except ImportError: # pragma: no cover import qutebrowser from qutebrowser.utils import qtutils, log, debug +from qutebrowser.keyinput import sequence fake_clipboard = None @@ -491,7 +492,7 @@ def _parse_single_key(keystr): raise KeyParseError(keystr, "Expecting either a single key or a " " like keybinding.") - seq = QKeySequence(normalize_keystr(keystr), QKeySequence.PortableText) + seq = sequence.KeySequence(normalize_keystr(keystr), QKeySequence.PortableText) if len(seq) != 1: raise KeyParseError(keystr, "Got {} keys instead of 1.".format( len(seq))) @@ -545,7 +546,7 @@ def _parse_keystring(keystr): def parse_keystring(keystr): """Parse a keystring like or xyz and return a KeyInfo list.""" s = ', '.join(_parse_keystring(keystr)) - return QKeySequence(s) + return sequence.KeySequence(s) def normalize_keystr(keystr): From dcf0d21121067a9ccb3a59902e3c689d42765d4a Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 29 Dec 2017 01:02:28 +0100 Subject: [PATCH 008/135] Move key related utils to sequence.py --- qutebrowser/config/configtypes.py | 3 +- qutebrowser/keyinput/sequence.py | 292 +++++++++++++++++++++++++++++- qutebrowser/utils/utils.py | 285 ----------------------------- 3 files changed, 293 insertions(+), 287 deletions(-) diff --git a/qutebrowser/config/configtypes.py b/qutebrowser/config/configtypes.py index e7e96cc44..b7a21bb42 100644 --- a/qutebrowser/config/configtypes.py +++ b/qutebrowser/config/configtypes.py @@ -62,6 +62,7 @@ from PyQt5.QtWidgets import QTabWidget, QTabBar from qutebrowser.commands import cmdutils from qutebrowser.config import configexc from qutebrowser.utils import standarddir, utils, qtutils, urlutils +from qutebrowser.keyinput import sequence SYSTEM_PROXY = object() # Return value for Proxy type @@ -1654,4 +1655,4 @@ class Key(BaseType): #if utils.is_special_key(value): # value = '<{}>'.format(utils.normalize_keystr(value[1:-1])) #return value - return utils.parse_keystring(value) + return sequence.parse_keystring(value) diff --git a/qutebrowser/keyinput/sequence.py b/qutebrowser/keyinput/sequence.py index 7db4f76a7..e47b37579 100644 --- a/qutebrowser/keyinput/sequence.py +++ b/qutebrowser/keyinput/sequence.py @@ -19,9 +19,299 @@ """Our own QKeySequence-like class and related utilities.""" +import unicodedata +import collections + +import attr +from PyQt5.QtCore import Qt from PyQt5.QtGui import QKeySequence -from qutebrowser.utils import utils +from qutebrowser.utils import utils, debug + + +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. + """ + 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. + 'Key_Blue': 'Blue', + 'Key_Calendar': 'Calendar', + 'Key_ChannelDown': 'Channel Down', + 'Key_ChannelUp': 'Channel Up', + 'Key_ContrastAdjust': 'Contrast Adjust', + 'Key_Dead_Abovedot': '˙', + 'Key_Dead_Abovering': '˚', + 'Key_Dead_Acute': '´', + 'Key_Dead_Belowdot': 'Belowdot', + 'Key_Dead_Breve': '˘', + 'Key_Dead_Caron': 'ˇ', + 'Key_Dead_Cedilla': '¸', + 'Key_Dead_Circumflex': '^', + 'Key_Dead_Diaeresis': '¨', + 'Key_Dead_Doubleacute': '˝', + 'Key_Dead_Grave': '`', + 'Key_Dead_Hook': 'Hook', + 'Key_Dead_Horn': 'Horn', + 'Key_Dead_Iota': 'Iota', + 'Key_Dead_Macron': '¯', + 'Key_Dead_Ogonek': '˛', + 'Key_Dead_Semivoiced_Sound': 'Semivoiced Sound', + 'Key_Dead_Tilde': '~', + 'Key_Dead_Voiced_Sound': 'Voiced Sound', + 'Key_Exit': 'Exit', + 'Key_Green': 'Green', + 'Key_Guide': 'Guide', + 'Key_Info': 'Info', + 'Key_LaunchG': 'LaunchG', + 'Key_LaunchH': 'LaunchH', + 'Key_MediaLast': 'MediaLast', + 'Key_Memo': 'Memo', + 'Key_MicMute': 'Mic Mute', + 'Key_Mode_switch': 'Mode switch', + 'Key_Multi_key': 'Multi key', + 'Key_PowerDown': 'Power Down', + 'Key_Red': 'Red', + 'Key_Settings': 'Settings', + 'Key_SingleCandidate': 'Single Candidate', + 'Key_ToDoList': 'Todo List', + 'Key_TouchpadOff': 'Touchpad Off', + 'Key_TouchpadOn': 'Touchpad On', + 'Key_TouchpadToggle': 'Touchpad toggle', + 'Key_Yellow': 'Yellow', + 'Key_Alt': 'Alt', + 'Key_AltGr': 'AltGr', + 'Key_Control': 'Control', + 'Key_Direction_L': 'Direction L', + 'Key_Direction_R': 'Direction R', + 'Key_Hyper_L': 'Hyper L', + 'Key_Hyper_R': 'Hyper R', + 'Key_Meta': 'Meta', + 'Key_Shift': 'Shift', + 'Key_Super_L': 'Super L', + 'Key_Super_R': 'Super R', + 'Key_unknown': 'Unknown', + } + # 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, k)] = v + except AttributeError: + pass + # Now we check if the key is any special one - if not, we use + # QKeySequence::toString. + try: + return special_names[key] + except KeyError: + name = QKeySequence(key).toString() + morphings = { + 'Backtab': 'Tab', + 'Esc': 'Escape', + } + if name in morphings: + return morphings[name] + else: + return name + + +def keyevent_to_string(e): + """Convert a QKeyEvent to a meaningful name. + + Args: + e: A QKeyEvent. + + Return: + A name of the key (combination) as a string or + None if only modifiers are pressed.. + """ + if utils.is_mac: + # Qt swaps Ctrl/Meta on macOS, so we switch it back here so the user + # can use it in the config as expected. See: + # https://github.com/qutebrowser/qutebrowser/issues/110 + # http://doc.qt.io/qt-5.4/osx-issues.html#special-keys + modmask2str = collections.OrderedDict([ + (Qt.MetaModifier, 'Ctrl'), + (Qt.AltModifier, 'Alt'), + (Qt.ControlModifier, 'Meta'), + (Qt.ShiftModifier, 'Shift'), + ]) + else: + modmask2str = collections.OrderedDict([ + (Qt.ControlModifier, 'Ctrl'), + (Qt.AltModifier, 'Alt'), + (Qt.MetaModifier, 'Meta'), + (Qt.ShiftModifier, 'Shift'), + ]) + modifiers = (Qt.Key_Control, Qt.Key_Alt, Qt.Key_Shift, Qt.Key_Meta, + Qt.Key_AltGr, Qt.Key_Super_L, Qt.Key_Super_R, Qt.Key_Hyper_L, + Qt.Key_Hyper_R, Qt.Key_Direction_L, Qt.Key_Direction_R) + if e.key() in modifiers: + # Only modifier pressed + return None + mod = e.modifiers() + parts = [] + + for (mask, s) in modmask2str.items(): + if mod & mask and s not in parts: + parts.append(s) + + key_string = key_to_string(e.key()) + if len(key_string) == 1: + category = unicodedata.category(key_string) + is_control_char = (category == 'Cc') + else: + is_control_char = False + + if e.modifiers() == Qt.ShiftModifier and not is_control_char: + parts = [] + + parts.append(key_string) + return normalize_keystr('+'.join(parts)) + + +@attr.s(repr=False) +class KeyInfo: + + """Stores information about a key, like used in a QKeyEvent. + + Attributes: + key: Qt::Key + modifiers: Qt::KeyboardModifiers + text: str + """ + + key = attr.ib() + modifiers = attr.ib() + text = attr.ib() + + def __repr__(self): + if self.modifiers is None: + modifiers = None + else: + #modifiers = qflags_key(Qt, self.modifiers) + modifiers = hex(int(self.modifiers)) + return utils.get_repr(self, constructor=True, + key=debug.qenum_key(Qt, self.key), + modifiers=modifiers, text=self.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 is_special_key(keystr): + """True if keystr is a 'special' keystring (e.g. or ).""" + return keystr.startswith('<') and keystr.endswith('>') + + +def _parse_single_key(keystr): + """Convert a single key string to a (Qt.Key, Qt.Modifiers, text) tuple.""" + # FIXME remove + + if is_special_key(keystr): + # 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 = KeySequence(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): + key = '' + special = False + for c in keystr: + if c == '>': + yield normalize_keystr(key) + key = '' + special = False + elif c == '<': + special = True + elif special: + key += c + else: + yield 'Shift+' + c if c.isupper() else c + + +def parse_keystring(keystr): + """Parse a keystring like or xyz and return a KeyInfo list.""" + s = ', '.join(_parse_keystring(keystr)) + return KeySequence(s) + + +def normalize_keystr(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'), + ) + for (orig, repl) in replacements: + keystr = keystr.replace(orig, repl) + for mod in ['ctrl', 'meta', 'alt', 'shift']: + keystr = keystr.replace(mod + '-', mod + '+') + return keystr class KeySequence: diff --git a/qutebrowser/utils/utils.py b/qutebrowser/utils/utils.py index a11c67bc4..e046b28b8 100644 --- a/qutebrowser/utils/utils.py +++ b/qutebrowser/utils/utils.py @@ -287,291 +287,6 @@ def format_size(size, base=1024, suffix=''): return '{:.02f}{}{}'.format(size, prefixes[-1], suffix) -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. - """ - 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. - 'Key_Blue': 'Blue', - 'Key_Calendar': 'Calendar', - 'Key_ChannelDown': 'Channel Down', - 'Key_ChannelUp': 'Channel Up', - 'Key_ContrastAdjust': 'Contrast Adjust', - 'Key_Dead_Abovedot': '˙', - 'Key_Dead_Abovering': '˚', - 'Key_Dead_Acute': '´', - 'Key_Dead_Belowdot': 'Belowdot', - 'Key_Dead_Breve': '˘', - 'Key_Dead_Caron': 'ˇ', - 'Key_Dead_Cedilla': '¸', - 'Key_Dead_Circumflex': '^', - 'Key_Dead_Diaeresis': '¨', - 'Key_Dead_Doubleacute': '˝', - 'Key_Dead_Grave': '`', - 'Key_Dead_Hook': 'Hook', - 'Key_Dead_Horn': 'Horn', - 'Key_Dead_Iota': 'Iota', - 'Key_Dead_Macron': '¯', - 'Key_Dead_Ogonek': '˛', - 'Key_Dead_Semivoiced_Sound': 'Semivoiced Sound', - 'Key_Dead_Tilde': '~', - 'Key_Dead_Voiced_Sound': 'Voiced Sound', - 'Key_Exit': 'Exit', - 'Key_Green': 'Green', - 'Key_Guide': 'Guide', - 'Key_Info': 'Info', - 'Key_LaunchG': 'LaunchG', - 'Key_LaunchH': 'LaunchH', - 'Key_MediaLast': 'MediaLast', - 'Key_Memo': 'Memo', - 'Key_MicMute': 'Mic Mute', - 'Key_Mode_switch': 'Mode switch', - 'Key_Multi_key': 'Multi key', - 'Key_PowerDown': 'Power Down', - 'Key_Red': 'Red', - 'Key_Settings': 'Settings', - 'Key_SingleCandidate': 'Single Candidate', - 'Key_ToDoList': 'Todo List', - 'Key_TouchpadOff': 'Touchpad Off', - 'Key_TouchpadOn': 'Touchpad On', - 'Key_TouchpadToggle': 'Touchpad toggle', - 'Key_Yellow': 'Yellow', - 'Key_Alt': 'Alt', - 'Key_AltGr': 'AltGr', - 'Key_Control': 'Control', - 'Key_Direction_L': 'Direction L', - 'Key_Direction_R': 'Direction R', - 'Key_Hyper_L': 'Hyper L', - 'Key_Hyper_R': 'Hyper R', - 'Key_Meta': 'Meta', - 'Key_Shift': 'Shift', - 'Key_Super_L': 'Super L', - 'Key_Super_R': 'Super R', - 'Key_unknown': 'Unknown', - } - # 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, k)] = v - except AttributeError: - pass - # Now we check if the key is any special one - if not, we use - # QKeySequence::toString. - try: - return special_names[key] - except KeyError: - name = QKeySequence(key).toString() - morphings = { - 'Backtab': 'Tab', - 'Esc': 'Escape', - } - if name in morphings: - return morphings[name] - else: - return name - - -def keyevent_to_string(e): - """Convert a QKeyEvent to a meaningful name. - - Args: - e: A QKeyEvent. - - Return: - A name of the key (combination) as a string or - None if only modifiers are pressed.. - """ - if is_mac: - # Qt swaps Ctrl/Meta on macOS, so we switch it back here so the user - # can use it in the config as expected. See: - # https://github.com/qutebrowser/qutebrowser/issues/110 - # http://doc.qt.io/qt-5.4/osx-issues.html#special-keys - modmask2str = collections.OrderedDict([ - (Qt.MetaModifier, 'Ctrl'), - (Qt.AltModifier, 'Alt'), - (Qt.ControlModifier, 'Meta'), - (Qt.ShiftModifier, 'Shift'), - ]) - else: - modmask2str = collections.OrderedDict([ - (Qt.ControlModifier, 'Ctrl'), - (Qt.AltModifier, 'Alt'), - (Qt.MetaModifier, 'Meta'), - (Qt.ShiftModifier, 'Shift'), - ]) - modifiers = (Qt.Key_Control, Qt.Key_Alt, Qt.Key_Shift, Qt.Key_Meta, - Qt.Key_AltGr, Qt.Key_Super_L, Qt.Key_Super_R, Qt.Key_Hyper_L, - Qt.Key_Hyper_R, Qt.Key_Direction_L, Qt.Key_Direction_R) - if e.key() in modifiers: - # Only modifier pressed - return None - mod = e.modifiers() - parts = [] - - for (mask, s) in modmask2str.items(): - if mod & mask and s not in parts: - parts.append(s) - - key_string = key_to_string(e.key()) - if len(key_string) == 1: - category = unicodedata.category(key_string) - is_control_char = (category == 'Cc') - else: - is_control_char = False - - if e.modifiers() == Qt.ShiftModifier and not is_control_char: - parts = [] - - parts.append(key_string) - return normalize_keystr('+'.join(parts)) - - -@attr.s(repr=False) -class KeyInfo: - - """Stores information about a key, like used in a QKeyEvent. - - Attributes: - key: Qt::Key - modifiers: Qt::KeyboardModifiers - text: str - """ - - key = attr.ib() - modifiers = attr.ib() - text = attr.ib() - - def __repr__(self): - 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=debug.qenum_key(Qt, self.key), - modifiers=modifiers, text=self.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 is_special_key(keystr): - """True if keystr is a 'special' keystring (e.g. or ).""" - return keystr.startswith('<') and keystr.endswith('>') - - -def _parse_single_key(keystr): - """Convert a single key string to a (Qt.Key, Qt.Modifiers, text) tuple.""" - # FIXME remove - - if is_special_key(keystr): - # 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 = sequence.KeySequence(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): - key = '' - special = False - for c in keystr: - if c == '>': - yield normalize_keystr(key) - key = '' - special = False - elif c == '<': - special = True - elif special: - key += c - else: - yield 'Shift+' + c if c.isupper() else c - - -def parse_keystring(keystr): - """Parse a keystring like or xyz and return a KeyInfo list.""" - s = ', '.join(_parse_keystring(keystr)) - return sequence.KeySequence(s) - - -def normalize_keystr(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'), - ) - for (orig, repl) in replacements: - keystr = keystr.replace(orig, repl) - for mod in ['ctrl', 'meta', 'alt', 'shift']: - keystr = keystr.replace(mod + '-', mod + '+') - return keystr - - class FakeIOStream(io.TextIOBase): """A fake file-like stream which calls a function for write-calls.""" From b1dde41b740262d89d0cf21a8d4e1940053f206d Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 29 Dec 2017 01:09:06 +0100 Subject: [PATCH 009/135] Rename sequence.py to keyutils.py --- qutebrowser/config/configtypes.py | 4 ++-- qutebrowser/keyinput/basekeyparser.py | 8 ++++---- qutebrowser/keyinput/{sequence.py => keyutils.py} | 0 qutebrowser/keyinput/modeparsers.py | 4 ++-- qutebrowser/misc/keyhintwidget.py | 4 ++-- qutebrowser/utils/utils.py | 1 - 6 files changed, 10 insertions(+), 11 deletions(-) rename qutebrowser/keyinput/{sequence.py => keyutils.py} (100%) diff --git a/qutebrowser/config/configtypes.py b/qutebrowser/config/configtypes.py index b7a21bb42..d6e3aa5fc 100644 --- a/qutebrowser/config/configtypes.py +++ b/qutebrowser/config/configtypes.py @@ -62,7 +62,7 @@ from PyQt5.QtWidgets import QTabWidget, QTabBar from qutebrowser.commands import cmdutils from qutebrowser.config import configexc from qutebrowser.utils import standarddir, utils, qtutils, urlutils -from qutebrowser.keyinput import sequence +from qutebrowser.keyinput import keyutils SYSTEM_PROXY = object() # Return value for Proxy type @@ -1655,4 +1655,4 @@ class Key(BaseType): #if utils.is_special_key(value): # value = '<{}>'.format(utils.normalize_keystr(value[1:-1])) #return value - return sequence.parse_keystring(value) + return keyutils.parse_keystring(value) diff --git a/qutebrowser/keyinput/basekeyparser.py b/qutebrowser/keyinput/basekeyparser.py index 14a7ccc6f..a2e07cb67 100644 --- a/qutebrowser/keyinput/basekeyparser.py +++ b/qutebrowser/keyinput/basekeyparser.py @@ -28,7 +28,7 @@ from PyQt5.QtGui import QKeySequence from qutebrowser.config import config from qutebrowser.utils import usertypes, log, utils -from qutebrowser.keyinput import sequence +from qutebrowser.keyinput import keyutils class BaseKeyParser(QObject): @@ -84,7 +84,7 @@ class BaseKeyParser(QObject): super().__init__(parent) self._win_id = win_id self._modename = None - self._sequence = sequence.KeySequence() + self._sequence = keyutils.KeySequence() self._count = '' if supports_count is None: supports_count = supports_chains @@ -121,7 +121,7 @@ class BaseKeyParser(QObject): A self.Match member. """ key = e.key() - txt = utils.keyevent_to_string(e) + txt = keyutils.keyevent_to_string(e) self._debug_log("Got key: 0x{:x} / text: '{}'".format(key, txt)) if txt is None: @@ -264,6 +264,6 @@ class BaseKeyParser(QObject): if self._sequence: self._debug_log("discarding keystring '{}'.".format( self._sequence)) - self._sequence = sequence.KeySequence() + self._sequence = keyutils.KeySequence() self._count = '' self.keystring_updated.emit('') diff --git a/qutebrowser/keyinput/sequence.py b/qutebrowser/keyinput/keyutils.py similarity index 100% rename from qutebrowser/keyinput/sequence.py rename to qutebrowser/keyinput/keyutils.py diff --git a/qutebrowser/keyinput/modeparsers.py b/qutebrowser/keyinput/modeparsers.py index 7c2088133..55521512a 100644 --- a/qutebrowser/keyinput/modeparsers.py +++ b/qutebrowser/keyinput/modeparsers.py @@ -30,7 +30,7 @@ from PyQt5.QtCore import pyqtSlot, Qt from qutebrowser.commands import cmdexc from qutebrowser.config import config -from qutebrowser.keyinput import keyparser +from qutebrowser.keyinput import keyparser, keyutils from qutebrowser.utils import usertypes, log, message, objreg, utils @@ -298,7 +298,7 @@ class RegisterKeyParser(keyparser.CommandKeyParser): key = e.text() - if key == '' or utils.keyevent_to_string(e) is None: + if key == '' or keyutils.keyevent_to_string(e) is None: # this is not a proper register key, let it pass and keep going return False diff --git a/qutebrowser/misc/keyhintwidget.py b/qutebrowser/misc/keyhintwidget.py index d4ed43141..7e6791d4e 100644 --- a/qutebrowser/misc/keyhintwidget.py +++ b/qutebrowser/misc/keyhintwidget.py @@ -35,7 +35,7 @@ from PyQt5.QtGui import QKeySequence from qutebrowser.config import config from qutebrowser.utils import utils, usertypes from qutebrowser.commands import cmdutils -from qutebrowser.keyinput import sequence +from qutebrowser.keyinput import keyutils class KeyHintView(QLabel): @@ -107,7 +107,7 @@ class KeyHintView(QLabel): bindings_dict = config.key_instance.get_bindings_for(modename) bindings = [(k, v) for (k, v) in sorted(bindings_dict.items()) - if k.matches(sequence.KeySequence(prefix)) and # FIXME + if k.matches(keyutils.KeySequence(prefix)) and # FIXME not blacklisted(k) and (takes_count(v) or not countstr)] diff --git a/qutebrowser/utils/utils.py b/qutebrowser/utils/utils.py index e046b28b8..6cf38229b 100644 --- a/qutebrowser/utils/utils.py +++ b/qutebrowser/utils/utils.py @@ -50,7 +50,6 @@ except ImportError: # pragma: no cover import qutebrowser from qutebrowser.utils import qtutils, log, debug -from qutebrowser.keyinput import sequence fake_clipboard = None From 21b3e05ed01c8c9eae15b8fb9db3a4483249a7c5 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 29 Dec 2017 01:10:25 +0100 Subject: [PATCH 010/135] Fix getting reverse bindings --- qutebrowser/config/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py index 1f910f549..865ab2834 100644 --- a/qutebrowser/config/config.py +++ b/qutebrowser/config/config.py @@ -165,7 +165,7 @@ class KeyConfig: # if utils.is_special_key(key): # cmd_to_keys[cmd].append(key) # else: - cmd_to_keys[cmd].insert(0, key.toString()) + cmd_to_keys[cmd].insert(0, str(key)) return cmd_to_keys def get_command(self, key, mode, default=False): From d961211188159c5ac6c814998030535d913d73c5 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 29 Dec 2017 01:13:07 +0100 Subject: [PATCH 011/135] Delete some old code --- qutebrowser/keyinput/keyutils.py | 80 -------------------------------- 1 file changed, 80 deletions(-) diff --git a/qutebrowser/keyinput/keyutils.py b/qutebrowser/keyinput/keyutils.py index e47b37579..9c7bc1169 100644 --- a/qutebrowser/keyinput/keyutils.py +++ b/qutebrowser/keyinput/keyutils.py @@ -181,32 +181,6 @@ def keyevent_to_string(e): return normalize_keystr('+'.join(parts)) -@attr.s(repr=False) -class KeyInfo: - - """Stores information about a key, like used in a QKeyEvent. - - Attributes: - key: Qt::Key - modifiers: Qt::KeyboardModifiers - text: str - """ - - key = attr.ib() - modifiers = attr.ib() - text = attr.ib() - - def __repr__(self): - if self.modifiers is None: - modifiers = None - else: - #modifiers = qflags_key(Qt, self.modifiers) - modifiers = hex(int(self.modifiers)) - return utils.get_repr(self, constructor=True, - key=debug.qenum_key(Qt, self.key), - modifiers=modifiers, text=self.text) - - class KeyParseError(Exception): """Raised by _parse_single_key/parse_keystring on parse errors.""" @@ -215,60 +189,6 @@ class KeyParseError(Exception): super().__init__("Could not parse {!r}: {}".format(keystr, error)) -def is_special_key(keystr): - """True if keystr is a 'special' keystring (e.g. or ).""" - return keystr.startswith('<') and keystr.endswith('>') - - -def _parse_single_key(keystr): - """Convert a single key string to a (Qt.Key, Qt.Modifiers, text) tuple.""" - # FIXME remove - - if is_special_key(keystr): - # 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 = KeySequence(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): key = '' special = False From c98eb5502d968c52ac9c567977e27a63d91d2382 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 29 Dec 2017 01:19:16 +0100 Subject: [PATCH 012/135] Add some FIXMEs --- qutebrowser/keyinput/modeparsers.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/qutebrowser/keyinput/modeparsers.py b/qutebrowser/keyinput/modeparsers.py index 55521512a..86e03877a 100644 --- a/qutebrowser/keyinput/modeparsers.py +++ b/qutebrowser/keyinput/modeparsers.py @@ -68,6 +68,7 @@ class NormalKeyParser(keyparser.CommandKeyParser): Return: A self.Match member. """ + # FIXME rewrite this txt = e.text().strip() if self._inhibited: self._debug_log("Ignoring key '{}', because the normal mode is " @@ -166,6 +167,7 @@ class HintKeyParser(keyparser.CommandKeyParser): Return: True if event has been handled, False otherwise. """ + # FIXME rewrite this log.keyboard.debug("Got special key 0x{:x} text {}".format( e.key(), e.text())) hintmanager = objreg.get('hintmanager', scope='tab', @@ -209,6 +211,7 @@ class HintKeyParser(keyparser.CommandKeyParser): Returns: True if the match has been handled, False otherwise. """ + # FIXME rewrite this match = self._handle_single_key(e) if match == self.Match.partial: self.keystring_updated.emit(self._keystring) @@ -293,6 +296,7 @@ class RegisterKeyParser(keyparser.CommandKeyParser): Return: True if event has been handled, False otherwise. """ + # FIXME rewrite this if super().handle(e): return True From 5cee39d315250b82dc61f7eb9365f7cf7e1feb96 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 29 Dec 2017 01:41:55 +0100 Subject: [PATCH 013/135] Initial move of keyutils tests --- tests/unit/keyinput/test_keyutils.py | 152 +++++++++++++++++++++++++++ tests/unit/utils/test_utils.py | 144 ------------------------- 2 files changed, 152 insertions(+), 144 deletions(-) create mode 100644 tests/unit/keyinput/test_keyutils.py diff --git a/tests/unit/keyinput/test_keyutils.py b/tests/unit/keyinput/test_keyutils.py new file mode 100644 index 000000000..e901de0b5 --- /dev/null +++ b/tests/unit/keyinput/test_keyutils.py @@ -0,0 +1,152 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2014-2017 Florian Bruhin (The Compiler) +# +# 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 . + +import pytest +from PyQt5.QtCore import Qt + +from qutebrowser.keyinput import keyutils + + +class TestKeyToString: + + """Test key_to_string.""" + + @pytest.mark.parametrize('key, expected', [ + (Qt.Key_Blue, 'Blue'), + (Qt.Key_Backtab, 'Tab'), + (Qt.Key_Escape, 'Escape'), + (Qt.Key_A, 'A'), + (Qt.Key_degree, '°'), + (Qt.Key_Meta, 'Meta'), + ]) + def test_normal(self, key, expected): + """Test a special key where QKeyEvent::toString works incorrectly.""" + assert keyutils.key_to_string(key) == expected + + def test_missing(self, monkeypatch): + """Test with a missing key.""" + monkeypatch.delattr(keyutils.Qt, 'Key_Blue') + # We don't want to test the key which is actually missing - we only + # want to know if the mapping still behaves properly. + assert keyutils.key_to_string(Qt.Key_A) == 'A' + + def test_all(self): + """Make sure there's some sensible output for all keys.""" + for name, value in sorted(vars(Qt).items()): + if not isinstance(value, Qt.Key): + continue + print(name) + string = keyutils.key_to_string(value) + assert string + string.encode('utf-8') # make sure it's encodable + + +class TestKeyEventToString: + + """Test keyevent_to_string.""" + + def test_only_control(self, fake_keyevent_factory): + """Test keyeevent when only control is pressed.""" + evt = fake_keyevent_factory(key=Qt.Key_Control, + modifiers=Qt.ControlModifier) + assert keyutils.keyevent_to_string(evt) is None + + def test_only_hyper_l(self, fake_keyevent_factory): + """Test keyeevent when only Hyper_L is pressed.""" + evt = fake_keyevent_factory(key=Qt.Key_Hyper_L, + modifiers=Qt.MetaModifier) + assert keyutils.keyevent_to_string(evt) is None + + def test_only_key(self, fake_keyevent_factory): + """Test with a simple key pressed.""" + evt = fake_keyevent_factory(key=Qt.Key_A) + assert keyutils.keyevent_to_string(evt) == 'a' + + def test_key_and_modifier(self, fake_keyevent_factory): + """Test with key and modifier pressed.""" + evt = fake_keyevent_factory(key=Qt.Key_A, modifiers=Qt.ControlModifier) + expected = 'meta+a' if keyutils.is_mac else 'ctrl+a' + assert keyutils.keyevent_to_string(evt) == expected + + def test_key_and_modifiers(self, fake_keyevent_factory): + """Test with key and multiple modifiers pressed.""" + evt = fake_keyevent_factory( + key=Qt.Key_A, modifiers=(Qt.ControlModifier | Qt.AltModifier | + Qt.MetaModifier | Qt.ShiftModifier)) + assert keyutils.keyevent_to_string(evt) == 'ctrl+alt+meta+shift+a' + + @pytest.mark.fake_os('mac') + def test_mac(self, fake_keyevent_factory): + """Test with a simulated mac.""" + evt = fake_keyevent_factory(key=Qt.Key_A, modifiers=Qt.ControlModifier) + assert keyutils.keyevent_to_string(evt) == 'meta+a' + + +@pytest.mark.parametrize('keystr, expected', [ + ('', keyutils.KeyInfo(Qt.Key_X, Qt.ControlModifier, '')), + ('', keyutils.KeyInfo(Qt.Key_X, Qt.MetaModifier, '')), + ('', + keyutils.KeyInfo(Qt.Key_Y, Qt.ControlModifier | Qt.AltModifier, '')), + ('x', keyutils.KeyInfo(Qt.Key_X, Qt.NoModifier, 'x')), + ('X', keyutils.KeyInfo(Qt.Key_X, Qt.ShiftModifier, 'X')), + ('', keyutils.KeyInfo(Qt.Key_Escape, Qt.NoModifier, '')), + + ('foobar', keyutils.KeyParseError), + ('x, y', keyutils.KeyParseError), + ('xyz', keyutils.KeyParseError), + ('Escape', keyutils.KeyParseError), + (', ', keyutils.KeyParseError), +]) +def test_parse_single_key(keystr, expected): + if expected is keyutils.KeyParseError: + with pytest.raises(keyutils.KeyParseError): + keyutils._parse_single_key(keystr) + else: + assert keyutils._parse_single_key(keystr) == expected + + +@pytest.mark.parametrize('keystr, expected', [ + ('', [keyutils.KeyInfo(Qt.Key_X, Qt.ControlModifier, '')]), + ('x', [keyutils.KeyInfo(Qt.Key_X, Qt.NoModifier, 'x')]), + ('xy', [keyutils.KeyInfo(Qt.Key_X, Qt.NoModifier, 'x'), + keyutils.KeyInfo(Qt.Key_Y, Qt.NoModifier, 'y')]), + + ('', keyutils.KeyParseError), +]) +def test_parse_keystring(keystr, expected): + if expected is keyutils.KeyParseError: + with pytest.raises(keyutils.KeyParseError): + keyutils.parse_keystring(keystr) + else: + assert keyutils.parse_keystring(keystr) == expected + + +@pytest.mark.parametrize('orig, repl', [ + ('Control+x', 'ctrl+x'), + ('Windows+x', 'meta+x'), + ('Mod1+x', 'alt+x'), + ('Mod4+x', 'meta+x'), + ('Control--', 'ctrl+-'), + ('Windows++', 'meta++'), + ('ctrl-x', 'ctrl+x'), + ('control+x', 'ctrl+x') +]) +def test_normalize_keystr(orig, repl): + assert keyutils.normalize_keystr(orig) == repl + diff --git a/tests/unit/utils/test_utils.py b/tests/unit/utils/test_utils.py index 28837e93c..2ca0bc91c 100644 --- a/tests/unit/utils/test_utils.py +++ b/tests/unit/utils/test_utils.py @@ -297,134 +297,6 @@ class TestFormatSize: assert utils.format_size(size, base=1000) == out -class TestKeyToString: - - """Test key_to_string.""" - - @pytest.mark.parametrize('key, expected', [ - (Qt.Key_Blue, 'Blue'), - (Qt.Key_Backtab, 'Tab'), - (Qt.Key_Escape, 'Escape'), - (Qt.Key_A, 'A'), - (Qt.Key_degree, '°'), - (Qt.Key_Meta, 'Meta'), - ]) - def test_normal(self, key, expected): - """Test a special key where QKeyEvent::toString works incorrectly.""" - assert utils.key_to_string(key) == expected - - def test_missing(self, monkeypatch): - """Test with a missing key.""" - monkeypatch.delattr(utils.Qt, 'Key_Blue') - # We don't want to test the key which is actually missing - we only - # want to know if the mapping still behaves properly. - assert utils.key_to_string(Qt.Key_A) == 'A' - - def test_all(self): - """Make sure there's some sensible output for all keys.""" - for name, value in sorted(vars(Qt).items()): - if not isinstance(value, Qt.Key): - continue - print(name) - string = utils.key_to_string(value) - assert string - string.encode('utf-8') # make sure it's encodable - - -class TestKeyEventToString: - - """Test keyevent_to_string.""" - - def test_only_control(self, fake_keyevent_factory): - """Test keyeevent when only control is pressed.""" - evt = fake_keyevent_factory(key=Qt.Key_Control, - modifiers=Qt.ControlModifier) - assert utils.keyevent_to_string(evt) is None - - def test_only_hyper_l(self, fake_keyevent_factory): - """Test keyeevent when only Hyper_L is pressed.""" - evt = fake_keyevent_factory(key=Qt.Key_Hyper_L, - modifiers=Qt.MetaModifier) - assert utils.keyevent_to_string(evt) is None - - def test_only_key(self, fake_keyevent_factory): - """Test with a simple key pressed.""" - evt = fake_keyevent_factory(key=Qt.Key_A) - assert utils.keyevent_to_string(evt) == 'a' - - def test_key_and_modifier(self, fake_keyevent_factory): - """Test with key and modifier pressed.""" - evt = fake_keyevent_factory(key=Qt.Key_A, modifiers=Qt.ControlModifier) - expected = 'meta+a' if utils.is_mac else 'ctrl+a' - assert utils.keyevent_to_string(evt) == expected - - def test_key_and_modifiers(self, fake_keyevent_factory): - """Test with key and multiple modifiers pressed.""" - evt = fake_keyevent_factory( - key=Qt.Key_A, modifiers=(Qt.ControlModifier | Qt.AltModifier | - Qt.MetaModifier | Qt.ShiftModifier)) - assert utils.keyevent_to_string(evt) == 'ctrl+alt+meta+shift+a' - - @pytest.mark.fake_os('mac') - def test_mac(self, fake_keyevent_factory): - """Test with a simulated mac.""" - evt = fake_keyevent_factory(key=Qt.Key_A, modifiers=Qt.ControlModifier) - 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'), - ('Mod1+x', 'alt+x'), - ('Mod4+x', 'meta+x'), - ('Control--', 'ctrl+-'), - ('Windows++', 'meta++'), - ('ctrl-x', 'ctrl+x'), - ('control+x', 'ctrl+x') -]) -def test_normalize_keystr(orig, repl): - assert utils.normalize_keystr(orig) == repl - - class TestFakeIOStream: """Test FakeIOStream.""" @@ -832,22 +704,6 @@ class TestGetSetClipboard: utils.get_clipboard(fallback=True) -@pytest.mark.parametrize('keystr, expected', [ - ('', True), - ('', True), - ('', True), - ('x', False), - ('X', False), - ('', True), - ('foobar', False), - ('foo>', False), - (' Date: Fri, 29 Dec 2017 01:43:47 +0100 Subject: [PATCH 014/135] fixme --- qutebrowser/browser/commands.py | 1 + 1 file changed, 1 insertion(+) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 92a841d72..823d12c99 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -2084,6 +2084,7 @@ class CommandDispatcher: keystring: The keystring to send. global_: If given, the keys are sent to the qutebrowser UI. """ + # FIXME: rewrite try: keyinfos = utils.parse_keystring(keystring) except utils.KeyParseError as e: From cc747b00ce617b61941e8edd29bcd7e1ec0df10e Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 29 Dec 2017 01:50:51 +0100 Subject: [PATCH 015/135] Move parsing to class --- qutebrowser/config/configtypes.py | 2 +- qutebrowser/keyinput/keyutils.py | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/qutebrowser/config/configtypes.py b/qutebrowser/config/configtypes.py index d6e3aa5fc..218d31193 100644 --- a/qutebrowser/config/configtypes.py +++ b/qutebrowser/config/configtypes.py @@ -1655,4 +1655,4 @@ class Key(BaseType): #if utils.is_special_key(value): # value = '<{}>'.format(utils.normalize_keystr(value[1:-1])) #return value - return keyutils.parse_keystring(value) + return keyutils.KeySequence.parse(value) diff --git a/qutebrowser/keyinput/keyutils.py b/qutebrowser/keyinput/keyutils.py index 9c7bc1169..43808f8a5 100644 --- a/qutebrowser/keyinput/keyutils.py +++ b/qutebrowser/keyinput/keyutils.py @@ -205,12 +205,6 @@ def _parse_keystring(keystr): yield 'Shift+' + c if c.isupper() else c -def parse_keystring(keystr): - """Parse a keystring like or xyz and return a KeyInfo list.""" - s = ', '.join(_parse_keystring(keystr)) - return KeySequence(s) - - def normalize_keystr(keystr): """Normalize a keystring like Ctrl-Q to a keystring like Ctrl+Q. @@ -266,3 +260,9 @@ class KeySequence: def append_event(self, ev): return self.__class__(*self._sequence, ev.modifiers() | ev.key()) + + @classmethod + def parse(cls, keystr): + """Parse a keystring like or xyz and return a KeySequence.""" + s = ', '.join(_parse_keystring(keystr)) + return cls(s) From 917f2a30de5e0f104e555a718a8d33b6898c9a73 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 29 Dec 2017 13:23:38 +0100 Subject: [PATCH 016/135] Get tests to collect --- qutebrowser/keyinput/keyutils.py | 1 + tests/unit/keyinput/test_keyutils.py | 41 ++++++++-------------------- 2 files changed, 13 insertions(+), 29 deletions(-) diff --git a/qutebrowser/keyinput/keyutils.py b/qutebrowser/keyinput/keyutils.py index 43808f8a5..3a107142b 100644 --- a/qutebrowser/keyinput/keyutils.py +++ b/qutebrowser/keyinput/keyutils.py @@ -232,6 +232,7 @@ class KeySequence: def __init__(self, *args): self._sequence = QKeySequence(*args) + # FIXME handle more than 4 keys def __str__(self): return self._sequence.toString() diff --git a/tests/unit/keyinput/test_keyutils.py b/tests/unit/keyinput/test_keyutils.py index e901de0b5..4311a4aeb 100644 --- a/tests/unit/keyinput/test_keyutils.py +++ b/tests/unit/keyinput/test_keyutils.py @@ -99,21 +99,20 @@ class TestKeyEventToString: @pytest.mark.parametrize('keystr, expected', [ - ('', keyutils.KeyInfo(Qt.Key_X, Qt.ControlModifier, '')), - ('', keyutils.KeyInfo(Qt.Key_X, Qt.MetaModifier, '')), + ('', keyutils.KeySequence(Qt.ControlModifier | Qt.Key_X)), + ('', keyutils.KeySequence(Qt.MetaModifier | Qt.Key_X)), ('', - keyutils.KeyInfo(Qt.Key_Y, Qt.ControlModifier | Qt.AltModifier, '')), - ('x', keyutils.KeyInfo(Qt.Key_X, Qt.NoModifier, 'x')), - ('X', keyutils.KeyInfo(Qt.Key_X, Qt.ShiftModifier, 'X')), - ('', keyutils.KeyInfo(Qt.Key_Escape, Qt.NoModifier, '')), - - ('foobar', keyutils.KeyParseError), - ('x, y', keyutils.KeyParseError), - ('xyz', keyutils.KeyParseError), - ('Escape', keyutils.KeyParseError), - (', ', keyutils.KeyParseError), + keyutils.KeySequence(Qt.ControlModifier | Qt.AltModifier | Qt.Key_Y)), + ('x', keyutils.KeySequence(Qt.Key_X)), + ('X', keyutils.KeySequence(Qt.ShiftModifier | Qt.Key_X)), + ('', keyutils.KeySequence(Qt.Key_Escape)), + ('xyz', keyutils.KeySequence(Qt.Key_X, Qt.Key_Y, Qt.Key_Z)), + ('', keyutils.KeySequence(Qt.ControlModifier | Qt.Key_X, + Qt.MetaModifier | Qt.Key_Y)), + # FIXME + # (', ', keyutils.KeyParseError), ]) -def test_parse_single_key(keystr, expected): +def test_parse(keystr, expected): if expected is keyutils.KeyParseError: with pytest.raises(keyutils.KeyParseError): keyutils._parse_single_key(keystr) @@ -121,22 +120,6 @@ def test_parse_single_key(keystr, expected): assert keyutils._parse_single_key(keystr) == expected -@pytest.mark.parametrize('keystr, expected', [ - ('', [keyutils.KeyInfo(Qt.Key_X, Qt.ControlModifier, '')]), - ('x', [keyutils.KeyInfo(Qt.Key_X, Qt.NoModifier, 'x')]), - ('xy', [keyutils.KeyInfo(Qt.Key_X, Qt.NoModifier, 'x'), - keyutils.KeyInfo(Qt.Key_Y, Qt.NoModifier, 'y')]), - - ('', keyutils.KeyParseError), -]) -def test_parse_keystring(keystr, expected): - if expected is keyutils.KeyParseError: - with pytest.raises(keyutils.KeyParseError): - keyutils.parse_keystring(keystr) - else: - assert keyutils.parse_keystring(keystr) == expected - - @pytest.mark.parametrize('orig, repl', [ ('Control+x', 'ctrl+x'), ('Windows+x', 'meta+x'), From d9c768ed86a30a8608ab934b8591dcdc88907215 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 29 Dec 2017 13:53:43 +0100 Subject: [PATCH 017/135] Strip out shift modifier for non-alpha bindings --- qutebrowser/keyinput/keyutils.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/qutebrowser/keyinput/keyutils.py b/qutebrowser/keyinput/keyutils.py index 3a107142b..8471f1479 100644 --- a/qutebrowser/keyinput/keyutils.py +++ b/qutebrowser/keyinput/keyutils.py @@ -260,7 +260,27 @@ class KeySequence: return self._sequence.matches(other._sequence) def append_event(self, ev): - return self.__class__(*self._sequence, ev.modifiers() | ev.key()) + """Create a new KeySequence object with the given QKeyEvent added. + + We need to do some sophisticated checking of modifiers here: + + 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. + + FIXME: create test cases! + """ + modifiers = ev.modifiers() + if (modifiers == Qt.ShiftModifier and + unicodedata.category(ev.text()) != 'Lu'): + modifiers = Qt.KeyboardModifiers() + return self.__class__(*self._sequence, modifiers | ev.key()) @classmethod def parse(cls, keystr): From 7b17ab4b3f3de33c85ebe1b88ddbf319143b67f5 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 29 Dec 2017 14:22:20 +0100 Subject: [PATCH 018/135] Initial str() attempt --- qutebrowser/config/config.py | 7 ++----- qutebrowser/keyinput/keyutils.py | 27 +++++++++++++++------------ 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py index 865ab2834..222b0fda3 100644 --- a/qutebrowser/config/config.py +++ b/qutebrowser/config/config.py @@ -28,6 +28,7 @@ from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject from qutebrowser.config import configdata, configexc from qutebrowser.utils import utils, log, jinja from qutebrowser.misc import objects +from qutebrowser.keyinput import keyutils # An easy way to access the config from other code via config.val.foo val = None @@ -136,11 +137,7 @@ class KeyConfig: """Make sure the given mode exists and normalize the key.""" if mode not in configdata.DATA['bindings.default'].default: raise configexc.KeybindingError("Invalid mode {}!".format(mode)) - # FIXME needed? - # if utils.is_special_key(key): - # # , , and should be considered equivalent - # return utils.normalize_keystr(key) - return key + return str(keyutils.KeySequence.parse(key)) def get_bindings_for(self, mode): """Get the combined bindings for the given mode.""" diff --git a/qutebrowser/keyinput/keyutils.py b/qutebrowser/keyinput/keyutils.py index 8471f1479..bbb081f3d 100644 --- a/qutebrowser/keyinput/keyutils.py +++ b/qutebrowser/keyinput/keyutils.py @@ -127,10 +127,12 @@ def key_to_string(key): def keyevent_to_string(e): - """Convert a QKeyEvent to a meaningful name. + """Convert a QKeyEvent to a meaningful name.""" + return key_with_modifiers_to_string(int(e.key()) | int(e.modifiers())) - Args: - e: A QKeyEvent. + +def key_with_modifiers_to_string(key): + """Convert a Qt.Key with modifiers to a meaningful name. Return: A name of the key (combination) as a string or @@ -154,27 +156,27 @@ def keyevent_to_string(e): (Qt.MetaModifier, 'Meta'), (Qt.ShiftModifier, 'Shift'), ]) - modifiers = (Qt.Key_Control, Qt.Key_Alt, Qt.Key_Shift, Qt.Key_Meta, - Qt.Key_AltGr, Qt.Key_Super_L, Qt.Key_Super_R, Qt.Key_Hyper_L, - Qt.Key_Hyper_R, Qt.Key_Direction_L, Qt.Key_Direction_R) - if e.key() in modifiers: + modifiers = (Qt.Key_Control | Qt.Key_Alt | Qt.Key_Shift | Qt.Key_Meta | + Qt.Key_AltGr | Qt.Key_Super_L | Qt.Key_Super_R | + Qt.Key_Hyper_L | Qt.Key_Hyper_R | Qt.Key_Direction_L | + Qt.Key_Direction_R) + if not (key & ~modifiers): # Only modifier pressed return None - mod = e.modifiers() parts = [] for (mask, s) in modmask2str.items(): - if mod & mask and s not in parts: + if key & mask and s not in parts: parts.append(s) - key_string = key_to_string(e.key()) + key_string = key_to_string(key & ~modifiers) if len(key_string) == 1: category = unicodedata.category(key_string) is_control_char = (category == 'Cc') else: is_control_char = False - if e.modifiers() == Qt.ShiftModifier and not is_control_char: + if key & ~modifiers == Qt.ShiftModifier and not is_control_char: parts = [] parts.append(key_string) @@ -235,7 +237,8 @@ class KeySequence: # FIXME handle more than 4 keys def __str__(self): - return self._sequence.toString() + return ''.join(key_with_modifiers_to_string(key) + for key in self._sequence) def __repr__(self): return utils.get_repr(self, keys=str(self)) From f1fe26b0b73326e092438134e72f8054abd7903b Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 29 Dec 2017 14:40:00 +0100 Subject: [PATCH 019/135] Handle modifiers correctly --- qutebrowser/keyinput/keyutils.py | 35 +++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/qutebrowser/keyinput/keyutils.py b/qutebrowser/keyinput/keyutils.py index bbb081f3d..77a45a7fe 100644 --- a/qutebrowser/keyinput/keyutils.py +++ b/qutebrowser/keyinput/keyutils.py @@ -128,10 +128,10 @@ def key_to_string(key): def keyevent_to_string(e): """Convert a QKeyEvent to a meaningful name.""" - return key_with_modifiers_to_string(int(e.key()) | int(e.modifiers())) + return key_with_modifiers_to_string(e.key(), e.modifiers()) -def key_with_modifiers_to_string(key): +def key_with_modifiers_to_string(key, modifiers): """Convert a Qt.Key with modifiers to a meaningful name. Return: @@ -156,27 +156,30 @@ def key_with_modifiers_to_string(key): (Qt.MetaModifier, 'Meta'), (Qt.ShiftModifier, 'Shift'), ]) - modifiers = (Qt.Key_Control | Qt.Key_Alt | Qt.Key_Shift | Qt.Key_Meta | - Qt.Key_AltGr | Qt.Key_Super_L | Qt.Key_Super_R | - Qt.Key_Hyper_L | Qt.Key_Hyper_R | Qt.Key_Direction_L | - Qt.Key_Direction_R) - if not (key & ~modifiers): + + modifier_keys = (Qt.Key_Control, Qt.Key_Alt, Qt.Key_Shift, Qt.Key_Meta, + Qt.Key_AltGr, Qt.Key_Super_L, Qt.Key_Super_R, + Qt.Key_Hyper_L, Qt.Key_Hyper_R, Qt.Key_Direction_L, + Qt.Key_Direction_R) + if key in modifier_keys: # Only modifier pressed return None parts = [] for (mask, s) in modmask2str.items(): - if key & mask and s not in parts: + if modifiers & mask and s not in parts: parts.append(s) - key_string = key_to_string(key & ~modifiers) + key_string = key_to_string(key) + + # FIXME needed? if len(key_string) == 1: category = unicodedata.category(key_string) is_control_char = (category == 'Cc') else: is_control_char = False - if key & ~modifiers == Qt.ShiftModifier and not is_control_char: + if modifiers == Qt.ShiftModifier and not is_control_char: parts = [] parts.append(key_string) @@ -237,8 +240,16 @@ class KeySequence: # FIXME handle more than 4 keys def __str__(self): - return ''.join(key_with_modifiers_to_string(key) - for key in self._sequence) + modifier_mask = int(Qt.ShiftModifier | Qt.ControlModifier | + Qt.AltModifier | Qt.MetaModifier | + Qt.KeypadModifier | Qt.GroupSwitchModifier) + parts = [] + for key in self._sequence: + part = key_with_modifiers_to_string( + key=int(key) & ~modifier_mask, + modifiers=int(key) & modifier_mask) + parts.append(part) + return ''.join(parts) def __repr__(self): return utils.get_repr(self, keys=str(self)) From 737ff2cc690b9c848ab54b33eec128917be8709b Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 29 Dec 2017 14:43:04 +0100 Subject: [PATCH 020/135] Add <> around special keys in __str__ --- qutebrowser/keyinput/keyutils.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/qutebrowser/keyinput/keyutils.py b/qutebrowser/keyinput/keyutils.py index 77a45a7fe..31c9507b3 100644 --- a/qutebrowser/keyinput/keyutils.py +++ b/qutebrowser/keyinput/keyutils.py @@ -183,7 +183,13 @@ def key_with_modifiers_to_string(key, modifiers): parts = [] parts.append(key_string) - return normalize_keystr('+'.join(parts)) + normalized = normalize_keystr('+'.join(parts)) + if len(normalized) > 1: + # "special" binding + return '<{}>'.format(normalized) + else: + # "normal" binding + return normalized class KeyParseError(Exception): From 28b6b97f39aab6f6402d6d76fb0f1e99378d5a60 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 29 Dec 2017 15:41:12 +0100 Subject: [PATCH 021/135] Try to have strings in KeyConfig --- qutebrowser/config/config.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py index 222b0fda3..059ddc089 100644 --- a/qutebrowser/config/config.py +++ b/qutebrowser/config/config.py @@ -141,8 +141,11 @@ class KeyConfig: def get_bindings_for(self, mode): """Get the combined bindings for the given mode.""" - bindings = dict(val.bindings.default[mode]) - for key, binding in val.bindings.commands[mode].items(): + bindings = self._config.get_obj( + 'bindings.default', mutable=False)[mode] + bindings_commands = self._config.get_obj( + 'bindings.commands', mutable=False).get(mode, {}) + for key, binding in bindings_commands.items(): if binding is None: bindings.pop(key, None) else: @@ -169,7 +172,8 @@ class KeyConfig: """Get the command for a given key (or None).""" key = self._prepare(key, mode) if default: - bindings = dict(val.bindings.default[mode]) + bindings = self._config.get_obj( + 'bindings.default', mutable=False)[mode] else: bindings = self.get_bindings_for(mode) return bindings.get(key, None) @@ -208,11 +212,12 @@ class KeyConfig: key = self._prepare(key, mode) bindings_commands = self._config.get_obj('bindings.commands') + bindings_default = self._config.get_obj('bindings.default') - if val.bindings.commands[mode].get(key, None) is not None: + if key in bindings_commands[mode]: # In custom bindings -> remove it del bindings_commands[mode][key] - elif key in val.bindings.default[mode]: + elif key in bindings_default[mode]: # In default bindings -> shadow it with None if mode not in bindings_commands: bindings_commands[mode] = {} From 7b3cb14e6ec59a64b8b4911829cc5eda4aaf1f82 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 29 Dec 2017 15:41:28 +0100 Subject: [PATCH 022/135] Revert "Try to have strings in KeyConfig" This reverts commit 28b6b97f39aab6f6402d6d76fb0f1e99378d5a60. --- qutebrowser/config/config.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py index 059ddc089..222b0fda3 100644 --- a/qutebrowser/config/config.py +++ b/qutebrowser/config/config.py @@ -141,11 +141,8 @@ class KeyConfig: def get_bindings_for(self, mode): """Get the combined bindings for the given mode.""" - bindings = self._config.get_obj( - 'bindings.default', mutable=False)[mode] - bindings_commands = self._config.get_obj( - 'bindings.commands', mutable=False).get(mode, {}) - for key, binding in bindings_commands.items(): + bindings = dict(val.bindings.default[mode]) + for key, binding in val.bindings.commands[mode].items(): if binding is None: bindings.pop(key, None) else: @@ -172,8 +169,7 @@ class KeyConfig: """Get the command for a given key (or None).""" key = self._prepare(key, mode) if default: - bindings = self._config.get_obj( - 'bindings.default', mutable=False)[mode] + bindings = dict(val.bindings.default[mode]) else: bindings = self.get_bindings_for(mode) return bindings.get(key, None) @@ -212,12 +208,11 @@ class KeyConfig: key = self._prepare(key, mode) bindings_commands = self._config.get_obj('bindings.commands') - bindings_default = self._config.get_obj('bindings.default') - if key in bindings_commands[mode]: + if val.bindings.commands[mode].get(key, None) is not None: # In custom bindings -> remove it del bindings_commands[mode][key] - elif key in bindings_default[mode]: + elif key in val.bindings.default[mode]: # In default bindings -> shadow it with None if mode not in bindings_commands: bindings_commands[mode] = {} From caa05df16d350b0119377a728040a7651be2968a Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 29 Dec 2017 15:58:20 +0100 Subject: [PATCH 023/135] Use KeySequences in config.py --- qutebrowser/completion/models/configmodel.py | 6 ++++-- qutebrowser/config/config.py | 14 ++++++++------ qutebrowser/config/configcommands.py | 11 +++++++---- 3 files changed, 19 insertions(+), 12 deletions(-) diff --git a/qutebrowser/completion/models/configmodel.py b/qutebrowser/completion/models/configmodel.py index 445a57a66..1a433a460 100644 --- a/qutebrowser/completion/models/configmodel.py +++ b/qutebrowser/completion/models/configmodel.py @@ -22,6 +22,7 @@ from qutebrowser.config import configdata, configexc from qutebrowser.completion.models import completionmodel, listcategory, util from qutebrowser.commands import runners, cmdexc +from qutebrowser.keyinput import keyutils def option(*, info): @@ -79,8 +80,9 @@ def bind(key, *, info): """ model = completionmodel.CompletionModel(column_widths=(20, 60, 20)) data = [] + seq = keyutils.KeySequence.parse(key) - cmd_text = info.keyconf.get_command(key, 'normal') + cmd_text = info.keyconf.get_command(seq, 'normal') if cmd_text: parser = runners.CommandParser() try: @@ -90,7 +92,7 @@ def bind(key, *, info): else: data.append((cmd_text, '(Current) {}'.format(cmd.desc), key)) - cmd_text = info.keyconf.get_command(key, 'normal', default=True) + cmd_text = info.keyconf.get_command(seq, 'normal', default=True) if cmd_text: parser = runners.CommandParser() cmd = parser.parse(cmd_text).cmd diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py index 222b0fda3..4d83db409 100644 --- a/qutebrowser/config/config.py +++ b/qutebrowser/config/config.py @@ -135,9 +135,11 @@ class KeyConfig: def _prepare(self, key, mode): """Make sure the given mode exists and normalize the key.""" + # Catch old usage of this code + assert isinstance(key, keyutils.KeySequence), key if mode not in configdata.DATA['bindings.default'].default: raise configexc.KeybindingError("Invalid mode {}!".format(mode)) - return str(keyutils.KeySequence.parse(key)) + return key def get_bindings_for(self, mode): """Get the combined bindings for the given mode.""" @@ -188,7 +190,7 @@ class KeyConfig: bindings = self._config.get_obj('bindings.commands') if mode not in bindings: bindings[mode] = {} - bindings[mode][key] = command + bindings[mode][str(key)] = command self._config.update_mutables(save_yaml=save_yaml) def bind_default(self, key, *, mode='normal', save_yaml=False): @@ -197,7 +199,7 @@ class KeyConfig: bindings_commands = self._config.get_obj('bindings.commands') try: - del bindings_commands[mode][key] + del bindings_commands[mode][str(key)] except KeyError: raise configexc.KeybindingError( "Can't find binding '{}' in {} mode".format(key, mode)) @@ -209,14 +211,14 @@ class KeyConfig: bindings_commands = self._config.get_obj('bindings.commands') - if val.bindings.commands[mode].get(key, None) is not None: + if str(key) in bindings_commands.get(mode, {}): # In custom bindings -> remove it - del bindings_commands[mode][key] + del bindings_commands[mode][str(key)] elif key in val.bindings.default[mode]: # In default bindings -> shadow it with None if mode not in bindings_commands: bindings_commands[mode] = {} - bindings_commands[mode][key] = None + bindings_commands[mode][str(key)] = None else: raise configexc.KeybindingError( "Can't find binding '{}' in {} mode".format(key, mode)) diff --git a/qutebrowser/config/configcommands.py b/qutebrowser/config/configcommands.py index 711fe861c..6d6d53c0c 100644 --- a/qutebrowser/config/configcommands.py +++ b/qutebrowser/config/configcommands.py @@ -29,6 +29,7 @@ from qutebrowser.completion.models import configmodel from qutebrowser.utils import objreg, utils, message, standarddir from qutebrowser.config import configtypes, configexc, configfiles, configdata from qutebrowser.misc import editor +from qutebrowser.keyinput import keyutils class ConfigCommands: @@ -108,11 +109,12 @@ class ConfigCommands: available modes. default: If given, restore a default binding. """ + seq = keyutils.KeySequence(key) if command is None: if default: # :bind --default: Restore default with self._handle_config_error(): - self._keyconfig.bind_default(key, mode=mode, + self._keyconfig.bind_default(seq, mode=mode, save_yaml=True) return @@ -122,7 +124,7 @@ class ConfigCommands: # # normalized for the output below # key = utils.normalize_keystr(key) with self._handle_config_error(): - cmd = self._keyconfig.get_command(key, mode) + cmd = self._keyconfig.get_command(seq, mode) if cmd is None: message.info("{} is unbound in {} mode".format(key, mode)) else: @@ -131,7 +133,7 @@ class ConfigCommands: return with self._handle_config_error(): - self._keyconfig.bind(key, command, mode=mode, save_yaml=True) + self._keyconfig.bind(seq, command, mode=mode, save_yaml=True) @cmdutils.register(instance='config-commands') def unbind(self, key, *, mode='normal'): @@ -143,7 +145,8 @@ class ConfigCommands: See `:help bindings.commands` for the available modes. """ with self._handle_config_error(): - self._keyconfig.unbind(key, mode=mode, save_yaml=True) + self._keyconfig.unbind(keyutils.KeySequence.parse(key), mode=mode, + save_yaml=True) @cmdutils.register(instance='config-commands', star_args_optional=True) @cmdutils.argument('option', completion=configmodel.option) From 81e90602393ae295d4dbbeeaf96eba797388334a Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 29 Dec 2017 16:01:30 +0100 Subject: [PATCH 024/135] Make sure KeySequence keys are valid --- qutebrowser/keyinput/keyutils.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/qutebrowser/keyinput/keyutils.py b/qutebrowser/keyinput/keyutils.py index 31c9507b3..c67ac7024 100644 --- a/qutebrowser/keyinput/keyutils.py +++ b/qutebrowser/keyinput/keyutils.py @@ -243,6 +243,8 @@ class KeySequence: def __init__(self, *args): self._sequence = QKeySequence(*args) + for key in self._sequence: + assert key != Qt.Key_unknown # FIXME handle more than 4 keys def __str__(self): @@ -275,6 +277,9 @@ class KeySequence: def __hash__(self): return hash(self._sequence) + def __len__(self): + return len(self._sequence) + def matches(self, other): # pylint: disable=protected-access return self._sequence.matches(other._sequence) @@ -306,4 +311,6 @@ class KeySequence: def parse(cls, keystr): """Parse a keystring like or xyz and return a KeySequence.""" s = ', '.join(_parse_keystring(keystr)) - return cls(s) + new = cls(s) + assert len(new) > 0 + return new From a145497c654ffbf35cbf549c20fb6da47a8a9290 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 29 Dec 2017 16:05:16 +0100 Subject: [PATCH 025/135] Make :unbind work correctly --- qutebrowser/config/configcommands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutebrowser/config/configcommands.py b/qutebrowser/config/configcommands.py index 6d6d53c0c..d171d164f 100644 --- a/qutebrowser/config/configcommands.py +++ b/qutebrowser/config/configcommands.py @@ -109,7 +109,7 @@ class ConfigCommands: available modes. default: If given, restore a default binding. """ - seq = keyutils.KeySequence(key) + seq = keyutils.KeySequence.parse(key) if command is None: if default: # :bind --default: Restore default From dcf89f7a2889698b4a45e28c157581350bcdd2b2 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 29 Dec 2017 16:10:12 +0100 Subject: [PATCH 026/135] Fix KeyConfig._prepare --- qutebrowser/config/config.py | 11 +++++------ tests/unit/config/test_config.py | 19 +++++++++---------- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py index 4d83db409..9ac05c7d6 100644 --- a/qutebrowser/config/config.py +++ b/qutebrowser/config/config.py @@ -134,12 +134,11 @@ class KeyConfig: self._config = config def _prepare(self, key, mode): - """Make sure the given mode exists and normalize the key.""" + """Make sure the given mode exists.""" # Catch old usage of this code assert isinstance(key, keyutils.KeySequence), key if mode not in configdata.DATA['bindings.default'].default: raise configexc.KeybindingError("Invalid mode {}!".format(mode)) - return key def get_bindings_for(self, mode): """Get the combined bindings for the given mode.""" @@ -169,7 +168,7 @@ class KeyConfig: def get_command(self, key, mode, default=False): """Get the command for a given key (or None).""" - key = self._prepare(key, mode) + self._prepare(key, mode) if default: bindings = dict(val.bindings.default[mode]) else: @@ -183,7 +182,7 @@ class KeyConfig: "Can't add binding '{}' with empty command in {} " 'mode'.format(key, mode)) - key = self._prepare(key, mode) + self._prepare(key, mode) log.keyboard.vdebug("Adding binding {} -> {} in mode {}.".format( key, command, mode)) @@ -195,7 +194,7 @@ class KeyConfig: def bind_default(self, key, *, mode='normal', save_yaml=False): """Restore a default keybinding.""" - key = self._prepare(key, mode) + self._prepare(key, mode) bindings_commands = self._config.get_obj('bindings.commands') try: @@ -207,7 +206,7 @@ class KeyConfig: def unbind(self, key, *, mode='normal', save_yaml=False): """Unbind the given key in the given mode.""" - key = self._prepare(key, mode) + self._prepare(key, mode) bindings_commands = self._config.get_obj('bindings.commands') diff --git a/tests/unit/config/test_config.py b/tests/unit/config/test_config.py index d8bf73700..964391154 100644 --- a/tests/unit/config/test_config.py +++ b/tests/unit/config/test_config.py @@ -29,6 +29,7 @@ from PyQt5.QtGui import QColor from qutebrowser.config import config, configdata, configexc, configfiles from qutebrowser.utils import usertypes from qutebrowser.misc import objects +from qutebrowser.keyinput import keyutils @pytest.fixture(autouse=True) @@ -98,18 +99,16 @@ class TestKeyConfig: """Get a dict with no bindings.""" return {'normal': {}} - @pytest.mark.parametrize('key, expected', [ - ('A', 'A'), - ('', ''), - ]) - def test_prepare_valid(self, key_config_stub, key, expected): - """Make sure prepare normalizes the key.""" - assert key_config_stub._prepare(key, 'normal') == expected - - def test_prepare_invalid(self, key_config_stub): + def test_prepare_invalid_mode(self, key_config_stub): """Make sure prepare checks the mode.""" + seq = keyutils.KeySequence('x') with pytest.raises(configexc.KeybindingError): - assert key_config_stub._prepare('x', 'abnormal') + assert key_config_stub._prepare(seq, 'abnormal') + + def test_prepare_invalid_type(self, key_config_stub): + """Make sure prepare checks the type.""" + with pytest.raises(AssertionError): + assert key_config_stub._prepare('x', 'normal') @pytest.mark.parametrize('commands, expected', [ # Unbinding default key From dc66ec5d8c8cb9e7a92d6e5294805220ed91382c Mon Sep 17 00:00:00 2001 From: Fritz Reichwald Date: Sat, 6 Jan 2018 20:01:57 +0100 Subject: [PATCH 027/135] Fix expectation in Fullscreen info message to fit new description of --- tests/unit/misc/test_miscwidgets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/misc/test_miscwidgets.py b/tests/unit/misc/test_miscwidgets.py index 1ee351a81..201b8ce30 100644 --- a/tests/unit/misc/test_miscwidgets.py +++ b/tests/unit/misc/test_miscwidgets.py @@ -106,7 +106,7 @@ class TestFullscreenNotification: @pytest.mark.parametrize('bindings, text', [ ({'': 'fullscreen --leave'}, - "Press Escape to exit fullscreen."), + "Press to exit fullscreen."), ({'': 'fullscreen'}, "Page is now fullscreen."), ({'a': 'fullscreen --leave'}, "Press a to exit fullscreen."), ({}, "Page is now fullscreen."), From 9b4da25578ea4dbaf73cd499e66b5bd8fc269aac Mon Sep 17 00:00:00 2001 From: Fritz Reichwald Date: Sun, 7 Jan 2018 00:11:47 +0100 Subject: [PATCH 028/135] Fix another test by using the new KeySequence --- tests/unit/keyinput/test_keyutils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/keyinput/test_keyutils.py b/tests/unit/keyinput/test_keyutils.py index 4311a4aeb..8a62071a3 100644 --- a/tests/unit/keyinput/test_keyutils.py +++ b/tests/unit/keyinput/test_keyutils.py @@ -131,5 +131,5 @@ def test_parse(keystr, expected): ('control+x', 'ctrl+x') ]) def test_normalize_keystr(orig, repl): - assert keyutils.normalize_keystr(orig) == repl + assert keyutils.KeySequence(orig) == repl From e273f163a634d4423bfb739a0c80b7bcd7529056 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 25 Feb 2018 22:09:39 +0100 Subject: [PATCH 029/135] Add a KeyInfo class --- qutebrowser/keyinput/keyutils.py | 151 +++++++++++++++++-------------- 1 file changed, 84 insertions(+), 67 deletions(-) diff --git a/qutebrowser/keyinput/keyutils.py b/qutebrowser/keyinput/keyutils.py index c67ac7024..1e8b3f248 100644 --- a/qutebrowser/keyinput/keyutils.py +++ b/qutebrowser/keyinput/keyutils.py @@ -128,68 +128,7 @@ def key_to_string(key): def keyevent_to_string(e): """Convert a QKeyEvent to a meaningful name.""" - return key_with_modifiers_to_string(e.key(), e.modifiers()) - - -def key_with_modifiers_to_string(key, modifiers): - """Convert a Qt.Key with modifiers to a meaningful name. - - Return: - A name of the key (combination) as a string or - None if only modifiers are pressed.. - """ - if utils.is_mac: - # Qt swaps Ctrl/Meta on macOS, so we switch it back here so the user - # can use it in the config as expected. See: - # https://github.com/qutebrowser/qutebrowser/issues/110 - # http://doc.qt.io/qt-5.4/osx-issues.html#special-keys - modmask2str = collections.OrderedDict([ - (Qt.MetaModifier, 'Ctrl'), - (Qt.AltModifier, 'Alt'), - (Qt.ControlModifier, 'Meta'), - (Qt.ShiftModifier, 'Shift'), - ]) - else: - modmask2str = collections.OrderedDict([ - (Qt.ControlModifier, 'Ctrl'), - (Qt.AltModifier, 'Alt'), - (Qt.MetaModifier, 'Meta'), - (Qt.ShiftModifier, 'Shift'), - ]) - - modifier_keys = (Qt.Key_Control, Qt.Key_Alt, Qt.Key_Shift, Qt.Key_Meta, - Qt.Key_AltGr, Qt.Key_Super_L, Qt.Key_Super_R, - Qt.Key_Hyper_L, Qt.Key_Hyper_R, Qt.Key_Direction_L, - Qt.Key_Direction_R) - if key in modifier_keys: - # Only modifier pressed - return None - parts = [] - - for (mask, s) in modmask2str.items(): - if modifiers & mask and s not in parts: - parts.append(s) - - key_string = key_to_string(key) - - # FIXME needed? - if len(key_string) == 1: - category = unicodedata.category(key_string) - is_control_char = (category == 'Cc') - else: - is_control_char = False - - if modifiers == Qt.ShiftModifier and not is_control_char: - parts = [] - - parts.append(key_string) - normalized = normalize_keystr('+'.join(parts)) - if len(normalized) > 1: - # "special" binding - return '<{}>'.format(normalized) - else: - # "normal" binding - return normalized + return str(KeyInfo(e.key(), e.modifiers())) class KeyParseError(Exception): @@ -239,6 +178,80 @@ def normalize_keystr(keystr): return 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() + + def __str__(self): + """Convert this KeyInfo to a meaningful name. + + Return: + A name of the key (combination) as a string or + an empty string if only modifiers are pressed. + """ + if utils.is_mac: + # Qt swaps Ctrl/Meta on macOS, so we switch it back here so the user + # can use it in the config as expected. See: + # https://github.com/qutebrowser/qutebrowser/issues/110 + # http://doc.qt.io/qt-5.4/osx-issues.html#special-keys + modmask2str = collections.OrderedDict([ + (Qt.MetaModifier, 'Ctrl'), + (Qt.AltModifier, 'Alt'), + (Qt.ControlModifier, 'Meta'), + (Qt.ShiftModifier, 'Shift'), + ]) + else: + modmask2str = collections.OrderedDict([ + (Qt.ControlModifier, 'Ctrl'), + (Qt.AltModifier, 'Alt'), + (Qt.MetaModifier, 'Meta'), + (Qt.ShiftModifier, 'Shift'), + ]) + + modifier_keys = (Qt.Key_Control, Qt.Key_Alt, Qt.Key_Shift, Qt.Key_Meta, + Qt.Key_AltGr, Qt.Key_Super_L, Qt.Key_Super_R, + Qt.Key_Hyper_L, Qt.Key_Hyper_R, Qt.Key_Direction_L, + Qt.Key_Direction_R) + if self.key in modifier_keys: + # Only modifier pressed + return '' + parts = [] + + for (mask, s) in modmask2str.items(): + if self.modifiers & mask and s not in parts: + parts.append(s) + + key_string = key_to_string(self.key) + + # FIXME needed? + if len(key_string) == 1: + category = unicodedata.category(key_string) + is_control_char = (category == 'Cc') + else: + is_control_char = False + + if self.modifiers == Qt.ShiftModifier and not is_control_char: + parts = [] + + parts.append(key_string) + normalized = normalize_keystr('+'.join(parts)) + if len(normalized) > 1: + # "special" binding + return '<{}>'.format(normalized) + else: + # "normal" binding + return normalized + + class KeySequence: def __init__(self, *args): @@ -248,16 +261,20 @@ class KeySequence: # FIXME handle more than 4 keys def __str__(self): + parts = [] + for info in self._sequence: + parts.append(str(info)) + return ''.join(parts) + + def __iter__(self): + """Iterate over KeyInfo objects.""" modifier_mask = int(Qt.ShiftModifier | Qt.ControlModifier | Qt.AltModifier | Qt.MetaModifier | Qt.KeypadModifier | Qt.GroupSwitchModifier) - parts = [] for key in self._sequence: - part = key_with_modifiers_to_string( + yield KeyInfo( key=int(key) & ~modifier_mask, - modifiers=int(key) & modifier_mask) - parts.append(part) - return ''.join(parts) + modifiers=Qt.KeyboardModifiers(int(key) & modifier_mask)) def __repr__(self): return utils.get_repr(self, keys=str(self)) From 79a337767a70bc562abcbc347b50fca8cc26bdcd Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 26 Feb 2018 05:20:57 +0100 Subject: [PATCH 030/135] Initial work at making :fake-key work --- qutebrowser/browser/commands.py | 16 +++++++--------- qutebrowser/keyinput/keyutils.py | 8 ++++++++ 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 8d7c0c2cf..701c8324f 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -34,7 +34,7 @@ from qutebrowser.commands import userscripts, cmdexc, cmdutils, runners from qutebrowser.config import config, configdata from qutebrowser.browser import (urlmarks, browsertab, inspector, navigate, webelem, downloads) -from qutebrowser.keyinput import modeman +from qutebrowser.keyinput import modeman, keyutils from qutebrowser.utils import (message, usertypes, log, qtutils, urlutils, objreg, utils, standarddir) from qutebrowser.utils.usertypes import KeyMode @@ -2111,17 +2111,15 @@ class CommandDispatcher: keystring: The keystring to send. global_: If given, the keys are sent to the qutebrowser UI. """ - # FIXME: rewrite try: - keyinfos = utils.parse_keystring(keystring) - except utils.KeyParseError as e: + sequence = keyutils.KeySequence.parse(keystring) + except keyutils.KeyParseError as e: raise cmdexc.CommandError(str(e)) - for keyinfo in keyinfos: - press_event = QKeyEvent(QEvent.KeyPress, keyinfo.key, - keyinfo.modifiers, keyinfo.text) - release_event = QKeyEvent(QEvent.KeyRelease, keyinfo.key, - keyinfo.modifiers, keyinfo.text) + for keyinfo in sequence: + args = (keyinfo.key, keyinfo.modifiers, keyinfo.text()) + press_event = QKeyEvent(QEvent.KeyPress, *args) + release_event = QKeyEvent(QEvent.KeyRelease, *args) if global_: window = QApplication.focusWindow() diff --git a/qutebrowser/keyinput/keyutils.py b/qutebrowser/keyinput/keyutils.py index 1e8b3f248..b0b05851a 100644 --- a/qutebrowser/keyinput/keyutils.py +++ b/qutebrowser/keyinput/keyutils.py @@ -251,6 +251,13 @@ class KeyInfo: # "normal" binding return normalized + def text(self): + """Get the text which would be displayed when pressing this key.""" + text = QKeySequence(self.key).toString() + if not self.modifiers & Qt.ShiftModifier: + text = text.lower() + return text + class KeySequence: @@ -327,6 +334,7 @@ class KeySequence: @classmethod def parse(cls, keystr): """Parse a keystring like or xyz and return a KeySequence.""" + # FIXME have multiple sequences in self! s = ', '.join(_parse_keystring(keystr)) new = cls(s) assert len(new) > 0 From d077f38ac41da7e534e6f05350b4ab6afec417ad Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 26 Feb 2018 09:13:32 +0100 Subject: [PATCH 031/135] Store multiple QKeySequences in KeySequence --- qutebrowser/keyinput/keyutils.py | 68 +++++++++++++++++++++++--------- qutebrowser/utils/utils.py | 10 +++++ 2 files changed, 60 insertions(+), 18 deletions(-) diff --git a/qutebrowser/keyinput/keyutils.py b/qutebrowser/keyinput/keyutils.py index b0b05851a..519b9d86a 100644 --- a/qutebrowser/keyinput/keyutils.py +++ b/qutebrowser/keyinput/keyutils.py @@ -21,6 +21,7 @@ import unicodedata import collections +import itertools import attr from PyQt5.QtCore import Qt @@ -261,15 +262,23 @@ class KeyInfo: class KeySequence: - def __init__(self, *args): - self._sequence = QKeySequence(*args) - for key in self._sequence: - assert key != Qt.Key_unknown - # FIXME handle more than 4 keys + _MAX_LEN = 4 + + def __init__(self, strings=None): + self._sequences = [] + if strings is None: + strings = [] + + for sub in utils.chunk(strings, 4): + # Catch old API usage FIXME + assert all(isinstance(s, str) for s in sub) + sequence = QKeySequence(', '.join(sub)) + self._sequences.append(sequence) + self._validate() def __str__(self): parts = [] - for info in self._sequence: + for info in self: parts.append(str(info)) return ''.join(parts) @@ -278,7 +287,7 @@ class KeySequence: modifier_mask = int(Qt.ShiftModifier | Qt.ControlModifier | Qt.AltModifier | Qt.MetaModifier | Qt.KeypadModifier | Qt.GroupSwitchModifier) - for key in self._sequence: + for key in itertools.chain.from_iterable(self._sequences): yield KeyInfo( key=int(key) & ~modifier_mask, modifiers=Qt.KeyboardModifiers(int(key) & modifier_mask)) @@ -287,26 +296,38 @@ class KeySequence: return utils.get_repr(self, keys=str(self)) def __lt__(self, other): - return self._sequence < other._sequence + return self._sequences < other._sequences def __gt__(self, other): - return self._sequence > other._sequence + return self._sequences > other._sequences def __eq__(self, other): - return self._sequence == other._sequence + return self._sequences == other._sequences def __ne__(self, other): - return self._sequence != other._sequence + return self._sequences != other._sequences def __hash__(self): - return hash(self._sequence) + # FIXME is this correct? + return hash(tuple(self._sequences)) def __len__(self): - return len(self._sequence) + return sum(len(seq) for seq in self._sequences) + + def _validate(self): + for info in self: + assert info.key != Qt.Key_unknown def matches(self, other): + # FIXME test this # pylint: disable=protected-access - return self._sequence.matches(other._sequence) + assert self._sequences + assert other._sequences + for seq1, seq2 in zip(self._sequences, other._sequences): + match = seq1.matches(seq2) + if match != QKeySequence.ExactMatch: + return match + return QKeySequence.ExactMatch def append_event(self, ev): """Create a new KeySequence object with the given QKeyEvent added. @@ -325,17 +346,28 @@ class KeySequence: FIXME: create test cases! """ + # pylint: disable=protected-access + new = self.__class__() + new._sequences = self._sequences[:] + modifiers = ev.modifiers() if (modifiers == Qt.ShiftModifier and unicodedata.category(ev.text()) != 'Lu'): modifiers = Qt.KeyboardModifiers() - return self.__class__(*self._sequence, modifiers | ev.key()) + + if new._sequences and len(new._sequences[-1]) < self._MAX_LEN: + new._sequences[-1] = QKeySequence(*new._sequences[-1], + ev.key() | int(modifiers)) + else: + new._sequences.append(QKeySequence(ev.key() | int(modifiers))) + + new._validate() + return new @classmethod def parse(cls, keystr): """Parse a keystring like or xyz and return a KeySequence.""" - # FIXME have multiple sequences in self! - s = ', '.join(_parse_keystring(keystr)) - new = cls(s) + parts = list(_parse_keystring(keystr)) + new = cls(parts) assert len(new) > 0 return new diff --git a/qutebrowser/utils/utils.py b/qutebrowser/utils/utils.py index 7c1d43d2b..9d95069c5 100644 --- a/qutebrowser/utils/utils.py +++ b/qutebrowser/utils/utils.py @@ -659,3 +659,13 @@ def yaml_dump(data, f=None): return None else: return yaml_data.decode('utf-8') + + +def chunk(elems, n): + """Yield successive n-sized chunks from elems. + + If elems % n != 0, the last chunk will be smaller. + """ + # FIXME test this + for i in range(0, len(elems), n): + yield elems[i:i + n] From be4cd94207e3da5743227e32af8b949e61f1b34d Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 26 Feb 2018 10:14:30 +0100 Subject: [PATCH 032/135] Try getting hints to work --- qutebrowser/keyinput/basekeyparser.py | 5 ++- qutebrowser/keyinput/keyutils.py | 12 +++++++ qutebrowser/keyinput/modeparsers.py | 45 ++++++++++++++------------- 3 files changed, 39 insertions(+), 23 deletions(-) diff --git a/qutebrowser/keyinput/basekeyparser.py b/qutebrowser/keyinput/basekeyparser.py index 1052c0eeb..774decd6e 100644 --- a/qutebrowser/keyinput/basekeyparser.py +++ b/qutebrowser/keyinput/basekeyparser.py @@ -118,7 +118,7 @@ class BaseKeyParser(QObject): e: the KeyPressEvent from Qt. Return: - A self.Match member. + A QKeySequence match or None. """ key = e.key() txt = keyutils.keyevent_to_string(e) @@ -186,8 +186,10 @@ class BaseKeyParser(QObject): - The found binding with Match.definitive. """ assert sequence + assert not isinstance(sequence, str) for seq, cmd in self.bindings.items(): + assert not isinstance(seq, str), seq match = sequence.matches(seq) if match != QKeySequence.NoMatch: return (match, cmd) @@ -238,6 +240,7 @@ class BaseKeyParser(QObject): self.bindings = {} for key, cmd in config.key_instance.get_bindings_for(modename).items(): + assert not isinstance(key, str), key assert cmd self.bindings[key] = cmd diff --git a/qutebrowser/keyinput/keyutils.py b/qutebrowser/keyinput/keyutils.py index 519b9d86a..626808f4a 100644 --- a/qutebrowser/keyinput/keyutils.py +++ b/qutebrowser/keyinput/keyutils.py @@ -352,6 +352,7 @@ class KeySequence: modifiers = ev.modifiers() if (modifiers == Qt.ShiftModifier and + ev.text() and unicodedata.category(ev.text()) != 'Lu'): modifiers = Qt.KeyboardModifiers() @@ -364,6 +365,17 @@ class KeySequence: new._validate() return new + def remove_last(self): + """Create a new KeySequence with the last key removed.""" + new = self.__class__() + new._sequences = self._sequeces[:] + if len(new._sequences[-1]) == 1: + del new._sequences[-1] + else: + new._sequences[-1] = QKeySequence(*new._sequences[-1][:-1]) + new._validate() + return new + @classmethod def parse(cls, keystr): """Parse a keystring like or xyz and return a KeySequence.""" diff --git a/qutebrowser/keyinput/modeparsers.py b/qutebrowser/keyinput/modeparsers.py index 2e23e2aa5..37fde6a14 100644 --- a/qutebrowser/keyinput/modeparsers.py +++ b/qutebrowser/keyinput/modeparsers.py @@ -27,6 +27,7 @@ import traceback import enum from PyQt5.QtCore import pyqtSlot, Qt +from PyQt5.QtGui import QKeySequence from qutebrowser.commands import cmdexc from qutebrowser.config import config @@ -73,9 +74,9 @@ 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 + return QKeySequence.NoMatch match = super()._handle_single_key(e) - if match == self.Match.partial: + if match == QKeySequence.PartialMatch: timeout = config.val.input.partial_timeout if timeout != 0: self._partial_timer.setInterval(timeout) @@ -97,9 +98,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): @@ -174,28 +175,28 @@ class HintKeyParser(keyparser.CommandKeyParser): 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: + elif self._last_press == LastPress.keystring and self._sequence: + self._sequence = self._sequence.remove_last() + 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 else: - return super()._handle_special_key(e) + return False elif hintmanager.current_mode() != 'number': - return super()._handle_special_key(e) + return False elif not e.text(): - return super()._handle_special_key(e) + return False else: self._filtertext += e.text() hintmanager.filter_hints(self._filtertext) @@ -212,17 +213,17 @@ class HintKeyParser(keyparser.CommandKeyParser): True if the match has been handled, False otherwise. """ # FIXME rewrite this - match = self._handle_single_key(e) - if match == self.Match.partial: - self.keystring_updated.emit(self._keystring) + match = super().handle(e) + if match == QKeySequence.PartialMatch: + self.keystring_updated.emit(str(self._sequence)) 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: + elif match is None: # FIXME 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) else: @@ -248,7 +249,7 @@ class HintKeyParser(keyparser.CommandKeyParser): preserve_filter: Whether to keep the current value of `self._filtertext`. """ - self.bindings = {s: s for s in strings} + self.bindings = {keyutils.KeySequence(s): s for s in strings} if not preserve_filter: self._filtertext = '' From 9aa37febbedf3232d050c83b724a98994882310e Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 26 Feb 2018 10:33:18 +0100 Subject: [PATCH 033/135] Make hints work --- qutebrowser/keyinput/basekeyparser.py | 3 ++- qutebrowser/keyinput/modeparsers.py | 32 ++++++++++++++++----------- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/qutebrowser/keyinput/basekeyparser.py b/qutebrowser/keyinput/basekeyparser.py index 774decd6e..5830171b8 100644 --- a/qutebrowser/keyinput/basekeyparser.py +++ b/qutebrowser/keyinput/basekeyparser.py @@ -61,6 +61,7 @@ class BaseKeyParser(QObject): _sequence: The currently entered key sequence _modename: The name of the input mode associated with this keyparser. _supports_count: Whether count is supported + # FIXME is this still needed? _supports_chains: Whether keychains are supported Signals: @@ -138,7 +139,7 @@ class BaseKeyParser(QObject): # self._debug_log("Ignoring, no text char") # return QKeySequence.NoMatch - if txt.isdigit(): + if txt.isdigit() and self._supports_count: assert len(txt) == 1, txt self._count += txt return None diff --git a/qutebrowser/keyinput/modeparsers.py b/qutebrowser/keyinput/modeparsers.py index 37fde6a14..d22b27d22 100644 --- a/qutebrowser/keyinput/modeparsers.py +++ b/qutebrowser/keyinput/modeparsers.py @@ -213,8 +213,9 @@ class HintKeyParser(keyparser.CommandKeyParser): True if the match has been handled, False otherwise. """ # FIXME rewrite this - match = super().handle(e) + match = self._handle_key(e) if match == QKeySequence.PartialMatch: + # FIXME do we need to check self._sequence here? self.keystring_updated.emit(str(self._sequence)) self._last_press = LastPress.keystring return True @@ -229,17 +230,20 @@ class HintKeyParser(keyparser.CommandKeyParser): 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 != QKeySequence.NoMatch + + # FIXME why is this needed? + # 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) def update_bindings(self, strings, preserve_filter=False): """Update bindings when the hint strings changed. @@ -249,7 +253,9 @@ class HintKeyParser(keyparser.CommandKeyParser): preserve_filter: Whether to keep the current value of `self._filtertext`. """ - self.bindings = {keyutils.KeySequence(s): s for s in strings} + self._read_config() + self.bindings.update({keyutils.KeySequence(s): + 'follow-hint ' + s for s in strings}) if not preserve_filter: self._filtertext = '' From f92bb164083f66c88959857862a4ba1135a6a888 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 26 Feb 2018 10:38:59 +0100 Subject: [PATCH 034/135] Make config.bind work --- qutebrowser/config/configfiles.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/qutebrowser/config/configfiles.py b/qutebrowser/config/configfiles.py index ba43e5015..82f90db76 100644 --- a/qutebrowser/config/configfiles.py +++ b/qutebrowser/config/configfiles.py @@ -33,6 +33,7 @@ from PyQt5.QtCore import pyqtSignal, QObject, QSettings import qutebrowser from qutebrowser.config import configexc, config, configdata, configutils +from qutebrowser.keyinput import keyutils from qutebrowser.utils import standarddir, utils, qtutils, log, urlmatch @@ -357,12 +358,14 @@ class ConfigAPI: def bind(self, key, command, mode='normal'): """Bind a key to a command, with an optional key mode.""" with self._handle_error('binding', key): - self._keyconfig.bind(key, command, mode=mode) + seq = keyutils.KeySequence.parse(key) + self._keyconfig.bind(seq, command, mode=mode) def unbind(self, key, mode='normal'): """Unbind a key from a command, with an optional key mode.""" with self._handle_error('unbinding', key): - self._keyconfig.unbind(key, mode=mode) + seq = keyutils.KeySequence.parse(key) + self._keyconfig.unbind(seq, mode=mode) def source(self, filename): """Read the given config file from disk.""" From 16940db8342a083cc56a6492f1b34a91a5513466 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 26 Feb 2018 11:10:05 +0100 Subject: [PATCH 035/135] Refactor KeySequence initialization --- qutebrowser/keyinput/keyutils.py | 54 ++++++++++++----------------- qutebrowser/keyinput/modeparsers.py | 4 +-- qutebrowser/misc/keyhintwidget.py | 2 +- 3 files changed, 25 insertions(+), 35 deletions(-) diff --git a/qutebrowser/keyinput/keyutils.py b/qutebrowser/keyinput/keyutils.py index 626808f4a..bd12ccb2c 100644 --- a/qutebrowser/keyinput/keyutils.py +++ b/qutebrowser/keyinput/keyutils.py @@ -264,16 +264,13 @@ class KeySequence: _MAX_LEN = 4 - def __init__(self, strings=None): + def __init__(self, *keys): self._sequences = [] - if strings is None: - strings = [] - - for sub in utils.chunk(strings, 4): - # Catch old API usage FIXME - assert all(isinstance(s, str) for s in sub) - sequence = QKeySequence(', '.join(sub)) + for sub in utils.chunk(keys, self._MAX_LEN): + sequence = QKeySequence(*sub) self._sequences.append(sequence) + if keys: + assert len(self) > 0 self._validate() def __str__(self): @@ -287,7 +284,7 @@ class KeySequence: modifier_mask = int(Qt.ShiftModifier | Qt.ControlModifier | Qt.AltModifier | Qt.MetaModifier | Qt.KeypadModifier | Qt.GroupSwitchModifier) - for key in itertools.chain.from_iterable(self._sequences): + for key in self._iter_keys(): yield KeyInfo( key=int(key) & ~modifier_mask, modifiers=Qt.KeyboardModifiers(int(key) & modifier_mask)) @@ -314,6 +311,13 @@ class KeySequence: def __len__(self): return sum(len(seq) for seq in self._sequences) + def __getitem__(self, item): + keys = list(self._iter_keys()) + return self.__class__(*keys[item]) + + def _iter_keys(self): + return itertools.chain.from_iterable(self._sequences) + def _validate(self): for info in self: assert info.key != Qt.Key_unknown @@ -347,39 +351,25 @@ class KeySequence: FIXME: create test cases! """ # pylint: disable=protected-access - new = self.__class__() - new._sequences = self._sequences[:] - modifiers = ev.modifiers() if (modifiers == Qt.ShiftModifier and ev.text() and unicodedata.category(ev.text()) != 'Lu'): modifiers = Qt.KeyboardModifiers() - if new._sequences and len(new._sequences[-1]) < self._MAX_LEN: - new._sequences[-1] = QKeySequence(*new._sequences[-1], - ev.key() | int(modifiers)) - else: - new._sequences.append(QKeySequence(ev.key() | int(modifiers))) + keys = list(self._iter_keys()) + keys.append(ev.key() | int(modifiers)) - new._validate() - return new - - def remove_last(self): - """Create a new KeySequence with the last key removed.""" - new = self.__class__() - new._sequences = self._sequeces[:] - if len(new._sequences[-1]) == 1: - del new._sequences[-1] - else: - new._sequences[-1] = QKeySequence(*new._sequences[-1][:-1]) - new._validate() - return new + return self.__class__(*keys) @classmethod def parse(cls, keystr): """Parse a keystring like or xyz and return a KeySequence.""" - parts = list(_parse_keystring(keystr)) - new = cls(parts) + new = cls() + strings = list(_parse_keystring(keystr)) + for sub in utils.chunk(strings, cls._MAX_LEN): + sequence = QKeySequence(', '.join(sub)) + new._sequences.append(sequence) assert len(new) > 0 + new._validate() return new diff --git a/qutebrowser/keyinput/modeparsers.py b/qutebrowser/keyinput/modeparsers.py index d22b27d22..7237a34de 100644 --- a/qutebrowser/keyinput/modeparsers.py +++ b/qutebrowser/keyinput/modeparsers.py @@ -183,7 +183,7 @@ class HintKeyParser(keyparser.CommandKeyParser): hintmanager.filter_hints(self._filtertext) return True elif self._last_press == LastPress.keystring and self._sequence: - self._sequence = self._sequence.remove_last() + 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 @@ -254,7 +254,7 @@ class HintKeyParser(keyparser.CommandKeyParser): `self._filtertext`. """ self._read_config() - self.bindings.update({keyutils.KeySequence(s): + self.bindings.update({keyutils.KeySequence.parse(s): 'follow-hint ' + s for s in strings}) if not preserve_filter: self._filtertext = '' diff --git a/qutebrowser/misc/keyhintwidget.py b/qutebrowser/misc/keyhintwidget.py index 0aa52116e..967bdd541 100644 --- a/qutebrowser/misc/keyhintwidget.py +++ b/qutebrowser/misc/keyhintwidget.py @@ -107,7 +107,7 @@ class KeyHintView(QLabel): bindings_dict = config.key_instance.get_bindings_for(modename) bindings = [(k, v) for (k, v) in sorted(bindings_dict.items()) - if k.matches(keyutils.KeySequence(prefix)) and # FIXME + if k.matches(keyutils.KeySequence.parse(prefix)) and # FIXME not blacklisted(k) and (takes_count(v) or not countstr)] From 1609e0d445f270087e081992e06c790043d128b7 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 26 Feb 2018 11:16:56 +0100 Subject: [PATCH 036/135] Fix keyhint widget --- qutebrowser/misc/keyhintwidget.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/qutebrowser/misc/keyhintwidget.py b/qutebrowser/misc/keyhintwidget.py index 967bdd541..b9985dbd0 100644 --- a/qutebrowser/misc/keyhintwidget.py +++ b/qutebrowser/misc/keyhintwidget.py @@ -107,7 +107,7 @@ class KeyHintView(QLabel): bindings_dict = config.key_instance.get_bindings_for(modename) bindings = [(k, v) for (k, v) in sorted(bindings_dict.items()) - if k.matches(keyutils.KeySequence.parse(prefix)) and # FIXME + if keyutils.KeySequence.parse(prefix).matches(k) and not blacklisted(k) and (takes_count(v) or not countstr)] @@ -121,7 +121,7 @@ class KeyHintView(QLabel): suffix_color = html.escape(config.val.colors.keyhint.suffix.fg) text = '' - for key, cmd in bindings: + for seq, cmd in bindings: text += ( "" "{}" @@ -131,7 +131,7 @@ class KeyHintView(QLabel): ).format( html.escape(prefix), suffix_color, - html.escape(key[len(prefix):]), + html.escape(str(seq[len(prefix):])), html.escape(cmd) ) text = '{}
'.format(text) From 508a12a84cd80cb51b92038fdd4037e36052eb8a Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 26 Feb 2018 11:36:24 +0100 Subject: [PATCH 037/135] Try fixing KeyInfo.__str__ with lower-/uppercase chars --- qutebrowser/keyinput/keyutils.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/qutebrowser/keyinput/keyutils.py b/qutebrowser/keyinput/keyutils.py index bd12ccb2c..2dfced76d 100644 --- a/qutebrowser/keyinput/keyutils.py +++ b/qutebrowser/keyinput/keyutils.py @@ -233,24 +233,28 @@ class KeyInfo: key_string = key_to_string(self.key) - # FIXME needed? if len(key_string) == 1: category = unicodedata.category(key_string) - is_control_char = (category == 'Cc') + is_special_char = (category == 'Cc') else: - is_control_char = False + is_special_char = False - if self.modifiers == Qt.ShiftModifier and not is_control_char: - parts = [] + if not is_special_char: + if self.modifiers == Qt.ShiftModifier: + parts = [] + key_string = key_string.upper() + else: + key_string = key_string.lower() parts.append(key_string) - normalized = normalize_keystr('+'.join(parts)) - if len(normalized) > 1: + part_string = '+'.join(parts) + + if len(part_string) > 1: # "special" binding - return '<{}>'.format(normalized) + return '<{}>'.format(part_string) else: # "normal" binding - return normalized + return part_string def text(self): """Get the text which would be displayed when pressing this key.""" From 0afaf2ce897fa090414253263fbac4d4d6ca66ec Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 26 Feb 2018 11:48:10 +0100 Subject: [PATCH 038/135] Fix capital chars after string change --- qutebrowser/keyinput/basekeyparser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutebrowser/keyinput/basekeyparser.py b/qutebrowser/keyinput/basekeyparser.py index 5830171b8..8eafccbb5 100644 --- a/qutebrowser/keyinput/basekeyparser.py +++ b/qutebrowser/keyinput/basekeyparser.py @@ -125,7 +125,7 @@ class BaseKeyParser(QObject): txt = keyutils.keyevent_to_string(e) self._debug_log("Got key: 0x{:x} / text: '{}'".format(key, txt)) - if txt is None: + if not txt: self._debug_log("Ignoring, no text char") return QKeySequence.NoMatch From f15e2285bab01e6175b3f457312c086cf0c85b17 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 26 Feb 2018 13:41:01 +0100 Subject: [PATCH 039/135] Fix bindings.key_mappings --- qutebrowser/keyinput/basekeyparser.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/qutebrowser/keyinput/basekeyparser.py b/qutebrowser/keyinput/basekeyparser.py index 8eafccbb5..16691af4c 100644 --- a/qutebrowser/keyinput/basekeyparser.py +++ b/qutebrowser/keyinput/basekeyparser.py @@ -147,14 +147,9 @@ class BaseKeyParser(QObject): sequence = self._sequence.append_event(e) match, binding = self._match_key(sequence) if match == QKeySequence.NoMatch: - mappings = config.val.bindings.key_mappings - mapped = mappings.get(txt, None) + mapped = config.val.bindings.key_mappings.get(sequence, None) if mapped is not None: - # FIXME - raise Exception - txt = mapped - sequence = self._sequence.append_event(e) - match, binding = self._match_key(sequence) + match, binding = self._match_key(mapped) self._sequence = self._sequence.append_event(e) if match == QKeySequence.ExactMatch: From 2698b8bb63143501610eba5b47aa3c1e18cfc033 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 26 Feb 2018 13:47:45 +0100 Subject: [PATCH 040/135] Fix unicodedata check --- qutebrowser/keyinput/keyutils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/qutebrowser/keyinput/keyutils.py b/qutebrowser/keyinput/keyutils.py index 2dfced76d..a82acbced 100644 --- a/qutebrowser/keyinput/keyutils.py +++ b/qutebrowser/keyinput/keyutils.py @@ -356,8 +356,9 @@ class KeySequence: """ # pylint: disable=protected-access modifiers = ev.modifiers() + if (modifiers == Qt.ShiftModifier and - ev.text() and + len(ev.text()) == 1 and unicodedata.category(ev.text()) != 'Lu'): modifiers = Qt.KeyboardModifiers() From bb647123b722e044400250bc079cb7d09c6718f5 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 26 Feb 2018 14:13:46 +0100 Subject: [PATCH 041/135] Fix invalid key sequences --- qutebrowser/keyinput/keyutils.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/qutebrowser/keyinput/keyutils.py b/qutebrowser/keyinput/keyutils.py index a82acbced..53d9fe2c3 100644 --- a/qutebrowser/keyinput/keyutils.py +++ b/qutebrowser/keyinput/keyutils.py @@ -137,7 +137,11 @@ 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)) + 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): @@ -322,9 +326,10 @@ class KeySequence: def _iter_keys(self): return itertools.chain.from_iterable(self._sequences) - def _validate(self): + def _validate(self, keystr=None): for info in self: - assert info.key != Qt.Key_unknown + if info.key == Qt.Key_unknown: + raise KeyParseError(keystr, "Got unknown key!") def matches(self, other): # FIXME test this @@ -376,5 +381,5 @@ class KeySequence: sequence = QKeySequence(', '.join(sub)) new._sequences.append(sequence) assert len(new) > 0 - new._validate() + new._validate(keystr) return new From 1444634abb1ed2d1e3d4c34bb3dfd48a7dd73b29 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 26 Feb 2018 14:26:12 +0100 Subject: [PATCH 042/135] Fix :fake-key test --- tests/end2end/features/keyinput.feature | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/end2end/features/keyinput.feature b/tests/end2end/features/keyinput.feature index ee5b667e8..5628a5796 100644 --- a/tests/end2end/features/keyinput.feature +++ b/tests/end2end/features/keyinput.feature @@ -54,7 +54,7 @@ Feature: Keyboard input Scenario: :fake-key with an unparsable key When I run :fake-key - Then the error "Could not parse 'blub': Got unknown key." should be shown + Then the error "Could not parse '': Got unknown key." should be shown Scenario: :fake-key sending key to the website When I open data/keyinput/log.html From f1b20f6dc478933efa361eb6ce0c9f5af2891357 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 26 Feb 2018 20:02:43 +0100 Subject: [PATCH 043/135] Fix forward_unbound_keys test --- qutebrowser/keyinput/keyutils.py | 4 ++++ tests/end2end/features/keyinput.feature | 9 ++++----- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/qutebrowser/keyinput/keyutils.py b/qutebrowser/keyinput/keyutils.py index 53d9fe2c3..f0832db15 100644 --- a/qutebrowser/keyinput/keyutils.py +++ b/qutebrowser/keyinput/keyutils.py @@ -263,6 +263,10 @@ class KeyInfo: def text(self): """Get the text which would be displayed when pressing this key.""" text = QKeySequence(self.key).toString() + if len(text) > 1: + # Special key? + return '' + if not self.modifiers & Qt.ShiftModifier: text = text.lower() return text diff --git a/tests/end2end/features/keyinput.feature b/tests/end2end/features/keyinput.feature index 5628a5796..b0cd765de 100644 --- a/tests/end2end/features/keyinput.feature +++ b/tests/end2end/features/keyinput.feature @@ -33,14 +33,13 @@ Feature: Keyboard input Scenario: Forwarding special keys When I open data/keyinput/log.html And I set input.forward_unbound_keys to auto - And I press the key "x" - And I press the key "" + And I press the keys "," # Then the javascript message "key press: 112" should be logged And the javascript message "key release: 112" should be logged - # x - And the javascript message "key press: 88" should not be logged - And the javascript message "key release: 88" should not be logged + # , + And the javascript message "key press: 188" should not be logged + And the javascript message "key release: 188" should not be logged Scenario: Forwarding no keys When I open data/keyinput/log.html From 8bce2ba8e806db1939f9c960c82040ef6222a867 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 26 Feb 2018 20:03:21 +0100 Subject: [PATCH 044/135] Fix expected message --- tests/end2end/features/keyinput.feature | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/end2end/features/keyinput.feature b/tests/end2end/features/keyinput.feature index b0cd765de..1337a48d3 100644 --- a/tests/end2end/features/keyinput.feature +++ b/tests/end2end/features/keyinput.feature @@ -53,7 +53,7 @@ Feature: Keyboard input Scenario: :fake-key with an unparsable key When I run :fake-key - Then the error "Could not parse '': Got unknown key." should be shown + Then the error "Could not parse '': Got unknown key!" should be shown Scenario: :fake-key sending key to the website When I open data/keyinput/log.html From 9f0e1a98a0c2bb255c7305984aefc553787ab84b Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 26 Feb 2018 20:22:52 +0100 Subject: [PATCH 045/135] Make hint keybinding inhibition work --- qutebrowser/keyinput/modeparsers.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/qutebrowser/keyinput/modeparsers.py b/qutebrowser/keyinput/modeparsers.py index 7237a34de..84eb5baf1 100644 --- a/qutebrowser/keyinput/modeparsers.py +++ b/qutebrowser/keyinput/modeparsers.py @@ -60,8 +60,8 @@ 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): + """Override to abort if the key is a startchar. Args: e: the KeyPressEvent from Qt. @@ -69,13 +69,14 @@ class NormalKeyParser(keyparser.CommandKeyParser): Return: A self.Match member. """ - # FIXME rewrite this txt = e.text().strip() if self._inhibited: self._debug_log("Ignoring key '{}', because the normal mode is " "currently inhibited.".format(txt)) return QKeySequence.NoMatch - match = super()._handle_single_key(e) + + match = super().handle(e) + if match == QKeySequence.PartialMatch: timeout = config.val.input.partial_timeout if timeout != 0: From e9d58dae2af9cf82291dd7b441800c1dc91ecbef Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 26 Feb 2018 20:48:11 +0100 Subject: [PATCH 046/135] Fix getting individual items from KeySequence --- qutebrowser/keyinput/keyutils.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/qutebrowser/keyinput/keyutils.py b/qutebrowser/keyinput/keyutils.py index f0832db15..e2df6a443 100644 --- a/qutebrowser/keyinput/keyutils.py +++ b/qutebrowser/keyinput/keyutils.py @@ -324,8 +324,12 @@ class KeySequence: return sum(len(seq) for seq in self._sequences) def __getitem__(self, item): - keys = list(self._iter_keys()) - return self.__class__(*keys[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) From 6fc391986fe26aab29d79c6fad398db7318b4fef Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 26 Feb 2018 20:48:22 +0100 Subject: [PATCH 047/135] Fix KeyInfo.text() for space --- qutebrowser/keyinput/keyutils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/qutebrowser/keyinput/keyutils.py b/qutebrowser/keyinput/keyutils.py index e2df6a443..9be7b4d60 100644 --- a/qutebrowser/keyinput/keyutils.py +++ b/qutebrowser/keyinput/keyutils.py @@ -263,7 +263,9 @@ class KeyInfo: def text(self): """Get the text which would be displayed when pressing this key.""" text = QKeySequence(self.key).toString() - if len(text) > 1: + if self.key == Qt.Key_Space: + return ' ' + elif len(text) > 1: # Special key? return '' From de3b4adfd8fed7ad549aeb4f2f27993ad9aa2f3e Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 26 Feb 2018 20:48:49 +0100 Subject: [PATCH 048/135] Don't force-follow hints when typing chars --- qutebrowser/browser/hints.py | 11 +++++++++-- qutebrowser/keyinput/modeparsers.py | 2 +- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/qutebrowser/browser/hints.py b/qutebrowser/browser/hints.py index 0390d5d1f..1d0184c73 100644 --- a/qutebrowser/browser/hints.py +++ b/qutebrowser/browser/hints.py @@ -909,20 +909,27 @@ class HintManager(QObject): @cmdutils.register(instance='hintmanager', scope='tab', modes=[usertypes.KeyMode.hint]) - def follow_hint(self, keystring=None): + def follow_hint(self, select=False, keystring=None): """Follow a hint. Args: + select: Only select the given hint, don't necessarily follow it. keystring: The hint to follow, or None. """ if keystring is None: if self._context.to_follow is None: raise cmdexc.CommandError("No hint to follow") + elif select: + raise cmdexc.CommandError("Can't use --select without hint.") else: keystring = self._context.to_follow elif keystring not in self._context.labels: raise cmdexc.CommandError("No hint {}!".format(keystring)) - self._fire(keystring) + + if select: + self._handle_auto_follow(keystring) + else: + self._fire(keystring) @pyqtSlot(usertypes.KeyMode) def on_mode_left(self, mode): diff --git a/qutebrowser/keyinput/modeparsers.py b/qutebrowser/keyinput/modeparsers.py index 84eb5baf1..583a65707 100644 --- a/qutebrowser/keyinput/modeparsers.py +++ b/qutebrowser/keyinput/modeparsers.py @@ -256,7 +256,7 @@ class HintKeyParser(keyparser.CommandKeyParser): """ self._read_config() self.bindings.update({keyutils.KeySequence.parse(s): - 'follow-hint ' + s for s in strings}) + 'follow-hint -s ' + s for s in strings}) if not preserve_filter: self._filtertext = '' From d9ae3fd5aa70851e4e7b1f2625d8ec58652d1592 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 26 Feb 2018 20:49:02 +0100 Subject: [PATCH 049/135] Fix more hinting issues --- qutebrowser/keyinput/modeparsers.py | 1 + tests/end2end/features/hints.feature | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/qutebrowser/keyinput/modeparsers.py b/qutebrowser/keyinput/modeparsers.py index 583a65707..72f5c5027 100644 --- a/qutebrowser/keyinput/modeparsers.py +++ b/qutebrowser/keyinput/modeparsers.py @@ -170,6 +170,7 @@ class HintKeyParser(keyparser.CommandKeyParser): True if event has been handled, False otherwise. """ # FIXME rewrite this + # FIXME should backspacing be a more generic hint feature? log.keyboard.debug("Got special key 0x{:x} text {}".format( e.key(), e.text())) hintmanager = objreg.get('hintmanager', scope='tab', diff --git a/tests/end2end/features/hints.feature b/tests/end2end/features/hints.feature index eb6a24df9..a2ac468d0 100644 --- a/tests/end2end/features/hints.feature +++ b/tests/end2end/features/hints.feature @@ -338,7 +338,7 @@ Feature: Using hints And I set hints.auto_follow to unique-match And I set hints.auto_follow_timeout to 0 And I hint with args "all" - And I press the keys "ten pos" + And I press the keys "ten p" Then data/numbers/11.txt should be loaded Scenario: Scattering is ignored with number hints From 01462008c9983d75c7e39d337bf25e06d97459eb Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 26 Feb 2018 22:48:32 +0100 Subject: [PATCH 050/135] Clearly separate yesno/prompt key modes --- qutebrowser/config/configdata.yml | 20 ++++++++++++-------- qutebrowser/keyinput/modeparsers.py | 4 +--- qutebrowser/mainwindow/prompt.py | 4 ++-- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml index d23682db8..7be015fd5 100644 --- a/qutebrowser/config/configdata.yml +++ b/qutebrowser/config/configdata.yml @@ -2379,8 +2379,6 @@ bindings.default: : leave-mode prompt: : prompt-accept - y: prompt-accept yes - n: prompt-accept no : prompt-open-download : prompt-item-focus prev : prompt-item-focus prev @@ -2403,6 +2401,14 @@ bindings.default: : rl-backward-delete-char : rl-yank : leave-mode + # FIXME can we do migrations? + yesno: + : prompt-accept + y: prompt-accept yes + n: prompt-accept no + : prompt-yank + : prompt-yank --sel + : leave-mode caret: v: toggle-selection : toggle-selection @@ -2438,7 +2444,7 @@ bindings.default: none_ok: true keytype: String # section name fixed_keys: ['normal', 'insert', 'hint', 'passthrough', 'command', - 'prompt', 'caret', 'register'] + 'prompt', 'yesno', 'caret', 'register'] valtype: name: Dict none_ok: true @@ -2462,7 +2468,7 @@ bindings.commands: none_ok: true keytype: String # section name fixed_keys: ['normal', 'insert', 'hint', 'passthrough', 'command', - 'prompt', 'caret', 'register'] + 'prompt', 'yesno', 'caret', 'register'] valtype: name: Dict none_ok: true @@ -2534,10 +2540,8 @@ bindings.commands: * prompt: Entered when there's a prompt to display, like for download locations or when invoked from JavaScript. - + - You can bind normal keys in this mode, but they will be only active when - a yes/no-prompt is asked. For other prompt modes, you can only bind - special keys. + + * yesno: Entered when there's a yes/no prompt displayed. * caret: Entered when pressing the `v` mode, used to select text using the keyboard. diff --git a/qutebrowser/keyinput/modeparsers.py b/qutebrowser/keyinput/modeparsers.py index 72f5c5027..0b196c23d 100644 --- a/qutebrowser/keyinput/modeparsers.py +++ b/qutebrowser/keyinput/modeparsers.py @@ -133,9 +133,7 @@ class PromptKeyParser(keyparser.CommandKeyParser): 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') + self._read_config('yesno') def __repr__(self): return utils.get_repr(self) diff --git a/qutebrowser/mainwindow/prompt.py b/qutebrowser/mainwindow/prompt.py index 931d32654..90415b261 100644 --- a/qutebrowser/mainwindow/prompt.py +++ b/qutebrowser/mainwindow/prompt.py @@ -507,8 +507,8 @@ class _BasePrompt(QWidget): self._key_grid = QGridLayout() self._key_grid.setVerticalSpacing(0) - # The bindings are all in the 'prompt' mode, even for yesno prompts - all_bindings = config.key_instance.get_reverse_bindings_for('prompt') + all_bindings = config.key_instance.get_reverse_bindings_for( + self.KEY_MODE.name) labels = [] for cmd, text in self._allowed_commands(): From 8416e97c6c8fba7088bc04934b2523e0f1b18da7 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 27 Feb 2018 06:50:57 +0100 Subject: [PATCH 051/135] Fix type which is stubbed in test_models --- tests/unit/completion/test_models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/completion/test_models.py b/tests/unit/completion/test_models.py index 32e6920fe..c3593031c 100644 --- a/tests/unit/completion/test_models.py +++ b/tests/unit/completion/test_models.py @@ -99,7 +99,7 @@ def configdata_stub(config_stub, monkeypatch, configdata_init): typ=configtypes.Dict( keytype=configtypes.String(), valtype=configtypes.Dict( - keytype=configtypes.String(), + keytype=configtypes.Key(), valtype=configtypes.Command(), ), ), @@ -117,7 +117,7 @@ def configdata_stub(config_stub, monkeypatch, configdata_init): typ=configtypes.Dict( keytype=configtypes.String(), valtype=configtypes.Dict( - keytype=configtypes.String(), + keytype=configtypes.Key(), valtype=configtypes.Command(), ), ), From 1b0aea5e0578bb439a8b564c7e4a944a7fb0c468 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 27 Feb 2018 06:56:57 +0100 Subject: [PATCH 052/135] Bring simple bindings to front in get_reverse_bindings_for --- qutebrowser/config/config.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py index 14022d1d2..0a106ea05 100644 --- a/qutebrowser/config/config.py +++ b/qutebrowser/config/config.py @@ -157,16 +157,15 @@ class KeyConfig: """Get a dict of commands to a list of bindings for the mode.""" cmd_to_keys = {} bindings = self.get_bindings_for(mode) - for key, full_cmd in sorted(bindings.items()): + for seq, full_cmd in sorted(bindings.items()): for cmd in full_cmd.split(';;'): cmd = cmd.strip() cmd_to_keys.setdefault(cmd, []) - # put special bindings last - # FIXME update - # if utils.is_special_key(key): - # cmd_to_keys[cmd].append(key) - # else: - cmd_to_keys[cmd].insert(0, str(key)) + # Put bindings involving modifiers last + if any(info.modifiers for info in seq): + cmd_to_keys[cmd].append(str(seq)) + else: + cmd_to_keys[cmd].insert(0, str(seq)) return cmd_to_keys def get_command(self, key, mode, default=False): From fa29a0b68620d938bb201bda8f617f95fc22aec4 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 27 Feb 2018 06:58:39 +0100 Subject: [PATCH 053/135] Expect capitalized bindings in test_models --- tests/unit/completion/test_models.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/unit/completion/test_models.py b/tests/unit/completion/test_models.py index c3593031c..d5ec4433b 100644 --- a/tests/unit/completion/test_models.py +++ b/tests/unit/completion/test_models.py @@ -105,7 +105,7 @@ def configdata_stub(config_stub, monkeypatch, configdata_init): ), default={ 'normal': collections.OrderedDict([ - ('', 'quit'), + ('', 'quit'), ('d', 'tab-close'), ]) }, @@ -123,7 +123,7 @@ def configdata_stub(config_stub, monkeypatch, configdata_init): ), default={ 'normal': collections.OrderedDict([ - ('', 'quit'), + ('', 'quit'), ('ZQ', 'quit'), ('I', 'invalid'), ('d', 'scroll down'), @@ -215,7 +215,7 @@ def test_command_completion(qtmodeltester, cmdutils_stub, configdata_stub, "Commands": [ ('open', 'open a url', ''), ('q', "Alias for 'quit'", ''), - ('quit', 'quit qutebrowser', 'ZQ, '), + ('quit', 'quit qutebrowser', 'ZQ, '), ('tab-close', 'Close the current tab.', ''), ] }) @@ -240,7 +240,7 @@ def test_help_completion(qtmodeltester, cmdutils_stub, key_config_stub, _check_completions(model, { "Commands": [ (':open', 'open a url', ''), - (':quit', 'quit qutebrowser', 'ZQ, '), + (':quit', 'quit qutebrowser', 'ZQ, '), (':scroll', 'Scroll the current tab in the given direction.', ''), (':tab-close', 'Close the current tab.', ''), ], @@ -644,10 +644,10 @@ def test_setting_option_completion(qtmodeltester, config_stub, "Options": [ ('aliases', 'Aliases for commands.', '{"q": "quit"}'), ('bindings.commands', 'Default keybindings', ( - '{"normal": {"": "quit", "ZQ": "quit", ' + '{"normal": {"": "quit", "ZQ": "quit", ' '"I": "invalid", "d": "scroll down"}}')), ('bindings.default', 'Default keybindings', - '{"normal": {"": "quit", "d": "tab-close"}}'), + '{"normal": {"": "quit", "d": "tab-close"}}'), ] }) @@ -674,7 +674,7 @@ def test_bind_completion(qtmodeltester, cmdutils_stub, config_stub, "Commands": [ ('open', 'open a url', ''), ('q', "Alias for 'quit'", ''), - ('quit', 'quit qutebrowser', 'ZQ, '), + ('quit', 'quit qutebrowser', 'ZQ, '), ('scroll', 'Scroll the current tab in the given direction.', ''), ('tab-close', 'Close the current tab.', ''), ], @@ -694,7 +694,7 @@ def test_bind_completion_invalid(cmdutils_stub, config_stub, key_config_stub, "Commands": [ ('open', 'open a url', ''), ('q', "Alias for 'quit'", ''), - ('quit', 'quit qutebrowser', 'ZQ, '), + ('quit', 'quit qutebrowser', 'ZQ, '), ('scroll', 'Scroll the current tab in the given direction.', ''), ('tab-close', 'Close the current tab.', ''), ], @@ -713,7 +713,7 @@ def test_bind_completion_no_binding(qtmodeltester, cmdutils_stub, config_stub, "Commands": [ ('open', 'open a url', ''), ('q', "Alias for 'quit'", ''), - ('quit', 'quit qutebrowser', 'ZQ, '), + ('quit', 'quit qutebrowser', 'ZQ, '), ('scroll', 'Scroll the current tab in the given direction.', ''), ('tab-close', 'Close the current tab.', ''), ], @@ -735,7 +735,7 @@ def test_bind_completion_changed(cmdutils_stub, config_stub, key_config_stub, "Commands": [ ('open', 'open a url', ''), ('q', "Alias for 'quit'", ''), - ('quit', 'quit qutebrowser', 'ZQ, '), + ('quit', 'quit qutebrowser', 'ZQ, '), ('scroll', 'Scroll the current tab in the given direction.', ''), ('tab-close', 'Close the current tab.', ''), ], From 49f8bc3d638fc7572455fad476e96b0f303cd882 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 27 Feb 2018 07:37:55 +0100 Subject: [PATCH 054/135] Use KeySequences correctly in test_config.py --- tests/unit/config/test_config.py | 146 ++++++++++++++++++++----------- 1 file changed, 95 insertions(+), 51 deletions(-) diff --git a/tests/unit/config/test_config.py b/tests/unit/config/test_config.py index 2800236f4..a2a2697b1 100644 --- a/tests/unit/config/test_config.py +++ b/tests/unit/config/test_config.py @@ -39,6 +39,11 @@ def configdata_init(): configdata.init() +# Alias because we need this a lot in here. +def keyseq(s): + return keyutils.KeySequence.parse(s) + + class TestChangeFilter: @pytest.fixture(autouse=True) @@ -101,9 +106,8 @@ class TestKeyConfig: def test_prepare_invalid_mode(self, key_config_stub): """Make sure prepare checks the mode.""" - seq = keyutils.KeySequence('x') with pytest.raises(configexc.KeybindingError): - assert key_config_stub._prepare(seq, 'abnormal') + assert key_config_stub._prepare(keyseq('x'), 'abnormal') def test_prepare_invalid_type(self, key_config_stub): """Make sure prepare checks the type.""" @@ -112,32 +116,50 @@ class TestKeyConfig: @pytest.mark.parametrize('commands, expected', [ # Unbinding default key - ({'a': None}, {'b': 'message-info bar'}), + ({'a': None}, {keyseq('b'): 'message-info bar'}), # Additional binding ({'c': 'message-info baz'}, - {'a': 'message-info foo', 'b': 'message-info bar', - 'c': 'message-info baz'}), + {keyseq('a'): 'message-info foo', + keyseq('b'): 'message-info bar', + keyseq('c'): 'message-info baz'}), # Unbinding unknown key - ({'x': None}, {'a': 'message-info foo', 'b': 'message-info bar'}), + ({'x': None}, {keyseq('a'): 'message-info foo', + keyseq('b'): 'message-info bar'}), ]) def test_get_bindings_for_and_get_command(self, key_config_stub, config_stub, commands, expected): - orig_default_bindings = {'normal': {'a': 'message-info foo', - 'b': 'message-info bar'}, - 'insert': {}, - 'hint': {}, - 'passthrough': {}, - 'command': {}, - 'prompt': {}, - 'caret': {}, - 'register': {}} - config_stub.val.bindings.default = copy.deepcopy(orig_default_bindings) + orig_default_bindings = { + 'normal': {'a': 'message-info foo', + 'b': 'message-info bar'}, + 'insert': {}, + 'hint': {}, + 'passthrough': {}, + 'command': {}, + 'prompt': {}, + 'caret': {}, + 'register': {}, + 'yesno': {} + } + expected_default_bindings = { + 'normal': {keyseq('a'): 'message-info foo', + keyseq('b'): 'message-info bar'}, + 'insert': {}, + 'hint': {}, + 'passthrough': {}, + 'command': {}, + 'prompt': {}, + 'caret': {}, + 'register': {}, + 'yesno': {} + } + + config_stub.val.bindings.default = orig_default_bindings config_stub.val.bindings.commands = {'normal': commands} bindings = key_config_stub.get_bindings_for('normal') # Make sure the code creates a copy and doesn't modify the setting - assert config_stub.val.bindings.default == orig_default_bindings + assert config_stub.val.bindings.default == expected_default_bindings assert bindings == expected for key, command in expected.items(): assert key_config_stub.get_command(key, 'normal') == command @@ -146,15 +168,18 @@ class TestKeyConfig: no_bindings): config_stub.val.bindings.default = no_bindings config_stub.val.bindings.commands = no_bindings - assert key_config_stub.get_command('foobar', 'normal') is None + command = key_config_stub.get_command(keyseq('foobar'), + 'normal') + assert command is None def test_get_command_default(self, key_config_stub, config_stub): config_stub.val.bindings.default = { 'normal': {'x': 'message-info default'}} config_stub.val.bindings.commands = { 'normal': {'x': 'message-info custom'}} - cmd = 'message-info default' - assert key_config_stub.get_command('x', 'normal', default=True) == cmd + command = key_config_stub.get_command(keyseq('x'), 'normal', + default=True) + assert command == 'message-info default' @pytest.mark.parametrize('bindings, expected', [ # Simple @@ -163,9 +188,9 @@ class TestKeyConfig: # Multiple bindings ({'a': 'message-info foo', 'b': 'message-info foo'}, {'message-info foo': ['b', 'a']}), - # With special keys (should be listed last and normalized) - ({'a': 'message-info foo', '': 'message-info foo'}, - {'message-info foo': ['a', '']}), + # With modifier keys (should be listed last and normalized) + ({'a': 'message-info foo', '': 'message-info foo'}, + {'message-info foo': ['a', '']}), # Chained command ({'a': 'message-info foo ;; message-info bar'}, {'message-info foo': ['a'], 'message-info bar': ['a']}), @@ -178,11 +203,14 @@ class TestKeyConfig: @pytest.mark.parametrize('key', ['a', '', 'b']) def test_bind_duplicate(self, key_config_stub, config_stub, key): + seq = keyseq(key) config_stub.val.bindings.default = {'normal': {'a': 'nop', '': 'nop'}} config_stub.val.bindings.commands = {'normal': {'b': 'nop'}} - key_config_stub.bind(key, 'message-info foo', mode='normal') - assert key_config_stub.get_command(key, 'normal') == 'message-info foo' + key_config_stub.bind(seq, 'message-info foo', mode='normal') + + command = key_config_stub.get_command(seq, 'normal') + assert command == 'message-info foo' @pytest.mark.parametrize('mode', ['normal', 'caret']) @pytest.mark.parametrize('command', [ @@ -193,13 +221,14 @@ class TestKeyConfig: mode, command): config_stub.val.bindings.default = no_bindings config_stub.val.bindings.commands = no_bindings + seq = keyseq('a') with qtbot.wait_signal(config_stub.changed): - key_config_stub.bind('a', command, mode=mode) + key_config_stub.bind(seq, command, mode=mode) - assert config_stub.val.bindings.commands[mode]['a'] == command - assert key_config_stub.get_bindings_for(mode)['a'] == command - assert key_config_stub.get_command('a', mode) == command + assert config_stub.val.bindings.commands[mode][seq] == command + assert key_config_stub.get_bindings_for(mode)[seq] == command + assert key_config_stub.get_command(seq, mode) == command def test_bind_mode_changing(self, key_config_stub, config_stub, no_bindings): @@ -209,7 +238,8 @@ class TestKeyConfig: """ config_stub.val.bindings.default = no_bindings config_stub.val.bindings.commands = no_bindings - key_config_stub.bind('a', 'set-cmd-text :nop ;; rl-beginning-of-line', + key_config_stub.bind(keyseq('a'), + 'set-cmd-text :nop ;; rl-beginning-of-line', mode='normal') def test_bind_default(self, key_config_stub, config_stub): @@ -218,11 +248,15 @@ class TestKeyConfig: bound_cmd = 'message-info bound' config_stub.val.bindings.default = {'normal': {'a': default_cmd}} config_stub.val.bindings.commands = {'normal': {'a': bound_cmd}} - assert key_config_stub.get_command('a', mode='normal') == bound_cmd + seq = keyseq('a') - key_config_stub.bind_default('a', mode='normal') + command = key_config_stub.get_command(seq, mode='normal') + assert command == bound_cmd - assert key_config_stub.get_command('a', mode='normal') == default_cmd + key_config_stub.bind_default(seq, mode='normal') + + command = key_config_stub.get_command(keyseq('a'), mode='normal') + assert command == default_cmd def test_bind_default_unbound(self, key_config_stub, config_stub, no_bindings): @@ -231,42 +265,51 @@ class TestKeyConfig: config_stub.val.bindings.commands = no_bindings with pytest.raises(configexc.KeybindingError, match="Can't find binding 'foobar' in normal mode"): - key_config_stub.bind_default('foobar', mode='normal') + key_config_stub.bind_default(keyseq('foobar'), mode='normal') - @pytest.mark.parametrize('key, normalized', [ - ('a', 'a'), # default bindings - ('b', 'b'), # custom bindings - ('', '') + @pytest.mark.parametrize('key', [ + 'a', # default bindings + 'b', # custom bindings + '', ]) @pytest.mark.parametrize('mode', ['normal', 'caret', 'prompt']) def test_unbind(self, key_config_stub, config_stub, qtbot, - key, normalized, mode): + key, mode): default_bindings = { 'normal': {'a': 'nop', '': 'nop'}, 'caret': {'a': 'nop', '': 'nop'}, # prompt: a mode which isn't in bindings.commands yet 'prompt': {'a': 'nop', 'b': 'nop', '': 'nop'}, } - old_default_bindings = copy.deepcopy(default_bindings) + expected_default_bindings = { + 'normal': {keyseq('a'): 'nop', keyseq(''): 'nop'}, + 'caret': {keyseq('a'): 'nop', keyseq(''): 'nop'}, + # prompt: a mode which isn't in bindings.commands yet + 'prompt': {keyseq('a'): 'nop', + keyseq('b'): 'nop', + keyseq(''): 'nop'}, + } + config_stub.val.bindings.default = default_bindings config_stub.val.bindings.commands = { 'normal': {'b': 'nop'}, 'caret': {'b': 'nop'}, } + seq = keyseq(key) with qtbot.wait_signal(config_stub.changed): - key_config_stub.unbind(key, mode=mode) + key_config_stub.unbind(seq, mode=mode) - assert key_config_stub.get_command(key, mode) is None + assert key_config_stub.get_command(seq, mode) is None mode_bindings = config_stub.val.bindings.commands[mode] if key == 'b' and mode != 'prompt': # Custom binding - assert normalized not in mode_bindings + assert seq not in mode_bindings else: default_bindings = config_stub.val.bindings.default - assert default_bindings[mode] == old_default_bindings[mode] - assert mode_bindings[normalized] is None + assert default_bindings[mode] == expected_default_bindings[mode] + assert mode_bindings[seq] is None def test_unbind_unbound(self, key_config_stub, config_stub, no_bindings): """Try unbinding a key which is not bound.""" @@ -274,7 +317,7 @@ class TestKeyConfig: config_stub.val.bindings.commands = no_bindings with pytest.raises(configexc.KeybindingError, match="Can't find binding 'foobar' in normal mode"): - key_config_stub.unbind('foobar', mode='normal') + key_config_stub.unbind(keyseq('foobar'), mode='normal') def test_unbound_twice(self, key_config_stub, config_stub, no_bindings): """Try unbinding an already-unbound default key. @@ -286,17 +329,18 @@ class TestKeyConfig: """ config_stub.val.bindings.default = {'normal': {'a': 'nop'}} config_stub.val.bindings.commands = no_bindings + seq = keyseq('a') - key_config_stub.unbind('a') - assert key_config_stub.get_command('a', mode='normal') is None - key_config_stub.unbind('a') - assert key_config_stub.get_command('a', mode='normal') is None + key_config_stub.unbind(seq) + assert key_config_stub.get_command(seq, mode='normal') is None + key_config_stub.unbind(seq) + assert key_config_stub.get_command(seq, mode='normal') is None def test_empty_command(self, key_config_stub): """Try binding a key to an empty command.""" message = "Can't add binding 'x' with empty command in normal mode" with pytest.raises(configexc.KeybindingError, match=message): - key_config_stub.bind('x', ' ', mode='normal') + key_config_stub.bind(keyseq('x'), ' ', mode='normal') class TestConfig: From b1f4b1eaba0621ba1d120280661864806574ec42 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 27 Feb 2018 07:40:06 +0100 Subject: [PATCH 055/135] Fix :unbind with already bound keys The previous change was incorrect and caused a regression (test_unbound_twice) --- qutebrowser/config/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py index 0a106ea05..f8f9e7902 100644 --- a/qutebrowser/config/config.py +++ b/qutebrowser/config/config.py @@ -212,7 +212,7 @@ class KeyConfig: bindings_commands = self._config.get_mutable_obj('bindings.commands') - if str(key) in bindings_commands.get(mode, {}): + if val.bindings.commands[mode].get(key, None) is not None: # In custom bindings -> remove it del bindings_commands[mode][str(key)] elif key in val.bindings.default[mode]: From 214e750c69bc9187c2dc1bc98b1576b2a19e4232 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 27 Feb 2018 07:51:07 +0100 Subject: [PATCH 056/135] Adjust test_configcommands.py --- tests/unit/config/test_configcommands.py | 25 ++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/tests/unit/config/test_configcommands.py b/tests/unit/config/test_configcommands.py index cafc1ac31..320eb2bd4 100644 --- a/tests/unit/config/test_configcommands.py +++ b/tests/unit/config/test_configcommands.py @@ -27,9 +27,15 @@ from PyQt5.QtCore import QUrl from qutebrowser.config import configcommands, configutils from qutebrowser.commands import cmdexc from qutebrowser.utils import usertypes, urlmatch +from qutebrowser.keyinput import keyutils from qutebrowser.misc import objects +# Alias because we need this a lot in here. +def keyseq(s): + return keyutils.KeySequence.parse(s) + + @pytest.fixture def commands(config_stub, key_config_stub): return configcommands.ConfigCommands(config_stub, key_config_stub) @@ -415,7 +421,7 @@ class TestWritePy: def test_custom(self, commands, config_stub, key_config_stub, tmpdir): confpy = tmpdir / 'config.py' config_stub.val.content.javascript.enabled = True - key_config_stub.bind(',x', 'message-info foo', mode='normal') + key_config_stub.bind(keyseq(',x'), 'message-info foo', mode='normal') commands.config_write_py(str(confpy)) @@ -496,7 +502,7 @@ class TestBind: config_stub.val.bindings.commands = no_bindings commands.bind(0, 'a', command) - assert key_config_stub.get_command('a', 'normal') == command + assert key_config_stub.get_command(keyseq('a'), 'normal') == command yaml_bindings = yaml_value('bindings.commands')['normal'] assert yaml_bindings['a'] == command @@ -509,7 +515,7 @@ class TestBind: ('c', 'normal', "c is bound to 'message-info c' in normal mode"), # Special key ('', 'normal', - " is bound to 'message-info C-x' in normal mode"), + " is bound to 'message-info C-x' in normal mode"), # unbound ('x', 'normal', "x is unbound in normal mode"), # non-default mode @@ -569,7 +575,8 @@ class TestBind: } commands.bind(0, key, 'message-info foo', mode='normal') - assert key_config_stub.get_command(key, 'normal') == 'message-info foo' + command = key_config_stub.get_command(keyseq(key), 'normal') + assert command == 'message-info foo' def test_bind_none(self, commands, config_stub): config_stub.val.bindings.commands = None @@ -581,11 +588,13 @@ class TestBind: bound_cmd = 'message-info bound' config_stub.val.bindings.default = {'normal': {'a': default_cmd}} config_stub.val.bindings.commands = {'normal': {'a': bound_cmd}} - assert key_config_stub.get_command('a', mode='normal') == bound_cmd + command = key_config_stub.get_command(keyseq('a'), mode='normal') + assert command == bound_cmd commands.bind(0, 'a', mode='normal', default=True) - assert key_config_stub.get_command('a', mode='normal') == default_cmd + command = key_config_stub.get_command(keyseq('a'), mode='normal') + assert command == default_cmd @pytest.mark.parametrize('key, mode, expected', [ ('foobar', 'normal', "Can't find binding 'foobar' in normal mode"), @@ -607,7 +616,7 @@ class TestBind: ('a', 'a'), # default bindings ('b', 'b'), # custom bindings ('c', 'c'), # :bind then :unbind - ('', '') # normalized special binding + ('', '') # normalized special binding ]) def test_unbind(self, commands, key_config_stub, config_stub, yaml_value, key, normalized): @@ -624,7 +633,7 @@ class TestBind: commands.bind(0, key, 'nop') commands.unbind(key) - assert key_config_stub.get_command(key, 'normal') is None + assert key_config_stub.get_command(keyseq(key), 'normal') is None yaml_bindings = yaml_value('bindings.commands')['normal'] if key in 'bc': From e8d5fb5cca1c95e5ddce7576cf53139f09480208 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 27 Feb 2018 07:51:14 +0100 Subject: [PATCH 057/135] Normalize keybinding with :bind --- qutebrowser/config/configcommands.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/qutebrowser/config/configcommands.py b/qutebrowser/config/configcommands.py index 7322b3878..43b9641fd 100644 --- a/qutebrowser/config/configcommands.py +++ b/qutebrowser/config/configcommands.py @@ -154,17 +154,13 @@ class ConfigCommands: return # No --default -> print binding - #if utils.is_special_key(key): - # # self._keyconfig.get_command does this, but we also need it - # # normalized for the output below - # key = utils.normalize_keystr(key) with self._handle_config_error(): cmd = self._keyconfig.get_command(seq, mode) if cmd is None: - message.info("{} is unbound in {} mode".format(key, mode)) + message.info("{} is unbound in {} mode".format(seq, mode)) else: message.info("{} is bound to '{}' in {} mode".format( - key, cmd, mode)) + seq, cmd, mode)) return with self._handle_config_error(): From 612387633df9986827a0780f364aa0051bbe9ed9 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 27 Feb 2018 07:53:29 +0100 Subject: [PATCH 058/135] Adjust test_configfiles --- tests/unit/config/test_configfiles.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/config/test_configfiles.py b/tests/unit/config/test_configfiles.py index 728dbb794..c902bb42d 100644 --- a/tests/unit/config/test_configfiles.py +++ b/tests/unit/config/test_configfiles.py @@ -607,7 +607,7 @@ class TestConfigPy: @pytest.mark.parametrize('line, key, mode', [ ('config.unbind("o")', 'o', 'normal'), - ('config.unbind("y", mode="prompt")', 'y', 'prompt'), + ('config.unbind("y", mode="yesno")', 'y', 'yesno'), ]) def test_unbind(self, confpy, line, key, mode): confpy.write(line) From f40f4082baaf11ba09312f37eb34e2f97e9afffe Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 27 Feb 2018 07:56:10 +0100 Subject: [PATCH 059/135] Validate configtypes.Key correctly --- qutebrowser/config/configtypes.py | 9 +++++---- tests/unit/config/test_configtypes.py | 4 ++++ 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/qutebrowser/config/configtypes.py b/qutebrowser/config/configtypes.py index 196e19647..14855bf03 100644 --- a/qutebrowser/config/configtypes.py +++ b/qutebrowser/config/configtypes.py @@ -1652,7 +1652,8 @@ class Key(BaseType): self._basic_py_validation(value, str) if not value: return None - #if utils.is_special_key(value): - # value = '<{}>'.format(utils.normalize_keystr(value[1:-1])) - #return value - return keyutils.KeySequence.parse(value) + + try: + return keyutils.KeySequence.parse(value) + except keyutils.KeyParseError as e: + raise configexc.ValidationError(value, str(e)) diff --git a/tests/unit/config/test_configtypes.py b/tests/unit/config/test_configtypes.py index 81a7d53e1..41da86ff8 100644 --- a/tests/unit/config/test_configtypes.py +++ b/tests/unit/config/test_configtypes.py @@ -2064,6 +2064,10 @@ class TestKey: def test_to_py_valid(self, klass, val, expected): assert klass().to_py(val) == expected + def test_to_py_invalid(self, klass): + with pytest.raises(configexc.ValidationError): + klass().to_py('\U00010000') + @pytest.mark.parametrize('first, second, equal', [ (re.compile('foo'), RegexEq('foo'), True), From 1e8f72dfe6283a3e692efbdbbd63cc508623be83 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 27 Feb 2018 08:10:54 +0100 Subject: [PATCH 060/135] Adjust test_configtypes --- tests/unit/config/test_configtypes.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/unit/config/test_configtypes.py b/tests/unit/config/test_configtypes.py index 41da86ff8..c64891e5a 100644 --- a/tests/unit/config/test_configtypes.py +++ b/tests/unit/config/test_configtypes.py @@ -37,6 +37,7 @@ from PyQt5.QtNetwork import QNetworkProxy from qutebrowser.config import configtypes, configexc from qutebrowser.utils import debug, utils, qtutils from qutebrowser.browser.network import pac +from qutebrowser.keyinput import keyutils from tests.helpers import utils as testutils @@ -2058,8 +2059,8 @@ class TestKey: return configtypes.Key @pytest.mark.parametrize('val, expected', [ - ('gC', 'gC'), - ('', '') + ('gC', keyutils.KeySequence.parse('gC')), + ('', keyutils.KeySequence.parse('')), ]) def test_to_py_valid(self, klass, val, expected): assert klass().to_py(val) == expected From ac4fd7c563832245b97846874a04721521aa5fbd Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 27 Feb 2018 08:20:06 +0100 Subject: [PATCH 061/135] Add KeyInfo.to_event() --- qutebrowser/browser/commands.py | 6 ++---- qutebrowser/keyinput/keyutils.py | 8 ++++++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 701c8324f..a5f60e2ac 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -27,7 +27,6 @@ import typing from PyQt5.QtWidgets import QApplication, QTabBar, QDialog from PyQt5.QtCore import pyqtSlot, Qt, QUrl, QEvent, QUrlQuery -from PyQt5.QtGui import QKeyEvent from PyQt5.QtPrintSupport import QPrintDialog, QPrintPreviewDialog from qutebrowser.commands import userscripts, cmdexc, cmdutils, runners @@ -2117,9 +2116,8 @@ class CommandDispatcher: raise cmdexc.CommandError(str(e)) for keyinfo in sequence: - args = (keyinfo.key, keyinfo.modifiers, keyinfo.text()) - press_event = QKeyEvent(QEvent.KeyPress, *args) - release_event = QKeyEvent(QEvent.KeyRelease, *args) + press_event = keyinfo.to_event(QEvent.KeyPress) + release_event = keyinfo.to_event(QEvent.KeyRelease) if global_: window = QApplication.focusWindow() diff --git a/qutebrowser/keyinput/keyutils.py b/qutebrowser/keyinput/keyutils.py index 9be7b4d60..80bf59a35 100644 --- a/qutebrowser/keyinput/keyutils.py +++ b/qutebrowser/keyinput/keyutils.py @@ -24,8 +24,8 @@ import collections import itertools import attr -from PyQt5.QtCore import Qt -from PyQt5.QtGui import QKeySequence +from PyQt5.QtCore import Qt, QEvent +from PyQt5.QtGui import QKeySequence, QKeyEvent from qutebrowser.utils import utils, debug @@ -273,6 +273,10 @@ class KeyInfo: 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()) + class KeySequence: From 44b4cb92be40bb6ed0d97712416fd9c08e7baebb Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 27 Feb 2018 08:35:14 +0100 Subject: [PATCH 062/135] Make keyutils.KeySequence.parse('') work --- qutebrowser/keyinput/keyutils.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/qutebrowser/keyinput/keyutils.py b/qutebrowser/keyinput/keyutils.py index 80bf59a35..e9abf476b 100644 --- a/qutebrowser/keyinput/keyutils.py +++ b/qutebrowser/keyinput/keyutils.py @@ -394,6 +394,9 @@ class KeySequence: for sub in utils.chunk(strings, cls._MAX_LEN): sequence = QKeySequence(', '.join(sub)) new._sequences.append(sequence) - assert len(new) > 0 + + if keystr: + assert len(new) > 0 + new._validate(keystr) return new From 9e27f2b3e7e9b4650af28d12266ddff68608f760 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 27 Feb 2018 08:48:16 +0100 Subject: [PATCH 063/135] Initial attempts at fixing test_basekeyparser --- qutebrowser/keyinput/basekeyparser.py | 1 + tests/unit/keyinput/test_basekeyparser.py | 95 +++++++++++++---------- 2 files changed, 56 insertions(+), 40 deletions(-) diff --git a/qutebrowser/keyinput/basekeyparser.py b/qutebrowser/keyinput/basekeyparser.py index 16691af4c..6636042ce 100644 --- a/qutebrowser/keyinput/basekeyparser.py +++ b/qutebrowser/keyinput/basekeyparser.py @@ -253,6 +253,7 @@ class BaseKeyParser(QObject): Args: cmdstr: The command to execute as a string. + # FIXME do we still need this? keytype: Type.chain or Type.special count: The count if given. """ diff --git a/tests/unit/keyinput/test_basekeyparser.py b/tests/unit/keyinput/test_basekeyparser.py index 423076bdd..8b3ce2d70 100644 --- a/tests/unit/keyinput/test_basekeyparser.py +++ b/tests/unit/keyinput/test_basekeyparser.py @@ -25,10 +25,15 @@ from unittest import mock from PyQt5.QtCore import Qt import pytest -from qutebrowser.keyinput import basekeyparser +from qutebrowser.keyinput import basekeyparser, keyutils from qutebrowser.utils import utils +# Alias because we need this a lot in here. +def keyseq(s): + return keyutils.KeySequence.parse(s) + + @pytest.fixture def keyparser(key_config_stub): """Fixture providing a BaseKeyParser supporting count/chains.""" @@ -80,18 +85,24 @@ class TestDebugLog: assert not caplog.records -@pytest.mark.parametrize('input_key, supports_count, expected', [ +@pytest.mark.parametrize('input_key, supports_count, count, command', [ # (input_key, supports_count, expected) - ('10', True, (10, '')), - ('10foo', True, (10, 'foo')), - ('-1foo', True, (None, '-1foo')), - ('10e4foo', True, (10, 'e4foo')), - ('foo', True, (None, 'foo')), - ('10foo', False, (None, '10foo')), + ('10', True, '10', ''), + ('10g', True, '10', 'g'), + ('10e4g', True, '4', 'g'), + ('g', True, '', 'g'), + ('10g', False, '', 'g'), ]) -def test_split_count(config_stub, input_key, supports_count, expected): +def test_split_count(config_stub, key_config_stub, + input_key, supports_count, count, command): kp = basekeyparser.BaseKeyParser(0, supports_count=supports_count) - assert kp._split_count(input_key) == expected + kp._read_config('normal') + + for info in keyseq(input_key): + kp._handle_key(info.to_event()) + + assert kp._count == count + assert kp._sequence == keyseq(command) @pytest.mark.usefixtures('keyinput_bindings') @@ -106,18 +117,18 @@ class TestReadConfig: """Test reading config with _modename set.""" keyparser._modename = 'normal' keyparser._read_config() - assert 'a' in keyparser.bindings + assert keyseq('a') in keyparser.bindings def test_read_config_valid(self, keyparser): """Test reading config.""" keyparser._read_config('prompt') - assert 'ccc' in keyparser.bindings - assert 'ctrl+a' in keyparser.special_bindings + assert keyseq('ccc') in keyparser.bindings + assert keyseq('') in keyparser.bindings keyparser._read_config('command') - assert 'ccc' not in keyparser.bindings - assert 'ctrl+a' not in keyparser.special_bindings - assert 'foo' in keyparser.bindings - assert 'ctrl+x' in keyparser.special_bindings + assert keyseq('ccc') not in keyparser.bindings + assert keyseq('') not in keyparser.bindings + assert keyseq('foo') in keyparser.bindings + assert keyseq('') in keyparser.bindings def test_read_config_modename_none(self, keyparser): assert keyparser._modename is None @@ -134,15 +145,18 @@ class TestReadConfig: mode, changed_mode, expected): keyparser._read_config(mode) # Sanity checks - assert 'a' in keyparser.bindings - assert 'new' not in keyparser.bindings + assert keyseq('a') in keyparser.bindings + assert keyseq('new') not in keyparser.bindings - key_config_stub.bind('new', 'message-info new', mode=changed_mode) + key_config_stub.bind(keyseq('new'), 'message-info new', + mode=changed_mode) - assert 'a' in keyparser.bindings - assert ('new' in keyparser.bindings) == expected + assert keyseq('a') in keyparser.bindings + assert (keyseq('new') in keyparser.bindings) == expected + # FIXME do we still need this? @pytest.mark.parametrize('warn_on_keychains', [True, False]) + @pytest.mark.skip(reason='unneeded?') def test_warn_on_keychains(self, caplog, warn_on_keychains): """Test _warn_on_keychains.""" kp = basekeyparser.BaseKeyParser( @@ -168,27 +182,28 @@ class TestSpecialKeys: keyparser.handle(fake_keyevent_factory(Qt.Key_A, modifier)) keyparser.handle(fake_keyevent_factory(Qt.Key_X, modifier)) keyparser.execute.assert_called_once_with( - 'message-info ctrla', keyparser.Type.special, None) + 'message-info ctrla', keyparser.Type.chain, None) def test_valid_key_count(self, fake_keyevent_factory, keyparser): modifier = Qt.MetaModifier if utils.is_mac else Qt.ControlModifier - keyparser.handle(fake_keyevent_factory(5, text='5')) + keyparser.handle(fake_keyevent_factory(Qt.Key_5, text='5')) keyparser.handle(fake_keyevent_factory(Qt.Key_A, modifier, text='A')) keyparser.execute.assert_called_once_with( - 'message-info ctrla', keyparser.Type.special, 5) + 'message-info ctrla', keyparser.Type.chain, 5) def test_invalid_key(self, fake_keyevent_factory, keyparser): keyparser.handle(fake_keyevent_factory( Qt.Key_A, (Qt.ControlModifier | Qt.AltModifier))) assert not keyparser.execute.called + @pytest.mark.skip(reason='unneeded?') def test_keychain(self, fake_keyevent_factory, keyparser): keyparser.handle(fake_keyevent_factory(Qt.Key_B)) keyparser.handle(fake_keyevent_factory(Qt.Key_A)) assert not keyparser.execute.called def test_no_binding(self, monkeypatch, fake_keyevent_factory, keyparser): - monkeypatch.setattr(utils, 'keyevent_to_string', lambda binding: None) + monkeypatch.setattr(keyutils, 'keyevent_to_string', lambda binding: None) keyparser.handle(fake_keyevent_factory(Qt.Key_A, Qt.NoModifier)) assert not keyparser.execute.called @@ -197,7 +212,7 @@ class TestSpecialKeys: keyparser.handle(fake_keyevent_factory(Qt.Key_B, modifier)) keyparser.execute.assert_called_once_with( - 'message-info ctrla', keyparser.Type.special, None) + 'message-info ctrla', keyparser.Type.chain, None) def test_binding_and_mapping(self, config_stub, fake_keyevent_factory, keyparser): @@ -206,7 +221,7 @@ class TestSpecialKeys: keyparser.handle(fake_keyevent_factory(Qt.Key_A, modifier)) keyparser.execute.assert_called_once_with( - 'message-info ctrla', keyparser.Type.special, None) + 'message-info ctrla', keyparser.Type.chain, None) class TestKeyChain: @@ -225,14 +240,14 @@ class TestKeyChain: keyparser.handle(fake_keyevent_factory(Qt.Key_A, modifier)) keyparser.handle(fake_keyevent_factory(Qt.Key_X, modifier)) keyparser.execute.assert_called_once_with( - 'message-info ctrla', keyparser.Type.special, None) - assert keyparser._keystring == '' + 'message-info ctrla', keyparser.Type.chain, None) + assert not keyparser._sequence def test_invalid_special_key(self, fake_keyevent_factory, keyparser): keyparser.handle(fake_keyevent_factory( Qt.Key_A, (Qt.ControlModifier | Qt.AltModifier))) assert not keyparser.execute.called - assert keyparser._keystring == '' + assert not keyparser._sequence def test_valid_keychain(self, handle_text, keyparser): # Press 'x' which is ignored because of no match @@ -241,13 +256,13 @@ class TestKeyChain: (Qt.Key_B, 'b'), (Qt.Key_A, 'a')) keyparser.execute.assert_called_with( 'message-info ba', keyparser.Type.chain, None) - assert keyparser._keystring == '' + assert not keyparser._sequence def test_0_press(self, handle_text, keyparser): handle_text((Qt.Key_0, '0')) keyparser.execute.assert_called_once_with( 'message-info 0', keyparser.Type.chain, None) - assert keyparser._keystring == '' + assert not keyparser._sequence def test_ambiguous_keychain(self, handle_text, keyparser): handle_text((Qt.Key_A, 'a')) @@ -256,7 +271,7 @@ class TestKeyChain: def test_invalid_keychain(self, handle_text, keyparser): handle_text((Qt.Key_B, 'b')) handle_text((Qt.Key_C, 'c')) - assert keyparser._keystring == '' + assert not keyparser._sequence def test_mapping(self, config_stub, handle_text, keyparser): handle_text((Qt.Key_X, 'x')) @@ -282,34 +297,34 @@ class TestCount: handle_text((Qt.Key_B, 'b'), (Qt.Key_A, 'a')) keyparser.execute.assert_called_once_with( 'message-info ba', keyparser.Type.chain, None) - assert keyparser._keystring == '' + assert not keyparser._sequence def test_count_0(self, handle_text, keyparser): handle_text((Qt.Key_0, '0'), (Qt.Key_B, 'b'), (Qt.Key_A, 'a')) calls = [mock.call('message-info 0', keyparser.Type.chain, None), mock.call('message-info ba', keyparser.Type.chain, None)] keyparser.execute.assert_has_calls(calls) - assert keyparser._keystring == '' + assert not keyparser._sequence def test_count_42(self, handle_text, keyparser): handle_text((Qt.Key_4, '4'), (Qt.Key_2, '2'), (Qt.Key_B, 'b'), (Qt.Key_A, 'a')) keyparser.execute.assert_called_once_with( 'message-info ba', keyparser.Type.chain, 42) - assert keyparser._keystring == '' + assert not keyparser._sequence def test_count_42_invalid(self, handle_text, keyparser): # Invalid call with ccx gets ignored handle_text((Qt.Key_4, '4'), (Qt.Key_2, '2'), (Qt.Key_C, 'c'), (Qt.Key_C, 'c'), (Qt.Key_X, 'x')) assert not keyparser.execute.called - assert keyparser._keystring == '' + assert not keyparser._sequence # Valid call with ccc gets the correct count handle_text((Qt.Key_6, '2'), (Qt.Key_2, '3'), (Qt.Key_C, 'c'), (Qt.Key_C, 'c'), (Qt.Key_C, 'c')) keyparser.execute.assert_called_once_with( 'message-info ccc', keyparser.Type.chain, 23) - assert keyparser._keystring == '' + assert not keyparser._sequence def test_clear_keystring(qtbot, keyparser): @@ -317,4 +332,4 @@ def test_clear_keystring(qtbot, keyparser): keyparser._keystring = 'test' with qtbot.waitSignal(keyparser.keystring_updated): keyparser.clear_keystring() - assert keyparser._keystring == '' + assert not keyparser._sequence From eeeb763f8a1e10f9ee1c4ce88e292d7232328628 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 27 Feb 2018 08:50:50 +0100 Subject: [PATCH 064/135] Make sure 0 is handled as command --- qutebrowser/keyinput/basekeyparser.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/qutebrowser/keyinput/basekeyparser.py b/qutebrowser/keyinput/basekeyparser.py index 6636042ce..9510209df 100644 --- a/qutebrowser/keyinput/basekeyparser.py +++ b/qutebrowser/keyinput/basekeyparser.py @@ -139,7 +139,8 @@ class BaseKeyParser(QObject): # self._debug_log("Ignoring, no text char") # return QKeySequence.NoMatch - if txt.isdigit() and self._supports_count: + if (txt.isdigit() and self._supports_count and not + (not self._count and txt == '0')): assert len(txt) == 1, txt self._count += txt return None From 5a03d31f6f9f95dcd916e39765ab9682ff54a566 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 27 Feb 2018 08:53:28 +0100 Subject: [PATCH 065/135] More test_basekeyparser fixes --- tests/unit/keyinput/test_basekeyparser.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/keyinput/test_basekeyparser.py b/tests/unit/keyinput/test_basekeyparser.py index 8b3ce2d70..91acfa423 100644 --- a/tests/unit/keyinput/test_basekeyparser.py +++ b/tests/unit/keyinput/test_basekeyparser.py @@ -320,7 +320,7 @@ class TestCount: assert not keyparser.execute.called assert not keyparser._sequence # Valid call with ccc gets the correct count - handle_text((Qt.Key_6, '2'), (Qt.Key_2, '3'), (Qt.Key_C, 'c'), + handle_text((Qt.Key_2, '2'), (Qt.Key_3, '3'), (Qt.Key_C, 'c'), (Qt.Key_C, 'c'), (Qt.Key_C, 'c')) keyparser.execute.assert_called_once_with( 'message-info ccc', keyparser.Type.chain, 23) @@ -329,7 +329,7 @@ class TestCount: def test_clear_keystring(qtbot, keyparser): """Test that the keystring is cleared and the signal is emitted.""" - keyparser._keystring = 'test' + keyparser._sequence = keyseq('test') with qtbot.waitSignal(keyparser.keystring_updated): keyparser.clear_keystring() assert not keyparser._sequence From 911b2daebf5349b22de7b908058450fcd4ce635e Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 27 Feb 2018 09:07:20 +0100 Subject: [PATCH 066/135] Fix test_keyutils --- qutebrowser/keyinput/keyutils.py | 1 + tests/unit/keyinput/test_keyutils.py | 39 ++++++++++++++-------------- 2 files changed, 21 insertions(+), 19 deletions(-) diff --git a/qutebrowser/keyinput/keyutils.py b/qutebrowser/keyinput/keyutils.py index e9abf476b..6a149a0a6 100644 --- a/qutebrowser/keyinput/keyutils.py +++ b/qutebrowser/keyinput/keyutils.py @@ -396,6 +396,7 @@ class KeySequence: new._sequences.append(sequence) if keystr: + # FIXME fails with " 0 new._validate(keystr) diff --git a/tests/unit/keyinput/test_keyutils.py b/tests/unit/keyinput/test_keyutils.py index 8a62071a3..db5b380d7 100644 --- a/tests/unit/keyinput/test_keyutils.py +++ b/tests/unit/keyinput/test_keyutils.py @@ -20,6 +20,7 @@ import pytest from PyQt5.QtCore import Qt +from qutebrowser.utils import utils from qutebrowser.keyinput import keyutils @@ -65,13 +66,13 @@ class TestKeyEventToString: """Test keyeevent when only control is pressed.""" evt = fake_keyevent_factory(key=Qt.Key_Control, modifiers=Qt.ControlModifier) - assert keyutils.keyevent_to_string(evt) is None + assert not keyutils.keyevent_to_string(evt) def test_only_hyper_l(self, fake_keyevent_factory): """Test keyeevent when only Hyper_L is pressed.""" evt = fake_keyevent_factory(key=Qt.Key_Hyper_L, modifiers=Qt.MetaModifier) - assert keyutils.keyevent_to_string(evt) is None + assert not keyutils.keyevent_to_string(evt) def test_only_key(self, fake_keyevent_factory): """Test with a simple key pressed.""" @@ -81,7 +82,7 @@ class TestKeyEventToString: def test_key_and_modifier(self, fake_keyevent_factory): """Test with key and modifier pressed.""" evt = fake_keyevent_factory(key=Qt.Key_A, modifiers=Qt.ControlModifier) - expected = 'meta+a' if keyutils.is_mac else 'ctrl+a' + expected = '' if utils.is_mac else '' assert keyutils.keyevent_to_string(evt) == expected def test_key_and_modifiers(self, fake_keyevent_factory): @@ -89,13 +90,13 @@ class TestKeyEventToString: evt = fake_keyevent_factory( key=Qt.Key_A, modifiers=(Qt.ControlModifier | Qt.AltModifier | Qt.MetaModifier | Qt.ShiftModifier)) - assert keyutils.keyevent_to_string(evt) == 'ctrl+alt+meta+shift+a' + assert keyutils.keyevent_to_string(evt) == '' @pytest.mark.fake_os('mac') def test_mac(self, fake_keyevent_factory): """Test with a simulated mac.""" evt = fake_keyevent_factory(key=Qt.Key_A, modifiers=Qt.ControlModifier) - assert keyutils.keyevent_to_string(evt) == 'meta+a' + assert keyutils.keyevent_to_string(evt) == '' @pytest.mark.parametrize('keystr, expected', [ @@ -115,21 +116,21 @@ class TestKeyEventToString: def test_parse(keystr, expected): if expected is keyutils.KeyParseError: with pytest.raises(keyutils.KeyParseError): - keyutils._parse_single_key(keystr) + keyutils.KeySequence.parse(keystr) else: - assert keyutils._parse_single_key(keystr) == expected + assert keyutils.KeySequence.parse(keystr) == expected -@pytest.mark.parametrize('orig, repl', [ - ('Control+x', 'ctrl+x'), - ('Windows+x', 'meta+x'), - ('Mod1+x', 'alt+x'), - ('Mod4+x', 'meta+x'), - ('Control--', 'ctrl+-'), - ('Windows++', 'meta++'), - ('ctrl-x', 'ctrl+x'), - ('control+x', 'ctrl+x') +@pytest.mark.parametrize('orig, normalized', [ + ('', ''), + ('', ''), + ('', ''), + ('', ''), + ('', ''), + ('', ''), + ('', ''), + ('', '') ]) -def test_normalize_keystr(orig, repl): - assert keyutils.KeySequence(orig) == repl - +def test_normalize_keystr(orig, normalized): + expected = keyutils.KeySequence.parse(normalized) + assert keyutils.KeySequence.parse(orig) == expected From 1ba61bbcbee5ed19d43e307436d7e7b0e5cff4e1 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 27 Feb 2018 09:22:01 +0100 Subject: [PATCH 067/135] Fix test_modeparsers --- tests/unit/keyinput/test_modeparsers.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/unit/keyinput/test_modeparsers.py b/tests/unit/keyinput/test_modeparsers.py index ade8c15cc..50332369f 100644 --- a/tests/unit/keyinput/test_modeparsers.py +++ b/tests/unit/keyinput/test_modeparsers.py @@ -25,7 +25,7 @@ from PyQt5.QtCore import Qt import pytest -from qutebrowser.keyinput import modeparsers +from qutebrowser.keyinput import modeparsers, keyutils class TestsNormalKeyParser: @@ -58,7 +58,7 @@ class TestsNormalKeyParser: keyparser.handle(fake_keyevent_factory(Qt.Key_A, text='a')) keyparser.execute.assert_called_with( 'message-info ba', keyparser.Type.chain, None) - assert keyparser._keystring == '' + assert not keyparser._sequence def test_partial_keychain_timeout(self, keyparser, config_stub, fake_keyevent_factory): @@ -74,11 +74,11 @@ class TestsNormalKeyParser: assert timer.isActive() assert not keyparser.execute.called - assert keyparser._keystring == 'b' + assert keyparser._sequence == keyutils.KeySequence.parse('b') # Now simulate a timeout and check the keystring has been cleared. keystring_updated_mock = mock.Mock() keyparser.keystring_updated.connect(keystring_updated_mock) timer.timeout.emit() assert not keyparser.execute.called - assert keyparser._keystring == '' + assert not keyparser._sequence keystring_updated_mock.assert_called_once_with('') From 5d581d42f5cd843d29e585e13d1cbd03ae376cd4 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 27 Feb 2018 09:22:11 +0100 Subject: [PATCH 068/135] Improve key parsing with simple keys containing --- qutebrowser/keyinput/keyutils.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/qutebrowser/keyinput/keyutils.py b/qutebrowser/keyinput/keyutils.py index 6a149a0a6..58b29123d 100644 --- a/qutebrowser/keyinput/keyutils.py +++ b/qutebrowser/keyinput/keyutils.py @@ -149,6 +149,7 @@ def _parse_keystring(keystr): special = False for c in keystr: if c == '>': + assert special yield normalize_keystr(key) key = '' special = False @@ -158,6 +159,10 @@ def _parse_keystring(keystr): key += c else: yield 'Shift+' + c if c.isupper() else c + if special: + yield '<' + for c in key: + yield 'Shift+' + c if c.isupper() else c def normalize_keystr(keystr): @@ -389,6 +394,7 @@ class KeySequence: @classmethod def parse(cls, keystr): """Parse a keystring like or xyz and return a KeySequence.""" + # FIXME: test stuff like new = cls() strings = list(_parse_keystring(keystr)) for sub in utils.chunk(strings, cls._MAX_LEN): @@ -396,8 +402,7 @@ class KeySequence: new._sequences.append(sequence) if keystr: - # FIXME fails with " 0 + assert len(new) > 0, keystr new._validate(keystr) return new From f18b5aa78298734421984df274b632d8820951f6 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 27 Feb 2018 09:23:06 +0100 Subject: [PATCH 069/135] Fix searching for blacklisted keys in keyhintwidget --- qutebrowser/misc/keyhintwidget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutebrowser/misc/keyhintwidget.py b/qutebrowser/misc/keyhintwidget.py index b9985dbd0..35d453557 100644 --- a/qutebrowser/misc/keyhintwidget.py +++ b/qutebrowser/misc/keyhintwidget.py @@ -108,7 +108,7 @@ class KeyHintView(QLabel): bindings_dict = config.key_instance.get_bindings_for(modename) bindings = [(k, v) for (k, v) in sorted(bindings_dict.items()) if keyutils.KeySequence.parse(prefix).matches(k) and - not blacklisted(k) and + not blacklisted(str(k)) and (takes_count(v) or not countstr)] if not bindings: From 362f923f06cecbdcc211ad3063cdefd6935d7b8a Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 27 Feb 2018 09:33:50 +0100 Subject: [PATCH 070/135] Fix lint --- qutebrowser/config/configcommands.py | 2 +- qutebrowser/keyinput/basekeyparser.py | 19 +++++---- qutebrowser/keyinput/keyutils.py | 48 +++++++++++++++++------ qutebrowser/misc/keyhintwidget.py | 1 - qutebrowser/utils/utils.py | 9 ++--- tests/unit/config/test_config.py | 1 - tests/unit/keyinput/test_basekeyparser.py | 3 +- tests/unit/utils/test_utils.py | 2 +- 8 files changed, 51 insertions(+), 34 deletions(-) diff --git a/qutebrowser/config/configcommands.py b/qutebrowser/config/configcommands.py index 43b9641fd..311ee4102 100644 --- a/qutebrowser/config/configcommands.py +++ b/qutebrowser/config/configcommands.py @@ -26,7 +26,7 @@ from PyQt5.QtCore import QUrl from qutebrowser.commands import cmdexc, cmdutils from qutebrowser.completion.models import configmodel -from qutebrowser.utils import objreg, utils, message, standarddir, urlmatch +from qutebrowser.utils import objreg, message, standarddir, urlmatch from qutebrowser.config import configtypes, configexc, configfiles, configdata from qutebrowser.misc import editor from qutebrowser.keyinput import keyutils diff --git a/qutebrowser/keyinput/basekeyparser.py b/qutebrowser/keyinput/basekeyparser.py index 9510209df..63fc11eee 100644 --- a/qutebrowser/keyinput/basekeyparser.py +++ b/qutebrowser/keyinput/basekeyparser.py @@ -20,8 +20,6 @@ """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 @@ -148,7 +146,8 @@ class BaseKeyParser(QObject): sequence = self._sequence.append_event(e) match, binding = self._match_key(sequence) if match == QKeySequence.NoMatch: - mapped = config.val.bindings.key_mappings.get(sequence, None) + mappings = config.val.bindings.key_mappings + mapped = mappings.get(sequence, None) if mapped is not None: match, binding = self._match_key(mapped) @@ -241,13 +240,13 @@ class BaseKeyParser(QObject): assert cmd self.bindings[key] = cmd - def _parse_key_command(self, modename, key, cmd): - """Parse the keys and their command and store them in the object.""" - # FIXME - # elif self._warn_on_keychains: - # log.keyboard.warning("Ignoring keychain '{}' in mode '{}' because " - # "keychains are not supported there." - # .format(key, modename)) + # FIXME + # def _parse_key_command(self, modename, key, cmd): + # """Parse the keys and their command and store them in the object.""" + # 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): """Handle a completed keychain. diff --git a/qutebrowser/keyinput/keyutils.py b/qutebrowser/keyinput/keyutils.py index 58b29123d..52c41d339 100644 --- a/qutebrowser/keyinput/keyutils.py +++ b/qutebrowser/keyinput/keyutils.py @@ -27,7 +27,7 @@ import attr from PyQt5.QtCore import Qt, QEvent from PyQt5.QtGui import QKeySequence, QKeyEvent -from qutebrowser.utils import utils, debug +from qutebrowser.utils import utils def key_to_string(key): @@ -209,8 +209,8 @@ class KeyInfo: an empty string if only modifiers are pressed. """ if utils.is_mac: - # Qt swaps Ctrl/Meta on macOS, so we switch it back here so the user - # can use it in the config as expected. See: + # Qt swaps Ctrl/Meta on macOS, so we switch it back here so the + # user can use it in the config as expected. See: # https://github.com/qutebrowser/qutebrowser/issues/110 # http://doc.qt.io/qt-5.4/osx-issues.html#special-keys modmask2str = collections.OrderedDict([ @@ -228,9 +228,9 @@ class KeyInfo: ]) modifier_keys = (Qt.Key_Control, Qt.Key_Alt, Qt.Key_Shift, Qt.Key_Meta, - Qt.Key_AltGr, Qt.Key_Super_L, Qt.Key_Super_R, - Qt.Key_Hyper_L, Qt.Key_Hyper_R, Qt.Key_Direction_L, - Qt.Key_Direction_R) + Qt.Key_AltGr, Qt.Key_Super_L, Qt.Key_Super_R, + Qt.Key_Hyper_L, Qt.Key_Hyper_R, Qt.Key_Direction_L, + Qt.Key_Direction_R) if self.key in modifier_keys: # Only modifier pressed return '' @@ -285,6 +285,18 @@ class KeyInfo: class KeySequence: + """A sequence of key presses. + + This internally uses chained QKeySequence objects and exposes a nicer + interface over it. + + 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): @@ -293,7 +305,7 @@ class KeySequence: sequence = QKeySequence(*sub) self._sequences.append(sequence) if keys: - assert len(self) > 0 + assert self self._validate() def __str__(self): @@ -316,15 +328,19 @@ class KeySequence: 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 __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): @@ -334,6 +350,9 @@ class KeySequence: 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()) @@ -351,6 +370,7 @@ class KeySequence: raise KeyParseError(keystr, "Got unknown key!") def matches(self, other): + """Check whether the given KeySequence matches with this one.""" # FIXME test this # pylint: disable=protected-access assert self._sequences @@ -369,16 +389,16 @@ class KeySequence: 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. + 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. + In addition, Shift also *is* relevant when other modifiers are + involved. Shift-Ctrl-X should not be equivalent to Ctrl-X. FIXME: create test cases! """ - # pylint: disable=protected-access modifiers = ev.modifiers() if (modifiers == Qt.ShiftModifier and @@ -394,6 +414,7 @@ class KeySequence: @classmethod def parse(cls, keystr): """Parse a keystring like or xyz and return a KeySequence.""" + # pylint: disable=protected-access # FIXME: test stuff like new = cls() strings = list(_parse_keystring(keystr)) @@ -402,7 +423,8 @@ class KeySequence: new._sequences.append(sequence) if keystr: - assert len(new) > 0, keystr + assert new, keystr + # pylint: disable=protected-access new._validate(keystr) return new diff --git a/qutebrowser/misc/keyhintwidget.py b/qutebrowser/misc/keyhintwidget.py index 35d453557..11446aa40 100644 --- a/qutebrowser/misc/keyhintwidget.py +++ b/qutebrowser/misc/keyhintwidget.py @@ -30,7 +30,6 @@ import re from PyQt5.QtWidgets import QLabel, QSizePolicy from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt -from PyQt5.QtGui import QKeySequence from qutebrowser.config import config from qutebrowser.utils import utils, usertypes diff --git a/qutebrowser/utils/utils.py b/qutebrowser/utils/utils.py index 9d95069c5..f7c1c90b0 100644 --- a/qutebrowser/utils/utils.py +++ b/qutebrowser/utils/utils.py @@ -26,18 +26,15 @@ import re import sys import enum import json -import collections import datetime import traceback import functools import contextlib import socket import shlex -import unicodedata -import attr -from PyQt5.QtCore import Qt, QUrl -from PyQt5.QtGui import QKeySequence, QColor, QClipboard, QDesktopServices +from PyQt5.QtCore import QUrl +from PyQt5.QtGui import QColor, QClipboard, QDesktopServices from PyQt5.QtWidgets import QApplication import pkg_resources import yaml @@ -49,7 +46,7 @@ except ImportError: # pragma: no cover YAML_C_EXT = False import qutebrowser -from qutebrowser.utils import qtutils, log, debug +from qutebrowser.utils import qtutils, log fake_clipboard = None diff --git a/tests/unit/config/test_config.py b/tests/unit/config/test_config.py index a2a2697b1..f47ee7a0a 100644 --- a/tests/unit/config/test_config.py +++ b/tests/unit/config/test_config.py @@ -18,7 +18,6 @@ """Tests for qutebrowser.config.config.""" -import copy import types import unittest.mock diff --git a/tests/unit/keyinput/test_basekeyparser.py b/tests/unit/keyinput/test_basekeyparser.py index 91acfa423..ffd57d5f4 100644 --- a/tests/unit/keyinput/test_basekeyparser.py +++ b/tests/unit/keyinput/test_basekeyparser.py @@ -203,7 +203,8 @@ class TestSpecialKeys: assert not keyparser.execute.called def test_no_binding(self, monkeypatch, fake_keyevent_factory, keyparser): - monkeypatch.setattr(keyutils, 'keyevent_to_string', lambda binding: None) + monkeypatch.setattr(keyutils, 'keyevent_to_string', + lambda binding: None) keyparser.handle(fake_keyevent_factory(Qt.Key_A, Qt.NoModifier)) assert not keyparser.execute.called diff --git a/tests/unit/utils/test_utils.py b/tests/unit/utils/test_utils.py index 577ede306..e8391a74f 100644 --- a/tests/unit/utils/test_utils.py +++ b/tests/unit/utils/test_utils.py @@ -30,7 +30,7 @@ import re import shlex import attr -from PyQt5.QtCore import Qt, QUrl +from PyQt5.QtCore import QUrl from PyQt5.QtGui import QColor, QClipboard import pytest From c0e2550046e48db647189c21a573d0ed5ac20090 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 27 Feb 2018 09:36:56 +0100 Subject: [PATCH 071/135] Fix scripts.keytester --- scripts/keytester.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/keytester.py b/scripts/keytester.py index 80260f6bf..4d27a3dd1 100644 --- a/scripts/keytester.py +++ b/scripts/keytester.py @@ -25,7 +25,7 @@ Use python3 -m scripts.keytester to launch it. from PyQt5.QtWidgets import QApplication, QWidget, QLabel, QHBoxLayout -from qutebrowser.utils import utils +from qutebrowser.keyinput import keyutils class KeyWidget(QWidget): @@ -41,7 +41,7 @@ class KeyWidget(QWidget): def keyPressEvent(self, e): """Show pressed keys.""" lines = [ - str(utils.keyevent_to_string(e)), + str(keyutils.keyevent_to_string(e)), '', 'key: 0x{:x}'.format(int(e.key())), 'modifiers: 0x{:x}'.format(int(e.modifiers())), From 079fcc7eeac7a486d4f8822738a3a8fa0e28c05b Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 27 Feb 2018 09:38:40 +0100 Subject: [PATCH 072/135] Add FIXME --- qutebrowser/keyinput/keyutils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/qutebrowser/keyinput/keyutils.py b/qutebrowser/keyinput/keyutils.py index 52c41d339..b3890f57c 100644 --- a/qutebrowser/keyinput/keyutils.py +++ b/qutebrowser/keyinput/keyutils.py @@ -416,6 +416,7 @@ class KeySequence: """Parse a keystring like or xyz and return a KeySequence.""" # pylint: disable=protected-access # FIXME: test stuff like + # FIXME make sure all callers handle KeyParseError new = cls() strings = list(_parse_keystring(keystr)) for sub in utils.chunk(strings, cls._MAX_LEN): From 72e30cc12c21bee189c71aa06baab3436632ebf0 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 27 Feb 2018 09:47:06 +0100 Subject: [PATCH 073/135] Fix following hints --- qutebrowser/browser/hints.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutebrowser/browser/hints.py b/qutebrowser/browser/hints.py index 1d0184c73..48a0193e6 100644 --- a/qutebrowser/browser/hints.py +++ b/qutebrowser/browser/hints.py @@ -927,7 +927,7 @@ class HintManager(QObject): raise cmdexc.CommandError("No hint {}!".format(keystring)) if select: - self._handle_auto_follow(keystring) + self.handle_partial_key(keystring) else: self._fire(keystring) From ba012c6ba894e2916ff1cfc5ba756f3f6da445da Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 27 Feb 2018 10:35:19 +0100 Subject: [PATCH 074/135] Get rid of BaseKeyparser.Type --- qutebrowser/keyinput/basekeyparser.py | 14 ++------- qutebrowser/keyinput/keyparser.py | 2 +- qutebrowser/keyinput/modeparsers.py | 13 -------- tests/unit/keyinput/test_basekeyparser.py | 37 ++++++++--------------- tests/unit/keyinput/test_modeparsers.py | 3 +- 5 files changed, 17 insertions(+), 52 deletions(-) diff --git a/qutebrowser/keyinput/basekeyparser.py b/qutebrowser/keyinput/basekeyparser.py index 63fc11eee..35c3c8c6d 100644 --- a/qutebrowser/keyinput/basekeyparser.py +++ b/qutebrowser/keyinput/basekeyparser.py @@ -19,8 +19,6 @@ """Base class for vim-like key sequence parser.""" -import enum - from PyQt5.QtCore import pyqtSignal, QObject from PyQt5.QtGui import QKeySequence @@ -43,10 +41,6 @@ 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. @@ -76,8 +70,6 @@ class BaseKeyParser(QObject): do_log = True passthrough = False - Type = enum.Enum('Type', ['chain', 'special']) - def __init__(self, win_id, parent=None, supports_count=None, supports_chains=False): super().__init__(parent) @@ -157,7 +149,7 @@ class BaseKeyParser(QObject): self._sequence)) count = int(self._count) if self._count else None self.clear_keystring() - self.execute(binding, self.Type.chain, count) + self.execute(binding, count) elif match == QKeySequence.PartialMatch: self._debug_log("No match for '{}' (added {})".format( self._sequence, txt)) @@ -248,13 +240,11 @@ class BaseKeyParser(QObject): # "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. - # FIXME do we still need this? - keytype: Type.chain or Type.special count: The count if given. """ raise NotImplementedError diff --git a/qutebrowser/keyinput/keyparser.py b/qutebrowser/keyinput/keyparser.py index aab92bdb0..9914f0686 100644 --- a/qutebrowser/keyinput/keyparser.py +++ b/qutebrowser/keyinput/keyparser.py @@ -39,7 +39,7 @@ class CommandKeyParser(BaseKeyParser): super().__init__(win_id, parent, supports_count, supports_chains) self._commandrunner = runners.CommandRunner(win_id) - def execute(self, cmdstr, _keytype, count=None): + def execute(self, cmdstr, count=None): try: self._commandrunner.run(cmdstr, count) except cmdexc.Error as e: diff --git a/qutebrowser/keyinput/modeparsers.py b/qutebrowser/keyinput/modeparsers.py index 0b196c23d..89f8f5ddb 100644 --- a/qutebrowser/keyinput/modeparsers.py +++ b/qutebrowser/keyinput/modeparsers.py @@ -232,19 +232,6 @@ class HintKeyParser(keyparser.CommandKeyParser): return match != QKeySequence.NoMatch - # FIXME why is this needed? - # 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) - def update_bindings(self, strings, preserve_filter=False): """Update bindings when the hint strings changed. diff --git a/tests/unit/keyinput/test_basekeyparser.py b/tests/unit/keyinput/test_basekeyparser.py index ffd57d5f4..ce660777f 100644 --- a/tests/unit/keyinput/test_basekeyparser.py +++ b/tests/unit/keyinput/test_basekeyparser.py @@ -181,15 +181,13 @@ class TestSpecialKeys: modifier = Qt.MetaModifier if utils.is_mac else Qt.ControlModifier keyparser.handle(fake_keyevent_factory(Qt.Key_A, modifier)) keyparser.handle(fake_keyevent_factory(Qt.Key_X, modifier)) - keyparser.execute.assert_called_once_with( - 'message-info ctrla', keyparser.Type.chain, None) + keyparser.execute.assert_called_once_with('message-info ctrla', None) def test_valid_key_count(self, fake_keyevent_factory, keyparser): modifier = Qt.MetaModifier if utils.is_mac else Qt.ControlModifier keyparser.handle(fake_keyevent_factory(Qt.Key_5, text='5')) keyparser.handle(fake_keyevent_factory(Qt.Key_A, modifier, text='A')) - keyparser.execute.assert_called_once_with( - 'message-info ctrla', keyparser.Type.chain, 5) + keyparser.execute.assert_called_once_with('message-info ctrla', 5) def test_invalid_key(self, fake_keyevent_factory, keyparser): keyparser.handle(fake_keyevent_factory( @@ -212,8 +210,7 @@ class TestSpecialKeys: modifier = Qt.MetaModifier if utils.is_mac else Qt.ControlModifier keyparser.handle(fake_keyevent_factory(Qt.Key_B, modifier)) - keyparser.execute.assert_called_once_with( - 'message-info ctrla', keyparser.Type.chain, None) + keyparser.execute.assert_called_once_with('message-info ctrla', None) def test_binding_and_mapping(self, config_stub, fake_keyevent_factory, keyparser): @@ -221,8 +218,7 @@ class TestSpecialKeys: modifier = Qt.MetaModifier if utils.is_mac else Qt.ControlModifier keyparser.handle(fake_keyevent_factory(Qt.Key_A, modifier)) - keyparser.execute.assert_called_once_with( - 'message-info ctrla', keyparser.Type.chain, None) + keyparser.execute.assert_called_once_with('message-info ctrla', None) class TestKeyChain: @@ -240,8 +236,7 @@ class TestKeyChain: modifier = Qt.ControlModifier keyparser.handle(fake_keyevent_factory(Qt.Key_A, modifier)) keyparser.handle(fake_keyevent_factory(Qt.Key_X, modifier)) - keyparser.execute.assert_called_once_with( - 'message-info ctrla', keyparser.Type.chain, None) + keyparser.execute.assert_called_once_with('message-info ctrla', None) assert not keyparser._sequence def test_invalid_special_key(self, fake_keyevent_factory, keyparser): @@ -255,14 +250,12 @@ class TestKeyChain: handle_text((Qt.Key_X, 'x'), # Then start the real chain (Qt.Key_B, 'b'), (Qt.Key_A, 'a')) - keyparser.execute.assert_called_with( - 'message-info ba', keyparser.Type.chain, None) + keyparser.execute.assert_called_with('message-info ba', None) assert not keyparser._sequence def test_0_press(self, handle_text, keyparser): handle_text((Qt.Key_0, '0')) - keyparser.execute.assert_called_once_with( - 'message-info 0', keyparser.Type.chain, None) + keyparser.execute.assert_called_once_with('message-info 0', None) assert not keyparser._sequence def test_ambiguous_keychain(self, handle_text, keyparser): @@ -276,8 +269,7 @@ class TestKeyChain: def test_mapping(self, config_stub, handle_text, keyparser): handle_text((Qt.Key_X, 'x')) - keyparser.execute.assert_called_once_with( - 'message-info a', keyparser.Type.chain, None) + keyparser.execute.assert_called_once_with('message-info a', None) def test_binding_and_mapping(self, config_stub, handle_text, keyparser): """with a conflicting binding/mapping, the binding should win.""" @@ -296,22 +288,20 @@ class TestCount: def test_no_count(self, handle_text, keyparser): """Test with no count added.""" handle_text((Qt.Key_B, 'b'), (Qt.Key_A, 'a')) - keyparser.execute.assert_called_once_with( - 'message-info ba', keyparser.Type.chain, None) + keyparser.execute.assert_called_once_with('message-info ba', None) assert not keyparser._sequence def test_count_0(self, handle_text, keyparser): handle_text((Qt.Key_0, '0'), (Qt.Key_B, 'b'), (Qt.Key_A, 'a')) - calls = [mock.call('message-info 0', keyparser.Type.chain, None), - mock.call('message-info ba', keyparser.Type.chain, None)] + calls = [mock.call('message-info 0', None), + mock.call('message-info ba', None)] keyparser.execute.assert_has_calls(calls) assert not keyparser._sequence def test_count_42(self, handle_text, keyparser): handle_text((Qt.Key_4, '4'), (Qt.Key_2, '2'), (Qt.Key_B, 'b'), (Qt.Key_A, 'a')) - keyparser.execute.assert_called_once_with( - 'message-info ba', keyparser.Type.chain, 42) + keyparser.execute.assert_called_once_with('message-info ba', 42) assert not keyparser._sequence def test_count_42_invalid(self, handle_text, keyparser): @@ -323,8 +313,7 @@ class TestCount: # Valid call with ccc gets the correct count handle_text((Qt.Key_2, '2'), (Qt.Key_3, '3'), (Qt.Key_C, 'c'), (Qt.Key_C, 'c'), (Qt.Key_C, 'c')) - keyparser.execute.assert_called_once_with( - 'message-info ccc', keyparser.Type.chain, 23) + keyparser.execute.assert_called_once_with('message-info ccc', 23) assert not keyparser._sequence diff --git a/tests/unit/keyinput/test_modeparsers.py b/tests/unit/keyinput/test_modeparsers.py index 50332369f..d53328b7e 100644 --- a/tests/unit/keyinput/test_modeparsers.py +++ b/tests/unit/keyinput/test_modeparsers.py @@ -56,8 +56,7 @@ class TestsNormalKeyParser: # Then start the real chain keyparser.handle(fake_keyevent_factory(Qt.Key_B, text='b')) keyparser.handle(fake_keyevent_factory(Qt.Key_A, text='a')) - keyparser.execute.assert_called_with( - 'message-info ba', keyparser.Type.chain, None) + keyparser.execute.assert_called_with('message-info ba', None) assert not keyparser._sequence def test_partial_keychain_timeout(self, keyparser, config_stub, From ec3ad8a9698f6c936a0f63281ea34ae16101f03e Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 27 Feb 2018 10:55:16 +0100 Subject: [PATCH 075/135] Get rid of _warn_on_keychains and _supports_chains --- qutebrowser/keyinput/basekeyparser.py | 26 ++----------------- qutebrowser/keyinput/keyparser.py | 13 ++++------ qutebrowser/keyinput/modeman.py | 3 +-- qutebrowser/keyinput/modeparsers.py | 15 ++++------- tests/unit/keyinput/test_basekeyparser.py | 31 +---------------------- 5 files changed, 14 insertions(+), 74 deletions(-) diff --git a/qutebrowser/keyinput/basekeyparser.py b/qutebrowser/keyinput/basekeyparser.py index 35c3c8c6d..ec1004316 100644 --- a/qutebrowser/keyinput/basekeyparser.py +++ b/qutebrowser/keyinput/basekeyparser.py @@ -48,13 +48,9 @@ class BaseKeyParser(QObject): Attributes: bindings: Bound key bindings _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. _sequence: The currently entered key sequence _modename: The name of the input mode associated with this keyparser. _supports_count: Whether count is supported - # FIXME is this still needed? - _supports_chains: Whether keychains are supported Signals: keystring_updated: Emitted when the keystring is updated. @@ -70,24 +66,18 @@ class BaseKeyParser(QObject): do_log = True passthrough = False - 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._sequence = keyutils.KeySequence() self._count = '' - if supports_count is None: - supports_count = supports_chains self._supports_count = supports_count - self._supports_chains = supports_chains - self._warn_on_keychains = True self.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. @@ -195,10 +185,6 @@ class BaseKeyParser(QObject): """ match = self._handle_key(e) - # FIXME - # if handled or not self._supports_chains: - # return handled - # don't emit twice if the keystring was cleared in self.clear_keystring if self._sequence: self.keystring_updated.emit(self._count + str(self._sequence)) @@ -232,14 +218,6 @@ class BaseKeyParser(QObject): assert cmd self.bindings[key] = cmd - # FIXME - # def _parse_key_command(self, modename, key, cmd): - # """Parse the keys and their command and store them in the object.""" - # 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, count=None): """Handle a completed keychain. diff --git a/qutebrowser/keyinput/keyparser.py b/qutebrowser/keyinput/keyparser.py index 9914f0686..4e7f032d0 100644 --- a/qutebrowser/keyinput/keyparser.py +++ b/qutebrowser/keyinput/keyparser.py @@ -34,9 +34,8 @@ class CommandKeyParser(BaseKeyParser): _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) + 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): @@ -60,7 +59,7 @@ class PassthroughKeyParser(CommandKeyParser): # do_log = False passthrough = True - def __init__(self, win_id, mode, parent=None, warn=True): + def __init__(self, win_id, mode, parent=None): """Constructor. Args: @@ -68,11 +67,9 @@ class PassthroughKeyParser(CommandKeyParser): 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 + super().__init__(win_id, parent) self._read_config(mode) self._mode = mode def __repr__(self): - return utils.get_repr(self, mode=self._mode, - warn=self._warn_on_keychains) + return utils.get_repr(self, mode=self._mode) diff --git a/qutebrowser/keyinput/modeman.py b/qutebrowser/keyinput/modeman.py index e32830f50..75e3af367 100644 --- a/qutebrowser/keyinput/modeman.py +++ b/qutebrowser/keyinput/modeman.py @@ -71,8 +71,7 @@ def init(win_id, parent): 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.prompt: keyparser.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, diff --git a/qutebrowser/keyinput/modeparsers.py b/qutebrowser/keyinput/modeparsers.py index 89f8f5ddb..9c44e4818 100644 --- a/qutebrowser/keyinput/modeparsers.py +++ b/qutebrowser/keyinput/modeparsers.py @@ -48,8 +48,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) @@ -131,8 +130,7 @@ class PromptKeyParser(keyparser.CommandKeyParser): """KeyParser for yes/no prompts.""" 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._read_config('yesno') def __repr__(self): @@ -149,8 +147,7 @@ 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') @@ -261,8 +258,7 @@ class CaretKeyParser(keyparser.CommandKeyParser): 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') @@ -276,8 +272,7 @@ 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') diff --git a/tests/unit/keyinput/test_basekeyparser.py b/tests/unit/keyinput/test_basekeyparser.py index ce660777f..0a8d44c60 100644 --- a/tests/unit/keyinput/test_basekeyparser.py +++ b/tests/unit/keyinput/test_basekeyparser.py @@ -19,7 +19,6 @@ """Tests for BaseKeyParser.""" -import logging from unittest import mock from PyQt5.QtCore import Qt @@ -37,8 +36,7 @@ def keyseq(s): @pytest.fixture def keyparser(key_config_stub): """Fixture providing a BaseKeyParser supporting count/chains.""" - kp = basekeyparser.BaseKeyParser( - 0, supports_count=True, supports_chains=True) + kp = basekeyparser.BaseKeyParser(0, supports_count=True) kp.execute = mock.Mock() yield kp @@ -56,19 +54,6 @@ def handle_text(fake_keyevent_factory, keyparser): return func -@pytest.mark.parametrize('count, chains, count_expected, chains_expected', [ - (True, False, True, False), - (False, True, False, True), - (None, True, True, True), -]) -def test_supports_args(config_stub, count, chains, count_expected, - chains_expected): - kp = basekeyparser.BaseKeyParser( - 0, supports_count=count, supports_chains=chains) - assert kp._supports_count == count_expected - assert kp._supports_chains == chains_expected - - class TestDebugLog: """Make sure _debug_log only logs when do_log is set.""" @@ -154,20 +139,6 @@ class TestReadConfig: assert keyseq('a') in keyparser.bindings assert (keyseq('new') in keyparser.bindings) == expected - # FIXME do we still need this? - @pytest.mark.parametrize('warn_on_keychains', [True, False]) - @pytest.mark.skip(reason='unneeded?') - def test_warn_on_keychains(self, caplog, warn_on_keychains): - """Test _warn_on_keychains.""" - kp = basekeyparser.BaseKeyParser( - 0, supports_count=False, supports_chains=False) - kp._warn_on_keychains = warn_on_keychains - - with caplog.at_level(logging.WARNING): - kp._read_config('normal') - - assert bool(caplog.records) == warn_on_keychains - class TestSpecialKeys: From bd87b4eb10a4541199d27621a7bf43537f8827e5 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 27 Feb 2018 11:04:05 +0100 Subject: [PATCH 076/135] Stop logging in PassthroughKeyParser --- qutebrowser/keyinput/keyparser.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/qutebrowser/keyinput/keyparser.py b/qutebrowser/keyinput/keyparser.py index 4e7f032d0..0ce123bfc 100644 --- a/qutebrowser/keyinput/keyparser.py +++ b/qutebrowser/keyinput/keyparser.py @@ -55,8 +55,7 @@ class PassthroughKeyParser(CommandKeyParser): _mode: The mode this keyparser is for. """ - # FIXME - # do_log = False + do_log = False passthrough = True def __init__(self, win_id, mode, parent=None): From 898f5c50c4e023c225917a34a1e3855a1db9a7a2 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 27 Feb 2018 11:20:19 +0100 Subject: [PATCH 077/135] Add a test for utils.chunk --- qutebrowser/utils/utils.py | 3 ++- tests/unit/utils/test_utils.py | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/qutebrowser/utils/utils.py b/qutebrowser/utils/utils.py index f7c1c90b0..f03d42844 100644 --- a/qutebrowser/utils/utils.py +++ b/qutebrowser/utils/utils.py @@ -663,6 +663,7 @@ def chunk(elems, n): If elems % n != 0, the last chunk will be smaller. """ - # FIXME test this + if n < 1: + raise ValueError("n needs to be at least 1!") for i in range(0, len(elems), n): yield elems[i:i + n] diff --git a/tests/unit/utils/test_utils.py b/tests/unit/utils/test_utils.py index e8391a74f..b2eef0237 100644 --- a/tests/unit/utils/test_utils.py +++ b/tests/unit/utils/test_utils.py @@ -787,3 +787,19 @@ class TestYaml: with tmpfile.open('w', encoding='utf-8') as f: utils.yaml_dump([1, 2], f) assert tmpfile.read() == '- 1\n- 2\n' + + +@pytest.mark.parametrize('elems, n, expected', [ + ([], 1, []), + ([1], 1, [[1]]), + ([1, 2], 2, [[1, 2]]), + ([1, 2, 3, 4], 2, [[1, 2], [3, 4]]), +]) +def test_chunk(elems, n, expected): + assert list(utils.chunk(elems, n)) == expected + + +@pytest.mark.parametrize('n', [-1, 0]) +def test_chunk_invalid(n): + with pytest.raises(ValueError): + list(utils.chunk([], n)) From 8090d3e28940f02e94858d80293dcfcd8a635fd6 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 27 Feb 2018 12:28:59 +0100 Subject: [PATCH 078/135] Handle invalid keys in config.py --- qutebrowser/config/configfiles.py | 3 +++ tests/unit/config/test_configfiles.py | 15 +++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/qutebrowser/config/configfiles.py b/qutebrowser/config/configfiles.py index 82f90db76..05ed23e60 100644 --- a/qutebrowser/config/configfiles.py +++ b/qutebrowser/config/configfiles.py @@ -333,6 +333,9 @@ class ConfigAPI: except urlmatch.ParseError as e: text = "While {} '{}' and parsing pattern".format(action, name) self.errors.append(configexc.ConfigErrorDesc(text, e)) + except keyutils.KeyParseError as e: + text = "While {} '{}' and parsing key".format(action, name) + self.errors.append(configexc.ConfigErrorDesc(text, e)) def finalize(self): """Do work which needs to be done after reading config.py.""" diff --git a/tests/unit/config/test_configfiles.py b/tests/unit/config/test_configfiles.py index c902bb42d..37b565374 100644 --- a/tests/unit/config/test_configfiles.py +++ b/tests/unit/config/test_configfiles.py @@ -29,6 +29,7 @@ from PyQt5.QtCore import QSettings from qutebrowser.config import (config, configfiles, configexc, configdata, configtypes) from qutebrowser.utils import utils, usertypes, urlmatch +from qutebrowser.keyinput import keyutils @pytest.fixture(autouse=True) @@ -699,6 +700,20 @@ class TestConfigPy: message = "'ConfigAPI' object has no attribute 'val'" assert str(error.exception) == message + @pytest.mark.parametrize('line', [ + 'config.bind("", "nop")', + 'config.bind("\U00010000", "nop")', + 'config.unbind("")', + 'config.unbind("\U00010000")', + ]) + def test_invalid_keys(self, confpy, line): + confpy.write(line) + error = confpy.read(error=True) + assert error.text.endswith("and parsing key") + assert isinstance(error.exception, keyutils.KeyParseError) + assert str(error.exception).startswith("Could not parse") + assert str(error.exception).endswith("Got unknown key!") + @pytest.mark.parametrize('line', ["c.foo = 42", "config.set('foo', 42)"]) def test_config_error(self, confpy, line): confpy.write(line) From 7a27469ecdfe7db5e8f3519824c8191b0acc0f10 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 27 Feb 2018 12:40:44 +0100 Subject: [PATCH 079/135] Handle unknown keys in :bind completion --- qutebrowser/completion/models/configmodel.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/qutebrowser/completion/models/configmodel.py b/qutebrowser/completion/models/configmodel.py index 435eb0643..e89dab227 100644 --- a/qutebrowser/completion/models/configmodel.py +++ b/qutebrowser/completion/models/configmodel.py @@ -80,9 +80,16 @@ def bind(key, *, info): """ model = completionmodel.CompletionModel(column_widths=(20, 60, 20)) data = [] - seq = keyutils.KeySequence.parse(key) - cmd_text = info.keyconf.get_command(seq, 'normal') + try: + seq = keyutils.KeySequence.parse(key) + except keyutils.KeyParseError as e: + seq = None + cmd_text = None + data.append(('', str(e), key)) + + if seq: + cmd_text = info.keyconf.get_command(seq, 'normal') if cmd_text: parser = runners.CommandParser() try: @@ -92,7 +99,8 @@ def bind(key, *, info): else: data.append((cmd_text, '(Current) {}'.format(cmd.desc), key)) - cmd_text = info.keyconf.get_command(seq, 'normal', default=True) + if seq: + cmd_text = info.keyconf.get_command(seq, 'normal') if cmd_text: parser = runners.CommandParser() cmd = parser.parse(cmd_text).cmd From 88b50074570a8abcdb5d64ffe18a68923c1b7a11 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 27 Feb 2018 12:54:11 +0100 Subject: [PATCH 080/135] Consolidate invalid :bind/:unbind tests --- tests/unit/config/test_configcommands.py | 67 +++++++++++------------- 1 file changed, 30 insertions(+), 37 deletions(-) diff --git a/tests/unit/config/test_configcommands.py b/tests/unit/config/test_configcommands.py index 320eb2bd4..0e2427c06 100644 --- a/tests/unit/config/test_configcommands.py +++ b/tests/unit/config/test_configcommands.py @@ -19,6 +19,7 @@ """Tests for qutebrowser.config.configcommands.""" import logging +import functools import unittest.mock import pytest @@ -543,23 +544,39 @@ class TestBind: msg = message_mock.getmsg(usertypes.MessageLevel.info) assert msg.text == expected - def test_bind_invalid_mode(self, commands): - """Run ':bind --mode=wrongmode a nop'. + @pytest.mark.parametrize('command, args, kwargs, expected', [ + # :bind --mode=wrongmode a nop + ('bind', ['a', 'nop'], {'mode': 'wrongmode'}, + 'Invalid mode wrongmode!'), + # :bind --mode=wrongmode a + ('bind', ['a'], {'mode': 'wrongmode'}, + 'Invalid mode wrongmode!'), + # :bind --default --mode=wrongmode a + ('bind', ['a'], {'mode': 'wrongmode', 'default': True}, + 'Invalid mode wrongmode!'), + # :bind --default foobar + ('bind', ['foobar'], {'default': True}, + "Can't find binding 'foobar' in normal mode"), + # :unbind foobar + ('unbind', ['foobar'], {}, + "Can't find binding 'foobar' in normal mode"), + # :unbind --mode=wrongmode x + ('unbind', ['x'], {'mode': 'wrongmode'}, + 'Invalid mode wrongmode!'), + ]) + def test_bind_invalid(self, commands, + command, args, kwargs, expected): + """Run various wrong :bind/:unbind invocations. Should show an error. """ - with pytest.raises(cmdexc.CommandError, - match='Invalid mode wrongmode!'): - commands.bind(0, 'a', 'nop', mode='wrongmode') + if command == 'bind': + func = functools.partial(commands.bind, 0) + elif command == 'unbind': + func = commands.unbind - def test_bind_print_invalid_mode(self, commands): - """Run ':bind --mode=wrongmode a'. - - Should show an error. - """ - with pytest.raises(cmdexc.CommandError, - match='Invalid mode wrongmode!'): - commands.bind(0, 'a', mode='wrongmode') + with pytest.raises(cmdexc.CommandError, match=expected): + func(*args, **kwargs) @pytest.mark.parametrize('key', ['a', 'b', '']) def test_bind_duplicate(self, commands, config_stub, key_config_stub, key): @@ -596,18 +613,6 @@ class TestBind: command = key_config_stub.get_command(keyseq('a'), mode='normal') assert command == default_cmd - @pytest.mark.parametrize('key, mode, expected', [ - ('foobar', 'normal', "Can't find binding 'foobar' in normal mode"), - ('x', 'wrongmode', "Invalid mode wrongmode!"), - ]) - def test_bind_default_invalid(self, commands, key, mode, expected): - """Run ':bind --default foobar' / ':bind --default x wrongmode'. - - Should show an error. - """ - with pytest.raises(cmdexc.CommandError, match=expected): - commands.bind(0, key, mode=mode, default=True) - def test_unbind_none(self, commands, config_stub): config_stub.val.bindings.commands = None commands.unbind('H') @@ -641,15 +646,3 @@ class TestBind: assert normalized not in yaml_bindings else: assert yaml_bindings[normalized] is None - - @pytest.mark.parametrize('key, mode, expected', [ - ('foobar', 'normal', "Can't find binding 'foobar' in normal mode"), - ('x', 'wrongmode', "Invalid mode wrongmode!"), - ]) - def test_unbind_invalid(self, commands, key, mode, expected): - """Run ':unbind foobar' / ':unbind x wrongmode'. - - Should show an error. - """ - with pytest.raises(cmdexc.CommandError, match=expected): - commands.unbind(key, mode=mode) From 244590f49df33dbbaec87e33ddaa8af7796eb128 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 27 Feb 2018 12:59:23 +0100 Subject: [PATCH 081/135] Handle unknown keys with :bind/:unbind --- qutebrowser/config/configcommands.py | 11 +++++++++-- tests/unit/config/test_configcommands.py | 6 ++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/qutebrowser/config/configcommands.py b/qutebrowser/config/configcommands.py index 311ee4102..f81c21aac 100644 --- a/qutebrowser/config/configcommands.py +++ b/qutebrowser/config/configcommands.py @@ -59,6 +59,13 @@ class ConfigCommands: raise cmdexc.CommandError("Error while parsing {}: {}" .format(pattern, str(e))) + def _parse_key(self, key): + """Parse a key argument.""" + try: + return keyutils.KeySequence.parse(key) + except keyutils.KeyParseError as e: + raise cmdexc.CommandError(str(e)) + def _print_value(self, option, pattern): """Print the value of the given option.""" with self._handle_config_error(): @@ -143,7 +150,7 @@ class ConfigCommands: tabbed_browser.openurl(QUrl('qute://bindings'), newtab=True) return - seq = keyutils.KeySequence.parse(key) + seq = self._parse_key(key) if command is None: if default: @@ -176,7 +183,7 @@ class ConfigCommands: See `:help bindings.commands` for the available modes. """ with self._handle_config_error(): - self._keyconfig.unbind(keyutils.KeySequence.parse(key), mode=mode, + self._keyconfig.unbind(self._parse_key(key), mode=mode, save_yaml=True) @cmdutils.register(instance='config-commands', star_args_optional=True) diff --git a/tests/unit/config/test_configcommands.py b/tests/unit/config/test_configcommands.py index 0e2427c06..a74b446d1 100644 --- a/tests/unit/config/test_configcommands.py +++ b/tests/unit/config/test_configcommands.py @@ -557,12 +557,18 @@ class TestBind: # :bind --default foobar ('bind', ['foobar'], {'default': True}, "Can't find binding 'foobar' in normal mode"), + # :bind nop + ('bind', ['', 'nop'], {}, + "Could not parse '': Got unknown key!"), # :unbind foobar ('unbind', ['foobar'], {}, "Can't find binding 'foobar' in normal mode"), # :unbind --mode=wrongmode x ('unbind', ['x'], {'mode': 'wrongmode'}, 'Invalid mode wrongmode!'), + # :unbind + ('unbind', [''], {}, + "Could not parse '': Got unknown key!"), ]) def test_bind_invalid(self, commands, command, args, kwargs, expected): From 3a79f1293f08fdadf462ece7cdaf068f3400d3fb Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 27 Feb 2018 12:59:45 +0100 Subject: [PATCH 082/135] Remove FIXMEs --- qutebrowser/keyinput/keyutils.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/qutebrowser/keyinput/keyutils.py b/qutebrowser/keyinput/keyutils.py index b3890f57c..06fcaae4b 100644 --- a/qutebrowser/keyinput/keyutils.py +++ b/qutebrowser/keyinput/keyutils.py @@ -290,6 +290,9 @@ class KeySequence: 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 @@ -344,7 +347,6 @@ class KeySequence: return self._sequences != other._sequences def __hash__(self): - # FIXME is this correct? return hash(tuple(self._sequences)) def __len__(self): @@ -371,7 +373,6 @@ class KeySequence: def matches(self, other): """Check whether the given KeySequence matches with this one.""" - # FIXME test this # pylint: disable=protected-access assert self._sequences assert other._sequences @@ -396,8 +397,6 @@ class KeySequence: In addition, Shift also *is* relevant when other modifiers are involved. Shift-Ctrl-X should not be equivalent to Ctrl-X. - - FIXME: create test cases! """ modifiers = ev.modifiers() @@ -416,7 +415,6 @@ class KeySequence: """Parse a keystring like or xyz and return a KeySequence.""" # pylint: disable=protected-access # FIXME: test stuff like - # FIXME make sure all callers handle KeyParseError new = cls() strings = list(_parse_keystring(keystr)) for sub in utils.chunk(strings, cls._MAX_LEN): From b85fe8f678a39deabbd4498ce579b15f88f58803 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 27 Feb 2018 14:07:20 +0100 Subject: [PATCH 083/135] Merge BaseKeyParser._handle_key into .handle --- qutebrowser/keyinput/basekeyparser.py | 126 ++++++++++++-------------- qutebrowser/keyinput/modeman.py | 8 +- qutebrowser/keyinput/modeparsers.py | 45 ++++----- 3 files changed, 78 insertions(+), 101 deletions(-) diff --git a/qutebrowser/keyinput/basekeyparser.py b/qutebrowser/keyinput/basekeyparser.py index ec1004316..27d760d9a 100644 --- a/qutebrowser/keyinput/basekeyparser.py +++ b/qutebrowser/keyinput/basekeyparser.py @@ -88,69 +88,6 @@ class BaseKeyParser(QObject): if self.do_log: log.keyboard.debug(message) - def _handle_key(self, e): - """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 - display an error. - - Args: - e: the KeyPressEvent from Qt. - - Return: - A QKeySequence match or None. - """ - key = e.key() - txt = keyutils.keyevent_to_string(e) - self._debug_log("Got key: 0x{:x} / text: '{}'".format(key, txt)) - - if not txt: - self._debug_log("Ignoring, no text char") - return QKeySequence.NoMatch - - # 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 QKeySequence.NoMatch - - if (txt.isdigit() and self._supports_count and not - (not self._count and txt == '0')): - assert len(txt) == 1, txt - self._count += txt - return None - - sequence = self._sequence.append_event(e) - 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: - match, binding = self._match_key(mapped) - - self._sequence = self._sequence.append_event(e) - if match == QKeySequence.ExactMatch: - self._debug_log("Definitive match for '{}'.".format( - self._sequence)) - count = int(self._count) if self._count else None - self.clear_keystring() - self.execute(binding, count) - elif match == QKeySequence.PartialMatch: - self._debug_log("No match for '{}' (added {})".format( - self._sequence, txt)) - elif match == QKeySequence.NoMatch: - self._debug_log("Giving up with '{}', no matches".format( - self._sequence)) - self.clear_keystring() - else: - raise utils.Unreachable("Invalid match value {!r}".format(match)) - return match - def _match_key(self, sequence): """Try to match a given keystring with any bound keychain. @@ -175,21 +112,70 @@ class BaseKeyParser(QObject): return (QKeySequence.NoMatch, None) def handle(self, e): - """Handle a new keypress and call the respective handlers. + """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 + display an error. Args: - e: the KeyPressEvent from Qt + e: the KeyPressEvent from Qt. Return: - True if the event was handled, False otherwise. + A QKeySequence match. """ - match = self._handle_key(e) + key = e.key() + txt = keyutils.keyevent_to_string(e) + self._debug_log("Got key: 0x{:x} / text: '{}'".format(key, txt)) - # don't emit twice if the keystring was cleared in self.clear_keystring - if self._sequence: + if not txt: + self._debug_log("Ignoring, no text char") + return QKeySequence.NoMatch + + # 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 QKeySequence.NoMatch + + if (txt.isdigit() and self._supports_count and not + (not self._count and txt == '0')): + assert len(txt) == 1, txt + self._count += txt + return QKeySequence.ExactMatch + + sequence = self._sequence.append_event(e) + 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: + match, binding = self._match_key(mapped) + + self._sequence = self._sequence.append_event(e) + + if match == QKeySequence.ExactMatch: + self._debug_log("Definitive match for '{}'.".format( + self._sequence)) + count = int(self._count) if self._count else None + self.clear_keystring() + self.execute(binding, count) + elif match == QKeySequence.PartialMatch: + self._debug_log("No match for '{}' (added {})".format( + self._sequence, txt)) self.keystring_updated.emit(self._count + str(self._sequence)) + elif match == QKeySequence.NoMatch: + self._debug_log("Giving up with '{}', no matches".format( + self._sequence)) + self.clear_keystring() + else: + raise utils.Unreachable("Invalid match value {!r}".format(match)) - return match != QKeySequence.NoMatch + return match @config.change_filter('bindings') def _on_config_changed(self): diff --git a/qutebrowser/keyinput/modeman.py b/qutebrowser/keyinput/modeman.py index 75e3af367..94d76832d 100644 --- a/qutebrowser/keyinput/modeman.py +++ b/qutebrowser/keyinput/modeman.py @@ -157,7 +157,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) is_non_alnum = ( event.modifiers() not in [Qt.NoModifier, Qt.ShiftModifier] or @@ -165,7 +165,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)): @@ -178,10 +178,10 @@ class ModeManager(QObject): if curmode != usertypes.KeyMode.insert: focus_widget = QApplication.instance().focusWidget() - log.modes.debug("handled: {}, forward_unbound_keys: {}, " + log.modes.debug("match: {}, forward_unbound_keys: {}, " "passthrough: {}, is_non_alnum: {} --> " "filter: {} (focused: {!r})".format( - handled, forward_unbound_keys, + match, forward_unbound_keys, parser.passthrough, is_non_alnum, filter_this, focus_widget)) return filter_this diff --git a/qutebrowser/keyinput/modeparsers.py b/qutebrowser/keyinput/modeparsers.py index 9c44e4818..f397b9fd5 100644 --- a/qutebrowser/keyinput/modeparsers.py +++ b/qutebrowser/keyinput/modeparsers.py @@ -153,8 +153,8 @@ class HintKeyParser(keyparser.CommandKeyParser): 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. @@ -162,10 +162,8 @@ class HintKeyParser(keyparser.CommandKeyParser): e: the KeyPressEvent from Qt. Return: - True if event has been handled, False otherwise. + A QKeySequence match. """ - # FIXME rewrite this - # FIXME should backspacing be a more generic hint feature? log.keyboard.debug("Got special key 0x{:x} text {}".format( e.key(), e.text())) hintmanager = objreg.get('hintmanager', scope='tab', @@ -178,7 +176,7 @@ class HintKeyParser(keyparser.CommandKeyParser): if self._last_press == LastPress.filtertext and self._filtertext: self._filtertext = self._filtertext[:-1] hintmanager.filter_hints(self._filtertext) - return True + return QKeySequence.ExactMatch elif self._last_press == LastPress.keystring and self._sequence: self._sequence = self._sequence[:-1] self.keystring_updated.emit(str(self._sequence)) @@ -187,18 +185,18 @@ class HintKeyParser(keyparser.CommandKeyParser): # 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 False + return QKeySequence.NoMatch elif hintmanager.current_mode() != 'number': - return False + return QKeySequence.NoMatch elif not e.text(): - return False + 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): """Handle a new keypress and call the respective handlers. @@ -209,25 +207,18 @@ class HintKeyParser(keyparser.CommandKeyParser): Returns: True if the match has been handled, False otherwise. """ - # FIXME rewrite this - match = self._handle_key(e) + match = super().handle(e) if match == QKeySequence.PartialMatch: - # FIXME do we need to check self._sequence here? - self.keystring_updated.emit(str(self._sequence)) self._last_press = LastPress.keystring - return True elif match == QKeySequence.ExactMatch: self._last_press = LastPress.none - return True - elif match is None: # FIXME - return 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)) - return match != QKeySequence.NoMatch + return match def update_bindings(self, strings, preserve_filter=False): """Update bindings when the hint strings changed. @@ -285,15 +276,16 @@ class RegisterKeyParser(keyparser.CommandKeyParser): Return: True if event has been handled, False otherwise. """ - # FIXME rewrite this - if super().handle(e): - return True + match = super().handle(e) + if match: + return match key = e.text() if key == '' or keyutils.keyevent_to_string(e) is None: # this is not a proper register key, let it pass and keep going - return False + # FIXME can we simplify this when we refactor keyutils.py? + return QKeySequence.NoMatch tabbed_browser = objreg.get('tabbed-browser', scope='window', window=self._win_id) @@ -315,5 +307,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 From 49d297f7bf33b4fcf46797c0158032d35867c5ec Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 27 Feb 2018 14:10:55 +0100 Subject: [PATCH 084/135] Fix tests for parsing KeySequences --- tests/unit/keyinput/test_keyutils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/keyinput/test_keyutils.py b/tests/unit/keyinput/test_keyutils.py index db5b380d7..92e52490d 100644 --- a/tests/unit/keyinput/test_keyutils.py +++ b/tests/unit/keyinput/test_keyutils.py @@ -110,8 +110,8 @@ class TestKeyEventToString: ('xyz', keyutils.KeySequence(Qt.Key_X, Qt.Key_Y, Qt.Key_Z)), ('', keyutils.KeySequence(Qt.ControlModifier | Qt.Key_X, Qt.MetaModifier | Qt.Key_Y)), - # FIXME - # (', ', keyutils.KeyParseError), + ('', keyutils.KeyParseError), + ('\U00010000', keyutils.KeyParseError), ]) def test_parse(keystr, expected): if expected is keyutils.KeyParseError: From 5a5873d4eebcec8fb5b380955b6c37b2c3da6edc Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 27 Feb 2018 14:16:41 +0100 Subject: [PATCH 085/135] Rename KeyConfig._prepare to ._validate --- qutebrowser/config/config.py | 12 ++++++------ tests/unit/config/test_config.py | 10 ++++------ 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py index f8f9e7902..eb2a81594 100644 --- a/qutebrowser/config/config.py +++ b/qutebrowser/config/config.py @@ -136,8 +136,8 @@ class KeyConfig: def __init__(self, config): self._config = config - def _prepare(self, key, mode): - """Make sure the given mode exists.""" + def _validate(self, key, mode): + """Validate the given key and mode.""" # Catch old usage of this code assert isinstance(key, keyutils.KeySequence), key if mode not in configdata.DATA['bindings.default'].default: @@ -170,7 +170,7 @@ class KeyConfig: def get_command(self, key, mode, default=False): """Get the command for a given key (or None).""" - self._prepare(key, mode) + self._validate(key, mode) if default: bindings = dict(val.bindings.default[mode]) else: @@ -184,7 +184,7 @@ class KeyConfig: "Can't add binding '{}' with empty command in {} " 'mode'.format(key, mode)) - self._prepare(key, mode) + self._validate(key, mode) log.keyboard.vdebug("Adding binding {} -> {} in mode {}.".format( key, command, mode)) @@ -196,7 +196,7 @@ class KeyConfig: def bind_default(self, key, *, mode='normal', save_yaml=False): """Restore a default keybinding.""" - self._prepare(key, mode) + self._validate(key, mode) bindings_commands = self._config.get_mutable_obj('bindings.commands') try: @@ -208,7 +208,7 @@ class KeyConfig: def unbind(self, key, *, mode='normal', save_yaml=False): """Unbind the given key in the given mode.""" - self._prepare(key, mode) + self._validate(key, mode) bindings_commands = self._config.get_mutable_obj('bindings.commands') diff --git a/tests/unit/config/test_config.py b/tests/unit/config/test_config.py index f47ee7a0a..40e82ba4b 100644 --- a/tests/unit/config/test_config.py +++ b/tests/unit/config/test_config.py @@ -103,15 +103,13 @@ class TestKeyConfig: """Get a dict with no bindings.""" return {'normal': {}} - def test_prepare_invalid_mode(self, key_config_stub): - """Make sure prepare checks the mode.""" + def test_validate_invalid_mode(self, key_config_stub): with pytest.raises(configexc.KeybindingError): - assert key_config_stub._prepare(keyseq('x'), 'abnormal') + assert key_config_stub._validate(keyseq('x'), 'abnormal') - def test_prepare_invalid_type(self, key_config_stub): - """Make sure prepare checks the type.""" + def test_validate_invalid_type(self, key_config_stub): with pytest.raises(AssertionError): - assert key_config_stub._prepare('x', 'normal') + assert key_config_stub._validate('x', 'normal') @pytest.mark.parametrize('commands, expected', [ # Unbinding default key From c3485821c720f5633e0fdc560c319975dd0be0ec Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 27 Feb 2018 14:16:59 +0100 Subject: [PATCH 086/135] Adjust copyright --- qutebrowser/keyinput/keyutils.py | 2 +- tests/unit/keyinput/test_keyutils.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/qutebrowser/keyinput/keyutils.py b/qutebrowser/keyinput/keyutils.py index 06fcaae4b..d2c32e1b5 100644 --- a/qutebrowser/keyinput/keyutils.py +++ b/qutebrowser/keyinput/keyutils.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/unit/keyinput/test_keyutils.py b/tests/unit/keyinput/test_keyutils.py index 92e52490d..760b58e19 100644 --- a/tests/unit/keyinput/test_keyutils.py +++ b/tests/unit/keyinput/test_keyutils.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2014-2018 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # From fdc2458657871aea8f1603c3fdcf65d7a1e98d3b Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 27 Feb 2018 16:03:09 +0100 Subject: [PATCH 087/135] Fix test_split_count after _handle_key merge --- tests/unit/keyinput/test_basekeyparser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/keyinput/test_basekeyparser.py b/tests/unit/keyinput/test_basekeyparser.py index 0a8d44c60..f99c6f4fd 100644 --- a/tests/unit/keyinput/test_basekeyparser.py +++ b/tests/unit/keyinput/test_basekeyparser.py @@ -84,7 +84,7 @@ def test_split_count(config_stub, key_config_stub, kp._read_config('normal') for info in keyseq(input_key): - kp._handle_key(info.to_event()) + kp.handle(info.to_event()) assert kp._count == count assert kp._sequence == keyseq(command) From 2ed480b40a00e709fc7eee04e628172651024d23 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 27 Feb 2018 16:13:05 +0100 Subject: [PATCH 088/135] Refactor configmodel.bind --- qutebrowser/completion/models/configmodel.py | 31 +++++++++++--------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/qutebrowser/completion/models/configmodel.py b/qutebrowser/completion/models/configmodel.py index e89dab227..0396459db 100644 --- a/qutebrowser/completion/models/configmodel.py +++ b/qutebrowser/completion/models/configmodel.py @@ -72,24 +72,16 @@ def value(optname, *_values, info): return model -def bind(key, *, info): - """A CompletionModel filled with all bindable commands and descriptions. - - Args: - key: the key being bound. - """ - model = completionmodel.CompletionModel(column_widths=(20, 60, 20)) +def _bind_current_default(key, info): + """Get current/default data for the given key.""" data = [] - try: seq = keyutils.KeySequence.parse(key) except keyutils.KeyParseError as e: - seq = None - cmd_text = None data.append(('', str(e), key)) + return data - if seq: - cmd_text = info.keyconf.get_command(seq, 'normal') + cmd_text = info.keyconf.get_command(seq, 'normal') if cmd_text: parser = runners.CommandParser() try: @@ -99,13 +91,24 @@ def bind(key, *, info): else: data.append((cmd_text, '(Current) {}'.format(cmd.desc), key)) - if seq: - cmd_text = info.keyconf.get_command(seq, 'normal') + cmd_text = info.keyconf.get_command(seq, 'normal') if cmd_text: parser = runners.CommandParser() cmd = parser.parse(cmd_text).cmd data.append((cmd_text, '(Default) {}'.format(cmd.desc), key)) + return data + + +def bind(key, *, info): + """A CompletionModel filled with all bindable commands and descriptions. + + Args: + key: the key being bound. + """ + model = completionmodel.CompletionModel(column_widths=(20, 60, 20)) + data = _bind_current_default(key, info) + if data: model.add_category(listcategory.ListCategory("Current/Default", data)) From 880da2d1432222248fec66481f578f8ba62bc71a Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 27 Feb 2018 16:17:51 +0100 Subject: [PATCH 089/135] Add missing default=True for configmodel.bind --- qutebrowser/completion/models/configmodel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutebrowser/completion/models/configmodel.py b/qutebrowser/completion/models/configmodel.py index 0396459db..05c36cb2b 100644 --- a/qutebrowser/completion/models/configmodel.py +++ b/qutebrowser/completion/models/configmodel.py @@ -91,7 +91,7 @@ def _bind_current_default(key, info): else: data.append((cmd_text, '(Current) {}'.format(cmd.desc), key)) - cmd_text = info.keyconf.get_command(seq, 'normal') + cmd_text = info.keyconf.get_command(seq, 'normal', default=True) if cmd_text: parser = runners.CommandParser() cmd = parser.parse(cmd_text).cmd From 19512e988bff813d166a3fbd911eb5e83e051903 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 27 Feb 2018 22:21:35 +0100 Subject: [PATCH 090/135] Expose less from keyutils publicly --- qutebrowser/keyinput/basekeyparser.py | 2 +- qutebrowser/keyinput/keyutils.py | 17 ++++++++--------- qutebrowser/keyinput/modeparsers.py | 2 +- scripts/keytester.py | 2 +- tests/unit/keyinput/test_basekeyparser.py | 3 +-- tests/unit/keyinput/test_keyutils.py | 19 ++++++++++--------- 6 files changed, 22 insertions(+), 23 deletions(-) diff --git a/qutebrowser/keyinput/basekeyparser.py b/qutebrowser/keyinput/basekeyparser.py index 27d760d9a..27f671dcd 100644 --- a/qutebrowser/keyinput/basekeyparser.py +++ b/qutebrowser/keyinput/basekeyparser.py @@ -125,7 +125,7 @@ class BaseKeyParser(QObject): A QKeySequence match. """ key = e.key() - txt = keyutils.keyevent_to_string(e) + txt = str(keyutils.KeyInfo.from_event(e)) self._debug_log("Got key: 0x{:x} / text: '{}'".format(key, txt)) if not txt: diff --git a/qutebrowser/keyinput/keyutils.py b/qutebrowser/keyinput/keyutils.py index d2c32e1b5..2cf151348 100644 --- a/qutebrowser/keyinput/keyutils.py +++ b/qutebrowser/keyinput/keyutils.py @@ -30,7 +30,7 @@ from PyQt5.QtGui import QKeySequence, QKeyEvent from qutebrowser.utils import utils -def key_to_string(key): +def _key_to_string(key): """Convert a Qt::Key member to a meaningful name. Args: @@ -127,11 +127,6 @@ def key_to_string(key): return name -def keyevent_to_string(e): - """Convert a QKeyEvent to a meaningful name.""" - return str(KeyInfo(e.key(), e.modifiers())) - - class KeyParseError(Exception): """Raised by _parse_single_key/parse_keystring on parse errors.""" @@ -150,7 +145,7 @@ def _parse_keystring(keystr): for c in keystr: if c == '>': assert special - yield normalize_keystr(key) + yield _normalize_keystr(key) key = '' special = False elif c == '<': @@ -165,7 +160,7 @@ def _parse_keystring(keystr): yield 'Shift+' + c if c.isupper() else c -def normalize_keystr(keystr): +def _normalize_keystr(keystr): """Normalize a keystring like Ctrl-Q to a keystring like Ctrl+Q. Args: @@ -201,6 +196,10 @@ class KeyInfo: 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. @@ -240,7 +239,7 @@ class KeyInfo: if self.modifiers & mask and s not in parts: parts.append(s) - key_string = key_to_string(self.key) + key_string = _key_to_string(self.key) if len(key_string) == 1: category = unicodedata.category(key_string) diff --git a/qutebrowser/keyinput/modeparsers.py b/qutebrowser/keyinput/modeparsers.py index f397b9fd5..42eeb53f8 100644 --- a/qutebrowser/keyinput/modeparsers.py +++ b/qutebrowser/keyinput/modeparsers.py @@ -282,7 +282,7 @@ class RegisterKeyParser(keyparser.CommandKeyParser): key = e.text() - if key == '' or keyutils.keyevent_to_string(e) is None: + if key == '' or not str(keyutils.KeyInfo.from_event(e)): # this is not a proper register key, let it pass and keep going # FIXME can we simplify this when we refactor keyutils.py? return QKeySequence.NoMatch diff --git a/scripts/keytester.py b/scripts/keytester.py index 4d27a3dd1..ee5eb347c 100644 --- a/scripts/keytester.py +++ b/scripts/keytester.py @@ -41,7 +41,7 @@ class KeyWidget(QWidget): def keyPressEvent(self, e): """Show pressed keys.""" lines = [ - str(keyutils.keyevent_to_string(e)), + str(keyutils.KeyInfo.from_event(e)), '', 'key: 0x{:x}'.format(int(e.key())), 'modifiers: 0x{:x}'.format(int(e.modifiers())), diff --git a/tests/unit/keyinput/test_basekeyparser.py b/tests/unit/keyinput/test_basekeyparser.py index f99c6f4fd..a32009390 100644 --- a/tests/unit/keyinput/test_basekeyparser.py +++ b/tests/unit/keyinput/test_basekeyparser.py @@ -172,8 +172,7 @@ class TestSpecialKeys: assert not keyparser.execute.called def test_no_binding(self, monkeypatch, fake_keyevent_factory, keyparser): - monkeypatch.setattr(keyutils, 'keyevent_to_string', - lambda binding: None) + monkeypatch.setattr(keyutils.KeyInfo, '__str__', lambda _self: '') keyparser.handle(fake_keyevent_factory(Qt.Key_A, Qt.NoModifier)) assert not keyparser.execute.called diff --git a/tests/unit/keyinput/test_keyutils.py b/tests/unit/keyinput/test_keyutils.py index 760b58e19..db6855d23 100644 --- a/tests/unit/keyinput/test_keyutils.py +++ b/tests/unit/keyinput/test_keyutils.py @@ -38,14 +38,14 @@ class TestKeyToString: ]) def test_normal(self, key, expected): """Test a special key where QKeyEvent::toString works incorrectly.""" - assert keyutils.key_to_string(key) == expected + assert keyutils._key_to_string(key) == expected def test_missing(self, monkeypatch): """Test with a missing key.""" monkeypatch.delattr(keyutils.Qt, 'Key_Blue') # We don't want to test the key which is actually missing - we only # want to know if the mapping still behaves properly. - assert keyutils.key_to_string(Qt.Key_A) == 'A' + assert keyutils._key_to_string(Qt.Key_A) == 'A' def test_all(self): """Make sure there's some sensible output for all keys.""" @@ -53,7 +53,7 @@ class TestKeyToString: if not isinstance(value, Qt.Key): continue print(name) - string = keyutils.key_to_string(value) + string = keyutils._key_to_string(value) assert string string.encode('utf-8') # make sure it's encodable @@ -66,37 +66,38 @@ class TestKeyEventToString: """Test keyeevent when only control is pressed.""" evt = fake_keyevent_factory(key=Qt.Key_Control, modifiers=Qt.ControlModifier) - assert not keyutils.keyevent_to_string(evt) + assert not str(keyutils.KeyInfo.from_event(evt)) def test_only_hyper_l(self, fake_keyevent_factory): """Test keyeevent when only Hyper_L is pressed.""" evt = fake_keyevent_factory(key=Qt.Key_Hyper_L, modifiers=Qt.MetaModifier) - assert not keyutils.keyevent_to_string(evt) + assert not str(keyutils.KeyInfo.from_event(evt)) def test_only_key(self, fake_keyevent_factory): """Test with a simple key pressed.""" evt = fake_keyevent_factory(key=Qt.Key_A) - assert keyutils.keyevent_to_string(evt) == 'a' + assert str(keyutils.KeyInfo.from_event(evt)) == 'a' def test_key_and_modifier(self, fake_keyevent_factory): """Test with key and modifier pressed.""" evt = fake_keyevent_factory(key=Qt.Key_A, modifiers=Qt.ControlModifier) expected = '' if utils.is_mac else '' - assert keyutils.keyevent_to_string(evt) == expected + assert str(keyutils.KeyInfo.from_event(evt)) == expected def test_key_and_modifiers(self, fake_keyevent_factory): """Test with key and multiple modifiers pressed.""" evt = fake_keyevent_factory( key=Qt.Key_A, modifiers=(Qt.ControlModifier | Qt.AltModifier | Qt.MetaModifier | Qt.ShiftModifier)) - assert keyutils.keyevent_to_string(evt) == '' + s = str(keyutils.KeyInfo.from_event(evt)) + assert s == '' @pytest.mark.fake_os('mac') def test_mac(self, fake_keyevent_factory): """Test with a simulated mac.""" evt = fake_keyevent_factory(key=Qt.Key_A, modifiers=Qt.ControlModifier) - assert keyutils.keyevent_to_string(evt) == '' + assert str(keyutils.KeyInfo.from_event(evt)) == '' @pytest.mark.parametrize('keystr, expected', [ From f714be0ff77eb26f7d4c5bf84b37da28ec71062e Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 28 Feb 2018 09:03:47 +0100 Subject: [PATCH 091/135] Initial tests on all Qt keys --- tests/unit/keyinput/key_data.py | 566 +++++++++++++++++++++++++++ tests/unit/keyinput/test_keyutils.py | 18 +- 2 files changed, 582 insertions(+), 2 deletions(-) create mode 100644 tests/unit/keyinput/key_data.py diff --git a/tests/unit/keyinput/key_data.py b/tests/unit/keyinput/key_data.py new file mode 100644 index 000000000..fb70bab72 --- /dev/null +++ b/tests/unit/keyinput/key_data.py @@ -0,0 +1,566 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2018 Florian Bruhin (The Compiler) +# +# 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 . + +import attr + +from PyQt5.QtCore import Qt + + +@attr.s +class Key: + + attribute = attr.ib() + name = attr.ib(None) # default: name == attribute + text = attr.ib('') + member = attr.ib(None) + + +# From enum Key in qt5/qtbase/src/corelib/global/qnamespace.h +KEYS = [ + ### misc keys + Key('Escape'), + Key('Tab'), + Key('Backtab'), + Key('Backspace'), + Key('Return'), + Key('Enter'), + Key('Insert'), + Key('Delete'), + Key('Pause'), + Key('Print'), # print screen + Key('SysReq'), + Key('Clear'), + ### cursor movement + Key('Home'), + Key('End'), + Key('Left'), + Key('Up'), + Key('Right'), + Key('Down'), + Key('PageUp'), + Key('PageDown'), + ### modifiers + Key('Shift'), + Key('Control'), + Key('Meta'), + Key('Alt'), + Key('CapsLock'), + Key('NumLock'), + Key('ScrollLock'), + ### function keys + Key('F1'), + Key('F2'), + Key('F3'), + Key('F4'), + Key('F5'), + Key('F6'), + Key('F7'), + Key('F8'), + Key('F9'), + Key('F10'), + Key('F11'), + Key('F12'), + Key('F13'), + Key('F14'), + Key('F15'), + Key('F16'), + Key('F17'), + Key('F18'), + Key('F19'), + Key('F20'), + Key('F21'), + Key('F22'), + Key('F23'), + Key('F24'), + # F25 .. F35 only on X11 + Key('F25'), + Key('F26'), + Key('F27'), + Key('F28'), + Key('F29'), + Key('F30'), + Key('F31'), + Key('F32'), + Key('F33'), + Key('F34'), + Key('F35'), + ### extra keys + Key('Super_L'), + Key('Super_R'), + Key('Menu'), + Key('Hyper_L'), + Key('Hyper_R'), + Key('Help'), + Key('Direction_L'), + Key('Direction_R'), + ### 7 bit printable ASCII + Key('Space'), + Key('Any'), + Key('Exclam'), + Key('QuoteDbl'), + Key('NumberSign'), + Key('Dollar'), + Key('Percent'), + Key('Ampersand'), + Key('Apostrophe'), + Key('ParenLeft'), + Key('ParenRight'), + Key('Asterisk'), + Key('Plus'), + Key('Comma'), + Key('Minus'), + Key('Period'), + Key('Slash'), + Key('0'), + Key('1'), + Key('2'), + Key('3'), + Key('4'), + Key('5'), + Key('6'), + Key('7'), + Key('8'), + Key('9'), + Key('Colon'), + Key('Semicolon'), + Key('Less'), + Key('Equal'), + Key('Greater'), + Key('Question'), + Key('At'), + Key('A'), + Key('B'), + Key('C'), + Key('D'), + Key('E'), + Key('F'), + Key('G'), + Key('H'), + Key('I'), + Key('J'), + Key('K'), + Key('L'), + Key('M'), + Key('N'), + Key('O'), + Key('P'), + Key('Q'), + Key('R'), + Key('S'), + Key('T'), + Key('U'), + Key('V'), + Key('W'), + Key('X'), + Key('Y'), + Key('Z'), + Key('BracketLeft'), + Key('Backslash'), + Key('BracketRight'), + Key('AsciiCircum'), + Key('Underscore'), + Key('QuoteLeft'), + Key('BraceLeft'), + Key('Bar'), + Key('BraceRight'), + Key('AsciiTilde'), + + Key('nobreakspace'), + Key('exclamdown'), + Key('cent'), + Key('sterling'), + Key('currency'), + Key('yen'), + Key('brokenbar'), + Key('section'), + Key('diaeresis'), + Key('copyright'), + Key('ordfeminine'), + Key('guillemotleft'), # left angle quotation mark + Key('notsign'), + Key('hyphen'), + Key('registered'), + Key('macron'), + Key('degree'), + Key('plusminus'), + Key('twosuperior'), + Key('threesuperior'), + Key('acute'), + Key('mu'), + Key('paragraph'), + Key('periodcentered'), + Key('cedilla'), + Key('onesuperior'), + Key('masculine'), + Key('guillemotright'), # right angle quotation mark + Key('onequarter'), + Key('onehalf'), + Key('threequarters'), + Key('questiondown'), + Key('Agrave'), + Key('Aacute'), + Key('Acircumflex'), + Key('Atilde'), + Key('Adiaeresis'), + Key('Aring'), + Key('AE'), + Key('Ccedilla'), + Key('Egrave'), + Key('Eacute'), + Key('Ecircumflex'), + Key('Ediaeresis'), + Key('Igrave'), + Key('Iacute'), + Key('Icircumflex'), + Key('Idiaeresis'), + Key('ETH'), + Key('Ntilde'), + Key('Ograve'), + Key('Oacute'), + Key('Ocircumflex'), + Key('Otilde'), + Key('Odiaeresis'), + Key('multiply'), + Key('Ooblique'), + Key('Ugrave'), + Key('Uacute'), + Key('Ucircumflex'), + Key('Udiaeresis'), + Key('Yacute'), + Key('THORN'), + Key('ssharp'), + Key('division'), + Key('ydiaeresis'), + + ### International input method support (X keycode - 0xEE00, the + ### definition follows Qt/Embedded 2.3.7) Only interesting if + ### you are writing your own input method + + ### International & multi-key character composition + Key('AltGr'), + Key('Multi_key'), # Multi-key character compose + Key('Codeinput'), + Key('SingleCandidate'), + Key('MultipleCandidate'), + Key('PreviousCandidate'), + + ### Misc Functions + Key('Mode_switch'), # Character set switch + # Key('script_switch'), # Alias for mode_switch + + ### Japanese keyboard support + Key('Kanji'), # Kanji, Kanji convert + Key('Muhenkan'), # Cancel Conversion + # Key('Henkan_Mode'), # Start/Stop Conversion + Key('Henkan'), # Alias for Henkan_Mode + Key('Romaji'), # to Romaji + Key('Hiragana'), # to Hiragana + Key('Katakana'), # to Katakana + # Hiragana/Katakana toggle + Key('Hiragana_Katakana'), + Key('Zenkaku'), # to Zenkaku + Key('Hankaku'), # to Hankaku + Key('Zenkaku_Hankaku'), # Zenkaku/Hankaku toggle + Key('Touroku'), # Add to Dictionary + Key('Massyo'), # Delete from Dictionary + Key('Kana_Lock'), # Kana Lock + Key('Kana_Shift'), # Kana Shift + Key('Eisu_Shift'), # Alphanumeric Shift + Key('Eisu_toggle'), # Alphanumeric toggle + # Key('Kanji_Bangou'), # Codeinput + # Key('Zen_Koho'), # Multiple/All Candidate(s) + # Key('Mae_Koho'), # Previous Candidate + + ### Korean keyboard support + ### + ### In fact, many Korean users need only 2 keys, Key_Hangul and + ### Key_Hangul_Hanja. But rest of the keys are good for future. + + Key('Hangul'), # Hangul start/stop(toggle) + Key('Hangul_Start'), # Hangul start + Key('Hangul_End'), # Hangul end, English start + Key('Hangul_Hanja'), # Start Hangul->Hanja Conversion + Key('Hangul_Jamo'), # Hangul Jamo mode + Key('Hangul_Romaja'), # Hangul Romaja mode + # Key('Hangul_Codeinput'),# Hangul code input mode + Key('Hangul_Jeonja'), # Jeonja mode + Key('Hangul_Banja'), # Banja mode + Key('Hangul_PreHanja'), # Pre Hanja conversion + Key('Hangul_PostHanja'), # Post Hanja conversion + # Key('Hangul_SingleCandidate'), # Single candidate + # Key('Hangul_MultipleCandidate'), # Multiple candidate + # Key('Hangul_PreviousCandidate'), # Previous candidate + Key('Hangul_Special'), # Special symbols + # Key('Hangul_switch'), # Alias for mode_switch + + # dead keys (X keycode - 0xED00 to avoid the conflict) + Key('Dead_Grave'), + Key('Dead_Acute'), + Key('Dead_Circumflex'), + Key('Dead_Tilde'), + Key('Dead_Macron'), + Key('Dead_Breve'), + Key('Dead_Abovedot'), + Key('Dead_Diaeresis'), + Key('Dead_Abovering'), + Key('Dead_Doubleacute'), + Key('Dead_Caron'), + Key('Dead_Cedilla'), + Key('Dead_Ogonek'), + Key('Dead_Iota'), + Key('Dead_Voiced_Sound'), + Key('Dead_Semivoiced_Sound'), + Key('Dead_Belowdot'), + Key('Dead_Hook'), + Key('Dead_Horn'), + + # Not in Qt 5.10, so data may be wrong! + Key('Dead_Stroke'), + Key('Dead_Abovecomma'), + Key('Dead_Abovereversedcomma'), + Key('Dead_Doublegrave'), + Key('Dead_Belowring'), + Key('Dead_Belowmacron'), + Key('Dead_Belowcircumflex'), + Key('Dead_Belowtilde'), + Key('Dead_Belowbreve'), + Key('Dead_Belowdiaeresis'), + Key('Dead_Invertedbreve'), + Key('Dead_Belowcomma'), + Key('Dead_Currency'), + Key('Dead_a'), + Key('Dead_A'), + Key('Dead_e'), + Key('Dead_E'), + Key('Dead_i'), + Key('Dead_I'), + Key('Dead_o'), + Key('Dead_O'), + Key('Dead_u'), + Key('Dead_U'), + Key('Dead_Small_Schwa'), + Key('Dead_Capital_Schwa'), + Key('Dead_Greek'), + Key('Dead_Lowline'), + Key('Dead_Aboveverticalline'), + Key('Dead_Belowverticalline'), + Key('Dead_Longsolidusoverlay'), + + ### multimedia/internet keys - ignored by default - see QKeyEvent c'tor + Key('Back'), + Key('Forward'), + Key('Stop'), + Key('Refresh'), + Key('VolumeDown'), + Key('VolumeMute'), + Key('VolumeUp'), + Key('BassBoost'), + Key('BassUp'), + Key('BassDown'), + Key('TrebleUp'), + Key('TrebleDown'), + Key('MediaPlay'), + Key('MediaStop'), + Key('MediaPrevious'), + Key('MediaNext'), + Key('MediaRecord'), + Key('MediaPause'), + Key('MediaTogglePlayPause'), + Key('HomePage'), + Key('Favorites'), + Key('Search'), + Key('Standby'), + Key('OpenUrl'), + Key('LaunchMail'), + Key('LaunchMedia'), + Key('Launch0'), + Key('Launch1'), + Key('Launch2'), + Key('Launch3'), + Key('Launch4'), + Key('Launch5'), + Key('Launch6'), + Key('Launch7'), + Key('Launch8'), + Key('Launch9'), + Key('LaunchA'), + Key('LaunchB'), + Key('LaunchC'), + Key('LaunchD'), + Key('LaunchE'), + Key('LaunchF'), + Key('MonBrightnessUp'), + Key('MonBrightnessDown'), + Key('KeyboardLightOnOff'), + Key('KeyboardBrightnessUp'), + Key('KeyboardBrightnessDown'), + Key('PowerOff'), + Key('WakeUp'), + Key('Eject'), + Key('ScreenSaver'), + Key('WWW'), + Key('Memo'), + Key('LightBulb'), + Key('Shop'), + Key('History'), + Key('AddFavorite'), + Key('HotLinks'), + Key('BrightnessAdjust'), + Key('Finance'), + Key('Community'), + Key('AudioRewind'), # Media rewind + Key('BackForward'), + Key('ApplicationLeft'), + Key('ApplicationRight'), + Key('Book'), + Key('CD'), + Key('Calculator'), + Key('ToDoList'), + Key('ClearGrab'), + Key('Close'), + Key('Copy'), + Key('Cut'), + Key('Display'), # Output switch key + Key('DOS'), + Key('Documents'), + Key('Excel'), + Key('Explorer'), + Key('Game'), + Key('Go'), + Key('iTouch'), + Key('LogOff'), + Key('Market'), + Key('Meeting'), + Key('MenuKB'), + Key('MenuPB'), + Key('MySites'), + Key('News'), + Key('OfficeHome'), + Key('Option'), + Key('Paste'), + Key('Phone'), + Key('Calendar'), + Key('Reply'), + Key('Reload'), + Key('RotateWindows'), + Key('RotationPB'), + Key('RotationKB'), + Key('Save'), + Key('Send'), + Key('Spell'), + Key('SplitScreen'), + Key('Support'), + Key('TaskPane'), + Key('Terminal'), + Key('Tools'), + Key('Travel'), + Key('Video'), + Key('Word'), + Key('Xfer'), + Key('ZoomIn'), + Key('ZoomOut'), + Key('Away'), + Key('Messenger'), + Key('WebCam'), + Key('MailForward'), + Key('Pictures'), + Key('Music'), + Key('Battery'), + Key('Bluetooth'), + Key('WLAN'), + Key('UWB'), + Key('AudioForward'), # Media fast-forward + Key('AudioRepeat'), # Toggle repeat mode + Key('AudioRandomPlay'), # Toggle shuffle mode + Key('Subtitle'), + Key('AudioCycleTrack'), + Key('Time'), + Key('Hibernate'), + Key('View'), + Key('TopMenu'), + Key('PowerDown'), + Key('Suspend'), + Key('ContrastAdjust'), + + Key('LaunchG'), + Key('LaunchH'), + + Key('TouchpadToggle'), + Key('TouchpadOn'), + Key('TouchpadOff'), + + Key('MicMute'), + + Key('Red'), + Key('Green'), + Key('Yellow'), + Key('Blue'), + + Key('ChannelUp'), + Key('ChannelDown'), + + Key('Guide'), + Key('Info'), + Key('Settings'), + + Key('MicVolumeUp'), + Key('MicVolumeDown'), + + Key('New'), + Key('Open'), + Key('Find'), + Key('Undo'), + Key('Redo'), + + Key('MediaLast'), + + ### Keypad navigation keys + Key('Select'), + Key('Yes'), + Key('No'), + + ### Newer misc keys + Key('Cancel'), + Key('Printer'), + Key('Execute'), + Key('Sleep'), + Key('Play'), # Not the same as Key_MediaPlay + Key('Zoom'), + # Key('Jisho'), # IME: Dictionary key + # Key('Oyayubi_Left'), # IME: Left Oyayubi key + # Key('Oyayubi_Right'), # IME: Right Oyayubi key + Key('Exit'), + + # Device keys + Key('Context1'), + Key('Context2'), + Key('Context3'), + Key('Context4'), + Key('Call'), # set absolute state to in a call (do not toggle state) + Key('Hangup'), # set absolute state to hang up (do not toggle state) + Key('Flip'), + Key('ToggleCallHangup'), # a toggle key for answering, or hanging up, based on current call state + Key('VoiceDial'), + Key('LastNumberRedial'), + + Key('Camera'), + Key('CameraFocus'), + + Key('unknown'), +] diff --git a/tests/unit/keyinput/test_keyutils.py b/tests/unit/keyinput/test_keyutils.py index db6855d23..3081b7558 100644 --- a/tests/unit/keyinput/test_keyutils.py +++ b/tests/unit/keyinput/test_keyutils.py @@ -20,13 +20,27 @@ import pytest from PyQt5.QtCore import Qt +from tests.unit.keyinput import key_data from qutebrowser.utils import utils from qutebrowser.keyinput import keyutils -class TestKeyToString: +@pytest.fixture(params=sorted(list(key_data.KEYS.items()))) +def qt_key(request): + attr, key = request.param + member = getattr(Qt, 'Key_' + attr, None) + if member is None: + pytest.skip("Did not find key {}".format(attr)) - """Test key_to_string.""" + key.member = member + return key + + +def test_new_to_string(qt_key): + assert keyutils._key_to_string(qt_key.member) == qt_key.name + + +class TestKeyToString: @pytest.mark.parametrize('key, expected', [ (Qt.Key_Blue, 'Blue'), From 8f479407a0f96094a6ccc0ea709de1513b4d72f7 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 28 Feb 2018 09:34:27 +0100 Subject: [PATCH 092/135] key_data: Update key names to reflect reality Generated by: import key_data from PyQt5.QtCore import Qt from PyQt5.QtGui import QKeySequence for key in key_data.KEYS: attr = key.attribute member = getattr(Qt, 'Key_' + attr, None) if member is None: continue name = QKeySequence(member).toString() if name != attr: try: print(" Key('{}', '{}')".format(attr, name)) except UnicodeEncodeError: print(" Key('{}', '{}') # FIXME".format(attr, name.encode('unicode-escape').decode('ascii'))) else: print() --- tests/unit/keyinput/key_data.py | 522 ++++++++++++++++---------------- 1 file changed, 262 insertions(+), 260 deletions(-) diff --git a/tests/unit/keyinput/key_data.py b/tests/unit/keyinput/key_data.py index fb70bab72..430885ebe 100644 --- a/tests/unit/keyinput/key_data.py +++ b/tests/unit/keyinput/key_data.py @@ -34,14 +34,14 @@ class Key: # From enum Key in qt5/qtbase/src/corelib/global/qnamespace.h KEYS = [ ### misc keys - Key('Escape'), + Key('Escape', 'Esc'), Key('Tab'), Key('Backtab'), Key('Backspace'), Key('Return'), Key('Enter'), - Key('Insert'), - Key('Delete'), + Key('Insert', 'Ins'), + Key('Delete', 'Del'), Key('Pause'), Key('Print'), # print screen Key('SysReq'), @@ -53,13 +53,13 @@ KEYS = [ Key('Up'), Key('Right'), Key('Down'), - Key('PageUp'), - Key('PageDown'), + Key('PageUp', 'PgUp'), + Key('PageDown', 'PgDown'), ### modifiers - Key('Shift'), - Key('Control'), - Key('Meta'), - Key('Alt'), + Key('Shift', '\u17c0\udc20'), # FIXME + Key('Control', '\u17c0\udc21'), # FIXME + Key('Meta', '\u17c0\udc22'), # FIXME + Key('Alt', '\u17c0\udc23'), # FIXME Key('CapsLock'), Key('NumLock'), Key('ScrollLock'), @@ -101,32 +101,32 @@ KEYS = [ Key('F34'), Key('F35'), ### extra keys - Key('Super_L'), - Key('Super_R'), + Key('Super_L', '\u17c0\udc53'), # FIXME + Key('Super_R', '\u17c0\udc54'), # FIXME Key('Menu'), - Key('Hyper_L'), - Key('Hyper_R'), + Key('Hyper_L', '\u17c0\udc56'), # FIXME + Key('Hyper_R', '\u17c0\udc57'), # FIXME Key('Help'), - Key('Direction_L'), - Key('Direction_R'), + Key('Direction_L', '\u17c0\udc59'), # FIXME + Key('Direction_R', '\u17c0\udc60'), # FIXME ### 7 bit printable ASCII Key('Space'), - Key('Any'), - Key('Exclam'), - Key('QuoteDbl'), - Key('NumberSign'), - Key('Dollar'), - Key('Percent'), - Key('Ampersand'), - Key('Apostrophe'), - Key('ParenLeft'), - Key('ParenRight'), - Key('Asterisk'), - Key('Plus'), - Key('Comma'), - Key('Minus'), - Key('Period'), - Key('Slash'), + Key('Any', 'Space'), # FIXME + Key('Exclam', '!'), + Key('QuoteDbl', '"'), + Key('NumberSign', '#'), + Key('Dollar', '$'), + Key('Percent', '%'), + Key('Ampersand', '&'), + Key('Apostrophe', "'"), + Key('ParenLeft', '('), + Key('ParenRight', '),') + Key('Asterisk', '*'), + Key('Plus', '+'), + Key('Comma', ','), + Key('Minus', '-'), + Key('Period', '.'), + Key('Slash', '/'), Key('0'), Key('1'), Key('2'), @@ -137,12 +137,13 @@ KEYS = [ Key('7'), Key('8'), Key('9'), - Key('Colon'), - Key('Semicolon'), - Key('Less'), - Key('Equal'), - Key('Greater'), - Key('Question'), + Key('Colon', ':'), + Key('Semicolon', ';'), + Key('Less', '<'), + Key('Equal', '='), + Key('Greater', '>'), + Key('Question', '?'), + Key('At', '@'), Key('At'), Key('A'), Key('B'), @@ -170,95 +171,96 @@ KEYS = [ Key('X'), Key('Y'), Key('Z'), - Key('BracketLeft'), - Key('Backslash'), - Key('BracketRight'), - Key('AsciiCircum'), - Key('Underscore'), - Key('QuoteLeft'), - Key('BraceLeft'), - Key('Bar'), - Key('BraceRight'), - Key('AsciiTilde'), + Key('BracketLeft', '['), + Key('Backslash', '\\'), + Key('BracketRight', ']'), + Key('AsciiCircum', '^'), + Key('Underscore', '_'), + Key('QuoteLeft', '`'), + Key('BraceLeft', '{'), + Key('Bar', '|'), + Key('BraceRight', '}'), + Key('AsciiTilde', '~'), - Key('nobreakspace'), - Key('exclamdown'), - Key('cent'), - Key('sterling'), - Key('currency'), - Key('yen'), - Key('brokenbar'), - Key('section'), - Key('diaeresis'), - Key('copyright'), - Key('ordfeminine'), - Key('guillemotleft'), # left angle quotation mark - Key('notsign'), - Key('hyphen'), - Key('registered'), - Key('macron'), - Key('degree'), - Key('plusminus'), - Key('twosuperior'), - Key('threesuperior'), - Key('acute'), - Key('mu'), - Key('paragraph'), - Key('periodcentered'), - Key('cedilla'), - Key('onesuperior'), - Key('masculine'), - Key('guillemotright'), # right angle quotation mark - Key('onequarter'), - Key('onehalf'), - Key('threequarters'), - Key('questiondown'), - Key('Agrave'), - Key('Aacute'), - Key('Acircumflex'), - Key('Atilde'), - Key('Adiaeresis'), - Key('Aring'), - Key('AE'), - Key('Ccedilla'), - Key('Egrave'), - Key('Eacute'), - Key('Ecircumflex'), - Key('Ediaeresis'), - Key('Igrave'), - Key('Iacute'), - Key('Icircumflex'), - Key('Idiaeresis'), - Key('ETH'), - Key('Ntilde'), - Key('Ograve'), - Key('Oacute'), - Key('Ocircumflex'), - Key('Otilde'), - Key('Odiaeresis'), - Key('multiply'), - Key('Ooblique'), - Key('Ugrave'), - Key('Uacute'), - Key('Ucircumflex'), - Key('Udiaeresis'), - Key('Yacute'), - Key('THORN'), - Key('ssharp'), - Key('division'), - Key('ydiaeresis'), + Key('nobreakspace', ' '), + Key('exclamdown', '¡'), + Key('cent', '¢'), + Key('sterling', '£'), + Key('currency', '¤'), + Key('yen', '¥'), + Key('brokenbar', '¦'), + Key('section', '§'), + Key('diaeresis', '¨'), + Key('copyright', '©'), + Key('ordfeminine', 'ª'), + Key('guillemotleft', '«'), + Key('notsign', '¬'), + Key('hyphen', '­'), + Key('registered', '®'), + Key('macron', '¯'), + Key('degree', '°'), + Key('plusminus', '±'), + Key('twosuperior', '²'), + Key('threesuperior', '³'), + Key('acute', '´'), + Key('mu', 'Μ'), + Key('paragraph', '¶'), + Key('periodcentered', '·'), + Key('cedilla', '¸'), + Key('onesuperior', '¹'), + Key('masculine', 'º'), + Key('guillemotright', '»'), + Key('onequarter', '¼'), + Key('onehalf', '½'), + Key('threequarters', '¾'), + Key('questiondown', '¿'), + Key('Agrave', 'À'), + Key('Aacute', 'Á'), + Key('Acircumflex', 'Â'), + Key('Atilde', 'Ã'), + Key('Adiaeresis', 'Ä'), + Key('Aring', 'Å'), + Key('AE', 'Æ'), + Key('Ccedilla', 'Ç'), + Key('Egrave', 'È'), + Key('Eacute', 'É'), + Key('Ecircumflex', 'Ê'), + Key('Ediaeresis', 'Ë'), + Key('Igrave', 'Ì'), + Key('Iacute', 'Í'), + Key('Icircumflex', 'Î'), + Key('Idiaeresis', 'Ï'), + Key('ETH', 'Ð'), + Key('Ntilde', 'Ñ'), + Key('Ograve', 'Ò'), + Key('Oacute', 'Ó'), + Key('Ocircumflex', 'Ô'), + Key('Otilde', 'Õ'), + Key('Odiaeresis', 'Ö'), + Key('multiply', '×'), + Key('Ooblique', 'Ø'), + Key('Ugrave', 'Ù'), + Key('Uacute', 'Ú'), + Key('Ucircumflex', 'Û'), + Key('Udiaeresis', 'Ü'), + Key('Yacute', 'Ý'), + Key('THORN', 'Þ'), + Key('ssharp', 'ß'), + Key('division', '÷'), + Key('ydiaeresis', 'Ÿ'), ### International input method support (X keycode - 0xEE00, the ### definition follows Qt/Embedded 2.3.7) Only interesting if ### you are writing your own input method ### International & multi-key character composition - Key('AltGr'), - Key('Multi_key'), # Multi-key character compose - Key('Codeinput'), - Key('SingleCandidate'), - Key('MultipleCandidate'), - Key('PreviousCandidate'), + Key('AltGr', '\u17c4\udd03'), # FIXME + Key('Multi_key', '\u17c4\udd20'), # FIXME Multi-key character compose + Key('Codeinput', 'Code input'), + Key('SingleCandidate', '\u17c4\udd3c'), # FIXME + Key('MultipleCandidate', 'Multiple Candidate'), + Key('PreviousCandidate', 'Previous Candidate'), + Key('Mode_switch', '\u17c4\udd7e'), # FIXME ### Misc Functions Key('Mode_switch'), # Character set switch @@ -272,17 +274,16 @@ KEYS = [ Key('Romaji'), # to Romaji Key('Hiragana'), # to Hiragana Key('Katakana'), # to Katakana - # Hiragana/Katakana toggle - Key('Hiragana_Katakana'), + Key('Hiragana_Katakana', 'Hiragana Katakana'), # Hiragana/Katakana toggle Key('Zenkaku'), # to Zenkaku Key('Hankaku'), # to Hankaku - Key('Zenkaku_Hankaku'), # Zenkaku/Hankaku toggle + Key('Zenkaku_Hankaku', 'Zenkaku Hankaku'), # Zenkaku/Hankaku toggle Key('Touroku'), # Add to Dictionary Key('Massyo'), # Delete from Dictionary - Key('Kana_Lock'), # Kana Lock - Key('Kana_Shift'), # Kana Shift - Key('Eisu_Shift'), # Alphanumeric Shift - Key('Eisu_toggle'), # Alphanumeric toggle + Key('Kana_Lock', 'Kana Lock'), + Key('Kana_Shift', 'Kana Shift'), + Key('Eisu_Shift', 'Eisu Shift'), # Alphanumeric Shift + Key('Eisu_toggle', 'Eisu toggle'), # Alphanumeric toggle # Key('Kanji_Bangou'), # Codeinput # Key('Zen_Koho'), # Multiple/All Candidate(s) # Key('Mae_Koho'), # Previous Candidate @@ -292,43 +293,43 @@ KEYS = [ ### In fact, many Korean users need only 2 keys, Key_Hangul and ### Key_Hangul_Hanja. But rest of the keys are good for future. - Key('Hangul'), # Hangul start/stop(toggle) - Key('Hangul_Start'), # Hangul start - Key('Hangul_End'), # Hangul end, English start - Key('Hangul_Hanja'), # Start Hangul->Hanja Conversion - Key('Hangul_Jamo'), # Hangul Jamo mode - Key('Hangul_Romaja'), # Hangul Romaja mode - # Key('Hangul_Codeinput'),# Hangul code input mode - Key('Hangul_Jeonja'), # Jeonja mode - Key('Hangul_Banja'), # Banja mode - Key('Hangul_PreHanja'), # Pre Hanja conversion - Key('Hangul_PostHanja'), # Post Hanja conversion - # Key('Hangul_SingleCandidate'), # Single candidate - # Key('Hangul_MultipleCandidate'), # Multiple candidate - # Key('Hangul_PreviousCandidate'), # Previous candidate - Key('Hangul_Special'), # Special symbols - # Key('Hangul_switch'), # Alias for mode_switch + Key('Hangul'), # Hangul start/stop(toggle), + Key('Hangul_Start', 'Hangul Start'), # Hangul start + Key('Hangul_End', 'Hangul End'), # Hangul end, English start + Key('Hangul_Hanja', 'Hangul Hanja'), # Start Hangul->Hanja Conversion + Key('Hangul_Jamo', 'Hangul Jamo'), # Hangul Jamo mode + Key('Hangul_Romaja', 'Hangul Romaja'), # Hangul Romaja mode + # Key('Hangul_Codeinput', 'Hangul Codeinput'),# Hangul code input mode + Key('Hangul_Jeonja', 'Hangul Jeonja'), # Jeonja mode + Key('Hangul_Banja', 'Hangul Banja'), # Banja mode + Key('Hangul_PreHanja', 'Hangul PreHanja'), # Pre Hanja conversion + Key('Hangul_PostHanja', 'Hangul PostHanja'), # Post Hanja conversion + # Key('Hangul_SingleCandidate', 'Hangul SingleCandidate'), # Single candidate + # Key('Hangul_MultipleCandidate', 'Hangul MultipleCandidate'), # Multiple candidate + # Key('Hangul_PreviousCandidate', 'Hangul PreviousCandidate'), # Previous candidate + Key('Hangul_Special', 'Hangul Special'), # Special symbols + # Key('Hangul_switch', 'Hangul switch'), # Alias for mode_switch - # dead keys (X keycode - 0xED00 to avoid the conflict) - Key('Dead_Grave'), - Key('Dead_Acute'), - Key('Dead_Circumflex'), - Key('Dead_Tilde'), - Key('Dead_Macron'), - Key('Dead_Breve'), - Key('Dead_Abovedot'), - Key('Dead_Diaeresis'), - Key('Dead_Abovering'), - Key('Dead_Doubleacute'), - Key('Dead_Caron'), - Key('Dead_Cedilla'), - Key('Dead_Ogonek'), - Key('Dead_Iota'), - Key('Dead_Voiced_Sound'), - Key('Dead_Semivoiced_Sound'), - Key('Dead_Belowdot'), - Key('Dead_Hook'), - Key('Dead_Horn'), + # dead keys (X keycode - 0xED00 to avoid the conflict), + Key('Dead_Grave', '\u17c4\ude50'), # FIXME + Key('Dead_Acute', '\u17c4\ude51'), # FIXME + Key('Dead_Circumflex', '\u17c4\ude52'), # FIXME + Key('Dead_Tilde', '\u17c4\ude53'), # FIXME + Key('Dead_Macron', '\u17c4\ude54'), # FIXME + Key('Dead_Breve', '\u17c4\ude55'), # FIXME + Key('Dead_Abovedot', '\u17c4\ude56'), # FIXME + Key('Dead_Diaeresis', '\u17c4\ude57'), # FIXME + Key('Dead_Abovering', '\u17c4\ude58'), # FIXME + Key('Dead_Doubleacute', '\u17c4\ude59'), # FIXME + Key('Dead_Caron', '\u17c4\ude5a'), # FIXME + Key('Dead_Cedilla', '\u17c4\ude5b'), # FIXME + Key('Dead_Ogonek', '\u17c4\ude5c'), # FIXME + Key('Dead_Iota', '\u17c4\ude5d'), # FIXME + Key('Dead_Voiced_Sound', '\u17c4\ude5e'), # FIXME + Key('Dead_Semivoiced_Sound', '\u17c4\ude5f'), # FIXME + Key('Dead_Belowdot', '\u17c4\ude60'), # FIXME + Key('Dead_Hook', '\u17c4\ude61'), # FIXME + Key('Dead_Horn', '\u17c4\ude62'), # FIXME # Not in Qt 5.10, so data may be wrong! Key('Dead_Stroke'), @@ -367,160 +368,161 @@ KEYS = [ Key('Forward'), Key('Stop'), Key('Refresh'), - Key('VolumeDown'), - Key('VolumeMute'), - Key('VolumeUp'), - Key('BassBoost'), - Key('BassUp'), - Key('BassDown'), - Key('TrebleUp'), - Key('TrebleDown'), - Key('MediaPlay'), - Key('MediaStop'), - Key('MediaPrevious'), - Key('MediaNext'), - Key('MediaRecord'), - Key('MediaPause'), - Key('MediaTogglePlayPause'), - Key('HomePage'), + Key('VolumeDown', 'Volume Down'), + Key('VolumeMute', 'Volume Mute'), + Key('VolumeUp', 'Volume Up'), + Key('BassBoost', 'Bass Boost'), + Key('BassUp', 'Bass Up'), + Key('BassDown', 'Bass Down'), + Key('TrebleUp', 'Treble Up'), + Key('TrebleDown', 'Treble Down'), + Key('MediaPlay', 'Media Play'), + Key('MediaStop', 'Media Stop'), + Key('MediaPrevious', 'Media Previous'), + Key('MediaNext', 'Media Next'), + Key('MediaRecord', 'Media Record'), + Key('MediaPause', 'Media Pause'), + Key('MediaTogglePlayPause', 'Toggle Media Play/Pause'), + Key('HomePage', 'Home Page'), Key('Favorites'), Key('Search'), Key('Standby'), - Key('OpenUrl'), - Key('LaunchMail'), - Key('LaunchMedia'), - Key('Launch0'), - Key('Launch1'), - Key('Launch2'), - Key('Launch3'), - Key('Launch4'), - Key('Launch5'), - Key('Launch6'), - Key('Launch7'), - Key('Launch8'), - Key('Launch9'), - Key('LaunchA'), - Key('LaunchB'), - Key('LaunchC'), - Key('LaunchD'), - Key('LaunchE'), - Key('LaunchF'), - Key('MonBrightnessUp'), - Key('MonBrightnessDown'), - Key('KeyboardLightOnOff'), - Key('KeyboardBrightnessUp'), - Key('KeyboardBrightnessDown'), - Key('PowerOff'), - Key('WakeUp'), + + Key('OpenUrl', 'Open URL'), + Key('LaunchMail', 'Launch Mail'), + Key('LaunchMedia', 'Launch Media'), + Key('Launch0', 'Launch (0),') + Key('Launch1', 'Launch (1),') + Key('Launch2', 'Launch (2),') + Key('Launch3', 'Launch (3),') + Key('Launch4', 'Launch (4),') + Key('Launch5', 'Launch (5),') + Key('Launch6', 'Launch (6),') + Key('Launch7', 'Launch (7),') + Key('Launch8', 'Launch (8),') + Key('Launch9', 'Launch (9),') + Key('LaunchA', 'Launch (A),') + Key('LaunchB', 'Launch (B),') + Key('LaunchC', 'Launch (C),') + Key('LaunchD', 'Launch (D),') + Key('LaunchE', 'Launch (E),') + Key('LaunchF', 'Launch (F),') + Key('MonBrightnessUp', 'Monitor Brightness Up'), + Key('MonBrightnessDown', 'Monitor Brightness Down'), + Key('KeyboardLightOnOff', 'Keyboard Light On/Off'), + Key('KeyboardBrightnessUp', 'Keyboard Brightness Up'), + Key('KeyboardBrightnessDown', 'Keyboard Brightness Down'), + Key('PowerOff', 'Power Off'), + Key('WakeUp', 'Wake Up'), Key('Eject'), - Key('ScreenSaver'), + Key('ScreenSaver', 'Screensaver'), Key('WWW'), - Key('Memo'), + Key('Memo', '\u17c0\udcbc'), # FIXME Key('LightBulb'), Key('Shop'), Key('History'), - Key('AddFavorite'), - Key('HotLinks'), - Key('BrightnessAdjust'), + Key('AddFavorite', 'Add Favorite'), + Key('HotLinks', 'Hot Links'), + Key('BrightnessAdjust', 'Adjust Brightness'), Key('Finance'), Key('Community'), - Key('AudioRewind'), # Media rewind - Key('BackForward'), - Key('ApplicationLeft'), - Key('ApplicationRight'), + Key('AudioRewind', 'Media Rewind'), + Key('BackForward', 'Back Forward'), + Key('ApplicationLeft', 'Application Left'), + Key('ApplicationRight', 'Application Right'), Key('Book'), Key('CD'), Key('Calculator'), - Key('ToDoList'), - Key('ClearGrab'), + Key('ToDoList', '\u17c0\udccc'), # FIXME + Key('ClearGrab', 'Clear Grab'), Key('Close'), Key('Copy'), Key('Cut'), Key('Display'), # Output switch key Key('DOS'), Key('Documents'), - Key('Excel'), - Key('Explorer'), + Key('Excel', 'Spreadsheet'), + Key('Explorer', 'Browser'), Key('Game'), Key('Go'), Key('iTouch'), - Key('LogOff'), + Key('LogOff', 'Logoff'), Key('Market'), Key('Meeting'), - Key('MenuKB'), - Key('MenuPB'), - Key('MySites'), + Key('MenuKB', 'Keyboard Menu'), + Key('MenuPB', 'Menu PB'), + Key('MySites', 'My Sites'), Key('News'), - Key('OfficeHome'), + Key('OfficeHome', 'Home Office'), Key('Option'), Key('Paste'), Key('Phone'), - Key('Calendar'), + Key('Calendar', '\u17c0\udce4'), # FIXME Key('Reply'), Key('Reload'), - Key('RotateWindows'), - Key('RotationPB'), - Key('RotationKB'), + Key('RotateWindows', 'Rotate Windows'), + Key('RotationPB', 'Rotation PB'), + Key('RotationKB', 'Rotation KB'), Key('Save'), Key('Send'), - Key('Spell'), - Key('SplitScreen'), + Key('Spell', 'Spellchecker'), + Key('SplitScreen', 'Split Screen'), Key('Support'), - Key('TaskPane'), + Key('TaskPane', 'Task Panel'), Key('Terminal'), Key('Tools'), Key('Travel'), Key('Video'), - Key('Word'), - Key('Xfer'), - Key('ZoomIn'), - Key('ZoomOut'), + Key('Word', 'Word Processor'), + Key('Xfer', 'XFer'), + Key('ZoomIn', 'Zoom In'), + Key('ZoomOut', 'Zoom Out'), Key('Away'), Key('Messenger'), Key('WebCam'), - Key('MailForward'), + Key('MailForward', 'Mail Forward'), Key('Pictures'), Key('Music'), Key('Battery'), Key('Bluetooth'), - Key('WLAN'), - Key('UWB'), - Key('AudioForward'), # Media fast-forward - Key('AudioRepeat'), # Toggle repeat mode - Key('AudioRandomPlay'), # Toggle shuffle mode + Key('WLAN', 'Wireless'), + Key('UWB', 'Ultra Wide Band'), + Key('AudioForward', 'Media Fast Forward'), + Key('AudioRepeat', 'Audio Repeat'), # Toggle repeat mode + Key('AudioRandomPlay', 'Audio Random Play'), # Toggle shuffle mode Key('Subtitle'), - Key('AudioCycleTrack'), + Key('AudioCycleTrack', 'Audio Cycle Track'), Key('Time'), Key('Hibernate'), Key('View'), - Key('TopMenu'), - Key('PowerDown'), + Key('TopMenu', 'Top Menu'), + Key('PowerDown', 'Power Down'), Key('Suspend'), - Key('ContrastAdjust'), + Key('ContrastAdjust', '\u17c0\udd0d'), # FIXME - Key('LaunchG'), - Key('LaunchH'), + Key('LaunchG', '\u17c0\udd0e'), # FIXME + Key('LaunchH', '\u17c0\udd0f'), # FIXME - Key('TouchpadToggle'), - Key('TouchpadOn'), - Key('TouchpadOff'), + Key('TouchpadToggle', 'Touchpad Toggle'), + Key('TouchpadOn', 'Touchpad On'), + Key('TouchpadOff', 'Touchpad Off'), - Key('MicMute'), + Key('MicMute', 'Microphone Mute'), Key('Red'), Key('Green'), Key('Yellow'), Key('Blue'), - Key('ChannelUp'), - Key('ChannelDown'), + Key('ChannelUp', 'Channel Up'), + Key('ChannelDown', 'Channel Down'), Key('Guide'), Key('Info'), Key('Settings'), - Key('MicVolumeUp'), - Key('MicVolumeDown'), + Key('MicVolumeUp', 'Microphone Volume Up'), + Key('MicVolumeDown', 'Microphone Volume Down'), Key('New'), Key('Open'), @@ -528,7 +530,7 @@ KEYS = [ Key('Undo'), Key('Redo'), - Key('MediaLast'), + Key('MediaLast', '\u17ff\udfff'), # FIXME ### Keypad navigation keys Key('Select'), @@ -555,12 +557,12 @@ KEYS = [ Key('Call'), # set absolute state to in a call (do not toggle state) Key('Hangup'), # set absolute state to hang up (do not toggle state) Key('Flip'), - Key('ToggleCallHangup'), # a toggle key for answering, or hanging up, based on current call state - Key('VoiceDial'), - Key('LastNumberRedial'), + Key('ToggleCallHangup', 'Toggle Call/Hangup'), # a toggle key for answering, or hanging up, based on current call state + Key('VoiceDial', 'Voice Dial'), + Key('LastNumberRedial', 'Last Number Redial'), - Key('Camera'), - Key('CameraFocus'), + Key('Camera', 'Camera Shutter'), + Key('CameraFocus', 'Camera Focus'), - Key('unknown'), + Key('unknown', ''), # FIXME ] From 601e56d2fa341b9cdb5f9a459fc14b5839c7876d Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 28 Feb 2018 09:51:19 +0100 Subject: [PATCH 093/135] Make test_keyutils work --- qutebrowser/keyinput/keyutils.py | 1 + tests/unit/keyinput/key_data.py | 38 +++++++++++++--------------- tests/unit/keyinput/test_keyutils.py | 14 +++++----- 3 files changed, 27 insertions(+), 26 deletions(-) diff --git a/qutebrowser/keyinput/keyutils.py b/qutebrowser/keyinput/keyutils.py index 2cf151348..c3c5baf40 100644 --- a/qutebrowser/keyinput/keyutils.py +++ b/qutebrowser/keyinput/keyutils.py @@ -39,6 +39,7 @@ def _key_to_string(key): Return: A name of the key as a string. """ + return QKeySequence(key).toString() # FIXME special_names_str = { # Some keys handled in a weird way by QKeySequence::toString. # See https://bugreports.qt.io/browse/QTBUG-40030 diff --git a/tests/unit/keyinput/key_data.py b/tests/unit/keyinput/key_data.py index 430885ebe..071fc8f43 100644 --- a/tests/unit/keyinput/key_data.py +++ b/tests/unit/keyinput/key_data.py @@ -120,7 +120,7 @@ KEYS = [ Key('Ampersand', '&'), Key('Apostrophe', "'"), Key('ParenLeft', '('), - Key('ParenRight', '),') + Key('ParenRight', ')'), Key('Asterisk', '*'), Key('Plus', '+'), Key('Comma', ','), @@ -144,7 +144,6 @@ KEYS = [ Key('Greater', '>'), Key('Question', '?'), Key('At', '@'), - Key('At'), Key('A'), Key('B'), Key('C'), @@ -260,10 +259,9 @@ KEYS = [ Key('SingleCandidate', '\u17c4\udd3c'), # FIXME Key('MultipleCandidate', 'Multiple Candidate'), Key('PreviousCandidate', 'Previous Candidate'), - Key('Mode_switch', '\u17c4\udd7e'), # FIXME ### Misc Functions - Key('Mode_switch'), # Character set switch + Key('Mode_switch', '\u17c4\udd7e'), # FIXME Character set switch # Key('script_switch'), # Alias for mode_switch ### Japanese keyboard support @@ -391,22 +389,22 @@ KEYS = [ Key('OpenUrl', 'Open URL'), Key('LaunchMail', 'Launch Mail'), Key('LaunchMedia', 'Launch Media'), - Key('Launch0', 'Launch (0),') - Key('Launch1', 'Launch (1),') - Key('Launch2', 'Launch (2),') - Key('Launch3', 'Launch (3),') - Key('Launch4', 'Launch (4),') - Key('Launch5', 'Launch (5),') - Key('Launch6', 'Launch (6),') - Key('Launch7', 'Launch (7),') - Key('Launch8', 'Launch (8),') - Key('Launch9', 'Launch (9),') - Key('LaunchA', 'Launch (A),') - Key('LaunchB', 'Launch (B),') - Key('LaunchC', 'Launch (C),') - Key('LaunchD', 'Launch (D),') - Key('LaunchE', 'Launch (E),') - Key('LaunchF', 'Launch (F),') + Key('Launch0', 'Launch (0)'), + Key('Launch1', 'Launch (1)'), + Key('Launch2', 'Launch (2)'), + Key('Launch3', 'Launch (3)'), + Key('Launch4', 'Launch (4)'), + Key('Launch5', 'Launch (5)'), + Key('Launch6', 'Launch (6)'), + Key('Launch7', 'Launch (7)'), + Key('Launch8', 'Launch (8)'), + Key('Launch9', 'Launch (9)'), + Key('LaunchA', 'Launch (A)'), + Key('LaunchB', 'Launch (B)'), + Key('LaunchC', 'Launch (C)'), + Key('LaunchD', 'Launch (D)'), + Key('LaunchE', 'Launch (E)'), + Key('LaunchF', 'Launch (F)'), Key('MonBrightnessUp', 'Monitor Brightness Up'), Key('MonBrightnessDown', 'Monitor Brightness Down'), Key('KeyboardLightOnOff', 'Keyboard Light On/Off'), diff --git a/tests/unit/keyinput/test_keyutils.py b/tests/unit/keyinput/test_keyutils.py index 3081b7558..e5b6268e5 100644 --- a/tests/unit/keyinput/test_keyutils.py +++ b/tests/unit/keyinput/test_keyutils.py @@ -25,19 +25,19 @@ from qutebrowser.utils import utils from qutebrowser.keyinput import keyutils -@pytest.fixture(params=sorted(list(key_data.KEYS.items()))) +@pytest.fixture(params=key_data.KEYS) def qt_key(request): - attr, key = request.param - member = getattr(Qt, 'Key_' + attr, None) + key = request.param + member = getattr(Qt, 'Key_' + key.attribute, None) if member is None: - pytest.skip("Did not find key {}".format(attr)) - + pytest.skip("Did not find key {}".format(key.attribute)) key.member = member return key def test_new_to_string(qt_key): - assert keyutils._key_to_string(qt_key.member) == qt_key.name + name = qt_key.attribute if qt_key.name is None else qt_key.name + assert keyutils._key_to_string(qt_key.member) == name class TestKeyToString: @@ -50,6 +50,7 @@ class TestKeyToString: (Qt.Key_degree, '°'), (Qt.Key_Meta, 'Meta'), ]) + @pytest.mark.skipif(True, reason='FIXME') def test_normal(self, key, expected): """Test a special key where QKeyEvent::toString works incorrectly.""" assert keyutils._key_to_string(key) == expected @@ -61,6 +62,7 @@ class TestKeyToString: # want to know if the mapping still behaves properly. assert keyutils._key_to_string(Qt.Key_A) == 'A' + @pytest.mark.skipif(True, reason='FIXME') def test_all(self): """Make sure there's some sensible output for all keys.""" for name, value in sorted(vars(Qt).items()): From 0b6d2c2b0a0b1101e4f84c024e4420c27fd2aee9 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 28 Feb 2018 10:21:02 +0100 Subject: [PATCH 094/135] Make all key names work --- qutebrowser/keyinput/keyutils.py | 127 ++++++++++++--------------- tests/unit/keyinput/key_data.py | 88 +++++++++---------- tests/unit/keyinput/test_keyutils.py | 37 +++----- 3 files changed, 110 insertions(+), 142 deletions(-) diff --git a/qutebrowser/keyinput/keyutils.py b/qutebrowser/keyinput/keyutils.py index c3c5baf40..5280c8a9b 100644 --- a/qutebrowser/keyinput/keyutils.py +++ b/qutebrowser/keyinput/keyutils.py @@ -39,69 +39,63 @@ def _key_to_string(key): Return: A name of the key as a string. """ - return QKeySequence(key).toString() # FIXME 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. - 'Key_Blue': 'Blue', - 'Key_Calendar': 'Calendar', - 'Key_ChannelDown': 'Channel Down', - 'Key_ChannelUp': 'Channel Up', - 'Key_ContrastAdjust': 'Contrast Adjust', - 'Key_Dead_Abovedot': '˙', - 'Key_Dead_Abovering': '˚', - 'Key_Dead_Acute': '´', - 'Key_Dead_Belowdot': 'Belowdot', - 'Key_Dead_Breve': '˘', - 'Key_Dead_Caron': 'ˇ', - 'Key_Dead_Cedilla': '¸', - 'Key_Dead_Circumflex': '^', - 'Key_Dead_Diaeresis': '¨', - 'Key_Dead_Doubleacute': '˝', - 'Key_Dead_Grave': '`', - 'Key_Dead_Hook': 'Hook', - 'Key_Dead_Horn': 'Horn', - 'Key_Dead_Iota': 'Iota', - 'Key_Dead_Macron': '¯', - 'Key_Dead_Ogonek': '˛', - 'Key_Dead_Semivoiced_Sound': 'Semivoiced Sound', - 'Key_Dead_Tilde': '~', - 'Key_Dead_Voiced_Sound': 'Voiced Sound', - 'Key_Exit': 'Exit', - 'Key_Green': 'Green', - 'Key_Guide': 'Guide', - 'Key_Info': 'Info', - 'Key_LaunchG': 'LaunchG', - 'Key_LaunchH': 'LaunchH', - 'Key_MediaLast': 'MediaLast', - 'Key_Memo': 'Memo', - 'Key_MicMute': 'Mic Mute', - 'Key_Mode_switch': 'Mode switch', - 'Key_Multi_key': 'Multi key', - 'Key_PowerDown': 'Power Down', - 'Key_Red': 'Red', - 'Key_Settings': 'Settings', - 'Key_SingleCandidate': 'Single Candidate', - 'Key_ToDoList': 'Todo List', - 'Key_TouchpadOff': 'Touchpad Off', - 'Key_TouchpadOn': 'Touchpad On', - 'Key_TouchpadToggle': 'Touchpad toggle', - 'Key_Yellow': 'Yellow', - 'Key_Alt': 'Alt', - 'Key_AltGr': 'AltGr', - 'Key_Control': 'Control', - 'Key_Direction_L': 'Direction L', - 'Key_Direction_R': 'Direction R', - 'Key_Hyper_L': 'Hyper L', - 'Key_Hyper_R': 'Hyper R', - 'Key_Meta': 'Meta', - 'Key_Shift': 'Shift', - 'Key_Super_L': 'Super L', - 'Key_Super_R': 'Super R', - 'Key_unknown': 'Unknown', + + '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 + 'Backtab': 'Tab', + '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 @@ -109,23 +103,14 @@ def _key_to_string(key): special_names = {} for k, v in special_names_str.items(): try: - special_names[getattr(Qt, k)] = v + special_names[getattr(Qt, 'Key_' + k)] = v except AttributeError: pass - # Now we check if the key is any special one - if not, we use - # QKeySequence::toString. - try: + + if key in special_names: return special_names[key] - except KeyError: - name = QKeySequence(key).toString() - morphings = { - 'Backtab': 'Tab', - 'Esc': 'Escape', - } - if name in morphings: - return morphings[name] - else: - return name + + return QKeySequence(key).toString() class KeyParseError(Exception): diff --git a/tests/unit/keyinput/key_data.py b/tests/unit/keyinput/key_data.py index 071fc8f43..2e9dda46a 100644 --- a/tests/unit/keyinput/key_data.py +++ b/tests/unit/keyinput/key_data.py @@ -34,9 +34,9 @@ class Key: # From enum Key in qt5/qtbase/src/corelib/global/qnamespace.h KEYS = [ ### misc keys - Key('Escape', 'Esc'), + Key('Escape'), # qutebrowser has a different name from Qt Key('Tab'), - Key('Backtab'), + Key('Backtab', 'Tab'), # qutebrowser has a different name from Qt Key('Backspace'), Key('Return'), Key('Enter'), @@ -56,10 +56,10 @@ KEYS = [ Key('PageUp', 'PgUp'), Key('PageDown', 'PgDown'), ### modifiers - Key('Shift', '\u17c0\udc20'), # FIXME - Key('Control', '\u17c0\udc21'), # FIXME - Key('Meta', '\u17c0\udc22'), # FIXME - Key('Alt', '\u17c0\udc23'), # FIXME + Key('Shift'), + Key('Control'), + Key('Meta'), + Key('Alt'), Key('CapsLock'), Key('NumLock'), Key('ScrollLock'), @@ -101,17 +101,17 @@ KEYS = [ Key('F34'), Key('F35'), ### extra keys - Key('Super_L', '\u17c0\udc53'), # FIXME - Key('Super_R', '\u17c0\udc54'), # FIXME + Key('Super_L', 'Super L'), + Key('Super_R', 'Super R'), Key('Menu'), - Key('Hyper_L', '\u17c0\udc56'), # FIXME - Key('Hyper_R', '\u17c0\udc57'), # FIXME + Key('Hyper_L', 'Hyper L'), + Key('Hyper_R', 'Hyper R'), Key('Help'), - Key('Direction_L', '\u17c0\udc59'), # FIXME - Key('Direction_R', '\u17c0\udc60'), # FIXME + Key('Direction_L', 'Direction L'), + Key('Direction_R', 'Direction R'), ### 7 bit printable ASCII Key('Space'), - Key('Any', 'Space'), # FIXME + Key('Any', 'Space'), # Same value Key('Exclam', '!'), Key('QuoteDbl', '"'), Key('NumberSign', '#'), @@ -253,15 +253,15 @@ KEYS = [ ### you are writing your own input method ### International & multi-key character composition - Key('AltGr', '\u17c4\udd03'), # FIXME - Key('Multi_key', '\u17c4\udd20'), # FIXME Multi-key character compose + Key('AltGr'), + Key('Multi_key', 'Multi key'), # Multi-key character compose Key('Codeinput', 'Code input'), - Key('SingleCandidate', '\u17c4\udd3c'), # FIXME + Key('SingleCandidate', 'Single Candidate'), Key('MultipleCandidate', 'Multiple Candidate'), Key('PreviousCandidate', 'Previous Candidate'), ### Misc Functions - Key('Mode_switch', '\u17c4\udd7e'), # FIXME Character set switch + Key('Mode_switch', 'Mode switch'), # Character set switch # Key('script_switch'), # Alias for mode_switch ### Japanese keyboard support @@ -309,25 +309,25 @@ KEYS = [ # Key('Hangul_switch', 'Hangul switch'), # Alias for mode_switch # dead keys (X keycode - 0xED00 to avoid the conflict), - Key('Dead_Grave', '\u17c4\ude50'), # FIXME - Key('Dead_Acute', '\u17c4\ude51'), # FIXME - Key('Dead_Circumflex', '\u17c4\ude52'), # FIXME - Key('Dead_Tilde', '\u17c4\ude53'), # FIXME - Key('Dead_Macron', '\u17c4\ude54'), # FIXME - Key('Dead_Breve', '\u17c4\ude55'), # FIXME - Key('Dead_Abovedot', '\u17c4\ude56'), # FIXME - Key('Dead_Diaeresis', '\u17c4\ude57'), # FIXME - Key('Dead_Abovering', '\u17c4\ude58'), # FIXME - Key('Dead_Doubleacute', '\u17c4\ude59'), # FIXME - Key('Dead_Caron', '\u17c4\ude5a'), # FIXME - Key('Dead_Cedilla', '\u17c4\ude5b'), # FIXME - Key('Dead_Ogonek', '\u17c4\ude5c'), # FIXME - Key('Dead_Iota', '\u17c4\ude5d'), # FIXME - Key('Dead_Voiced_Sound', '\u17c4\ude5e'), # FIXME - Key('Dead_Semivoiced_Sound', '\u17c4\ude5f'), # FIXME - Key('Dead_Belowdot', '\u17c4\ude60'), # FIXME - Key('Dead_Hook', '\u17c4\ude61'), # FIXME - Key('Dead_Horn', '\u17c4\ude62'), # FIXME + Key('Dead_Grave', '`'), + Key('Dead_Acute', '´'), + Key('Dead_Circumflex', '^'), + Key('Dead_Tilde', '~'), + Key('Dead_Macron', '¯'), + Key('Dead_Breve', '˘'), + Key('Dead_Abovedot', '˙'), + Key('Dead_Diaeresis', '¨'), + Key('Dead_Abovering', '˚'), + Key('Dead_Doubleacute', '˝'), + Key('Dead_Caron', 'ˇ'), + Key('Dead_Cedilla', '¸'), + Key('Dead_Ogonek', '˛'), + Key('Dead_Iota', 'Iota'), + Key('Dead_Voiced_Sound', 'Voiced Sound'), + Key('Dead_Semivoiced_Sound', 'Semivoiced Sound'), + Key('Dead_Belowdot', 'Belowdot'), + Key('Dead_Hook', 'Hook'), + Key('Dead_Horn', 'Horn'), # Not in Qt 5.10, so data may be wrong! Key('Dead_Stroke'), @@ -415,7 +415,7 @@ KEYS = [ Key('Eject'), Key('ScreenSaver', 'Screensaver'), Key('WWW'), - Key('Memo', '\u17c0\udcbc'), # FIXME + Key('Memo', 'Memo'), Key('LightBulb'), Key('Shop'), Key('History'), @@ -431,7 +431,7 @@ KEYS = [ Key('Book'), Key('CD'), Key('Calculator'), - Key('ToDoList', '\u17c0\udccc'), # FIXME + Key('ToDoList', 'To Do List'), Key('ClearGrab', 'Clear Grab'), Key('Close'), Key('Copy'), @@ -455,7 +455,7 @@ KEYS = [ Key('Option'), Key('Paste'), Key('Phone'), - Key('Calendar', '\u17c0\udce4'), # FIXME + Key('Calendar'), Key('Reply'), Key('Reload'), Key('RotateWindows', 'Rotate Windows'), @@ -496,10 +496,10 @@ KEYS = [ Key('TopMenu', 'Top Menu'), Key('PowerDown', 'Power Down'), Key('Suspend'), - Key('ContrastAdjust', '\u17c0\udd0d'), # FIXME + Key('ContrastAdjust', 'Contrast Adjust'), - Key('LaunchG', '\u17c0\udd0e'), # FIXME - Key('LaunchH', '\u17c0\udd0f'), # FIXME + Key('LaunchG', 'Launch (G)'), + Key('LaunchH', 'Launch (H)'), Key('TouchpadToggle', 'Touchpad Toggle'), Key('TouchpadOn', 'Touchpad On'), @@ -528,7 +528,7 @@ KEYS = [ Key('Undo'), Key('Redo'), - Key('MediaLast', '\u17ff\udfff'), # FIXME + Key('MediaLast', 'Media Last'), ### Keypad navigation keys Key('Select'), @@ -562,5 +562,5 @@ KEYS = [ Key('Camera', 'Camera Shutter'), Key('CameraFocus', 'Camera Focus'), - Key('unknown', ''), # FIXME + Key('unknown', 'Unknown'), ] diff --git a/tests/unit/keyinput/test_keyutils.py b/tests/unit/keyinput/test_keyutils.py index e5b6268e5..1e8bdfb74 100644 --- a/tests/unit/keyinput/test_keyutils.py +++ b/tests/unit/keyinput/test_keyutils.py @@ -35,43 +35,26 @@ def qt_key(request): return key -def test_new_to_string(qt_key): - name = qt_key.attribute if qt_key.name is None else qt_key.name - assert keyutils._key_to_string(qt_key.member) == name - - class TestKeyToString: - @pytest.mark.parametrize('key, expected', [ - (Qt.Key_Blue, 'Blue'), - (Qt.Key_Backtab, 'Tab'), - (Qt.Key_Escape, 'Escape'), - (Qt.Key_A, 'A'), - (Qt.Key_degree, '°'), - (Qt.Key_Meta, 'Meta'), - ]) - @pytest.mark.skipif(True, reason='FIXME') - def test_normal(self, key, expected): - """Test a special key where QKeyEvent::toString works incorrectly.""" - assert keyutils._key_to_string(key) == expected + def test_to_string(self, qt_key): + name = qt_key.attribute if qt_key.name is None else qt_key.name + assert keyutils._key_to_string(qt_key.member) == name def test_missing(self, monkeypatch): - """Test with a missing key.""" monkeypatch.delattr(keyutils.Qt, 'Key_Blue') # We don't want to test the key which is actually missing - we only # want to know if the mapping still behaves properly. assert keyutils._key_to_string(Qt.Key_A) == 'A' - @pytest.mark.skipif(True, reason='FIXME') def test_all(self): - """Make sure there's some sensible output for all keys.""" - for name, value in sorted(vars(Qt).items()): - if not isinstance(value, Qt.Key): - continue - print(name) - string = keyutils._key_to_string(value) - assert string - string.encode('utf-8') # make sure it's encodable + """Make sure all possible keys are in key_data.KEYS.""" + key_names = {name[len("Key_"):] + for name, value in sorted(vars(Qt).items()) + if isinstance(value, Qt.Key)} + key_data_names = {key.attribute for key in sorted(key_data.KEYS)} + diff = key_names - key_data_names + assert not diff class TestKeyEventToString: From 8c87040cb655239e83c3e706daf402f21a2c4eb2 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 28 Feb 2018 10:41:09 +0100 Subject: [PATCH 095/135] Improve IDs for qt_key fixture --- tests/unit/keyinput/test_keyutils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/keyinput/test_keyutils.py b/tests/unit/keyinput/test_keyutils.py index 1e8bdfb74..23354935c 100644 --- a/tests/unit/keyinput/test_keyutils.py +++ b/tests/unit/keyinput/test_keyutils.py @@ -25,7 +25,7 @@ from qutebrowser.utils import utils from qutebrowser.keyinput import keyutils -@pytest.fixture(params=key_data.KEYS) +@pytest.fixture(params=key_data.KEYS, ids=lambda k: k.attribute) def qt_key(request): key = request.param member = getattr(Qt, 'Key_' + key.attribute, None) From 2ca15d7667de0ef519deff16e0e24adcd6937738 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 28 Feb 2018 10:59:01 +0100 Subject: [PATCH 096/135] Add tests for lower-/uppercase text --- tests/unit/keyinput/key_data.py | 273 ++++++++++++++------------- tests/unit/keyinput/test_keyutils.py | 8 + 2 files changed, 145 insertions(+), 136 deletions(-) diff --git a/tests/unit/keyinput/key_data.py b/tests/unit/keyinput/key_data.py index 2e9dda46a..d9111d9aa 100644 --- a/tests/unit/keyinput/key_data.py +++ b/tests/unit/keyinput/key_data.py @@ -28,6 +28,7 @@ class Key: attribute = attr.ib() name = attr.ib(None) # default: name == attribute text = attr.ib('') + uppertext = attr.ib('') member = attr.ib(None) @@ -110,143 +111,143 @@ KEYS = [ Key('Direction_L', 'Direction L'), Key('Direction_R', 'Direction R'), ### 7 bit printable ASCII - Key('Space'), - Key('Any', 'Space'), # Same value - Key('Exclam', '!'), - Key('QuoteDbl', '"'), - Key('NumberSign', '#'), - Key('Dollar', '$'), - Key('Percent', '%'), - Key('Ampersand', '&'), - Key('Apostrophe', "'"), - Key('ParenLeft', '('), - Key('ParenRight', ')'), - Key('Asterisk', '*'), - Key('Plus', '+'), - Key('Comma', ','), - Key('Minus', '-'), - Key('Period', '.'), - Key('Slash', '/'), - Key('0'), - Key('1'), - Key('2'), - Key('3'), - Key('4'), - Key('5'), - Key('6'), - Key('7'), - Key('8'), - Key('9'), - Key('Colon', ':'), - Key('Semicolon', ';'), - Key('Less', '<'), - Key('Equal', '='), - Key('Greater', '>'), - Key('Question', '?'), - Key('At', '@'), - Key('A'), - Key('B'), - Key('C'), - Key('D'), - Key('E'), - Key('F'), - Key('G'), - Key('H'), - Key('I'), - Key('J'), - Key('K'), - Key('L'), - Key('M'), - Key('N'), - Key('O'), - Key('P'), - Key('Q'), - Key('R'), - Key('S'), - Key('T'), - Key('U'), - Key('V'), - Key('W'), - Key('X'), - Key('Y'), - Key('Z'), - Key('BracketLeft', '['), - Key('Backslash', '\\'), - Key('BracketRight', ']'), - Key('AsciiCircum', '^'), - Key('Underscore', '_'), - Key('QuoteLeft', '`'), - Key('BraceLeft', '{'), - Key('Bar', '|'), - Key('BraceRight', '}'), - Key('AsciiTilde', '~'), + Key('Space', text=' ', uppertext=' '), + Key('Any', 'Space', text=' ', uppertext=' '), # Same value + Key('Exclam', '!', text='!', uppertext='!'), + Key('QuoteDbl', '"', text='"', uppertext='"'), + Key('NumberSign', '#', text='#', uppertext='#'), + Key('Dollar', '$', text='$', uppertext='$'), + Key('Percent', '%', text='%', uppertext='%'), + Key('Ampersand', '&', text='&', uppertext='&'), + Key('Apostrophe', "'", text="'", uppertext="'"), + Key('ParenLeft', '(', text='(', uppertext='('), + Key('ParenRight', ')', text=')', uppertext=')'), + Key('Asterisk', '*', text='*', uppertext='*'), + Key('Plus', '+', text='+', uppertext='+'), + Key('Comma', ',', text=',', uppertext=','), + Key('Minus', '-', text='-', uppertext='-'), + Key('Period', '.', text='.', uppertext='.'), + Key('Slash', '/', text='/', uppertext='/'), + Key('0', text='0', uppertext='0'), + Key('1', text='1', uppertext='1'), + Key('2', text='2', uppertext='2'), + Key('3', text='3', uppertext='3'), + Key('4', text='4', uppertext='4'), + Key('5', text='5', uppertext='5'), + Key('6', text='6', uppertext='6'), + Key('7', text='7', uppertext='7'), + Key('8', text='8', uppertext='8'), + Key('9', text='9', uppertext='9'), + Key('Colon', ':', text=':', uppertext=':'), + Key('Semicolon', ';', text=';', uppertext=';'), + Key('Less', '<', text='<', uppertext='<'), + Key('Equal', '=', text='=', uppertext='='), + Key('Greater', '>', text='>', uppertext='>'), + Key('Question', '?', text='?', uppertext='?'), + Key('At', '@', text='@', uppertext='@'), + Key('A', text='a', uppertext='A'), + Key('B', text='b', uppertext='B'), + Key('C', text='c', uppertext='C'), + Key('D', text='d', uppertext='D'), + Key('E', text='e', uppertext='E'), + Key('F', text='f', uppertext='F'), + Key('G', text='g', uppertext='G'), + Key('H', text='h', uppertext='H'), + Key('I', text='i', uppertext='I'), + Key('J', text='j', uppertext='J'), + Key('K', text='k', uppertext='K'), + Key('L', text='l', uppertext='L'), + Key('M', text='m', uppertext='M'), + Key('N', text='n', uppertext='N'), + Key('O', text='o', uppertext='O'), + Key('P', text='p', uppertext='P'), + Key('Q', text='q', uppertext='Q'), + Key('R', text='r', uppertext='R'), + Key('S', text='s', uppertext='S'), + Key('T', text='t', uppertext='T'), + Key('U', text='u', uppertext='U'), + Key('V', text='v', uppertext='V'), + Key('W', text='w', uppertext='W'), + Key('X', text='x', uppertext='X'), + Key('Y', text='y', uppertext='Y'), + Key('Z', text='z', uppertext='Z'), + Key('BracketLeft', '[', text='[', uppertext='['), + Key('Backslash', '\\', text='\\', uppertext='\\'), + Key('BracketRight', ']', text=']', uppertext=']'), + Key('AsciiCircum', '^', text='^', uppertext='^'), + Key('Underscore', '_', text='_', uppertext='_'), + Key('QuoteLeft', '`', text='`', uppertext='`'), + Key('BraceLeft', '{', text='{', uppertext='{'), + Key('Bar', '|', text='|', uppertext='|'), + Key('BraceRight', '}', text='}', uppertext='}'), + Key('AsciiTilde', '~', text='~', uppertext='~'), - Key('nobreakspace', ' '), - Key('exclamdown', '¡'), - Key('cent', '¢'), - Key('sterling', '£'), - Key('currency', '¤'), - Key('yen', '¥'), - Key('brokenbar', '¦'), - Key('section', '§'), - Key('diaeresis', '¨'), - Key('copyright', '©'), - Key('ordfeminine', 'ª'), - Key('guillemotleft', '«'), - Key('notsign', '¬'), - Key('hyphen', '­'), - Key('registered', '®'), - Key('macron', '¯'), - Key('degree', '°'), - Key('plusminus', '±'), - Key('twosuperior', '²'), - Key('threesuperior', '³'), - Key('acute', '´'), - Key('mu', 'Μ'), - Key('paragraph', '¶'), - Key('periodcentered', '·'), - Key('cedilla', '¸'), - Key('onesuperior', '¹'), - Key('masculine', 'º'), - Key('guillemotright', '»'), - Key('onequarter', '¼'), - Key('onehalf', '½'), - Key('threequarters', '¾'), - Key('questiondown', '¿'), - Key('Agrave', 'À'), - Key('Aacute', 'Á'), - Key('Acircumflex', 'Â'), - Key('Atilde', 'Ã'), - Key('Adiaeresis', 'Ä'), - Key('Aring', 'Å'), - Key('AE', 'Æ'), - Key('Ccedilla', 'Ç'), - Key('Egrave', 'È'), - Key('Eacute', 'É'), - Key('Ecircumflex', 'Ê'), - Key('Ediaeresis', 'Ë'), - Key('Igrave', 'Ì'), - Key('Iacute', 'Í'), - Key('Icircumflex', 'Î'), - Key('Idiaeresis', 'Ï'), - Key('ETH', 'Ð'), - Key('Ntilde', 'Ñ'), - Key('Ograve', 'Ò'), - Key('Oacute', 'Ó'), - Key('Ocircumflex', 'Ô'), - Key('Otilde', 'Õ'), - Key('Odiaeresis', 'Ö'), - Key('multiply', '×'), - Key('Ooblique', 'Ø'), - Key('Ugrave', 'Ù'), - Key('Uacute', 'Ú'), - Key('Ucircumflex', 'Û'), - Key('Udiaeresis', 'Ü'), - Key('Yacute', 'Ý'), - Key('THORN', 'Þ'), - Key('ssharp', 'ß'), - Key('division', '÷'), - Key('ydiaeresis', 'Ÿ'), + Key('nobreakspace', ' ', text=' ', uppertext=' '), + Key('exclamdown', '¡', text='¡', uppertext='¡'), + Key('cent', '¢', text='¢', uppertext='¢'), + Key('sterling', '£', text='£', uppertext='£'), + Key('currency', '¤', text='¤', uppertext='¤'), + Key('yen', '¥', text='¥', uppertext='¥'), + Key('brokenbar', '¦', text='¦', uppertext='¦'), + Key('section', '§', text='§', uppertext='§'), + Key('diaeresis', '¨', text='¨', uppertext='¨'), + Key('copyright', '©', text='©', uppertext='©'), + Key('ordfeminine', 'ª', text='ª', uppertext='ª'), + Key('guillemotleft', '«', text='«', uppertext='«'), + Key('notsign', '¬', text='¬', uppertext='¬'), + Key('hyphen', '­', text='­', uppertext='­'), + Key('registered', '®', text='®', uppertext='®'), + Key('macron', '¯', text='¯', uppertext='¯'), + Key('degree', '°', text='°', uppertext='°'), + Key('plusminus', '±', text='±', uppertext='±'), + Key('twosuperior', '²', text='²', uppertext='²'), + Key('threesuperior', '³', text='³', uppertext='³'), + Key('acute', '´', text='´', uppertext='´'), + Key('mu', 'Μ', text='μ', uppertext='Μ'), + Key('paragraph', '¶', text='¶', uppertext='¶'), + Key('periodcentered', '·', text='·', uppertext='·'), + Key('cedilla', '¸', text='¸', uppertext='¸'), + Key('onesuperior', '¹', text='¹', uppertext='¹'), + Key('masculine', 'º', text='º', uppertext='º'), + Key('guillemotright', '»', text='»', uppertext='»'), + Key('onequarter', '¼', text='¼', uppertext='¼'), + Key('onehalf', '½', text='½', uppertext='½'), + Key('threequarters', '¾', text='¾', uppertext='¾'), + Key('questiondown', '¿', text='¿', uppertext='¿'), + Key('Agrave', 'À', text='à', uppertext='À'), + Key('Aacute', 'Á', text='á', uppertext='Á'), + Key('Acircumflex', 'Â', text='â', uppertext='Â'), + Key('Atilde', 'Ã', text='ã', uppertext='Ã'), + Key('Adiaeresis', 'Ä', text='ä', uppertext='Ä'), + Key('Aring', 'Å', text='å', uppertext='Å'), + Key('AE', 'Æ', text='æ', uppertext='Æ'), + Key('Ccedilla', 'Ç', text='ç', uppertext='Ç'), + Key('Egrave', 'È', text='è', uppertext='È'), + Key('Eacute', 'É', text='é', uppertext='É'), + Key('Ecircumflex', 'Ê', text='ê', uppertext='Ê'), + Key('Ediaeresis', 'Ë', text='ë', uppertext='Ë'), + Key('Igrave', 'Ì', text='ì', uppertext='Ì'), + Key('Iacute', 'Í', text='í', uppertext='Í'), + Key('Icircumflex', 'Î', text='î', uppertext='Î'), + Key('Idiaeresis', 'Ï', text='ï', uppertext='Ï'), + Key('ETH', 'Ð', text='ð', uppertext='Ð'), + Key('Ntilde', 'Ñ', text='ñ', uppertext='Ñ'), + Key('Ograve', 'Ò', text='ò', uppertext='Ò'), + Key('Oacute', 'Ó', text='ó', uppertext='Ó'), + Key('Ocircumflex', 'Ô', text='ô', uppertext='Ô'), + Key('Otilde', 'Õ', text='õ', uppertext='Õ'), + Key('Odiaeresis', 'Ö', text='ö', uppertext='Ö'), + Key('multiply', '×', text='×', uppertext='×'), + Key('Ooblique', 'Ø', text='ø', uppertext='Ø'), + Key('Ugrave', 'Ù', text='ù', uppertext='Ù'), + Key('Uacute', 'Ú', text='ú', uppertext='Ú'), + Key('Ucircumflex', 'Û', text='û', uppertext='Û'), + Key('Udiaeresis', 'Ü', text='ü', uppertext='Ü'), + Key('Yacute', 'Ý', text='ý', uppertext='Ý'), + Key('THORN', 'Þ', text='þ', uppertext='Þ'), + Key('ssharp', 'ß', text='ß', uppertext='ß'), + Key('division', '÷', text='÷', uppertext='÷'), + Key('ydiaeresis', 'Ÿ', text='ÿ', uppertext='Ÿ'), ### International input method support (X keycode - 0xEE00, the ### definition follows Qt/Embedded 2.3.7) Only interesting if diff --git a/tests/unit/keyinput/test_keyutils.py b/tests/unit/keyinput/test_keyutils.py index 23354935c..b714cf77b 100644 --- a/tests/unit/keyinput/test_keyutils.py +++ b/tests/unit/keyinput/test_keyutils.py @@ -35,6 +35,14 @@ def qt_key(request): return key +@pytest.mark.parametrize('upper', [False, True]) +def test_key_text(qt_key, upper): + modifiers = Qt.ShiftModifier if upper else Qt.KeyboardModifiers() + info = keyutils.KeyInfo(qt_key.member, modifiers=modifiers) + expected = qt_key.uppertext if upper else qt_key.text + assert info.text() == expected + + class TestKeyToString: def test_to_string(self, qt_key): From b4d232badd7394875b78f61d31450fdeaee8d33b Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 28 Feb 2018 11:15:44 +0100 Subject: [PATCH 097/135] Simplify KeyInfo.text() --- qutebrowser/keyinput/keyutils.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/qutebrowser/keyinput/keyutils.py b/qutebrowser/keyinput/keyutils.py index 5280c8a9b..7f0e669a6 100644 --- a/qutebrowser/keyinput/keyutils.py +++ b/qutebrowser/keyinput/keyutils.py @@ -252,13 +252,13 @@ class KeyInfo: def text(self): """Get the text which would be displayed when pressing this key.""" - text = QKeySequence(self.key).toString() if self.key == Qt.Key_Space: return ' ' - elif len(text) > 1: - # Special key? + elif self.key > 0xff: + # Unprintable keys return '' + text = QKeySequence(self.key).toString() if not self.modifiers & Qt.ShiftModifier: text = text.lower() return text From 1cd86d79d9e499c0fbbe6262c293c1d78e34ee0e Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 28 Feb 2018 11:33:19 +0100 Subject: [PATCH 098/135] Add keyutils.is_printable() --- qutebrowser/keyinput/keyutils.py | 11 +++++++---- qutebrowser/keyinput/modeparsers.py | 7 +++---- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/qutebrowser/keyinput/keyutils.py b/qutebrowser/keyinput/keyutils.py index 7f0e669a6..1bd9ee737 100644 --- a/qutebrowser/keyinput/keyutils.py +++ b/qutebrowser/keyinput/keyutils.py @@ -30,6 +30,10 @@ from PyQt5.QtGui import QKeySequence, QKeyEvent from qutebrowser.utils import utils +def is_printable(key): + return key <= 0xff + + def _key_to_string(key): """Convert a Qt::Key member to a meaningful name. @@ -227,7 +231,7 @@ class KeyInfo: key_string = _key_to_string(self.key) - if len(key_string) == 1: + if is_printable(self.key) and self.key != Qt.Key_Space: category = unicodedata.category(key_string) is_special_char = (category == 'Cc') else: @@ -254,8 +258,7 @@ class KeyInfo: """Get the text which would be displayed when pressing this key.""" if self.key == Qt.Key_Space: return ' ' - elif self.key > 0xff: - # Unprintable keys + elif not is_printable(self.key): return '' text = QKeySequence(self.key).toString() @@ -386,7 +389,7 @@ class KeySequence: modifiers = ev.modifiers() if (modifiers == Qt.ShiftModifier and - len(ev.text()) == 1 and + is_printable(ev.key()) and unicodedata.category(ev.text()) != 'Lu'): modifiers = Qt.KeyboardModifiers() diff --git a/qutebrowser/keyinput/modeparsers.py b/qutebrowser/keyinput/modeparsers.py index 42eeb53f8..d56d7dcd1 100644 --- a/qutebrowser/keyinput/modeparsers.py +++ b/qutebrowser/keyinput/modeparsers.py @@ -280,13 +280,12 @@ class RegisterKeyParser(keyparser.CommandKeyParser): if match: return match - key = e.text() - - if key == '' or not str(keyutils.KeyInfo.from_event(e)): + if not keyutils.is_printable(e.key()): # this is not a proper register key, let it pass and keep going - # FIXME can we simplify this when we refactor keyutils.py? return QKeySequence.NoMatch + key = e.text() + tabbed_browser = objreg.get('tabbed-browser', scope='window', window=self._win_id) macro_recorder = objreg.get('macro-recorder') From e26eaaddc2c2de006c9bc9108cac512f9f2df65d Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 28 Feb 2018 12:52:19 +0100 Subject: [PATCH 099/135] Add keyutils.is_modifier_key() --- qutebrowser/keyinput/basekeyparser.py | 14 ++------------ qutebrowser/keyinput/keyutils.py | 16 ++++++++++------ 2 files changed, 12 insertions(+), 18 deletions(-) diff --git a/qutebrowser/keyinput/basekeyparser.py b/qutebrowser/keyinput/basekeyparser.py index 27f671dcd..2c934617b 100644 --- a/qutebrowser/keyinput/basekeyparser.py +++ b/qutebrowser/keyinput/basekeyparser.py @@ -128,20 +128,10 @@ class BaseKeyParser(QObject): txt = str(keyutils.KeyInfo.from_event(e)) self._debug_log("Got key: 0x{:x} / text: '{}'".format(key, txt)) - if not txt: - self._debug_log("Ignoring, no text char") + if keyutils.is_modifier_key(key): + self._debug_log("Ignoring, only modifier") return QKeySequence.NoMatch - # 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 QKeySequence.NoMatch - if (txt.isdigit() and self._supports_count and not (not self._count and txt == '0')): assert len(txt) == 1, txt diff --git a/qutebrowser/keyinput/keyutils.py b/qutebrowser/keyinput/keyutils.py index 1bd9ee737..b1d1cec45 100644 --- a/qutebrowser/keyinput/keyutils.py +++ b/qutebrowser/keyinput/keyutils.py @@ -34,6 +34,14 @@ def is_printable(key): return key <= 0xff +def is_modifier_key(key): + # FIXME docs + return key in (Qt.Key_Control, Qt.Key_Alt, Qt.Key_Shift, Qt.Key_Meta, + Qt.Key_AltGr, Qt.Key_Super_L, Qt.Key_Super_R, + Qt.Key_Hyper_L, Qt.Key_Hyper_R, Qt.Key_Direction_L, + Qt.Key_Direction_R) + + def _key_to_string(key): """Convert a Qt::Key member to a meaningful name. @@ -216,13 +224,9 @@ class KeyInfo: (Qt.ShiftModifier, 'Shift'), ]) - modifier_keys = (Qt.Key_Control, Qt.Key_Alt, Qt.Key_Shift, Qt.Key_Meta, - Qt.Key_AltGr, Qt.Key_Super_L, Qt.Key_Super_R, - Qt.Key_Hyper_L, Qt.Key_Hyper_R, Qt.Key_Direction_L, - Qt.Key_Direction_R) - if self.key in modifier_keys: - # Only modifier pressed + if is_modifier_key(self.key): return '' + parts = [] for (mask, s) in modmask2str.items(): From 63e05e12bad22f27a7182759a0fdfdcbd1c4435f Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 28 Feb 2018 12:57:25 +0100 Subject: [PATCH 100/135] Fix lint and tests --- tests/unit/keyinput/key_data.py | 20 +++++++++++++++++--- tests/unit/keyinput/test_basekeyparser.py | 11 +++-------- tests/unit/keyinput/test_keyutils.py | 4 ++-- 3 files changed, 22 insertions(+), 13 deletions(-) diff --git a/tests/unit/keyinput/key_data.py b/tests/unit/keyinput/key_data.py index d9111d9aa..f967389b1 100644 --- a/tests/unit/keyinput/key_data.py +++ b/tests/unit/keyinput/key_data.py @@ -17,14 +17,28 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see . -import attr +# pylint: disable=line-too-long -from PyQt5.QtCore import Qt + +"""Data used by test_keyutils.py to test all keys.""" + + +import attr @attr.s class Key: + """A key with expected values. + + Attributes: + attribute: The name of the Qt::Key attribute ('Foo' -> Qt.Key_Foo) + name: The name returned by str(KeyInfo) with that key. + text: The text returned by KeyInfo.text(). + uppertext: The text returned by KeyInfo.text() with shift. + member: Filled by the test fixture, the numeric value. + """ + attribute = attr.ib() name = attr.ib(None) # default: name == attribute text = attr.ib('') @@ -289,7 +303,7 @@ KEYS = [ ### Korean keyboard support ### - ### In fact, many Korean users need only 2 keys, Key_Hangul and + ### In fact, many users from Korea need only 2 keys, Key_Hangul and ### Key_Hangul_Hanja. But rest of the keys are good for future. Key('Hangul'), # Hangul start/stop(toggle), diff --git a/tests/unit/keyinput/test_basekeyparser.py b/tests/unit/keyinput/test_basekeyparser.py index a32009390..5da4efa9b 100644 --- a/tests/unit/keyinput/test_basekeyparser.py +++ b/tests/unit/keyinput/test_basekeyparser.py @@ -165,15 +165,10 @@ class TestSpecialKeys: Qt.Key_A, (Qt.ControlModifier | Qt.AltModifier))) assert not keyparser.execute.called - @pytest.mark.skip(reason='unneeded?') - def test_keychain(self, fake_keyevent_factory, keyparser): - keyparser.handle(fake_keyevent_factory(Qt.Key_B)) - keyparser.handle(fake_keyevent_factory(Qt.Key_A)) - assert not keyparser.execute.called - - def test_no_binding(self, monkeypatch, fake_keyevent_factory, keyparser): + def test_only_modifiers(self, monkeypatch, fake_keyevent_factory, + keyparser): monkeypatch.setattr(keyutils.KeyInfo, '__str__', lambda _self: '') - keyparser.handle(fake_keyevent_factory(Qt.Key_A, Qt.NoModifier)) + keyparser.handle(fake_keyevent_factory(Qt.Key_Shift, Qt.NoModifier)) assert not keyparser.execute.called def test_mapping(self, config_stub, fake_keyevent_factory, keyparser): diff --git a/tests/unit/keyinput/test_keyutils.py b/tests/unit/keyinput/test_keyutils.py index b714cf77b..94b18eb57 100644 --- a/tests/unit/keyinput/test_keyutils.py +++ b/tests/unit/keyinput/test_keyutils.py @@ -118,8 +118,8 @@ class TestKeyEventToString: ('xyz', keyutils.KeySequence(Qt.Key_X, Qt.Key_Y, Qt.Key_Z)), ('', keyutils.KeySequence(Qt.ControlModifier | Qt.Key_X, Qt.MetaModifier | Qt.Key_Y)), - ('', keyutils.KeyParseError), - ('\U00010000', keyutils.KeyParseError), + ('', keyutils.KeyParseError), + ('\U00010000', keyutils.KeyParseError), ]) def test_parse(keystr, expected): if expected is keyutils.KeyParseError: From 0aa17bfa332bd563ea7cabefbc69aef8e5688e9a Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 28 Feb 2018 13:56:49 +0100 Subject: [PATCH 101/135] Simplify unicodedata.category calls --- qutebrowser/keyinput/keyutils.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/qutebrowser/keyinput/keyutils.py b/qutebrowser/keyinput/keyutils.py index b1d1cec45..dc04fcd7f 100644 --- a/qutebrowser/keyinput/keyutils.py +++ b/qutebrowser/keyinput/keyutils.py @@ -236,8 +236,7 @@ class KeyInfo: key_string = _key_to_string(self.key) if is_printable(self.key) and self.key != Qt.Key_Space: - category = unicodedata.category(key_string) - is_special_char = (category == 'Cc') + is_special_char = unicodedata.category(key_string) == 'Cc' else: is_special_char = False @@ -394,7 +393,7 @@ class KeySequence: if (modifiers == Qt.ShiftModifier and is_printable(ev.key()) and - unicodedata.category(ev.text()) != 'Lu'): + not ev.text().isupper()): modifiers = Qt.KeyboardModifiers() keys = list(self._iter_keys()) From 4e505d52dfe7e06d340dcd6b0585b279af9d9244 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 28 Feb 2018 15:28:04 +0100 Subject: [PATCH 102/135] Regenerate docs --- doc/help/commands.asciidoc | 5 ++++- doc/help/settings.asciidoc | 15 +++++++++------ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/doc/help/commands.asciidoc b/doc/help/commands.asciidoc index 58dfaaa16..2174e69ac 100644 --- a/doc/help/commands.asciidoc +++ b/doc/help/commands.asciidoc @@ -1497,13 +1497,16 @@ Drop selection and keep selection mode enabled. [[follow-hint]] === follow-hint -Syntax: +:follow-hint ['keystring']+ +Syntax: +:follow-hint [*--select*] ['keystring']+ Follow a hint. ==== positional arguments * +'keystring'+: The hint to follow. +==== optional arguments +* +*-s*+, +*--select*+: Only select the given hint, don't necessarily follow it. + [[leave-mode]] === leave-mode Leave the mode we're currently in. diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc index a9e7becd2..fc809b98e 100644 --- a/doc/help/settings.asciidoc +++ b/doc/help/settings.asciidoc @@ -358,11 +358,8 @@ The following modes are available: * prompt: Entered when there's a prompt to display, like for download locations or when invoked from JavaScript. - + - You can bind normal keys in this mode, but they will be only active when - a yes/no-prompt is asked. For other prompt modes, you can only bind - special keys. +* yesno: Entered when there's a yes/no prompt displayed. * caret: Entered when pressing the `v` mode, used to select text using the keyboard. @@ -642,11 +639,17 @@ Default: * +pass:[<Shift-Tab>]+: +pass:[prompt-item-focus prev]+ * +pass:[<Tab>]+: +pass:[prompt-item-focus next]+ * +pass:[<Up>]+: +pass:[prompt-item-focus prev]+ -* +pass:[n]+: +pass:[prompt-accept no]+ -* +pass:[y]+: +pass:[prompt-accept yes]+ - +pass:[register]+: * +pass:[<Escape>]+: +pass:[leave-mode]+ +- +pass:[yesno]+: + +* +pass:[<Alt-Shift-Y>]+: +pass:[prompt-yank --sel]+ +* +pass:[<Alt-Y>]+: +pass:[prompt-yank]+ +* +pass:[<Escape>]+: +pass:[leave-mode]+ +* +pass:[<Return>]+: +pass:[prompt-accept]+ +* +pass:[n]+: +pass:[prompt-accept no]+ +* +pass:[y]+: +pass:[prompt-accept yes]+ [[bindings.key_mappings]] === bindings.key_mappings From 65a05f334e008caa567b07fc80ec48ccd51be7b0 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 28 Feb 2018 20:33:23 +0100 Subject: [PATCH 103/135] Fix KeyInfo.__str__ for --- qutebrowser/keyinput/keyutils.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/qutebrowser/keyinput/keyutils.py b/qutebrowser/keyinput/keyutils.py index dc04fcd7f..b9dc47fa4 100644 --- a/qutebrowser/keyinput/keyutils.py +++ b/qutebrowser/keyinput/keyutils.py @@ -19,7 +19,6 @@ """Our own QKeySequence-like class and related utilities.""" -import unicodedata import collections import itertools @@ -235,12 +234,9 @@ class KeyInfo: key_string = _key_to_string(self.key) - if is_printable(self.key) and self.key != Qt.Key_Space: - is_special_char = unicodedata.category(key_string) == 'Cc' - else: - is_special_char = False - - if not is_special_char: + if is_printable(self.key): + # FIXME Add a test to make sure Tab doesn;t become TAB + assert len(key_string) == 1 or self.key == Qt.Key_Space, key_string if self.modifiers == Qt.ShiftModifier: parts = [] key_string = key_string.upper() From 934d5862861e7cb9765647cc47445c2a0e5ad3d0 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 28 Feb 2018 20:46:52 +0100 Subject: [PATCH 104/135] Fix handling of Shift-Tab aka. Backtab --- qutebrowser/keyinput/keyutils.py | 10 ++++++++-- tests/unit/keyinput/key_data.py | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/qutebrowser/keyinput/keyutils.py b/qutebrowser/keyinput/keyutils.py index b9dc47fa4..1feb371a8 100644 --- a/qutebrowser/keyinput/keyutils.py +++ b/qutebrowser/keyinput/keyutils.py @@ -105,7 +105,6 @@ def _key_to_string(key): 'unknown': 'Unknown', # For some keys, we just want a different name - 'Backtab': 'Tab', 'Escape': 'Escape', } # We now build our real special_names dict from the string mapping above. @@ -384,16 +383,23 @@ class KeySequence: In addition, Shift also *is* relevant when other modifiers are involved. Shift-Ctrl-X should not be equivalent to Ctrl-X. + + We also change Qt.Key_Backtab to Key_Tab here because nobody would + configure "Shift-Backtab" in their config. """ + key = ev.key() modifiers = ev.modifiers() + if modifiers & Qt.ShiftModifier and key == Qt.Key_Backtab: + key = Qt.Key_Tab + if (modifiers == Qt.ShiftModifier and is_printable(ev.key()) and not ev.text().isupper()): modifiers = Qt.KeyboardModifiers() keys = list(self._iter_keys()) - keys.append(ev.key() | int(modifiers)) + keys.append(key | int(modifiers)) return self.__class__(*keys) diff --git a/tests/unit/keyinput/key_data.py b/tests/unit/keyinput/key_data.py index f967389b1..73a978fd2 100644 --- a/tests/unit/keyinput/key_data.py +++ b/tests/unit/keyinput/key_data.py @@ -51,7 +51,7 @@ KEYS = [ ### misc keys Key('Escape'), # qutebrowser has a different name from Qt Key('Tab'), - Key('Backtab', 'Tab'), # qutebrowser has a different name from Qt + Key('Backtab'), Key('Backspace'), Key('Return'), Key('Enter'), From d28c323074ffcfbdf011399fe1d24f59c1522ca1 Mon Sep 17 00:00:00 2001 From: Fritz Reichwald Date: Thu, 1 Mar 2018 00:34:33 +0100 Subject: [PATCH 105/135] Add printable and ismodifier test --- tests/unit/keyinput/test_keyutils.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/unit/keyinput/test_keyutils.py b/tests/unit/keyinput/test_keyutils.py index 94b18eb57..df93d268c 100644 --- a/tests/unit/keyinput/test_keyutils.py +++ b/tests/unit/keyinput/test_keyutils.py @@ -142,3 +142,19 @@ def test_parse(keystr, expected): def test_normalize_keystr(orig, normalized): expected = keyutils.KeySequence.parse(normalized) assert keyutils.KeySequence.parse(orig) == expected + + +@pytest.mark.parametrize('key, printable', [ + (Qt.Key_Control, False), + (Qt.Key_X, True) +]) +def test_is_printable(key, printable): + assert keyutils.is_printable(key) == printable + + +@pytest.mark.parametrize('key, ismodifier', [ + (Qt.Key_Control, True), + (Qt.Key_X, False) +]) +def test_is_modifier_key(key, ismodifier): + assert keyutils.is_modifier_key(key) == ismodifier From 4223e2f85d9d753c6c330ce742ffc494b8681b44 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 1 Mar 2018 08:01:23 +0100 Subject: [PATCH 106/135] Check all keys against QTest::keyToAscii --- tests/unit/keyinput/key_data.py | 487 ++++++++++++++------------- tests/unit/keyinput/test_keyutils.py | 30 +- 2 files changed, 273 insertions(+), 244 deletions(-) diff --git a/tests/unit/keyinput/key_data.py b/tests/unit/keyinput/key_data.py index 73a978fd2..cc5c249b5 100644 --- a/tests/unit/keyinput/key_data.py +++ b/tests/unit/keyinput/key_data.py @@ -44,17 +44,18 @@ class Key: text = attr.ib('') uppertext = attr.ib('') member = attr.ib(None) + qtest = attr.ib(True) # From enum Key in qt5/qtbase/src/corelib/global/qnamespace.h KEYS = [ ### misc keys - Key('Escape'), # qutebrowser has a different name from Qt - Key('Tab'), - Key('Backtab'), - Key('Backspace'), - Key('Return'), - Key('Enter'), + Key('Escape', text='\x1b', uppertext='\x1b'), + Key('Tab', text='\t', uppertext='\t'), + Key('Backtab', qtest=False), # Qt assumes VT (vertical tab) + Key('Backspace', text='\b', uppertext='\b'), + Key('Return', text='\r', uppertext='\r'), + Key('Enter', text='\r', uppertext='\r'), Key('Insert', 'Ins'), Key('Delete', 'Del'), Key('Pause'), @@ -217,7 +218,7 @@ KEYS = [ Key('twosuperior', '²', text='²', uppertext='²'), Key('threesuperior', '³', text='³', uppertext='³'), Key('acute', '´', text='´', uppertext='´'), - Key('mu', 'Μ', text='μ', uppertext='Μ'), + Key('mu', 'Μ', text='μ', uppertext='Μ', qtest=False), # Qt assumes U+00B5 instead of U+03BC Key('paragraph', '¶', text='¶', uppertext='¶'), Key('periodcentered', '·', text='·', uppertext='·'), Key('cedilla', '¸', text='¸', uppertext='¸'), @@ -268,113 +269,113 @@ KEYS = [ ### you are writing your own input method ### International & multi-key character composition - Key('AltGr'), - Key('Multi_key', 'Multi key'), # Multi-key character compose - Key('Codeinput', 'Code input'), - Key('SingleCandidate', 'Single Candidate'), - Key('MultipleCandidate', 'Multiple Candidate'), - Key('PreviousCandidate', 'Previous Candidate'), + Key('AltGr', qtest=False), + Key('Multi_key', 'Multi key', qtest=False), # Multi-key character compose + Key('Codeinput', 'Code input', qtest=False), + Key('SingleCandidate', 'Single Candidate', qtest=False), + Key('MultipleCandidate', 'Multiple Candidate', qtest=False), + Key('PreviousCandidate', 'Previous Candidate', qtest=False), ### Misc Functions - Key('Mode_switch', 'Mode switch'), # Character set switch + Key('Mode_switch', 'Mode switch', qtest=False), # Character set switch # Key('script_switch'), # Alias for mode_switch ### Japanese keyboard support - Key('Kanji'), # Kanji, Kanji convert - Key('Muhenkan'), # Cancel Conversion - # Key('Henkan_Mode'), # Start/Stop Conversion - Key('Henkan'), # Alias for Henkan_Mode - Key('Romaji'), # to Romaji - Key('Hiragana'), # to Hiragana - Key('Katakana'), # to Katakana - Key('Hiragana_Katakana', 'Hiragana Katakana'), # Hiragana/Katakana toggle - Key('Zenkaku'), # to Zenkaku - Key('Hankaku'), # to Hankaku - Key('Zenkaku_Hankaku', 'Zenkaku Hankaku'), # Zenkaku/Hankaku toggle - Key('Touroku'), # Add to Dictionary - Key('Massyo'), # Delete from Dictionary - Key('Kana_Lock', 'Kana Lock'), - Key('Kana_Shift', 'Kana Shift'), - Key('Eisu_Shift', 'Eisu Shift'), # Alphanumeric Shift - Key('Eisu_toggle', 'Eisu toggle'), # Alphanumeric toggle - # Key('Kanji_Bangou'), # Codeinput - # Key('Zen_Koho'), # Multiple/All Candidate(s) - # Key('Mae_Koho'), # Previous Candidate + Key('Kanji', qtest=False), # Kanji, Kanji convert + Key('Muhenkan', qtest=False), # Cancel Conversion + # Key('Henkan_Mode', qtest=False), # Start/Stop Conversion + Key('Henkan', qtest=False), # Alias for Henkan_Mode + Key('Romaji', qtest=False), # to Romaji + Key('Hiragana', qtest=False), # to Hiragana + Key('Katakana', qtest=False), # to Katakana + Key('Hiragana_Katakana', 'Hiragana Katakana', qtest=False), # Hiragana/Katakana toggle + Key('Zenkaku', qtest=False), # to Zenkaku + Key('Hankaku', qtest=False), # to Hankaku + Key('Zenkaku_Hankaku', 'Zenkaku Hankaku', qtest=False), # Zenkaku/Hankaku toggle + Key('Touroku', qtest=False), # Add to Dictionary + Key('Massyo', qtest=False), # Delete from Dictionary + Key('Kana_Lock', 'Kana Lock', qtest=False), + Key('Kana_Shift', 'Kana Shift', qtest=False), + Key('Eisu_Shift', 'Eisu Shift', qtest=False), # Alphanumeric Shift + Key('Eisu_toggle', 'Eisu toggle', qtest=False), # Alphanumeric toggle + # Key('Kanji_Bangou', qtest=False), # Codeinput + # Key('Zen_Koho', qtest=False), # Multiple/All Candidate(s) + # Key('Mae_Koho', qtest=False), # Previous Candidate ### Korean keyboard support ### ### In fact, many users from Korea need only 2 keys, Key_Hangul and ### Key_Hangul_Hanja. But rest of the keys are good for future. - Key('Hangul'), # Hangul start/stop(toggle), - Key('Hangul_Start', 'Hangul Start'), # Hangul start - Key('Hangul_End', 'Hangul End'), # Hangul end, English start - Key('Hangul_Hanja', 'Hangul Hanja'), # Start Hangul->Hanja Conversion - Key('Hangul_Jamo', 'Hangul Jamo'), # Hangul Jamo mode - Key('Hangul_Romaja', 'Hangul Romaja'), # Hangul Romaja mode - # Key('Hangul_Codeinput', 'Hangul Codeinput'),# Hangul code input mode - Key('Hangul_Jeonja', 'Hangul Jeonja'), # Jeonja mode - Key('Hangul_Banja', 'Hangul Banja'), # Banja mode - Key('Hangul_PreHanja', 'Hangul PreHanja'), # Pre Hanja conversion - Key('Hangul_PostHanja', 'Hangul PostHanja'), # Post Hanja conversion - # Key('Hangul_SingleCandidate', 'Hangul SingleCandidate'), # Single candidate - # Key('Hangul_MultipleCandidate', 'Hangul MultipleCandidate'), # Multiple candidate - # Key('Hangul_PreviousCandidate', 'Hangul PreviousCandidate'), # Previous candidate - Key('Hangul_Special', 'Hangul Special'), # Special symbols - # Key('Hangul_switch', 'Hangul switch'), # Alias for mode_switch + Key('Hangul', qtest=False), # Hangul start/stop(toggle), + Key('Hangul_Start', 'Hangul Start', qtest=False), # Hangul start + Key('Hangul_End', 'Hangul End', qtest=False), # Hangul end, English start + Key('Hangul_Hanja', 'Hangul Hanja', qtest=False), # Start Hangul->Hanja Conversion + Key('Hangul_Jamo', 'Hangul Jamo', qtest=False), # Hangul Jamo mode + Key('Hangul_Romaja', 'Hangul Romaja', qtest=False), # Hangul Romaja mode + # Key('Hangul_Codeinput', 'Hangul Codeinput', qtest=False),# Hangul code input mode + Key('Hangul_Jeonja', 'Hangul Jeonja', qtest=False), # Jeonja mode + Key('Hangul_Banja', 'Hangul Banja', qtest=False), # Banja mode + Key('Hangul_PreHanja', 'Hangul PreHanja', qtest=False), # Pre Hanja conversion + Key('Hangul_PostHanja', 'Hangul PostHanja', qtest=False), # Post Hanja conversion + # Key('Hangul_SingleCandidate', 'Hangul SingleCandidate', qtest=False), # Single candidate + # Key('Hangul_MultipleCandidate', 'Hangul MultipleCandidate', qtest=False), # Multiple candidate + # Key('Hangul_PreviousCandidate', 'Hangul PreviousCandidate', qtest=False), # Previous candidate + Key('Hangul_Special', 'Hangul Special', qtest=False), # Special symbols + # Key('Hangul_switch', 'Hangul switch', qtest=False), # Alias for mode_switch - # dead keys (X keycode - 0xED00 to avoid the conflict), - Key('Dead_Grave', '`'), - Key('Dead_Acute', '´'), - Key('Dead_Circumflex', '^'), - Key('Dead_Tilde', '~'), - Key('Dead_Macron', '¯'), - Key('Dead_Breve', '˘'), - Key('Dead_Abovedot', '˙'), - Key('Dead_Diaeresis', '¨'), - Key('Dead_Abovering', '˚'), - Key('Dead_Doubleacute', '˝'), - Key('Dead_Caron', 'ˇ'), - Key('Dead_Cedilla', '¸'), - Key('Dead_Ogonek', '˛'), - Key('Dead_Iota', 'Iota'), - Key('Dead_Voiced_Sound', 'Voiced Sound'), - Key('Dead_Semivoiced_Sound', 'Semivoiced Sound'), - Key('Dead_Belowdot', 'Belowdot'), - Key('Dead_Hook', 'Hook'), - Key('Dead_Horn', 'Horn'), + # dead keys (X keycode - 0xED00 to avoid the conflict, qtest=False), + Key('Dead_Grave', '`', qtest=False), + Key('Dead_Acute', '´', qtest=False), + Key('Dead_Circumflex', '^', qtest=False), + Key('Dead_Tilde', '~', qtest=False), + Key('Dead_Macron', '¯', qtest=False), + Key('Dead_Breve', '˘', qtest=False), + Key('Dead_Abovedot', '˙', qtest=False), + Key('Dead_Diaeresis', '¨', qtest=False), + Key('Dead_Abovering', '˚', qtest=False), + Key('Dead_Doubleacute', '˝', qtest=False), + Key('Dead_Caron', 'ˇ', qtest=False), + Key('Dead_Cedilla', '¸', qtest=False), + Key('Dead_Ogonek', '˛', qtest=False), + Key('Dead_Iota', 'Iota', qtest=False), + Key('Dead_Voiced_Sound', 'Voiced Sound', qtest=False), + Key('Dead_Semivoiced_Sound', 'Semivoiced Sound', qtest=False), + Key('Dead_Belowdot', 'Belowdot', qtest=False), + Key('Dead_Hook', 'Hook', qtest=False), + Key('Dead_Horn', 'Horn', qtest=False), # Not in Qt 5.10, so data may be wrong! - Key('Dead_Stroke'), - Key('Dead_Abovecomma'), - Key('Dead_Abovereversedcomma'), - Key('Dead_Doublegrave'), - Key('Dead_Belowring'), - Key('Dead_Belowmacron'), - Key('Dead_Belowcircumflex'), - Key('Dead_Belowtilde'), - Key('Dead_Belowbreve'), - Key('Dead_Belowdiaeresis'), - Key('Dead_Invertedbreve'), - Key('Dead_Belowcomma'), - Key('Dead_Currency'), - Key('Dead_a'), - Key('Dead_A'), - Key('Dead_e'), - Key('Dead_E'), - Key('Dead_i'), - Key('Dead_I'), - Key('Dead_o'), - Key('Dead_O'), - Key('Dead_u'), - Key('Dead_U'), - Key('Dead_Small_Schwa'), - Key('Dead_Capital_Schwa'), - Key('Dead_Greek'), - Key('Dead_Lowline'), - Key('Dead_Aboveverticalline'), - Key('Dead_Belowverticalline'), - Key('Dead_Longsolidusoverlay'), + Key('Dead_Stroke', qtest=False), + Key('Dead_Abovecomma', qtest=False), + Key('Dead_Abovereversedcomma', qtest=False), + Key('Dead_Doublegrave', qtest=False), + Key('Dead_Belowring', qtest=False), + Key('Dead_Belowmacron', qtest=False), + Key('Dead_Belowcircumflex', qtest=False), + Key('Dead_Belowtilde', qtest=False), + Key('Dead_Belowbreve', qtest=False), + Key('Dead_Belowdiaeresis', qtest=False), + Key('Dead_Invertedbreve', qtest=False), + Key('Dead_Belowcomma', qtest=False), + Key('Dead_Currency', qtest=False), + Key('Dead_a', qtest=False), + Key('Dead_A', qtest=False), + Key('Dead_e', qtest=False), + Key('Dead_E', qtest=False), + Key('Dead_i', qtest=False), + Key('Dead_I', qtest=False), + Key('Dead_o', qtest=False), + Key('Dead_O', qtest=False), + Key('Dead_u', qtest=False), + Key('Dead_U', qtest=False), + Key('Dead_Small_Schwa', qtest=False), + Key('Dead_Capital_Schwa', qtest=False), + Key('Dead_Greek', qtest=False), + Key('Dead_Lowline', qtest=False), + Key('Dead_Aboveverticalline', qtest=False), + Key('Dead_Belowverticalline', qtest=False), + Key('Dead_Longsolidusoverlay', qtest=False), ### multimedia/internet keys - ignored by default - see QKeyEvent c'tor Key('Back'), @@ -394,8 +395,8 @@ KEYS = [ Key('MediaPrevious', 'Media Previous'), Key('MediaNext', 'Media Next'), Key('MediaRecord', 'Media Record'), - Key('MediaPause', 'Media Pause'), - Key('MediaTogglePlayPause', 'Toggle Media Play/Pause'), + Key('MediaPause', 'Media Pause', qtest=False), + Key('MediaTogglePlayPause', 'Toggle Media Play/Pause', qtest=False), Key('HomePage', 'Home Page'), Key('Favorites'), Key('Search'), @@ -420,162 +421,162 @@ KEYS = [ Key('LaunchD', 'Launch (D)'), Key('LaunchE', 'Launch (E)'), Key('LaunchF', 'Launch (F)'), - Key('MonBrightnessUp', 'Monitor Brightness Up'), - Key('MonBrightnessDown', 'Monitor Brightness Down'), - Key('KeyboardLightOnOff', 'Keyboard Light On/Off'), - Key('KeyboardBrightnessUp', 'Keyboard Brightness Up'), - Key('KeyboardBrightnessDown', 'Keyboard Brightness Down'), - Key('PowerOff', 'Power Off'), - Key('WakeUp', 'Wake Up'), - Key('Eject'), - Key('ScreenSaver', 'Screensaver'), - Key('WWW'), - Key('Memo', 'Memo'), - Key('LightBulb'), - Key('Shop'), - Key('History'), - Key('AddFavorite', 'Add Favorite'), - Key('HotLinks', 'Hot Links'), - Key('BrightnessAdjust', 'Adjust Brightness'), - Key('Finance'), - Key('Community'), - Key('AudioRewind', 'Media Rewind'), - Key('BackForward', 'Back Forward'), - Key('ApplicationLeft', 'Application Left'), - Key('ApplicationRight', 'Application Right'), - Key('Book'), - Key('CD'), - Key('Calculator'), - Key('ToDoList', 'To Do List'), - Key('ClearGrab', 'Clear Grab'), - Key('Close'), - Key('Copy'), - Key('Cut'), - Key('Display'), # Output switch key - Key('DOS'), - Key('Documents'), - Key('Excel', 'Spreadsheet'), - Key('Explorer', 'Browser'), - Key('Game'), - Key('Go'), - Key('iTouch'), - Key('LogOff', 'Logoff'), - Key('Market'), - Key('Meeting'), - Key('MenuKB', 'Keyboard Menu'), - Key('MenuPB', 'Menu PB'), - Key('MySites', 'My Sites'), - Key('News'), - Key('OfficeHome', 'Home Office'), - Key('Option'), - Key('Paste'), - Key('Phone'), - Key('Calendar'), - Key('Reply'), - Key('Reload'), - Key('RotateWindows', 'Rotate Windows'), - Key('RotationPB', 'Rotation PB'), - Key('RotationKB', 'Rotation KB'), - Key('Save'), - Key('Send'), - Key('Spell', 'Spellchecker'), - Key('SplitScreen', 'Split Screen'), - Key('Support'), - Key('TaskPane', 'Task Panel'), - Key('Terminal'), - Key('Tools'), - Key('Travel'), - Key('Video'), - Key('Word', 'Word Processor'), - Key('Xfer', 'XFer'), - Key('ZoomIn', 'Zoom In'), - Key('ZoomOut', 'Zoom Out'), - Key('Away'), - Key('Messenger'), - Key('WebCam'), - Key('MailForward', 'Mail Forward'), - Key('Pictures'), - Key('Music'), - Key('Battery'), - Key('Bluetooth'), - Key('WLAN', 'Wireless'), - Key('UWB', 'Ultra Wide Band'), - Key('AudioForward', 'Media Fast Forward'), - Key('AudioRepeat', 'Audio Repeat'), # Toggle repeat mode - Key('AudioRandomPlay', 'Audio Random Play'), # Toggle shuffle mode - Key('Subtitle'), - Key('AudioCycleTrack', 'Audio Cycle Track'), - Key('Time'), - Key('Hibernate'), - Key('View'), - Key('TopMenu', 'Top Menu'), - Key('PowerDown', 'Power Down'), - Key('Suspend'), - Key('ContrastAdjust', 'Contrast Adjust'), + Key('MonBrightnessUp', 'Monitor Brightness Up', qtest=False), + Key('MonBrightnessDown', 'Monitor Brightness Down', qtest=False), + Key('KeyboardLightOnOff', 'Keyboard Light On/Off', qtest=False), + Key('KeyboardBrightnessUp', 'Keyboard Brightness Up', qtest=False), + Key('KeyboardBrightnessDown', 'Keyboard Brightness Down', qtest=False), + Key('PowerOff', 'Power Off', qtest=False), + Key('WakeUp', 'Wake Up', qtest=False), + Key('Eject', qtest=False), + Key('ScreenSaver', 'Screensaver', qtest=False), + Key('WWW', qtest=False), + Key('Memo', 'Memo', qtest=False), + Key('LightBulb', qtest=False), + Key('Shop', qtest=False), + Key('History', qtest=False), + Key('AddFavorite', 'Add Favorite', qtest=False), + Key('HotLinks', 'Hot Links', qtest=False), + Key('BrightnessAdjust', 'Adjust Brightness', qtest=False), + Key('Finance', qtest=False), + Key('Community', qtest=False), + Key('AudioRewind', 'Media Rewind', qtest=False), + Key('BackForward', 'Back Forward', qtest=False), + Key('ApplicationLeft', 'Application Left', qtest=False), + Key('ApplicationRight', 'Application Right', qtest=False), + Key('Book', qtest=False), + Key('CD', qtest=False), + Key('Calculator', qtest=False), + Key('ToDoList', 'To Do List', qtest=False), + Key('ClearGrab', 'Clear Grab', qtest=False), + Key('Close', qtest=False), + Key('Copy', qtest=False), + Key('Cut', qtest=False), + Key('Display', qtest=False), # Output switch key + Key('DOS', qtest=False), + Key('Documents', qtest=False), + Key('Excel', 'Spreadsheet', qtest=False), + Key('Explorer', 'Browser', qtest=False), + Key('Game', qtest=False), + Key('Go', qtest=False), + Key('iTouch', qtest=False), + Key('LogOff', 'Logoff', qtest=False), + Key('Market', qtest=False), + Key('Meeting', qtest=False), + Key('MenuKB', 'Keyboard Menu', qtest=False), + Key('MenuPB', 'Menu PB', qtest=False), + Key('MySites', 'My Sites', qtest=False), + Key('News', qtest=False), + Key('OfficeHome', 'Home Office', qtest=False), + Key('Option', qtest=False), + Key('Paste', qtest=False), + Key('Phone', qtest=False), + Key('Calendar', qtest=False), + Key('Reply', qtest=False), + Key('Reload', qtest=False), + Key('RotateWindows', 'Rotate Windows', qtest=False), + Key('RotationPB', 'Rotation PB', qtest=False), + Key('RotationKB', 'Rotation KB', qtest=False), + Key('Save', qtest=False), + Key('Send', qtest=False), + Key('Spell', 'Spellchecker', qtest=False), + Key('SplitScreen', 'Split Screen', qtest=False), + Key('Support', qtest=False), + Key('TaskPane', 'Task Panel', qtest=False), + Key('Terminal', qtest=False), + Key('Tools', qtest=False), + Key('Travel', qtest=False), + Key('Video', qtest=False), + Key('Word', 'Word Processor', qtest=False), + Key('Xfer', 'XFer', qtest=False), + Key('ZoomIn', 'Zoom In', qtest=False), + Key('ZoomOut', 'Zoom Out', qtest=False), + Key('Away', qtest=False), + Key('Messenger', qtest=False), + Key('WebCam', qtest=False), + Key('MailForward', 'Mail Forward', qtest=False), + Key('Pictures', qtest=False), + Key('Music', qtest=False), + Key('Battery', qtest=False), + Key('Bluetooth', qtest=False), + Key('WLAN', 'Wireless', qtest=False), + Key('UWB', 'Ultra Wide Band', qtest=False), + Key('AudioForward', 'Media Fast Forward', qtest=False), + Key('AudioRepeat', 'Audio Repeat', qtest=False), # Toggle repeat mode + Key('AudioRandomPlay', 'Audio Random Play', qtest=False), # Toggle shuffle mode + Key('Subtitle', qtest=False), + Key('AudioCycleTrack', 'Audio Cycle Track', qtest=False), + Key('Time', qtest=False), + Key('Hibernate', qtest=False), + Key('View', qtest=False), + Key('TopMenu', 'Top Menu', qtest=False), + Key('PowerDown', 'Power Down', qtest=False), + Key('Suspend', qtest=False), + Key('ContrastAdjust', 'Contrast Adjust', qtest=False), - Key('LaunchG', 'Launch (G)'), - Key('LaunchH', 'Launch (H)'), + Key('LaunchG', 'Launch (G)', qtest=False), + Key('LaunchH', 'Launch (H)', qtest=False), - Key('TouchpadToggle', 'Touchpad Toggle'), - Key('TouchpadOn', 'Touchpad On'), - Key('TouchpadOff', 'Touchpad Off'), + Key('TouchpadToggle', 'Touchpad Toggle', qtest=False), + Key('TouchpadOn', 'Touchpad On', qtest=False), + Key('TouchpadOff', 'Touchpad Off', qtest=False), - Key('MicMute', 'Microphone Mute'), + Key('MicMute', 'Microphone Mute', qtest=False), - Key('Red'), - Key('Green'), - Key('Yellow'), - Key('Blue'), + Key('Red', qtest=False), + Key('Green', qtest=False), + Key('Yellow', qtest=False), + Key('Blue', qtest=False), - Key('ChannelUp', 'Channel Up'), - Key('ChannelDown', 'Channel Down'), + Key('ChannelUp', 'Channel Up', qtest=False), + Key('ChannelDown', 'Channel Down', qtest=False), - Key('Guide'), - Key('Info'), - Key('Settings'), + Key('Guide', qtest=False), + Key('Info', qtest=False), + Key('Settings', qtest=False), - Key('MicVolumeUp', 'Microphone Volume Up'), - Key('MicVolumeDown', 'Microphone Volume Down'), + Key('MicVolumeUp', 'Microphone Volume Up', qtest=False), + Key('MicVolumeDown', 'Microphone Volume Down', qtest=False), - Key('New'), - Key('Open'), - Key('Find'), - Key('Undo'), - Key('Redo'), + Key('New', qtest=False), + Key('Open', qtest=False), + Key('Find', qtest=False), + Key('Undo', qtest=False), + Key('Redo', qtest=False), - Key('MediaLast', 'Media Last'), + Key('MediaLast', 'Media Last', qtest=False), ### Keypad navigation keys - Key('Select'), - Key('Yes'), - Key('No'), + Key('Select', qtest=False), + Key('Yes', qtest=False), + Key('No', qtest=False), ### Newer misc keys - Key('Cancel'), - Key('Printer'), - Key('Execute'), - Key('Sleep'), - Key('Play'), # Not the same as Key_MediaPlay - Key('Zoom'), - # Key('Jisho'), # IME: Dictionary key - # Key('Oyayubi_Left'), # IME: Left Oyayubi key - # Key('Oyayubi_Right'), # IME: Right Oyayubi key - Key('Exit'), + Key('Cancel', qtest=False), + Key('Printer', qtest=False), + Key('Execute', qtest=False), + Key('Sleep', qtest=False), + Key('Play', qtest=False), # Not the same as Key_MediaPlay + Key('Zoom', qtest=False), + # Key('Jisho', qtest=False), # IME: Dictionary key + # Key('Oyayubi_Left', qtest=False), # IME: Left Oyayubi key + # Key('Oyayubi_Right', qtest=False), # IME: Right Oyayubi key + Key('Exit', qtest=False), # Device keys - Key('Context1'), - Key('Context2'), - Key('Context3'), - Key('Context4'), - Key('Call'), # set absolute state to in a call (do not toggle state) - Key('Hangup'), # set absolute state to hang up (do not toggle state) - Key('Flip'), - Key('ToggleCallHangup', 'Toggle Call/Hangup'), # a toggle key for answering, or hanging up, based on current call state - Key('VoiceDial', 'Voice Dial'), - Key('LastNumberRedial', 'Last Number Redial'), + Key('Context1', qtest=False), + Key('Context2', qtest=False), + Key('Context3', qtest=False), + Key('Context4', qtest=False), + Key('Call', qtest=False), # set absolute state to in a call (do not toggle state) + Key('Hangup', qtest=False), # set absolute state to hang up (do not toggle state) + Key('Flip', qtest=False), + Key('ToggleCallHangup', 'Toggle Call/Hangup', qtest=False), # a toggle key for answering, or hanging up, based on current call state + Key('VoiceDial', 'Voice Dial', qtest=False), + Key('LastNumberRedial', 'Last Number Redial', qtest=False), - Key('Camera', 'Camera Shutter'), - Key('CameraFocus', 'Camera Focus'), + Key('Camera', 'Camera Shutter', qtest=False), + Key('CameraFocus', 'Camera Focus', qtest=False), - Key('unknown', 'Unknown'), + Key('unknown', 'Unknown', qtest=False), ] diff --git a/tests/unit/keyinput/test_keyutils.py b/tests/unit/keyinput/test_keyutils.py index df93d268c..df399497e 100644 --- a/tests/unit/keyinput/test_keyutils.py +++ b/tests/unit/keyinput/test_keyutils.py @@ -18,7 +18,8 @@ # along with qutebrowser. If not, see . import pytest -from PyQt5.QtCore import Qt +from PyQt5.QtCore import Qt, pyqtSignal +from PyQt5.QtWidgets import QWidget from tests.unit.keyinput import key_data from qutebrowser.utils import utils @@ -43,6 +44,33 @@ def test_key_text(qt_key, upper): assert info.text() == expected +class KeyTestWidget(QWidget): + + got_text = pyqtSignal() + + def keyPressEvent(self, e): + self.text = e.text() + self.got_text.emit() + + +@pytest.fixture +def key_test(qtbot): + w = KeyTestWidget() + qtbot.add_widget(w) + return w + + +def test_key_test_qtest(qt_key, qtbot, key_test): + if not qt_key.qtest: + pytest.skip("Unsupported by QtTest") + + with qtbot.wait_signal(key_test.got_text): + qtbot.keyPress(key_test, qt_key.member) + + info = keyutils.KeyInfo(qt_key.member, modifiers=Qt.KeyboardModifiers()) + assert info.text() == key_test.text.lower() + + class TestKeyToString: def test_to_string(self, qt_key): From 50d2ef3b90d22c40306b40dccd7f9211d60b491c Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 1 Mar 2018 08:01:39 +0100 Subject: [PATCH 107/135] Fix handling of printable control keys in KeyInfo.text() --- qutebrowser/keyinput/keyutils.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/qutebrowser/keyinput/keyutils.py b/qutebrowser/keyinput/keyutils.py index 1feb371a8..5c1097aa8 100644 --- a/qutebrowser/keyinput/keyutils.py +++ b/qutebrowser/keyinput/keyutils.py @@ -254,8 +254,17 @@ class KeyInfo: def text(self): """Get the text which would be displayed when pressing this key.""" - if self.key == Qt.Key_Space: - return ' ' + 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 '' From a57fc5c746358dd08c2ab8703bcd0162be479ea3 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 1 Mar 2018 08:12:19 +0100 Subject: [PATCH 108/135] Refactor keyutils tests involving all keys --- tests/unit/keyinput/key_data.py | 4 +++ tests/unit/keyinput/test_keyutils.py | 50 ++++++++++++++++------------ 2 files changed, 33 insertions(+), 21 deletions(-) diff --git a/tests/unit/keyinput/key_data.py b/tests/unit/keyinput/key_data.py index cc5c249b5..0164a839b 100644 --- a/tests/unit/keyinput/key_data.py +++ b/tests/unit/keyinput/key_data.py @@ -24,6 +24,7 @@ import attr +from PyQt5.QtCore import Qt @attr.s @@ -46,6 +47,9 @@ class Key: member = attr.ib(None) qtest = attr.ib(True) + def __attrs_post_init__(self): + self.member = getattr(Qt, 'Key_' + self.attribute, None) + # From enum Key in qt5/qtbase/src/corelib/global/qnamespace.h KEYS = [ diff --git a/tests/unit/keyinput/test_keyutils.py b/tests/unit/keyinput/test_keyutils.py index df399497e..43d9bf879 100644 --- a/tests/unit/keyinput/test_keyutils.py +++ b/tests/unit/keyinput/test_keyutils.py @@ -29,23 +29,25 @@ from qutebrowser.keyinput import keyutils @pytest.fixture(params=key_data.KEYS, ids=lambda k: k.attribute) def qt_key(request): key = request.param - member = getattr(Qt, 'Key_' + key.attribute, None) - if member is None: + if key.member is None: pytest.skip("Did not find key {}".format(key.attribute)) - key.member = member return key -@pytest.mark.parametrize('upper', [False, True]) -def test_key_text(qt_key, upper): - modifiers = Qt.ShiftModifier if upper else Qt.KeyboardModifiers() - info = keyutils.KeyInfo(qt_key.member, modifiers=modifiers) - expected = qt_key.uppertext if upper else qt_key.text - assert info.text() == expected +@pytest.fixture(params=[key for key in key_data.KEYS if key.qtest], + ids=lambda k: k.attribute) +def qtest_key(request): + return request.param class KeyTestWidget(QWidget): + """Widget to get the text of QKeyPressEvents. + + This is done so we can check QTest::keyToAscii (qasciikey.cpp) as we can't + call that directly, only via QTest::keyPress. + """ + got_text = pyqtSignal() def keyPressEvent(self, e): @@ -53,22 +55,28 @@ class KeyTestWidget(QWidget): self.got_text.emit() -@pytest.fixture -def key_test(qtbot): - w = KeyTestWidget() - qtbot.add_widget(w) - return w +class TestKeyText: + @pytest.mark.parametrize('upper', [False, True]) + def test_key_text(self, qt_key, upper): + modifiers = Qt.ShiftModifier if upper else Qt.KeyboardModifiers() + info = keyutils.KeyInfo(qt_key.member, modifiers=modifiers) + expected = qt_key.uppertext if upper else qt_key.text + assert info.text() == expected -def test_key_test_qtest(qt_key, qtbot, key_test): - if not qt_key.qtest: - pytest.skip("Unsupported by QtTest") + @pytest.fixture + def key_test(self, qtbot): + w = KeyTestWidget() + qtbot.add_widget(w) + return w - with qtbot.wait_signal(key_test.got_text): - qtbot.keyPress(key_test, qt_key.member) + def test_key_test_qtest(self, qtest_key, qtbot, key_test): + with qtbot.wait_signal(key_test.got_text): + qtbot.keyPress(key_test, qtest_key.member) - info = keyutils.KeyInfo(qt_key.member, modifiers=Qt.KeyboardModifiers()) - assert info.text() == key_test.text.lower() + info = keyutils.KeyInfo(qtest_key.member, + modifiers=Qt.KeyboardModifiers()) + assert info.text() == key_test.text.lower() class TestKeyToString: From fac8d72d8c204f526a845574603d3c5dbd55e80b Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 1 Mar 2018 08:44:27 +0100 Subject: [PATCH 109/135] Capitalize Escape in TestFullscreenNotification --- tests/unit/misc/test_miscwidgets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/misc/test_miscwidgets.py b/tests/unit/misc/test_miscwidgets.py index 39f078fa6..d6b099418 100644 --- a/tests/unit/misc/test_miscwidgets.py +++ b/tests/unit/misc/test_miscwidgets.py @@ -106,7 +106,7 @@ class TestFullscreenNotification: @pytest.mark.parametrize('bindings, text', [ ({'': 'fullscreen --leave'}, - "Press to exit fullscreen."), + "Press to exit fullscreen."), ({'': 'fullscreen'}, "Page is now fullscreen."), ({'a': 'fullscreen --leave'}, "Press a to exit fullscreen."), ({}, "Page is now fullscreen."), From af6e5b1838d5dd72a7c8fef10df7d920c16770ba Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 1 Mar 2018 08:56:39 +0100 Subject: [PATCH 110/135] Fix lint --- tests/unit/keyinput/test_keyutils.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/unit/keyinput/test_keyutils.py b/tests/unit/keyinput/test_keyutils.py index 43d9bf879..c1ddbcb5a 100644 --- a/tests/unit/keyinput/test_keyutils.py +++ b/tests/unit/keyinput/test_keyutils.py @@ -50,6 +50,10 @@ class KeyTestWidget(QWidget): got_text = pyqtSignal() + def __init__(self, parent=None): + super().__init__(parent) + self.text = None + def keyPressEvent(self, e): self.text = e.text() self.got_text.emit() From 693178c8ee0c22f5b0c168a1dcbca9db1e5dd49c Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 1 Mar 2018 20:26:23 +0100 Subject: [PATCH 111/135] Refactor KeyInfo.__str__ This removes the special handling for macOS, but this is hopefully not needed anymore as we don't compare strings. --- qutebrowser/keyinput/keyutils.py | 46 ++++++---------------------- tests/unit/keyinput/test_keyutils.py | 8 +---- 2 files changed, 10 insertions(+), 44 deletions(-) diff --git a/qutebrowser/keyinput/keyutils.py b/qutebrowser/keyinput/keyutils.py index 5c1097aa8..c45dab290 100644 --- a/qutebrowser/keyinput/keyutils.py +++ b/qutebrowser/keyinput/keyutils.py @@ -203,54 +203,26 @@ class KeyInfo: A name of the key (combination) as a string or an empty string if only modifiers are pressed. """ - if utils.is_mac: - # Qt swaps Ctrl/Meta on macOS, so we switch it back here so the - # user can use it in the config as expected. See: - # https://github.com/qutebrowser/qutebrowser/issues/110 - # http://doc.qt.io/qt-5.4/osx-issues.html#special-keys - modmask2str = collections.OrderedDict([ - (Qt.MetaModifier, 'Ctrl'), - (Qt.AltModifier, 'Alt'), - (Qt.ControlModifier, 'Meta'), - (Qt.ShiftModifier, 'Shift'), - ]) - else: - modmask2str = collections.OrderedDict([ - (Qt.ControlModifier, 'Ctrl'), - (Qt.AltModifier, 'Alt'), - (Qt.MetaModifier, 'Meta'), - (Qt.ShiftModifier, 'Shift'), - ]) - if is_modifier_key(self.key): return '' - parts = [] - - for (mask, s) in modmask2str.items(): - if self.modifiers & mask and s not in parts: - parts.append(s) - key_string = _key_to_string(self.key) if is_printable(self.key): - # FIXME Add a test to make sure Tab doesn;t become TAB + # "normal" binding + # FIXME Add a test to make sure Tab doesn't become TAB assert len(key_string) == 1 or self.key == Qt.Key_Space, key_string if self.modifiers == Qt.ShiftModifier: - parts = [] - key_string = key_string.upper() + return key_string.upper() + elif self.modifiers == Qt.NoModifier: + return key_string.lower() else: + # Use special binding syntax, but instead of key_string = key_string.lower() - parts.append(key_string) - part_string = '+'.join(parts) - - if len(part_string) > 1: - # "special" binding - return '<{}>'.format(part_string) - else: - # "normal" binding - return part_string + # "special" binding + modifier_string = QKeySequence(self.modifiers).toString() + return '<{}{}>'.format(modifier_string, key_string) def text(self): """Get the text which would be displayed when pressing this key.""" diff --git a/tests/unit/keyinput/test_keyutils.py b/tests/unit/keyinput/test_keyutils.py index c1ddbcb5a..7301b7747 100644 --- a/tests/unit/keyinput/test_keyutils.py +++ b/tests/unit/keyinput/test_keyutils.py @@ -138,13 +138,7 @@ class TestKeyEventToString: key=Qt.Key_A, modifiers=(Qt.ControlModifier | Qt.AltModifier | Qt.MetaModifier | Qt.ShiftModifier)) s = str(keyutils.KeyInfo.from_event(evt)) - assert s == '' - - @pytest.mark.fake_os('mac') - def test_mac(self, fake_keyevent_factory): - """Test with a simulated mac.""" - evt = fake_keyevent_factory(key=Qt.Key_A, modifiers=Qt.ControlModifier) - assert str(keyutils.KeyInfo.from_event(evt)) == '' + assert s == '' @pytest.mark.parametrize('keystr, expected', [ From 7cb781cc92a4d541659e243d3d92be6934e2b26c Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 1 Mar 2018 20:45:55 +0100 Subject: [PATCH 112/135] Simplify handling of modifier-only keys Now that we don't rely on str(KeyInfo) being empty anywhere, there's no reason to return an empty string for only-modifier keypresses anymore. While those keys can't be bound (QKeySequence('Shift') == Qt.Key_unknown) there's also no reason to explicitly ignore them. --- qutebrowser/keyinput/basekeyparser.py | 4 ---- qutebrowser/keyinput/keyutils.py | 11 +++++------ tests/unit/keyinput/test_keyutils.py | 4 ++-- 3 files changed, 7 insertions(+), 12 deletions(-) diff --git a/qutebrowser/keyinput/basekeyparser.py b/qutebrowser/keyinput/basekeyparser.py index 2c934617b..a8501ab4c 100644 --- a/qutebrowser/keyinput/basekeyparser.py +++ b/qutebrowser/keyinput/basekeyparser.py @@ -128,10 +128,6 @@ class BaseKeyParser(QObject): txt = str(keyutils.KeyInfo.from_event(e)) self._debug_log("Got key: 0x{:x} / text: '{}'".format(key, txt)) - if keyutils.is_modifier_key(key): - self._debug_log("Ignoring, only modifier") - return QKeySequence.NoMatch - if (txt.isdigit() and self._supports_count and not (not self._count and txt == '0')): assert len(txt) == 1, txt diff --git a/qutebrowser/keyinput/keyutils.py b/qutebrowser/keyinput/keyutils.py index c45dab290..85c35895a 100644 --- a/qutebrowser/keyinput/keyutils.py +++ b/qutebrowser/keyinput/keyutils.py @@ -200,15 +200,14 @@ class KeyInfo: """Convert this KeyInfo to a meaningful name. Return: - A name of the key (combination) as a string or - an empty string if only modifiers are pressed. + A name of the key (combination) as a string. """ - if is_modifier_key(self.key): - return '' - key_string = _key_to_string(self.key) - if is_printable(self.key): + if is_modifier_key(self.key): + # Don't return e.g. + return '<{}>'.format(key_string) + elif is_printable(self.key): # "normal" binding # FIXME Add a test to make sure Tab doesn't become TAB assert len(key_string) == 1 or self.key == Qt.Key_Space, key_string diff --git a/tests/unit/keyinput/test_keyutils.py b/tests/unit/keyinput/test_keyutils.py index 7301b7747..bf24de401 100644 --- a/tests/unit/keyinput/test_keyutils.py +++ b/tests/unit/keyinput/test_keyutils.py @@ -113,13 +113,13 @@ class TestKeyEventToString: """Test keyeevent when only control is pressed.""" evt = fake_keyevent_factory(key=Qt.Key_Control, modifiers=Qt.ControlModifier) - assert not str(keyutils.KeyInfo.from_event(evt)) + assert str(keyutils.KeyInfo.from_event(evt)) == '' def test_only_hyper_l(self, fake_keyevent_factory): """Test keyeevent when only Hyper_L is pressed.""" evt = fake_keyevent_factory(key=Qt.Key_Hyper_L, modifiers=Qt.MetaModifier) - assert not str(keyutils.KeyInfo.from_event(evt)) + assert str(keyutils.KeyInfo.from_event(evt)) == '' def test_only_key(self, fake_keyevent_factory): """Test with a simple key pressed.""" From 3a11a24be022cbc9115b49f3f63707a65a425b19 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 1 Mar 2018 23:04:00 +0100 Subject: [PATCH 113/135] Fix modifier handling We don't want to show , but should still work correctly. --- qutebrowser/keyinput/keyutils.py | 22 +++++++++++----------- tests/unit/keyinput/test_keyutils.py | 26 ++++++++++++-------------- 2 files changed, 23 insertions(+), 25 deletions(-) diff --git a/qutebrowser/keyinput/keyutils.py b/qutebrowser/keyinput/keyutils.py index 85c35895a..1cb941ac7 100644 --- a/qutebrowser/keyinput/keyutils.py +++ b/qutebrowser/keyinput/keyutils.py @@ -33,14 +33,6 @@ def is_printable(key): return key <= 0xff -def is_modifier_key(key): - # FIXME docs - return key in (Qt.Key_Control, Qt.Key_Alt, Qt.Key_Shift, Qt.Key_Meta, - Qt.Key_AltGr, Qt.Key_Super_L, Qt.Key_Super_R, - Qt.Key_Hyper_L, Qt.Key_Hyper_R, Qt.Key_Direction_L, - Qt.Key_Direction_R) - - def _key_to_string(key): """Convert a Qt::Key member to a meaningful name. @@ -203,10 +195,18 @@ class KeyInfo: A name of the key (combination) as a string. """ key_string = _key_to_string(self.key) + 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, + } + modifiers = int(self.modifiers) - if is_modifier_key(self.key): + if self.key in modifier_map: # Don't return e.g. - return '<{}>'.format(key_string) + modifiers &= ~modifier_map[self.key] elif is_printable(self.key): # "normal" binding # FIXME Add a test to make sure Tab doesn't become TAB @@ -220,7 +220,7 @@ class KeyInfo: key_string = key_string.lower() # "special" binding - modifier_string = QKeySequence(self.modifiers).toString() + modifier_string = QKeySequence(modifiers).toString() return '<{}{}>'.format(modifier_string, key_string) def text(self): diff --git a/tests/unit/keyinput/test_keyutils.py b/tests/unit/keyinput/test_keyutils.py index bf24de401..25fe8e50e 100644 --- a/tests/unit/keyinput/test_keyutils.py +++ b/tests/unit/keyinput/test_keyutils.py @@ -115,12 +115,6 @@ class TestKeyEventToString: modifiers=Qt.ControlModifier) assert str(keyutils.KeyInfo.from_event(evt)) == '' - def test_only_hyper_l(self, fake_keyevent_factory): - """Test keyeevent when only Hyper_L is pressed.""" - evt = fake_keyevent_factory(key=Qt.Key_Hyper_L, - modifiers=Qt.MetaModifier) - assert str(keyutils.KeyInfo.from_event(evt)) == '' - def test_only_key(self, fake_keyevent_factory): """Test with a simple key pressed.""" evt = fake_keyevent_factory(key=Qt.Key_A) @@ -140,6 +134,18 @@ class TestKeyEventToString: s = str(keyutils.KeyInfo.from_event(evt)) assert s == '' + def test_modifier_key(self, fake_keyevent_factory): + evt = fake_keyevent_factory(key=Qt.Key_Shift, + modifiers=Qt.ShiftModifier) + s = str(keyutils.KeyInfo.from_event(evt)) + assert s == '' + + def test_modifier_key_with_modifiers(self, fake_keyevent_factory): + evt = fake_keyevent_factory(key=Qt.Key_Shift, + modifiers=(Qt.ShiftModifier | + Qt.ControlModifier)) + s = str(keyutils.KeyInfo.from_event(evt)) + assert s == '' @pytest.mark.parametrize('keystr, expected', [ ('', keyutils.KeySequence(Qt.ControlModifier | Qt.Key_X)), @@ -184,11 +190,3 @@ def test_normalize_keystr(orig, normalized): ]) def test_is_printable(key, printable): assert keyutils.is_printable(key) == printable - - -@pytest.mark.parametrize('key, ismodifier', [ - (Qt.Key_Control, True), - (Qt.Key_X, False) -]) -def test_is_modifier_key(key, ismodifier): - assert keyutils.is_modifier_key(key) == ismodifier From d9a88e139c3a6dc4a4d09600f13f03cc2bb8a689 Mon Sep 17 00:00:00 2001 From: Fritz Reichwald Date: Fri, 2 Mar 2018 12:54:29 +0100 Subject: [PATCH 114/135] Test for modifiers and corner case 0xFF --- tests/unit/keyinput/test_keyutils.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/unit/keyinput/test_keyutils.py b/tests/unit/keyinput/test_keyutils.py index 25fe8e50e..2d6e6310a 100644 --- a/tests/unit/keyinput/test_keyutils.py +++ b/tests/unit/keyinput/test_keyutils.py @@ -186,6 +186,13 @@ def test_normalize_keystr(orig, normalized): @pytest.mark.parametrize('key, printable', [ (Qt.Key_Control, False), + (Qt.Key_Space, False), + (Qt.Key_Escape, False), + (Qt.Key_Tab, False), + (Qt.Key_Backspace, False), + (Qt.Key_Return, False), + (Qt.Key_Enter, False), + (Qt.Key_ydiaeresis, True), (Qt.Key_X, True) ]) def test_is_printable(key, printable): From e306e2dadb3d2d38d7f9b9c794a59d6eb163ef81 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 2 Mar 2018 13:44:59 +0100 Subject: [PATCH 115/135] Improve and fix test_is_printable --- tests/unit/keyinput/test_keyutils.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/unit/keyinput/test_keyutils.py b/tests/unit/keyinput/test_keyutils.py index 2d6e6310a..154330af2 100644 --- a/tests/unit/keyinput/test_keyutils.py +++ b/tests/unit/keyinput/test_keyutils.py @@ -186,14 +186,17 @@ def test_normalize_keystr(orig, normalized): @pytest.mark.parametrize('key, printable', [ (Qt.Key_Control, False), - (Qt.Key_Space, False), (Qt.Key_Escape, False), (Qt.Key_Tab, False), + (Qt.Key_Backtab, False), (Qt.Key_Backspace, False), (Qt.Key_Return, False), (Qt.Key_Enter, False), + (Qt.Key_X | Qt.ControlModifier, False), # Wrong usage + + (Qt.Key_Space, True), (Qt.Key_ydiaeresis, True), - (Qt.Key_X, True) + (Qt.Key_X, True), ]) def test_is_printable(key, printable): assert keyutils.is_printable(key) == printable From b3834835edb32188bb53dcaab502fef689e6b07d Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 2 Mar 2018 14:16:40 +0100 Subject: [PATCH 116/135] Bring back keyutils.is_modifier() and modifier handling Turns out when we press yY, we get three events: Qt.Key_Y, Qt.NoModifier Qt.Key_Shift, Qt.ShiftModifier Qt.Key_Y, Qt.ShiftModifier If we don't ignore the second one, our keychain will be interrupted by the Shift keypress. --- qutebrowser/keyinput/basekeyparser.py | 4 +++ qutebrowser/keyinput/keyutils.py | 30 ++++++++++++++++------- tests/unit/keyinput/conftest.py | 1 + tests/unit/keyinput/test_basekeyparser.py | 11 +++++++++ tests/unit/keyinput/test_keyutils.py | 9 +++++++ 5 files changed, 46 insertions(+), 9 deletions(-) diff --git a/qutebrowser/keyinput/basekeyparser.py b/qutebrowser/keyinput/basekeyparser.py index a8501ab4c..2c934617b 100644 --- a/qutebrowser/keyinput/basekeyparser.py +++ b/qutebrowser/keyinput/basekeyparser.py @@ -128,6 +128,10 @@ class BaseKeyParser(QObject): txt = str(keyutils.KeyInfo.from_event(e)) self._debug_log("Got key: 0x{:x} / text: '{}'".format(key, txt)) + if keyutils.is_modifier_key(key): + self._debug_log("Ignoring, only modifier") + return QKeySequence.NoMatch + if (txt.isdigit() and self._supports_count and not (not self._count and txt == '0')): assert len(txt) == 1, txt diff --git a/qutebrowser/keyinput/keyutils.py b/qutebrowser/keyinput/keyutils.py index 1cb941ac7..e456da045 100644 --- a/qutebrowser/keyinput/keyutils.py +++ b/qutebrowser/keyinput/keyutils.py @@ -29,10 +29,29 @@ 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 is_printable(key): return key <= 0xff +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. + """ + return key in _MODIFIER_MAP + + def _key_to_string(key): """Convert a Qt::Key member to a meaningful name. @@ -195,18 +214,11 @@ class KeyInfo: A name of the key (combination) as a string. """ key_string = _key_to_string(self.key) - 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, - } modifiers = int(self.modifiers) - if self.key in modifier_map: + if self.key in _MODIFIER_MAP: # Don't return e.g. - modifiers &= ~modifier_map[self.key] + modifiers &= ~_MODIFIER_MAP[self.key] elif is_printable(self.key): # "normal" binding # FIXME Add a test to make sure Tab doesn't become TAB diff --git a/tests/unit/keyinput/conftest.py b/tests/unit/keyinput/conftest.py index 0713c5d26..f78ed61ad 100644 --- a/tests/unit/keyinput/conftest.py +++ b/tests/unit/keyinput/conftest.py @@ -27,6 +27,7 @@ BINDINGS = {'prompt': {'': 'message-info ctrla', 'ba': 'message-info ba', 'ax': 'message-info ax', 'ccc': 'message-info ccc', + 'yY': 'yank -s', '0': 'message-info 0'}, 'command': {'foo': 'message-info bar', '': 'message-info ctrlx'}, diff --git a/tests/unit/keyinput/test_basekeyparser.py b/tests/unit/keyinput/test_basekeyparser.py index 5da4efa9b..872709d6e 100644 --- a/tests/unit/keyinput/test_basekeyparser.py +++ b/tests/unit/keyinput/test_basekeyparser.py @@ -241,6 +241,17 @@ class TestKeyChain: handle_text((Qt.Key_B, 'b')) assert not keyparser.execute.called + def test_binding_with_shift(self, keyparser, fake_keyevent_factory): + """Simulate a binding which involves shift.""" + keyparser.handle( + fake_keyevent_factory(Qt.Key_Y, text='y')) + keyparser.handle( + fake_keyevent_factory(Qt.Key_Shift, Qt.ShiftModifier, text='')) + keyparser.handle( + fake_keyevent_factory(Qt.Key_Y, Qt.ShiftModifier, text='Y')) + + keyparser.execute.assert_called_once_with('yank -s', None) + class TestCount: diff --git a/tests/unit/keyinput/test_keyutils.py b/tests/unit/keyinput/test_keyutils.py index 154330af2..220d16dc6 100644 --- a/tests/unit/keyinput/test_keyutils.py +++ b/tests/unit/keyinput/test_keyutils.py @@ -200,3 +200,12 @@ def test_normalize_keystr(orig, normalized): ]) def test_is_printable(key, printable): assert keyutils.is_printable(key) == printable + + +@pytest.mark.parametrize('key, ismodifier', [ + (Qt.Key_Control, True), + (Qt.Key_X, False), + (Qt.Key_Super_L, False), # Modifier but not in _MODIFIER_MAP +]) +def test_is_modifier_key(key, ismodifier): + assert keyutils.is_modifier_key(key) == ismodifier From da60d11b2415e2671f6eb408695edd55362fd6cb Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sat, 3 Mar 2018 22:47:19 +0100 Subject: [PATCH 117/135] Refactor keyutils tests --- qutebrowser/keyinput/keyutils.py | 1 - tests/unit/keyinput/key_data.py | 4 +- tests/unit/keyinput/test_keyutils.py | 126 ++++++++++++--------------- 3 files changed, 60 insertions(+), 71 deletions(-) diff --git a/qutebrowser/keyinput/keyutils.py b/qutebrowser/keyinput/keyutils.py index e456da045..61e3e53b4 100644 --- a/qutebrowser/keyinput/keyutils.py +++ b/qutebrowser/keyinput/keyutils.py @@ -221,7 +221,6 @@ class KeyInfo: modifiers &= ~_MODIFIER_MAP[self.key] elif is_printable(self.key): # "normal" binding - # FIXME Add a test to make sure Tab doesn't become TAB assert len(key_string) == 1 or self.key == Qt.Key_Space, key_string if self.modifiers == Qt.ShiftModifier: return key_string.upper() diff --git a/tests/unit/keyinput/key_data.py b/tests/unit/keyinput/key_data.py index 0164a839b..2f13c8b5e 100644 --- a/tests/unit/keyinput/key_data.py +++ b/tests/unit/keyinput/key_data.py @@ -41,7 +41,7 @@ class Key: """ attribute = attr.ib() - name = attr.ib(None) # default: name == attribute + name = attr.ib(None) text = attr.ib('') uppertext = attr.ib('') member = attr.ib(None) @@ -49,6 +49,8 @@ class Key: def __attrs_post_init__(self): self.member = getattr(Qt, 'Key_' + self.attribute, None) + if self.name is None: + self.name = self.attribute # From enum Key in qt5/qtbase/src/corelib/global/qnamespace.h diff --git a/tests/unit/keyinput/test_keyutils.py b/tests/unit/keyinput/test_keyutils.py index 220d16dc6..7fc6cc12c 100644 --- a/tests/unit/keyinput/test_keyutils.py +++ b/tests/unit/keyinput/test_keyutils.py @@ -28,6 +28,10 @@ from qutebrowser.keyinput import keyutils @pytest.fixture(params=key_data.KEYS, ids=lambda k: k.attribute) def qt_key(request): + """Get all existing keys from key_data.py. + + Keys which don't exist with this Qt version result in skipped tests. + """ key = request.param if key.member is None: pytest.skip("Did not find key {}".format(key.attribute)) @@ -37,10 +41,21 @@ def qt_key(request): @pytest.fixture(params=[key for key in key_data.KEYS if key.qtest], ids=lambda k: k.attribute) def qtest_key(request): + """Get keys from key_data.py which can be used with QTest.""" return request.param -class KeyTestWidget(QWidget): +def test_key_data(): + """Make sure all possible keys are in key_data.KEYS.""" + key_names = {name[len("Key_"):] + for name, value in sorted(vars(Qt).items()) + if isinstance(value, Qt.Key)} + key_data_names = {key.attribute for key in sorted(key_data.KEYS)} + diff = key_names - key_data_names + assert not diff + + +class KeyTesterWidget(QWidget): """Widget to get the text of QKeyPressEvents. @@ -59,35 +74,42 @@ class KeyTestWidget(QWidget): self.got_text.emit() -class TestKeyText: +class TestKeyInfoText: @pytest.mark.parametrize('upper', [False, True]) - def test_key_text(self, qt_key, upper): + def test_text(self, qt_key, upper): + """Test KeyInfo.text() with all possible keys. + + See key_data.py for inputs and expected values. + """ modifiers = Qt.ShiftModifier if upper else Qt.KeyboardModifiers() info = keyutils.KeyInfo(qt_key.member, modifiers=modifiers) expected = qt_key.uppertext if upper else qt_key.text assert info.text() == expected @pytest.fixture - def key_test(self, qtbot): - w = KeyTestWidget() + def key_tester(self, qtbot): + w = KeyTesterWidget() qtbot.add_widget(w) return w - def test_key_test_qtest(self, qtest_key, qtbot, key_test): - with qtbot.wait_signal(key_test.got_text): - qtbot.keyPress(key_test, qtest_key.member) + def test_text_qtest(self, qtest_key, qtbot, key_tester): + """Make sure KeyInfo.text() lines up with QTest::keyToAscii. + + See key_data.py for inputs and expected values. + """ + with qtbot.wait_signal(key_tester.got_text): + qtbot.keyPress(key_tester, qtest_key.member) info = keyutils.KeyInfo(qtest_key.member, modifiers=Qt.KeyboardModifiers()) - assert info.text() == key_test.text.lower() + assert info.text() == key_tester.text.lower() class TestKeyToString: def test_to_string(self, qt_key): - name = qt_key.attribute if qt_key.name is None else qt_key.name - assert keyutils._key_to_string(qt_key.member) == name + assert keyutils._key_to_string(qt_key.member) == qt_key.name def test_missing(self, monkeypatch): monkeypatch.delattr(keyutils.Qt, 'Key_Blue') @@ -95,57 +117,24 @@ class TestKeyToString: # want to know if the mapping still behaves properly. assert keyutils._key_to_string(Qt.Key_A) == 'A' - def test_all(self): - """Make sure all possible keys are in key_data.KEYS.""" - key_names = {name[len("Key_"):] - for name, value in sorted(vars(Qt).items()) - if isinstance(value, Qt.Key)} - key_data_names = {key.attribute for key in sorted(key_data.KEYS)} - diff = key_names - key_data_names - assert not diff +@pytest.mark.parametrize('key, modifiers, expected', [ + (Qt.Key_A, Qt.NoModifier, 'a'), + (Qt.Key_A, Qt.ShiftModifier, 'A'), -class TestKeyEventToString: + (Qt.Key_Tab, Qt.ShiftModifier, ''), + (Qt.Key_A, Qt.ControlModifier, ''), + (Qt.Key_A, Qt.ControlModifier | Qt.ShiftModifier, ''), + (Qt.Key_A, + Qt.ControlModifier | Qt.AltModifier | Qt.MetaModifier | Qt.ShiftModifier, + ''), - """Test keyevent_to_string.""" + (Qt.Key_Shift, Qt.ShiftModifier, ''), + (Qt.Key_Shift, Qt.ShiftModifier | Qt.ControlModifier, ''), +]) +def test_key_info_str(key, modifiers, expected): + assert str(keyutils.KeyInfo(key, modifiers)) == expected - def test_only_control(self, fake_keyevent_factory): - """Test keyeevent when only control is pressed.""" - evt = fake_keyevent_factory(key=Qt.Key_Control, - modifiers=Qt.ControlModifier) - assert str(keyutils.KeyInfo.from_event(evt)) == '' - - def test_only_key(self, fake_keyevent_factory): - """Test with a simple key pressed.""" - evt = fake_keyevent_factory(key=Qt.Key_A) - assert str(keyutils.KeyInfo.from_event(evt)) == 'a' - - def test_key_and_modifier(self, fake_keyevent_factory): - """Test with key and modifier pressed.""" - evt = fake_keyevent_factory(key=Qt.Key_A, modifiers=Qt.ControlModifier) - expected = '' if utils.is_mac else '' - assert str(keyutils.KeyInfo.from_event(evt)) == expected - - def test_key_and_modifiers(self, fake_keyevent_factory): - """Test with key and multiple modifiers pressed.""" - evt = fake_keyevent_factory( - key=Qt.Key_A, modifiers=(Qt.ControlModifier | Qt.AltModifier | - Qt.MetaModifier | Qt.ShiftModifier)) - s = str(keyutils.KeyInfo.from_event(evt)) - assert s == '' - - def test_modifier_key(self, fake_keyevent_factory): - evt = fake_keyevent_factory(key=Qt.Key_Shift, - modifiers=Qt.ShiftModifier) - s = str(keyutils.KeyInfo.from_event(evt)) - assert s == '' - - def test_modifier_key_with_modifiers(self, fake_keyevent_factory): - evt = fake_keyevent_factory(key=Qt.Key_Shift, - modifiers=(Qt.ShiftModifier | - Qt.ControlModifier)) - s = str(keyutils.KeyInfo.from_event(evt)) - assert s == '' @pytest.mark.parametrize('keystr, expected', [ ('', keyutils.KeySequence(Qt.ControlModifier | Qt.Key_X)), @@ -170,18 +159,17 @@ def test_parse(keystr, expected): @pytest.mark.parametrize('orig, normalized', [ - ('', ''), - ('', ''), - ('', ''), - ('', ''), - ('', ''), - ('', ''), - ('', ''), - ('', '') + ('', ''), + ('', ''), + ('', ''), + ('', ''), + ('', ''), + ('', ''), + ('', ''), + ('', '') ]) def test_normalize_keystr(orig, normalized): - expected = keyutils.KeySequence.parse(normalized) - assert keyutils.KeySequence.parse(orig) == expected + assert str(keyutils.KeySequence.parse(orig)) == normalized @pytest.mark.parametrize('key, printable', [ @@ -194,7 +182,7 @@ def test_normalize_keystr(orig, normalized): (Qt.Key_Enter, False), (Qt.Key_X | Qt.ControlModifier, False), # Wrong usage - (Qt.Key_Space, True), + (Qt.Key_Space, True), # FIXME broken with upper/lower! (Qt.Key_ydiaeresis, True), (Qt.Key_X, True), ]) From 7f8508a367fdbd218b5e50ee018c611ca4030f7a Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sat, 3 Mar 2018 23:00:02 +0100 Subject: [PATCH 118/135] Change the way Space keybindings are handled Using it as " " in a keystring won't work anymore, but instead and does. --- qutebrowser/keyinput/keyutils.py | 4 ++-- tests/unit/keyinput/test_keyutils.py | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/qutebrowser/keyinput/keyutils.py b/qutebrowser/keyinput/keyutils.py index 61e3e53b4..3518e53dd 100644 --- a/qutebrowser/keyinput/keyutils.py +++ b/qutebrowser/keyinput/keyutils.py @@ -40,7 +40,7 @@ _MODIFIER_MAP = { def is_printable(key): - return key <= 0xff + return key <= 0xff and key != Qt.Key_Space def is_modifier_key(key): @@ -221,7 +221,7 @@ class KeyInfo: modifiers &= ~_MODIFIER_MAP[self.key] elif is_printable(self.key): # "normal" binding - assert len(key_string) == 1 or self.key == Qt.Key_Space, key_string + assert len(key_string) == 1, key_string if self.modifiers == Qt.ShiftModifier: return key_string.upper() elif self.modifiers == Qt.NoModifier: diff --git a/tests/unit/keyinput/test_keyutils.py b/tests/unit/keyinput/test_keyutils.py index 7fc6cc12c..2e595c19a 100644 --- a/tests/unit/keyinput/test_keyutils.py +++ b/tests/unit/keyinput/test_keyutils.py @@ -122,6 +122,8 @@ class TestKeyToString: (Qt.Key_A, Qt.NoModifier, 'a'), (Qt.Key_A, Qt.ShiftModifier, 'A'), + (Qt.Key_Space, Qt.NoModifier, ''), + (Qt.Key_Space, Qt.ShiftModifier, ''), (Qt.Key_Tab, Qt.ShiftModifier, ''), (Qt.Key_A, Qt.ControlModifier, ''), (Qt.Key_A, Qt.ControlModifier | Qt.ShiftModifier, ''), @@ -180,9 +182,9 @@ def test_normalize_keystr(orig, normalized): (Qt.Key_Backspace, False), (Qt.Key_Return, False), (Qt.Key_Enter, False), + (Qt.Key_Space, False), (Qt.Key_X | Qt.ControlModifier, False), # Wrong usage - (Qt.Key_Space, True), # FIXME broken with upper/lower! (Qt.Key_ydiaeresis, True), (Qt.Key_X, True), ]) From 3c9e8ff9ab6cec113682837348c77e963ec94256 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sat, 3 Mar 2018 23:22:03 +0100 Subject: [PATCH 119/135] Test and fix keyutils._parse_keystring --- qutebrowser/keyinput/keyutils.py | 12 ++++++++---- tests/unit/keyinput/test_keyutils.py | 25 +++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/qutebrowser/keyinput/keyutils.py b/qutebrowser/keyinput/keyutils.py index 3518e53dd..1741fbe33 100644 --- a/qutebrowser/keyinput/keyutils.py +++ b/qutebrowser/keyinput/keyutils.py @@ -151,10 +151,14 @@ def _parse_keystring(keystr): special = False for c in keystr: if c == '>': - assert special - yield _normalize_keystr(key) - key = '' - special = False + if special: + yield _normalize_keystr(key) + key = '' + special = False + else: + yield '>' + for c in key: + yield 'Shift+' + c if c.isupper() else c elif c == '<': special = True elif special: diff --git a/tests/unit/keyinput/test_keyutils.py b/tests/unit/keyinput/test_keyutils.py index 2e595c19a..f8b8382cd 100644 --- a/tests/unit/keyinput/test_keyutils.py +++ b/tests/unit/keyinput/test_keyutils.py @@ -138,6 +138,31 @@ def test_key_info_str(key, modifiers, expected): assert str(keyutils.KeyInfo(key, modifiers)) == expected +@pytest.mark.parametrize('keystr, expected', [ + ('foo', "Could not parse 'foo': error"), + (None, "Could not parse keystring: error"), +]) +def test_key_parse_error(keystr, expected): + exc = keyutils.KeyParseError(keystr, "error") + assert str(exc) == expected + + +@pytest.mark.parametrize('keystr, parts', [ + ('a', ['a']), + ('ab', ['a', 'b']), + ('a<', ['a', '<']), + ('a>', ['a', '>']), + ('a', ['>', 'a']), + ('aA', ['a', 'Shift+A']), + ('ab', ['a', 'ctrl+a', 'b']), + ('a', ['ctrl+a', 'a']), + ('a', ['a', 'ctrl+a']), +]) +def test_parse_keystr(keystr, parts): + assert list(keyutils._parse_keystring(keystr)) == parts + + @pytest.mark.parametrize('keystr, expected', [ ('', keyutils.KeySequence(Qt.ControlModifier | Qt.Key_X)), ('', keyutils.KeySequence(Qt.MetaModifier | Qt.Key_X)), From fb7c75a09092c5f7980f01d2af97f5a8c0030abe Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sat, 3 Mar 2018 23:29:16 +0100 Subject: [PATCH 120/135] Improve keyutils tests --- qutebrowser/keyinput/keyutils.py | 3 +-- tests/unit/keyinput/test_keyutils.py | 20 ++++++++++++++++++-- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/qutebrowser/keyinput/keyutils.py b/qutebrowser/keyinput/keyutils.py index 1741fbe33..6dbebfef0 100644 --- a/qutebrowser/keyinput/keyutils.py +++ b/qutebrowser/keyinput/keyutils.py @@ -157,8 +157,7 @@ def _parse_keystring(keystr): special = False else: yield '>' - for c in key: - yield 'Shift+' + c if c.isupper() else c + assert not key, key elif c == '<': special = True elif special: diff --git a/tests/unit/keyinput/test_keyutils.py b/tests/unit/keyinput/test_keyutils.py index f8b8382cd..b9d770c5c 100644 --- a/tests/unit/keyinput/test_keyutils.py +++ b/tests/unit/keyinput/test_keyutils.py @@ -18,7 +18,8 @@ # along with qutebrowser. If not, see . import pytest -from PyQt5.QtCore import Qt, pyqtSignal +from PyQt5.QtCore import Qt, QEvent, pyqtSignal +from PyQt5.QtGui import QKeyEvent from PyQt5.QtWidgets import QWidget from tests.unit.keyinput import key_data @@ -112,7 +113,7 @@ class TestKeyToString: assert keyutils._key_to_string(qt_key.member) == qt_key.name def test_missing(self, monkeypatch): - monkeypatch.delattr(keyutils.Qt, 'Key_Blue') + monkeypatch.delattr(keyutils.Qt, 'Key_AltGr') # We don't want to test the key which is actually missing - we only # want to know if the mapping still behaves properly. assert keyutils._key_to_string(Qt.Key_A) == 'A' @@ -199,6 +200,21 @@ def test_normalize_keystr(orig, normalized): assert str(keyutils.KeySequence.parse(orig)) == normalized +def test_key_info_from_event(): + ev = QKeyEvent(QEvent.KeyPress, Qt.Key_A, Qt.ShiftModifier, 'A') + info = keyutils.KeyInfo.from_event(ev) + assert info.key == Qt.Key_A + assert info.modifiers == Qt.ShiftModifier + + +def test_key_info_to_event(): + info = keyutils.KeyInfo(Qt.Key_A, Qt.ShiftModifier) + ev = info.to_event() + assert ev.key() == Qt.Key_A + assert ev.modifiers() == Qt.ShiftModifier + assert ev.text() == 'A' + + @pytest.mark.parametrize('key, printable', [ (Qt.Key_Control, False), (Qt.Key_Escape, False), From 3649a368692c0ebae5626bc6f161d147dea0b948 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 4 Mar 2018 17:37:07 +0100 Subject: [PATCH 121/135] KeySequence: Add __le__/__ge__ --- qutebrowser/keyinput/keyutils.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/qutebrowser/keyinput/keyutils.py b/qutebrowser/keyinput/keyutils.py index 6dbebfef0..90b6dae96 100644 --- a/qutebrowser/keyinput/keyutils.py +++ b/qutebrowser/keyinput/keyutils.py @@ -318,6 +318,14 @@ class KeySequence: # 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 From 68db8d04adb2caf4a56475f9ea9b605e04105af1 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 4 Mar 2018 17:37:37 +0100 Subject: [PATCH 122/135] KeySequence: Make sure we got valid key codes --- qutebrowser/keyinput/keyutils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/qutebrowser/keyinput/keyutils.py b/qutebrowser/keyinput/keyutils.py index 90b6dae96..50041b75a 100644 --- a/qutebrowser/keyinput/keyutils.py +++ b/qutebrowser/keyinput/keyutils.py @@ -356,6 +356,7 @@ class KeySequence: def _validate(self, keystr=None): for info in self: + assert Qt.Key_Space <= info.key <= Qt.Key_unknown, info.key if info.key == Qt.Key_unknown: raise KeyParseError(keystr, "Got unknown key!") From 866c758660eef58783374823ed82d4d40345c302 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 4 Mar 2018 17:38:49 +0100 Subject: [PATCH 123/135] Add more KeySequence tests --- tests/unit/keyinput/test_keyutils.py | 177 ++++++++++++++++++++++----- 1 file changed, 146 insertions(+), 31 deletions(-) diff --git a/tests/unit/keyinput/test_keyutils.py b/tests/unit/keyinput/test_keyutils.py index b9d770c5c..0528618a8 100644 --- a/tests/unit/keyinput/test_keyutils.py +++ b/tests/unit/keyinput/test_keyutils.py @@ -17,6 +17,8 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see . +import operator + import pytest from PyQt5.QtCore import Qt, QEvent, pyqtSignal from PyQt5.QtGui import QKeyEvent @@ -164,40 +166,153 @@ def test_parse_keystr(keystr, parts): assert list(keyutils._parse_keystring(keystr)) == parts -@pytest.mark.parametrize('keystr, expected', [ - ('', keyutils.KeySequence(Qt.ControlModifier | Qt.Key_X)), - ('', keyutils.KeySequence(Qt.MetaModifier | Qt.Key_X)), - ('', - keyutils.KeySequence(Qt.ControlModifier | Qt.AltModifier | Qt.Key_Y)), - ('x', keyutils.KeySequence(Qt.Key_X)), - ('X', keyutils.KeySequence(Qt.ShiftModifier | Qt.Key_X)), - ('', keyutils.KeySequence(Qt.Key_Escape)), - ('xyz', keyutils.KeySequence(Qt.Key_X, Qt.Key_Y, Qt.Key_Z)), - ('', keyutils.KeySequence(Qt.ControlModifier | Qt.Key_X, - Qt.MetaModifier | Qt.Key_Y)), - ('', keyutils.KeyParseError), - ('\U00010000', keyutils.KeyParseError), -]) -def test_parse(keystr, expected): - if expected is keyutils.KeyParseError: +class TestKeySequence: + + def test_init(self): + seq = keyutils.KeySequence(Qt.Key_A, Qt.Key_B, Qt.Key_C, Qt.Key_D, + Qt.Key_E) + assert len(seq._sequences) == 2 + assert len(seq._sequences[0]) == 4 + assert len(seq._sequences[1]) == 1 + + def test_init_empty(self): + seq = keyutils.KeySequence() + assert not seq + + def test_init_unknown(self): with pytest.raises(keyutils.KeyParseError): - keyutils.KeySequence.parse(keystr) - else: - assert keyutils.KeySequence.parse(keystr) == expected + keyutils.KeySequence(Qt.Key_unknown) + + def test_init_invalid(self): + with pytest.raises(AssertionError): + keyutils.KeySequence(-1) + + @pytest.mark.parametrize('orig, normalized', [ + ('', ''), + ('', ''), + ('', ''), + ('', ''), + ('', ''), + ('', ''), + ('', ''), + ('', ''), + ('b', 'ab'), + ]) + def test_str_normalization(self, orig, normalized): + assert str(keyutils.KeySequence.parse(orig)) == normalized + + def test_iter(self): + seq = keyutils.KeySequence(Qt.Key_A | Qt.ControlModifier, + Qt.Key_B | Qt.ShiftModifier, + Qt.Key_C, + Qt.Key_D, + Qt.Key_E) + expected = [keyutils.KeyInfo(Qt.Key_A, Qt.ControlModifier), + keyutils.KeyInfo(Qt.Key_B, Qt.ShiftModifier), + keyutils.KeyInfo(Qt.Key_C, Qt.NoModifier), + keyutils.KeyInfo(Qt.Key_D, Qt.NoModifier), + keyutils.KeyInfo(Qt.Key_E, Qt.NoModifier)] + assert list(seq) == expected + + def test_repr(self): + seq = keyutils.KeySequence(Qt.Key_A | Qt.ControlModifier, + Qt.Key_B | Qt.ShiftModifier) + assert repr(seq) == ("") + + @pytest.mark.parametrize('sequences, expected', [ + (['a', ''], ['', 'a']), + (['abcdf', 'abcd', 'abcde'], ['abcd', 'abcde', 'abcdf']), + ]) + def test_sorting(self, sequences, expected): + result = sorted(keyutils.KeySequence.parse(seq) for seq in sequences) + expected_result = [keyutils.KeySequence.parse(seq) for seq in expected] + assert result == expected_result + + @pytest.mark.parametrize('seq1, seq2, op, result', [ + ('a', 'a', operator.eq, True), + ('a', '', operator.eq, True), + ('a', '', operator.eq, False), + ('a', 'b', operator.lt, True), + ('a', 'b', operator.le, True), + ]) + def test_operators(self, seq1, seq2, op, result): + seq1 = keyutils.KeySequence.parse(seq1) + seq2 = keyutils.KeySequence.parse(seq2) + assert op(seq1, seq2) == result + + opposite = { + operator.lt: operator.ge, + operator.gt: operator.le, + operator.le: operator.gt, + operator.ge: operator.lt, + operator.eq: operator.ne, + operator.ne: operator.eq, + } + assert opposite[op](seq1, seq2) != result + + @pytest.mark.parametrize('seq1, seq2, equal', [ + ('a', 'a', True), + ('a', 'A', False), + ('a', '', True), + ('abcd', 'abcde', False), + ]) + def test_hash(self, seq1, seq2, equal): + seq1 = keyutils.KeySequence.parse(seq1) + seq2 = keyutils.KeySequence.parse(seq2) + assert (hash(seq1) == hash(seq2)) == equal + + @pytest.mark.parametrize('seq, length', [ + ('', 0), + ('a', 1), + ('A', 1), + ('', 1), + ('abcde', 5) + ]) + def test_len(self, seq, length): + assert len(keyutils.KeySequence.parse(seq)) == length + + def test_bool(self): + seq1 = keyutils.KeySequence.parse('abcd') + seq2 = keyutils.KeySequence() + assert seq1 + assert not seq2 + + def test_getitem(self): + seq = keyutils.KeySequence.parse('ab') + expected = keyutils.KeyInfo(Qt.Key_B, Qt.NoModifier) + assert seq[1] == expected + + def test_getitem_slice(self): + s1 = 'abcdef' + s2 = 'de' + seq = keyutils.KeySequence.parse(s1) + expected = keyutils.KeySequence.parse(s2) + assert s1[3:5] == s2 + assert seq[3:5] == expected -@pytest.mark.parametrize('orig, normalized', [ - ('', ''), - ('', ''), - ('', ''), - ('', ''), - ('', ''), - ('', ''), - ('', ''), - ('', '') -]) -def test_normalize_keystr(orig, normalized): - assert str(keyutils.KeySequence.parse(orig)) == normalized + @pytest.mark.parametrize('keystr, expected', [ + ('', keyutils.KeySequence(Qt.ControlModifier | Qt.Key_X)), + ('', keyutils.KeySequence(Qt.MetaModifier | Qt.Key_X)), + ('', + keyutils.KeySequence(Qt.ControlModifier | Qt.AltModifier | Qt.Key_Y)), + ('x', keyutils.KeySequence(Qt.Key_X)), + ('X', keyutils.KeySequence(Qt.ShiftModifier | Qt.Key_X)), + ('', keyutils.KeySequence(Qt.Key_Escape)), + ('xyz', keyutils.KeySequence(Qt.Key_X, Qt.Key_Y, Qt.Key_Z)), + ('', + keyutils.KeySequence(Qt.ControlModifier | Qt.Key_X, + Qt.MetaModifier | Qt.Key_Y)), + ('', keyutils.KeyParseError), + ('\U00010000', keyutils.KeyParseError), + ]) + def test_parse(self, keystr, expected): + if expected is keyutils.KeyParseError: + with pytest.raises(keyutils.KeyParseError): + keyutils.KeySequence.parse(keystr) + else: + assert keyutils.KeySequence.parse(keystr) == expected def test_key_info_from_event(): From 8da878c77cb217f14057cea040e8406536f2282b Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 4 Mar 2018 17:46:26 +0100 Subject: [PATCH 124/135] Make KeySequence.matchs() work correctly --- qutebrowser/keyinput/keyutils.py | 35 +++++++++++++++++++++++----- tests/unit/keyinput/test_keyutils.py | 30 +++++++++++++++++++++++- 2 files changed, 58 insertions(+), 7 deletions(-) diff --git a/qutebrowser/keyinput/keyutils.py b/qutebrowser/keyinput/keyutils.py index 50041b75a..ddc78b2b4 100644 --- a/qutebrowser/keyinput/keyutils.py +++ b/qutebrowser/keyinput/keyutils.py @@ -361,15 +361,38 @@ class KeySequence: raise KeyParseError(keystr, "Got unknown key!") def matches(self, other): - """Check whether the given KeySequence matches with this one.""" + """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 - assert self._sequences - assert other._sequences - for seq1, seq2 in zip(self._sequences, other._sequences): - match = seq1.matches(seq2) + + 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 - return QKeySequence.ExactMatch + + # 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: + assert False, (self, other) def append_event(self, ev): """Create a new KeySequence object with the given QKeyEvent added. diff --git a/tests/unit/keyinput/test_keyutils.py b/tests/unit/keyinput/test_keyutils.py index 0528618a8..0f13c762d 100644 --- a/tests/unit/keyinput/test_keyutils.py +++ b/tests/unit/keyinput/test_keyutils.py @@ -21,7 +21,7 @@ import operator import pytest from PyQt5.QtCore import Qt, QEvent, pyqtSignal -from PyQt5.QtGui import QKeyEvent +from PyQt5.QtGui import QKeyEvent, QKeySequence from PyQt5.QtWidgets import QWidget from tests.unit.keyinput import key_data @@ -291,6 +291,34 @@ class TestKeySequence: assert s1[3:5] == s2 assert seq[3:5] == expected + @pytest.mark.parametrize('entered, configured, expected', [ + # config: abcd + ('abc', 'abcd', QKeySequence.PartialMatch), + ('abcd', 'abcd', QKeySequence.ExactMatch), + ('ax', 'abcd', QKeySequence.NoMatch), + ('abcdef', 'abcd', QKeySequence.NoMatch), + + # config: abcd ef + ('abc', 'abcdef', QKeySequence.PartialMatch), + ('abcde', 'abcdef', QKeySequence.PartialMatch), + ('abcd', 'abcdef', QKeySequence.PartialMatch), + ('abcdx', 'abcdef', QKeySequence.NoMatch), + ('ax', 'abcdef', QKeySequence.NoMatch), + ('abcdefg', 'abcdef', QKeySequence.NoMatch), + ('abcdef', 'abcdef', QKeySequence.ExactMatch), + + # other examples + ('ab', 'a', QKeySequence.NoMatch), + + # empty strings + ('', '', QKeySequence.ExactMatch), + ('', 'a', QKeySequence.PartialMatch), + ('a', '', QKeySequence.NoMatch), + ]) + def test_matches(self, entered, configured, expected): + entered = keyutils.KeySequence.parse(entered) + configured = keyutils.KeySequence.parse(configured) + assert entered.matches(configured) == expected @pytest.mark.parametrize('keystr, expected', [ ('', keyutils.KeySequence(Qt.ControlModifier | Qt.Key_X)), From 2be7db29edd80c3020c9f1d91c4d11a98eb4c8d2 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 4 Mar 2018 19:12:24 +0100 Subject: [PATCH 125/135] 100% coverage for keyinput.keyutils --- qutebrowser/keyinput/keyutils.py | 2 +- scripts/dev/check_coverage.py | 2 ++ tests/unit/keyinput/test_keyutils.py | 24 ++++++++++++++++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/qutebrowser/keyinput/keyutils.py b/qutebrowser/keyinput/keyutils.py index ddc78b2b4..fd973eb80 100644 --- a/qutebrowser/keyinput/keyutils.py +++ b/qutebrowser/keyinput/keyutils.py @@ -391,7 +391,7 @@ class KeySequence: return QKeySequence.ExactMatch elif len(self._sequences) < len(other._sequences): return QKeySequence.PartialMatch - else: + else: # pragma: no cover assert False, (self, other) def append_event(self, ev): diff --git a/scripts/dev/check_coverage.py b/scripts/dev/check_coverage.py index 0e79a6e02..b40188429 100644 --- a/scripts/dev/check_coverage.py +++ b/scripts/dev/check_coverage.py @@ -86,6 +86,8 @@ PERFECT_FILES = [ ('tests/unit/keyinput/test_basekeyparser.py', 'keyinput/basekeyparser.py'), + ('tests/unit/keyinput/test_keyutils.py', + 'keyinput/keyutils.py'), ('tests/unit/misc/test_autoupdate.py', 'misc/autoupdate.py'), diff --git a/tests/unit/keyinput/test_keyutils.py b/tests/unit/keyinput/test_keyutils.py index 0f13c762d..3f03f883d 100644 --- a/tests/unit/keyinput/test_keyutils.py +++ b/tests/unit/keyinput/test_keyutils.py @@ -320,6 +320,30 @@ class TestKeySequence: configured = keyutils.KeySequence.parse(configured) assert entered.matches(configured) == expected + @pytest.mark.parametrize('old, key, modifiers, text, expected', [ + ('a', Qt.Key_B, Qt.NoModifier, 'b', 'ab'), + ('a', Qt.Key_B, Qt.ShiftModifier, 'B', 'aB'), + ('a', Qt.Key_B, Qt.ControlModifier | Qt.ShiftModifier, 'B', + 'a'), + + # Modifier stripping with symbols + ('', Qt.Key_Colon, Qt.NoModifier, ':', ':'), + ('', Qt.Key_Colon, Qt.ShiftModifier, ':', ':'), + ('', Qt.Key_Colon, Qt.ControlModifier | Qt.ShiftModifier, ':', + ''), + + # Handling of Backtab + ('', Qt.Key_Backtab, Qt.NoModifier, '', ''), + ('', Qt.Key_Backtab, Qt.ShiftModifier, '', ''), + ('', Qt.Key_Backtab, Qt.ControlModifier | Qt.ShiftModifier, '', + ''), + ]) + def test_append_event(self, old, key, modifiers, text, expected): + seq = keyutils.KeySequence.parse(old) + event = QKeyEvent(QKeyEvent.KeyPress, key, modifiers, text) + new = seq.append_event(event) + assert new == keyutils.KeySequence.parse(expected) + @pytest.mark.parametrize('keystr, expected', [ ('', keyutils.KeySequence(Qt.ControlModifier | Qt.Key_X)), ('', keyutils.KeySequence(Qt.MetaModifier | Qt.Key_X)), From f85e69ec7762a0fb7106d5f73a77438d8492b4bc Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 4 Mar 2018 19:38:15 +0100 Subject: [PATCH 126/135] Refactor other keyinput tests --- tests/helpers/fixtures.py | 18 +-- tests/unit/keyinput/conftest.py | 18 ++- tests/unit/keyinput/test_basekeyparser.py | 137 ++++++++-------------- tests/unit/keyinput/test_modeman.py | 15 ++- tests/unit/keyinput/test_modeparsers.py | 12 +- 5 files changed, 77 insertions(+), 123 deletions(-) diff --git a/tests/helpers/fixtures.py b/tests/helpers/fixtures.py index 193a40a8a..d30514f83 100644 --- a/tests/helpers/fixtures.py +++ b/tests/helpers/fixtures.py @@ -35,8 +35,7 @@ import types import attr import pytest import py.path # pylint: disable=no-name-in-module -from PyQt5.QtCore import QEvent, QSize, Qt -from PyQt5.QtGui import QKeyEvent +from PyQt5.QtCore import QSize, Qt from PyQt5.QtWidgets import QWidget, QHBoxLayout, QVBoxLayout from PyQt5.QtNetwork import QNetworkCookieJar @@ -354,21 +353,6 @@ def webframe(webpage): return webpage.mainFrame() -@pytest.fixture -def fake_keyevent_factory(): - """Fixture that when called will return a mock instance of a QKeyEvent.""" - def fake_keyevent(key, modifiers=0, text='', typ=QEvent.KeyPress): - """Generate a new fake QKeyPressEvent.""" - evtmock = unittest.mock.create_autospec(QKeyEvent, instance=True) - evtmock.key.return_value = key - evtmock.modifiers.return_value = modifiers - evtmock.text.return_value = text - evtmock.type.return_value = typ - return evtmock - - return fake_keyevent - - @pytest.fixture def cookiejar_and_cache(stubs): """Fixture providing a fake cookie jar and cache.""" diff --git a/tests/unit/keyinput/conftest.py b/tests/unit/keyinput/conftest.py index f78ed61ad..96d07f744 100644 --- a/tests/unit/keyinput/conftest.py +++ b/tests/unit/keyinput/conftest.py @@ -21,6 +21,11 @@ import pytest +from PyQt5.QtCore import QEvent, Qt +from PyQt5.QtGui import QKeyEvent + +from qutebrowser.keyinput import keyutils + BINDINGS = {'prompt': {'': 'message-info ctrla', 'a': 'message-info a', @@ -33,8 +38,6 @@ BINDINGS = {'prompt': {'': 'message-info ctrla', '': 'message-info ctrlx'}, 'normal': {'a': 'message-info a', 'ba': 'message-info ba'}} MAPPINGS = { - '': 'a', - '': '', 'x': 'a', 'b': 'a', } @@ -46,3 +49,14 @@ def keyinput_bindings(config_stub, key_config_stub): config_stub.val.bindings.default = {} config_stub.val.bindings.commands = dict(BINDINGS) config_stub.val.bindings.key_mappings = dict(MAPPINGS) + + +@pytest.fixture +def fake_keyevent(): + """Fixture that when called will return a mock instance of a QKeyEvent.""" + def func(key, modifiers=Qt.NoModifier, typ=QEvent.KeyPress): + """Generate a new fake QKeyPressEvent.""" + text = keyutils.KeyInfo(key, modifiers).text() + return QKeyEvent(QKeyEvent.KeyPress, key, modifiers, text) + + return func diff --git a/tests/unit/keyinput/test_basekeyparser.py b/tests/unit/keyinput/test_basekeyparser.py index 872709d6e..95860399d 100644 --- a/tests/unit/keyinput/test_basekeyparser.py +++ b/tests/unit/keyinput/test_basekeyparser.py @@ -42,15 +42,15 @@ def keyparser(key_config_stub): @pytest.fixture -def handle_text(fake_keyevent_factory, keyparser): +def handle_text(fake_keyevent, keyparser): """Helper function to handle multiple fake keypresses. Automatically uses the keyparser of the current test via the keyparser fixture. """ def func(*args): - for enumval, text in args: - keyparser.handle(fake_keyevent_factory(enumval, text=text)) + for enumval in args: + keyparser.handle(fake_keyevent(enumval)) return func @@ -76,6 +76,7 @@ class TestDebugLog: ('10g', True, '10', 'g'), ('10e4g', True, '4', 'g'), ('g', True, '', 'g'), + ('0', True, '', ''), ('10g', False, '', 'g'), ]) def test_split_count(config_stub, key_config_stub, @@ -140,115 +141,65 @@ class TestReadConfig: assert (keyseq('new') in keyparser.bindings) == expected -class TestSpecialKeys: - - """Check execute() with special keys.""" +class TestHandle: @pytest.fixture(autouse=True) def read_config(self, keyinput_bindings, keyparser): keyparser._read_config('prompt') - def test_valid_key(self, fake_keyevent_factory, keyparser): + def test_valid_key(self, fake_keyevent, keyparser): modifier = Qt.MetaModifier if utils.is_mac else Qt.ControlModifier - keyparser.handle(fake_keyevent_factory(Qt.Key_A, modifier)) - keyparser.handle(fake_keyevent_factory(Qt.Key_X, modifier)) - keyparser.execute.assert_called_once_with('message-info ctrla', None) - - def test_valid_key_count(self, fake_keyevent_factory, keyparser): - modifier = Qt.MetaModifier if utils.is_mac else Qt.ControlModifier - keyparser.handle(fake_keyevent_factory(Qt.Key_5, text='5')) - keyparser.handle(fake_keyevent_factory(Qt.Key_A, modifier, text='A')) - keyparser.execute.assert_called_once_with('message-info ctrla', 5) - - def test_invalid_key(self, fake_keyevent_factory, keyparser): - keyparser.handle(fake_keyevent_factory( - Qt.Key_A, (Qt.ControlModifier | Qt.AltModifier))) - assert not keyparser.execute.called - - def test_only_modifiers(self, monkeypatch, fake_keyevent_factory, - keyparser): - monkeypatch.setattr(keyutils.KeyInfo, '__str__', lambda _self: '') - keyparser.handle(fake_keyevent_factory(Qt.Key_Shift, Qt.NoModifier)) - assert not keyparser.execute.called - - def test_mapping(self, config_stub, fake_keyevent_factory, keyparser): - modifier = Qt.MetaModifier if utils.is_mac else Qt.ControlModifier - - keyparser.handle(fake_keyevent_factory(Qt.Key_B, modifier)) - keyparser.execute.assert_called_once_with('message-info ctrla', None) - - def test_binding_and_mapping(self, config_stub, fake_keyevent_factory, - keyparser): - """with a conflicting binding/mapping, the binding should win.""" - modifier = Qt.MetaModifier if utils.is_mac else Qt.ControlModifier - - keyparser.handle(fake_keyevent_factory(Qt.Key_A, modifier)) - keyparser.execute.assert_called_once_with('message-info ctrla', None) - - -class TestKeyChain: - - """Test execute() with keychain support.""" - - @pytest.fixture(autouse=True) - def read_config(self, keyinput_bindings, keyparser): - keyparser._read_config('prompt') - - def test_valid_special_key(self, fake_keyevent_factory, keyparser): - if utils.is_mac: - modifier = Qt.MetaModifier - else: - modifier = Qt.ControlModifier - keyparser.handle(fake_keyevent_factory(Qt.Key_A, modifier)) - keyparser.handle(fake_keyevent_factory(Qt.Key_X, modifier)) + keyparser.handle(fake_keyevent(Qt.Key_A, modifier)) + keyparser.handle(fake_keyevent(Qt.Key_X, modifier)) keyparser.execute.assert_called_once_with('message-info ctrla', None) assert not keyparser._sequence - def test_invalid_special_key(self, fake_keyevent_factory, keyparser): - keyparser.handle(fake_keyevent_factory( - Qt.Key_A, (Qt.ControlModifier | Qt.AltModifier))) + def test_valid_key_count(self, fake_keyevent, keyparser): + modifier = Qt.MetaModifier if utils.is_mac else Qt.ControlModifier + keyparser.handle(fake_keyevent(Qt.Key_5)) + keyparser.handle(fake_keyevent(Qt.Key_A, modifier)) + keyparser.execute.assert_called_once_with('message-info ctrla', 5) + + @pytest.mark.parametrize('keys', [ + [(Qt.Key_B, Qt.NoModifier), (Qt.Key_C, Qt.NoModifier)], + [(Qt.Key_A, Qt.ControlModifier | Qt.AltModifier)], + # Only modifier + [(Qt.Key_Shift, Qt.ShiftModifier)], + ]) + def test_invalid_keys(self, fake_keyevent, keyparser, keys): + for key, modifiers in keys: + keyparser.handle(fake_keyevent(key, modifiers)) assert not keyparser.execute.called assert not keyparser._sequence def test_valid_keychain(self, handle_text, keyparser): # Press 'x' which is ignored because of no match - handle_text((Qt.Key_X, 'x'), + handle_text(Qt.Key_X, # Then start the real chain - (Qt.Key_B, 'b'), (Qt.Key_A, 'a')) + Qt.Key_B, Qt.Key_A) keyparser.execute.assert_called_with('message-info ba', None) assert not keyparser._sequence def test_0_press(self, handle_text, keyparser): - handle_text((Qt.Key_0, '0')) + handle_text(Qt.Key_0) keyparser.execute.assert_called_once_with('message-info 0', None) assert not keyparser._sequence - def test_ambiguous_keychain(self, handle_text, keyparser): - handle_text((Qt.Key_A, 'a')) - assert keyparser.execute.called - - def test_invalid_keychain(self, handle_text, keyparser): - handle_text((Qt.Key_B, 'b')) - handle_text((Qt.Key_C, 'c')) - assert not keyparser._sequence - def test_mapping(self, config_stub, handle_text, keyparser): - handle_text((Qt.Key_X, 'x')) + handle_text(Qt.Key_X) keyparser.execute.assert_called_once_with('message-info a', None) def test_binding_and_mapping(self, config_stub, handle_text, keyparser): """with a conflicting binding/mapping, the binding should win.""" - handle_text((Qt.Key_B, 'b')) + handle_text(Qt.Key_B) assert not keyparser.execute.called - def test_binding_with_shift(self, keyparser, fake_keyevent_factory): + def test_binding_with_shift(self, keyparser, fake_keyevent): """Simulate a binding which involves shift.""" - keyparser.handle( - fake_keyevent_factory(Qt.Key_Y, text='y')) - keyparser.handle( - fake_keyevent_factory(Qt.Key_Shift, Qt.ShiftModifier, text='')) - keyparser.handle( - fake_keyevent_factory(Qt.Key_Y, Qt.ShiftModifier, text='Y')) + for key, modifiers in [(Qt.Key_Y, Qt.NoModifier), + (Qt.Key_Shift, Qt.ShiftModifier), + (Qt.Key_Y, Qt.ShiftModifier)]: + keyparser.handle(fake_keyevent(key, modifiers)) keyparser.execute.assert_called_once_with('yank -s', None) @@ -263,32 +214,29 @@ class TestCount: def test_no_count(self, handle_text, keyparser): """Test with no count added.""" - handle_text((Qt.Key_B, 'b'), (Qt.Key_A, 'a')) + handle_text(Qt.Key_B, Qt.Key_A) keyparser.execute.assert_called_once_with('message-info ba', None) assert not keyparser._sequence def test_count_0(self, handle_text, keyparser): - handle_text((Qt.Key_0, '0'), (Qt.Key_B, 'b'), (Qt.Key_A, 'a')) + handle_text(Qt.Key_0, Qt.Key_B, Qt.Key_A) calls = [mock.call('message-info 0', None), mock.call('message-info ba', None)] keyparser.execute.assert_has_calls(calls) assert not keyparser._sequence def test_count_42(self, handle_text, keyparser): - handle_text((Qt.Key_4, '4'), (Qt.Key_2, '2'), (Qt.Key_B, 'b'), - (Qt.Key_A, 'a')) + handle_text(Qt.Key_4, Qt.Key_2, Qt.Key_B, Qt.Key_A) keyparser.execute.assert_called_once_with('message-info ba', 42) assert not keyparser._sequence def test_count_42_invalid(self, handle_text, keyparser): # Invalid call with ccx gets ignored - handle_text((Qt.Key_4, '4'), (Qt.Key_2, '2'), (Qt.Key_C, 'c'), - (Qt.Key_C, 'c'), (Qt.Key_X, 'x')) + handle_text(Qt.Key_4, Qt.Key_2, Qt.Key_C, Qt.Key_C, Qt.Key_X) assert not keyparser.execute.called assert not keyparser._sequence # Valid call with ccc gets the correct count - handle_text((Qt.Key_2, '2'), (Qt.Key_3, '3'), (Qt.Key_C, 'c'), - (Qt.Key_C, 'c'), (Qt.Key_C, 'c')) + handle_text(Qt.Key_2, Qt.Key_3, Qt.Key_C, Qt.Key_C, Qt.Key_C) keyparser.execute.assert_called_once_with('message-info ccc', 23) assert not keyparser._sequence @@ -296,6 +244,15 @@ class TestCount: def test_clear_keystring(qtbot, keyparser): """Test that the keystring is cleared and the signal is emitted.""" keyparser._sequence = keyseq('test') + keyparser._count = '23' with qtbot.waitSignal(keyparser.keystring_updated): keyparser.clear_keystring() assert not keyparser._sequence + assert not keyparser._count + + +def test_clear_keystring_empty(qtbot, keyparser): + """Test that no signal is emitted when clearing an empty keystring..""" + keyparser._sequence = keyseq('') + with qtbot.assert_not_emitted(keyparser.keystring_updated): + keyparser.clear_keystring() diff --git a/tests/unit/keyinput/test_modeman.py b/tests/unit/keyinput/test_modeman.py index 221b675be..de9671961 100644 --- a/tests/unit/keyinput/test_modeman.py +++ b/tests/unit/keyinput/test_modeman.py @@ -44,15 +44,14 @@ def modeman(mode_manager): return mode_manager -@pytest.mark.parametrize('key, modifiers, text, filtered', [ - (Qt.Key_A, Qt.NoModifier, 'a', True), - (Qt.Key_Up, Qt.NoModifier, '', False), +@pytest.mark.parametrize('key, modifiers, filtered', [ + (Qt.Key_A, Qt.NoModifier, True), + (Qt.Key_Up, Qt.NoModifier, False), # https://github.com/qutebrowser/qutebrowser/issues/1207 - (Qt.Key_A, Qt.ShiftModifier, 'A', True), - (Qt.Key_A, Qt.ShiftModifier | Qt.ControlModifier, 'x', False), + (Qt.Key_A, Qt.ShiftModifier, True), + (Qt.Key_A, Qt.ShiftModifier | Qt.ControlModifier, False), ]) -def test_non_alphanumeric(key, modifiers, text, filtered, - fake_keyevent_factory, modeman): +def test_non_alphanumeric(key, modifiers, filtered, fake_keyevent, modeman): """Make sure non-alphanumeric keys are passed through correctly.""" - evt = fake_keyevent_factory(key=key, modifiers=modifiers, text=text) + evt = fake_keyevent(key=key, modifiers=modifiers) assert modeman.eventFilter(evt) == filtered diff --git a/tests/unit/keyinput/test_modeparsers.py b/tests/unit/keyinput/test_modeparsers.py index d53328b7e..c3be9d70f 100644 --- a/tests/unit/keyinput/test_modeparsers.py +++ b/tests/unit/keyinput/test_modeparsers.py @@ -49,25 +49,25 @@ class TestsNormalKeyParser: kp.execute = mock.Mock() return kp - def test_keychain(self, keyparser, fake_keyevent_factory): + def test_keychain(self, keyparser, fake_keyevent): """Test valid keychain.""" # Press 'x' which is ignored because of no match - keyparser.handle(fake_keyevent_factory(Qt.Key_X, text='x')) + keyparser.handle(fake_keyevent(Qt.Key_X)) # Then start the real chain - keyparser.handle(fake_keyevent_factory(Qt.Key_B, text='b')) - keyparser.handle(fake_keyevent_factory(Qt.Key_A, text='a')) + keyparser.handle(fake_keyevent(Qt.Key_B)) + keyparser.handle(fake_keyevent(Qt.Key_A)) keyparser.execute.assert_called_with('message-info ba', None) assert not keyparser._sequence def test_partial_keychain_timeout(self, keyparser, config_stub, - fake_keyevent_factory): + fake_keyevent): """Test partial keychain timeout.""" config_stub.val.input.partial_timeout = 100 timer = keyparser._partial_timer assert not timer.isActive() # Press 'b' for a partial match. # Then we check if the timer has been set up correctly - keyparser.handle(fake_keyevent_factory(Qt.Key_B, text='b')) + keyparser.handle(fake_keyevent(Qt.Key_B)) assert timer.isSingleShot() assert timer.interval() == 100 assert timer.isActive() From 58b7599152a523850e8bc699e311f6bc11de6e72 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 4 Mar 2018 19:40:03 +0100 Subject: [PATCH 127/135] Remove old fixme --- qutebrowser/config/configdata.yml | 1 - qutebrowser/keyinput/keyutils.py | 1 - 2 files changed, 2 deletions(-) diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml index 7be015fd5..306d744fe 100644 --- a/qutebrowser/config/configdata.yml +++ b/qutebrowser/config/configdata.yml @@ -2401,7 +2401,6 @@ bindings.default: : rl-backward-delete-char : rl-yank : leave-mode - # FIXME can we do migrations? yesno: : prompt-accept y: prompt-accept yes diff --git a/qutebrowser/keyinput/keyutils.py b/qutebrowser/keyinput/keyutils.py index fd973eb80..2da32349a 100644 --- a/qutebrowser/keyinput/keyutils.py +++ b/qutebrowser/keyinput/keyutils.py @@ -433,7 +433,6 @@ class KeySequence: def parse(cls, keystr): """Parse a keystring like or xyz and return a KeySequence.""" # pylint: disable=protected-access - # FIXME: test stuff like new = cls() strings = list(_parse_keystring(keystr)) for sub in utils.chunk(strings, cls._MAX_LEN): From d8bfe23c0dfabf84ddafba3175b0e07cde75fec6 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 4 Mar 2018 19:43:29 +0100 Subject: [PATCH 128/135] Fix lint --- qutebrowser/keyinput/keyutils.py | 22 +++++++++++----------- tests/unit/keyinput/test_keyutils.py | 3 +-- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/qutebrowser/keyinput/keyutils.py b/qutebrowser/keyinput/keyutils.py index 2da32349a..dd2668569 100644 --- a/qutebrowser/keyinput/keyutils.py +++ b/qutebrowser/keyinput/keyutils.py @@ -19,7 +19,6 @@ """Our own QKeySequence-like class and related utilities.""" -import collections import itertools import attr @@ -46,8 +45,8 @@ def is_printable(key): 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. + This only considers keys which are part of Qt::KeyboardModifiers, i.e. + which would interrupt a key chain like "yY" when handled. """ return key in _MODIFIER_MAP @@ -363,14 +362,15 @@ class KeySequence: 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. + 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. + # 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): @@ -385,14 +385,14 @@ class KeySequence: # 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 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: # pragma: no cover - assert False, (self, other) + 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. diff --git a/tests/unit/keyinput/test_keyutils.py b/tests/unit/keyinput/test_keyutils.py index 3f03f883d..a1e078521 100644 --- a/tests/unit/keyinput/test_keyutils.py +++ b/tests/unit/keyinput/test_keyutils.py @@ -25,7 +25,6 @@ from PyQt5.QtGui import QKeyEvent, QKeySequence from PyQt5.QtWidgets import QWidget from tests.unit.keyinput import key_data -from qutebrowser.utils import utils from qutebrowser.keyinput import keyutils @@ -348,7 +347,7 @@ class TestKeySequence: ('', keyutils.KeySequence(Qt.ControlModifier | Qt.Key_X)), ('', keyutils.KeySequence(Qt.MetaModifier | Qt.Key_X)), ('', - keyutils.KeySequence(Qt.ControlModifier | Qt.AltModifier | Qt.Key_Y)), + keyutils.KeySequence(Qt.ControlModifier | Qt.AltModifier | Qt.Key_Y)), ('x', keyutils.KeySequence(Qt.Key_X)), ('X', keyutils.KeySequence(Qt.ShiftModifier | Qt.Key_X)), ('', keyutils.KeySequence(Qt.Key_Escape)), From c9c0bc0bbd514b30674dee0c8a6592c3c0607a6f Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 4 Mar 2018 20:28:46 +0100 Subject: [PATCH 129/135] Update docs --- doc/help/commands.asciidoc | 6 ++++-- doc/help/settings.asciidoc | 2 +- qutebrowser/config/configcommands.py | 6 ++++-- qutebrowser/config/configdata.yml | 1 - qutebrowser/keyinput/modeparsers.py | 2 +- 5 files changed, 10 insertions(+), 7 deletions(-) diff --git a/doc/help/commands.asciidoc b/doc/help/commands.asciidoc index 2174e69ac..2fc8cd093 100644 --- a/doc/help/commands.asciidoc +++ b/doc/help/commands.asciidoc @@ -153,7 +153,8 @@ Bind a key to a command. If no command is given, show the current binding for the given key. Using :bind without any arguments opens a page showing all keybindings. ==== positional arguments -* +'key'+: The keychain or special key (inside `<...>`) to bind. +* +'key'+: The keychain to bind. Examples of valid keychains are `gC`, `` or `a`. + * +'command'+: The command to execute, with optional args. ==== optional arguments @@ -1316,7 +1317,8 @@ Syntax: +:unbind [*--mode* 'mode'] 'key'+ Unbind a keychain. ==== positional arguments -* +'key'+: The keychain or special key (inside <...>) to unbind. +* +'key'+: The keychain to unbind. See the help for `:bind` for the correct syntax for keychains. + ==== optional arguments * +*-m*+, +*--mode*+: A mode to unbind the key in (default: `normal`). See `:help bindings.commands` for the available modes. diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc index fc809b98e..216718de1 100644 --- a/doc/help/settings.asciidoc +++ b/doc/help/settings.asciidoc @@ -322,7 +322,7 @@ While it's possible to add bindings with this setting, it's recommended to use ` This setting is a dictionary containing mode names and dictionaries mapping keys to commands: `{mode: {key: command}}` If you want to map a key to another key, check the `bindings.key_mappings` setting instead. -For special keys (can't be part of a keychain), enclose them in `<`...`>`. For modifiers, you can use either `-` or `+` as delimiters, and these names: +For modifiers, you can use either `-` or `+` as delimiters, and these names: * Control: `Control`, `Ctrl` diff --git a/qutebrowser/config/configcommands.py b/qutebrowser/config/configcommands.py index f81c21aac..792eacaf0 100644 --- a/qutebrowser/config/configcommands.py +++ b/qutebrowser/config/configcommands.py @@ -137,7 +137,8 @@ class ConfigCommands: Using :bind without any arguments opens a page showing all keybindings. Args: - key: The keychain or special key (inside `<...>`) to bind. + key: The keychain to bind. Examples of valid keychains are `gC`, + `` or `a`. command: The command to execute, with optional args. mode: A comma-separated list of modes to bind the key in (default: `normal`). See `:help bindings.commands` for the @@ -178,7 +179,8 @@ class ConfigCommands: """Unbind a keychain. Args: - key: The keychain or special key (inside <...>) to unbind. + key: The keychain to unbind. See the help for `:bind` for the + correct syntax for keychains. mode: A mode to unbind the key in (default: `normal`). See `:help bindings.commands` for the available modes. """ diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml index 306d744fe..dc730bcb3 100644 --- a/qutebrowser/config/configdata.yml +++ b/qutebrowser/config/configdata.yml @@ -2490,7 +2490,6 @@ bindings.commands: If you want to map a key to another key, check the `bindings.key_mappings` setting instead. - For special keys (can't be part of a keychain), enclose them in `<`...`>`. For modifiers, you can use either `-` or `+` as delimiters, and these names: diff --git a/qutebrowser/keyinput/modeparsers.py b/qutebrowser/keyinput/modeparsers.py index d56d7dcd1..169232e01 100644 --- a/qutebrowser/keyinput/modeparsers.py +++ b/qutebrowser/keyinput/modeparsers.py @@ -164,7 +164,7 @@ class HintKeyParser(keyparser.CommandKeyParser): Return: 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') From 910bbc85216135a6f974e6724a6b40a99bbdd4dc Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 4 Mar 2018 20:40:05 +0100 Subject: [PATCH 130/135] Refactor keyutils._parse_keystring --- qutebrowser/keyinput/keyutils.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/qutebrowser/keyinput/keyutils.py b/qutebrowser/keyinput/keyutils.py index dd2668569..4441bb3b4 100644 --- a/qutebrowser/keyinput/keyutils.py +++ b/qutebrowser/keyinput/keyutils.py @@ -151,7 +151,7 @@ def _parse_keystring(keystr): for c in keystr: if c == '>': if special: - yield _normalize_keystr(key) + yield _parse_special_key(key) key = '' special = False else: @@ -162,14 +162,14 @@ def _parse_keystring(keystr): elif special: key += c else: - yield 'Shift+' + c if c.isupper() else c + yield _parse_single_key(c) if special: yield '<' for c in key: - yield 'Shift+' + c if c.isupper() else c + yield _parse_single_key(c) -def _normalize_keystr(keystr): +def _parse_special_key(keystr): """Normalize a keystring like Ctrl-Q to a keystring like Ctrl+Q. Args: @@ -187,11 +187,17 @@ def _normalize_keystr(keystr): ) for (orig, repl) in replacements: keystr = keystr.replace(orig, repl) + for mod in ['ctrl', 'meta', 'alt', 'shift']: 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: From 0967b6abd276daf3f0543af3438fbf39335f8f2d Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 4 Mar 2018 20:40:16 +0100 Subject: [PATCH 131/135] Fix handling of keys --- qutebrowser/keyinput/keyutils.py | 2 ++ tests/unit/keyinput/test_keyutils.py | 14 ++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/qutebrowser/keyinput/keyutils.py b/qutebrowser/keyinput/keyutils.py index 4441bb3b4..9a2c31572 100644 --- a/qutebrowser/keyinput/keyutils.py +++ b/qutebrowser/keyinput/keyutils.py @@ -184,6 +184,8 @@ def _parse_special_key(keystr): ('windows', 'meta'), ('mod1', 'alt'), ('mod4', 'meta'), + ('less', '<'), + ('greater', '>'), ) for (orig, repl) in replacements: keystr = keystr.replace(orig, repl) diff --git a/tests/unit/keyinput/test_keyutils.py b/tests/unit/keyinput/test_keyutils.py index a1e078521..88710766b 100644 --- a/tests/unit/keyinput/test_keyutils.py +++ b/tests/unit/keyinput/test_keyutils.py @@ -355,6 +355,20 @@ class TestKeySequence: ('', keyutils.KeySequence(Qt.ControlModifier | Qt.Key_X, Qt.MetaModifier | Qt.Key_Y)), + + ('>', keyutils.KeySequence(Qt.Key_Greater)), + ('<', keyutils.KeySequence(Qt.Key_Less)), + ('a>', keyutils.KeySequence(Qt.Key_A, Qt.Key_Greater)), + ('a<', keyutils.KeySequence(Qt.Key_A, Qt.Key_Less)), + ('>a', keyutils.KeySequence(Qt.Key_Greater, Qt.Key_A)), + ('', + keyutils.KeySequence(Qt.Key_Greater | Qt.AltModifier)), + ('', + keyutils.KeySequence(Qt.Key_Less | Qt.AltModifier)), + + ('', keyutils.KeyParseError), + ('>', keyutils.KeyParseError), ('', keyutils.KeyParseError), ('\U00010000', keyutils.KeyParseError), ]) From f2fadd7addf3b86b2414f6ed1787588c3f0549ad Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 4 Mar 2018 21:32:28 +0100 Subject: [PATCH 132/135] Fix handling of key_mappings --- qutebrowser/keyinput/basekeyparser.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/qutebrowser/keyinput/basekeyparser.py b/qutebrowser/keyinput/basekeyparser.py index 2c934617b..f22df8cf7 100644 --- a/qutebrowser/keyinput/basekeyparser.py +++ b/qutebrowser/keyinput/basekeyparser.py @@ -138,15 +138,16 @@ class BaseKeyParser(QObject): self._count += txt return QKeySequence.ExactMatch - sequence = self._sequence.append_event(e) - match, binding = self._match_key(sequence) + self._sequence = self._sequence.append_event(e) + match, binding = self._match_key(self._sequence) if match == QKeySequence.NoMatch: mappings = config.val.bindings.key_mappings - mapped = mappings.get(sequence, None) + mapped = mappings.get(self._sequence, None) if mapped is not None: + self._debug_log("Mapped {} -> {}".format( + self._sequence, mapped)) match, binding = self._match_key(mapped) - - self._sequence = self._sequence.append_event(e) + self._sequence = mapped if match == QKeySequence.ExactMatch: self._debug_log("Definitive match for '{}'.".format( From 40c3295cd1d6e980d46dc4080b0356e67ee921b2 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 4 Mar 2018 21:32:42 +0100 Subject: [PATCH 133/135] Improve logging message for clear_keystring --- qutebrowser/keyinput/basekeyparser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutebrowser/keyinput/basekeyparser.py b/qutebrowser/keyinput/basekeyparser.py index f22df8cf7..e9347fa7b 100644 --- a/qutebrowser/keyinput/basekeyparser.py +++ b/qutebrowser/keyinput/basekeyparser.py @@ -207,7 +207,7 @@ class BaseKeyParser(QObject): def clear_keystring(self): """Clear the currently entered key sequence.""" if self._sequence: - self._debug_log("discarding keystring '{}'.".format( + self._debug_log("Clearing keystring (was: {}).".format( self._sequence)) self._sequence = keyutils.KeySequence() self._count = '' From e2f17c4be10c94331e1b37cc11bf7f10861c1e81 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 4 Mar 2018 21:43:24 +0100 Subject: [PATCH 134/135] Always prefer exact over partial matches --- qutebrowser/keyinput/basekeyparser.py | 7 +++++-- tests/unit/keyinput/test_basekeyparser.py | 13 +++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/qutebrowser/keyinput/basekeyparser.py b/qutebrowser/keyinput/basekeyparser.py index e9347fa7b..901e96b55 100644 --- a/qutebrowser/keyinput/basekeyparser.py +++ b/qutebrowser/keyinput/basekeyparser.py @@ -102,14 +102,17 @@ class BaseKeyParser(QObject): """ 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.NoMatch: + if match == QKeySequence.ExactMatch: return (match, cmd) + elif match == QKeySequence.PartialMatch: + result = QKeySequence.PartialMatch - return (QKeySequence.NoMatch, None) + return (result, None) def handle(self, e): """Handle a new keypress. diff --git a/tests/unit/keyinput/test_basekeyparser.py b/tests/unit/keyinput/test_basekeyparser.py index 95860399d..893836335 100644 --- a/tests/unit/keyinput/test_basekeyparser.py +++ b/tests/unit/keyinput/test_basekeyparser.py @@ -203,6 +203,19 @@ class TestHandle: keyparser.execute.assert_called_once_with('yank -s', None) + def test_partial_before_full_match(self, keyparser, fake_keyevent, + config_stub): + """Make sure full matches always take precedence over partial ones.""" + config_stub.val.bindings.commands = { + 'normal': { + 'ab': 'message-info bar', + 'a': 'message-info foo' + } + } + keyparser._read_config('normal') + keyparser.handle(fake_keyevent(Qt.Key_A)) + keyparser.execute.assert_called_once_with('message-info foo', None) + class TestCount: From 88a5c8d29dd42539d28d269b85d6b6f4f1f7c89c Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 4 Mar 2018 22:38:33 +0100 Subject: [PATCH 135/135] Make sure bindings with umlauts work See #303 --- tests/unit/keyinput/test_basekeyparser.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/unit/keyinput/test_basekeyparser.py b/tests/unit/keyinput/test_basekeyparser.py index 893836335..9ed996922 100644 --- a/tests/unit/keyinput/test_basekeyparser.py +++ b/tests/unit/keyinput/test_basekeyparser.py @@ -185,6 +185,12 @@ class TestHandle: keyparser.execute.assert_called_once_with('message-info 0', None) assert not keyparser._sequence + def test_umlauts(self, handle_text, keyparser, config_stub): + config_stub.val.bindings.commands = {'normal': {'ü': 'message-info ü'}} + keyparser._read_config('normal') + handle_text(Qt.Key_Udiaeresis) + keyparser.execute.assert_called_once_with('message-info ü', None) + def test_mapping(self, config_stub, handle_text, keyparser): handle_text(Qt.Key_X) keyparser.execute.assert_called_once_with('message-info a', None)