d594798db8
Added option to webview for selection enabled caret mode. In status bar checking value of this option to identificate about it. Added bindings: <Space> for toggle selection mode, <Ctrl+Space> drop selection and keep selection mode enabled. In webview added javascript snippet to position caret at top of the viewport after caret enabling. This code mostly was taken from cVim sources.
360 lines
13 KiB
Python
360 lines
13 KiB
Python
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
|
|
|
# Copyright 2014-2015 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 re
|
|
import functools
|
|
import unicodedata
|
|
|
|
from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QObject
|
|
|
|
from qutebrowser.config import config
|
|
from qutebrowser.utils import usertypes, log, utils, objreg
|
|
|
|
|
|
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.
|
|
ambiguous: There are both a partial and a definitive match.
|
|
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.
|
|
|
|
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
|
|
_ambiguous_timer: Timer for delayed execution with ambiguous bindings.
|
|
_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.
|
|
"""
|
|
|
|
keystring_updated = pyqtSignal(str)
|
|
do_log = True
|
|
|
|
Match = usertypes.enum('Match', ['partial', 'definitive', 'ambiguous',
|
|
'other', 'none'])
|
|
Type = usertypes.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._ambiguous_timer = usertypes.Timer(self, 'ambiguous-match')
|
|
self._ambiguous_timer.setSingleShot(True)
|
|
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 = {}
|
|
|
|
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
|
|
binding = binding.lower()
|
|
try:
|
|
cmdstr = self.special_bindings[binding]
|
|
except KeyError:
|
|
self._debug_log("No binding found for {}.".format(binding))
|
|
return False
|
|
self.execute(cmdstr, self.Type.special)
|
|
return True
|
|
|
|
def _split_count(self):
|
|
"""Get count and command from the current keystring.
|
|
|
|
Return:
|
|
A (count, command) tuple.
|
|
"""
|
|
if self._supports_count:
|
|
(countstr, cmd_input) = re.match(r'^(\d*)(.*)',
|
|
self._keystring).groups()
|
|
count = int(countstr) if countstr else None
|
|
else:
|
|
cmd_input = self._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 key == Qt.Key_Escape:
|
|
self._debug_log("Escape pressed, discarding '{}'.".format(
|
|
self._keystring))
|
|
self._keystring = ''
|
|
return self.Match.none
|
|
|
|
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
|
|
|
|
self._stop_timers()
|
|
self._keystring += txt
|
|
|
|
count, cmd_input = self._split_count()
|
|
|
|
print(count, cmd_input)
|
|
if not cmd_input:
|
|
# Only a count, no command yet, but we handled it
|
|
return self.Match.other
|
|
|
|
match, binding = self._match_key(cmd_input)
|
|
|
|
if not isinstance(match, self.Match):
|
|
raise TypeError("Value {} is no Match member!".format(match))
|
|
|
|
if match == self.Match.definitive:
|
|
self._debug_log("Definitive match for '{}'.".format(
|
|
self._keystring))
|
|
self._keystring = ''
|
|
self.execute(binding, self.Type.chain, count)
|
|
elif match == self.Match.ambiguous:
|
|
self._debug_log("Ambigious match for '{}'.".format(
|
|
self._keystring))
|
|
self._handle_ambiguous_match(binding, 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._keystring = ''
|
|
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.ambiguous, Match.partial or
|
|
Match.none
|
|
binding: - None with Match.partial/Match.none
|
|
- The found binding with Match.definitive/
|
|
Match.ambiguous
|
|
"""
|
|
# 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 and partial_match:
|
|
return (self.Match.ambiguous, definitive_match[1])
|
|
elif 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 _stop_timers(self):
|
|
"""Stop a delayed execution if any is running."""
|
|
if self._ambiguous_timer.isActive() and self.do_log:
|
|
log.keyboard.debug("Stopping delayed execution.")
|
|
self._ambiguous_timer.stop()
|
|
try:
|
|
self._ambiguous_timer.timeout.disconnect()
|
|
except TypeError:
|
|
# no connections
|
|
pass
|
|
|
|
def _handle_ambiguous_match(self, binding, count):
|
|
"""Handle an ambiguous match.
|
|
|
|
Args:
|
|
binding: The command-string to execute.
|
|
count: The count to pass.
|
|
"""
|
|
self._debug_log("Ambiguous match for '{}'".format(self._keystring))
|
|
time = config.get('input', 'timeout')
|
|
if time == 0:
|
|
# execute immediately
|
|
self._keystring = ''
|
|
self.execute(binding, self.Type.chain, count)
|
|
else:
|
|
# execute in `time' ms
|
|
self._debug_log("Scheduling execution of {} in {}ms".format(
|
|
binding, time))
|
|
self._ambiguous_timer.setInterval(time)
|
|
self._ambiguous_timer.timeout.connect(
|
|
functools.partial(self.delayed_exec, binding, count))
|
|
self._ambiguous_timer.start()
|
|
|
|
def delayed_exec(self, command, count):
|
|
"""Execute a delayed command.
|
|
|
|
Args:
|
|
command/count: As if passed to self.execute()
|
|
"""
|
|
self._debug_log("Executing delayed command now!")
|
|
self._keystring = ''
|
|
self.keystring_updated.emit(self._keystring)
|
|
self.execute(command, self.Type.chain, count)
|
|
|
|
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)
|
|
self.keystring_updated.emit(self._keystring)
|
|
return match != self.Match.none
|
|
|
|
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 = {}
|
|
keyconfparser = objreg.get('key-config')
|
|
for (key, cmd) in keyconfparser.get_bindings_for(modename).items():
|
|
if not cmd:
|
|
continue
|
|
elif key.startswith('<') and key.endswith('>'):
|
|
keystr = utils.normalize_keystr(key[1:-1])
|
|
self.special_bindings[keystr] = 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
|
|
|
|
@pyqtSlot(str)
|
|
def on_keyconfig_changed(self, mode):
|
|
"""Re-read the config if a key binding was changed."""
|
|
if self._modename is None:
|
|
raise AttributeError("on_keyconfig_changed called but no section "
|
|
"defined!")
|
|
if mode == self._modename:
|
|
self.read_config()
|