Unify KeyParsers again

This commit is contained in:
Florian Bruhin 2014-04-24 22:40:16 +02:00
parent 9ab8f42e20
commit 7a6a605702
4 changed files with 81 additions and 125 deletions

View File

@ -19,26 +19,25 @@
from PyQt5.QtCore import pyqtSignal, Qt from PyQt5.QtCore import pyqtSignal, Qt
from qutebrowser.keyinput.keyparser import KeyChainParser from qutebrowser.keyinput.keyparser import KeyParser
class HintKeyParser(KeyChainParser): class HintKeyParser(KeyParser):
"""KeyChainParser for hints. """KeyChainParser for hints.
Class attributes:
supports_count: If the keyparser should support counts.
Signals: Signals:
fire_hint: When a hint keybinding was completed. fire_hint: When a hint keybinding was completed.
Arg: the keystring/hint string pressed. Arg: the keystring/hint string pressed.
abort_hinting: Esc pressed, so abort hinting. abort_hinting: Esc pressed, so abort hinting.
""" """
supports_count = False
fire_hint = pyqtSignal(str) fire_hint = pyqtSignal(str)
abort_hinting = pyqtSignal() abort_hinting = pyqtSignal()
def __init__(self, parent=None):
super().__init__(parent, supports_count=False, supports_chains=True)
def _handle_special_key(self, e): def _handle_special_key(self, e):
"""Handle the escape key. """Handle the escape key.

View File

@ -28,7 +28,7 @@ class InsertKeyParser(KeyParser):
"""KeyParser for insert mode.""" """KeyParser for insert mode."""
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__(parent) super().__init__(parent, supports_chains=False)
self.read_config('keybind.insert') self.read_config('keybind.insert')
def execute(self, cmdstr, count=None): def execute(self, cmdstr, count=None):

View File

@ -29,25 +29,57 @@ import qutebrowser.config.config as config
class KeyParser(QObject): class KeyParser(QObject):
"""Parser for non-chained Qt keypresses ("special bindings"). """Parser for vim-like key sequences and shortcuts.
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 Not intended to be instantiated directly. Subclasses have to override
execute() to do whatever they want to. 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).
Attributes: Attributes:
bindings: Bound keybindings
special_bindings: Bound special bindings (<Foo>). special_bindings: Bound special bindings (<Foo>).
_keystring: The currently entered key sequence
_timer: QTimer for delayed execution.
_confsectname: The name of the configsection. _confsectname: The name of the configsection.
_supports_count: Whether count is supported
_supports_chains: Whether keychains are supported
Signals:
keystring_updated: Emitted when the keystring is updated.
arg: New keystring.
""" """
def __init__(self, parent=None, special_bindings=None): keystring_updated = pyqtSignal(str)
MATCH_PARTIAL = 0
MATCH_DEFINITIVE = 1
MATCH_AMBIGUOUS = 2
MATCH_NONE = 3
def __init__(self, parent=None, bindings=None, special_bindings=None,
supports_count=None, supports_chains=False):
super().__init__(parent) super().__init__(parent)
self._timer = None
self._confsectname = None self._confsectname = None
self._keystring = ''
if supports_count is None:
supports_count = supports_chains
self._supports_count = supports_count
self._supports_chains = supports_chains
self.special_bindings = ({} if special_bindings is None self.special_bindings = ({} if special_bindings is None
else special_bindings) else special_bindings)
if bindings is None:
self.bindings = {}
elif supports_chains:
self.bindsings = bindings
else:
raise ValueError("bindings given with supports_chains=False!")
def _normalize_keystr(self, keystr): def _normalize_keystr(self, keystr):
"""Normalize a keystring like Ctrl-Q to a keystring like Ctrl+Q. """Normalize a keystring like Ctrl-Q to a keystring like Ctrl+Q.
@ -105,99 +137,6 @@ class KeyParser(QObject):
self.execute(cmdstr) self.execute(cmdstr)
return True 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): def _handle_single_key(self, e):
"""Handle a new keypress with a single key (no modifiers). """Handle a new keypress with a single key (no modifiers).
@ -220,7 +159,7 @@ class KeyChainParser(KeyParser):
self._stop_delayed_exec() self._stop_delayed_exec()
self._keystring += txt self._keystring += txt
if self.supports_count: if self._supports_count:
(countstr, cmd_input) = re.match(r'^(\d*)(.*)', (countstr, cmd_input) = re.match(r'^(\d*)(.*)',
self._keystring).groups() self._keystring).groups()
count = int(countstr) if countstr else None count = int(countstr) if countstr else None
@ -338,7 +277,7 @@ class KeyChainParser(KeyParser):
self.execute(command, count) self.execute(command, count)
def handle(self, e): def handle(self, e):
"""Override KeyParser.handle() to also handle keychains. """Handle a new keypress and call the respective handlers.
Args: Args:
e: the KeyPressEvent from Qt e: the KeyPressEvent from Qt
@ -346,15 +285,15 @@ class KeyChainParser(KeyParser):
Emit: Emit:
keystring_updated: If a new keystring should be set. keystring_updated: If a new keystring should be set.
""" """
handled = super().handle(e) handled = self._handle_special_key(e)
if handled: if handled or not self._supports_chains:
return True return handled
handled = self._handle_single_key(e) handled = self._handle_single_key(e)
self.keystring_updated.emit(self._keystring) self.keystring_updated.emit(self._keystring)
return handled return handled
def read_config(self, sectname=None): def read_config(self, sectname=None):
"""Extend KeyParser.read_config to also read keychains. """Read the configuration.
Config format: key = command, e.g.: Config format: key = command, e.g.:
<Ctrl+Q> = quit <Ctrl+Q> = quit
@ -362,12 +301,35 @@ class KeyChainParser(KeyParser):
Args: Args:
sectname: Name of the section to read. sectname: Name of the section to read.
""" """
super().read_config(sectname) 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] sect = config.instance[sectname]
if not sect.items():
logging.warn("No keybindings defined!")
for (key, cmd) in sect.items(): for (key, cmd) in sect.items():
if key.startswith('<') and key.endswith('>'): if key.startswith('<') and key.endswith('>'):
# Already registered by superclass keystr = self._normalize_keystr(key[1:-1])
pass logging.debug("registered special key: {} -> {}".format(keystr,
else: cmd))
logging.debug('registered key: {} -> {}'.format(key, cmd)) self.special_bindings[keystr] = cmd
elif self._supports_chains:
logging.debug("registered key: {} -> {}".format(key, cmd))
self.bindings[key] = cmd self.bindings[key] = cmd
else:
logging.warn("Ignoring keychain \"{}\" in section \"{}\" "
"because keychains are not supported there.".format(
key, sectname))
@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()

View File

@ -26,28 +26,23 @@ import logging
from PyQt5.QtCore import pyqtSignal from PyQt5.QtCore import pyqtSignal
import qutebrowser.utils.message as message import qutebrowser.utils.message as message
from qutebrowser.keyinput.keyparser import KeyChainParser from qutebrowser.keyinput.keyparser import KeyParser
from qutebrowser.commands.parsers import (CommandParser, ArgumentCountError, from qutebrowser.commands.parsers import (CommandParser, ArgumentCountError,
NoSuchCommandError) NoSuchCommandError)
STARTCHARS = ":/?" STARTCHARS = ":/?"
class CommandKeyParser(KeyChainParser): class CommandKeyParser(KeyParser):
"""KeyChainParser for command bindings. """KeyChainParser for command bindings.
Class attributes:
supports_count: If the keyparser should support counts.
Attributes: Attributes:
commandparser: Commandparser instance. commandparser: Commandparser instance.
""" """
supports_count = True
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__(parent) super().__init__(parent, supports_count=True, supports_chains=True)
self.commandparser = CommandParser() self.commandparser = CommandParser()
self.read_config('keybind') self.read_config('keybind')