qutebrowser/qutebrowser/keyinput/modeman.py

275 lines
9.6 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-04-24 16:53:16 +02:00
import qutebrowser.config.config as config
import qutebrowser.commands.utils as cmdutils
2014-04-25 00:10:07 +02:00
import qutebrowser.utils.debug as debug
2014-05-23 16:11:55 +02:00
from qutebrowser.utils.log import modes as logger
2014-04-24 16:53:16 +02:00
2014-04-23 16:57:12 +02:00
2014-05-05 17:56:14 +02:00
def instance():
"""Get the global modeman instance."""
return QApplication.instance().modeman
2014-04-23 16:57:12 +02:00
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-05-09 16:03:46 +02:00
instance().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-05-09 16:03:46 +02:00
instance().leave(mode, reason)
2014-04-24 06:44:58 +02:00
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-05-09 16:03:46 +02:00
instance().leave(mode, reason)
2014-04-24 16:03:16 +02:00
except ValueError:
pass
2014-04-23 16:57:12 +02:00
class ModeManager(QObject):
"""Manager for keyboard modes.
Attributes:
2014-04-24 06:59:39 +02:00
mode: The current mode (readonly property).
passthrough: A list of modes in which to pass through events.
mainwindow: The mainwindow object
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.
arg: Name of the entered mode.
2014-04-24 07:01:27 +02:00
left: Emitted when a mode is left.
arg: Name of the left mode.
2014-04-23 16:57:12 +02:00
"""
entered = pyqtSignal(str)
2014-04-24 07:01:27 +02:00
left = pyqtSignal(str)
def __init__(self, parent=None):
2014-04-23 16:57:12 +02:00
super().__init__(parent)
self.mainwindow = None
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):
return '<{} mode={}>'.format(self.__class__.__name__, self.mode)
2014-04-24 06:44:58 +02:00
@property
def mode(self):
"""Read-only property for the current mode."""
2014-05-07 18:00:38 +02:00
# For some reason, on Ubuntu (Python 3.3.2, PyQt 5.0.1, Qt 5.0.2) there
# is a lingering exception here sometimes. With this construct, we
# clear this exception which makes no sense at all anyways.
# Details:
# http://www.riverbankcomputing.com/pipermail/pyqt/2014-May/034196.html
# If we wouldn't clear the exception, we would actually get an
# AttributeError for the mode property in eventFilter because of
# another PyQt oddity.
try:
raise
except: # pylint: disable=bare-except
pass
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.
"""
handler = self._handlers[self.mode]
2014-05-23 16:11:55 +02:00
logger.debug("calling handler {}".format(handler.__qualname__))
2014-04-25 11:21:00 +02:00
handled = handler(event) if handler is not None else False
is_non_alnum = event.modifiers() or not event.text().strip()
2014-04-25 11:21:00 +02:00
if handled:
filter_this = True
elif (self.mode 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)
logger.debug("handled: {}, forward-unbound-keys: {}, passthrough: {}, "
"is_non_alnum: {} --> filter: {}".format(
handled, self._forward_unbound_keys,
self.mode in self.passthrough, 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
2014-05-23 16:11:55 +02:00
logger.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
"""
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-16 23:01:40 +02:00
@cmdutils.register(instance='modeman', name='enter-mode', hide=True)
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-05-09 16:03:46 +02:00
mode: The name of the mode to enter.
reason: Why the mode was entered.
2014-04-24 06:44:58 +02:00
Emit:
entered: With the new mode name.
"""
2014-05-23 16:11:55 +02:00
logger.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-05-23 16:11:55 +02:00
logger.debug("Already at end of stack, doing nothing")
return
2014-04-24 06:44:58 +02:00
self._mode_stack.append(mode)
2014-05-23 16:11:55 +02:00
logger.debug("New mode stack: {}".format(self._mode_stack))
self.entered.emit(mode)
2014-05-09 16:03:46 +02:00
def leave(self, mode, reason=None):
2014-04-24 06:44:58 +02:00
"""Leave a mode.
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 ValueError("Mode {} not on mode stack!".format(mode))
2014-05-23 16:11:55 +02:00
logger.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-05-16 23:01:40 +02:00
@cmdutils.register(instance='modeman', name='leave-mode',
not_modes=['normal'], hide=True)
def leave_current_mode(self):
2014-04-25 11:21:00 +02:00
"""Leave the mode we're currently in."""
2014-04-25 16:53:23 +02:00
if self.mode == 'normal':
raise ValueError("Can't leave normal mode!")
2014-05-09 16:03:46 +02:00
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
"""
2014-05-07 18:00:38 +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.
2014-05-23 16:11:55 +02:00
logger.debug("Ignoring event {} for {}".format(
2014-06-15 11:11:08 +02:00
debug.qenum_key(QEvent, typ), obj.__class__.__name__))
return False
if QApplication.instance().activeWindow() is not self.mainwindow:
# Some other window (print dialog, etc.) is focused so we pass
# the event through.
return False
2014-05-23 16:11:55 +02:00
logger.debug("Got event {} for {} in mode {}".format(
2014-06-15 11:11:08 +02:00
debug.qenum_key(QEvent, typ), obj.__class__.__name__, self.mode))
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)