qutebrowser/qutebrowser/keyinput/modeman.py

327 lines
12 KiB
Python
Raw Normal View History

2014-06-19 09:04:37 +02:00
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
2016-01-04 07:12:39 +01:00
# Copyright 2014-2016 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
2014-04-23 16:57:12 +02:00
#
# 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/>.
2015-01-09 20:14:50 +01:00
"""Mode manager singleton which handles the current keyboard mode."""
2014-04-23 16:57:12 +02:00
import functools
from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QObject, QEvent
from PyQt5.QtWidgets import QApplication
2014-04-23 16:57:12 +02:00
2014-09-28 00:18:57 +02:00
from qutebrowser.keyinput import modeparsers, keyparser
2014-08-26 19:10:14 +02:00
from qutebrowser.config import config
from qutebrowser.commands import cmdexc, cmdutils
from qutebrowser.utils import usertypes, log, objreg, utils, usertypes
2014-04-24 16:53:16 +02:00
2014-04-23 16:57:12 +02:00
class KeyEvent:
"""A small wrapper over a QKeyEvent storing its data.
This is needed because Qt apparently mutates existing events with new data.
It doesn't store the modifiers because they can be different for a key
press/release.
Attributes:
key: A Qt.Key member (QKeyEvent::key).
text: A string (QKeyEvent::text).
"""
def __init__(self, keyevent):
self.key = keyevent.key()
self.text = keyevent.text()
def __repr__(self):
return utils.get_repr(self, key=self.key, text=self.text)
def __eq__(self, other):
return self.key == other.key and self.text == other.text
def __hash__(self):
return hash((self.key, self.text))
class NotInModeError(Exception):
"""Exception raised when we want to leave a mode we're not in."""
2014-09-28 22:13:14 +02:00
def init(win_id, parent):
2014-10-06 22:23:27 +02:00
"""Initialize the mode manager and the keyparsers for the given win_id."""
2014-09-28 00:18:57 +02:00
KM = usertypes.KeyMode # pylint: disable=invalid-name
2014-09-28 22:13:14 +02:00
modeman = ModeManager(win_id, parent)
objreg.register('mode-manager', modeman, scope='window', window=win_id)
2014-09-28 00:18:57 +02:00
keyparsers = {
2014-09-28 22:13:14 +02:00
KM.normal: modeparsers.NormalKeyParser(win_id, modeman),
KM.hint: modeparsers.HintKeyParser(win_id, modeman),
KM.insert: keyparser.PassthroughKeyParser(win_id, 'insert', modeman),
KM.passthrough: keyparser.PassthroughKeyParser(win_id, 'passthrough',
modeman),
KM.command: keyparser.PassthroughKeyParser(win_id, 'command', modeman),
KM.prompt: keyparser.PassthroughKeyParser(win_id, 'prompt', modeman,
2014-09-28 00:18:57 +02:00
warn=False),
2014-09-28 22:13:14 +02:00
KM.yesno: modeparsers.PromptKeyParser(win_id, modeman),
KM.caret: modeparsers.CaretKeyParser(win_id, modeman),
KM.set_mark: modeparsers.MarkKeyParser(win_id, KM.set_mark, modeman),
KM.jump_mark: modeparsers.MarkKeyParser(win_id, KM.jump_mark, modeman),
2014-09-28 00:18:57 +02:00
}
2014-09-28 22:13:14 +02:00
objreg.register('keyparsers', keyparsers, scope='window', window=win_id)
modeman.destroyed.connect(
functools.partial(objreg.delete, 'keyparsers', scope='window',
window=win_id))
for mode, parser in keyparsers.items():
modeman.register(mode, parser)
2014-09-28 22:13:14 +02:00
return modeman
2014-09-28 00:18:57 +02:00
2015-06-05 11:15:18 +02:00
def instance(win_id):
2014-09-28 22:13:14 +02:00
"""Get a modemanager object."""
return objreg.get('mode-manager', scope='window', window=win_id)
2014-09-28 00:18:57 +02:00
2014-10-06 22:30:37 +02:00
def enter(win_id, mode, reason=None, only_if_normal=False):
2014-04-23 16:57:12 +02:00
"""Enter the mode 'mode'."""
2015-06-05 11:15:18 +02:00
instance(win_id).enter(mode, reason, only_if_normal)
2014-04-23 16:57:12 +02:00
2014-09-28 22:13:14 +02:00
def leave(win_id, mode, reason=None):
2014-04-24 06:44:58 +02:00
"""Leave the mode 'mode'."""
2015-06-05 11:15:18 +02:00
instance(win_id).leave(mode, reason)
2014-04-24 06:44:58 +02:00
2014-09-28 22:13:14 +02:00
def maybe_leave(win_id, mode, reason=None):
2014-04-24 16:03:16 +02:00
"""Convenience method to leave 'mode' without exceptions."""
try:
2015-06-05 11:15:18 +02:00
instance(win_id).leave(mode, reason)
except NotInModeError as e:
# This is rather likely to happen, so we only log to debug log.
log.modes.debug("{} (leave reason: {})".format(e, reason))
2014-04-24 16:03:16 +02:00
2014-04-23 16:57:12 +02:00
class ModeManager(QObject):
"""Manager for keyboard modes.
Attributes:
mode: The mode we're currently in.
2014-09-28 22:13:14 +02:00
_win_id: The window ID of this ModeManager
_parsers: A dictionary of modes and their keyparsers.
_forward_unbound_keys: If we should forward unbound keys.
_releaseevents_to_pass: A set of KeyEvents where the keyPressEvent was
2014-04-25 09:20:19 +02:00
passed through, so the release event should as
well.
Signals:
entered: Emitted when a mode is entered.
2014-09-28 22:13:14 +02:00
arg1: The mode which has been entered.
arg2: The window ID of this mode manager.
2014-04-24 07:01:27 +02:00
left: Emitted when a mode is left.
2014-09-28 22:13:14 +02:00
arg1: The mode which has been left.
arg2: The new current mode.
arg3: The window ID of this mode manager.
2014-04-23 16:57:12 +02:00
"""
2014-09-28 22:13:14 +02:00
entered = pyqtSignal(usertypes.KeyMode, int)
left = pyqtSignal(usertypes.KeyMode, usertypes.KeyMode, int)
2014-09-28 22:13:14 +02:00
def __init__(self, win_id, parent=None):
2014-04-23 16:57:12 +02:00
super().__init__(parent)
2014-09-28 22:13:14 +02:00
self._win_id = win_id
self._parsers = {}
self.mode = usertypes.KeyMode.normal
self._releaseevents_to_pass = set()
self._forward_unbound_keys = config.get(
'input', 'forward-unbound-keys')
objreg.get('config').changed.connect(self.set_forward_unbound_keys)
2014-04-24 06:44:58 +02:00
def __repr__(self):
return utils.get_repr(self, mode=self.mode)
2014-04-25 11:21:00 +02:00
def _eventFilter_keypress(self, event):
"""Handle filtering of KeyPress events.
Args:
event: The KeyPress to examine.
Return:
True if event should be filtered, False otherwise.
"""
curmode = self.mode
parser = self._parsers[curmode]
if curmode != usertypes.KeyMode.insert:
log.modes.debug("got keypress in mode {} - delegating to "
"{}".format(curmode, utils.qualname(parser)))
handled = parser.handle(event)
2014-04-25 11:21:00 +02:00
is_non_alnum = (
event.modifiers() not in (Qt.NoModifier, Qt.ShiftModifier) or
not event.text().strip())
2014-04-25 11:21:00 +02:00
if handled:
filter_this = True
elif (parser.passthrough or
self._forward_unbound_keys == 'all' or
(self._forward_unbound_keys == 'auto' and is_non_alnum)):
2014-04-25 11:21:00 +02:00
filter_this = False
else:
filter_this = True
if not filter_this:
self._releaseevents_to_pass.add(KeyEvent(event))
2014-04-25 11:21:00 +02:00
if curmode != usertypes.KeyMode.insert:
focus_widget = QApplication.instance().focusWidget()
2014-08-26 20:15:41 +02:00
log.modes.debug("handled: {}, forward-unbound-keys: {}, "
"passthrough: {}, is_non_alnum: {} --> "
"filter: {} (focused: {!r})".format(
handled, self._forward_unbound_keys,
parser.passthrough, is_non_alnum,
filter_this, focus_widget))
2014-04-25 11:21:00 +02:00
return filter_this
def _eventFilter_keyrelease(self, event):
"""Handle filtering of KeyRelease events.
Args:
event: The KeyPress to examine.
Return:
True if event should be filtered, False otherwise.
"""
# handle like matching KeyPress
keyevent = KeyEvent(event)
if keyevent in self._releaseevents_to_pass:
self._releaseevents_to_pass.remove(keyevent)
2014-04-25 11:21:00 +02:00
filter_this = False
else:
filter_this = True
if self.mode != usertypes.KeyMode.insert:
2014-08-26 20:15:41 +02:00
log.modes.debug("filter: {}".format(filter_this))
2014-04-25 11:21:00 +02:00
return filter_this
def register(self, mode, parser):
2014-04-23 16:57:12 +02:00
"""Register a new mode.
Args:
mode: The name of the mode.
parser: The KeyParser which should be used.
2014-04-23 16:57:12 +02:00
"""
assert isinstance(mode, usertypes.KeyMode)
assert parser is not None
self._parsers[mode] = parser
parser.request_leave.connect(self.leave)
2014-04-23 16:57:12 +02:00
def enter(self, mode, reason=None, only_if_normal=False):
"""Enter a new mode.
2014-04-24 06:44:58 +02:00
Args:
2014-07-28 22:40:58 +02:00
mode: The mode to enter as a KeyMode member.
2014-05-09 16:03:46 +02:00
reason: Why the mode was entered.
only_if_normal: Only enter the new mode if we're in normal mode.
"""
2014-08-26 19:10:14 +02:00
if not isinstance(mode, usertypes.KeyMode):
2014-07-29 00:23:20 +02:00
raise TypeError("Mode {} is no KeyMode member!".format(mode))
2014-08-26 20:15:41 +02:00
log.modes.debug("Entering mode {}{}".format(
2014-05-09 16:03:46 +02:00
mode, '' if reason is None else ' (reason: {})'.format(reason)))
if mode not in self._parsers:
raise ValueError("No keyparser for mode {}".format(mode))
prompt_modes = (usertypes.KeyMode.prompt, usertypes.KeyMode.yesno)
if self.mode == mode or (self.mode in prompt_modes and
mode in prompt_modes):
log.modes.debug("Ignoring request as we're in mode {} "
"already.".format(self.mode))
return
if self.mode != usertypes.KeyMode.normal:
if only_if_normal:
log.modes.debug("Ignoring request as we're in mode {} "
"and only_if_normal is set..".format(
self.mode))
return
log.modes.debug("Overriding mode {}.".format(self.mode))
self.left.emit(self.mode, mode, self._win_id)
self.mode = mode
2014-09-28 22:13:14 +02:00
self.entered.emit(mode, self._win_id)
2014-09-28 22:13:14 +02:00
@cmdutils.register(instance='mode-manager', hide=True, scope='window')
2014-07-28 22:40:58 +02:00
def enter_mode(self, mode):
2014-08-03 00:33:39 +02:00
"""Enter a key mode.
2014-07-28 22:40:58 +02:00
Args:
2014-08-03 00:33:39 +02:00
mode: The mode to enter.
2014-07-28 22:40:58 +02:00
"""
try:
2014-08-26 19:10:14 +02:00
m = usertypes.KeyMode[mode]
2014-07-28 22:40:58 +02:00
except KeyError:
2014-08-26 19:10:14 +02:00
raise cmdexc.CommandError("Mode {} does not exist!".format(mode))
2014-07-28 22:40:58 +02:00
self.enter(m, 'command')
@pyqtSlot(usertypes.KeyMode, str)
2014-05-09 16:03:46 +02:00
def leave(self, mode, reason=None):
2014-08-03 00:33:39 +02:00
"""Leave a key mode.
2014-04-24 06:44:58 +02:00
Args:
2016-04-21 20:11:47 +02:00
mode: The mode to leave as a usertypes.KeyMode member.
2014-05-09 16:03:46 +02:00
reason: Why the mode was left.
2014-04-24 06:44:58 +02:00
"""
if self.mode != mode:
raise NotInModeError("Not in mode {}!".format(mode))
log.modes.debug("Leaving mode {}{}".format(
mode, '' if reason is None else ' (reason: {})'.format(reason)))
self.mode = usertypes.KeyMode.normal
self.left.emit(mode, self.mode, self._win_id)
2014-04-24 06:44:58 +02:00
2014-09-23 23:05:55 +02:00
@cmdutils.register(instance='mode-manager', name='leave-mode',
2014-09-28 22:13:14 +02:00
not_modes=[usertypes.KeyMode.normal], hide=True,
scope='window')
def leave_current_mode(self):
2014-04-25 11:21:00 +02:00
"""Leave the mode we're currently in."""
if self.mode == usertypes.KeyMode.normal:
raise ValueError("Can't leave normal mode!")
self.leave(self.mode, 'leave current')
@config.change_filter('input', 'forward-unbound-keys')
def set_forward_unbound_keys(self):
"""Update local setting when config changed."""
self._forward_unbound_keys = config.get(
'input', 'forward-unbound-keys')
def eventFilter(self, event):
2014-04-23 23:24:46 +02:00
"""Filter all events based on the currently set mode.
Also calls the real keypress handler.
2014-04-24 15:47:38 +02:00
2014-04-25 11:21:00 +02:00
Args:
event: The KeyPress to examine.
Return:
True if event should be filtered, False otherwise.
2014-04-23 23:24:46 +02:00
"""
if self.mode is None:
# We got events before mode is set, so just pass them through.
return False
if event.type() == QEvent.KeyPress:
2014-04-25 11:21:00 +02:00
return self._eventFilter_keypress(event)
2014-04-25 06:39:17 +02:00
else:
2014-04-25 11:21:00 +02:00
return self._eventFilter_keyrelease(event)
@cmdutils.register(instance='mode-manager', scope='window', hide=True)
def clear_keychain(self):
"""Clear the currently entered key chain."""
self._parsers[self.mode].clear_keystring()