Split KeyParser into KeyParser (non-chain) and KeyChainParser
This commit is contained in:
parent
6311deb6b0
commit
ecdd887664
3
TODO
3
TODO
@ -1,8 +1,7 @@
|
|||||||
keyparser foo
|
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:
|
- Add more element-selection-detection code (with options?) based on:
|
||||||
-> javascript: http://stackoverflow.com/a/2848120/2085149
|
-> javascript: http://stackoverflow.com/a/2848120/2085149
|
||||||
-> microFocusChanged and check active element via:
|
-> microFocusChanged and check active element via:
|
||||||
|
@ -30,15 +30,15 @@ import qutebrowser.utils.message as message
|
|||||||
import qutebrowser.utils.url as urlutils
|
import qutebrowser.utils.url as urlutils
|
||||||
import qutebrowser.utils.modemanager as modemanager
|
import qutebrowser.utils.modemanager as modemanager
|
||||||
import qutebrowser.utils.webelem as webelem
|
import qutebrowser.utils.webelem as webelem
|
||||||
from qutebrowser.utils.keyparser import KeyParser
|
from qutebrowser.utils.keyparser import KeyChainParser
|
||||||
|
|
||||||
|
|
||||||
ElemTuple = namedtuple('ElemTuple', 'elem, label')
|
ElemTuple = namedtuple('ElemTuple', 'elem, label')
|
||||||
|
|
||||||
|
|
||||||
class HintKeyParser(KeyParser):
|
class HintKeyParser(KeyChainParser):
|
||||||
|
|
||||||
"""KeyParser for hints.
|
"""KeyChainParser for hints.
|
||||||
|
|
||||||
Class attributes:
|
Class attributes:
|
||||||
supports_count: If the keyparser should support counts.
|
supports_count: If the keyparser should support counts.
|
||||||
|
@ -23,19 +23,18 @@ Module attributes:
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot
|
from PyQt5.QtCore import pyqtSignal
|
||||||
|
|
||||||
import qutebrowser.config.config as config
|
from qutebrowser.utils.keyparser import KeyChainParser
|
||||||
from qutebrowser.utils.keyparser import KeyParser
|
|
||||||
from qutebrowser.commands.parsers import (CommandParser, ArgumentCountError,
|
from qutebrowser.commands.parsers import (CommandParser, ArgumentCountError,
|
||||||
NoSuchCommandError)
|
NoSuchCommandError)
|
||||||
|
|
||||||
STARTCHARS = ":/?"
|
STARTCHARS = ":/?"
|
||||||
|
|
||||||
|
|
||||||
class CommandKeyParser(KeyParser):
|
class CommandKeyParser(KeyChainParser):
|
||||||
|
|
||||||
"""Keyparser for command bindings.
|
"""KeyChainParser for command bindings.
|
||||||
|
|
||||||
Class attributes:
|
Class attributes:
|
||||||
supports_count: If the keyparser should support counts.
|
supports_count: If the keyparser should support counts.
|
||||||
@ -54,7 +53,7 @@ class CommandKeyParser(KeyParser):
|
|||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.commandparser = CommandParser()
|
self.commandparser = CommandParser()
|
||||||
self.read_config()
|
self.read_config('keybind')
|
||||||
|
|
||||||
def _run_or_fill(self, cmdstr, count=None, ignore_exc=True):
|
def _run_or_fill(self, cmdstr, count=None, ignore_exc=True):
|
||||||
"""Run the command in cmdstr or fill the statusbar if args missing.
|
"""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):
|
def execute(self, cmdstr, count=None):
|
||||||
"""Handle a completed keychain."""
|
"""Handle a completed keychain."""
|
||||||
self._run_or_fill(cmdstr, count, ignore_exc=False)
|
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
|
import logging
|
||||||
from functools import partial
|
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
|
from PyQt5.QtGui import QKeySequence
|
||||||
|
|
||||||
import qutebrowser.config.config as config
|
import qutebrowser.config.config as config
|
||||||
@ -29,47 +29,48 @@ import qutebrowser.config.config as config
|
|||||||
|
|
||||||
class KeyParser(QObject):
|
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
|
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).
|
|
||||||
supports_count: If the keyparser should support counts.
|
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
_keystring: The currently entered key sequence
|
|
||||||
_timer: QTimer for delayed execution.
|
|
||||||
bindings: Bound keybindings
|
|
||||||
special_bindings: Bound special bindings (<Foo>).
|
special_bindings: Bound special bindings (<Foo>).
|
||||||
|
_confsectname: The name of the configsection.
|
||||||
Signals:
|
|
||||||
keystring_updated: Emitted when the keystring is updated.
|
|
||||||
arg: New keystring.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
keystring_updated = pyqtSignal(str)
|
def __init__(self, parent=None, special_bindings=None):
|
||||||
|
|
||||||
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)
|
super().__init__(parent)
|
||||||
self._timer = None
|
self._confsectname = None
|
||||||
self._keystring = ''
|
|
||||||
self.bindings = {} if bindings is None else bindings
|
|
||||||
self.special_bindings = ({} if special_bindings is None
|
self.special_bindings = ({} if special_bindings is None
|
||||||
else special_bindings)
|
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):
|
def _handle_special_key(self, e):
|
||||||
"""Handle a new keypress with special keys (<Foo>).
|
"""Handle a new keypress with special keys (<Foo>).
|
||||||
|
|
||||||
@ -104,6 +105,99 @@ 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).
|
||||||
|
|
||||||
@ -228,28 +322,6 @@ class KeyParser(QObject):
|
|||||||
count))
|
count))
|
||||||
self._timer.start()
|
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):
|
def delayed_exec(self, command, count):
|
||||||
"""Execute a delayed command.
|
"""Execute a delayed command.
|
||||||
|
|
||||||
@ -265,12 +337,8 @@ class KeyParser(QObject):
|
|||||||
self.keystring_updated.emit(self._keystring)
|
self.keystring_updated.emit(self._keystring)
|
||||||
self.execute(command, count)
|
self.execute(command, count)
|
||||||
|
|
||||||
def execute(self, cmdstr, count=None):
|
|
||||||
"""Execute an action when a binding is triggered."""
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def handle(self, e):
|
def handle(self, e):
|
||||||
"""Handle a new keypress and call the respective handlers.
|
"""Override KeyParser.handle() to also handle keychains.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
e: the KeyPressEvent from Qt
|
e: the KeyPressEvent from Qt
|
||||||
@ -278,7 +346,28 @@ class KeyParser(QObject):
|
|||||||
Emit:
|
Emit:
|
||||||
keystring_updated: If a new keystring should be set.
|
keystring_updated: If a new keystring should be set.
|
||||||
"""
|
"""
|
||||||
handled = self._handle_special_key(e)
|
handled = super().handle(e)
|
||||||
if not handled:
|
if handled:
|
||||||
self._handle_single_key(e)
|
return True
|
||||||
self.keystring_updated.emit(self._keystring)
|
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