From 8b5daad367080ab2f337dba46c078d97c899e8a5 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 23 Apr 2014 16:57:12 +0200 Subject: [PATCH 01/54] Add ModeManager --- qutebrowser/app.py | 21 +----- qutebrowser/browser/hints.py | 15 +--- qutebrowser/utils/modemanager.py | 106 +++++++++++++++++++++++++++ qutebrowser/widgets/tabbedbrowser.py | 4 - 4 files changed, 113 insertions(+), 33 deletions(-) create mode 100644 qutebrowser/utils/modemanager.py diff --git a/qutebrowser/app.py b/qutebrowser/app.py index 694ca8a61..f90e07a06 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -53,6 +53,7 @@ import qutebrowser.config.style as style import qutebrowser.config.config as config import qutebrowser.network.qutescheme as qutescheme import qutebrowser.utils.message as message +import qutebrowser.utils.modemanager as modemanager from qutebrowser.widgets.mainwindow import MainWindow from qutebrowser.widgets.crash import CrashDialog from qutebrowser.commands.keys import CommandKeyParser @@ -83,7 +84,6 @@ class QuteBrowser(QApplication): _timers: List of used QTimers so they don't get GCed. _shutting_down: True if we're currently shutting down. _quit_status: The current quitting status. - _mode: The mode we're currently in. _opened_urls: List of opened URLs. """ @@ -93,7 +93,6 @@ class QuteBrowser(QApplication): self._timers = [] self._opened_urls = [] self._shutting_down = False - self._mode = None sys.excepthook = self._exception_hook @@ -130,10 +129,11 @@ class QuteBrowser(QApplication): } self._init_cmds() self.mainwindow = MainWindow() + modemanager.init(self.mainwindow.tabs.keypress, self._keyparsers, self) self.setQuitOnLastWindowClosed(False) self._connect_signals() - self.set_mode("normal") + modemanager.enter("normal") self.mainwindow.show() self._python_hacks() @@ -246,7 +246,6 @@ class QuteBrowser(QApplication): # misc self.lastWindowClosed.connect(self.shutdown) tabs.quit.connect(self.shutdown) - tabs.set_mode.connect(self.set_mode) tabs.currentChanged.connect(self.mainwindow.update_inspector) # status bar @@ -401,20 +400,6 @@ class QuteBrowser(QApplication): logging.debug("maybe_quit quitting.") self.quit() - @pyqtSlot(str) - def set_mode(self, mode): - """Set a key input mode. - - Args: - mode: The new mode to set, as an index for self._keyparsers. - """ - if self._mode is not None: - oldhandler = self._keyparsers[self._mode] - self.mainwindow.tabs.keypress.disconnect(oldhandler.handle) - handler = self._keyparsers[mode] - self.mainwindow.tabs.keypress.connect(handler.handle) - self._mode = mode - @cmdutils.register(instance='', maxsplit=0) def pyeval(self, s): """Evaluate a python string and display the results as a webpage. diff --git a/qutebrowser/browser/hints.py b/qutebrowser/browser/hints.py index 00cbf8141..c5dde063a 100644 --- a/qutebrowser/browser/hints.py +++ b/qutebrowser/browser/hints.py @@ -28,6 +28,7 @@ from PyQt5.QtWidgets import QApplication import qutebrowser.config.config as config import qutebrowser.utils.message as message import qutebrowser.utils.url as urlutils +import qutebrowser.utils.modemanager as modemanager from qutebrowser.utils.keyparser import KeyParser @@ -104,8 +105,6 @@ class HintManager(QObject): Signals: hint_strings_updated: Emitted when the possible hint strings changed. arg: A list of hint strings. - set_mode: Emitted when the input mode should be changed. - arg: The new mode, as a string. mouse_event: Mouse event to be posted in the web view. arg: A QMouseEvent openurl: Open a new url @@ -146,7 +145,6 @@ class HintManager(QObject): """ hint_strings_updated = pyqtSignal(list) - set_mode = pyqtSignal(str) mouse_event = pyqtSignal('QMouseEvent') set_open_target = pyqtSignal(str) set_cmd_text = pyqtSignal(str) @@ -353,7 +351,6 @@ class HintManager(QObject): Emit: hint_strings_updated: Emitted to update keypraser. - set_mode: Emitted to enter hinting mode """ self._target = target self._baseurl = baseurl @@ -395,14 +392,10 @@ class HintManager(QObject): self._elems[string] = ElemTuple(e, label) frame.contentsSizeChanged.connect(self.on_contents_size_changed) self.hint_strings_updated.emit(strings) - self.set_mode.emit("hint") + modemanager.enter("hint") def stop(self): - """Stop hinting. - - Emit: - set_mode: Emitted to leave hinting mode. - """ + """Stop hinting.""" for elem in self._elems.values(): elem.label.removeFromDocument() self._frame.contentsSizeChanged.disconnect( @@ -410,7 +403,7 @@ class HintManager(QObject): self._elems = {} self._target = None self._frame = None - self.set_mode.emit("normal") + modemanager.enter("normal") message.clear() def handle_partial_key(self, keystr): diff --git a/qutebrowser/utils/modemanager.py b/qutebrowser/utils/modemanager.py new file mode 100644 index 000000000..11c34451a --- /dev/null +++ b/qutebrowser/utils/modemanager.py @@ -0,0 +1,106 @@ +# Copyright 2014 Florian Bruhin (The Compiler) +# +# 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 . + +"""Mode manager singleton which handles the current keyboard mode. + +Module attributes: + manager: The ModeManager instance. +""" + +import logging + +from PyQt5.QtCore import QObject + + +manager = None + + +def init(source, parsers=None, parent=None): + """Initialize the global ModeManager. + + This needs to be done by hand because the import time is before Qt is ready + for everything. + + Args: + source: The keypress source signal. + parsers: A dict of KeyParsers to register. + parent: Parent to use for ModeManager. + """ + global manager + manager = ModeManager(source, parsers, parent) + + +def enter(mode): + """Enter the mode 'mode'.""" + manager.enter(mode) + + +def register(mode, handler): + """Register a new mode. + + Args: + mode: The name of the mode. + handler: Handler for keyPressEvents. + """ + manager.register(mode, handler) + + +class ModeManager(QObject): + + """Manager for keyboard modes. + + Attributes: + _source: The keypress source signal. + _handlers: A dictionary of modes and their handlers. + mode: The current mode. + """ + + def __init__(self, sourcesig, parsers=None, parent=None): + """Constructor. + + Args: + sourcesig: The signal which gets emitted on a keypress. + parsers: A list of parsers to register. + """ + super().__init__(parent) + self._source = sourcesig + self._handlers = {} + self.mode = None + if parsers is not None: + for name, parser in parsers.items(): + self._handlers[name] = parser.handle + + def register(self, mode, handler): + """Register a new mode. + + Args: + mode: The name of the mode. + handler: Handler for keyPressEvents. + """ + self._handlers[mode] = handler + + def enter(self, mode): + """Enter a new mode.""" + oldmode = self.mode + logging.debug("Switching mode: {} -> {}".format(oldmode, mode)) + if oldmode is not None: + try: + self._source.disconnect(self._handlers[oldmode]) + except TypeError: + logging.debug("Could not disconnect mode {}".format(oldmode)) + self._source.connect(self._handlers[mode]) + self.mode = mode diff --git a/qutebrowser/widgets/tabbedbrowser.py b/qutebrowser/widgets/tabbedbrowser.py index ddd5460c4..7e334e630 100644 --- a/qutebrowser/widgets/tabbedbrowser.py +++ b/qutebrowser/widgets/tabbedbrowser.py @@ -69,8 +69,6 @@ class TabbedBrowser(TabWidget): arg 2: y-position in %. hint_strings_updated: Hint strings were updated. arg: A list of hint strings. - set_mode: The input mode should be changed. - arg: The new mode as a string. keypress: A key was pressed. arg: The QKeyEvent leading to the keypress. shutdown_complete: The shuttdown is completed. @@ -89,7 +87,6 @@ class TabbedBrowser(TabWidget): cur_scroll_perc_changed = pyqtSignal(int, int) hint_strings_updated = pyqtSignal(list) set_cmd_text = pyqtSignal(str) - set_mode = pyqtSignal(str) keypress = pyqtSignal('QKeyEvent') shutdown_complete = pyqtSignal() quit = pyqtSignal() @@ -143,7 +140,6 @@ class TabbedBrowser(TabWidget): tab.urlChanged.connect(self._filter.create(self.cur_url_changed)) # hintmanager tab.hintmanager.hint_strings_updated.connect(self.hint_strings_updated) - tab.hintmanager.set_mode.connect(self.set_mode) tab.hintmanager.set_cmd_text.connect(self.set_cmd_text) # misc tab.titleChanged.connect(self.on_title_changed) From ce48ed9b8e7ad2f1fe145c5f421d8706ba60ea73 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 23 Apr 2014 17:56:36 +0200 Subject: [PATCH 02/54] Add entered/leaved signals to modemanager --- qutebrowser/utils/modemanager.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/qutebrowser/utils/modemanager.py b/qutebrowser/utils/modemanager.py index 11c34451a..aaa0855bd 100644 --- a/qutebrowser/utils/modemanager.py +++ b/qutebrowser/utils/modemanager.py @@ -23,7 +23,7 @@ Module attributes: import logging -from PyQt5.QtCore import QObject +from PyQt5.QtCore import pyqtSignal, QObject manager = None @@ -67,8 +67,17 @@ class ModeManager(QObject): _source: The keypress source signal. _handlers: A dictionary of modes and their handlers. mode: The current mode. + + Signals: + entered: Emitted when a mode is entered. + arg: Name of the entered mode. + leaved: Emitted when a mode is leaved. + arg: Name of the leaved mode. """ + entered = pyqtSignal(str) + leaved = pyqtSignal(str) + def __init__(self, sourcesig, parsers=None, parent=None): """Constructor. @@ -94,7 +103,12 @@ class ModeManager(QObject): self._handlers[mode] = handler def enter(self, mode): - """Enter a new mode.""" + """Enter a new mode. + + Emit: + leaved: With the old mode name. + entered: With the new mode name. + """ oldmode = self.mode logging.debug("Switching mode: {} -> {}".format(oldmode, mode)) if oldmode is not None: @@ -102,5 +116,7 @@ class ModeManager(QObject): self._source.disconnect(self._handlers[oldmode]) except TypeError: logging.debug("Could not disconnect mode {}".format(oldmode)) + self.leaved.emit(oldmode) self._source.connect(self._handlers[mode]) self.mode = mode + self.entered.emit(mode) From a33c9827d69bc484e47c417184979720432c76e0 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 23 Apr 2014 17:57:59 +0200 Subject: [PATCH 03/54] Check for editable fields in mousePressEvent --- qutebrowser/widgets/browsertab.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/qutebrowser/widgets/browsertab.py b/qutebrowser/widgets/browsertab.py index e22032434..0d748bf9f 100644 --- a/qutebrowser/widgets/browsertab.py +++ b/qutebrowser/widgets/browsertab.py @@ -249,12 +249,13 @@ class BrowserTab(QWebView): return super().paintEvent(e) def mousePressEvent(self, e): - """Check if a link was clicked with the middle button or Ctrl. + """Extend QWidget::mousePressEvent(). - Extend the superclass mousePressEvent(). - - This also is a bit of a hack, but it seems it's the only possible way. - Set the _open_target attribute accordingly. + This does the following things: + - Check if a link was clicked with the middle button or Ctrl and + set the _open_target attribute accordingly. + - Emit the editable_elem_selected signal if an editable element was + clicked. Args: e: The arrived event. @@ -262,6 +263,14 @@ class BrowserTab(QWebView): Return: The superclass return value. """ + pos = e.pos() + frame = self.page_.frameAt(pos) + pos -= frame.geometry().topLeft() + hitresult = frame.hitTestContent(pos) + if hitresult.isContentEditable(): + logging.debug("Clicked editable element!") + self.setFocus() + if self._force_open_target is not None: self._open_target = self._force_open_target self._force_open_target = None From 8f44b569239ef80bc40b53cff25c25ce4feed92d Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 23 Apr 2014 22:22:58 +0200 Subject: [PATCH 04/54] Start implementing eventFilter in ModeManager --- THANKS | 1 + qutebrowser/app.py | 6 ++++-- qutebrowser/utils/modemanager.py | 28 +++++++++++++++------------- qutebrowser/widgets/browsertab.py | 3 ++- 4 files changed, 22 insertions(+), 16 deletions(-) diff --git a/THANKS b/THANKS index d5230912a..225b09651 100644 --- a/THANKS +++ b/THANKS @@ -54,6 +54,7 @@ channels: - scummos - svuorela - kpj + - hyde Thanks to these projects which were essential while developing qutebrowser: - Python diff --git a/qutebrowser/app.py b/qutebrowser/app.py index f90e07a06..861040200 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -45,7 +45,7 @@ import qutebrowser.utils.harfbuzz as harfbuzz harfbuzz.fix() from PyQt5.QtWidgets import QApplication, QDialog, QMessageBox -from PyQt5.QtCore import pyqtSlot, QTimer, QEventLoop +from PyQt5.QtCore import pyqtSignal, pyqtSlot, QTimer, QEventLoop, QEvent import qutebrowser import qutebrowser.commands.utils as cmdutils @@ -129,7 +129,8 @@ class QuteBrowser(QApplication): } self._init_cmds() self.mainwindow = MainWindow() - modemanager.init(self.mainwindow.tabs.keypress, self._keyparsers, self) + modemanager.init(self._keyparsers, self) + self.installEventFilter(modemanager.manager) self.setQuitOnLastWindowClosed(False) self._connect_signals() @@ -249,6 +250,7 @@ class QuteBrowser(QApplication): tabs.currentChanged.connect(self.mainwindow.update_inspector) # status bar + # FIXME what to do here? tabs.keypress.connect(status.keypress) for obj in [kp["normal"], tabs]: obj.set_cmd_text.connect(cmd.set_cmd_text) diff --git a/qutebrowser/utils/modemanager.py b/qutebrowser/utils/modemanager.py index aaa0855bd..523c7adf2 100644 --- a/qutebrowser/utils/modemanager.py +++ b/qutebrowser/utils/modemanager.py @@ -23,25 +23,24 @@ Module attributes: import logging -from PyQt5.QtCore import pyqtSignal, QObject +from PyQt5.QtCore import pyqtSignal, QObject, QEvent manager = None -def init(source, parsers=None, parent=None): +def init(parsers=None, parent=None): """Initialize the global ModeManager. This needs to be done by hand because the import time is before Qt is ready for everything. Args: - source: The keypress source signal. parsers: A dict of KeyParsers to register. parent: Parent to use for ModeManager. """ global manager - manager = ModeManager(source, parsers, parent) + manager = ModeManager(parsers, parent) def enter(mode): @@ -64,7 +63,6 @@ class ModeManager(QObject): """Manager for keyboard modes. Attributes: - _source: The keypress source signal. _handlers: A dictionary of modes and their handlers. mode: The current mode. @@ -78,15 +76,13 @@ class ModeManager(QObject): entered = pyqtSignal(str) leaved = pyqtSignal(str) - def __init__(self, sourcesig, parsers=None, parent=None): + def __init__(self, parsers=None, parent=None): """Constructor. Args: - sourcesig: The signal which gets emitted on a keypress. parsers: A list of parsers to register. """ super().__init__(parent) - self._source = sourcesig self._handlers = {} self.mode = None if parsers is not None: @@ -112,11 +108,17 @@ class ModeManager(QObject): oldmode = self.mode logging.debug("Switching mode: {} -> {}".format(oldmode, mode)) if oldmode is not None: - try: - self._source.disconnect(self._handlers[oldmode]) - except TypeError: - logging.debug("Could not disconnect mode {}".format(oldmode)) self.leaved.emit(oldmode) - self._source.connect(self._handlers[mode]) self.mode = mode self.entered.emit(mode) + + def eventFilter(self, obj, evt): + if evt.type() not in [QEvent.KeyPress, QEvent.KeyRelease]: + return False + elif self.mode == "insert": + return False + elif evt.type() == QEvent.KeyPress: + self._handlers[self.mode](evt) + return True + else: + return True diff --git a/qutebrowser/widgets/browsertab.py b/qutebrowser/widgets/browsertab.py index 0d748bf9f..dd9d3e995 100644 --- a/qutebrowser/widgets/browsertab.py +++ b/qutebrowser/widgets/browsertab.py @@ -28,6 +28,7 @@ from PyQt5.QtWebKitWidgets import QWebView, QWebPage import qutebrowser.utils.url as urlutils import qutebrowser.config.config as config import qutebrowser.utils.message as message +import qutebrowser.utils.modemanager as modemanager from qutebrowser.browser.webpage import BrowserPage from qutebrowser.browser.hints import HintManager from qutebrowser.utils.signals import SignalCache @@ -269,7 +270,7 @@ class BrowserTab(QWebView): hitresult = frame.hitTestContent(pos) if hitresult.isContentEditable(): logging.debug("Clicked editable element!") - self.setFocus() + modemanager.enter("insert") if self._force_open_target is not None: self._open_target = self._force_open_target From 522a703863506eb6f46481370a9cc0ed72406e4c Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 23 Apr 2014 23:21:29 +0200 Subject: [PATCH 05/54] Remove module-level modemanager register() --- qutebrowser/utils/modemanager.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/qutebrowser/utils/modemanager.py b/qutebrowser/utils/modemanager.py index 523c7adf2..000d04b9d 100644 --- a/qutebrowser/utils/modemanager.py +++ b/qutebrowser/utils/modemanager.py @@ -48,16 +48,6 @@ def enter(mode): manager.enter(mode) -def register(mode, handler): - """Register a new mode. - - Args: - mode: The name of the mode. - handler: Handler for keyPressEvents. - """ - manager.register(mode, handler) - - class ModeManager(QObject): """Manager for keyboard modes. From fc11021c089427f75ffd30ef288e8ed30f9d7e66 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 23 Apr 2014 23:22:34 +0200 Subject: [PATCH 06/54] Add a passthrough argument to modemanager register() --- qutebrowser/utils/modemanager.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/qutebrowser/utils/modemanager.py b/qutebrowser/utils/modemanager.py index 000d04b9d..208a35965 100644 --- a/qutebrowser/utils/modemanager.py +++ b/qutebrowser/utils/modemanager.py @@ -54,6 +54,7 @@ class ModeManager(QObject): Attributes: _handlers: A dictionary of modes and their handlers. + _passthrough: A list of modes in which to pass through events. mode: The current mode. Signals: @@ -74,19 +75,24 @@ class ModeManager(QObject): """ super().__init__(parent) self._handlers = {} + self._passthrough = [] self.mode = None if parsers is not None: for name, parser in parsers.items(): self._handlers[name] = parser.handle - def register(self, mode, handler): + def register(self, mode, handler, passthrough=False): """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. """ self._handlers[mode] = handler + if passthrough: + self._passthrough.append(mode) def enter(self, mode): """Enter a new mode. From 5385307582e4462ba9fe59369b41e65527cb1e07 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 23 Apr 2014 23:23:04 +0200 Subject: [PATCH 07/54] Register modes explicitely with modemanager. --- qutebrowser/app.py | 7 ++++++- qutebrowser/utils/modemanager.py | 15 +++------------ 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/qutebrowser/app.py b/qutebrowser/app.py index 861040200..23f32cd3f 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -129,7 +129,12 @@ class QuteBrowser(QApplication): } self._init_cmds() self.mainwindow = MainWindow() - modemanager.init(self._keyparsers, self) + modemanager.init(self) + modemanager.manager.register("normal", + self._keyparsers["normal"].handle) + modemanager.manager.register("hint", self._keyparsers["hint"].handle) + modemanager.manager.register("insert", None, passthrough=True) + modemanager.manager.register("command", None, passthrough=True) self.installEventFilter(modemanager.manager) self.setQuitOnLastWindowClosed(False) diff --git a/qutebrowser/utils/modemanager.py b/qutebrowser/utils/modemanager.py index 208a35965..c51b92ad7 100644 --- a/qutebrowser/utils/modemanager.py +++ b/qutebrowser/utils/modemanager.py @@ -29,18 +29,17 @@ from PyQt5.QtCore import pyqtSignal, QObject, QEvent manager = None -def init(parsers=None, parent=None): +def init(parent=None): """Initialize the global ModeManager. This needs to be done by hand because the import time is before Qt is ready for everything. Args: - parsers: A dict of KeyParsers to register. parent: Parent to use for ModeManager. """ global manager - manager = ModeManager(parsers, parent) + manager = ModeManager(parent) def enter(mode): @@ -67,19 +66,11 @@ class ModeManager(QObject): entered = pyqtSignal(str) leaved = pyqtSignal(str) - def __init__(self, parsers=None, parent=None): - """Constructor. - - Args: - parsers: A list of parsers to register. - """ + def __init__(self, parent=None): super().__init__(parent) self._handlers = {} self._passthrough = [] self.mode = None - if parsers is not None: - for name, parser in parsers.items(): - self._handlers[name] = parser.handle def register(self, mode, handler, passthrough=False): """Register a new mode. From e56d33badcb10b50d5dca1f2100ac6ce27d0f3ba Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 23 Apr 2014 23:23:30 +0200 Subject: [PATCH 08/54] Check if handler is available for new mode --- qutebrowser/utils/modemanager.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/qutebrowser/utils/modemanager.py b/qutebrowser/utils/modemanager.py index c51b92ad7..1fb262cb5 100644 --- a/qutebrowser/utils/modemanager.py +++ b/qutebrowser/utils/modemanager.py @@ -94,6 +94,8 @@ class ModeManager(QObject): """ oldmode = self.mode logging.debug("Switching mode: {} -> {}".format(oldmode, mode)) + if mode not in self._handlers: + raise ValueError("No handler for mode {}".format(mode)) if oldmode is not None: self.leaved.emit(oldmode) self.mode = mode From cd5f2562aabaa44cff0b641202a986b3a201e6d4 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 23 Apr 2014 23:24:46 +0200 Subject: [PATCH 09/54] Adjust eventFilter to use new features --- qutebrowser/utils/modemanager.py | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/qutebrowser/utils/modemanager.py b/qutebrowser/utils/modemanager.py index 1fb262cb5..bc5fc7f10 100644 --- a/qutebrowser/utils/modemanager.py +++ b/qutebrowser/utils/modemanager.py @@ -102,12 +102,29 @@ class ModeManager(QObject): self.entered.emit(mode) def eventFilter(self, obj, evt): - if evt.type() not in [QEvent.KeyPress, QEvent.KeyRelease]: + """Filter all events based on the currently set mode. + + Also calls the real keypress handler. + """ + typ = evt.type() + handler = self._handlers[self.mode] + if typ not in [QEvent.KeyPress, QEvent.KeyRelease]: + # We're not interested in non-key-events so we pass them through. return False - elif self.mode == "insert": + elif self.mode in self._passthrough: + # We're currently in a passthrough mode so we pass everything + # through.*and* let the passthrough keyhandler know. + # FIXME what if we leave the passthrough mode right here? + if handler is not None: + handler(evt) return False - elif evt.type() == QEvent.KeyPress: - self._handlers[self.mode](evt) + elif typ == QEvent.KeyPress: + # KeyPress in a non-passthrough mode - call handler and filter + # event from widgets + if handler is not None: + handler(evt) return True else: + # KeyRelease in a non-passthrough mode - filter event and ignore it + # entirely. return True From b4b9b6c69dd6fb29d7454bf5b69a8503d6279b56 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 23 Apr 2014 23:25:06 +0200 Subject: [PATCH 10/54] Enter/leave command mode for Command widget --- qutebrowser/widgets/statusbar.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/qutebrowser/widgets/statusbar.py b/qutebrowser/widgets/statusbar.py index dbb9bb45d..08eabab04 100644 --- a/qutebrowser/widgets/statusbar.py +++ b/qutebrowser/widgets/statusbar.py @@ -23,8 +23,9 @@ from PyQt5.QtWidgets import (QWidget, QLineEdit, QProgressBar, QLabel, QShortcut) from PyQt5.QtGui import QPainter, QKeySequence, QValidator -from qutebrowser.config.style import set_register_stylesheet, get_stylesheet import qutebrowser.commands.keys as keys +import qutebrowser.utils.modemanager as modemanager +from qutebrowser.config.style import set_register_stylesheet, get_stylesheet from qutebrowser.utils.url import urlstring from qutebrowser.commands.parsers import split_cmdline from qutebrowser.models.cmdhistory import (History, HistoryEmptyError, @@ -342,8 +343,18 @@ class _Command(QLineEdit): self.setFocus() self.show_cmd.emit() + def focusInEvent(self, e): + """Extend focusInEvent to enter command mode.""" + modemanager.enter("command") + super().focusInEvent(e) + def focusOutEvent(self, e): - """Clear the statusbar text if it's explicitely unfocused. + """Extend focusOutEvent to do several tasks. + + - Clear the statusbar text if it's explicitely unfocused. + - Leave command mode + - Clear completion selection + - Hide completion Args: e: The QFocusEvent. @@ -352,6 +363,7 @@ class _Command(QLineEdit): clear_completion_selection: Always emitted. hide_completion: Always emitted so the completion is hidden. """ + modemanager.enter("normal") if e.reason() in [Qt.MouseFocusReason, Qt.TabFocusReason, Qt.BacktabFocusReason, Qt.OtherFocusReason]: self.setText('') From f3db29c0106c27fbd60ff24f389b3ba01ad8da8b Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 23 Apr 2014 23:26:02 +0200 Subject: [PATCH 11/54] Enter normal mode when clicking non-editable elem --- qutebrowser/widgets/browsertab.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/qutebrowser/widgets/browsertab.py b/qutebrowser/widgets/browsertab.py index dd9d3e995..0fd04a91f 100644 --- a/qutebrowser/widgets/browsertab.py +++ b/qutebrowser/widgets/browsertab.py @@ -271,6 +271,8 @@ class BrowserTab(QWebView): if hitresult.isContentEditable(): logging.debug("Clicked editable element!") modemanager.enter("insert") + else: + modemanager.enter("normal") if self._force_open_target is not None: self._open_target = self._force_open_target From 19d25b228200a85a7a958193912ce2b03bb995be Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 24 Apr 2014 00:00:59 +0200 Subject: [PATCH 12/54] Update TODO --- TODO | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/TODO b/TODO index ece038464..43268b4b9 100644 --- a/TODO +++ b/TODO @@ -1,3 +1,20 @@ +keyparser foo +============= + +- Get to insert mode when clicking flash plugins +- Handle keybind to get out of insert mode (e.g. esc) +- Pass keypresses to statusbar correctly +- Use stack for modes and .leave() +- Switch to normal mode if new page loaded +- Add passthrough-keys option to pass through unmapped keys +- Add auto-insert-mode to switch to insert mode if editable element is focused + after page load +- Add more element-selection-detection code (with options?) based on: + -> javascript: http://stackoverflow.com/a/2848120/2085149 + -> microFocusChanged and check active element via: + frame = page.currentFrame() + elem = frame.findFirstElement('*:focus') + Bugs ==== @@ -37,13 +54,6 @@ Before Blink session handling / saving IPC, like dwb -x -Mode handling? - - Problem: how to detect we're going to insert mode: - -> Detect mouse clicks and use QWebFrame::hitTestContent (only mouse) - -> Use javascript: http://stackoverflow.com/a/2848120/2085149 - -> Use microFocusChanged and check active element via: - frame = page.currentFrame() - elem = frame.findFirstElement('*:focus') Bookmarks Internationalization Marks From 3d292fbc27ad1c60152279a8198e2502451d5bd5 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 24 Apr 2014 06:44:58 +0200 Subject: [PATCH 13/54] Use a stack for current modes --- TODO | 1 - qutebrowser/browser/hints.py | 2 +- qutebrowser/utils/modemanager.py | 45 +++++++++++++++++++++++++------ qutebrowser/widgets/browsertab.py | 5 +++- qutebrowser/widgets/statusbar.py | 2 +- 5 files changed, 43 insertions(+), 12 deletions(-) diff --git a/TODO b/TODO index 43268b4b9..b1c9de953 100644 --- a/TODO +++ b/TODO @@ -4,7 +4,6 @@ keyparser foo - Get to insert mode when clicking flash plugins - Handle keybind to get out of insert mode (e.g. esc) - Pass keypresses to statusbar correctly -- Use stack for modes and .leave() - Switch to normal mode if new page loaded - Add passthrough-keys option to pass through unmapped keys - Add auto-insert-mode to switch to insert mode if editable element is focused diff --git a/qutebrowser/browser/hints.py b/qutebrowser/browser/hints.py index c5dde063a..775c9c4ca 100644 --- a/qutebrowser/browser/hints.py +++ b/qutebrowser/browser/hints.py @@ -403,7 +403,7 @@ class HintManager(QObject): self._elems = {} self._target = None self._frame = None - modemanager.enter("normal") + modemanager.leave("hint") message.clear() def handle_partial_key(self, keystr): diff --git a/qutebrowser/utils/modemanager.py b/qutebrowser/utils/modemanager.py index bc5fc7f10..88d2bb86c 100644 --- a/qutebrowser/utils/modemanager.py +++ b/qutebrowser/utils/modemanager.py @@ -47,6 +47,11 @@ def enter(mode): manager.enter(mode) +def leave(mode): + """Leave the mode 'mode'.""" + manager.leave(mode) + + class ModeManager(QObject): """Manager for keyboard modes. @@ -54,7 +59,9 @@ class ModeManager(QObject): Attributes: _handlers: A dictionary of modes and their handlers. _passthrough: A list of modes in which to pass through events. - mode: The current mode. + _mode_stack: A list of the modes we're currently in, with the active + one on the right. + mode: The current mode (readonly property). Signals: entered: Emitted when a mode is entered. @@ -70,7 +77,14 @@ class ModeManager(QObject): super().__init__(parent) self._handlers = {} self._passthrough = [] - self.mode = None + self._mode_stack = [] + + @property + def mode(self): + """Read-only property for the current mode.""" + if not self._mode_stack: + return None + return self._mode_stack[-1] def register(self, mode, handler, passthrough=False): """Register a new mode. @@ -88,19 +102,34 @@ class ModeManager(QObject): def enter(self, mode): """Enter a new mode. + Args: + mode; The name of the mode to enter. + Emit: - leaved: With the old mode name. entered: With the new mode name. """ - oldmode = self.mode - logging.debug("Switching mode: {} -> {}".format(oldmode, mode)) + logging.debug("Switching mode to {}".format(mode)) + logging.debug("Mode stack: {}".format(self._mode_stack)) if mode not in self._handlers: raise ValueError("No handler for mode {}".format(mode)) - if oldmode is not None: - self.leaved.emit(oldmode) - self.mode = mode + self._mode_stack.append(mode) self.entered.emit(mode) + def leave(self, mode): + """Leave a mode. + + Args: + mode; The name of the mode to leave. + + Emit: + leaved: With the old mode name. + """ + try: + self._mode_stack.remove(mode) + except ValueError: + raise ValueError("Mode {} not on mode stack!".format(mode)) + self.leaved.emit(mode) + def eventFilter(self, obj, evt): """Filter all events based on the currently set mode. diff --git a/qutebrowser/widgets/browsertab.py b/qutebrowser/widgets/browsertab.py index 0fd04a91f..e75291b02 100644 --- a/qutebrowser/widgets/browsertab.py +++ b/qutebrowser/widgets/browsertab.py @@ -272,7 +272,10 @@ class BrowserTab(QWebView): logging.debug("Clicked editable element!") modemanager.enter("insert") else: - modemanager.enter("normal") + try: + modemanager.leave("insert") + except ValueError: + pass if self._force_open_target is not None: self._open_target = self._force_open_target diff --git a/qutebrowser/widgets/statusbar.py b/qutebrowser/widgets/statusbar.py index 08eabab04..c2c6a311d 100644 --- a/qutebrowser/widgets/statusbar.py +++ b/qutebrowser/widgets/statusbar.py @@ -363,7 +363,7 @@ class _Command(QLineEdit): clear_completion_selection: Always emitted. hide_completion: Always emitted so the completion is hidden. """ - modemanager.enter("normal") + modemanager.leave("command") if e.reason() in [Qt.MouseFocusReason, Qt.TabFocusReason, Qt.BacktabFocusReason, Qt.OtherFocusReason]: self.setText('') From afa9c478674726350914b982702beb521609884f Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 24 Apr 2014 06:45:38 +0200 Subject: [PATCH 14/54] Lint cleanups --- qutebrowser/app.py | 2 +- qutebrowser/utils/modemanager.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/qutebrowser/app.py b/qutebrowser/app.py index 23f32cd3f..7ed4d4661 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -45,7 +45,7 @@ import qutebrowser.utils.harfbuzz as harfbuzz harfbuzz.fix() from PyQt5.QtWidgets import QApplication, QDialog, QMessageBox -from PyQt5.QtCore import pyqtSignal, pyqtSlot, QTimer, QEventLoop, QEvent +from PyQt5.QtCore import pyqtSlot, QTimer, QEventLoop import qutebrowser import qutebrowser.commands.utils as cmdutils diff --git a/qutebrowser/utils/modemanager.py b/qutebrowser/utils/modemanager.py index 88d2bb86c..64fdc67c7 100644 --- a/qutebrowser/utils/modemanager.py +++ b/qutebrowser/utils/modemanager.py @@ -130,7 +130,7 @@ class ModeManager(QObject): raise ValueError("Mode {} not on mode stack!".format(mode)) self.leaved.emit(mode) - def eventFilter(self, obj, evt): + def eventFilter(self, _obj, evt): """Filter all events based on the currently set mode. Also calls the real keypress handler. From 95691e1e11ae503bcdbdb7461f1bc455bc2152ef Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 24 Apr 2014 06:59:39 +0200 Subject: [PATCH 15/54] Show passthrough modes in statusbar --- qutebrowser/app.py | 2 ++ qutebrowser/utils/modemanager.py | 10 +++++----- qutebrowser/widgets/statusbar.py | 12 ++++++++++++ 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/qutebrowser/app.py b/qutebrowser/app.py index 7ed4d4661..55c999ece 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -255,6 +255,8 @@ class QuteBrowser(QApplication): tabs.currentChanged.connect(self.mainwindow.update_inspector) # status bar + modemanager.manager.entered.connect(status.on_mode_entered) + modemanager.manager.leaved.connect(status.on_mode_left) # FIXME what to do here? tabs.keypress.connect(status.keypress) for obj in [kp["normal"], tabs]: diff --git a/qutebrowser/utils/modemanager.py b/qutebrowser/utils/modemanager.py index 64fdc67c7..ab3e66a5b 100644 --- a/qutebrowser/utils/modemanager.py +++ b/qutebrowser/utils/modemanager.py @@ -57,11 +57,11 @@ class ModeManager(QObject): """Manager for keyboard modes. Attributes: + mode: The current mode (readonly property). + passthrough: A list of modes in which to pass through events. _handlers: A dictionary of modes and their handlers. - _passthrough: A list of modes in which to pass through events. _mode_stack: A list of the modes we're currently in, with the active one on the right. - mode: The current mode (readonly property). Signals: entered: Emitted when a mode is entered. @@ -76,7 +76,7 @@ class ModeManager(QObject): def __init__(self, parent=None): super().__init__(parent) self._handlers = {} - self._passthrough = [] + self.passthrough = [] self._mode_stack = [] @property @@ -97,7 +97,7 @@ class ModeManager(QObject): """ self._handlers[mode] = handler if passthrough: - self._passthrough.append(mode) + self.passthrough.append(mode) def enter(self, mode): """Enter a new mode. @@ -140,7 +140,7 @@ class ModeManager(QObject): if typ not in [QEvent.KeyPress, QEvent.KeyRelease]: # We're not interested in non-key-events so we pass them through. return False - elif self.mode in self._passthrough: + elif self.mode in self.passthrough: # We're currently in a passthrough mode so we pass everything # through.*and* let the passthrough keyhandler know. # FIXME what if we leave the passthrough mode right here? diff --git a/qutebrowser/widgets/statusbar.py b/qutebrowser/widgets/statusbar.py index c2c6a311d..755030450 100644 --- a/qutebrowser/widgets/statusbar.py +++ b/qutebrowser/widgets/statusbar.py @@ -170,6 +170,18 @@ class StatusBar(QWidget): self.txt.set_temptext('') self.clear_error() + @pyqtSlot(str) + def on_mode_entered(self, mode): + """Mark certain modes in the commandline.""" + if mode in modemanager.manager.passthrough: + self.txt.normaltext = "-- {} MODE --".format(mode.upper()) + + @pyqtSlot(str) + def on_mode_left(self, mode): + """Clear marked mode.""" + if mode in modemanager.manager.passthrough: + self.txt.normaltext = "" + def resizeEvent(self, e): """Extend resizeEvent of QWidget to emit a resized signal afterwards. From 5b84848ad98274d0c475c466297e70e083cd5e4b Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 24 Apr 2014 07:01:27 +0200 Subject: [PATCH 16/54] s/leaved/left/g --- qutebrowser/app.py | 2 +- qutebrowser/utils/modemanager.py | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/qutebrowser/app.py b/qutebrowser/app.py index 55c999ece..10d454a92 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -256,7 +256,7 @@ class QuteBrowser(QApplication): # status bar modemanager.manager.entered.connect(status.on_mode_entered) - modemanager.manager.leaved.connect(status.on_mode_left) + modemanager.manager.left.connect(status.on_mode_left) # FIXME what to do here? tabs.keypress.connect(status.keypress) for obj in [kp["normal"], tabs]: diff --git a/qutebrowser/utils/modemanager.py b/qutebrowser/utils/modemanager.py index ab3e66a5b..b177bf233 100644 --- a/qutebrowser/utils/modemanager.py +++ b/qutebrowser/utils/modemanager.py @@ -66,12 +66,12 @@ class ModeManager(QObject): Signals: entered: Emitted when a mode is entered. arg: Name of the entered mode. - leaved: Emitted when a mode is leaved. - arg: Name of the leaved mode. + left: Emitted when a mode is left. + arg: Name of the left mode. """ entered = pyqtSignal(str) - leaved = pyqtSignal(str) + left = pyqtSignal(str) def __init__(self, parent=None): super().__init__(parent) @@ -122,13 +122,13 @@ class ModeManager(QObject): mode; The name of the mode to leave. Emit: - leaved: With the old mode name. + left: With the old mode name. """ try: self._mode_stack.remove(mode) except ValueError: raise ValueError("Mode {} not on mode stack!".format(mode)) - self.leaved.emit(mode) + self.left.emit(mode) def eventFilter(self, _obj, evt): """Filter all events based on the currently set mode. From a82ab6d7073e2c8b22d2b3a430f2e08f0207e973 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 24 Apr 2014 07:41:20 +0200 Subject: [PATCH 17/54] Go to insert mode when plugin clicked --- TODO | 1 - qutebrowser/config/configdata.py | 5 +++++ qutebrowser/widgets/browsertab.py | 27 ++++++++++++++++++++++++++- 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/TODO b/TODO index b1c9de953..4f6a8db2b 100644 --- a/TODO +++ b/TODO @@ -1,7 +1,6 @@ keyparser foo ============= -- Get to insert mode when clicking flash plugins - Handle keybind to get out of insert mode (e.g. esc) - Pass keypresses to statusbar correctly - Switch to normal mode if new page loaded diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index 808b61b18..6193b4002 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -166,6 +166,11 @@ DATA = OrderedDict([ ('cmd_timeout', SettingValue(types.Int(minval=0), "500"), "Timeout for ambiguous keybindings."), + + ('insert_mode_on_plugins', + SettingValue(types.Bool(), "true"), + "Whether to switch to insert mode when clicking flash and other " + "plugins."), )), ('tabbar', sect.KeyValue( diff --git a/qutebrowser/widgets/browsertab.py b/qutebrowser/widgets/browsertab.py index e75291b02..43a6619cd 100644 --- a/qutebrowser/widgets/browsertab.py +++ b/qutebrowser/widgets/browsertab.py @@ -110,6 +110,30 @@ class BrowserTab(QWebView): logging.debug("Everything destroyed, calling callback") self._shutdown_callback() + def _is_editable(self, hitresult): + """Checks if the hitresult needs keyboard focus. + + Args: + hitresult: A QWebHitTestResult + """ + # FIXME is this algorithm accurate? + if hitresult.isContentEditable(): + return True + if not config.get('general', 'insert_mode_on_plugins'): + return False + elem = hitresult.element() + tag = elem.tagName().lower() + if tag in ['embed', 'applet']: + return True + elif tag == 'object': + if not elem.hasAttribute("type"): + logging.warn(" without type clicked...") + return False + objtype = elem.attribute("type") + if not objtype.startswith("image/"): + logging.debug(" clicked.".format(objtype)) + return True + def openurl(self, url): """Open an URL in the browser. @@ -268,10 +292,11 @@ class BrowserTab(QWebView): frame = self.page_.frameAt(pos) pos -= frame.geometry().topLeft() hitresult = frame.hitTestContent(pos) - if hitresult.isContentEditable(): + if self._is_editable(hitresult): logging.debug("Clicked editable element!") modemanager.enter("insert") else: + logging.debug("Clicked non-editable element!") try: modemanager.leave("insert") except ValueError: From b9d845180ea2be94cc15144263a9a3db10922e86 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 24 Apr 2014 07:44:47 +0200 Subject: [PATCH 18/54] Better logging for ModeManager --- qutebrowser/utils/modemanager.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/qutebrowser/utils/modemanager.py b/qutebrowser/utils/modemanager.py index b177bf233..ba28462c3 100644 --- a/qutebrowser/utils/modemanager.py +++ b/qutebrowser/utils/modemanager.py @@ -109,10 +109,10 @@ class ModeManager(QObject): entered: With the new mode name. """ logging.debug("Switching mode to {}".format(mode)) - logging.debug("Mode stack: {}".format(self._mode_stack)) if mode not in self._handlers: raise ValueError("No handler for mode {}".format(mode)) self._mode_stack.append(mode) + logging.debug("New mode stack: {}".format(self._mode_stack)) self.entered.emit(mode) def leave(self, mode): @@ -128,6 +128,8 @@ class ModeManager(QObject): self._mode_stack.remove(mode) except ValueError: raise ValueError("Mode {} not on mode stack!".format(mode)) + logging.debug("Leaving mode {}".format(mode)) + logging.debug("New mode stack: {}".format(self._mode_stack)) self.left.emit(mode) def eventFilter(self, _obj, evt): From ff887c647d399ab0e504a53aef6e42f9b39e26a8 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 24 Apr 2014 07:44:54 +0200 Subject: [PATCH 19/54] Don't append mode to mode stack if it's already --- qutebrowser/utils/modemanager.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/qutebrowser/utils/modemanager.py b/qutebrowser/utils/modemanager.py index ba28462c3..f3060ce68 100644 --- a/qutebrowser/utils/modemanager.py +++ b/qutebrowser/utils/modemanager.py @@ -111,6 +111,9 @@ class ModeManager(QObject): logging.debug("Switching mode to {}".format(mode)) if mode not in self._handlers: raise ValueError("No handler for mode {}".format(mode)) + if self._mode_stack and self._mode_stack[-1] == mode: + logging.debug("Already at end of stack, doing nothing") + return self._mode_stack.append(mode) logging.debug("New mode stack: {}".format(self._mode_stack)) self.entered.emit(mode) From b3418cae5dd96de0321c14305fafd69f1686b7a1 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 24 Apr 2014 13:13:58 +0200 Subject: [PATCH 20/54] Improve _is_editable() --- qutebrowser/widgets/browsertab.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/qutebrowser/widgets/browsertab.py b/qutebrowser/widgets/browsertab.py index 43a6619cd..06c78854c 100644 --- a/qutebrowser/widgets/browsertab.py +++ b/qutebrowser/widgets/browsertab.py @@ -111,28 +111,36 @@ class BrowserTab(QWebView): self._shutdown_callback() def _is_editable(self, hitresult): - """Checks if the hitresult needs keyboard focus. + """Check if a hit result needs keyboard focus. Args: hitresult: A QWebHitTestResult """ # FIXME is this algorithm accurate? if hitresult.isContentEditable(): + # text fields and the like return True if not config.get('general', 'insert_mode_on_plugins'): return False elem = hitresult.element() tag = elem.tagName().lower() if tag in ['embed', 'applet']: + # Flash/Java/... return True - elif tag == 'object': + if tag == 'object': + # Could be Flash/Java/..., could be image/audio/... if not elem.hasAttribute("type"): - logging.warn(" without type clicked...") + logging.debug(" without type clicked...") return False objtype = elem.attribute("type") - if not objtype.startswith("image/"): + if (objtype.startswith("application/") or + elem.hasAttribute("classid")): + # Let's hope flash/java stuff has an application/* mimetype OR + # at least a classid attribute. Oh, and let's home images/... + # DON"T have a classid attribute. HTML sucks. logging.debug(" clicked.".format(objtype)) return True + return False def openurl(self, url): """Open an URL in the browser. From 10d7d887ec99ce330c27afde3227acbfc2f785ec Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 24 Apr 2014 15:47:38 +0200 Subject: [PATCH 21/54] Pass keypresses to statusbar correctly --- TODO | 1 - qutebrowser/app.py | 2 +- qutebrowser/utils/modemanager.py | 7 +++++++ qutebrowser/widgets/statusbar.py | 2 +- qutebrowser/widgets/tabbedbrowser.py | 15 --------------- 5 files changed, 9 insertions(+), 18 deletions(-) diff --git a/TODO b/TODO index 4f6a8db2b..409dc37c3 100644 --- a/TODO +++ b/TODO @@ -2,7 +2,6 @@ keyparser foo ============= - Handle keybind to get out of insert mode (e.g. esc) -- Pass keypresses to statusbar correctly - Switch to normal mode if new page loaded - Add passthrough-keys option to pass through unmapped keys - Add auto-insert-mode to switch to insert mode if editable element is focused diff --git a/qutebrowser/app.py b/qutebrowser/app.py index 10d454a92..333a2178a 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -258,7 +258,7 @@ class QuteBrowser(QApplication): modemanager.manager.entered.connect(status.on_mode_entered) modemanager.manager.left.connect(status.on_mode_left) # FIXME what to do here? - tabs.keypress.connect(status.keypress) + modemanager.manager.key_pressed.connect(status.on_key_pressed) for obj in [kp["normal"], tabs]: obj.set_cmd_text.connect(cmd.set_cmd_text) diff --git a/qutebrowser/utils/modemanager.py b/qutebrowser/utils/modemanager.py index f3060ce68..9a02743e2 100644 --- a/qutebrowser/utils/modemanager.py +++ b/qutebrowser/utils/modemanager.py @@ -68,10 +68,12 @@ class ModeManager(QObject): arg: Name of the entered mode. left: Emitted when a mode is left. arg: Name of the left mode. + key_pressed; A key was pressed. """ entered = pyqtSignal(str) left = pyqtSignal(str) + key_pressed = pyqtSignal('QKeyEvent') def __init__(self, parent=None): super().__init__(parent) @@ -139,6 +141,9 @@ class ModeManager(QObject): """Filter all events based on the currently set mode. Also calls the real keypress handler. + + Emit: + key_pressed: When a key was actually pressed. """ typ = evt.type() handler = self._handlers[self.mode] @@ -149,12 +154,14 @@ class ModeManager(QObject): # We're currently in a passthrough mode so we pass everything # through.*and* let the passthrough keyhandler know. # FIXME what if we leave the passthrough mode right here? + self.key_pressed.emit(evt) if handler is not None: handler(evt) return False elif typ == QEvent.KeyPress: # KeyPress in a non-passthrough mode - call handler and filter # event from widgets + self.key_pressed.emit(evt) if handler is not None: handler(evt) return True diff --git a/qutebrowser/widgets/statusbar.py b/qutebrowser/widgets/statusbar.py index 755030450..959aa0d63 100644 --- a/qutebrowser/widgets/statusbar.py +++ b/qutebrowser/widgets/statusbar.py @@ -158,7 +158,7 @@ class StatusBar(QWidget): self.txt.errortext = '' @pyqtSlot('QKeyEvent') - def keypress(self, e): + def on_key_pressed(self, e): """Hide temporary error message if a key was pressed. Args: diff --git a/qutebrowser/widgets/tabbedbrowser.py b/qutebrowser/widgets/tabbedbrowser.py index 7e334e630..013d5429c 100644 --- a/qutebrowser/widgets/tabbedbrowser.py +++ b/qutebrowser/widgets/tabbedbrowser.py @@ -69,8 +69,6 @@ class TabbedBrowser(TabWidget): arg 2: y-position in %. hint_strings_updated: Hint strings were updated. arg: A list of hint strings. - keypress: A key was pressed. - arg: The QKeyEvent leading to the keypress. shutdown_complete: The shuttdown is completed. quit: The last tab was closed, quit application. resized: Emitted when the browser window has resized, so the completion @@ -87,7 +85,6 @@ class TabbedBrowser(TabWidget): cur_scroll_perc_changed = pyqtSignal(int, int) hint_strings_updated = pyqtSignal(list) set_cmd_text = pyqtSignal(str) - keypress = pyqtSignal('QKeyEvent') shutdown_complete = pyqtSignal() quit = pyqtSignal() resized = pyqtSignal('QRect') @@ -356,18 +353,6 @@ class TabbedBrowser(TabWidget): else: logging.debug('ignoring title change') - def keyPressEvent(self, e): - """Extend TabWidget (QWidget)'s keyPressEvent to emit a signal. - - Args: - e: The QKeyPressEvent - - Emit: - keypress: Always emitted. - """ - self.keypress.emit(e) - super().keyPressEvent(e) - def resizeEvent(self, e): """Extend resizeEvent of QWidget to emit a resized signal afterwards. From c2d871a7c38627f543f1f24317b349997b61e3b5 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 24 Apr 2014 15:47:53 +0200 Subject: [PATCH 22/54] TODO -= keypress-signal-foo --- TODO | 1 - 1 file changed, 1 deletion(-) diff --git a/TODO b/TODO index 409dc37c3..4e7d7c29c 100644 --- a/TODO +++ b/TODO @@ -25,7 +25,6 @@ Style ===== Refactor completion widget mess (initializing / changing completions) -keypress-signal-foo is a bit of a chaos and might be done better we probably could replace CompletionModel with QStandardModel... Major features From 1c5ae25b683a73c5265f7fb24239780fac24f947 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 24 Apr 2014 16:03:16 +0200 Subject: [PATCH 23/54] Start implementing auto_insert_mode --- TODO | 2 -- qutebrowser/config/configdata.py | 5 +++++ qutebrowser/utils/modemanager.py | 8 ++++++++ qutebrowser/widgets/browsertab.py | 16 ++++++++++++++++ 4 files changed, 29 insertions(+), 2 deletions(-) diff --git a/TODO b/TODO index 4e7d7c29c..8ca472745 100644 --- a/TODO +++ b/TODO @@ -4,8 +4,6 @@ keyparser foo - Handle keybind to get out of insert mode (e.g. esc) - Switch to normal mode if new page loaded - Add passthrough-keys option to pass through unmapped keys -- Add auto-insert-mode to switch to insert mode if editable element is focused - after page load - Add more element-selection-detection code (with options?) based on: -> javascript: http://stackoverflow.com/a/2848120/2085149 -> microFocusChanged and check active element via: diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index 6193b4002..5d18bd43e 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -171,6 +171,11 @@ DATA = OrderedDict([ SettingValue(types.Bool(), "true"), "Whether to switch to insert mode when clicking flash and other " "plugins."), + + ('auto_insert_mode', + SettingValue(types.Bool(), "true"), + "Whether to automatically enter insert mode if an editable element " + "is focused after page load."), )), ('tabbar', sect.KeyValue( diff --git a/qutebrowser/utils/modemanager.py b/qutebrowser/utils/modemanager.py index 9a02743e2..87632c766 100644 --- a/qutebrowser/utils/modemanager.py +++ b/qutebrowser/utils/modemanager.py @@ -52,6 +52,14 @@ def leave(mode): manager.leave(mode) +def maybe_leave(mode): + """Convenience method to leave 'mode' without exceptions.""" + try: + manager.leave(mode) + except ValueError: + pass + + class ModeManager(QObject): """Manager for keyboard modes. diff --git a/qutebrowser/widgets/browsertab.py b/qutebrowser/widgets/browsertab.py index 06c78854c..4c0913ac0 100644 --- a/qutebrowser/widgets/browsertab.py +++ b/qutebrowser/widgets/browsertab.py @@ -85,6 +85,7 @@ class BrowserTab(QWebView): self.page_.setLinkDelegationPolicy(QWebPage.DelegateAllLinks) self.page_.linkHovered.connect(self.linkHovered) self.linkClicked.connect(self.on_link_clicked) + self.loadFinished.connect(self.on_load_finished) # FIXME find some way to hide scrollbars without setScrollBarPolicy def _init_neighborlist(self): @@ -242,6 +243,21 @@ class BrowserTab(QWebView): self.setFocus() QApplication.postEvent(self, evt) + @pyqtSlot(bool) + def on_load_finished(self, ok): + """Handle insert mode after loading finished.""" + if config.get('general', 'auto_insert_mode'): + frame = self.page_.currentFrame() + # FIXME better selector (from hintmanager) + elem = frame.findFirstElement('*:focus') + logging.debug("focus element: {}".format(not elem.isNull())) + if elem.isNull(): + modemanager.maybe_leave("insert") + else: + modemanager.enter("insert") + else: + modemanager.maybe_leave("insert") + @pyqtSlot(str) def set_force_open_target(self, target): """Change the forced link target. Setter for _force_open_target. From 0e542772d0d03ee07f3f2271b28ae63e5eeaa3ae Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 24 Apr 2014 16:26:50 +0200 Subject: [PATCH 24/54] TODO -= normal mode --- TODO | 1 - 1 file changed, 1 deletion(-) diff --git a/TODO b/TODO index 8ca472745..c2a1df38b 100644 --- a/TODO +++ b/TODO @@ -2,7 +2,6 @@ keyparser foo ============= - Handle keybind to get out of insert mode (e.g. esc) -- Switch to normal mode if new page loaded - Add passthrough-keys option to pass through unmapped keys - Add more element-selection-detection code (with options?) based on: -> javascript: http://stackoverflow.com/a/2848120/2085149 From 6fb52e610da4b2923b79c483d67974bafc0e9c49 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 24 Apr 2014 16:27:18 +0200 Subject: [PATCH 25/54] Leave insert mode when loadFinished with ok=False --- qutebrowser/widgets/browsertab.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/qutebrowser/widgets/browsertab.py b/qutebrowser/widgets/browsertab.py index 4c0913ac0..9ea3c9a18 100644 --- a/qutebrowser/widgets/browsertab.py +++ b/qutebrowser/widgets/browsertab.py @@ -246,7 +246,9 @@ class BrowserTab(QWebView): @pyqtSlot(bool) def on_load_finished(self, ok): """Handle insert mode after loading finished.""" - if config.get('general', 'auto_insert_mode'): + if not ok: + modemanager.maybe_leave("insert") + elif config.get('general', 'auto_insert_mode'): frame = self.page_.currentFrame() # FIXME better selector (from hintmanager) elem = frame.findFirstElement('*:focus') From b372c23b80df38f8e953d1ccb11a4a90d38fc41a Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 24 Apr 2014 16:28:00 +0200 Subject: [PATCH 26/54] Move hint webelem code to utils.webelem --- qutebrowser/browser/hints.py | 40 +++-------------- qutebrowser/utils/webelem.py | 74 +++++++++++++++++++++++++++++++ qutebrowser/widgets/browsertab.py | 5 ++- 3 files changed, 82 insertions(+), 37 deletions(-) create mode 100644 qutebrowser/utils/webelem.py diff --git a/qutebrowser/browser/hints.py b/qutebrowser/browser/hints.py index 775c9c4ca..4dbdc4862 100644 --- a/qutebrowser/browser/hints.py +++ b/qutebrowser/browser/hints.py @@ -29,6 +29,7 @@ import qutebrowser.config.config as config import qutebrowser.utils.message as message import qutebrowser.utils.url as urlutils import qutebrowser.utils.modemanager as modemanager +import qutebrowser.utils.webelem as webelem from qutebrowser.utils.keyparser import KeyParser @@ -86,10 +87,6 @@ class HintManager(QObject): """Manage drawing hints over links or other elements. Class attributes: - SELECTORS: CSS selectors for the different highlighting modes. - FILTERS: A dictionary of filter functions for the modes. - The filter for "links" filters javascript:-links and a-tags - without "href". HINT_CSS: The CSS template to use for hints. Attributes: @@ -114,23 +111,6 @@ class HintManager(QObject): set_cmd_text: Emitted when the commandline text should be set. """ - SELECTORS = { - "all": ("a, textarea, select, input:not([type=hidden]), button, " - "frame, iframe, [onclick], [onmousedown], [role=link], " - "[role=option], [role=button], img"), - "links": "a", - "images": "img", - "editable": ("input[type=text], input[type=email], input[type=url]," - "input[type=tel], input[type=number], " - "input[type=password], input[type=search], textarea"), - "url": "[src], [href]", - } - - FILTERS = { - "links": (lambda e: e.hasAttribute("href") and - urlutils.qurl(e.attribute("href")).scheme() != "javascript"), - } - HINT_CSS = """ color: {config[colors][hints.fg]}; background: {config[colors][hints.bg]}; @@ -355,22 +335,12 @@ class HintManager(QObject): self._target = target self._baseurl = baseurl self._frame = frame - elems = frame.findAllElements(self.SELECTORS[mode]) - filterfunc = self.FILTERS.get(mode, lambda e: True) + elems = frame.findAllElements(webelem.SELECTORS[mode]) + filterfunc = webelem.FILTERS.get(mode, lambda e: True) visible_elems = [] for e in elems: - if not filterfunc(e): - continue - rect = e.geometry() - if (not rect.isValid()) and rect.x() == 0: - # Most likely an invisible link - continue - framegeom = frame.geometry() - framegeom.translate(frame.scrollPosition()) - if not framegeom.contains(rect.topLeft()): - # out of screen - continue - visible_elems.append(e) + if filterfunc(e) and webelem.is_visible(e, self._frame): + visible_elems.append(e) if not visible_elems: message.error("No elements found.") return diff --git a/qutebrowser/utils/webelem.py b/qutebrowser/utils/webelem.py new file mode 100644 index 000000000..69f09eae0 --- /dev/null +++ b/qutebrowser/utils/webelem.py @@ -0,0 +1,74 @@ +# Copyright 2014 Florian Bruhin (The Compiler) +# +# 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 . + +"""Utilities related to QWebElements. + +Module attributes: + SELECTORS: CSS selectors for different groups of elements. + FILTERS: A dictionary of filter functions for the modes. + The filter for "links" filters javascript:-links and a-tags + without "href". +""" + +import qutebrowser.utils.url as urlutils + +SELECTORS = { + 'all': ('a, textarea, select, input:not([type=hidden]), button, ' + 'frame, iframe, [onclick], [onmousedown], [role=link], ' + '[role=option], [role=button], img'), + 'links': 'a', + 'images': 'img', + 'editable': ('input[type=text], input[type=email], input[type=url], ' + 'input[type=tel], input[type=number], ' + 'input[type=password], input[type=search], textarea'), + 'url': '[src], [href]', +} + +SELECTORS['editable_focused'] = ', '.join( + [sel.strip() + ':focus' for sel in SELECTORS['editable'].split(',')]) + +FILTERS = { + 'links': (lambda e: e.hasAttribute('href') and + urlutils.qurl(e.attribute('href')).scheme() != 'javascript'), +} + + +def is_visible(e, frame=None): + """Check whether the element is currently visible in its frame. + + Args: + e: The QWebElement to check. + frame: The QWebFrame in which the element should be visible in. + If None, the element's frame is used. + + Return: + True if the element is visible, False otherwise. + """ + if e.isNull(): + raise ValueError("Element is a null-element!") + if frame is None: + frame = e.webFrame() + rect = e.geometry() + if (not rect.isValid()) and rect.x() == 0: + # Most likely an invisible link + return False + framegeom = frame.geometry() + framegeom.translate(frame.scrollPosition()) + if not framegeom.contains(rect.topLeft()): + # out of screen + return False + return True diff --git a/qutebrowser/widgets/browsertab.py b/qutebrowser/widgets/browsertab.py index 9ea3c9a18..133732a29 100644 --- a/qutebrowser/widgets/browsertab.py +++ b/qutebrowser/widgets/browsertab.py @@ -29,6 +29,7 @@ import qutebrowser.utils.url as urlutils import qutebrowser.config.config as config import qutebrowser.utils.message as message import qutebrowser.utils.modemanager as modemanager +import qutebrowser.utils.webelem as webelem from qutebrowser.browser.webpage import BrowserPage from qutebrowser.browser.hints import HintManager from qutebrowser.utils.signals import SignalCache @@ -250,8 +251,8 @@ class BrowserTab(QWebView): modemanager.maybe_leave("insert") elif config.get('general', 'auto_insert_mode'): frame = self.page_.currentFrame() - # FIXME better selector (from hintmanager) - elem = frame.findFirstElement('*:focus') + elem = frame.findFirstElement( + webelem.SELECTORS['editable_focused']) logging.debug("focus element: {}".format(not elem.isNull())) if elem.isNull(): modemanager.maybe_leave("insert") From 0e3e5880381a564f49a3c65afabeeeb9e45ba43b Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 24 Apr 2014 16:53:16 +0200 Subject: [PATCH 27/54] Add forward_unbound_keys setting --- TODO | 1 - qutebrowser/commands/keys.py | 7 +++++-- qutebrowser/config/configdata.py | 4 ++++ qutebrowser/utils/keyparser.py | 10 ++++++++-- qutebrowser/utils/modemanager.py | 16 +++++++++++++--- 5 files changed, 30 insertions(+), 8 deletions(-) diff --git a/TODO b/TODO index c2a1df38b..972c80736 100644 --- a/TODO +++ b/TODO @@ -2,7 +2,6 @@ keyparser foo ============= - Handle keybind to get out of insert mode (e.g. esc) -- Add passthrough-keys option to pass through unmapped keys - Add more element-selection-detection code (with options?) based on: -> javascript: http://stackoverflow.com/a/2848120/2085149 -> microFocusChanged and check active element via: diff --git a/qutebrowser/commands/keys.py b/qutebrowser/commands/keys.py index 83727e6bd..512d60f0b 100644 --- a/qutebrowser/commands/keys.py +++ b/qutebrowser/commands/keys.py @@ -85,12 +85,15 @@ class CommandKeyParser(KeyParser): Emit: set_cmd_text: If the keystring should be shown in the statusbar. + + Return: + True if event has been handled, False otherwise. """ txt = e.text().strip() if not self._keystring and any(txt == c for c in STARTCHARS): self.set_cmd_text.emit(txt) - return - super()._handle_single_key(e) + return True + return super()._handle_single_key(e) def execute(self, cmdstr, count=None): """Handle a completed keychain.""" diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index 5d18bd43e..c8c90b6d9 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -176,6 +176,10 @@ DATA = OrderedDict([ SettingValue(types.Bool(), "true"), "Whether to automatically enter insert mode if an editable element " "is focused after page load."), + + ('forward_unbound_keys', + SettingValue(types.Bool(), "false"), + "Whether to forward unbound keys to the website in normal mode."), )), ('tabbar', sect.KeyValue( diff --git a/qutebrowser/utils/keyparser.py b/qutebrowser/utils/keyparser.py index a8e3ae7b7..a5b76cf60 100644 --- a/qutebrowser/utils/keyparser.py +++ b/qutebrowser/utils/keyparser.py @@ -116,12 +116,15 @@ class KeyParser(QObject): Args: e: the KeyPressEvent from Qt. + + Return: + True if event has been handled, False otherwise. """ logging.debug('Got key: {} / text: "{}"'.format(e.key(), e.text())) txt = e.text().strip() if not txt: logging.debug('Ignoring, no text') - return + return False self._stop_delayed_exec() self._keystring += txt @@ -135,7 +138,8 @@ class KeyParser(QObject): count = None if not cmd_input: - return + # Only a count, no command yet, but we handled it + return True (match, binding) = self._match_key(cmd_input) @@ -151,6 +155,8 @@ class KeyParser(QObject): logging.debug('Giving up with "{}", no matches'.format( self._keystring)) self._keystring = '' + return False + return True def _match_key(self, cmd_input): """Try to match a given keystring with any bound keychain. diff --git a/qutebrowser/utils/modemanager.py b/qutebrowser/utils/modemanager.py index 87632c766..75a16486a 100644 --- a/qutebrowser/utils/modemanager.py +++ b/qutebrowser/utils/modemanager.py @@ -25,6 +25,8 @@ import logging from PyQt5.QtCore import pyqtSignal, QObject, QEvent +import qutebrowser.config.config as config + manager = None @@ -168,11 +170,19 @@ class ModeManager(QObject): return False elif typ == QEvent.KeyPress: # KeyPress in a non-passthrough mode - call handler and filter - # event from widgets + # event from widgets (unless unhandled and configured to pass + # unhandled events through) self.key_pressed.emit(evt) if handler is not None: - handler(evt) - return True + handled = handler(evt) + else: + handled = False + if handled: + return True + elif config.get('general', 'forward_unbound_keys'): + return False + else: + return True else: # KeyRelease in a non-passthrough mode - filter event and ignore it # entirely. From 8648d88b510456d50d1c34bf17b1dd0a03bcf085 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 24 Apr 2014 17:43:19 +0200 Subject: [PATCH 28/54] Handle special keys instead of only modifiers --- qutebrowser/browser/hints.py | 6 ++++-- qutebrowser/commands/keys.py | 6 +++--- qutebrowser/utils/keyparser.py | 21 +++++++++------------ 3 files changed, 16 insertions(+), 17 deletions(-) diff --git a/qutebrowser/browser/hints.py b/qutebrowser/browser/hints.py index 4dbdc4862..04c872a93 100644 --- a/qutebrowser/browser/hints.py +++ b/qutebrowser/browser/hints.py @@ -53,8 +53,10 @@ class HintKeyParser(KeyParser): fire_hint = pyqtSignal(str) abort_hinting = pyqtSignal() - def _handle_modifier_key(self, e): - """We don't support modifiers here, but we'll handle escape in here. + def _handle_special_key(self, e): + """Handle the escape key. + + FIXME make this more generic Emit: abort_hinting: Emitted if hinting was aborted. diff --git a/qutebrowser/commands/keys.py b/qutebrowser/commands/keys.py index 512d60f0b..8f8d897d8 100644 --- a/qutebrowser/commands/keys.py +++ b/qutebrowser/commands/keys.py @@ -118,9 +118,9 @@ class CommandKeyParser(KeyParser): if key.startswith('@') and key.endswith('@'): # normalize keystring keystr = self._normalize_keystr(key.strip('@')) - logging.debug('registered mod key: {} -> {}'.format(keystr, - cmd)) - self.modifier_bindings[keystr] = cmd + logging.debug('registered special key: {} -> {}'.format(keystr, + cmd)) + self.special_bindings[keystr] = cmd else: logging.debug('registered key: {} -> {}'.format(key, cmd)) self.bindings[key] = cmd diff --git a/qutebrowser/utils/keyparser.py b/qutebrowser/utils/keyparser.py index a5b76cf60..bd16cf75e 100644 --- a/qutebrowser/utils/keyparser.py +++ b/qutebrowser/utils/keyparser.py @@ -46,7 +46,7 @@ class KeyParser(QObject): _keystring: The currently entered key sequence _timer: QTimer for delayed execution. bindings: Bound keybindings - modifier_bindings: Bound modifier bindings. + special_bindings: Bound special bindings (@Foo@). Signals: keystring_updated: Emitted when the keystring is updated. @@ -62,16 +62,16 @@ class KeyParser(QObject): supports_count = False - def __init__(self, parent=None, bindings=None, modifier_bindings=None): + def __init__(self, parent=None, bindings=None, special_bindings=None): super().__init__(parent) self._timer = None self._keystring = '' self.bindings = {} if bindings is None else bindings - self.modifier_bindings = ({} if modifier_bindings is None - else modifier_bindings) + self.special_bindings = ({} if special_bindings is None + else special_bindings) - def _handle_modifier_key(self, e): - """Handle a new keypress with modifiers. + 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. @@ -92,18 +92,15 @@ class KeyParser(QObject): return False mod = e.modifiers() modstr = '' - if not mod & (Qt.ControlModifier | Qt.AltModifier | Qt.MetaModifier): - # won't be a shortcut with modifiers - return False for (mask, s) in modmask2str.items(): if mod & mask: modstr += s + '+' keystr = QKeySequence(e.key()).toString() try: - cmdstr = self.modifier_bindings[modstr + keystr] + cmdstr = self.special_bindings[modstr + keystr] except KeyError: logging.debug('No binding found for {}.'.format(modstr + keystr)) - return True + return False self.execute(cmdstr) return True @@ -281,7 +278,7 @@ class KeyParser(QObject): Emit: keystring_updated: If a new keystring should be set. """ - handled = self._handle_modifier_key(e) + handled = self._handle_special_key(e) if not handled: self._handle_single_key(e) self.keystring_updated.emit(self._keystring) From 718295eb9f5c9340922c314a1c8d7c62bf123300 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 24 Apr 2014 17:48:38 +0200 Subject: [PATCH 29/54] Use <> instead of @@ for special keys --- qutebrowser/commands/keys.py | 4 ++-- qutebrowser/config/configdata.py | 24 ++++++++++++------------ qutebrowser/utils/keyparser.py | 4 ++-- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/qutebrowser/commands/keys.py b/qutebrowser/commands/keys.py index 8f8d897d8..6474a3f40 100644 --- a/qutebrowser/commands/keys.py +++ b/qutebrowser/commands/keys.py @@ -115,9 +115,9 @@ class CommandKeyParser(KeyParser): if not sect.items(): logging.warn("No keybindings defined!") for (key, cmd) in sect.items(): - if key.startswith('@') and key.endswith('@'): + if key.startswith('<') and key.endswith('>'): # normalize keystring - keystr = self._normalize_keystr(key.strip('@')) + keystr = self._normalize_keystr(key[1:-1]) logging.debug('registered special key: {} -> {}'.format(keystr, cmd)) self.special_bindings[keystr] = cmd diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index c8c90b6d9..7f34399fa 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -68,14 +68,14 @@ SECTION_DESC = { 'keybind': ( "Bindings from a key(chain) to a command.\n" "For special keys (can't be part of a keychain), enclose them in " - "@-signs. For modifiers, you can use either - or + as delimiters, and " + "<...>. For modifiers, you can use either - or + as delimiters, and " "these names:\n" " Control: Control, Ctrl\n" " Meta: Meta, Windows, Mod4\n" " Alt: Alt, Mod1\n" " Shift: Shift\n" - "For simple keys (no @ signs), a capital letter means the key is " - "pressed with Shift. For modifier keys (with @ signs), you need " + "For simple keys (no <>-signs), a capital letter means the key is " + "pressed with Shift. For special keys (with <>-signs), you need " "to explicitely add \"Shift-\" to match a key pressed with shift. " "You can bind multiple commands by separating them with \";;\"."), 'aliases': ( @@ -409,15 +409,15 @@ DATA = OrderedDict([ ('PP', 'tabpaste sel'), ('-', 'zoomout'), ('+', 'zoomin'), - ('@Ctrl-Q@', 'quit'), - ('@Ctrl-Shift-T@', 'undo'), - ('@Ctrl-W@', 'tabclose'), - ('@Ctrl-T@', 'tabopen about:blank'), - ('@Ctrl-F@', 'scroll_page 0 1'), - ('@Ctrl-B@', 'scroll_page 0 -1'), - ('@Ctrl-D@', 'scroll_page 0 0.5'), - ('@Ctrl-U@', 'scroll_page 0 -0.5'), - ('@Backspace@', 'back'), + ('', 'quit'), + ('', 'undo'), + ('', 'tabclose'), + ('', 'tabopen about:blank'), + ('', 'scroll_page 0 1'), + ('', 'scroll_page 0 -1'), + ('', 'scroll_page 0 0.5'), + ('', 'scroll_page 0 -0.5'), + ('', 'back'), )), ('aliases', sect.ValueList( diff --git a/qutebrowser/utils/keyparser.py b/qutebrowser/utils/keyparser.py index bd16cf75e..177e17eb2 100644 --- a/qutebrowser/utils/keyparser.py +++ b/qutebrowser/utils/keyparser.py @@ -46,7 +46,7 @@ class KeyParser(QObject): _keystring: The currently entered key sequence _timer: QTimer for delayed execution. bindings: Bound keybindings - special_bindings: Bound special bindings (@Foo@). + special_bindings: Bound special bindings (). Signals: keystring_updated: Emitted when the keystring is updated. @@ -71,7 +71,7 @@ class KeyParser(QObject): else special_bindings) def _handle_special_key(self, e): - """Handle a new keypress with special keys (@Foo@). + """Handle a new keypress with special keys (). Return True if the keypress has been handled, and False if not. From 745e0374fff9d3b1895a667981e24dd1c094e209 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 24 Apr 2014 17:50:12 +0200 Subject: [PATCH 30/54] Update TODO --- TODO | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/TODO b/TODO index 972c80736..ab5002bcb 100644 --- a/TODO +++ b/TODO @@ -1,7 +1,10 @@ keyparser foo ============= -- Handle keybind to get out of insert mode (e.g. esc) +- Create a SimpleKeyParser and inherit KeyParser from that. + - Handle keybind to get out of insert mode (e.g. esc) +- Enter normal mode in loadingStarted already +- Read unbound-keys setting only once - Add more element-selection-detection code (with options?) based on: -> javascript: http://stackoverflow.com/a/2848120/2085149 -> microFocusChanged and check active element via: From 6f7391d7d127e4081ed88751270b0216805ba508 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 24 Apr 2014 17:59:35 +0200 Subject: [PATCH 31/54] Leave insert mode in loadStarted --- TODO | 1 - qutebrowser/widgets/browsertab.py | 24 +++++++++++------------- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/TODO b/TODO index ab5002bcb..da196003d 100644 --- a/TODO +++ b/TODO @@ -3,7 +3,6 @@ keyparser foo - Create a SimpleKeyParser and inherit KeyParser from that. - Handle keybind to get out of insert mode (e.g. esc) -- Enter normal mode in loadingStarted already - Read unbound-keys setting only once - Add more element-selection-detection code (with options?) based on: -> javascript: http://stackoverflow.com/a/2848120/2085149 diff --git a/qutebrowser/widgets/browsertab.py b/qutebrowser/widgets/browsertab.py index 133732a29..4153e0cdf 100644 --- a/qutebrowser/widgets/browsertab.py +++ b/qutebrowser/widgets/browsertab.py @@ -86,6 +86,7 @@ class BrowserTab(QWebView): self.page_.setLinkDelegationPolicy(QWebPage.DelegateAllLinks) self.page_.linkHovered.connect(self.linkHovered) self.linkClicked.connect(self.on_link_clicked) + self.loadStarted.connect(lambda: modemanager.maybe_leave("insert")) self.loadFinished.connect(self.on_load_finished) # FIXME find some way to hide scrollbars without setScrollBarPolicy @@ -245,21 +246,18 @@ class BrowserTab(QWebView): QApplication.postEvent(self, evt) @pyqtSlot(bool) - def on_load_finished(self, ok): - """Handle insert mode after loading finished.""" - if not ok: + def on_load_finished(self, _ok): + """Handle auto_insert_mode after loading finished.""" + if not config.get('general', 'auto_insert_mode'): + return + frame = self.page_.currentFrame() + elem = frame.findFirstElement( + webelem.SELECTORS['editable_focused']) + logging.debug("focus element: {}".format(not elem.isNull())) + if elem.isNull(): modemanager.maybe_leave("insert") - elif config.get('general', 'auto_insert_mode'): - frame = self.page_.currentFrame() - elem = frame.findFirstElement( - webelem.SELECTORS['editable_focused']) - logging.debug("focus element: {}".format(not elem.isNull())) - if elem.isNull(): - modemanager.maybe_leave("insert") - else: - modemanager.enter("insert") else: - modemanager.maybe_leave("insert") + modemanager.enter("insert") @pyqtSlot(str) def set_force_open_target(self, target): From 6311deb6b0620972d58adc8b3b164a8c90466176 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 24 Apr 2014 18:31:01 +0200 Subject: [PATCH 32/54] Read unbound-keys setting only once --- TODO | 1 - qutebrowser/app.py | 2 +- qutebrowser/utils/modemanager.py | 16 +++++++++++++--- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/TODO b/TODO index da196003d..b9c514f5c 100644 --- a/TODO +++ b/TODO @@ -3,7 +3,6 @@ keyparser foo - Create a SimpleKeyParser and inherit KeyParser from that. - Handle keybind to get out of insert mode (e.g. esc) -- Read unbound-keys setting only once - Add more element-selection-detection code (with options?) based on: -> javascript: http://stackoverflow.com/a/2848120/2085149 -> microFocusChanged and check active element via: diff --git a/qutebrowser/app.py b/qutebrowser/app.py index 333a2178a..f78de1f4f 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -284,7 +284,7 @@ class QuteBrowser(QApplication): # config self.config.style_changed.connect(style.invalidate_caches) for obj in [tabs, completion, self.mainwindow, config.cmd_history, - websettings, kp["normal"]]: + websettings, kp["normal"], modemanager.manager]: self.config.changed.connect(obj.on_config_changed) # statusbar diff --git a/qutebrowser/utils/modemanager.py b/qutebrowser/utils/modemanager.py index 75a16486a..6174f7b8a 100644 --- a/qutebrowser/utils/modemanager.py +++ b/qutebrowser/utils/modemanager.py @@ -23,7 +23,7 @@ Module attributes: import logging -from PyQt5.QtCore import pyqtSignal, QObject, QEvent +from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QEvent import qutebrowser.config.config as config @@ -72,13 +72,14 @@ class ModeManager(QObject): _handlers: A dictionary of modes and their handlers. _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. Signals: entered: Emitted when a mode is entered. arg: Name of the entered mode. left: Emitted when a mode is left. arg: Name of the left mode. - key_pressed; A key was pressed. + key_pressed: A key was pressed. """ entered = pyqtSignal(str) @@ -90,6 +91,8 @@ class ModeManager(QObject): self._handlers = {} self.passthrough = [] self._mode_stack = [] + self._forward_unbound_keys = config.get('general', + 'forward_unbound_keys') @property def mode(self): @@ -147,6 +150,13 @@ class ModeManager(QObject): logging.debug("New mode stack: {}".format(self._mode_stack)) self.left.emit(mode) + @pyqtSlot(str, str) + def on_config_changed(self, section, option): + """Update local setting when config changed.""" + if (section, option) == ('general', 'forward_unbound_keys'): + self._forward_unbound_keys = config.get('general', + 'forward_unbound_keys') + def eventFilter(self, _obj, evt): """Filter all events based on the currently set mode. @@ -179,7 +189,7 @@ class ModeManager(QObject): handled = False if handled: return True - elif config.get('general', 'forward_unbound_keys'): + elif self._forward_unbound_keys: return False else: return True From ecdd8876645bfbb06bdaf0819c4af4199c983cee Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 24 Apr 2014 19:21:38 +0200 Subject: [PATCH 33/54] Split KeyParser into KeyParser (non-chain) and KeyChainParser --- TODO | 3 +- qutebrowser/browser/hints.py | 6 +- qutebrowser/commands/keys.py | 37 +----- qutebrowser/utils/keyparser.py | 211 +++++++++++++++++++++++---------- 4 files changed, 159 insertions(+), 98 deletions(-) diff --git a/TODO b/TODO index b9c514f5c..972c80736 100644 --- a/TODO +++ b/TODO @@ -1,8 +1,7 @@ keyparser foo ============= -- Create a SimpleKeyParser and inherit KeyParser from that. - - Handle keybind to get out of insert mode (e.g. esc) +- Handle keybind to get out of insert mode (e.g. esc) - Add more element-selection-detection code (with options?) based on: -> javascript: http://stackoverflow.com/a/2848120/2085149 -> microFocusChanged and check active element via: diff --git a/qutebrowser/browser/hints.py b/qutebrowser/browser/hints.py index 04c872a93..ea9044dd9 100644 --- a/qutebrowser/browser/hints.py +++ b/qutebrowser/browser/hints.py @@ -30,15 +30,15 @@ import qutebrowser.utils.message as message import qutebrowser.utils.url as urlutils import qutebrowser.utils.modemanager as modemanager import qutebrowser.utils.webelem as webelem -from qutebrowser.utils.keyparser import KeyParser +from qutebrowser.utils.keyparser import KeyChainParser ElemTuple = namedtuple('ElemTuple', 'elem, label') -class HintKeyParser(KeyParser): +class HintKeyParser(KeyChainParser): - """KeyParser for hints. + """KeyChainParser for hints. Class attributes: supports_count: If the keyparser should support counts. diff --git a/qutebrowser/commands/keys.py b/qutebrowser/commands/keys.py index 6474a3f40..cf11d21ed 100644 --- a/qutebrowser/commands/keys.py +++ b/qutebrowser/commands/keys.py @@ -23,19 +23,18 @@ Module attributes: import logging -from PyQt5.QtCore import pyqtSignal, pyqtSlot +from PyQt5.QtCore import pyqtSignal -import qutebrowser.config.config as config -from qutebrowser.utils.keyparser import KeyParser +from qutebrowser.utils.keyparser import KeyChainParser from qutebrowser.commands.parsers import (CommandParser, ArgumentCountError, NoSuchCommandError) STARTCHARS = ":/?" -class CommandKeyParser(KeyParser): +class CommandKeyParser(KeyChainParser): - """Keyparser for command bindings. + """KeyChainParser for command bindings. Class attributes: supports_count: If the keyparser should support counts. @@ -54,7 +53,7 @@ class CommandKeyParser(KeyParser): def __init__(self, parent=None): super().__init__(parent) self.commandparser = CommandParser() - self.read_config() + self.read_config('keybind') def _run_or_fill(self, cmdstr, count=None, ignore_exc=True): """Run the command in cmdstr or fill the statusbar if args missing. @@ -98,29 +97,3 @@ class CommandKeyParser(KeyParser): def execute(self, cmdstr, count=None): """Handle a completed keychain.""" self._run_or_fill(cmdstr, count, ignore_exc=False) - - @pyqtSlot(str, str) - def on_config_changed(self, section, _option): - """Re-read the config if a keybinding was changed.""" - if section == 'keybind': - self.read_config() - - def read_config(self): - """Read the configuration. - - Config format: key = command, e.g.: - gg = scrollstart - """ - sect = config.instance['keybind'] - if not sect.items(): - logging.warn("No keybindings defined!") - for (key, cmd) in sect.items(): - if key.startswith('<') and key.endswith('>'): - # normalize keystring - keystr = self._normalize_keystr(key[1:-1]) - logging.debug('registered special key: {} -> {}'.format(keystr, - cmd)) - self.special_bindings[keystr] = cmd - else: - logging.debug('registered key: {} -> {}'.format(key, cmd)) - self.bindings[key] = cmd diff --git a/qutebrowser/utils/keyparser.py b/qutebrowser/utils/keyparser.py index 177e17eb2..74a292b4b 100644 --- a/qutebrowser/utils/keyparser.py +++ b/qutebrowser/utils/keyparser.py @@ -21,7 +21,7 @@ import re import logging from functools import partial -from PyQt5.QtCore import pyqtSignal, Qt, QObject, QTimer +from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QObject, QTimer from PyQt5.QtGui import QKeySequence import qutebrowser.config.config as config @@ -29,47 +29,48 @@ import qutebrowser.config.config as config class KeyParser(QObject): - """Parser for vim-like key sequences. + """Parser for non-chained Qt keypresses ("special bindings"). + + We call these special because chained keypresses are the "normal" ones in + qutebrowser, however there are some cases where we can _only_ use special + keys (like in insert mode). Not intended to be instantiated directly. Subclasses have to override execute() to do whatever they want to. - Class Attributes: - MATCH_PARTIAL: Constant for a partial match (no keychain matched yet, - but it's still possible in the future. - MATCH_DEFINITIVE: Constant for a full match (keychain matches exactly). - MATCH_AMBIGUOUS: There are both a partial and a definitive match. - MATCH_NONE: Constant for no match (no more matches possible). - supports_count: If the keyparser should support counts. - Attributes: - _keystring: The currently entered key sequence - _timer: QTimer for delayed execution. - bindings: Bound keybindings special_bindings: Bound special bindings (). - - Signals: - keystring_updated: Emitted when the keystring is updated. - arg: New keystring. + _confsectname: The name of the configsection. """ - keystring_updated = pyqtSignal(str) - - MATCH_PARTIAL = 0 - MATCH_DEFINITIVE = 1 - MATCH_AMBIGUOUS = 2 - MATCH_NONE = 3 - - supports_count = False - - def __init__(self, parent=None, bindings=None, special_bindings=None): + def __init__(self, parent=None, special_bindings=None): super().__init__(parent) - self._timer = None - self._keystring = '' - self.bindings = {} if bindings is None else bindings + self._confsectname = None self.special_bindings = ({} if special_bindings is None else special_bindings) + def _normalize_keystr(self, keystr): + """Normalize a keystring like Ctrl-Q to a keystring like Ctrl+Q. + + Args: + keystr: The key combination as a string. + + Return: + The normalized keystring. + """ + replacements = [ + ('Control', 'Ctrl'), + ('Windows', 'Meta'), + ('Mod1', 'Alt'), + ('Mod4', 'Meta'), + ] + for (orig, repl) in replacements: + keystr = keystr.replace(orig, repl) + for mod in ['Ctrl', 'Meta', 'Alt', 'Shift']: + keystr = keystr.replace(mod + '-', mod + '+') + keystr = QKeySequence(keystr).toString() + return keystr + def _handle_special_key(self, e): """Handle a new keypress with special keys (). @@ -104,6 +105,99 @@ class KeyParser(QObject): self.execute(cmdstr) return True + def handle(self, e): + """Handle a new keypress and call the respective handlers. + + Args: + e: the KeyPressEvent from Qt + """ + return self._handle_special_key(e) + + def execute(self, cmdstr, count=None): + """Execute an action when a binding is triggered. + + Needs to be overriden in superclasses.""" + raise NotImplementedError + + def read_config(self, sectname=None): + """Read the configuration. + + Config format: key = command, e.g.: + = quit + + Args: + sectname: Name of the section to read. + """ + if sectname is None: + if self._confsectname is None: + raise ValueError("read_config called with no section, but " + "None defined so far!") + sectname = self._confsectname + else: + self._confsectname = sectname + sect = config.instance[sectname] + if not sect.items(): + logging.warn("No keybindings defined!") + for (key, cmd) in sect.items(): + if key.startswith('<') and key.endswith('>'): + keystr = self._normalize_keystr(key[1:-1]) + logging.debug('registered special key: {} -> {}'.format(keystr, + cmd)) + self.special_bindings[keystr] = cmd + + @pyqtSlot(str, str) + def on_config_changed(self, section, _option): + """Re-read the config if a keybinding was changed.""" + if self._confsectname is None: + raise AttributeError("on_config_changed called but no section " + "defined!") + if section == self._confsectname: + self.read_config() + + +class KeyChainParser(KeyParser): + + """Parser for vim-like key sequences. + + Not intended to be instantiated directly. Subclasses have to override + execute() to do whatever they want to. + + Class Attributes: + MATCH_PARTIAL: Constant for a partial match (no keychain matched yet, + but it's still possible in the future. + MATCH_DEFINITIVE: Constant for a full match (keychain matches exactly). + MATCH_AMBIGUOUS: There are both a partial and a definitive match. + MATCH_NONE: Constant for no match (no more matches possible). + supports_count: If the keyparser should support counts. + + Attributes: + _keystring: The currently entered key sequence + _timer: QTimer for delayed execution. + bindings: Bound keybindings + + Signals: + keystring_updated: Emitted when the keystring is updated. + arg: New keystring. + """ + + # This is an abstract superclass of an abstract class. + # pylint: disable=abstract-method + + keystring_updated = pyqtSignal(str) + + MATCH_PARTIAL = 0 + MATCH_DEFINITIVE = 1 + MATCH_AMBIGUOUS = 2 + MATCH_NONE = 3 + + supports_count = False + + def __init__(self, parent=None, bindings=None, special_bindings=None): + super().__init__(parent, special_bindings) + self._timer = None + self._keystring = '' + self.bindings = {} if bindings is None else bindings + def _handle_single_key(self, e): """Handle a new keypress with a single key (no modifiers). @@ -228,28 +322,6 @@ class KeyParser(QObject): count)) self._timer.start() - def _normalize_keystr(self, keystr): - """Normalize a keystring like Ctrl-Q to a keystring like Ctrl+Q. - - Args: - keystr: The key combination as a string. - - Return: - The normalized keystring. - """ - replacements = [ - ('Control', 'Ctrl'), - ('Windows', 'Meta'), - ('Mod1', 'Alt'), - ('Mod4', 'Meta'), - ] - for (orig, repl) in replacements: - keystr = keystr.replace(orig, repl) - for mod in ['Ctrl', 'Meta', 'Alt', 'Shift']: - keystr = keystr.replace(mod + '-', mod + '+') - keystr = QKeySequence(keystr).toString() - return keystr - def delayed_exec(self, command, count): """Execute a delayed command. @@ -265,12 +337,8 @@ class KeyParser(QObject): self.keystring_updated.emit(self._keystring) self.execute(command, count) - def execute(self, cmdstr, count=None): - """Execute an action when a binding is triggered.""" - raise NotImplementedError - def handle(self, e): - """Handle a new keypress and call the respective handlers. + """Override KeyParser.handle() to also handle keychains. Args: e: the KeyPressEvent from Qt @@ -278,7 +346,28 @@ class KeyParser(QObject): Emit: keystring_updated: If a new keystring should be set. """ - handled = self._handle_special_key(e) - if not handled: - self._handle_single_key(e) - self.keystring_updated.emit(self._keystring) + handled = super().handle(e) + if handled: + return True + handled = self._handle_single_key(e) + self.keystring_updated.emit(self._keystring) + return handled + + def read_config(self, sectname=None): + """Extend KeyParser.read_config to also read keychains. + + Config format: key = command, e.g.: + = quit + + Args: + sectname: Name of the section to read. + """ + super().read_config(sectname) + sect = config.instance[sectname] + for (key, cmd) in sect.items(): + if key.startswith('<') and key.endswith('>'): + # Already registered by superclass + pass + else: + logging.debug('registered key: {} -> {}'.format(key, cmd)) + self.bindings[key] = cmd From c674d96cfeac1ac50c0e240a1100a15eee3f5b64 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 24 Apr 2014 21:03:45 +0200 Subject: [PATCH 34/54] Reorganize input modules into keyinput subpackage --- qutebrowser/app.py | 27 ++++--- qutebrowser/browser/hints.py | 56 ++------------- .../keys.py => keyinput/commandmode.py} | 2 +- qutebrowser/keyinput/hintmode.py | 70 +++++++++++++++++++ qutebrowser/{utils => keyinput}/keyparser.py | 0 .../modemanager.py => keyinput/modes.py} | 0 qutebrowser/widgets/browsertab.py | 12 ++-- qutebrowser/widgets/statusbar.py | 16 ++--- 8 files changed, 102 insertions(+), 81 deletions(-) rename qutebrowser/{commands/keys.py => keyinput/commandmode.py} (98%) create mode 100644 qutebrowser/keyinput/hintmode.py rename qutebrowser/{utils => keyinput}/keyparser.py (100%) rename qutebrowser/{utils/modemanager.py => keyinput/modes.py} (100%) diff --git a/qutebrowser/app.py b/qutebrowser/app.py index f78de1f4f..50324f030 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -52,11 +52,11 @@ import qutebrowser.commands.utils as cmdutils import qutebrowser.config.style as style import qutebrowser.config.config as config import qutebrowser.network.qutescheme as qutescheme +import qutebrowser.keyinput.modes as modes import qutebrowser.utils.message as message -import qutebrowser.utils.modemanager as modemanager from qutebrowser.widgets.mainwindow import MainWindow from qutebrowser.widgets.crash import CrashDialog -from qutebrowser.commands.keys import CommandKeyParser +from qutebrowser.keyinput.commandmode import CommandKeyParser from qutebrowser.commands.parsers import CommandParser, SearchParser from qutebrowser.browser.hints import HintKeyParser from qutebrowser.utils.appdirs import AppDirs @@ -129,17 +129,16 @@ class QuteBrowser(QApplication): } self._init_cmds() self.mainwindow = MainWindow() - modemanager.init(self) - modemanager.manager.register("normal", - self._keyparsers["normal"].handle) - modemanager.manager.register("hint", self._keyparsers["hint"].handle) - modemanager.manager.register("insert", None, passthrough=True) - modemanager.manager.register("command", None, passthrough=True) - self.installEventFilter(modemanager.manager) + modes.init(self) + modes.manager.register("normal", self._keyparsers["normal"].handle) + modes.manager.register("hint", self._keyparsers["hint"].handle) + modes.manager.register("insert", None, passthrough=True) + modes.manager.register("command", None, passthrough=True) + self.installEventFilter(modes.manager) self.setQuitOnLastWindowClosed(False) self._connect_signals() - modemanager.enter("normal") + modes.enter("normal") self.mainwindow.show() self._python_hacks() @@ -255,10 +254,10 @@ class QuteBrowser(QApplication): tabs.currentChanged.connect(self.mainwindow.update_inspector) # status bar - modemanager.manager.entered.connect(status.on_mode_entered) - modemanager.manager.left.connect(status.on_mode_left) + modes.manager.entered.connect(status.on_mode_entered) + modes.manager.left.connect(status.on_mode_left) # FIXME what to do here? - modemanager.manager.key_pressed.connect(status.on_key_pressed) + modes.manager.key_pressed.connect(status.on_key_pressed) for obj in [kp["normal"], tabs]: obj.set_cmd_text.connect(cmd.set_cmd_text) @@ -284,7 +283,7 @@ class QuteBrowser(QApplication): # config self.config.style_changed.connect(style.invalidate_caches) for obj in [tabs, completion, self.mainwindow, config.cmd_history, - websettings, kp["normal"], modemanager.manager]: + websettings, kp["normal"], modes.manager]: self.config.changed.connect(obj.on_config_changed) # statusbar diff --git a/qutebrowser/browser/hints.py b/qutebrowser/browser/hints.py index ea9044dd9..736a5b1d9 100644 --- a/qutebrowser/browser/hints.py +++ b/qutebrowser/browser/hints.py @@ -26,64 +26,16 @@ from PyQt5.QtGui import QMouseEvent, QClipboard from PyQt5.QtWidgets import QApplication import qutebrowser.config.config as config +import qutebrowser.keyinput.modes as modes import qutebrowser.utils.message as message import qutebrowser.utils.url as urlutils -import qutebrowser.utils.modemanager as modemanager import qutebrowser.utils.webelem as webelem -from qutebrowser.utils.keyparser import KeyChainParser +from qutebrowser.keyinput.hintmode import HintKeyParser ElemTuple = namedtuple('ElemTuple', 'elem, label') -class HintKeyParser(KeyChainParser): - - """KeyChainParser for hints. - - Class attributes: - supports_count: If the keyparser should support counts. - - Signals: - fire_hint: When a hint keybinding was completed. - Arg: the keystring/hint string pressed. - abort_hinting: Esc pressed, so abort hinting. - """ - - supports_count = False - fire_hint = pyqtSignal(str) - abort_hinting = pyqtSignal() - - def _handle_special_key(self, e): - """Handle the escape key. - - FIXME make this more generic - - Emit: - abort_hinting: Emitted if hinting was aborted. - """ - if e.key() == Qt.Key_Escape: - self._keystring = '' - self.abort_hinting.emit() - return True - return False - - def execute(self, cmdstr, count=None): - """Handle a completed keychain. - - Emit: - fire_hint: Always emitted. - """ - self.fire_hint.emit(cmdstr) - - def on_hint_strings_updated(self, strings): - """Handler for HintManager's hint_strings_updated. - - Args: - strings: A list of hint strings. - """ - self.bindings = {s: s for s in strings} - - class HintManager(QObject): """Manage drawing hints over links or other elements. @@ -364,7 +316,7 @@ class HintManager(QObject): self._elems[string] = ElemTuple(e, label) frame.contentsSizeChanged.connect(self.on_contents_size_changed) self.hint_strings_updated.emit(strings) - modemanager.enter("hint") + modes.enter("hint") def stop(self): """Stop hinting.""" @@ -375,7 +327,7 @@ class HintManager(QObject): self._elems = {} self._target = None self._frame = None - modemanager.leave("hint") + modes.leave("hint") message.clear() def handle_partial_key(self, keystr): diff --git a/qutebrowser/commands/keys.py b/qutebrowser/keyinput/commandmode.py similarity index 98% rename from qutebrowser/commands/keys.py rename to qutebrowser/keyinput/commandmode.py index cf11d21ed..bd4a040b4 100644 --- a/qutebrowser/commands/keys.py +++ b/qutebrowser/keyinput/commandmode.py @@ -25,7 +25,7 @@ import logging from PyQt5.QtCore import pyqtSignal -from qutebrowser.utils.keyparser import KeyChainParser +from qutebrowser.keyinput.keyparser import KeyChainParser from qutebrowser.commands.parsers import (CommandParser, ArgumentCountError, NoSuchCommandError) diff --git a/qutebrowser/keyinput/hintmode.py b/qutebrowser/keyinput/hintmode.py new file mode 100644 index 000000000..d689eeabf --- /dev/null +++ b/qutebrowser/keyinput/hintmode.py @@ -0,0 +1,70 @@ +# Copyright 2014 Florian Bruhin (The Compiler) +# +# 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 . + +"""KeyChainParser for "hint" mode.""" + +from PyQt5.QtCore import pyqtSignal, Qt + +from qutebrowser.keyinput.keyparser import KeyChainParser + + +class HintKeyParser(KeyChainParser): + + """KeyChainParser for hints. + + Class attributes: + supports_count: If the keyparser should support counts. + + Signals: + fire_hint: When a hint keybinding was completed. + Arg: the keystring/hint string pressed. + abort_hinting: Esc pressed, so abort hinting. + """ + + supports_count = False + fire_hint = pyqtSignal(str) + abort_hinting = pyqtSignal() + + def _handle_special_key(self, e): + """Handle the escape key. + + FIXME make this more generic + + Emit: + abort_hinting: Emitted if hinting was aborted. + """ + if e.key() == Qt.Key_Escape: + self._keystring = '' + self.abort_hinting.emit() + return True + return False + + def execute(self, cmdstr, count=None): + """Handle a completed keychain. + + Emit: + fire_hint: Always emitted. + """ + self.fire_hint.emit(cmdstr) + + def on_hint_strings_updated(self, strings): + """Handler for HintManager's hint_strings_updated. + + Args: + strings: A list of hint strings. + """ + self.bindings = {s: s for s in strings} diff --git a/qutebrowser/utils/keyparser.py b/qutebrowser/keyinput/keyparser.py similarity index 100% rename from qutebrowser/utils/keyparser.py rename to qutebrowser/keyinput/keyparser.py diff --git a/qutebrowser/utils/modemanager.py b/qutebrowser/keyinput/modes.py similarity index 100% rename from qutebrowser/utils/modemanager.py rename to qutebrowser/keyinput/modes.py diff --git a/qutebrowser/widgets/browsertab.py b/qutebrowser/widgets/browsertab.py index 4153e0cdf..b3993fdfe 100644 --- a/qutebrowser/widgets/browsertab.py +++ b/qutebrowser/widgets/browsertab.py @@ -27,8 +27,8 @@ from PyQt5.QtWebKitWidgets import QWebView, QWebPage import qutebrowser.utils.url as urlutils import qutebrowser.config.config as config +import qutebrowser.keyinput.modes as modes import qutebrowser.utils.message as message -import qutebrowser.utils.modemanager as modemanager import qutebrowser.utils.webelem as webelem from qutebrowser.browser.webpage import BrowserPage from qutebrowser.browser.hints import HintManager @@ -86,7 +86,7 @@ class BrowserTab(QWebView): self.page_.setLinkDelegationPolicy(QWebPage.DelegateAllLinks) self.page_.linkHovered.connect(self.linkHovered) self.linkClicked.connect(self.on_link_clicked) - self.loadStarted.connect(lambda: modemanager.maybe_leave("insert")) + self.loadStarted.connect(lambda: modes.maybe_leave("insert")) self.loadFinished.connect(self.on_load_finished) # FIXME find some way to hide scrollbars without setScrollBarPolicy @@ -255,9 +255,9 @@ class BrowserTab(QWebView): webelem.SELECTORS['editable_focused']) logging.debug("focus element: {}".format(not elem.isNull())) if elem.isNull(): - modemanager.maybe_leave("insert") + modes.maybe_leave("insert") else: - modemanager.enter("insert") + modes.enter("insert") @pyqtSlot(str) def set_force_open_target(self, target): @@ -319,11 +319,11 @@ class BrowserTab(QWebView): hitresult = frame.hitTestContent(pos) if self._is_editable(hitresult): logging.debug("Clicked editable element!") - modemanager.enter("insert") + modes.enter("insert") else: logging.debug("Clicked non-editable element!") try: - modemanager.leave("insert") + modes.leave("insert") except ValueError: pass diff --git a/qutebrowser/widgets/statusbar.py b/qutebrowser/widgets/statusbar.py index 959aa0d63..03ac8d7d3 100644 --- a/qutebrowser/widgets/statusbar.py +++ b/qutebrowser/widgets/statusbar.py @@ -23,8 +23,8 @@ from PyQt5.QtWidgets import (QWidget, QLineEdit, QProgressBar, QLabel, QShortcut) from PyQt5.QtGui import QPainter, QKeySequence, QValidator -import qutebrowser.commands.keys as keys -import qutebrowser.utils.modemanager as modemanager +import qutebrowser.keyinput.modes as modes +from qutebrowser.keyinput.commandmode import STARTCHARS from qutebrowser.config.style import set_register_stylesheet, get_stylesheet from qutebrowser.utils.url import urlstring from qutebrowser.commands.parsers import split_cmdline @@ -173,13 +173,13 @@ class StatusBar(QWidget): @pyqtSlot(str) def on_mode_entered(self, mode): """Mark certain modes in the commandline.""" - if mode in modemanager.manager.passthrough: + if mode in modes.manager.passthrough: self.txt.normaltext = "-- {} MODE --".format(mode.upper()) @pyqtSlot(str) def on_mode_left(self, mode): """Clear marked mode.""" - if mode in modemanager.manager.passthrough: + if mode in modes.manager.passthrough: self.txt.normaltext = "" def resizeEvent(self, e): @@ -344,7 +344,7 @@ class _Command(QLineEdit): """ # FIXME we should consider the cursor position. text = self.text() - if text[0] in keys.STARTCHARS: + if text[0] in STARTCHARS: prefix = text[0] text = text[1:] else: @@ -357,7 +357,7 @@ class _Command(QLineEdit): def focusInEvent(self, e): """Extend focusInEvent to enter command mode.""" - modemanager.enter("command") + modes.enter("command") super().focusInEvent(e) def focusOutEvent(self, e): @@ -375,7 +375,7 @@ class _Command(QLineEdit): clear_completion_selection: Always emitted. hide_completion: Always emitted so the completion is hidden. """ - modemanager.leave("command") + modes.leave("command") if e.reason() in [Qt.MouseFocusReason, Qt.TabFocusReason, Qt.BacktabFocusReason, Qt.OtherFocusReason]: self.setText('') @@ -400,7 +400,7 @@ class _CommandValidator(QValidator): Return: A tuple (status, string, pos) as a QValidator should. """ - if any(string.startswith(c) for c in keys.STARTCHARS): + if any(string.startswith(c) for c in STARTCHARS): return (QValidator.Acceptable, string, pos) else: return (QValidator.Invalid, string, pos) From d2dc0b7ac597f8ce37ce2ffb03a050f22af3fc43 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 24 Apr 2014 21:12:55 +0200 Subject: [PATCH 35/54] Add KeyParser for normal mode --- qutebrowser/app.py | 15 +++++++----- qutebrowser/config/configdata.py | 13 +++++++++++ qutebrowser/config/conftypes.py | 16 +++++++++++-- qutebrowser/keyinput/insertmode.py | 37 ++++++++++++++++++++++++++++++ 4 files changed, 73 insertions(+), 8 deletions(-) create mode 100644 qutebrowser/keyinput/insertmode.py diff --git a/qutebrowser/app.py b/qutebrowser/app.py index 50324f030..5db5fd2bc 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -57,6 +57,7 @@ import qutebrowser.utils.message as message from qutebrowser.widgets.mainwindow import MainWindow from qutebrowser.widgets.crash import CrashDialog from qutebrowser.keyinput.commandmode import CommandKeyParser +from qutebrowser.keyinput.insertmode import InsertKeyParser from qutebrowser.commands.parsers import CommandParser, SearchParser from qutebrowser.browser.hints import HintKeyParser from qutebrowser.utils.appdirs import AppDirs @@ -124,16 +125,18 @@ class QuteBrowser(QApplication): self.commandparser = CommandParser() self.searchparser = SearchParser() self._keyparsers = { - "normal": CommandKeyParser(self), - "hint": HintKeyParser(self), + 'normal': CommandKeyParser(self), + 'hint': HintKeyParser(self), + 'insert': InsertKeyParser(self), } self._init_cmds() self.mainwindow = MainWindow() modes.init(self) - modes.manager.register("normal", self._keyparsers["normal"].handle) - modes.manager.register("hint", self._keyparsers["hint"].handle) - modes.manager.register("insert", None, passthrough=True) - modes.manager.register("command", None, passthrough=True) + modes.manager.register('normal', self._keyparsers['normal'].handle) + modes.manager.register('hint', self._keyparsers['hint'].handle) + modes.manager.register('insert', self._keyparsers['insert'].handle, + passthrough=True) + modes.manager.register('command', None, passthrough=True) self.installEventFilter(modes.manager) self.setQuitOnLastWindowClosed(False) diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index 7f34399fa..9719fc207 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -78,6 +78,13 @@ SECTION_DESC = { "pressed with Shift. For special keys (with <>-signs), you need " "to explicitely add \"Shift-\" to match a key pressed with shift. " "You can bind multiple commands by separating them with \";;\"."), + 'keybind.insert': ( + "Keybindings for insert mode.\n" + "Since normal keypresses are passed through, only special keys are " + "supported in this mode.\n" + "In addition to the normal commands, the following special commands " + "are defined:\n" + " : Switch back to normal mode."), 'aliases': ( "Aliases for commands.\n" "By default, no aliases are defined. Example which adds a new command " @@ -420,6 +427,12 @@ DATA = OrderedDict([ ('', 'back'), )), + ('keybind.insert', sect.ValueList( + types.KeyBindingName(), types.KeyBinding(['']), + ('', ''), + ('', ''), + )), + ('aliases', sect.ValueList( types.Command(), types.Command(), )), diff --git a/qutebrowser/config/conftypes.py b/qutebrowser/config/conftypes.py index 832e61852..5694cfa03 100644 --- a/qutebrowser/config/conftypes.py +++ b/qutebrowser/config/conftypes.py @@ -538,6 +538,18 @@ class LastClose(String): class KeyBinding(Command): - """The command of a keybinding.""" + """The command of a keybinding. - pass + Attributes: + _special_keys: Specially defined keys which are no commands. + """ + + def __init__(self, special_keys=None): + if special_keys == None: + special_keys = [] + self._special_keys = special_keys + + def validate(self, value): + if value in self._special_keys: + return + super().validate(value) diff --git a/qutebrowser/keyinput/insertmode.py b/qutebrowser/keyinput/insertmode.py new file mode 100644 index 000000000..fbeba0300 --- /dev/null +++ b/qutebrowser/keyinput/insertmode.py @@ -0,0 +1,37 @@ +# Copyright 2014 Florian Bruhin (The Compiler) +# +# 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 . + +"""KeyParser for "insert" mode.""" + +from PyQt5.QtCore import pyqtSignal, Qt + +import qutebrowser.keyinput.modes as modes +from qutebrowser.keyinput.keyparser import KeyParser + + +class InsertKeyParser(KeyParser): + + """KeyParser for insert mode.""" + + def __init__(self, parent=None): + super().__init__(parent) + self.read_config('keybind.insert') + + def execute(self, cmdstr, count=None): + """Handle a completed keychain.""" + if cmdstr == '': + modes.leave("insert") From 0c1551735249fa48bd9d692afcd5d4c9f6468f1a Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 24 Apr 2014 21:19:29 +0200 Subject: [PATCH 36/54] s/// --- qutebrowser/config/configdata.py | 8 ++++---- qutebrowser/keyinput/insertmode.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index 9719fc207..725683860 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -84,7 +84,7 @@ SECTION_DESC = { "supported in this mode.\n" "In addition to the normal commands, the following special commands " "are defined:\n" - " : Switch back to normal mode."), + " : Leave the insert mode."), 'aliases': ( "Aliases for commands.\n" "By default, no aliases are defined. Example which adds a new command " @@ -428,9 +428,9 @@ DATA = OrderedDict([ )), ('keybind.insert', sect.ValueList( - types.KeyBindingName(), types.KeyBinding(['']), - ('', ''), - ('', ''), + types.KeyBindingName(), types.KeyBinding(['']), + ('', ''), + ('', ''), )), ('aliases', sect.ValueList( diff --git a/qutebrowser/keyinput/insertmode.py b/qutebrowser/keyinput/insertmode.py index fbeba0300..33191c53e 100644 --- a/qutebrowser/keyinput/insertmode.py +++ b/qutebrowser/keyinput/insertmode.py @@ -33,5 +33,5 @@ class InsertKeyParser(KeyParser): def execute(self, cmdstr, count=None): """Handle a completed keychain.""" - if cmdstr == '': + if cmdstr == '': modes.leave("insert") From 9320c813f71004e085e424c315eea180c078e1e9 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 24 Apr 2014 21:28:24 +0200 Subject: [PATCH 37/54] Add set_cmd_text to MessageBridge --- qutebrowser/app.py | 4 +--- qutebrowser/browser/hints.py | 18 ++---------------- qutebrowser/keyinput/commandmode.py | 17 +++-------------- qutebrowser/utils/message.py | 6 ++++++ qutebrowser/widgets/tabbedbrowser.py | 18 ++++-------------- 5 files changed, 16 insertions(+), 47 deletions(-) diff --git a/qutebrowser/app.py b/qutebrowser/app.py index 5db5fd2bc..98289e274 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -259,10 +259,7 @@ class QuteBrowser(QApplication): # status bar modes.manager.entered.connect(status.on_mode_entered) modes.manager.left.connect(status.on_mode_left) - # FIXME what to do here? modes.manager.key_pressed.connect(status.on_key_pressed) - for obj in [kp["normal"], tabs]: - obj.set_cmd_text.connect(cmd.set_cmd_text) # commands cmd.got_cmd.connect(self.commandparser.run) @@ -282,6 +279,7 @@ class QuteBrowser(QApplication): message.bridge.error.connect(status.disp_error) message.bridge.info.connect(status.txt.set_temptext) message.bridge.text.connect(status.txt.set_normaltext) + message.bridge.set_cmd_text.connect(cmd.set_cmd_text) # config self.config.style_changed.connect(style.invalidate_caches) diff --git a/qutebrowser/browser/hints.py b/qutebrowser/browser/hints.py index 736a5b1d9..e0d2e5147 100644 --- a/qutebrowser/browser/hints.py +++ b/qutebrowser/browser/hints.py @@ -62,7 +62,6 @@ class HintManager(QObject): arg 0: URL to open as a string. arg 1: true if it should be opened in a new tab, else false. set_open_target: Set a new target to open the links in. - set_cmd_text: Emitted when the commandline text should be set. """ HINT_CSS = """ @@ -81,7 +80,6 @@ class HintManager(QObject): hint_strings_updated = pyqtSignal(list) mouse_event = pyqtSignal('QMouseEvent') set_open_target = pyqtSignal(str) - set_cmd_text = pyqtSignal(str) def __init__(self, parent=None): """Constructor. @@ -244,19 +242,6 @@ class HintManager(QObject): message.info('URL yanked to {}'.format('primary selection' if sel else 'clipboard')) - def _set_cmd_text(self, link, command): - """Fill the command line with an element link. - - Args: - link: The URL to open. - command: The command to use. - - Emit: - set_cmd_text: Always emitted. - """ - self.set_cmd_text.emit(':{} {}'.format(command, - urlutils.urlstring(link))) - def _resolve_link(self, elem): """Resolve a link and check if we want to keep it. @@ -368,7 +353,8 @@ class HintManager(QObject): 'cmd_tab': 'tabopen', 'cmd_bgtab': 'backtabopen', } - self._set_cmd_text(link, commands[self._target]) + message.set_cmd_text(':{} {}'.format(commands[self._target], + urlutils.urlstring(link))) if self._target != 'rapid': self.stop() diff --git a/qutebrowser/keyinput/commandmode.py b/qutebrowser/keyinput/commandmode.py index bd4a040b4..07b1bc728 100644 --- a/qutebrowser/keyinput/commandmode.py +++ b/qutebrowser/keyinput/commandmode.py @@ -25,6 +25,7 @@ import logging from PyQt5.QtCore import pyqtSignal +import qutebrowser.utils.message as message from qutebrowser.keyinput.keyparser import KeyChainParser from qutebrowser.commands.parsers import (CommandParser, ArgumentCountError, NoSuchCommandError) @@ -41,13 +42,8 @@ class CommandKeyParser(KeyChainParser): Attributes: commandparser: Commandparser instance. - - Signals: - set_cmd_text: Emitted when the statusbar should set a partial command. - arg: Text to set. """ - set_cmd_text = pyqtSignal(str) supports_count = True def __init__(self, parent=None): @@ -62,10 +58,6 @@ class CommandKeyParser(KeyChainParser): cmdstr: The command string. count: Optional command count. ignore_exc: Ignore exceptions. - - Emit: - set_cmd_text: If a partial command should be printed to the - statusbar. """ try: self.commandparser.run(cmdstr, count=count, ignore_exc=ignore_exc) @@ -74,7 +66,7 @@ class CommandKeyParser(KeyChainParser): except ArgumentCountError: logging.debug('Filling statusbar with partial command {}'.format( cmdstr)) - self.set_cmd_text.emit(':{} '.format(cmdstr)) + message.set_cmd_text(':{} '.format(cmdstr)) def _handle_single_key(self, e): """Override _handle_single_key to abort if the key is a startchar. @@ -82,15 +74,12 @@ class CommandKeyParser(KeyChainParser): Args: e: the KeyPressEvent from Qt. - Emit: - set_cmd_text: If the keystring should be shown in the statusbar. - Return: True if event has been handled, False otherwise. """ txt = e.text().strip() if not self._keystring and any(txt == c for c in STARTCHARS): - self.set_cmd_text.emit(txt) + message.set_cmd_text(txt) return True return super()._handle_single_key(e) diff --git a/qutebrowser/utils/message.py b/qutebrowser/utils/message.py index 4157114a6..293e5acb6 100644 --- a/qutebrowser/utils/message.py +++ b/qutebrowser/utils/message.py @@ -57,6 +57,11 @@ def clear(): bridge.text.emit('') +def set_cmd_text(text): + """Set the statusbar command line to a preset text.""" + bridge.set_cmd_text.emit(text) + + class MessageBridge(QObject): """Bridge for messages to be shown in the statusbar.""" @@ -64,3 +69,4 @@ class MessageBridge(QObject): error = pyqtSignal(str) info = pyqtSignal(str) text = pyqtSignal(str) + set_cmd_text = pyqtSignal(str) diff --git a/qutebrowser/widgets/tabbedbrowser.py b/qutebrowser/widgets/tabbedbrowser.py index 013d5429c..f8c926bca 100644 --- a/qutebrowser/widgets/tabbedbrowser.py +++ b/qutebrowser/widgets/tabbedbrowser.py @@ -84,7 +84,6 @@ class TabbedBrowser(TabWidget): cur_link_hovered = pyqtSignal(str, str, str) cur_scroll_perc_changed = pyqtSignal(int, int) hint_strings_updated = pyqtSignal(list) - set_cmd_text = pyqtSignal(str) shutdown_complete = pyqtSignal() quit = pyqtSignal() resized = pyqtSignal('QRect') @@ -137,7 +136,6 @@ class TabbedBrowser(TabWidget): tab.urlChanged.connect(self._filter.create(self.cur_url_changed)) # hintmanager tab.hintmanager.hint_strings_updated.connect(self.hint_strings_updated) - tab.hintmanager.set_cmd_text.connect(self.set_cmd_text) # misc tab.titleChanged.connect(self.on_title_changed) tab.open_tab.connect(self.tabopen) @@ -235,23 +233,15 @@ class TabbedBrowser(TabWidget): @cmdutils.register(instance='mainwindow.tabs', hide=True) def tabopencur(self): - """Set the statusbar to :tabopen and the current URL. - - Emit: - set_cmd_text prefilled with :tabopen $URL - """ + """Set the statusbar to :tabopen and the current URL.""" url = urlutils.urlstring(self.currentWidget().url()) - self.set_cmd_text.emit(':tabopen ' + url) + message.set_cmd_text(':tabopen ' + url) @cmdutils.register(instance='mainwindow.tabs', hide=True) def opencur(self): - """Set the statusbar to :open and the current URL. - - Emit: - set_cmd_text prefilled with :open $URL - """ + """Set the statusbar to :open and the current URL.""" url = urlutils.urlstring(self.currentWidget().url()) - self.set_cmd_text.emit(':open ' + url) + message.set_cmd_text(':open ' + url) @cmdutils.register(instance='mainwindow.tabs', name='undo') def undo_close(self): From ea6a25714a1148e1093d3cdb8db9a711c4800112 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 24 Apr 2014 21:29:28 +0200 Subject: [PATCH 38/54] Make eventFilter work when mode is None --- qutebrowser/keyinput/modes.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/qutebrowser/keyinput/modes.py b/qutebrowser/keyinput/modes.py index 6174f7b8a..40d2e229f 100644 --- a/qutebrowser/keyinput/modes.py +++ b/qutebrowser/keyinput/modes.py @@ -165,8 +165,11 @@ class ModeManager(QObject): Emit: key_pressed: When a key was actually pressed. """ - typ = evt.type() + if self.mode is None: + # We got events before mode is set, so just pass them through. + return False handler = self._handlers[self.mode] + typ = evt.type() if typ not in [QEvent.KeyPress, QEvent.KeyRelease]: # We're not interested in non-key-events so we pass them through. return False From 9ab8f42e208ae09b33089d012730cc5e1f888fc6 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 24 Apr 2014 21:37:10 +0200 Subject: [PATCH 39/54] s/commandmode/normalmode --- qutebrowser/app.py | 2 +- qutebrowser/keyinput/{commandmode.py => normalmode.py} | 0 qutebrowser/widgets/statusbar.py | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename qutebrowser/keyinput/{commandmode.py => normalmode.py} (100%) diff --git a/qutebrowser/app.py b/qutebrowser/app.py index 98289e274..bfb555385 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -56,7 +56,7 @@ import qutebrowser.keyinput.modes as modes import qutebrowser.utils.message as message from qutebrowser.widgets.mainwindow import MainWindow from qutebrowser.widgets.crash import CrashDialog -from qutebrowser.keyinput.commandmode import CommandKeyParser +from qutebrowser.keyinput.normalmode import CommandKeyParser from qutebrowser.keyinput.insertmode import InsertKeyParser from qutebrowser.commands.parsers import CommandParser, SearchParser from qutebrowser.browser.hints import HintKeyParser diff --git a/qutebrowser/keyinput/commandmode.py b/qutebrowser/keyinput/normalmode.py similarity index 100% rename from qutebrowser/keyinput/commandmode.py rename to qutebrowser/keyinput/normalmode.py diff --git a/qutebrowser/widgets/statusbar.py b/qutebrowser/widgets/statusbar.py index 03ac8d7d3..91479f7e9 100644 --- a/qutebrowser/widgets/statusbar.py +++ b/qutebrowser/widgets/statusbar.py @@ -24,7 +24,7 @@ from PyQt5.QtWidgets import (QWidget, QLineEdit, QProgressBar, QLabel, from PyQt5.QtGui import QPainter, QKeySequence, QValidator import qutebrowser.keyinput.modes as modes -from qutebrowser.keyinput.commandmode import STARTCHARS +from qutebrowser.keyinput.normalmode import STARTCHARS from qutebrowser.config.style import set_register_stylesheet, get_stylesheet from qutebrowser.utils.url import urlstring from qutebrowser.commands.parsers import split_cmdline From 7a6a60570218b38932524d5c2ea45b64fdffe5b1 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 24 Apr 2014 22:40:16 +0200 Subject: [PATCH 40/54] Unify KeyParsers again --- qutebrowser/keyinput/hintmode.py | 11 +- qutebrowser/keyinput/insertmode.py | 2 +- qutebrowser/keyinput/keyparser.py | 182 ++++++++++++----------------- qutebrowser/keyinput/normalmode.py | 11 +- 4 files changed, 81 insertions(+), 125 deletions(-) diff --git a/qutebrowser/keyinput/hintmode.py b/qutebrowser/keyinput/hintmode.py index d689eeabf..4bf3a06e9 100644 --- a/qutebrowser/keyinput/hintmode.py +++ b/qutebrowser/keyinput/hintmode.py @@ -19,26 +19,25 @@ from PyQt5.QtCore import pyqtSignal, Qt -from qutebrowser.keyinput.keyparser import KeyChainParser +from qutebrowser.keyinput.keyparser import KeyParser -class HintKeyParser(KeyChainParser): +class HintKeyParser(KeyParser): """KeyChainParser for hints. - Class attributes: - supports_count: If the keyparser should support counts. - Signals: fire_hint: When a hint keybinding was completed. Arg: the keystring/hint string pressed. abort_hinting: Esc pressed, so abort hinting. """ - supports_count = False fire_hint = pyqtSignal(str) abort_hinting = pyqtSignal() + def __init__(self, parent=None): + super().__init__(parent, supports_count=False, supports_chains=True) + def _handle_special_key(self, e): """Handle the escape key. diff --git a/qutebrowser/keyinput/insertmode.py b/qutebrowser/keyinput/insertmode.py index 33191c53e..660891b74 100644 --- a/qutebrowser/keyinput/insertmode.py +++ b/qutebrowser/keyinput/insertmode.py @@ -28,7 +28,7 @@ class InsertKeyParser(KeyParser): """KeyParser for insert mode.""" def __init__(self, parent=None): - super().__init__(parent) + super().__init__(parent, supports_chains=False) self.read_config('keybind.insert') def execute(self, cmdstr, count=None): diff --git a/qutebrowser/keyinput/keyparser.py b/qutebrowser/keyinput/keyparser.py index 74a292b4b..2fd901cd1 100644 --- a/qutebrowser/keyinput/keyparser.py +++ b/qutebrowser/keyinput/keyparser.py @@ -29,25 +29,57 @@ import qutebrowser.config.config as config class KeyParser(QObject): - """Parser for non-chained Qt keypresses ("special bindings"). - - We call these special because chained keypresses are the "normal" ones in - qutebrowser, however there are some cases where we can _only_ use special - keys (like in insert mode). + """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_PARTIAL: Constant for a partial match (no keychain matched yet, + but it's still possible in the future. + MATCH_DEFINITIVE: Constant for a full match (keychain matches exactly). + MATCH_AMBIGUOUS: There are both a partial and a definitive match. + MATCH_NONE: Constant for no match (no more matches possible). + Attributes: + bindings: Bound keybindings special_bindings: Bound special bindings (). + _keystring: The currently entered key sequence + _timer: QTimer for delayed execution. _confsectname: The name of the configsection. + _supports_count: Whether count is supported + _supports_chains: Whether keychains are supported + + Signals: + keystring_updated: Emitted when the keystring is updated. + arg: New keystring. """ - def __init__(self, parent=None, special_bindings=None): + keystring_updated = pyqtSignal(str) + + MATCH_PARTIAL = 0 + MATCH_DEFINITIVE = 1 + MATCH_AMBIGUOUS = 2 + MATCH_NONE = 3 + + def __init__(self, parent=None, bindings=None, special_bindings=None, + supports_count=None, supports_chains=False): super().__init__(parent) + self._timer = None self._confsectname = None + self._keystring = '' + if supports_count is None: + supports_count = supports_chains + self._supports_count = supports_count + self._supports_chains = supports_chains self.special_bindings = ({} if special_bindings is None else special_bindings) + if bindings is None: + self.bindings = {} + elif supports_chains: + self.bindsings = bindings + else: + raise ValueError("bindings given with supports_chains=False!") def _normalize_keystr(self, keystr): """Normalize a keystring like Ctrl-Q to a keystring like Ctrl+Q. @@ -105,99 +137,6 @@ class KeyParser(QObject): self.execute(cmdstr) return True - def handle(self, e): - """Handle a new keypress and call the respective handlers. - - Args: - e: the KeyPressEvent from Qt - """ - return self._handle_special_key(e) - - def execute(self, cmdstr, count=None): - """Execute an action when a binding is triggered. - - Needs to be overriden in superclasses.""" - raise NotImplementedError - - def read_config(self, sectname=None): - """Read the configuration. - - Config format: key = command, e.g.: - = quit - - Args: - sectname: Name of the section to read. - """ - if sectname is None: - if self._confsectname is None: - raise ValueError("read_config called with no section, but " - "None defined so far!") - sectname = self._confsectname - else: - self._confsectname = sectname - sect = config.instance[sectname] - if not sect.items(): - logging.warn("No keybindings defined!") - for (key, cmd) in sect.items(): - if key.startswith('<') and key.endswith('>'): - keystr = self._normalize_keystr(key[1:-1]) - logging.debug('registered special key: {} -> {}'.format(keystr, - cmd)) - self.special_bindings[keystr] = cmd - - @pyqtSlot(str, str) - def on_config_changed(self, section, _option): - """Re-read the config if a keybinding was changed.""" - if self._confsectname is None: - raise AttributeError("on_config_changed called but no section " - "defined!") - if section == self._confsectname: - self.read_config() - - -class KeyChainParser(KeyParser): - - """Parser for vim-like key sequences. - - Not intended to be instantiated directly. Subclasses have to override - execute() to do whatever they want to. - - Class Attributes: - MATCH_PARTIAL: Constant for a partial match (no keychain matched yet, - but it's still possible in the future. - MATCH_DEFINITIVE: Constant for a full match (keychain matches exactly). - MATCH_AMBIGUOUS: There are both a partial and a definitive match. - MATCH_NONE: Constant for no match (no more matches possible). - supports_count: If the keyparser should support counts. - - Attributes: - _keystring: The currently entered key sequence - _timer: QTimer for delayed execution. - bindings: Bound keybindings - - Signals: - keystring_updated: Emitted when the keystring is updated. - arg: New keystring. - """ - - # This is an abstract superclass of an abstract class. - # pylint: disable=abstract-method - - keystring_updated = pyqtSignal(str) - - MATCH_PARTIAL = 0 - MATCH_DEFINITIVE = 1 - MATCH_AMBIGUOUS = 2 - MATCH_NONE = 3 - - supports_count = False - - def __init__(self, parent=None, bindings=None, special_bindings=None): - super().__init__(parent, special_bindings) - self._timer = None - self._keystring = '' - self.bindings = {} if bindings is None else bindings - def _handle_single_key(self, e): """Handle a new keypress with a single key (no modifiers). @@ -220,7 +159,7 @@ class KeyChainParser(KeyParser): self._stop_delayed_exec() self._keystring += txt - if self.supports_count: + if self._supports_count: (countstr, cmd_input) = re.match(r'^(\d*)(.*)', self._keystring).groups() count = int(countstr) if countstr else None @@ -338,7 +277,7 @@ class KeyChainParser(KeyParser): self.execute(command, count) def handle(self, e): - """Override KeyParser.handle() to also handle keychains. + """Handle a new keypress and call the respective handlers. Args: e: the KeyPressEvent from Qt @@ -346,15 +285,15 @@ class KeyChainParser(KeyParser): Emit: keystring_updated: If a new keystring should be set. """ - handled = super().handle(e) - if handled: - return True + handled = self._handle_special_key(e) + if handled or not self._supports_chains: + return handled handled = self._handle_single_key(e) self.keystring_updated.emit(self._keystring) return handled def read_config(self, sectname=None): - """Extend KeyParser.read_config to also read keychains. + """Read the configuration. Config format: key = command, e.g.: = quit @@ -362,12 +301,35 @@ class KeyChainParser(KeyParser): Args: sectname: Name of the section to read. """ - super().read_config(sectname) + if sectname is None: + if self._confsectname is None: + raise ValueError("read_config called with no section, but " + "None defined so far!") + sectname = self._confsectname + else: + self._confsectname = sectname sect = config.instance[sectname] + if not sect.items(): + logging.warn("No keybindings defined!") for (key, cmd) in sect.items(): if key.startswith('<') and key.endswith('>'): - # Already registered by superclass - pass - else: - logging.debug('registered key: {} -> {}'.format(key, cmd)) + keystr = self._normalize_keystr(key[1:-1]) + logging.debug("registered special key: {} -> {}".format(keystr, + cmd)) + self.special_bindings[keystr] = cmd + elif self._supports_chains: + logging.debug("registered key: {} -> {}".format(key, cmd)) self.bindings[key] = cmd + else: + logging.warn("Ignoring keychain \"{}\" in section \"{}\" " + "because keychains are not supported there.".format( + key, sectname)) + + @pyqtSlot(str, str) + def on_config_changed(self, section, _option): + """Re-read the config if a keybinding was changed.""" + if self._confsectname is None: + raise AttributeError("on_config_changed called but no section " + "defined!") + if section == self._confsectname: + self.read_config() diff --git a/qutebrowser/keyinput/normalmode.py b/qutebrowser/keyinput/normalmode.py index 07b1bc728..846edb216 100644 --- a/qutebrowser/keyinput/normalmode.py +++ b/qutebrowser/keyinput/normalmode.py @@ -26,28 +26,23 @@ import logging from PyQt5.QtCore import pyqtSignal import qutebrowser.utils.message as message -from qutebrowser.keyinput.keyparser import KeyChainParser +from qutebrowser.keyinput.keyparser import KeyParser from qutebrowser.commands.parsers import (CommandParser, ArgumentCountError, NoSuchCommandError) STARTCHARS = ":/?" -class CommandKeyParser(KeyChainParser): +class CommandKeyParser(KeyParser): """KeyChainParser for command bindings. - Class attributes: - supports_count: If the keyparser should support counts. - Attributes: commandparser: Commandparser instance. """ - supports_count = True - def __init__(self, parent=None): - super().__init__(parent) + super().__init__(parent, supports_count=True, supports_chains=True) self.commandparser = CommandParser() self.read_config('keybind') From 581b715b42c7e17835b3d2811b721020e997d781 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 24 Apr 2014 22:41:01 +0200 Subject: [PATCH 41/54] Add __init__.py for keyinput --- qutebrowser/keyinput/__init__.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 qutebrowser/keyinput/__init__.py diff --git a/qutebrowser/keyinput/__init__.py b/qutebrowser/keyinput/__init__.py new file mode 100644 index 000000000..307ed0a30 --- /dev/null +++ b/qutebrowser/keyinput/__init__.py @@ -0,0 +1,18 @@ +# Copyright 2014 Florian Bruhin (The Compiler) +# +# 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 . + +"""Modules related to keyboard input and mode handling.""" From 0def82fe8c554fce22b85953a14d4105abc2105c Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 24 Apr 2014 22:49:06 +0200 Subject: [PATCH 42/54] Fix lint --- qutebrowser/app.py | 2 +- qutebrowser/browser/hints.py | 1 - qutebrowser/config/conftypes.py | 2 +- qutebrowser/keyinput/hintmode.py | 2 +- qutebrowser/keyinput/insertmode.py | 4 +--- qutebrowser/keyinput/keyparser.py | 6 +++--- qutebrowser/keyinput/modes.py | 4 +--- qutebrowser/keyinput/normalmode.py | 2 -- qutebrowser/utils/message.py | 4 ++-- 9 files changed, 10 insertions(+), 17 deletions(-) diff --git a/qutebrowser/app.py b/qutebrowser/app.py index bfb555385..292890f37 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -58,8 +58,8 @@ from qutebrowser.widgets.mainwindow import MainWindow from qutebrowser.widgets.crash import CrashDialog from qutebrowser.keyinput.normalmode import CommandKeyParser from qutebrowser.keyinput.insertmode import InsertKeyParser +from qutebrowser.keyinput.hintmode import HintKeyParser from qutebrowser.commands.parsers import CommandParser, SearchParser -from qutebrowser.browser.hints import HintKeyParser from qutebrowser.utils.appdirs import AppDirs from qutebrowser.utils.misc import dotted_getattr from qutebrowser.utils.debug import set_trace # pylint: disable=unused-import diff --git a/qutebrowser/browser/hints.py b/qutebrowser/browser/hints.py index e0d2e5147..58f36e295 100644 --- a/qutebrowser/browser/hints.py +++ b/qutebrowser/browser/hints.py @@ -30,7 +30,6 @@ import qutebrowser.keyinput.modes as modes import qutebrowser.utils.message as message import qutebrowser.utils.url as urlutils import qutebrowser.utils.webelem as webelem -from qutebrowser.keyinput.hintmode import HintKeyParser ElemTuple = namedtuple('ElemTuple', 'elem, label') diff --git a/qutebrowser/config/conftypes.py b/qutebrowser/config/conftypes.py index 5694cfa03..afee10b86 100644 --- a/qutebrowser/config/conftypes.py +++ b/qutebrowser/config/conftypes.py @@ -545,7 +545,7 @@ class KeyBinding(Command): """ def __init__(self, special_keys=None): - if special_keys == None: + if special_keys is None: special_keys = [] self._special_keys = special_keys diff --git a/qutebrowser/keyinput/hintmode.py b/qutebrowser/keyinput/hintmode.py index 4bf3a06e9..60ff8f3bb 100644 --- a/qutebrowser/keyinput/hintmode.py +++ b/qutebrowser/keyinput/hintmode.py @@ -52,7 +52,7 @@ class HintKeyParser(KeyParser): return True return False - def execute(self, cmdstr, count=None): + def execute(self, cmdstr, _count=None): """Handle a completed keychain. Emit: diff --git a/qutebrowser/keyinput/insertmode.py b/qutebrowser/keyinput/insertmode.py index 660891b74..c429b045a 100644 --- a/qutebrowser/keyinput/insertmode.py +++ b/qutebrowser/keyinput/insertmode.py @@ -17,8 +17,6 @@ """KeyParser for "insert" mode.""" -from PyQt5.QtCore import pyqtSignal, Qt - import qutebrowser.keyinput.modes as modes from qutebrowser.keyinput.keyparser import KeyParser @@ -31,7 +29,7 @@ class InsertKeyParser(KeyParser): super().__init__(parent, supports_chains=False) self.read_config('keybind.insert') - def execute(self, cmdstr, count=None): + def execute(self, cmdstr, _count=None): """Handle a completed keychain.""" if cmdstr == '': modes.leave("insert") diff --git a/qutebrowser/keyinput/keyparser.py b/qutebrowser/keyinput/keyparser.py index 2fd901cd1..9c1b0181f 100644 --- a/qutebrowser/keyinput/keyparser.py +++ b/qutebrowser/keyinput/keyparser.py @@ -321,9 +321,9 @@ class KeyParser(QObject): logging.debug("registered key: {} -> {}".format(key, cmd)) self.bindings[key] = cmd else: - logging.warn("Ignoring keychain \"{}\" in section \"{}\" " - "because keychains are not supported there.".format( - key, sectname)) + logging.warn( + "Ignoring keychain \"{}\" in section \"{}\" because " + "keychains are not supported there.".format(key, sectname)) @pyqtSlot(str, str) def on_config_changed(self, section, _option): diff --git a/qutebrowser/keyinput/modes.py b/qutebrowser/keyinput/modes.py index 40d2e229f..e293edc5f 100644 --- a/qutebrowser/keyinput/modes.py +++ b/qutebrowser/keyinput/modes.py @@ -190,10 +190,8 @@ class ModeManager(QObject): handled = handler(evt) else: handled = False - if handled: + if handled or not self._forward_unbound_keys: return True - elif self._forward_unbound_keys: - return False else: return True else: diff --git a/qutebrowser/keyinput/normalmode.py b/qutebrowser/keyinput/normalmode.py index 846edb216..bf6207b4f 100644 --- a/qutebrowser/keyinput/normalmode.py +++ b/qutebrowser/keyinput/normalmode.py @@ -23,8 +23,6 @@ Module attributes: import logging -from PyQt5.QtCore import pyqtSignal - import qutebrowser.utils.message as message from qutebrowser.keyinput.keyparser import KeyParser from qutebrowser.commands.parsers import (CommandParser, ArgumentCountError, diff --git a/qutebrowser/utils/message.py b/qutebrowser/utils/message.py index 293e5acb6..c5b9a8619 100644 --- a/qutebrowser/utils/message.py +++ b/qutebrowser/utils/message.py @@ -57,9 +57,9 @@ def clear(): bridge.text.emit('') -def set_cmd_text(text): +def set_cmd_text(txt): """Set the statusbar command line to a preset text.""" - bridge.set_cmd_text.emit(text) + bridge.set_cmd_text.emit(txt) class MessageBridge(QObject): From 8cca182734537408685de9c798cedfce87ec7bce Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 24 Apr 2014 22:56:55 +0200 Subject: [PATCH 43/54] Split NormalKeyParser from CommandKeyParser --- qutebrowser/app.py | 4 +-- qutebrowser/keyinput/keyparser.py | 39 ++++++++++++++++++++++++++++++ qutebrowser/keyinput/normalmode.py | 34 ++------------------------ 3 files changed, 43 insertions(+), 34 deletions(-) diff --git a/qutebrowser/app.py b/qutebrowser/app.py index 292890f37..d8c197dab 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -56,7 +56,7 @@ import qutebrowser.keyinput.modes as modes import qutebrowser.utils.message as message from qutebrowser.widgets.mainwindow import MainWindow from qutebrowser.widgets.crash import CrashDialog -from qutebrowser.keyinput.normalmode import CommandKeyParser +from qutebrowser.keyinput.normalmode import NormalKeyParser from qutebrowser.keyinput.insertmode import InsertKeyParser from qutebrowser.keyinput.hintmode import HintKeyParser from qutebrowser.commands.parsers import CommandParser, SearchParser @@ -125,7 +125,7 @@ class QuteBrowser(QApplication): self.commandparser = CommandParser() self.searchparser = SearchParser() self._keyparsers = { - 'normal': CommandKeyParser(self), + 'normal': NormalKeyParser(self), 'hint': HintKeyParser(self), 'insert': InsertKeyParser(self), } diff --git a/qutebrowser/keyinput/keyparser.py b/qutebrowser/keyinput/keyparser.py index 9c1b0181f..a70d39a68 100644 --- a/qutebrowser/keyinput/keyparser.py +++ b/qutebrowser/keyinput/keyparser.py @@ -25,6 +25,9 @@ from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QObject, QTimer from PyQt5.QtGui import QKeySequence import qutebrowser.config.config as config +import qutebrowser.utils.message as message +from qutebrowser.commands.parsers import (CommandParser, ArgumentCountError, + NoSuchCommandError) class KeyParser(QObject): @@ -333,3 +336,39 @@ class KeyParser(QObject): "defined!") if section == self._confsectname: self.read_config() + + +class CommandKeyParser(KeyParser): + + """KeyChainParser for command bindings. + + Attributes: + commandparser: Commandparser instance. + """ + + def __init__(self, parent=None, supports_count=None, + supports_chains=False): + super().__init__(parent, supports_count=supports_count, + supports_chains=supports_chains) + self.commandparser = CommandParser() + + def _run_or_fill(self, cmdstr, count=None, ignore_exc=True): + """Run the command in cmdstr or fill the statusbar if args missing. + + Args: + cmdstr: The command string. + count: Optional command count. + ignore_exc: Ignore exceptions. + """ + try: + self.commandparser.run(cmdstr, count=count, ignore_exc=ignore_exc) + except NoSuchCommandError: + pass + except ArgumentCountError: + logging.debug('Filling statusbar with partial command {}'.format( + cmdstr)) + message.set_cmd_text(':{} '.format(cmdstr)) + + def execute(self, cmdstr, count=None): + """Handle a completed keychain.""" + self._run_or_fill(cmdstr, count, ignore_exc=False) diff --git a/qutebrowser/keyinput/normalmode.py b/qutebrowser/keyinput/normalmode.py index bf6207b4f..456d3b488 100644 --- a/qutebrowser/keyinput/normalmode.py +++ b/qutebrowser/keyinput/normalmode.py @@ -24,43 +24,17 @@ Module attributes: import logging import qutebrowser.utils.message as message -from qutebrowser.keyinput.keyparser import KeyParser -from qutebrowser.commands.parsers import (CommandParser, ArgumentCountError, - NoSuchCommandError) +from qutebrowser.keyinput.keyparser import CommandKeyParser STARTCHARS = ":/?" -class CommandKeyParser(KeyParser): - - """KeyChainParser for command bindings. - - Attributes: - commandparser: Commandparser instance. - """ +class NormalKeyParser(CommandKeyParser): def __init__(self, parent=None): super().__init__(parent, supports_count=True, supports_chains=True) - self.commandparser = CommandParser() self.read_config('keybind') - def _run_or_fill(self, cmdstr, count=None, ignore_exc=True): - """Run the command in cmdstr or fill the statusbar if args missing. - - Args: - cmdstr: The command string. - count: Optional command count. - ignore_exc: Ignore exceptions. - """ - try: - self.commandparser.run(cmdstr, count=count, ignore_exc=ignore_exc) - except NoSuchCommandError: - pass - except ArgumentCountError: - logging.debug('Filling statusbar with partial command {}'.format( - cmdstr)) - message.set_cmd_text(':{} '.format(cmdstr)) - def _handle_single_key(self, e): """Override _handle_single_key to abort if the key is a startchar. @@ -75,7 +49,3 @@ class CommandKeyParser(KeyParser): message.set_cmd_text(txt) return True return super()._handle_single_key(e) - - def execute(self, cmdstr, count=None): - """Handle a completed keychain.""" - self._run_or_fill(cmdstr, count, ignore_exc=False) From a1fd1537bdec7bed015f347834f603aba5a79408 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 24 Apr 2014 22:59:01 +0200 Subject: [PATCH 44/54] Remove the possibility to pass bindings to KeyParser init --- qutebrowser/keyinput/keyparser.py | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/qutebrowser/keyinput/keyparser.py b/qutebrowser/keyinput/keyparser.py index a70d39a68..8029636f4 100644 --- a/qutebrowser/keyinput/keyparser.py +++ b/qutebrowser/keyinput/keyparser.py @@ -65,8 +65,8 @@ class KeyParser(QObject): MATCH_AMBIGUOUS = 2 MATCH_NONE = 3 - def __init__(self, parent=None, bindings=None, special_bindings=None, - supports_count=None, supports_chains=False): + def __init__(self, parent=None, supports_count=None, + supports_chains=False): super().__init__(parent) self._timer = None self._confsectname = None @@ -75,14 +75,8 @@ class KeyParser(QObject): supports_count = supports_chains self._supports_count = supports_count self._supports_chains = supports_chains - self.special_bindings = ({} if special_bindings is None - else special_bindings) - if bindings is None: - self.bindings = {} - elif supports_chains: - self.bindsings = bindings - else: - raise ValueError("bindings given with supports_chains=False!") + self.bindings = {} + self.special_bindings = {} def _normalize_keystr(self, keystr): """Normalize a keystring like Ctrl-Q to a keystring like Ctrl+Q. @@ -348,8 +342,7 @@ class CommandKeyParser(KeyParser): def __init__(self, parent=None, supports_count=None, supports_chains=False): - super().__init__(parent, supports_count=supports_count, - supports_chains=supports_chains) + super().__init__(parent, supports_count, supports_chains) self.commandparser = CommandParser() def _run_or_fill(self, cmdstr, count=None, ignore_exc=True): From e06583ade277b109cfae4db88e2a794e7bba77f8 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 24 Apr 2014 23:09:12 +0200 Subject: [PATCH 45/54] Use normal command to leave insert mode --- qutebrowser/app.py | 1 + qutebrowser/config/configdata.py | 10 ++++------ qutebrowser/config/conftypes.py | 16 ++-------------- qutebrowser/keyinput/insertmode.py | 9 ++------- qutebrowser/keyinput/modes.py | 8 ++++++++ 5 files changed, 17 insertions(+), 27 deletions(-) diff --git a/qutebrowser/app.py b/qutebrowser/app.py index d8c197dab..188efbd81 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -137,6 +137,7 @@ class QuteBrowser(QApplication): modes.manager.register('insert', self._keyparsers['insert'].handle, passthrough=True) modes.manager.register('command', None, passthrough=True) + self.modeman = modes.manager # for commands self.installEventFilter(modes.manager) self.setQuitOnLastWindowClosed(False) diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index 725683860..cea2e9d92 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -82,9 +82,7 @@ SECTION_DESC = { "Keybindings for insert mode.\n" "Since normal keypresses are passed through, only special keys are " "supported in this mode.\n" - "In addition to the normal commands, the following special commands " - "are defined:\n" - " : Leave the insert mode."), + "An useful command to map here is the hidden command leave_mode."), 'aliases': ( "Aliases for commands.\n" "By default, no aliases are defined. Example which adds a new command " @@ -428,9 +426,9 @@ DATA = OrderedDict([ )), ('keybind.insert', sect.ValueList( - types.KeyBindingName(), types.KeyBinding(['']), - ('', ''), - ('', ''), + types.KeyBindingName(), types.KeyBinding(), + ('', 'leave_mode'), + ('', 'leave_mode'), )), ('aliases', sect.ValueList( diff --git a/qutebrowser/config/conftypes.py b/qutebrowser/config/conftypes.py index afee10b86..832e61852 100644 --- a/qutebrowser/config/conftypes.py +++ b/qutebrowser/config/conftypes.py @@ -538,18 +538,6 @@ class LastClose(String): class KeyBinding(Command): - """The command of a keybinding. + """The command of a keybinding.""" - Attributes: - _special_keys: Specially defined keys which are no commands. - """ - - def __init__(self, special_keys=None): - if special_keys is None: - special_keys = [] - self._special_keys = special_keys - - def validate(self, value): - if value in self._special_keys: - return - super().validate(value) + pass diff --git a/qutebrowser/keyinput/insertmode.py b/qutebrowser/keyinput/insertmode.py index c429b045a..6b57b3822 100644 --- a/qutebrowser/keyinput/insertmode.py +++ b/qutebrowser/keyinput/insertmode.py @@ -18,18 +18,13 @@ """KeyParser for "insert" mode.""" import qutebrowser.keyinput.modes as modes -from qutebrowser.keyinput.keyparser import KeyParser +from qutebrowser.keyinput.keyparser import CommandKeyParser -class InsertKeyParser(KeyParser): +class InsertKeyParser(CommandKeyParser): """KeyParser for insert mode.""" def __init__(self, parent=None): super().__init__(parent, supports_chains=False) self.read_config('keybind.insert') - - def execute(self, cmdstr, _count=None): - """Handle a completed keychain.""" - if cmdstr == '': - modes.leave("insert") diff --git a/qutebrowser/keyinput/modes.py b/qutebrowser/keyinput/modes.py index e293edc5f..c143efb8e 100644 --- a/qutebrowser/keyinput/modes.py +++ b/qutebrowser/keyinput/modes.py @@ -26,6 +26,7 @@ import logging from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QEvent import qutebrowser.config.config as config +import qutebrowser.commands.utils as cmdutils manager = None @@ -150,6 +151,13 @@ class ModeManager(QObject): logging.debug("New mode stack: {}".format(self._mode_stack)) self.left.emit(mode) + # FIXME handle modes=[] and not_modes=[] params + @cmdutils.register(instance='modeman', name='leave_mode', hide=True) + def leave_current_mode(self): + if self.mode == "normal": + raise ValueError("Can't leave normal mode!") + self.leave(self.mode) + @pyqtSlot(str, str) def on_config_changed(self, section, option): """Update local setting when config changed.""" From 5aaf3b343039dad5157edcf59285980332016bd9 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 24 Apr 2014 23:23:28 +0200 Subject: [PATCH 46/54] Add abstract execute() to KeyParser --- qutebrowser/keyinput/keyparser.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/qutebrowser/keyinput/keyparser.py b/qutebrowser/keyinput/keyparser.py index 8029636f4..e3265c59a 100644 --- a/qutebrowser/keyinput/keyparser.py +++ b/qutebrowser/keyinput/keyparser.py @@ -322,6 +322,15 @@ class KeyParser(QObject): "Ignoring keychain \"{}\" in section \"{}\" because " "keychains are not supported there.".format(key, sectname)) + def execute(self, cmdstr, count=None): + """Handle a completed keychain. + + Args: + cmdstr: The command to execute as a string. + count: The count if given. + """ + raise NotImplementedError + @pyqtSlot(str, str) def on_config_changed(self, section, _option): """Re-read the config if a keybinding was changed.""" @@ -363,5 +372,4 @@ class CommandKeyParser(KeyParser): message.set_cmd_text(':{} '.format(cmdstr)) def execute(self, cmdstr, count=None): - """Handle a completed keychain.""" self._run_or_fill(cmdstr, count, ignore_exc=False) From 540c134f061f1cee3e95a196efcc2726d479000a Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 24 Apr 2014 23:46:37 +0200 Subject: [PATCH 47/54] Add keytypes to KeyParser.execute() --- qutebrowser/keyinput/keyparser.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/qutebrowser/keyinput/keyparser.py b/qutebrowser/keyinput/keyparser.py index e3265c59a..b9724c86e 100644 --- a/qutebrowser/keyinput/keyparser.py +++ b/qutebrowser/keyinput/keyparser.py @@ -44,6 +44,9 @@ class KeyParser(QObject): MATCH_AMBIGUOUS: There are both a partial and a definitive match. MATCH_NONE: Constant for no match (no more matches possible). + TYPE_CHAIN: execute() was called via a chain-like keybinding + TYPE_SPECIAL: execute() was called via a special keybinding + Attributes: bindings: Bound keybindings special_bindings: Bound special bindings (). @@ -65,6 +68,9 @@ class KeyParser(QObject): MATCH_AMBIGUOUS = 2 MATCH_NONE = 3 + TYPE_CHAIN = 0 + TYPE_SPECIAL = 1 + def __init__(self, parent=None, supports_count=None, supports_chains=False): super().__init__(parent) @@ -131,7 +137,7 @@ class KeyParser(QObject): except KeyError: logging.debug('No binding found for {}.'.format(modstr + keystr)) return False - self.execute(cmdstr) + self.execute(cmdstr, self.TYPE_SPECIAL) return True def _handle_single_key(self, e): @@ -172,7 +178,7 @@ class KeyParser(QObject): if match == self.MATCH_DEFINITIVE: self._keystring = '' - self.execute(binding, count) + self.execute(binding, self.TYPE_CHAIN, count) elif match == self.MATCH_AMBIGUOUS: self._handle_ambiguous_match(binding, count) elif match == self.MATCH_PARTIAL: @@ -246,7 +252,7 @@ class KeyParser(QObject): if time == 0: # execute immediately self._keystring = '' - self.execute(binding, count) + self.execute(binding, self.TYPE_CHAIN, count) else: # execute in `time' ms logging.debug("Scheduling execution of {} in {}ms".format(binding, @@ -271,7 +277,7 @@ class KeyParser(QObject): self._timer = None self._keystring = '' self.keystring_updated.emit(self._keystring) - self.execute(command, count) + self.execute(command, self.TYPE_CHAIN, count) def handle(self, e): """Handle a new keypress and call the respective handlers. @@ -322,11 +328,12 @@ class KeyParser(QObject): "Ignoring keychain \"{}\" in section \"{}\" because " "keychains are not supported there.".format(key, sectname)) - def execute(self, cmdstr, count=None): + 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 @@ -371,5 +378,5 @@ class CommandKeyParser(KeyParser): cmdstr)) message.set_cmd_text(':{} '.format(cmdstr)) - def execute(self, cmdstr, count=None): + def execute(self, cmdstr, _keytype, count=None): self._run_or_fill(cmdstr, count, ignore_exc=False) From 926194849c048a704a0885156f7f48d5e0925d6b Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 24 Apr 2014 23:47:02 +0200 Subject: [PATCH 48/54] Use normal commands for hint mode --- qutebrowser/app.py | 1 - qutebrowser/browser/curcommand.py | 5 ----- qutebrowser/browser/hints.py | 29 ++++++++++++++++------------- qutebrowser/config/configdata.py | 11 +++++++++++ qutebrowser/keyinput/hintmode.py | 31 ++++++++++--------------------- 5 files changed, 37 insertions(+), 40 deletions(-) diff --git a/qutebrowser/app.py b/qutebrowser/app.py index 188efbd81..6c4341b54 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -272,7 +272,6 @@ class QuteBrowser(QApplication): # hints kp["hint"].fire_hint.connect(tabs.cur.fire_hint) - kp["hint"].abort_hinting.connect(tabs.cur.abort_hinting) kp["hint"].keystring_updated.connect(tabs.cur.handle_hint_key) tabs.hint_strings_updated.connect(kp["hint"].on_hint_strings_updated) diff --git a/qutebrowser/browser/curcommand.py b/qutebrowser/browser/curcommand.py index c74704d42..e3eb9f325 100644 --- a/qutebrowser/browser/curcommand.py +++ b/qutebrowser/browser/curcommand.py @@ -207,11 +207,6 @@ class CurCommandDispatcher(QObject): """Fire a completed hint.""" self._tabs.currentWidget().hintmanager.fire(keystr) - @pyqtSlot() - def abort_hinting(self): - """Abort hinting.""" - self._tabs.currentWidget().hintmanager.stop() - @pyqtSlot(str, int) def search(self, text, flags): """Search for text in the current page. diff --git a/qutebrowser/browser/hints.py b/qutebrowser/browser/hints.py index 58f36e295..eb6f3e887 100644 --- a/qutebrowser/browser/hints.py +++ b/qutebrowser/browser/hints.py @@ -91,6 +91,7 @@ class HintManager(QObject): self._frame = None self._target = None self._baseurl = None + modes.manager.left.connect(self.on_mode_left) def _hint_strings(self, elems): """Calculate the hint strings for elems. @@ -302,18 +303,6 @@ class HintManager(QObject): self.hint_strings_updated.emit(strings) modes.enter("hint") - def stop(self): - """Stop hinting.""" - for elem in self._elems.values(): - elem.label.removeFromDocument() - self._frame.contentsSizeChanged.disconnect( - self.on_contents_size_changed) - self._elems = {} - self._target = None - self._frame = None - modes.leave("hint") - message.clear() - def handle_partial_key(self, keystr): """Handle a new partial keypress.""" delete = [] @@ -355,7 +344,7 @@ class HintManager(QObject): message.set_cmd_text(':{} {}'.format(commands[self._target], urlutils.urlstring(link))) if self._target != 'rapid': - self.stop() + modes.leave("hint") @pyqtSlot('QSize') def on_contents_size_changed(self, _size): @@ -365,3 +354,17 @@ class HintManager(QObject): css = self.HINT_CSS.format(left=rect.x(), top=rect.y(), config=config.instance) elems.label.setAttribute("style", css) + + @pyqtSlot(str) + def on_mode_left(self, mode): + """Stop hinting when hinting mode was left.""" + if mode != "hint": + return + for elem in self._elems.values(): + elem.label.removeFromDocument() + self._frame.contentsSizeChanged.disconnect( + self.on_contents_size_changed) + self._elems = {} + self._target = None + self._frame = None + message.clear() diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index cea2e9d92..b795316bd 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -83,6 +83,11 @@ SECTION_DESC = { "Since normal keypresses are passed through, only special keys are " "supported in this mode.\n" "An useful command to map here is the hidden command leave_mode."), + 'keybind.hint': ( + "Keybindings for hint mode.\n" + "Since normal keypresses are passed through, only special keys are " + "supported in this mode.\n" + "An useful command to map here is the hidden command leave_mode."), 'aliases': ( "Aliases for commands.\n" "By default, no aliases are defined. Example which adds a new command " @@ -431,6 +436,12 @@ DATA = OrderedDict([ ('', 'leave_mode'), )), + ('keybind.hint', sect.ValueList( + types.KeyBindingName(), types.KeyBinding(), + ('', 'leave_mode'), + ('', 'leave_mode'), + )), + ('aliases', sect.ValueList( types.Command(), types.Command(), )), diff --git a/qutebrowser/keyinput/hintmode.py b/qutebrowser/keyinput/hintmode.py index 60ff8f3bb..615c524b1 100644 --- a/qutebrowser/keyinput/hintmode.py +++ b/qutebrowser/keyinput/hintmode.py @@ -19,46 +19,35 @@ from PyQt5.QtCore import pyqtSignal, Qt -from qutebrowser.keyinput.keyparser import KeyParser +from qutebrowser.keyinput.keyparser import CommandKeyParser -class HintKeyParser(KeyParser): +class HintKeyParser(CommandKeyParser): """KeyChainParser for hints. Signals: fire_hint: When a hint keybinding was completed. Arg: the keystring/hint string pressed. - abort_hinting: Esc pressed, so abort hinting. """ fire_hint = pyqtSignal(str) - abort_hinting = pyqtSignal() def __init__(self, parent=None): super().__init__(parent, supports_count=False, supports_chains=True) + self.read_config('keybind.hint') - def _handle_special_key(self, e): - """Handle the escape key. - - FIXME make this more generic - - Emit: - abort_hinting: Emitted if hinting was aborted. - """ - if e.key() == Qt.Key_Escape: - self._keystring = '' - self.abort_hinting.emit() - return True - return False - - def execute(self, cmdstr, _count=None): + def execute(self, cmdstr, keytype, count=None): """Handle a completed keychain. Emit: - fire_hint: Always emitted. + fire_hint: Emitted if keytype is TYPE_CHAIN """ - self.fire_hint.emit(cmdstr) + if keytype == self.TYPE_CHAIN: + self.fire_hint.emit(cmdstr) + else: + # execute as command + super().execute(cmdstr, keytype, count) def on_hint_strings_updated(self, strings): """Handler for HintManager's hint_strings_updated. From 855fbaa05ff1f7f448db30a02a1c4b1939eb9c60 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 24 Apr 2014 23:53:36 +0200 Subject: [PATCH 49/54] Update TODO --- TODO | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/TODO b/TODO index 972c80736..61f86822c 100644 --- a/TODO +++ b/TODO @@ -1,12 +1,14 @@ keyparser foo ============= -- Handle keybind to get out of insert mode (e.g. esc) +- Add mode=[]/no_mode=[] to cmdutils.register so we can avoid executing + commands in the wrong mode - Add more element-selection-detection code (with options?) based on: -> javascript: http://stackoverflow.com/a/2848120/2085149 -> microFocusChanged and check active element via: frame = page.currentFrame() elem = frame.findFirstElement('*:focus') +- BUG: sometimes events arrive like 5 times... :( Bugs ==== From 84682f90fa0e64354cb3f1798203d66f92dd751a Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 25 Apr 2014 00:10:07 +0200 Subject: [PATCH 50/54] Log events in eventHandler --- qutebrowser/keyinput/modes.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/qutebrowser/keyinput/modes.py b/qutebrowser/keyinput/modes.py index c143efb8e..aa97d6de3 100644 --- a/qutebrowser/keyinput/modes.py +++ b/qutebrowser/keyinput/modes.py @@ -27,6 +27,7 @@ from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QEvent import qutebrowser.config.config as config import qutebrowser.commands.utils as cmdutils +import qutebrowser.utils.debug as debug manager = None @@ -181,7 +182,8 @@ class ModeManager(QObject): if typ not in [QEvent.KeyPress, QEvent.KeyRelease]: # We're not interested in non-key-events so we pass them through. return False - elif self.mode in self.passthrough: + logging.debug("Got event {} for {}".format(debug.EVENTS[typ], _obj)) + if self.mode in self.passthrough: # We're currently in a passthrough mode so we pass everything # through.*and* let the passthrough keyhandler know. # FIXME what if we leave the passthrough mode right here? From 8f9d7542a6372dc85c9b6a67cdb99f336c9ad129 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 25 Apr 2014 06:22:01 +0200 Subject: [PATCH 51/54] Fix eventFilter logic to not handle same event multiple times --- qutebrowser/keyinput/modes.py | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/qutebrowser/keyinput/modes.py b/qutebrowser/keyinput/modes.py index aa97d6de3..28606ee26 100644 --- a/qutebrowser/keyinput/modes.py +++ b/qutebrowser/keyinput/modes.py @@ -23,6 +23,7 @@ Module attributes: import logging +from PyQt5.QtGui import QWindow from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QEvent import qutebrowser.config.config as config @@ -166,7 +167,7 @@ class ModeManager(QObject): self._forward_unbound_keys = config.get('general', 'forward_unbound_keys') - def eventFilter(self, _obj, evt): + def eventFilter(self, obj, evt): """Filter all events based on the currently set mode. Also calls the real keypress handler. @@ -177,21 +178,29 @@ class ModeManager(QObject): if self.mode is None: # We got events before mode is set, so just pass them through. return False - handler = self._handlers[self.mode] typ = evt.type() if typ not in [QEvent.KeyPress, QEvent.KeyRelease]: # We're not interested in non-key-events so we pass them through. return False - logging.debug("Got event {} for {}".format(debug.EVENTS[typ], _obj)) + logging.debug("Got event {} for {}".format(debug.EVENTS[typ], + obj.__class__)) + 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 + + handler = self._handlers[self.mode] + if self.mode in self.passthrough: # We're currently in a passthrough mode so we pass everything # through.*and* let the passthrough keyhandler know. # FIXME what if we leave the passthrough mode right here? - self.key_pressed.emit(evt) - if handler is not None: - handler(evt) + if typ == QEvent.KeyPress: + self.key_pressed.emit(evt) + if handler is not None: + handler(evt) return False - elif typ == QEvent.KeyPress: + if typ == QEvent.KeyPress: # KeyPress in a non-passthrough mode - call handler and filter # event from widgets (unless unhandled and configured to pass # unhandled events through) @@ -205,6 +214,6 @@ class ModeManager(QObject): else: return True else: - # KeyRelease in a non-passthrough mode - filter event and ignore it - # entirely. + # KeyRelease in a non-passthrough mode - filter event and + # ignore it entirely. return True From 83f829ed9383ac4dc4808885762a0dbb615d97a5 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 25 Apr 2014 06:39:17 +0200 Subject: [PATCH 52/54] Add more logging to eventFilter --- qutebrowser/keyinput/modes.py | 48 ++++++++++++++++++++++------------- 1 file changed, 30 insertions(+), 18 deletions(-) diff --git a/qutebrowser/keyinput/modes.py b/qutebrowser/keyinput/modes.py index 28606ee26..e5a075e6d 100644 --- a/qutebrowser/keyinput/modes.py +++ b/qutebrowser/keyinput/modes.py @@ -182,38 +182,50 @@ class ModeManager(QObject): if typ not in [QEvent.KeyPress, QEvent.KeyRelease]: # We're not interested in non-key-events so we pass them through. return False - logging.debug("Got event {} for {}".format(debug.EVENTS[typ], - obj.__class__)) if not isinstance(obj, QWindow): # We already handled this same event at some point earlier, so # we're not interested in it anymore. + logging.debug("Got event {} for {} -> ignoring".format( + debug.EVENTS[typ], obj.__class__.__name__)) return False + logging.debug("Got event {} for {}".format( + debug.EVENTS[typ], obj.__class__.__name__)) + handler = self._handlers[self.mode] if self.mode in self.passthrough: # We're currently in a passthrough mode so we pass everything # through.*and* let the passthrough keyhandler know. # FIXME what if we leave the passthrough mode right here? + logging.debug("We're in a passthrough mode -> passing through") if typ == QEvent.KeyPress: + logging.debug("KeyPress, calling handler {}".format(handler)) self.key_pressed.emit(evt) if handler is not None: handler(evt) + else: + logging.debug("KeyRelease, not calling anything") return False - if typ == QEvent.KeyPress: - # KeyPress in a non-passthrough mode - call handler and filter - # event from widgets (unless unhandled and configured to pass - # unhandled events through) - self.key_pressed.emit(evt) - if handler is not None: - handled = handler(evt) - else: - handled = False - if handled or not self._forward_unbound_keys: - return True - else: - return True else: - # KeyRelease in a non-passthrough mode - filter event and - # ignore it entirely. - return True + logging.debug("We're in a non-passthrough mode") + if typ == QEvent.KeyPress: + # KeyPress in a non-passthrough mode - call handler and filter + # event from widgets (unless unhandled and configured to pass + # unhandled events through) + logging.debug("KeyPress, calling handler {}".format(handler)) + self.key_pressed.emit(evt) + handled = handler(evt) if handler is not None else False + logging.debug("handled: {}, forward_unbound_keys: {} -> " + "filtering: {}".format( + handled, self._forward_unbound_keys, + handled or not self._forward_unbound_keys)) + if handled or not self._forward_unbound_keys: + return True + else: + return False + else: + # KeyRelease in a non-passthrough mode - filter event and + # ignore it entirely. + logging.debug("KeyRelease, not calling anything and filtering") + return True From d07e22bd91593714a81e8e24463fb9ceb0c86192 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 25 Apr 2014 06:52:18 +0200 Subject: [PATCH 53/54] Remove setting forward_unbound_keys. It introduced a lot of unnecessary complexity (e.g. tracking KeyReleases to their KeyPresses...) for little benefit. --- qutebrowser/app.py | 2 +- qutebrowser/config/configdata.py | 4 ---- qutebrowser/keyinput/modes.py | 22 +++------------------- 3 files changed, 4 insertions(+), 24 deletions(-) diff --git a/qutebrowser/app.py b/qutebrowser/app.py index 6c4341b54..6fd8238e9 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -284,7 +284,7 @@ class QuteBrowser(QApplication): # config self.config.style_changed.connect(style.invalidate_caches) for obj in [tabs, completion, self.mainwindow, config.cmd_history, - websettings, kp["normal"], modes.manager]: + websettings, kp["normal"]]: self.config.changed.connect(obj.on_config_changed) # statusbar diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index b795316bd..0216b4892 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -186,10 +186,6 @@ DATA = OrderedDict([ SettingValue(types.Bool(), "true"), "Whether to automatically enter insert mode if an editable element " "is focused after page load."), - - ('forward_unbound_keys', - SettingValue(types.Bool(), "false"), - "Whether to forward unbound keys to the website in normal mode."), )), ('tabbar', sect.KeyValue( diff --git a/qutebrowser/keyinput/modes.py b/qutebrowser/keyinput/modes.py index e5a075e6d..6c2f966ee 100644 --- a/qutebrowser/keyinput/modes.py +++ b/qutebrowser/keyinput/modes.py @@ -75,7 +75,6 @@ class ModeManager(QObject): _handlers: A dictionary of modes and their handlers. _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. Signals: entered: Emitted when a mode is entered. @@ -94,8 +93,6 @@ class ModeManager(QObject): self._handlers = {} self.passthrough = [] self._mode_stack = [] - self._forward_unbound_keys = config.get('general', - 'forward_unbound_keys') @property def mode(self): @@ -160,13 +157,6 @@ class ModeManager(QObject): raise ValueError("Can't leave normal mode!") self.leave(self.mode) - @pyqtSlot(str, str) - def on_config_changed(self, section, option): - """Update local setting when config changed.""" - if (section, option) == ('general', 'forward_unbound_keys'): - self._forward_unbound_keys = config.get('general', - 'forward_unbound_keys') - def eventFilter(self, obj, evt): """Filter all events based on the currently set mode. @@ -213,17 +203,11 @@ class ModeManager(QObject): # KeyPress in a non-passthrough mode - call handler and filter # event from widgets (unless unhandled and configured to pass # unhandled events through) - logging.debug("KeyPress, calling handler {}".format(handler)) + logging.debug("KeyPress, calling handler {} and " + "filtering".format(handler)) self.key_pressed.emit(evt) handled = handler(evt) if handler is not None else False - logging.debug("handled: {}, forward_unbound_keys: {} -> " - "filtering: {}".format( - handled, self._forward_unbound_keys, - handled or not self._forward_unbound_keys)) - if handled or not self._forward_unbound_keys: - return True - else: - return False + return True else: # KeyRelease in a non-passthrough mode - filter event and # ignore it entirely. From ba1947f904592db92b7d666c88bf25f4cb0aee80 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 25 Apr 2014 06:57:11 +0200 Subject: [PATCH 54/54] Update TODO --- TODO | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/TODO b/TODO index 61f86822c..343d92179 100644 --- a/TODO +++ b/TODO @@ -1,15 +1,3 @@ -keyparser foo -============= - -- Add mode=[]/no_mode=[] to cmdutils.register so we can avoid executing - commands in the wrong mode -- Add more element-selection-detection code (with options?) based on: - -> javascript: http://stackoverflow.com/a/2848120/2085149 - -> microFocusChanged and check active element via: - frame = page.currentFrame() - elem = frame.findFirstElement('*:focus') -- BUG: sometimes events arrive like 5 times... :( - Bugs ==== @@ -73,6 +61,13 @@ Ctrl+A/X to increase/decrease last number in URL command completion gets hidden when doing a new ValueList value logging contexts catch import errors for PyQt and QtWebKit +- Add mode=[]/no_mode=[] to cmdutils.register so we can avoid executing + commands in the wrong mode +- Add more element-selection-detection code (with options?) based on: + -> javascript: http://stackoverflow.com/a/2848120/2085149 + -> microFocusChanged and check active element via: + frame = page.currentFrame() + elem = frame.findFirstElement('*:focus') Qt Bugs ========