qutebrowser/qutebrowser/keyinput/basekeyparser.py
Gyorgy Orban 98c6b49cde use enum module instead or usertypes.enum
Remove the usertypes.enum from the source and use
the standard enum module instead.
Enum start number is available since python 3.5
2017-11-02 18:56:11 +01:00

331 lines
12 KiB
Python

# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014-2017 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""Base class for vim-like key sequence parser."""
import enum
import re
import unicodedata
from PyQt5.QtCore import pyqtSignal, QObject
from qutebrowser.config import config
from qutebrowser.utils import usertypes, log, utils
class BaseKeyParser(QObject):
"""Parser for vim-like key sequences and shortcuts.
Not intended to be instantiated directly. Subclasses have to override
execute() to do whatever they want to.
Class Attributes:
Match: types of a match between a binding and the keystring.
partial: No keychain matched yet, but it's still possible in the
future.
definitive: Keychain matches exactly.
none: No more matches possible.
Types: type of a key binding.
chain: execute() was called via a chain-like key binding
special: execute() was called via a special key binding
do_log: Whether to log keypresses or not.
passthrough: Whether unbound keys should be passed through with this
handler.
Attributes:
bindings: Bound key bindings
special_bindings: Bound special bindings (<Foo>).
_win_id: The window ID this keyparser is associated with.
_warn_on_keychains: Whether a warning should be logged when binding
keychains in a section which does not support them.
_keystring: The currently entered key sequence
_modename: The name of the input mode associated with this keyparser.
_supports_count: Whether count is supported
_supports_chains: Whether keychains are supported
Signals:
keystring_updated: Emitted when the keystring is updated.
arg: New keystring.
request_leave: Emitted to request leaving a mode.
arg 0: Mode to leave.
arg 1: Reason for leaving.
arg 2: Ignore the request if we're not in that mode
"""
keystring_updated = pyqtSignal(str)
request_leave = pyqtSignal(usertypes.KeyMode, str, bool)
do_log = True
passthrough = False
Match = enum.Enum('Match', ['partial', 'definitive', 'other', 'none'])
Type = enum.Enum('Type', ['chain', 'special'])
def __init__(self, win_id, parent=None, supports_count=None,
supports_chains=False):
super().__init__(parent)
self._win_id = win_id
self._modename = None
self._keystring = ''
if supports_count is None:
supports_count = supports_chains
self._supports_count = supports_count
self._supports_chains = supports_chains
self._warn_on_keychains = True
self.bindings = {}
self.special_bindings = {}
config.instance.changed.connect(self._on_config_changed)
def __repr__(self):
return utils.get_repr(self, supports_count=self._supports_count,
supports_chains=self._supports_chains)
def _debug_log(self, message):
"""Log a message to the debug log if logging is active.
Args:
message: The message to log.
"""
if self.do_log:
log.keyboard.debug(message)
def _handle_special_key(self, e):
"""Handle a new keypress with special keys (<Foo>).
Return True if the keypress has been handled, and False if not.
Args:
e: the KeyPressEvent from Qt.
Return:
True if event has been handled, False otherwise.
"""
binding = utils.keyevent_to_string(e)
if binding is None:
self._debug_log("Ignoring only-modifier keyeevent.")
return False
if binding not in self.special_bindings:
key_mappings = config.val.bindings.key_mappings
try:
binding = key_mappings['<{}>'.format(binding)][1:-1]
except KeyError:
pass
try:
cmdstr = self.special_bindings[binding]
except KeyError:
self._debug_log("No special binding found for {}.".format(binding))
return False
count, _command = self._split_count(self._keystring)
self.execute(cmdstr, self.Type.special, count)
self.clear_keystring()
return True
def _split_count(self, keystring):
"""Get count and command from the current keystring.
Args:
keystring: The key string to split.
Return:
A (count, command) tuple.
"""
if self._supports_count:
(countstr, cmd_input) = re.match(r'^(\d*)(.*)', keystring).groups()
count = int(countstr) if countstr else None
if count == 0 and not cmd_input:
cmd_input = keystring
count = None
else:
cmd_input = keystring
count = None
return count, cmd_input
def _handle_single_key(self, e):
"""Handle a new keypress with a single key (no modifiers).
Separate the keypress into count/command, then check if it matches
any possible command, and either run the command, ignore it, or
display an error.
Args:
e: the KeyPressEvent from Qt.
Return:
A self.Match member.
"""
txt = e.text()
key = e.key()
self._debug_log("Got key: 0x{:x} / text: '{}'".format(key, txt))
if len(txt) == 1:
category = unicodedata.category(txt)
is_control_char = (category == 'Cc')
else:
is_control_char = False
if (not txt) or is_control_char:
self._debug_log("Ignoring, no text char")
return self.Match.none
count, cmd_input = self._split_count(self._keystring + txt)
match, binding = self._match_key(cmd_input)
if match == self.Match.none:
mappings = config.val.bindings.key_mappings
mapped = mappings.get(txt, None)
if mapped is not None:
txt = mapped
count, cmd_input = self._split_count(self._keystring + txt)
match, binding = self._match_key(cmd_input)
self._keystring += txt
if match == self.Match.definitive:
self._debug_log("Definitive match for '{}'.".format(
self._keystring))
self.clear_keystring()
self.execute(binding, self.Type.chain, count)
elif match == self.Match.partial:
self._debug_log("No match for '{}' (added {})".format(
self._keystring, txt))
elif match == self.Match.none:
self._debug_log("Giving up with '{}', no matches".format(
self._keystring))
self.clear_keystring()
elif match == self.Match.other:
pass
else:
raise AssertionError("Invalid match value {!r}".format(match))
return match
def _match_key(self, cmd_input):
"""Try to match a given keystring with any bound keychain.
Args:
cmd_input: The command string to find.
Return:
A tuple (matchtype, binding).
matchtype: Match.definitive, Match.partial or Match.none.
binding: - None with Match.partial/Match.none.
- The found binding with Match.definitive.
"""
if not cmd_input:
# Only a count, no command yet, but we handled it
return (self.Match.other, None)
# A (cmd_input, binding) tuple (k, v of bindings) or None.
definitive_match = None
partial_match = False
# Check definitive match
try:
definitive_match = (cmd_input, self.bindings[cmd_input])
except KeyError:
pass
# Check partial match
for binding in self.bindings:
if definitive_match is not None and binding == definitive_match[0]:
# We already matched that one
continue
elif binding.startswith(cmd_input):
partial_match = True
break
if definitive_match is not None:
return (self.Match.definitive, definitive_match[1])
elif partial_match:
return (self.Match.partial, None)
else:
return (self.Match.none, None)
def handle(self, e):
"""Handle a new keypress and call the respective handlers.
Args:
e: the KeyPressEvent from Qt
Return:
True if the event was handled, False otherwise.
"""
handled = self._handle_special_key(e)
if handled or not self._supports_chains:
return handled
match = self._handle_single_key(e)
# don't emit twice if the keystring was cleared in self.clear_keystring
if self._keystring:
self.keystring_updated.emit(self._keystring)
return match != self.Match.none
@config.change_filter('bindings')
def _on_config_changed(self):
self._read_config()
def _read_config(self, modename=None):
"""Read the configuration.
Config format: key = command, e.g.:
<Ctrl+Q> = quit
Args:
modename: Name of the mode to use.
"""
if modename is None:
if self._modename is None:
raise ValueError("read_config called with no mode given, but "
"None defined so far!")
modename = self._modename
else:
self._modename = modename
self.bindings = {}
self.special_bindings = {}
for key, cmd in config.key_instance.get_bindings_for(modename).items():
assert cmd
self._parse_key_command(modename, key, cmd)
def _parse_key_command(self, modename, key, cmd):
"""Parse the keys and their command and store them in the object."""
if utils.is_special_key(key):
self.special_bindings[key[1:-1]] = cmd
elif self._supports_chains:
self.bindings[key] = cmd
elif self._warn_on_keychains:
log.keyboard.warning("Ignoring keychain '{}' in mode '{}' because "
"keychains are not supported there."
.format(key, modename))
def execute(self, cmdstr, keytype, count=None):
"""Handle a completed keychain.
Args:
cmdstr: The command to execute as a string.
keytype: Type.chain or Type.special
count: The count if given.
"""
raise NotImplementedError
def clear_keystring(self):
"""Clear the currently entered key sequence."""
if self._keystring:
self._debug_log("discarding keystring '{}'.".format(
self._keystring))
self._keystring = ''
self.keystring_updated.emit(self._keystring)