Split KeyParser into KeyParser (non-chain) and KeyChainParser
This commit is contained in:
parent
6311deb6b0
commit
ecdd887664
1
TODO
1
TODO
@ -1,7 +1,6 @@
|
||||
keyparser foo
|
||||
=============
|
||||
|
||||
- Create a SimpleKeyParser and inherit KeyParser from that.
|
||||
- Handle keybind to get out of insert mode (e.g. esc)
|
||||
- Add more element-selection-detection code (with options?) based on:
|
||||
-> javascript: http://stackoverflow.com/a/2848120/2085149
|
||||
|
@ -30,15 +30,15 @@ import qutebrowser.utils.message as message
|
||||
import qutebrowser.utils.url as urlutils
|
||||
import qutebrowser.utils.modemanager as modemanager
|
||||
import qutebrowser.utils.webelem as webelem
|
||||
from qutebrowser.utils.keyparser import KeyParser
|
||||
from qutebrowser.utils.keyparser import KeyChainParser
|
||||
|
||||
|
||||
ElemTuple = namedtuple('ElemTuple', 'elem, label')
|
||||
|
||||
|
||||
class HintKeyParser(KeyParser):
|
||||
class HintKeyParser(KeyChainParser):
|
||||
|
||||
"""KeyParser for hints.
|
||||
"""KeyChainParser for hints.
|
||||
|
||||
Class attributes:
|
||||
supports_count: If the keyparser should support counts.
|
||||
|
@ -23,19 +23,18 @@ Module attributes:
|
||||
|
||||
import logging
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot
|
||||
from PyQt5.QtCore import pyqtSignal
|
||||
|
||||
import qutebrowser.config.config as config
|
||||
from qutebrowser.utils.keyparser import KeyParser
|
||||
from qutebrowser.utils.keyparser import KeyChainParser
|
||||
from qutebrowser.commands.parsers import (CommandParser, ArgumentCountError,
|
||||
NoSuchCommandError)
|
||||
|
||||
STARTCHARS = ":/?"
|
||||
|
||||
|
||||
class CommandKeyParser(KeyParser):
|
||||
class CommandKeyParser(KeyChainParser):
|
||||
|
||||
"""Keyparser for command bindings.
|
||||
"""KeyChainParser for command bindings.
|
||||
|
||||
Class attributes:
|
||||
supports_count: If the keyparser should support counts.
|
||||
@ -54,7 +53,7 @@ class CommandKeyParser(KeyParser):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.commandparser = CommandParser()
|
||||
self.read_config()
|
||||
self.read_config('keybind')
|
||||
|
||||
def _run_or_fill(self, cmdstr, count=None, ignore_exc=True):
|
||||
"""Run the command in cmdstr or fill the statusbar if args missing.
|
||||
@ -98,29 +97,3 @@ class CommandKeyParser(KeyParser):
|
||||
def execute(self, cmdstr, count=None):
|
||||
"""Handle a completed keychain."""
|
||||
self._run_or_fill(cmdstr, count, ignore_exc=False)
|
||||
|
||||
@pyqtSlot(str, str)
|
||||
def on_config_changed(self, section, _option):
|
||||
"""Re-read the config if a keybinding was changed."""
|
||||
if section == 'keybind':
|
||||
self.read_config()
|
||||
|
||||
def read_config(self):
|
||||
"""Read the configuration.
|
||||
|
||||
Config format: key = command, e.g.:
|
||||
gg = scrollstart
|
||||
"""
|
||||
sect = config.instance['keybind']
|
||||
if not sect.items():
|
||||
logging.warn("No keybindings defined!")
|
||||
for (key, cmd) in sect.items():
|
||||
if key.startswith('<') and key.endswith('>'):
|
||||
# normalize keystring
|
||||
keystr = self._normalize_keystr(key[1:-1])
|
||||
logging.debug('registered special key: {} -> {}'.format(keystr,
|
||||
cmd))
|
||||
self.special_bindings[keystr] = cmd
|
||||
else:
|
||||
logging.debug('registered key: {} -> {}'.format(key, cmd))
|
||||
self.bindings[key] = cmd
|
||||
|
@ -21,7 +21,7 @@ import re
|
||||
import logging
|
||||
from functools import partial
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, Qt, QObject, QTimer
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QObject, QTimer
|
||||
from PyQt5.QtGui import QKeySequence
|
||||
|
||||
import qutebrowser.config.config as config
|
||||
@ -29,47 +29,48 @@ import qutebrowser.config.config as config
|
||||
|
||||
class KeyParser(QObject):
|
||||
|
||||
"""Parser for vim-like key sequences.
|
||||
"""Parser for non-chained Qt keypresses ("special bindings").
|
||||
|
||||
We call these special because chained keypresses are the "normal" ones in
|
||||
qutebrowser, however there are some cases where we can _only_ use special
|
||||
keys (like in insert mode).
|
||||
|
||||
Not intended to be instantiated directly. Subclasses have to override
|
||||
execute() to do whatever they want to.
|
||||
|
||||
Class Attributes:
|
||||
MATCH_PARTIAL: Constant for a partial match (no keychain matched yet,
|
||||
but it's still possible in the future.
|
||||
MATCH_DEFINITIVE: Constant for a full match (keychain matches exactly).
|
||||
MATCH_AMBIGUOUS: There are both a partial and a definitive match.
|
||||
MATCH_NONE: Constant for no match (no more matches possible).
|
||||
supports_count: If the keyparser should support counts.
|
||||
|
||||
Attributes:
|
||||
_keystring: The currently entered key sequence
|
||||
_timer: QTimer for delayed execution.
|
||||
bindings: Bound keybindings
|
||||
special_bindings: Bound special bindings (<Foo>).
|
||||
|
||||
Signals:
|
||||
keystring_updated: Emitted when the keystring is updated.
|
||||
arg: New keystring.
|
||||
_confsectname: The name of the configsection.
|
||||
"""
|
||||
|
||||
keystring_updated = pyqtSignal(str)
|
||||
|
||||
MATCH_PARTIAL = 0
|
||||
MATCH_DEFINITIVE = 1
|
||||
MATCH_AMBIGUOUS = 2
|
||||
MATCH_NONE = 3
|
||||
|
||||
supports_count = False
|
||||
|
||||
def __init__(self, parent=None, bindings=None, special_bindings=None):
|
||||
def __init__(self, parent=None, special_bindings=None):
|
||||
super().__init__(parent)
|
||||
self._timer = None
|
||||
self._keystring = ''
|
||||
self.bindings = {} if bindings is None else bindings
|
||||
self._confsectname = None
|
||||
self.special_bindings = ({} if special_bindings is None
|
||||
else special_bindings)
|
||||
|
||||
def _normalize_keystr(self, 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.
|
||||
"""
|
||||
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 + '+')
|
||||
keystr = QKeySequence(keystr).toString()
|
||||
return keystr
|
||||
|
||||
def _handle_special_key(self, e):
|
||||
"""Handle a new keypress with special keys (<Foo>).
|
||||
|
||||
@ -104,6 +105,99 @@ class KeyParser(QObject):
|
||||
self.execute(cmdstr)
|
||||
return True
|
||||
|
||||
def handle(self, e):
|
||||
"""Handle a new keypress and call the respective handlers.
|
||||
|
||||
Args:
|
||||
e: the KeyPressEvent from Qt
|
||||
"""
|
||||
return self._handle_special_key(e)
|
||||
|
||||
def execute(self, cmdstr, count=None):
|
||||
"""Execute an action when a binding is triggered.
|
||||
|
||||
Needs to be overriden in superclasses."""
|
||||
raise NotImplementedError
|
||||
|
||||
def read_config(self, sectname=None):
|
||||
"""Read the configuration.
|
||||
|
||||
Config format: key = command, e.g.:
|
||||
<Ctrl+Q> = quit
|
||||
|
||||
Args:
|
||||
sectname: Name of the section to read.
|
||||
"""
|
||||
if sectname is None:
|
||||
if self._confsectname is None:
|
||||
raise ValueError("read_config called with no section, but "
|
||||
"None defined so far!")
|
||||
sectname = self._confsectname
|
||||
else:
|
||||
self._confsectname = sectname
|
||||
sect = config.instance[sectname]
|
||||
if not sect.items():
|
||||
logging.warn("No keybindings defined!")
|
||||
for (key, cmd) in sect.items():
|
||||
if key.startswith('<') and key.endswith('>'):
|
||||
keystr = self._normalize_keystr(key[1:-1])
|
||||
logging.debug('registered special key: {} -> {}'.format(keystr,
|
||||
cmd))
|
||||
self.special_bindings[keystr] = cmd
|
||||
|
||||
@pyqtSlot(str, str)
|
||||
def on_config_changed(self, section, _option):
|
||||
"""Re-read the config if a keybinding was changed."""
|
||||
if self._confsectname is None:
|
||||
raise AttributeError("on_config_changed called but no section "
|
||||
"defined!")
|
||||
if section == self._confsectname:
|
||||
self.read_config()
|
||||
|
||||
|
||||
class KeyChainParser(KeyParser):
|
||||
|
||||
"""Parser for vim-like key sequences.
|
||||
|
||||
Not intended to be instantiated directly. Subclasses have to override
|
||||
execute() to do whatever they want to.
|
||||
|
||||
Class Attributes:
|
||||
MATCH_PARTIAL: Constant for a partial match (no keychain matched yet,
|
||||
but it's still possible in the future.
|
||||
MATCH_DEFINITIVE: Constant for a full match (keychain matches exactly).
|
||||
MATCH_AMBIGUOUS: There are both a partial and a definitive match.
|
||||
MATCH_NONE: Constant for no match (no more matches possible).
|
||||
supports_count: If the keyparser should support counts.
|
||||
|
||||
Attributes:
|
||||
_keystring: The currently entered key sequence
|
||||
_timer: QTimer for delayed execution.
|
||||
bindings: Bound keybindings
|
||||
|
||||
Signals:
|
||||
keystring_updated: Emitted when the keystring is updated.
|
||||
arg: New keystring.
|
||||
"""
|
||||
|
||||
# This is an abstract superclass of an abstract class.
|
||||
# pylint: disable=abstract-method
|
||||
|
||||
keystring_updated = pyqtSignal(str)
|
||||
|
||||
MATCH_PARTIAL = 0
|
||||
MATCH_DEFINITIVE = 1
|
||||
MATCH_AMBIGUOUS = 2
|
||||
MATCH_NONE = 3
|
||||
|
||||
supports_count = False
|
||||
|
||||
def __init__(self, parent=None, bindings=None, special_bindings=None):
|
||||
super().__init__(parent, special_bindings)
|
||||
self._timer = None
|
||||
self._keystring = ''
|
||||
self.bindings = {} if bindings is None else bindings
|
||||
|
||||
def _handle_single_key(self, e):
|
||||
"""Handle a new keypress with a single key (no modifiers).
|
||||
|
||||
@ -228,28 +322,6 @@ class KeyParser(QObject):
|
||||
count))
|
||||
self._timer.start()
|
||||
|
||||
def _normalize_keystr(self, 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.
|
||||
"""
|
||||
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 + '+')
|
||||
keystr = QKeySequence(keystr).toString()
|
||||
return keystr
|
||||
|
||||
def delayed_exec(self, command, count):
|
||||
"""Execute a delayed command.
|
||||
|
||||
@ -265,12 +337,8 @@ class KeyParser(QObject):
|
||||
self.keystring_updated.emit(self._keystring)
|
||||
self.execute(command, count)
|
||||
|
||||
def execute(self, cmdstr, count=None):
|
||||
"""Execute an action when a binding is triggered."""
|
||||
raise NotImplementedError
|
||||
|
||||
def handle(self, e):
|
||||
"""Handle a new keypress and call the respective handlers.
|
||||
"""Override KeyParser.handle() to also handle keychains.
|
||||
|
||||
Args:
|
||||
e: the KeyPressEvent from Qt
|
||||
@ -278,7 +346,28 @@ class KeyParser(QObject):
|
||||
Emit:
|
||||
keystring_updated: If a new keystring should be set.
|
||||
"""
|
||||
handled = self._handle_special_key(e)
|
||||
if not handled:
|
||||
self._handle_single_key(e)
|
||||
handled = super().handle(e)
|
||||
if handled:
|
||||
return True
|
||||
handled = self._handle_single_key(e)
|
||||
self.keystring_updated.emit(self._keystring)
|
||||
return handled
|
||||
|
||||
def read_config(self, sectname=None):
|
||||
"""Extend KeyParser.read_config to also read keychains.
|
||||
|
||||
Config format: key = command, e.g.:
|
||||
<Ctrl+Q> = quit
|
||||
|
||||
Args:
|
||||
sectname: Name of the section to read.
|
||||
"""
|
||||
super().read_config(sectname)
|
||||
sect = config.instance[sectname]
|
||||
for (key, cmd) in sect.items():
|
||||
if key.startswith('<') and key.endswith('>'):
|
||||
# Already registered by superclass
|
||||
pass
|
||||
else:
|
||||
logging.debug('registered key: {} -> {}'.format(key, cmd))
|
||||
self.bindings[key] = cmd
|
||||
|
Loading…
Reference in New Issue
Block a user