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/TODO b/TODO index 9289e7c3a..e802345fb 100644 --- a/TODO +++ b/TODO @@ -14,7 +14,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... replace foo_bar options with foo-bar reorder options @@ -52,13 +51,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 @@ -89,6 +81,13 @@ How do we handle empty values in input bar? Human-readable error messages for unknown settings / wrong interpolations / ... auto-click on active hint click hints on enter +- 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 ======== diff --git a/qutebrowser/app.py b/qutebrowser/app.py index 694ca8a61..6fd8238e9 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -52,12 +52,14 @@ 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 from qutebrowser.widgets.mainwindow import MainWindow from qutebrowser.widgets.crash import CrashDialog -from qutebrowser.commands.keys 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 -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 @@ -83,7 +85,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 +94,6 @@ class QuteBrowser(QApplication): self._timers = [] self._opened_urls = [] self._shutting_down = False - self._mode = None sys.excepthook = self._exception_hook @@ -125,15 +125,24 @@ class QuteBrowser(QApplication): self.commandparser = CommandParser() self.searchparser = SearchParser() self._keyparsers = { - "normal": CommandKeyParser(self), - "hint": HintKeyParser(self), + 'normal': NormalKeyParser(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', 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) self._connect_signals() - self.set_mode("normal") + modes.enter("normal") self.mainwindow.show() self._python_hacks() @@ -246,13 +255,12 @@ 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 - tabs.keypress.connect(status.keypress) - for obj in [kp["normal"], tabs]: - obj.set_cmd_text.connect(cmd.set_cmd_text) + modes.manager.entered.connect(status.on_mode_entered) + modes.manager.left.connect(status.on_mode_left) + modes.manager.key_pressed.connect(status.on_key_pressed) # commands cmd.got_cmd.connect(self.commandparser.run) @@ -264,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) @@ -272,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) @@ -401,20 +409,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/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 00cbf8141..eb6f3e887 100644 --- a/qutebrowser/browser/hints.py +++ b/qutebrowser/browser/hints.py @@ -26,69 +26,20 @@ 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 -from qutebrowser.utils.keyparser import KeyParser +import qutebrowser.utils.webelem as webelem ElemTuple = namedtuple('ElemTuple', 'elem, label') -class HintKeyParser(KeyParser): - - """KeyParser 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_modifier_key(self, e): - """We don't support modifiers here, but we'll handle escape in here. - - 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. 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: @@ -104,34 +55,14 @@ 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 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. """ - 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]}; @@ -146,10 +77,8 @@ 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) def __init__(self, parent=None): """Constructor. @@ -162,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. @@ -312,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. @@ -353,27 +270,16 @@ class HintManager(QObject): Emit: hint_strings_updated: Emitted to update keypraser. - set_mode: Emitted to enter hinting mode """ 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 @@ -395,23 +301,7 @@ 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") - - def stop(self): - """Stop hinting. - - Emit: - set_mode: Emitted to leave hinting mode. - """ - 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 - self.set_mode.emit("normal") - message.clear() + modes.enter("hint") def handle_partial_key(self, keystr): """Handle a new partial keypress.""" @@ -451,9 +341,10 @@ 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() + modes.leave("hint") @pyqtSlot('QSize') def on_contents_size_changed(self, _size): @@ -463,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/commands/keys.py b/qutebrowser/commands/keys.py deleted file mode 100644 index 83727e6bd..000000000 --- a/qutebrowser/commands/keys.py +++ /dev/null @@ -1,123 +0,0 @@ -# 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 . - -"""Parse keypresses/keychains in the main window. - -Module attributes: - STARTCHARS: Possible chars for starting a commandline input. -""" - -import logging - -from PyQt5.QtCore import pyqtSignal, pyqtSlot - -import qutebrowser.config.config as config -from qutebrowser.utils.keyparser import KeyParser -from qutebrowser.commands.parsers import (CommandParser, ArgumentCountError, - NoSuchCommandError) - -STARTCHARS = ":/?" - - -class CommandKeyParser(KeyParser): - - """Keyparser for command bindings. - - Class attributes: - supports_count: If the keyparser should support counts. - - 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): - super().__init__(parent) - self.commandparser = CommandParser() - self.read_config() - - 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. - - 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) - except NoSuchCommandError: - pass - except ArgumentCountError: - logging.debug('Filling statusbar with partial command {}'.format( - cmdstr)) - self.set_cmd_text.emit(':{} '.format(cmdstr)) - - def _handle_single_key(self, e): - """Override _handle_single_key to abort if the key is a startchar. - - Args: - e: the KeyPressEvent from Qt. - - Emit: - set_cmd_text: If the keystring should be shown in the statusbar. - """ - 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) - - 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.strip('@')) - logging.debug('registered mod key: {} -> {}'.format(keystr, - cmd)) - self.modifier_bindings[keystr] = cmd - else: - logging.debug('registered key: {} -> {}'.format(key, cmd)) - self.bindings[key] = cmd diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index 808b61b18..0216b4892 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -68,16 +68,26 @@ 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 \";;\"."), + 'keybind.insert': ( + "Keybindings for insert 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."), + '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 " @@ -166,6 +176,16 @@ 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."), + + ('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( @@ -395,15 +415,27 @@ 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'), + )), + + ('keybind.insert', sect.ValueList( + types.KeyBindingName(), types.KeyBinding(), + ('', 'leave_mode'), + ('', 'leave_mode'), + )), + + ('keybind.hint', sect.ValueList( + types.KeyBindingName(), types.KeyBinding(), + ('', 'leave_mode'), + ('', 'leave_mode'), )), ('aliases', sect.ValueList( 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.""" diff --git a/qutebrowser/keyinput/hintmode.py b/qutebrowser/keyinput/hintmode.py new file mode 100644 index 000000000..615c524b1 --- /dev/null +++ b/qutebrowser/keyinput/hintmode.py @@ -0,0 +1,58 @@ +# 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 CommandKeyParser + + +class HintKeyParser(CommandKeyParser): + + """KeyChainParser for hints. + + Signals: + fire_hint: When a hint keybinding was completed. + Arg: the keystring/hint string pressed. + """ + + fire_hint = pyqtSignal(str) + + def __init__(self, parent=None): + super().__init__(parent, supports_count=False, supports_chains=True) + self.read_config('keybind.hint') + + def execute(self, cmdstr, keytype, count=None): + """Handle a completed keychain. + + Emit: + fire_hint: Emitted if keytype is TYPE_CHAIN + """ + 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. + + Args: + strings: A list of hint strings. + """ + self.bindings = {s: s for s in strings} diff --git a/qutebrowser/keyinput/insertmode.py b/qutebrowser/keyinput/insertmode.py new file mode 100644 index 000000000..6b57b3822 --- /dev/null +++ b/qutebrowser/keyinput/insertmode.py @@ -0,0 +1,30 @@ +# 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.""" + +import qutebrowser.keyinput.modes as modes +from qutebrowser.keyinput.keyparser import CommandKeyParser + + +class InsertKeyParser(CommandKeyParser): + + """KeyParser for insert mode.""" + + def __init__(self, parent=None): + super().__init__(parent, supports_chains=False) + self.read_config('keybind.insert') diff --git a/qutebrowser/utils/keyparser.py b/qutebrowser/keyinput/keyparser.py similarity index 62% rename from qutebrowser/utils/keyparser.py rename to qutebrowser/keyinput/keyparser.py index a8e3ae7b7..b9724c86e 100644 --- a/qutebrowser/utils/keyparser.py +++ b/qutebrowser/keyinput/keyparser.py @@ -21,15 +21,18 @@ 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 +import qutebrowser.utils.message as message +from qutebrowser.commands.parsers import (CommandParser, ArgumentCountError, + NoSuchCommandError) class KeyParser(QObject): - """Parser for vim-like key sequences. + """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. @@ -40,13 +43,18 @@ class KeyParser(QObject): 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. + + 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 (). _keystring: The currently entered key sequence _timer: QTimer for delayed execution. - bindings: Bound keybindings - modifier_bindings: Bound modifier bindings. + _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. @@ -60,18 +68,46 @@ class KeyParser(QObject): MATCH_AMBIGUOUS = 2 MATCH_NONE = 3 - supports_count = False + TYPE_CHAIN = 0 + TYPE_SPECIAL = 1 - def __init__(self, parent=None, bindings=None, modifier_bindings=None): + def __init__(self, parent=None, supports_count=None, + supports_chains=False): super().__init__(parent) self._timer = None + self._confsectname = None self._keystring = '' - self.bindings = {} if bindings is None else bindings - self.modifier_bindings = ({} if modifier_bindings is None - else modifier_bindings) + if supports_count is None: + supports_count = supports_chains + self._supports_count = supports_count + self._supports_chains = supports_chains + self.bindings = {} + self.special_bindings = {} - def _handle_modifier_key(self, e): - """Handle a new keypress with modifiers. + 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 (). Return True if the keypress has been handled, and False if not. @@ -92,19 +128,16 @@ 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 - self.execute(cmdstr) + return False + self.execute(cmdstr, self.TYPE_SPECIAL) return True def _handle_single_key(self, e): @@ -116,17 +149,20 @@ 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 - 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 @@ -135,13 +171,14 @@ 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) 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: @@ -151,6 +188,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. @@ -213,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, @@ -225,28 +264,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. @@ -260,11 +277,7 @@ class KeyParser(QObject): self._timer = None self._keystring = '' 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 + self.execute(command, self.TYPE_CHAIN, count) def handle(self, e): """Handle a new keypress and call the respective handlers. @@ -275,7 +288,95 @@ class KeyParser(QObject): Emit: keystring_updated: If a new keystring should be set. """ - handled = self._handle_modifier_key(e) - if not handled: - self._handle_single_key(e) - self.keystring_updated.emit(self._keystring) + 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): + """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 + 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)) + + def execute(self, cmdstr, keytype, count=None): + """Handle a completed keychain. + + Args: + cmdstr: The command to execute as a string. + keytype: TYPE_CHAIN or TYPE_SPECIAL + count: The count if given. + """ + raise NotImplementedError + + @pyqtSlot(str, 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 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_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, _keytype, count=None): + self._run_or_fill(cmdstr, count, ignore_exc=False) diff --git a/qutebrowser/keyinput/modes.py b/qutebrowser/keyinput/modes.py new file mode 100644 index 000000000..6c2f966ee --- /dev/null +++ b/qutebrowser/keyinput/modes.py @@ -0,0 +1,215 @@ +# 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.QtGui import QWindow +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 + + +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: + parent: Parent to use for ModeManager. + """ + global manager + manager = ModeManager(parent) + + +def enter(mode): + """Enter the mode 'mode'.""" + manager.enter(mode) + + +def leave(mode): + """Leave the mode '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. + + 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. + _mode_stack: A list of the modes we're currently in, with the active + one on the right. + + 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. + """ + + entered = pyqtSignal(str) + left = pyqtSignal(str) + key_pressed = pyqtSignal('QKeyEvent') + + def __init__(self, parent=None): + super().__init__(parent) + self._handlers = {} + self.passthrough = [] + 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. + + 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. + + Args: + mode; The name of the mode to enter. + + Emit: + entered: With the new mode name. + """ + 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) + + def leave(self, mode): + """Leave a mode. + + Args: + mode; The name of the mode to leave. + + Emit: + left: With the old mode name. + """ + try: + 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) + + # 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) + + def eventFilter(self, obj, evt): + """Filter all events based on the currently set mode. + + Also calls the real keypress handler. + + Emit: + key_pressed: When a key was actually pressed. + """ + if self.mode is None: + # We got events before mode is set, so just pass them through. + return False + 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 + 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 + else: + 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 {} and " + "filtering".format(handler)) + self.key_pressed.emit(evt) + handled = handler(evt) if handler is not None else False + return True + else: + # KeyRelease in a non-passthrough mode - filter event and + # ignore it entirely. + logging.debug("KeyRelease, not calling anything and filtering") + return True diff --git a/qutebrowser/keyinput/normalmode.py b/qutebrowser/keyinput/normalmode.py new file mode 100644 index 000000000..456d3b488 --- /dev/null +++ b/qutebrowser/keyinput/normalmode.py @@ -0,0 +1,51 @@ +# 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 . + +"""Parse keypresses/keychains in the main window. + +Module attributes: + STARTCHARS: Possible chars for starting a commandline input. +""" + +import logging + +import qutebrowser.utils.message as message +from qutebrowser.keyinput.keyparser import CommandKeyParser + +STARTCHARS = ":/?" + + +class NormalKeyParser(CommandKeyParser): + + def __init__(self, parent=None): + super().__init__(parent, supports_count=True, supports_chains=True) + self.read_config('keybind') + + def _handle_single_key(self, e): + """Override _handle_single_key to abort if the key is a startchar. + + Args: + e: the KeyPressEvent from Qt. + + 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): + 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..c5b9a8619 100644 --- a/qutebrowser/utils/message.py +++ b/qutebrowser/utils/message.py @@ -57,6 +57,11 @@ def clear(): bridge.text.emit('') +def set_cmd_text(txt): + """Set the statusbar command line to a preset text.""" + bridge.set_cmd_text.emit(txt) + + 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/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 e22032434..b3993fdfe 100644 --- a/qutebrowser/widgets/browsertab.py +++ b/qutebrowser/widgets/browsertab.py @@ -27,7 +27,9 @@ 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.webelem as webelem from qutebrowser.browser.webpage import BrowserPage from qutebrowser.browser.hints import HintManager from qutebrowser.utils.signals import SignalCache @@ -84,6 +86,8 @@ 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: modes.maybe_leave("insert")) + self.loadFinished.connect(self.on_load_finished) # FIXME find some way to hide scrollbars without setScrollBarPolicy def _init_neighborlist(self): @@ -109,6 +113,38 @@ class BrowserTab(QWebView): logging.debug("Everything destroyed, calling callback") self._shutdown_callback() + def _is_editable(self, hitresult): + """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 + if tag == 'object': + # Could be Flash/Java/..., could be image/audio/... + if not elem.hasAttribute("type"): + logging.debug(" without type clicked...") + return False + objtype = elem.attribute("type") + 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. @@ -209,6 +245,20 @@ class BrowserTab(QWebView): self.setFocus() QApplication.postEvent(self, evt) + @pyqtSlot(bool) + 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(): + modes.maybe_leave("insert") + else: + modes.enter("insert") + @pyqtSlot(str) def set_force_open_target(self, target): """Change the forced link target. Setter for _force_open_target. @@ -249,12 +299,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 +313,20 @@ 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 self._is_editable(hitresult): + logging.debug("Clicked editable element!") + modes.enter("insert") + else: + logging.debug("Clicked non-editable element!") + try: + modes.leave("insert") + except ValueError: + pass + if self._force_open_target is not None: self._open_target = self._force_open_target self._force_open_target = None diff --git a/qutebrowser/widgets/statusbar.py b/qutebrowser/widgets/statusbar.py index dbb9bb45d..91479f7e9 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 +import qutebrowser.keyinput.modes as modes +from qutebrowser.keyinput.normalmode import STARTCHARS from qutebrowser.config.style import set_register_stylesheet, get_stylesheet -import qutebrowser.commands.keys as keys from qutebrowser.utils.url import urlstring from qutebrowser.commands.parsers import split_cmdline from qutebrowser.models.cmdhistory import (History, HistoryEmptyError, @@ -157,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: @@ -169,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 modes.manager.passthrough: + self.txt.normaltext = "-- {} MODE --".format(mode.upper()) + + @pyqtSlot(str) + def on_mode_left(self, mode): + """Clear marked mode.""" + if mode in modes.manager.passthrough: + self.txt.normaltext = "" + def resizeEvent(self, e): """Extend resizeEvent of QWidget to emit a resized signal afterwards. @@ -331,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: @@ -342,8 +355,18 @@ class _Command(QLineEdit): self.setFocus() self.show_cmd.emit() + def focusInEvent(self, e): + """Extend focusInEvent to enter command mode.""" + modes.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 +375,7 @@ class _Command(QLineEdit): clear_completion_selection: Always emitted. hide_completion: Always emitted so the completion is hidden. """ + modes.leave("command") if e.reason() in [Qt.MouseFocusReason, Qt.TabFocusReason, Qt.BacktabFocusReason, Qt.OtherFocusReason]: self.setText('') @@ -376,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) diff --git a/qutebrowser/widgets/tabbedbrowser.py b/qutebrowser/widgets/tabbedbrowser.py index ddd5460c4..f8c926bca 100644 --- a/qutebrowser/widgets/tabbedbrowser.py +++ b/qutebrowser/widgets/tabbedbrowser.py @@ -69,10 +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. quit: The last tab was closed, quit application. resized: Emitted when the browser window has resized, so the completion @@ -88,9 +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) - set_mode = pyqtSignal(str) - keypress = pyqtSignal('QKeyEvent') shutdown_complete = pyqtSignal() quit = pyqtSignal() resized = pyqtSignal('QRect') @@ -143,8 +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_mode.connect(self.set_mode) - tab.hintmanager.set_cmd_text.connect(self.set_cmd_text) # misc tab.titleChanged.connect(self.on_title_changed) tab.open_tab.connect(self.tabopen) @@ -242,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): @@ -360,18 +343,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.