From ecdd8876645bfbb06bdaf0819c4af4199c983cee Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 24 Apr 2014 19:21:38 +0200 Subject: [PATCH] Split KeyParser into KeyParser (non-chain) and KeyChainParser --- TODO | 3 +- qutebrowser/browser/hints.py | 6 +- qutebrowser/commands/keys.py | 37 +----- qutebrowser/utils/keyparser.py | 211 +++++++++++++++++++++++---------- 4 files changed, 159 insertions(+), 98 deletions(-) diff --git a/TODO b/TODO index b9c514f5c..972c80736 100644 --- a/TODO +++ b/TODO @@ -1,8 +1,7 @@ keyparser foo ============= -- Create a SimpleKeyParser and inherit KeyParser from that. - - Handle keybind to get out of insert mode (e.g. esc) +- 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 -> microFocusChanged and check active element via: diff --git a/qutebrowser/browser/hints.py b/qutebrowser/browser/hints.py index 04c872a93..ea9044dd9 100644 --- a/qutebrowser/browser/hints.py +++ b/qutebrowser/browser/hints.py @@ -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. diff --git a/qutebrowser/commands/keys.py b/qutebrowser/commands/keys.py index 6474a3f40..cf11d21ed 100644 --- a/qutebrowser/commands/keys.py +++ b/qutebrowser/commands/keys.py @@ -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 diff --git a/qutebrowser/utils/keyparser.py b/qutebrowser/utils/keyparser.py index 177e17eb2..74a292b4b 100644 --- a/qutebrowser/utils/keyparser.py +++ b/qutebrowser/utils/keyparser.py @@ -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 (). - - 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 (). @@ -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.: + = 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) - self.keystring_updated.emit(self._keystring) + 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.: + = 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