Merge branch 'keys'
This commit is contained in:
commit
155a1901c0
@ -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.
|
||||
|
@ -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:[<Shift-Tab>]+: +pass:[prompt-item-focus prev]+
|
||||
* +pass:[<Tab>]+: +pass:[prompt-item-focus next]+
|
||||
* +pass:[<Up>]+: +pass:[prompt-item-focus prev]+
|
||||
* +pass:[n]+: +pass:[prompt-accept no]+
|
||||
* +pass:[y]+: +pass:[prompt-accept yes]+
|
||||
- +pass:[register]+:
|
||||
|
||||
* +pass:[<Escape>]+: +pass:[leave-mode]+
|
||||
- +pass:[yesno]+:
|
||||
|
||||
* +pass:[<Alt-Shift-Y>]+: +pass:[prompt-yank --sel]+
|
||||
* +pass:[<Alt-Y>]+: +pass:[prompt-yank]+
|
||||
* +pass:[<Escape>]+: +pass:[leave-mode]+
|
||||
* +pass:[<Return>]+: +pass:[prompt-accept]+
|
||||
* +pass:[n]+: +pass:[prompt-accept no]+
|
||||
* +pass:[y]+: +pass:[prompt-accept yes]+
|
||||
|
||||
[[bindings.key_mappings]]
|
||||
=== bindings.key_mappings
|
||||
|
@ -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()
|
||||
|
@ -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):
|
||||
|
@ -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))
|
||||
|
||||
|
@ -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))
|
||||
|
@ -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)
|
||||
|
@ -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.
|
||||
|
@ -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."""
|
||||
|
@ -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))
|
||||
|
@ -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('')
|
||||
|
@ -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)
|
||||
|
455
qutebrowser/keyinput/keyutils.py
Normal file
455
qutebrowser/keyinput/keyutils.py
Normal 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
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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():
|
||||
|
@ -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)
|
||||
|
@ -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.")
|
||||
|
@ -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]
|
||||
|
@ -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'),
|
||||
|
@ -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())),
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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."""
|
||||
|
@ -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.', ''),
|
||||
],
|
||||
|
@ -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:
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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),
|
||||
|
@ -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
|
||||
|
588
tests/unit/keyinput/key_data.py
Normal file
588
tests/unit/keyinput/key_data.py
Normal 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),
|
||||
]
|
@ -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()
|
||||
|
422
tests/unit/keyinput/test_keyutils.py
Normal file
422
tests/unit/keyinput/test_keyutils.py
Normal 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
|
@ -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
|
||||
|
@ -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('')
|
||||
|
@ -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."),
|
||||
|
@ -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))
|
||||
|
Loading…
Reference in New Issue
Block a user