Merge branch 'keys'

This commit is contained in:
Florian Bruhin 2018-03-04 22:50:41 +01:00
commit 155a1901c0
37 changed files with 2107 additions and 1088 deletions

View File

@ -154,7 +154,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`, `<Ctrl-X>` or `<Ctrl-C>a`.
* +'command'+: The command to execute, with optional args.
==== optional arguments
@ -1317,7 +1318,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.
@ -1498,13 +1500,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.

View File

@ -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`
@ -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:[&lt;Shift-Tab&gt;]+: +pass:[prompt-item-focus prev]+
* +pass:[&lt;Tab&gt;]+: +pass:[prompt-item-focus next]+
* +pass:[&lt;Up&gt;]+: +pass:[prompt-item-focus prev]+
* +pass:[n]+: +pass:[prompt-accept no]+
* +pass:[y]+: +pass:[prompt-accept yes]+
- +pass:[register]+:
* +pass:[&lt;Escape&gt;]+: +pass:[leave-mode]+
- +pass:[yesno]+:
* +pass:[&lt;Alt-Shift-Y&gt;]+: +pass:[prompt-yank --sel]+
* +pass:[&lt;Alt-Y&gt;]+: +pass:[prompt-yank]+
* +pass:[&lt;Escape&gt;]+: +pass:[leave-mode]+
* +pass:[&lt;Return&gt;]+: +pass:[prompt-accept]+
* +pass:[n]+: +pass:[prompt-accept no]+
* +pass:[y]+: +pass:[prompt-accept yes]+
[[bindings.key_mappings]]
=== bindings.key_mappings

View File

@ -27,14 +27,13 @@ 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
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
@ -2112,15 +2111,13 @@ class CommandDispatcher:
global_: If given, the keys are sent to the qutebrowser UI.
"""
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:
press_event = keyinfo.to_event(QEvent.KeyPress)
release_event = keyinfo.to_event(QEvent.KeyRelease)
if global_:
window = QApplication.focusWindow()

View File

@ -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_partial_key(keystring)
else:
self._fire(keystring)
@pyqtSlot(usertypes.KeyMode)
def on_mode_left(self, mode):

View File

@ -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):
@ -73,16 +74,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:
data.append(('', str(e), key))
return data
cmd_text = info.keyconf.get_command(key, 'normal')
cmd_text = info.keyconf.get_command(seq, 'normal')
if cmd_text:
parser = runners.CommandParser()
try:
@ -92,12 +93,24 @@ 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
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))

View File

@ -28,6 +28,7 @@ from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject
from qutebrowser.config import configdata, configexc, configutils
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
@ -135,14 +136,12 @@ class KeyConfig:
def __init__(self, config):
self._config = config
def _prepare(self, key, mode):
"""Make sure the given mode exists and normalize the key."""
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:
raise configexc.KeybindingError("Invalid mode {}!".format(mode))
if utils.is_special_key(key):
# <Ctrl-t>, <ctrl-T>, and <ctrl-t> should be considered equivalent
return utils.normalize_keystr(key)
return key
def get_bindings_for(self, mode):
"""Get the combined bindings for the given mode."""
@ -158,20 +157,20 @@ 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
if utils.is_special_key(key):
cmd_to_keys[cmd].append(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, key)
cmd_to_keys[cmd].insert(0, str(seq))
return cmd_to_keys
def get_command(self, key, mode, default=False):
"""Get the command for a given key (or None)."""
key = self._prepare(key, mode)
self._validate(key, mode)
if default:
bindings = dict(val.bindings.default[mode])
else:
@ -185,23 +184,23 @@ class KeyConfig:
"Can't add binding '{}' with empty command in {} "
'mode'.format(key, mode))
key = self._prepare(key, mode)
self._validate(key, mode)
log.keyboard.vdebug("Adding binding {} -> {} in mode {}.".format(
key, command, mode))
bindings = self._config.get_mutable_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):
"""Restore a default keybinding."""
key = self._prepare(key, mode)
self._validate(key, mode)
bindings_commands = self._config.get_mutable_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,18 +208,18 @@ 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._validate(key, mode)
bindings_commands = self._config.get_mutable_obj('bindings.commands')
if val.bindings.commands[mode].get(key, None) is not None:
# 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))

View File

@ -26,9 +26,10 @@ 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
class ConfigCommands:
@ -58,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():
@ -129,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`,
`<Ctrl-X>` or `<Ctrl-C>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
@ -142,42 +151,42 @@ class ConfigCommands:
tabbed_browser.openurl(QUrl('qute://bindings'), newtab=True)
return
seq = self._parse_key(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
# 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(key, mode)
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():
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'):
"""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.
"""
with self._handle_config_error():
self._keyconfig.unbind(key, mode=mode, save_yaml=True)
self._keyconfig.unbind(self._parse_key(key), mode=mode,
save_yaml=True)
@cmdutils.register(instance='config-commands', star_args_optional=True)
@cmdutils.argument('option', completion=configmodel.option)

View File

@ -2379,8 +2379,6 @@ bindings.default:
<Escape>: leave-mode
prompt:
<Return>: prompt-accept
y: prompt-accept yes
n: prompt-accept no
<Ctrl-X>: prompt-open-download
<Shift-Tab>: prompt-item-focus prev
<Up>: prompt-item-focus prev
@ -2403,6 +2401,13 @@ bindings.default:
<Ctrl-H>: rl-backward-delete-char
<Ctrl-Y>: rl-yank
<Escape>: leave-mode
yesno:
<Return>: prompt-accept
y: prompt-accept yes
n: prompt-accept no
<Alt-Y>: prompt-yank
<Alt-Shift-Y>: prompt-yank --sel
<Escape>: leave-mode
caret:
v: toggle-selection
<Space>: toggle-selection
@ -2438,7 +2443,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 +2467,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
@ -2485,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:
@ -2534,10 +2538,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.

View File

@ -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
@ -332,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."""
@ -357,12 +361,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."""

View File

@ -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 keyutils
SYSTEM_PROXY = object() # Return value for Proxy type
@ -1651,6 +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
try:
return keyutils.KeySequence.parse(value)
except keyutils.KeyParseError as e:
raise configexc.ValidationError(value, str(e))

View File

@ -19,14 +19,12 @@
"""Base class for vim-like key sequence parser."""
import enum
import re
import unicodedata
from PyQt5.QtCore import pyqtSignal, QObject
from PyQt5.QtGui import QKeySequence
from qutebrowser.config import config
from qutebrowser.utils import usertypes, log, utils
from qutebrowser.keyinput import keyutils
class BaseKeyParser(QObject):
@ -43,24 +41,16 @@ class BaseKeyParser(QObject):
definitive: Keychain matches exactly.
none: No more matches possible.
Types: type of a key binding.
chain: execute() was called via a chain-like key binding
special: execute() was called via a special key binding
do_log: Whether to log keypresses or not.
passthrough: Whether unbound keys should be passed through with this
handler.
Attributes:
bindings: Bound key bindings
special_bindings: Bound special bindings (<Foo>).
_win_id: The window ID this keyparser is associated with.
_warn_on_keychains: Whether a warning should be logged when binding
keychains in a section which does not support them.
_keystring: The currently entered key sequence
_sequence: The currently entered key sequence
_modename: The name of the input mode associated with this keyparser.
_supports_count: Whether count is supported
_supports_chains: Whether keychains are supported
Signals:
keystring_updated: Emitted when the keystring is updated.
@ -76,27 +66,18 @@ class BaseKeyParser(QObject):
do_log = True
passthrough = False
Match = enum.Enum('Match', ['partial', 'definitive', 'other', 'none'])
Type = enum.Enum('Type', ['chain', 'special'])
def __init__(self, win_id, parent=None, supports_count=None,
supports_chains=False):
def __init__(self, win_id, parent=None, supports_count=True):
super().__init__(parent)
self._win_id = win_id
self._modename = None
self._keystring = ''
if supports_count is None:
supports_count = supports_chains
self._sequence = keyutils.KeySequence()
self._count = ''
self._supports_count = supports_count
self._supports_chains = supports_chains
self._warn_on_keychains = True
self.bindings = {}
self.special_bindings = {}
config.instance.changed.connect(self._on_config_changed)
def __repr__(self):
return utils.get_repr(self, supports_count=self._supports_count,
supports_chains=self._supports_chains)
return utils.get_repr(self, supports_count=self._supports_count)
def _debug_log(self, message):
"""Log a message to the debug log if logging is active.
@ -107,62 +88,34 @@ class BaseKeyParser(QObject):
if self.do_log:
log.keyboard.debug(message)
def _handle_special_key(self, e):
"""Handle a new keypress with special keys (<Foo>).
Return True if the keypress has been handled, and False if not.
def _match_key(self, sequence):
"""Try to match a given keystring with any bound keychain.
Args:
e: the KeyPressEvent from Qt.
sequence: The command string to find.
Return:
True if event has been handled, False otherwise.
A tuple (matchtype, binding).
matchtype: Match.definitive, Match.partial or Match.none.
binding: - None with Match.partial/Match.none.
- The found binding with Match.definitive.
"""
binding = utils.keyevent_to_string(e)
if binding is None:
self._debug_log("Ignoring only-modifier keyeevent.")
return False
assert sequence
assert not isinstance(sequence, str)
result = QKeySequence.NoMatch
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
for seq, cmd in self.bindings.items():
assert not isinstance(seq, str), seq
match = sequence.matches(seq)
if match == QKeySequence.ExactMatch:
return (match, cmd)
elif match == QKeySequence.PartialMatch:
result = QKeySequence.PartialMatch
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
return (result, None)
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_single_key(self, e):
"""Handle a new keypress with a single key (no modifiers).
def handle(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
@ -172,108 +125,52 @@ class BaseKeyParser(QObject):
e: the KeyPressEvent from Qt.
Return:
A self.Match member.
A QKeySequence match.
"""
txt = e.text()
key = e.key()
txt = str(keyutils.KeyInfo.from_event(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 keyutils.is_modifier_key(key):
self._debug_log("Ignoring, only modifier")
return QKeySequence.NoMatch
if (not txt) or is_control_char:
self._debug_log("Ignoring, no text char")
return self.Match.none
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
count, cmd_input = self._split_count(self._keystring + txt)
match, binding = self._match_key(cmd_input)
if match == self.Match.none:
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(txt, None)
mapped = mappings.get(self._sequence, None)
if mapped is not None:
txt = mapped
count, cmd_input = self._split_count(self._keystring + txt)
match, binding = self._match_key(cmd_input)
self._debug_log("Mapped {} -> {}".format(
self._sequence, mapped))
match, binding = self._match_key(mapped)
self._sequence = mapped
self._keystring += txt
if match == self.Match.definitive:
if match == QKeySequence.ExactMatch:
self._debug_log("Definitive match for '{}'.".format(
self._keystring))
self._sequence))
count = int(self._count) if self._count else None
self.clear_keystring()
self.execute(binding, self.Type.chain, count)
elif match == self.Match.partial:
self.execute(binding, count)
elif match == QKeySequence.PartialMatch:
self._debug_log("No match for '{}' (added {})".format(
self._keystring, txt))
elif match == self.Match.none:
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._keystring))
self._sequence))
self.clear_keystring()
elif match == self.Match.other:
pass
else:
raise utils.Unreachable("Invalid match value {!r}".format(match))
return match
def _match_key(self, cmd_input):
"""Try to match a given keystring with any bound keychain.
Args:
cmd_input: The command string to find.
Return:
A tuple (matchtype, binding).
matchtype: Match.definitive, Match.partial or Match.none.
binding: - None with Match.partial/Match.none.
- The found binding with Match.definitive.
"""
if not cmd_input:
# Only a count, no command yet, but we handled it
return (self.Match.other, None)
# A (cmd_input, binding) tuple (k, v of bindings) or None.
definitive_match = None
partial_match = False
# Check definitive match
try:
definitive_match = (cmd_input, self.bindings[cmd_input])
except KeyError:
pass
# Check partial match
for binding in self.bindings:
if definitive_match is not None and binding == definitive_match[0]:
# We already matched that one
continue
elif binding.startswith(cmd_input):
partial_match = True
break
if definitive_match is not None:
return (self.Match.definitive, definitive_match[1])
elif partial_match:
return (self.Match.partial, None)
else:
return (self.Match.none, None)
def handle(self, e):
"""Handle a new keypress and call the respective handlers.
Args:
e: the KeyPressEvent from Qt
Return:
True if the event was handled, False otherwise.
"""
handled = self._handle_special_key(e)
if handled or not self._supports_chains:
return handled
match = self._handle_single_key(e)
# don't emit twice if the keystring was cleared in self.clear_keystring
if self._keystring:
self.keystring_updated.emit(self._keystring)
return match != self.Match.none
@config.change_filter('bindings')
def _on_config_changed(self):
self._read_config()
@ -295,37 +192,26 @@ class BaseKeyParser(QObject):
else:
self._modename = modename
self.bindings = {}
self.special_bindings = {}
for key, cmd in config.key_instance.get_bindings_for(modename).items():
assert not isinstance(key, str), key
assert cmd
self._parse_key_command(modename, key, cmd)
def _parse_key_command(self, modename, key, cmd):
"""Parse the keys and their command and store them in the object."""
if utils.is_special_key(key):
self.special_bindings[key[1:-1]] = cmd
elif self._supports_chains:
self.bindings[key] = cmd
elif self._warn_on_keychains:
log.keyboard.warning("Ignoring keychain '{}' in mode '{}' because "
"keychains are not supported there."
.format(key, modename))
def execute(self, cmdstr, keytype, count=None):
def execute(self, cmdstr, count=None):
"""Handle a completed keychain.
Args:
cmdstr: The command to execute as a string.
keytype: Type.chain or Type.special
count: The count if given.
"""
raise NotImplementedError
def clear_keystring(self):
"""Clear the currently entered key sequence."""
if self._keystring:
self._debug_log("discarding keystring '{}'.".format(
self._keystring))
self._keystring = ''
self.keystring_updated.emit(self._keystring)
if self._sequence:
self._debug_log("Clearing keystring (was: {}).".format(
self._sequence))
self._sequence = keyutils.KeySequence()
self._count = ''
self.keystring_updated.emit('')

View File

@ -34,12 +34,11 @@ 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, _keytype, count=None):
def execute(self, cmdstr, count=None):
try:
self._commandrunner.run(cmdstr, count)
except cmdexc.Error as e:
@ -59,7 +58,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:
@ -67,11 +66,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)

View File

@ -0,0 +1,455 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014-2018 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""Our own QKeySequence-like class and related utilities."""
import itertools
import attr
from PyQt5.QtCore import Qt, QEvent
from PyQt5.QtGui import QKeySequence, QKeyEvent
from qutebrowser.utils import utils
# Map Qt::Key values to their Qt::KeyboardModifier value.
_MODIFIER_MAP = {
Qt.Key_Shift: Qt.ShiftModifier,
Qt.Key_Control: Qt.ControlModifier,
Qt.Key_Alt: Qt.AltModifier,
Qt.Key_Meta: Qt.MetaModifier,
Qt.Key_Mode_switch: Qt.GroupSwitchModifier,
}
def is_printable(key):
return key <= 0xff and key != Qt.Key_Space
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.
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.
'Super_L': 'Super L',
'Super_R': 'Super R',
'Hyper_L': 'Hyper L',
'Hyper_R': 'Hyper R',
'Direction_L': 'Direction L',
'Direction_R': 'Direction R',
'Shift': 'Shift',
'Control': 'Control',
'Meta': 'Meta',
'Alt': 'Alt',
'AltGr': 'AltGr',
'Multi_key': 'Multi key',
'SingleCandidate': 'Single Candidate',
'Mode_switch': 'Mode switch',
'Dead_Grave': '`',
'Dead_Acute': '´',
'Dead_Circumflex': '^',
'Dead_Tilde': '~',
'Dead_Macron': '¯',
'Dead_Breve': '˘',
'Dead_Abovedot': '˙',
'Dead_Diaeresis': '¨',
'Dead_Abovering': '˚',
'Dead_Doubleacute': '˝',
'Dead_Caron': 'ˇ',
'Dead_Cedilla': '¸',
'Dead_Ogonek': '˛',
'Dead_Iota': 'Iota',
'Dead_Voiced_Sound': 'Voiced Sound',
'Dead_Semivoiced_Sound': 'Semivoiced Sound',
'Dead_Belowdot': 'Belowdot',
'Dead_Hook': 'Hook',
'Dead_Horn': 'Horn',
'Memo': 'Memo',
'ToDoList': 'To Do List',
'Calendar': 'Calendar',
'ContrastAdjust': 'Contrast Adjust',
'LaunchG': 'Launch (G)',
'LaunchH': 'Launch (H)',
'MediaLast': 'Media Last',
'unknown': 'Unknown',
# For some keys, we just want a different name
'Escape': 'Escape',
}
# We now build our real special_names dict from the string mapping above.
# The reason we don't do this directly is that certain Qt versions don't
# have all the keys, so we want to ignore AttributeErrors.
special_names = {}
for k, v in special_names_str.items():
try:
special_names[getattr(Qt, 'Key_' + k)] = v
except AttributeError:
pass
if key in special_names:
return special_names[key]
return QKeySequence(key).toString()
class KeyParseError(Exception):
"""Raised by _parse_single_key/parse_keystring on parse errors."""
def __init__(self, keystr, error):
if keystr is None:
msg = "Could not parse keystring: {}".format(error)
else:
msg = "Could not parse {!r}: {}".format(keystr, error)
super().__init__(msg)
def _parse_keystring(keystr):
key = ''
special = False
for c in keystr:
if c == '>':
if special:
yield _parse_special_key(key)
key = ''
special = False
else:
yield '>'
assert not key, key
elif c == '<':
special = True
elif special:
key += c
else:
yield _parse_single_key(c)
if special:
yield '<'
for c in key:
yield _parse_single_key(c)
def _parse_special_key(keystr):
"""Normalize a keystring like Ctrl-Q to a keystring like Ctrl+Q.
Args:
keystr: The key combination as a string.
Return:
The normalized keystring.
"""
keystr = keystr.lower()
replacements = (
('control', 'ctrl'),
('windows', 'meta'),
('mod1', 'alt'),
('mod4', 'meta'),
('less', '<'),
('greater', '>'),
)
for (orig, repl) in replacements:
keystr = keystr.replace(orig, repl)
for mod in ['ctrl', 'meta', 'alt', 'shift']:
keystr = keystr.replace(mod + '-', mod + '+')
return keystr
def _parse_single_key(keystr):
"""Get a keystring for QKeySequence for a single key."""
return 'Shift+' + keystr if keystr.isupper() else keystr
@attr.s
class KeyInfo:
"""A key with optional modifiers.
Attributes:
key: A Qt::Key member.
modifiers: A Qt::KeyboardModifiers enum value.
"""
key = attr.ib()
modifiers = attr.ib()
@classmethod
def from_event(cls, e):
return cls(e.key(), e.modifiers())
def __str__(self):
"""Convert this KeyInfo to a meaningful name.
Return:
A name of the key (combination) as a string.
"""
key_string = _key_to_string(self.key)
modifiers = int(self.modifiers)
if self.key in _MODIFIER_MAP:
# Don't return e.g. <Shift+Shift>
modifiers &= ~_MODIFIER_MAP[self.key]
elif is_printable(self.key):
# "normal" binding
assert len(key_string) == 1, key_string
if self.modifiers == Qt.ShiftModifier:
return key_string.upper()
elif self.modifiers == Qt.NoModifier:
return key_string.lower()
else:
# Use special binding syntax, but <Ctrl-a> instead of <Ctrl-A>
key_string = key_string.lower()
# "special" binding
modifier_string = QKeySequence(modifiers).toString()
return '<{}{}>'.format(modifier_string, key_string)
def text(self):
"""Get the text which would be displayed when pressing this key."""
control = {
Qt.Key_Space: ' ',
Qt.Key_Tab: '\t',
Qt.Key_Backspace: '\b',
Qt.Key_Return: '\r',
Qt.Key_Enter: '\r',
Qt.Key_Escape: '\x1b',
}
if self.key in control:
return control[self.key]
elif not is_printable(self.key):
return ''
text = QKeySequence(self.key).toString()
if not self.modifiers & Qt.ShiftModifier:
text = text.lower()
return text
def to_event(self, typ=QEvent.KeyPress):
"""Get a QKeyEvent from this KeyInfo."""
return QKeyEvent(typ, self.key, self.modifiers, self.text())
class KeySequence:
"""A sequence of key presses.
This internally uses chained QKeySequence objects and exposes a nicer
interface over it.
NOTE: While private members of this class are in theory mutable, they must
not be mutated in order to ensure consistent hashing.
Attributes:
_sequences: A list of QKeySequence
Class attributes:
_MAX_LEN: The maximum amount of keys in a QKeySequence.
"""
_MAX_LEN = 4
def __init__(self, *keys):
self._sequences = []
for sub in utils.chunk(keys, self._MAX_LEN):
sequence = QKeySequence(*sub)
self._sequences.append(sequence)
if keys:
assert self
self._validate()
def __str__(self):
parts = []
for info in self:
parts.append(str(info))
return ''.join(parts)
def __iter__(self):
"""Iterate over KeyInfo objects."""
modifier_mask = int(Qt.ShiftModifier | Qt.ControlModifier |
Qt.AltModifier | Qt.MetaModifier |
Qt.KeypadModifier | Qt.GroupSwitchModifier)
for key in self._iter_keys():
yield KeyInfo(
key=int(key) & ~modifier_mask,
modifiers=Qt.KeyboardModifiers(int(key) & modifier_mask))
def __repr__(self):
return utils.get_repr(self, keys=str(self))
def __lt__(self, other):
# pylint: disable=protected-access
return self._sequences < other._sequences
def __gt__(self, other):
# pylint: disable=protected-access
return self._sequences > other._sequences
def __le__(self, other):
# pylint: disable=protected-access
return self._sequences <= other._sequences
def __ge__(self, other):
# pylint: disable=protected-access
return self._sequences >= other._sequences
def __eq__(self, other):
# pylint: disable=protected-access
return self._sequences == other._sequences
def __ne__(self, other):
# pylint: disable=protected-access
return self._sequences != other._sequences
def __hash__(self):
return hash(tuple(self._sequences))
def __len__(self):
return sum(len(seq) for seq in self._sequences)
def __bool__(self):
return bool(self._sequences)
def __getitem__(self, item):
if isinstance(item, slice):
keys = list(self._iter_keys())
return self.__class__(*keys[item])
else:
infos = list(self)
return infos[item]
def _iter_keys(self):
return itertools.chain.from_iterable(self._sequences)
def _validate(self, keystr=None):
for info in self:
assert Qt.Key_Space <= info.key <= Qt.Key_unknown, info.key
if info.key == Qt.Key_unknown:
raise KeyParseError(keystr, "Got unknown key!")
def matches(self, other):
"""Check whether the given KeySequence matches with this one.
We store multiple QKeySequences with <= 4 keys each, so we need to
match those pair-wise, and account for an unequal amount of sequences
as well.
"""
# pylint: disable=protected-access
if len(self._sequences) > len(other._sequences):
# If we entered more sequences than there are in the config,
# there's no way there can be a match.
return QKeySequence.NoMatch
for entered, configured in zip(self._sequences, other._sequences):
# If we get NoMatch/PartialMatch in a sequence, we can abort there.
match = entered.matches(configured)
if match != QKeySequence.ExactMatch:
return match
# We checked all common sequences and they had an ExactMatch.
#
# If there's still more sequences configured than entered, that's a
# PartialMatch, as more keypresses can still follow and new sequences
# will appear which we didn't check above.
#
# If there's the same amount of sequences configured and entered,
# that's an EqualMatch.
if len(self._sequences) == len(other._sequences):
return QKeySequence.ExactMatch
elif len(self._sequences) < len(other._sequences):
return QKeySequence.PartialMatch
else:
raise utils.Unreachable("self={!r} other={!r}".format(self, other))
def append_event(self, ev):
"""Create a new KeySequence object with the given QKeyEvent added.
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.
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(key | int(modifiers))
return self.__class__(*keys)
@classmethod
def parse(cls, keystr):
"""Parse a keystring like <Ctrl-x> or xyz and return a KeySequence."""
# pylint: disable=protected-access
new = cls()
strings = list(_parse_keystring(keystr))
for sub in utils.chunk(strings, cls._MAX_LEN):
sequence = QKeySequence(', '.join(sub))
new._sequences.append(sequence)
if keystr:
assert new, keystr
# pylint: disable=protected-access
new._validate(keystr)
return new

View File

@ -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,
@ -158,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
@ -166,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)):
@ -179,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

View File

@ -27,10 +27,11 @@ import traceback
import enum
from PyQt5.QtCore import pyqtSlot, Qt
from PyQt5.QtGui import QKeySequence
from qutebrowser.commands import cmdexc
from qutebrowser.config import config
from qutebrowser.keyinput import keyparser
from qutebrowser.keyinput import keyparser, keyutils
from qutebrowser.utils import usertypes, log, message, objreg, utils
@ -47,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)
@ -59,8 +59,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.
@ -72,9 +72,11 @@ class NormalKeyParser(keyparser.CommandKeyParser):
if self._inhibited:
self._debug_log("Ignoring key '{}', because the normal mode is "
"currently inhibited.".format(txt))
return self.Match.none
match = super()._handle_single_key(e)
if match == self.Match.partial:
return QKeySequence.NoMatch
match = super().handle(e)
if match == QKeySequence.PartialMatch:
timeout = config.val.input.partial_timeout
if timeout != 0:
self._partial_timer.setInterval(timeout)
@ -96,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):
@ -128,11 +130,8 @@ 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)
# We don't want an extra section for this in the config, so we just
# abuse the prompt section.
self._read_config('prompt')
super().__init__(win_id, parent, supports_count=False)
self._read_config('yesno')
def __repr__(self):
return utils.get_repr(self)
@ -148,15 +147,14 @@ class HintKeyParser(keyparser.CommandKeyParser):
"""
def __init__(self, win_id, parent=None):
super().__init__(win_id, parent, supports_count=False,
supports_chains=True)
super().__init__(win_id, parent, supports_count=False)
self._filtertext = ''
self._last_press = LastPress.none
self._read_config('hint')
self.keystring_updated.connect(self.on_keystring_updated)
def _handle_special_key(self, e):
"""Override _handle_special_key to handle string filtering.
def _handle_filter_key(self, e):
"""Handle keys for string filtering.
Return True if the keypress has been handled, and False if not.
@ -164,41 +162,41 @@ class HintKeyParser(keyparser.CommandKeyParser):
e: the KeyPressEvent from Qt.
Return:
True if event has been handled, False otherwise.
A QKeySequence match.
"""
log.keyboard.debug("Got special key 0x{:x} text {}".format(
log.keyboard.debug("Got filter key 0x{:x} text {}".format(
e.key(), e.text()))
hintmanager = objreg.get('hintmanager', scope='tab',
window=self._win_id, tab='current')
if e.key() == Qt.Key_Backspace:
log.keyboard.debug("Got backspace, mode {}, filtertext '{}', "
"keystring '{}'".format(self._last_press,
self._filtertext,
self._keystring))
"sequence '{}'".format(self._last_press,
self._filtertext,
self._sequence))
if self._last_press == LastPress.filtertext and self._filtertext:
self._filtertext = self._filtertext[:-1]
hintmanager.filter_hints(self._filtertext)
return True
elif self._last_press == LastPress.keystring and self._keystring:
self._keystring = self._keystring[:-1]
self.keystring_updated.emit(self._keystring)
if not self._keystring and self._filtertext:
return QKeySequence.ExactMatch
elif self._last_press == LastPress.keystring and self._sequence:
self._sequence = self._sequence[:-1]
self.keystring_updated.emit(str(self._sequence))
if not self._sequence and self._filtertext:
# Switch back to hint filtering mode (this can happen only
# in numeric mode after the number has been deleted).
hintmanager.filter_hints(self._filtertext)
self._last_press = LastPress.filtertext
return True
return QKeySequence.ExactMatch
else:
return super()._handle_special_key(e)
return QKeySequence.NoMatch
elif hintmanager.current_mode() != 'number':
return super()._handle_special_key(e)
return QKeySequence.NoMatch
elif not e.text():
return super()._handle_special_key(e)
return QKeySequence.NoMatch
else:
self._filtertext += e.text()
hintmanager.filter_hints(self._filtertext)
self._last_press = LastPress.filtertext
return True
return QKeySequence.ExactMatch
def handle(self, e):
"""Handle a new keypress and call the respective handlers.
@ -209,33 +207,18 @@ class HintKeyParser(keyparser.CommandKeyParser):
Returns:
True if the match has been handled, False otherwise.
"""
match = self._handle_single_key(e)
if match == self.Match.partial:
self.keystring_updated.emit(self._keystring)
match = super().handle(e)
if match == QKeySequence.PartialMatch:
self._last_press = LastPress.keystring
return True
elif match == self.Match.definitive:
elif match == QKeySequence.ExactMatch:
self._last_press = LastPress.none
return True
elif match == self.Match.other:
return None
elif match == self.Match.none:
elif match == QKeySequence.NoMatch:
# We couldn't find a keychain so we check if it's a special key.
return self._handle_special_key(e)
return self._handle_filter_key(e)
else:
raise ValueError("Got invalid match type {}!".format(match))
def execute(self, cmdstr, keytype, count=None):
"""Handle a completed keychain."""
if not isinstance(keytype, self.Type):
raise TypeError("Type {} is no Type member!".format(keytype))
if keytype == self.Type.chain:
hintmanager = objreg.get('hintmanager', scope='tab',
window=self._win_id, tab='current')
hintmanager.handle_partial_key(cmdstr)
else:
# execute as command
super().execute(cmdstr, keytype, count)
return match
def update_bindings(self, strings, preserve_filter=False):
"""Update bindings when the hint strings changed.
@ -245,7 +228,9 @@ class HintKeyParser(keyparser.CommandKeyParser):
preserve_filter: Whether to keep the current value of
`self._filtertext`.
"""
self.bindings = {s: s for s in strings}
self._read_config()
self.bindings.update({keyutils.KeySequence.parse(s):
'follow-hint -s ' + s for s in strings})
if not preserve_filter:
self._filtertext = ''
@ -264,8 +249,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')
@ -279,8 +263,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')
@ -293,15 +276,16 @@ class RegisterKeyParser(keyparser.CommandKeyParser):
Return:
True if event has been handled, False otherwise.
"""
if super().handle(e):
return True
match = super().handle(e)
if match:
return match
if not keyutils.is_printable(e.key()):
# this is not a proper register key, let it pass and keep going
return QKeySequence.NoMatch
key = e.text()
if key == '' or utils.keyevent_to_string(e) is None:
# this is not a proper register key, let it pass and keep going
return False
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=self._win_id)
macro_recorder = objreg.get('macro-recorder')
@ -322,5 +306,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

View File

@ -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():

View File

@ -34,6 +34,7 @@ from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt
from qutebrowser.config import config
from qutebrowser.utils import utils, usertypes
from qutebrowser.commands import cmdutils
from qutebrowser.keyinput import keyutils
class KeyHintView(QLabel):
@ -105,9 +106,8 @@ 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
if keyutils.KeySequence.parse(prefix).matches(k) and
not blacklisted(str(k)) and
(takes_count(v) or not countstr)]
if not bindings:
@ -120,7 +120,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 += (
"<tr>"
"<td>{}</td>"
@ -130,7 +130,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 = '<table>{}</table>'.format(text)

View File

@ -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.")

View File

@ -26,7 +26,6 @@ import re
import sys
import enum
import json
import collections
import datetime
import traceback
import functools
@ -34,9 +33,8 @@ import contextlib
import socket
import shlex
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
@ -48,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
@ -285,263 +283,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)
parts.append(key_to_string(e.key()))
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. <ctrl-x> or <space>)."""
return keystr.startswith('<') and keystr.endswith('>')
def _parse_single_key(keystr):
"""Convert a single key string to a (Qt.Key, Qt.Modifiers, text) tuple."""
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 "
"<Ctrl-x> like keybinding.")
seq = QKeySequence(normalize_keystr(keystr), QKeySequence.PortableText)
if len(seq) != 1:
raise KeyParseError(keystr, "Got {} keys instead of 1.".format(
len(seq)))
result = seq[0]
if result == Qt.Key_unknown:
raise KeyParseError(keystr, "Got unknown key.")
modifier_mask = int(Qt.ShiftModifier | Qt.ControlModifier |
Qt.AltModifier | Qt.MetaModifier | Qt.KeypadModifier |
Qt.GroupSwitchModifier)
assert Qt.Key_unknown & ~modifier_mask == Qt.Key_unknown
modifiers = result & modifier_mask
key = result & ~modifier_mask
if len(keystr) == 1 and keystr.isupper():
modifiers |= Qt.ShiftModifier
assert key != 0, key
key = Qt.Key(key)
modifiers = Qt.KeyboardModifiers(modifiers)
# Let's hope this is accurate...
if len(keystr) == 1 and not modifiers:
text = keystr
elif len(keystr) == 1 and modifiers == Qt.ShiftModifier:
text = keystr.upper()
else:
text = ''
return KeyInfo(key, modifiers, text)
def parse_keystring(keystr):
"""Parse a keystring like <Ctrl-x> 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]
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."""
@ -915,3 +656,14 @@ 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.
"""
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]

View File

@ -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'),

View File

@ -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.KeyInfo.from_event(e)),
'',
'key: 0x{:x}'.format(int(e.key())),
'modifiers: 0x{:x}'.format(int(e.modifiers())),

View File

@ -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

View File

@ -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 "<F1>"
And I press the keys ",<F1>"
# <F1>
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
@ -54,7 +53,7 @@ Feature: Keyboard input
Scenario: :fake-key with an unparsable key
When I run :fake-key <blub>
Then the error "Could not parse 'blub': Got unknown key." should be shown
Then the error "Could not parse '<blub>': Got unknown key!" should be shown
Scenario: :fake-key sending key to the website
When I open data/keyinput/log.html

View File

@ -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."""

View File

@ -100,13 +100,13 @@ 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(),
),
),
default={
'normal': collections.OrderedDict([
('<ctrl+q>', 'quit'),
('<Ctrl+q>', 'quit'),
('d', 'tab-close'),
])
},
@ -118,13 +118,13 @@ 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(),
),
),
default={
'normal': collections.OrderedDict([
('<ctrl+q>', 'quit'),
('<Ctrl+q>', 'quit'),
('ZQ', 'quit'),
('I', 'invalid'),
('d', 'scroll down'),
@ -223,7 +223,7 @@ def test_command_completion(qtmodeltester, cmdutils_stub, configdata_stub,
"Commands": [
('open', 'open a url', ''),
('q', "Alias for 'quit'", ''),
('quit', 'quit qutebrowser', 'ZQ, <ctrl+q>'),
('quit', 'quit qutebrowser', 'ZQ, <Ctrl+q>'),
('tab-close', 'Close the current tab.', ''),
]
})
@ -248,7 +248,7 @@ def test_help_completion(qtmodeltester, cmdutils_stub, key_config_stub,
_check_completions(model, {
"Commands": [
(':open', 'open a url', ''),
(':quit', 'quit qutebrowser', 'ZQ, <ctrl+q>'),
(':quit', 'quit qutebrowser', 'ZQ, <Ctrl+q>'),
(':scroll', 'Scroll the current tab in the given direction.', ''),
(':tab-close', 'Close the current tab.', ''),
],
@ -653,10 +653,10 @@ def test_setting_option_completion(qtmodeltester, config_stub,
"Options": [
('aliases', 'Aliases for commands.', '{"q": "quit"}'),
('bindings.commands', 'Default keybindings', (
'{"normal": {"<ctrl+q>": "quit", "ZQ": "quit", '
'{"normal": {"<Ctrl+q>": "quit", "ZQ": "quit", '
'"I": "invalid", "d": "scroll down"}}')),
('bindings.default', 'Default keybindings',
'{"normal": {"<ctrl+q>": "quit", "d": "tab-close"}}'),
'{"normal": {"<Ctrl+q>": "quit", "d": "tab-close"}}'),
('content.javascript.enabled', 'Enable/Disable JavaScript',
'true'),
]
@ -739,7 +739,7 @@ def test_bind_completion(qtmodeltester, cmdutils_stub, config_stub,
"Commands": [
('open', 'open a url', ''),
('q', "Alias for 'quit'", ''),
('quit', 'quit qutebrowser', 'ZQ, <ctrl+q>'),
('quit', 'quit qutebrowser', 'ZQ, <Ctrl+q>'),
('scroll', 'Scroll the current tab in the given direction.', ''),
('tab-close', 'Close the current tab.', ''),
],
@ -759,7 +759,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, <ctrl+q>'),
('quit', 'quit qutebrowser', 'ZQ, <Ctrl+q>'),
('scroll', 'Scroll the current tab in the given direction.', ''),
('tab-close', 'Close the current tab.', ''),
],
@ -778,7 +778,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, <ctrl+q>'),
('quit', 'quit qutebrowser', 'ZQ, <Ctrl+q>'),
('scroll', 'Scroll the current tab in the given direction.', ''),
('tab-close', 'Close the current tab.', ''),
],
@ -800,7 +800,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, <ctrl+q>'),
('quit', 'quit qutebrowser', 'ZQ, <Ctrl+q>'),
('scroll', 'Scroll the current tab in the given direction.', ''),
('tab-close', 'Close the current tab.', ''),
],

View File

@ -18,7 +18,6 @@
"""Tests for qutebrowser.config.config."""
import copy
import types
import unittest.mock
@ -29,6 +28,7 @@ from PyQt5.QtGui import QColor
from qutebrowser.config import config, configdata, configexc, configutils
from qutebrowser.utils import usertypes, urlmatch
from qutebrowser.misc import objects
from qutebrowser.keyinput import keyutils
@pytest.fixture(autouse=True)
@ -38,6 +38,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)
@ -98,47 +103,60 @@ class TestKeyConfig:
"""Get a dict with no bindings."""
return {'normal': {}}
@pytest.mark.parametrize('key, expected', [
('A', 'A'),
('<Ctrl-X>', '<ctrl+x>'),
])
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):
"""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('x', 'abnormal')
assert key_config_stub._validate(keyseq('x'), 'abnormal')
def test_validate_invalid_type(self, key_config_stub):
with pytest.raises(AssertionError):
assert key_config_stub._validate('x', 'normal')
@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
@ -147,15 +165,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
@ -164,9 +185,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', '<Escape>': 'message-info foo'},
{'message-info foo': ['a', '<escape>']}),
# With modifier keys (should be listed last and normalized)
({'a': 'message-info foo', '<ctrl-a>': 'message-info foo'},
{'message-info foo': ['a', '<Ctrl+a>']}),
# Chained command
({'a': 'message-info foo ;; message-info bar'},
{'message-info foo': ['a'], 'message-info bar': ['a']}),
@ -179,11 +200,14 @@ class TestKeyConfig:
@pytest.mark.parametrize('key', ['a', '<Ctrl-X>', 'b'])
def test_bind_duplicate(self, key_config_stub, config_stub, key):
seq = keyseq(key)
config_stub.val.bindings.default = {'normal': {'a': 'nop',
'<Ctrl+x>': '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', [
@ -194,13 +218,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):
@ -210,7 +235,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):
@ -219,11 +245,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):
@ -232,42 +262,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
('<Ctrl-X>', '<ctrl+x>')
@pytest.mark.parametrize('key', [
'a', # default bindings
'b', # custom bindings
'<Ctrl-X>',
])
@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', '<ctrl+x>': 'nop'},
'caret': {'a': 'nop', '<ctrl+x>': 'nop'},
# prompt: a mode which isn't in bindings.commands yet
'prompt': {'a': 'nop', 'b': 'nop', '<ctrl+x>': 'nop'},
}
old_default_bindings = copy.deepcopy(default_bindings)
expected_default_bindings = {
'normal': {keyseq('a'): 'nop', keyseq('<ctrl+x>'): 'nop'},
'caret': {keyseq('a'): 'nop', keyseq('<ctrl+x>'): 'nop'},
# prompt: a mode which isn't in bindings.commands yet
'prompt': {keyseq('a'): 'nop',
keyseq('b'): 'nop',
keyseq('<ctrl+x>'): '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."""
@ -275,7 +314,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.
@ -287,17 +326,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:

View File

@ -19,6 +19,7 @@
"""Tests for qutebrowser.config.configcommands."""
import logging
import functools
import unittest.mock
import pytest
@ -27,9 +28,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 +422,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 +503,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 +516,7 @@ class TestBind:
('c', 'normal', "c is bound to 'message-info c' in normal mode"),
# Special key
('<Ctrl-X>', 'normal',
"<ctrl+x> is bound to 'message-info C-x' in normal mode"),
"<Ctrl+x> is bound to 'message-info C-x' in normal mode"),
# unbound
('x', 'normal', "x is unbound in normal mode"),
# non-default mode
@ -537,23 +544,45 @@ 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"),
# :bind <blub> nop
('bind', ['<blub>', 'nop'], {},
"Could not parse '<blub>': 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 <blub>
('unbind', ['<blub>'], {},
"Could not parse '<blub>': Got unknown key!"),
])
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', '<Ctrl-X>'])
def test_bind_duplicate(self, commands, config_stub, key_config_stub, key):
@ -569,7 +598,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,23 +611,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
@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)
command = key_config_stub.get_command(keyseq('a'), mode='normal')
assert command == default_cmd
def test_unbind_none(self, commands, config_stub):
config_stub.val.bindings.commands = None
@ -607,7 +627,7 @@ class TestBind:
('a', 'a'), # default bindings
('b', 'b'), # custom bindings
('c', 'c'), # :bind then :unbind
('<Ctrl-X>', '<ctrl+x>') # normalized special binding
('<Ctrl-X>', '<Ctrl+x>') # normalized special binding
])
def test_unbind(self, commands, key_config_stub, config_stub, yaml_value,
key, normalized):
@ -624,7 +644,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':
@ -632,15 +652,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)

View File

@ -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)
@ -607,7 +608,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)
@ -699,6 +700,20 @@ class TestConfigPy:
message = "'ConfigAPI' object has no attribute 'val'"
assert str(error.exception) == message
@pytest.mark.parametrize('line', [
'config.bind("<blub>", "nop")',
'config.bind("\U00010000", "nop")',
'config.unbind("<blub>")',
'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)

View File

@ -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,12 +2059,16 @@ class TestKey:
return configtypes.Key
@pytest.mark.parametrize('val, expected', [
('gC', 'gC'),
('<Control-x>', '<ctrl+x>')
('gC', keyutils.KeySequence.parse('gC')),
('<Control-x>', keyutils.KeySequence.parse('<ctrl+x>')),
])
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),

View File

@ -21,19 +21,23 @@
import pytest
from PyQt5.QtCore import QEvent, Qt
from PyQt5.QtGui import QKeyEvent
from qutebrowser.keyinput import keyutils
BINDINGS = {'prompt': {'<Ctrl-a>': 'message-info ctrla',
'a': 'message-info a',
'ba': 'message-info ba',
'ax': 'message-info ax',
'ccc': 'message-info ccc',
'yY': 'yank -s',
'0': 'message-info 0'},
'command': {'foo': 'message-info bar',
'<Ctrl+X>': 'message-info ctrlx'},
'normal': {'a': 'message-info a', 'ba': 'message-info ba'}}
MAPPINGS = {
'<Ctrl+a>': 'a',
'<Ctrl+b>': '<Ctrl+a>',
'x': 'a',
'b': 'a',
}
@ -45,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

View File

@ -0,0 +1,588 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2018 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
# pylint: disable=line-too-long
"""Data used by test_keyutils.py to test all keys."""
import attr
from PyQt5.QtCore import Qt
@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)
text = attr.ib('')
uppertext = attr.ib('')
member = attr.ib(None)
qtest = attr.ib(True)
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
KEYS = [
### misc keys
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'),
Key('Print'), # print screen
Key('SysReq'),
Key('Clear'),
### cursor movement
Key('Home'),
Key('End'),
Key('Left'),
Key('Up'),
Key('Right'),
Key('Down'),
Key('PageUp', 'PgUp'),
Key('PageDown', 'PgDown'),
### 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', 'Super L'),
Key('Super_R', 'Super R'),
Key('Menu'),
Key('Hyper_L', 'Hyper L'),
Key('Hyper_R', 'Hyper R'),
Key('Help'),
Key('Direction_L', 'Direction L'),
Key('Direction_R', 'Direction R'),
### 7 bit printable ASCII
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', ' ', 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='Μ', qtest=False), # Qt assumes U+00B5 instead of U+03BC
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
### you are writing your own input method
### International & multi-key character composition
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', qtest=False), # Character set switch
# Key('script_switch'), # Alias for mode_switch
### Japanese keyboard support
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', 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, 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', 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'),
Key('Forward'),
Key('Stop'),
Key('Refresh'),
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', qtest=False),
Key('MediaTogglePlayPause', 'Toggle Media Play/Pause', qtest=False),
Key('HomePage', 'Home Page'),
Key('Favorites'),
Key('Search'),
Key('Standby'),
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', 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)', qtest=False),
Key('LaunchH', 'Launch (H)', qtest=False),
Key('TouchpadToggle', 'Touchpad Toggle', qtest=False),
Key('TouchpadOn', 'Touchpad On', qtest=False),
Key('TouchpadOff', 'Touchpad Off', qtest=False),
Key('MicMute', 'Microphone Mute', qtest=False),
Key('Red', qtest=False),
Key('Green', qtest=False),
Key('Yellow', qtest=False),
Key('Blue', qtest=False),
Key('ChannelUp', 'Channel Up', qtest=False),
Key('ChannelDown', 'Channel Down', qtest=False),
Key('Guide', qtest=False),
Key('Info', qtest=False),
Key('Settings', qtest=False),
Key('MicVolumeUp', 'Microphone Volume Up', qtest=False),
Key('MicVolumeDown', 'Microphone Volume Down', qtest=False),
Key('New', qtest=False),
Key('Open', qtest=False),
Key('Find', qtest=False),
Key('Undo', qtest=False),
Key('Redo', qtest=False),
Key('MediaLast', 'Media Last', qtest=False),
### Keypad navigation keys
Key('Select', qtest=False),
Key('Yes', qtest=False),
Key('No', qtest=False),
### Newer misc keys
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', 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', qtest=False),
Key('CameraFocus', 'Camera Focus', qtest=False),
Key('unknown', 'Unknown', qtest=False),
]

View File

@ -19,51 +19,41 @@
"""Tests for BaseKeyParser."""
import logging
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."""
kp = basekeyparser.BaseKeyParser(
0, supports_count=True, supports_chains=True)
kp = basekeyparser.BaseKeyParser(0, supports_count=True)
kp.execute = mock.Mock()
yield kp
@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
@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."""
@ -80,18 +70,25 @@ 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'),
('0', True, '', ''),
('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(info.to_event())
assert kp._count == count
assert kp._sequence == keyseq(command)
@pytest.mark.usefixtures('keyinput_bindings')
@ -106,18 +103,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('<ctrl+a>') 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('<ctrl+a>') not in keyparser.bindings
assert keyseq('foo') in keyparser.bindings
assert keyseq('<ctrl+x>') in keyparser.bindings
def test_read_config_modename_none(self, keyparser):
assert keyparser._modename is None
@ -134,140 +131,97 @@ 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
@pytest.mark.parametrize('warn_on_keychains', [True, False])
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
assert keyseq('a') in keyparser.bindings
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', keyparser.Type.special, None)
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_valid_key_count(self, fake_keyevent_factory, keyparser):
def test_valid_key_count(self, fake_keyevent, 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_A, modifier, text='A'))
keyparser.execute.assert_called_once_with(
'message-info ctrla', keyparser.Type.special, 5)
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)
def test_invalid_key(self, fake_keyevent_factory, keyparser):
keyparser.handle(fake_keyevent_factory(
Qt.Key_A, (Qt.ControlModifier | Qt.AltModifier)))
@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
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)
keyparser.handle(fake_keyevent_factory(Qt.Key_A, 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', keyparser.Type.special, 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', keyparser.Type.special, 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.execute.assert_called_once_with(
'message-info ctrla', keyparser.Type.special, None)
assert keyparser._keystring == ''
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
handle_text((Qt.Key_X, 'x'),
handle_text(Qt.Key_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)
assert keyparser._keystring == ''
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'))
keyparser.execute.assert_called_once_with(
'message-info 0', keyparser.Type.chain, None)
assert keyparser._keystring == ''
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 keyparser._keystring == ''
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, 'x'))
keyparser.execute.assert_called_once_with(
'message-info a', keyparser.Type.chain, None)
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):
"""Simulate a binding which involves shift."""
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)
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:
@ -279,42 +233,45 @@ 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)
assert keyparser._keystring == ''
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'))
calls = [mock.call('message-info 0', keyparser.Type.chain, None),
mock.call('message-info ba', keyparser.Type.chain, None)]
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 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 == ''
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 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 == ''
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
def test_clear_keystring(qtbot, keyparser):
"""Test that the keystring is cleared and the signal is emitted."""
keyparser._keystring = 'test'
keyparser._sequence = keyseq('test')
keyparser._count = '23'
with qtbot.waitSignal(keyparser.keystring_updated):
keyparser.clear_keystring()
assert keyparser._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()

View File

@ -0,0 +1,422 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014-2018 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
import operator
import pytest
from PyQt5.QtCore import Qt, QEvent, pyqtSignal
from PyQt5.QtGui import QKeyEvent, QKeySequence
from PyQt5.QtWidgets import QWidget
from tests.unit.keyinput import key_data
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))
return key
@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
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.
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 __init__(self, parent=None):
super().__init__(parent)
self.text = None
def keyPressEvent(self, e):
self.text = e.text()
self.got_text.emit()
class TestKeyInfoText:
@pytest.mark.parametrize('upper', [False, True])
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_tester(self, qtbot):
w = KeyTesterWidget()
qtbot.add_widget(w)
return w
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_tester.text.lower()
class TestKeyToString:
def test_to_string(self, qt_key):
assert keyutils._key_to_string(qt_key.member) == qt_key.name
def test_missing(self, monkeypatch):
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'
@pytest.mark.parametrize('key, modifiers, expected', [
(Qt.Key_A, Qt.NoModifier, 'a'),
(Qt.Key_A, Qt.ShiftModifier, 'A'),
(Qt.Key_Space, Qt.NoModifier, '<Space>'),
(Qt.Key_Space, Qt.ShiftModifier, '<Shift+Space>'),
(Qt.Key_Tab, Qt.ShiftModifier, '<Shift+Tab>'),
(Qt.Key_A, Qt.ControlModifier, '<Ctrl+a>'),
(Qt.Key_A, Qt.ControlModifier | Qt.ShiftModifier, '<Ctrl+Shift+a>'),
(Qt.Key_A,
Qt.ControlModifier | Qt.AltModifier | Qt.MetaModifier | Qt.ShiftModifier,
'<Meta+Ctrl+Alt+Shift+a>'),
(Qt.Key_Shift, Qt.ShiftModifier, '<Shift>'),
(Qt.Key_Shift, Qt.ShiftModifier | Qt.ControlModifier, '<Ctrl+Shift>'),
])
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']),
('>a', ['>', 'a']),
('aA', ['a', 'Shift+A']),
('a<Ctrl+a>b', ['a', 'ctrl+a', 'b']),
('<Ctrl+a>a', ['ctrl+a', 'a']),
('a<Ctrl+a>', ['a', 'ctrl+a']),
])
def test_parse_keystr(keystr, parts):
assert list(keyutils._parse_keystring(keystr)) == parts
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(Qt.Key_unknown)
def test_init_invalid(self):
with pytest.raises(AssertionError):
keyutils.KeySequence(-1)
@pytest.mark.parametrize('orig, normalized', [
('<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>'),
('<a>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) == ("<qutebrowser.keyinput.keyutils.KeySequence "
"keys='<Ctrl+a>B'>")
@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', '<a>', operator.eq, True),
('a', '<Shift-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', '<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),
('<Ctrl-a>', 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('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('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<Ctrl+Shift+b>'),
# Modifier stripping with symbols
('', Qt.Key_Colon, Qt.NoModifier, ':', ':'),
('', Qt.Key_Colon, Qt.ShiftModifier, ':', ':'),
('', Qt.Key_Colon, Qt.ControlModifier | Qt.ShiftModifier, ':',
'<Ctrl+Shift+:>'),
# Handling of Backtab
('', Qt.Key_Backtab, Qt.NoModifier, '', '<Backtab>'),
('', Qt.Key_Backtab, Qt.ShiftModifier, '', '<Shift+Tab>'),
('', Qt.Key_Backtab, Qt.ControlModifier | Qt.ShiftModifier, '',
'<Control+Shift+Tab>'),
])
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', [
('<Control-x>', keyutils.KeySequence(Qt.ControlModifier | Qt.Key_X)),
('<Meta-x>', keyutils.KeySequence(Qt.MetaModifier | Qt.Key_X)),
('<Ctrl-Alt-y>',
keyutils.KeySequence(Qt.ControlModifier | Qt.AltModifier | Qt.Key_Y)),
('x', keyutils.KeySequence(Qt.Key_X)),
('X', keyutils.KeySequence(Qt.ShiftModifier | Qt.Key_X)),
('<Escape>', keyutils.KeySequence(Qt.Key_Escape)),
('xyz', keyutils.KeySequence(Qt.Key_X, Qt.Key_Y, Qt.Key_Z)),
('<Control-x><Meta-y>',
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)),
('<a', keyutils.KeySequence(Qt.Key_Less, Qt.Key_A)),
('<alt+greater>',
keyutils.KeySequence(Qt.Key_Greater | Qt.AltModifier)),
('<alt+less>',
keyutils.KeySequence(Qt.Key_Less | Qt.AltModifier)),
('<alt+<>', keyutils.KeyParseError),
('<alt+>>', keyutils.KeyParseError),
('<blub>', 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():
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),
(Qt.Key_Tab, False),
(Qt.Key_Backtab, False),
(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_ydiaeresis, True),
(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),
(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

View File

@ -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

View File

@ -25,7 +25,7 @@ from PyQt5.QtCore import Qt
import pytest
from qutebrowser.keyinput import modeparsers
from qutebrowser.keyinput import modeparsers, keyutils
class TestsNormalKeyParser:
@ -49,36 +49,35 @@ 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.execute.assert_called_with(
'message-info ba', keyparser.Type.chain, None)
assert keyparser._keystring == ''
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()
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('')

View File

@ -106,7 +106,7 @@ class TestFullscreenNotification:
@pytest.mark.parametrize('bindings, text', [
({'<escape>': 'fullscreen --leave'},
"Press Escape to exit fullscreen."),
"Press <Escape> to exit fullscreen."),
({'<escape>': 'fullscreen'}, "Page is now fullscreen."),
({'a': 'fullscreen --leave'}, "Press a to exit fullscreen."),
({}, "Page is now fullscreen."),

View File

@ -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
@ -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', [
('<Control-x>', utils.KeyInfo(Qt.Key_X, Qt.ControlModifier, '')),
('<Meta-x>', utils.KeyInfo(Qt.Key_X, Qt.MetaModifier, '')),
('<Ctrl-Alt-y>',
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')),
('<Escape>', utils.KeyInfo(Qt.Key_Escape, Qt.NoModifier, '')),
('foobar', utils.KeyParseError),
('x, y', utils.KeyParseError),
('xyz', utils.KeyParseError),
('Escape', utils.KeyParseError),
('<Ctrl-x>, <Ctrl-y>', 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', [
('<Control-x>', [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')]),
('<Control-x><Meta-x>', 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', [
('<Control-x>', True),
('<Meta-x>', True),
('<Ctrl-Alt-y>', True),
('x', False),
('X', False),
('<Escape>', True),
('foobar', False),
('foo>', False),
('<foo', False),
('<<', False),
])
def test_is_special_key(keystr, expected):
assert utils.is_special_key(keystr) == expected
def test_random_port():
port = utils.random_port()
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
@ -931,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))