Split KeyParser into KeyParser (non-chain) and KeyChainParser

This commit is contained in:
Florian Bruhin 2014-04-24 19:21:38 +02:00
parent 6311deb6b0
commit ecdd887664
4 changed files with 159 additions and 98 deletions

3
TODO
View File

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

View File

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

View File

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

View File

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