qutebrowser/qutebrowser/keyinput/modeman.py

301 lines
11 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:
2014-04-23 16:57:12 +02:00
# Copyright 2014 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/>.
"""Mode manager singleton which handles the current keyboard mode.
Module attributes:
manager: The ModeManager instance.
"""
from PyQt5.QtGui import QWindow
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QEvent
from PyQt5.QtWidgets import QApplication
2014-04-23 16:57:12 +02:00
2014-08-26 19:10:14 +02:00
from qutebrowser.config import config
from qutebrowser.commands import cmdexc, cmdutils
2014-09-26 15:48:24 +02:00
from qutebrowser.utils import usertypes, log, objreg, utils
2014-04-24 16:53:16 +02:00
2014-04-23 16:57:12 +02:00
2014-08-04 03:14:14 +02:00
class ModeLockedError(Exception):
"""Exception raised when the mode is currently locked."""
class NotInModeError(Exception):
"""Exception raised when we want to leave a mode we're not in."""
2014-05-09 16:03:46 +02:00
def enter(mode, reason=None):
2014-04-23 16:57:12 +02:00
"""Enter the mode 'mode'."""
2014-09-24 07:06:45 +02:00
objreg.get('mode-manager').enter(mode, reason)
2014-04-23 16:57:12 +02:00
2014-05-09 16:03:46 +02:00
def leave(mode, reason=None):
2014-04-24 06:44:58 +02:00
"""Leave the mode 'mode'."""
2014-09-24 07:06:45 +02:00
objreg.get('mode-manager').leave(mode, reason)
2014-04-24 06:44:58 +02:00
2014-08-04 03:14:14 +02:00
def maybe_enter(mode, reason=None):
"""Convenience method to enter 'mode' without exceptions."""
try:
2014-09-24 07:06:45 +02:00
objreg.get('mode-manager').enter(mode, reason)
2014-08-04 03:47:09 +02:00
except ModeLockedError:
2014-08-04 03:14:14 +02:00
pass
2014-05-09 16:03:46 +02:00
def maybe_leave(mode, reason=None):
2014-04-24 16:03:16 +02:00
"""Convenience method to leave 'mode' without exceptions."""
try:
2014-09-24 07:06:45 +02:00
objreg.get('mode-manager').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:
2014-04-24 06:59:39 +02:00
passthrough: A list of modes in which to pass through events.
2014-08-04 03:14:14 +02:00
locked: Whether current mode is locked. This means the current mode can
still be left (then locked will be reset), but no new mode can
be entered while the current mode is active.
2014-04-23 16:57:12 +02:00
_handlers: A dictionary of modes and their handlers.
2014-04-24 06:44:58 +02:00
_mode_stack: A list of the modes we're currently in, with the active
one on the right.
_forward_unbound_keys: If we should forward unbound keys.
2014-04-25 09:20:19 +02:00
_releaseevents_to_pass: A list of keys where the keyPressEvent was
passed through, so the release event should as
well.
Signals:
entered: Emitted when a mode is entered.
2014-07-28 22:40:58 +02:00
arg: The mode which has been entered.
2014-04-24 07:01:27 +02:00
left: Emitted when a mode is left.
2014-07-28 22:40:58 +02:00
arg: The mode which has been left.
2014-04-23 16:57:12 +02:00
"""
2014-08-26 19:10:14 +02:00
entered = pyqtSignal(usertypes.KeyMode)
left = pyqtSignal(usertypes.KeyMode)
def __init__(self, parent=None):
2014-04-23 16:57:12 +02:00
super().__init__(parent)
2014-08-04 03:14:14 +02:00
self.locked = False
2014-04-23 16:57:12 +02:00
self._handlers = {}
2014-04-24 06:59:39 +02:00
self.passthrough = []
2014-04-24 06:44:58 +02:00
self._mode_stack = []
2014-04-25 09:20:19 +02:00
self._releaseevents_to_pass = []
2014-04-27 21:21:14 +02:00
self._forward_unbound_keys = config.get('input',
'forward-unbound-keys')
2014-04-24 06:44:58 +02:00
def __repr__(self):
2014-09-26 15:48:24 +02:00
return utils.get_repr(self, mode=self.mode(), locked=self.locked,
passthrough=self.passthrough)
2014-04-24 06:44:58 +02:00
def mode(self):
"""Get the current mode.."""
2014-04-24 06:44:58 +02:00
if not self._mode_stack:
return None
return self._mode_stack[-1]
2014-04-23 16:57:12 +02:00
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()
handler = self._handlers[curmode]
if curmode != usertypes.KeyMode.insert:
2014-08-26 20:15:41 +02:00
log.modes.debug("got keypress in mode {} - calling handler "
"{}".format(curmode, handler.__qualname__))
2014-04-25 11:21:00 +02:00
handled = handler(event) if handler is not None else False
2014-06-25 15:25:24 +02:00
is_non_alnum = bool(event.modifiers()) or not event.text().strip()
2014-04-25 11:21:00 +02:00
if handled:
filter_this = True
elif (curmode in self.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.append(event)
if curmode != usertypes.KeyMode.insert:
2014-08-26 20:15:41 +02:00
log.modes.debug("handled: {}, forward-unbound-keys: {}, "
"passthrough: {}, is_non_alnum: {} --> filter: "
"{}".format(handled, self._forward_unbound_keys,
curmode in self.passthrough,
2014-08-26 20:15:41 +02:00
is_non_alnum, filter_this))
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
if event in self._releaseevents_to_pass:
# remove all occurences
self._releaseevents_to_pass = [
e for e in self._releaseevents_to_pass if e != event]
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, handler, passthrough=False):
2014-04-23 16:57:12 +02:00
"""Register a new mode.
Args:
mode: The name of the mode.
handler: Handler for keyPressEvents.
passthrough: Whether to pass keybindings in this mode through to
the widgets.
2014-04-23 16:57:12 +02:00
"""
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-04-23 16:57:12 +02:00
self._handlers[mode] = handler
if passthrough:
2014-04-24 06:59:39 +02:00
self.passthrough.append(mode)
2014-04-23 16:57:12 +02:00
2014-05-09 16:03:46 +02:00
def enter(self, mode, reason=None):
"""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.
2014-04-24 06:44:58 +02:00
Emit:
entered: With the new mode name.
"""
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-04 03:14:14 +02:00
if self.locked:
2014-08-26 20:15:41 +02:00
log.modes.debug("Not entering mode {} because mode is locked to "
"{}.".format(mode, self.mode()))
2014-08-04 03:14:14 +02:00
raise ModeLockedError("Mode is currently locked to {}".format(
self.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._handlers:
raise ValueError("No handler for mode {}".format(mode))
if self._mode_stack and self._mode_stack[-1] == mode:
2014-08-26 20:15:41 +02:00
log.modes.debug("Already at end of stack, doing nothing")
return
2014-04-24 06:44:58 +02:00
self._mode_stack.append(mode)
2014-08-26 20:15:41 +02:00
log.modes.debug("New mode stack: {}".format(self._mode_stack))
self.entered.emit(mode)
2014-09-23 23:05:55 +02:00
@cmdutils.register(instance='mode-manager', hide=True)
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')
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:
2014-05-09 16:03:46 +02:00
mode: The name of the mode to leave.
reason: Why the mode was left.
2014-04-24 06:44:58 +02:00
Emit:
2014-04-24 07:01:27 +02:00
left: With the old mode name.
2014-04-24 06:44:58 +02:00
"""
try:
self._mode_stack.remove(mode)
except ValueError:
raise NotInModeError("Mode {} not on mode stack!".format(mode))
2014-08-04 03:14:14 +02:00
self.locked = False
2014-08-26 20:15:41 +02:00
log.modes.debug("Leaving mode {}{}, new mode stack {}".format(
2014-05-09 16:03:46 +02:00
mode, '' if reason is None else ' (reason: {})'.format(reason),
self._mode_stack))
2014-04-24 07:01:27 +02:00
self.left.emit(mode)
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-08-26 19:10:14 +02:00
not_modes=[usertypes.KeyMode.normal], hide=True)
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')
@pyqtSlot(str, str)
def on_config_changed(self, section, option):
"""Update local setting when config changed."""
2014-04-27 21:21:14 +02:00
if (section, option) == ('input', 'forward-unbound-keys'):
self._forward_unbound_keys = config.get('input',
'forward-unbound-keys')
2014-04-25 11:21:00 +02:00
def eventFilter(self, obj, 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
2014-04-25 11:21:00 +02:00
typ = event.type()
2014-04-23 23:24:46 +02:00
if typ not in [QEvent.KeyPress, QEvent.KeyRelease]:
# We're not interested in non-key-events so we pass them through.
return False
if not isinstance(obj, QWindow):
# We already handled this same event at some point earlier, so
# we're not interested in it anymore.
return False
2014-09-23 22:02:21 +02:00
if (QApplication.instance().activeWindow() is not
2014-09-24 07:06:45 +02:00
objreg.get('main-window')):
# Some other window (print dialog, etc.) is focused so we pass
# the event through.
return False
2014-04-25 06:39:17 +02:00
2014-04-25 09:20:19 +02:00
if typ == 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)