From 6e78f67a8160fc43344466f0915b5ce6e481e8ec Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 25 Apr 2014 12:21:01 +0200 Subject: [PATCH] Rename keyinput stuff --- qutebrowser/app.py | 39 +- qutebrowser/browser/hints.py | 8 +- qutebrowser/commands/_command.py | 2 +- qutebrowser/keyinput/_basekeyparser.py | 345 +++++++++++++ qutebrowser/keyinput/keyparser.py | 476 +++--------------- qutebrowser/keyinput/{modes.py => modeman.py} | 0 .../keyinput/{hintmode.py => modeparsers.py} | 148 +++--- qutebrowser/keyinput/normalmode.py | 51 -- qutebrowser/widgets/_statusbar.py | 12 +- qutebrowser/widgets/browsertab.py | 12 +- 10 files changed, 548 insertions(+), 545 deletions(-) create mode 100644 qutebrowser/keyinput/_basekeyparser.py rename qutebrowser/keyinput/{modes.py => modeman.py} (100%) rename qutebrowser/keyinput/{hintmode.py => modeparsers.py} (64%) delete mode 100644 qutebrowser/keyinput/normalmode.py diff --git a/qutebrowser/app.py b/qutebrowser/app.py index c9feb9cc4..b94725203 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -52,13 +52,12 @@ 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.keyinput.modeman as modeman import qutebrowser.utils.message as message from qutebrowser.widgets.mainwindow import MainWindow from qutebrowser.widgets.crash import CrashDialog -from qutebrowser.keyinput.normalmode import NormalKeyParser +from qutebrowser.keyinput.modeparsers import NormalKeyParser, HintKeyParser from qutebrowser.keyinput.keyparser import PassthroughKeyParser -from qutebrowser.keyinput.hintmode import HintKeyParser from qutebrowser.commands.managers import CommandManager, SearchManager from qutebrowser.utils.appdirs import AppDirs from qutebrowser.utils.misc import dotted_getattr @@ -132,22 +131,22 @@ class QuteBrowser(QApplication): } 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('passthrough', + modeman.init(self) + modeman.manager.register('normal', self._keyparsers['normal'].handle) + modeman.manager.register('hint', self._keyparsers['hint'].handle) + modeman.manager.register('insert', self._keyparsers['insert'].handle, + passthrough=True) + modeman.manager.register('passthrough', self._keyparsers['passthrough'].handle, passthrough=True) - modes.manager.register('command', self._keyparsers['command'].handle, - passthrough=True) - self.modeman = modes.manager # for commands - self.installEventFilter(modes.manager) + modeman.manager.register('command', self._keyparsers['command'].handle, + passthrough=True) + self.modeman = modeman.manager # for commands + self.installEventFilter(modeman.manager) self.setQuitOnLastWindowClosed(False) self._connect_signals() - modes.enter("normal") + modeman.enter("normal") self.mainwindow.show() self._python_hacks() @@ -263,10 +262,10 @@ class QuteBrowser(QApplication): tabs.currentChanged.connect(self.mainwindow.update_inspector) # status bar - modes.manager.entered.connect(status.on_mode_entered) - modes.manager.left.connect(status.on_mode_left) - modes.manager.left.connect(status.cmd.on_mode_left) - modes.manager.key_pressed.connect(status.on_key_pressed) + modeman.manager.entered.connect(status.on_mode_entered) + modeman.manager.left.connect(status.on_mode_left) + modeman.manager.left.connect(status.cmd.on_mode_left) + modeman.manager.key_pressed.connect(status.on_key_pressed) # commands cmd.got_cmd.connect(self.commandmanager.run) @@ -290,7 +289,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"], modeman.manager]: self.config.changed.connect(obj.on_config_changed) # statusbar @@ -304,7 +303,7 @@ class QuteBrowser(QApplication): tabs.cur_link_hovered.connect(status.url.set_hover_url) # command input / completion - modes.manager.left.connect(tabs.on_mode_left) + modeman.manager.left.connect(tabs.on_mode_left) cmd.clear_completion_selection.connect( completion.on_clear_completion_selection) cmd.hide_completion.connect(completion.hide) diff --git a/qutebrowser/browser/hints.py b/qutebrowser/browser/hints.py index eb6f3e887..58f7a97dd 100644 --- a/qutebrowser/browser/hints.py +++ b/qutebrowser/browser/hints.py @@ -26,7 +26,7 @@ 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.keyinput.modeman as modeman import qutebrowser.utils.message as message import qutebrowser.utils.url as urlutils import qutebrowser.utils.webelem as webelem @@ -91,7 +91,7 @@ class HintManager(QObject): self._frame = None self._target = None self._baseurl = None - modes.manager.left.connect(self.on_mode_left) + modeman.manager.left.connect(self.on_mode_left) def _hint_strings(self, elems): """Calculate the hint strings for elems. @@ -301,7 +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) - modes.enter("hint") + modeman.enter("hint") def handle_partial_key(self, keystr): """Handle a new partial keypress.""" @@ -344,7 +344,7 @@ class HintManager(QObject): message.set_cmd_text(':{} {}'.format(commands[self._target], urlutils.urlstring(link))) if self._target != 'rapid': - modes.leave("hint") + modeman.leave("hint") @pyqtSlot('QSize') def on_contents_size_changed(self, _size): diff --git a/qutebrowser/commands/_command.py b/qutebrowser/commands/_command.py index 5fc000852..7e7251459 100644 --- a/qutebrowser/commands/_command.py +++ b/qutebrowser/commands/_command.py @@ -82,7 +82,7 @@ class Command(QObject): ArgumentCountError if the argument count is wrong. InvalidModeError if the command can't be called in this mode. """ - import qutebrowser.keyinput.modes as modeman + import qutebrowser.keyinput.modeman as modeman if self.modes is not None and modeman.manager.mode not in self.modes: raise InvalidModeError("This command is only allowed in {} " "mode.".format('/'.join(self.modes))) diff --git a/qutebrowser/keyinput/_basekeyparser.py b/qutebrowser/keyinput/_basekeyparser.py new file mode 100644 index 000000000..a4664d4dc --- /dev/null +++ b/qutebrowser/keyinput/_basekeyparser.py @@ -0,0 +1,345 @@ +# 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 . + +"""Base class for vim-like keysequence parser.""" + +import re +import logging +from functools import partial + +from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QObject, QTimer +from PyQt5.QtGui import QKeySequence + +import qutebrowser.config.config as config + + +class BaseKeyParser(QObject): + + """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). + + 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. + _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. + """ + + keystring_updated = pyqtSignal(str) + + MATCH_PARTIAL = 0 + MATCH_DEFINITIVE = 1 + 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) + 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.bindings = {} + self.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 (). + + Return True if the keypress has been handled, and False if not. + + Args: + e: the KeyPressEvent from Qt. + + Return: + True if event has been handled, False otherwise. + """ + modmask2str = { + Qt.ControlModifier: 'Ctrl', + Qt.AltModifier: 'Alt', + Qt.MetaModifier: 'Meta', + Qt.ShiftModifier: 'Shift' + } + if e.key() in [Qt.Key_Control, Qt.Key_Alt, Qt.Key_Shift, Qt.Key_Meta]: + # Only modifier pressed + return False + mod = e.modifiers() + modstr = '' + for (mask, s) in modmask2str.items(): + if mod & mask: + modstr += s + '+' + keystr = QKeySequence(e.key()).toString().replace("Backtab", "Tab") + try: + cmdstr = self.special_bindings[modstr + keystr] + except KeyError: + logging.debug('No binding found for {}.'.format(modstr + keystr)) + return False + self.execute(cmdstr, self.TYPE_SPECIAL) + return True + + def _handle_single_key(self, e): + """Handle a new keypress with a single key (no modifiers). + + Separate the keypress into count/command, then check if it matches + any possible command, and either run the command, ignore it, or + display an error. + + 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 False + + self._stop_delayed_exec() + self._keystring += txt + + if self._supports_count: + (countstr, cmd_input) = re.match(r'^(\d*)(.*)', + self._keystring).groups() + count = int(countstr) if countstr else None + else: + cmd_input = self._keystring + count = None + + if not cmd_input: + # 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, self.TYPE_CHAIN, count) + elif match == self.MATCH_AMBIGUOUS: + self._handle_ambiguous_match(binding, count) + elif match == self.MATCH_PARTIAL: + logging.debug('No match for "{}" (added {})'.format( + self._keystring, txt)) + elif match == self.MATCH_NONE: + 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. + + Args: + cmd_input: The command string to find. + + Return: + A tuple (matchtype, binding). + matchtype: MATCH_DEFINITIVE, MATCH_AMBIGUOUS, MATCH_PARTIAL or + MATCH_NONE + binding: - None with MATCH_PARTIAL/MATCH_NONE + - The found binding with MATCH_DEFINITIVE/ + MATCH_AMBIGUOUS + """ + # A (cmd_input, binding) tuple (k, v of bindings) or None. + definitive_match = None + partial_match = False + # Check definitive match + try: + definitive_match = (cmd_input, self.bindings[cmd_input]) + except KeyError: + pass + # Check partial match + for binding in self.bindings: + if definitive_match is not None and binding == definitive_match[0]: + # We already matched that one + continue + if len(binding) < len(cmd_input): + # binding is shorter than cmd_input, so it can't possibly match + continue + elif cmd_input[-1] == binding[len(cmd_input) - 1]: + partial_match = True + break + if definitive_match is not None and partial_match: + return (self.MATCH_AMBIGUOUS, definitive_match[1]) + elif definitive_match is not None: + return (self.MATCH_DEFINITIVE, definitive_match[1]) + elif partial_match: + return (self.MATCH_PARTIAL, None) + else: + return (self.MATCH_NONE, None) + + def _stop_delayed_exec(self): + """Stop a delayed execution if any is running.""" + if self._timer is not None: + logging.debug("Stopping delayed execution.") + self._timer.stop() + self._timer = None + + def _handle_ambiguous_match(self, binding, count): + """Handle an ambiguous match. + + Args: + binding: The command-string to execute. + count: The count to pass. + """ + logging.debug("Ambiguous match for \"{}\"".format(self._keystring)) + time = config.get('general', 'cmd_timeout') + if time == 0: + # execute immediately + self._keystring = '' + self.execute(binding, self.TYPE_CHAIN, count) + else: + # execute in `time' ms + logging.debug("Scheduling execution of {} in {}ms".format(binding, + time)) + self._timer = QTimer(self) + self._timer.setSingleShot(True) + self._timer.setInterval(time) + self._timer.timeout.connect(partial(self.delayed_exec, binding, + count)) + self._timer.start() + + def delayed_exec(self, command, count): + """Execute a delayed command. + + Args: + command/count: As if passed to self.execute() + + Emit: + keystring_updated to do a delayed update. + """ + logging.debug("Executing delayed command now!") + self._timer = None + self._keystring = '' + self.keystring_updated.emit(self._keystring) + self.execute(command, self.TYPE_CHAIN, count) + + def handle(self, e): + """Handle a new keypress and call the respective handlers. + + Args: + e: the KeyPressEvent from Qt + + Emit: + keystring_updated: If a new keystring should be set. + """ + 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() diff --git a/qutebrowser/keyinput/keyparser.py b/qutebrowser/keyinput/keyparser.py index f21b98841..5c7e7ee7b 100644 --- a/qutebrowser/keyinput/keyparser.py +++ b/qutebrowser/keyinput/keyparser.py @@ -1,399 +1,77 @@ -# 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 . - -"""Base class for vim-like keysequence parser.""" - -import re -import logging -from functools import partial - -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.managers import (CommandManager, ArgumentCountError, - NoSuchCommandError) - - -class KeyParser(QObject): - - """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). - - 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. - _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. - """ - - keystring_updated = pyqtSignal(str) - - MATCH_PARTIAL = 0 - MATCH_DEFINITIVE = 1 - 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) - 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.bindings = {} - self.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 (). - - Return True if the keypress has been handled, and False if not. - - Args: - e: the KeyPressEvent from Qt. - - Return: - True if event has been handled, False otherwise. - """ - modmask2str = { - Qt.ControlModifier: 'Ctrl', - Qt.AltModifier: 'Alt', - Qt.MetaModifier: 'Meta', - Qt.ShiftModifier: 'Shift' - } - if e.key() in [Qt.Key_Control, Qt.Key_Alt, Qt.Key_Shift, Qt.Key_Meta]: - # Only modifier pressed - return False - mod = e.modifiers() - modstr = '' - for (mask, s) in modmask2str.items(): - if mod & mask: - modstr += s + '+' - keystr = QKeySequence(e.key()).toString().replace("Backtab", "Tab") - try: - cmdstr = self.special_bindings[modstr + keystr] - except KeyError: - logging.debug('No binding found for {}.'.format(modstr + keystr)) - return False - self.execute(cmdstr, self.TYPE_SPECIAL) - return True - - def _handle_single_key(self, e): - """Handle a new keypress with a single key (no modifiers). - - Separate the keypress into count/command, then check if it matches - any possible command, and either run the command, ignore it, or - display an error. - - 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 False - - self._stop_delayed_exec() - self._keystring += txt - - if self._supports_count: - (countstr, cmd_input) = re.match(r'^(\d*)(.*)', - self._keystring).groups() - count = int(countstr) if countstr else None - else: - cmd_input = self._keystring - count = None - - if not cmd_input: - # 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, self.TYPE_CHAIN, count) - elif match == self.MATCH_AMBIGUOUS: - self._handle_ambiguous_match(binding, count) - elif match == self.MATCH_PARTIAL: - logging.debug('No match for "{}" (added {})'.format( - self._keystring, txt)) - elif match == self.MATCH_NONE: - 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. - - Args: - cmd_input: The command string to find. - - Return: - A tuple (matchtype, binding). - matchtype: MATCH_DEFINITIVE, MATCH_AMBIGUOUS, MATCH_PARTIAL or - MATCH_NONE - binding: - None with MATCH_PARTIAL/MATCH_NONE - - The found binding with MATCH_DEFINITIVE/ - MATCH_AMBIGUOUS - """ - # A (cmd_input, binding) tuple (k, v of bindings) or None. - definitive_match = None - partial_match = False - # Check definitive match - try: - definitive_match = (cmd_input, self.bindings[cmd_input]) - except KeyError: - pass - # Check partial match - for binding in self.bindings: - if definitive_match is not None and binding == definitive_match[0]: - # We already matched that one - continue - if len(binding) < len(cmd_input): - # binding is shorter than cmd_input, so it can't possibly match - continue - elif cmd_input[-1] == binding[len(cmd_input) - 1]: - partial_match = True - break - if definitive_match is not None and partial_match: - return (self.MATCH_AMBIGUOUS, definitive_match[1]) - elif definitive_match is not None: - return (self.MATCH_DEFINITIVE, definitive_match[1]) - elif partial_match: - return (self.MATCH_PARTIAL, None) - else: - return (self.MATCH_NONE, None) - - def _stop_delayed_exec(self): - """Stop a delayed execution if any is running.""" - if self._timer is not None: - logging.debug("Stopping delayed execution.") - self._timer.stop() - self._timer = None - - def _handle_ambiguous_match(self, binding, count): - """Handle an ambiguous match. - - Args: - binding: The command-string to execute. - count: The count to pass. - """ - logging.debug("Ambiguous match for \"{}\"".format(self._keystring)) - time = config.get('general', 'cmd_timeout') - if time == 0: - # execute immediately - self._keystring = '' - self.execute(binding, self.TYPE_CHAIN, count) - else: - # execute in `time' ms - logging.debug("Scheduling execution of {} in {}ms".format(binding, - time)) - self._timer = QTimer(self) - self._timer.setSingleShot(True) - self._timer.setInterval(time) - self._timer.timeout.connect(partial(self.delayed_exec, binding, - count)) - self._timer.start() - - def delayed_exec(self, command, count): - """Execute a delayed command. - - Args: - command/count: As if passed to self.execute() - - Emit: - keystring_updated to do a delayed update. - """ - logging.debug("Executing delayed command now!") - self._timer = None - self._keystring = '' - self.keystring_updated.emit(self._keystring) - self.execute(command, self.TYPE_CHAIN, count) - - def handle(self, e): - """Handle a new keypress and call the respective handlers. - - Args: - e: the KeyPressEvent from Qt - - Emit: - keystring_updated: If a new keystring should be set. - """ - 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: - commandmanager: CommandManager instance. - """ - - def __init__(self, parent=None, supports_count=None, - supports_chains=False): - super().__init__(parent, supports_count, supports_chains) - self.commandmanager = CommandManager() - - 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.commandmanager.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) - - -class PassthroughKeyParser(CommandKeyParser): - - """KeyChainParser which passes through normal keys. - - Used for insert/passthrough modes. - """ - - def __init__(self, confsect, parent=None): - """Constructor. - - Args: - confsect: The config section to use. - """ - super().__init__(parent, supports_chains=False) - self.read_config(confsect) +# 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 . + +"""Advanced keyparsers.""" + +import logging + +from qutebrowser.keyinput._basekeyparser import BaseKeyParser +import qutebrowser.utils.message as message + +from qutebrowser.commands.managers import (CommandManager, ArgumentCountError, + NoSuchCommandError) + + +class CommandKeyParser(BaseKeyParser): + + """KeyChainParser for command bindings. + + Attributes: + commandmanager: CommandManager instance. + """ + + def __init__(self, parent=None, supports_count=None, + supports_chains=False): + super().__init__(parent, supports_count, supports_chains) + self.commandmanager = CommandManager() + + 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.commandmanager.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) + + +class PassthroughKeyParser(CommandKeyParser): + + """KeyChainParser which passes through normal keys. + + Used for insert/passthrough modes. + """ + + def __init__(self, confsect, parent=None): + """Constructor. + + Args: + confsect: The config section to use. + """ + super().__init__(parent, supports_chains=False) + self.read_config(confsect) diff --git a/qutebrowser/keyinput/modes.py b/qutebrowser/keyinput/modeman.py similarity index 100% rename from qutebrowser/keyinput/modes.py rename to qutebrowser/keyinput/modeman.py diff --git a/qutebrowser/keyinput/hintmode.py b/qutebrowser/keyinput/modeparsers.py similarity index 64% rename from qutebrowser/keyinput/hintmode.py rename to qutebrowser/keyinput/modeparsers.py index e83780a92..709d81622 100644 --- a/qutebrowser/keyinput/hintmode.py +++ b/qutebrowser/keyinput/modeparsers.py @@ -1,58 +1,90 @@ -# 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 - -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} +# 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" and "normal" modes. + +Module attributes: + STARTCHARS: Possible chars for starting a commandline input. +""" + +from PyQt5.QtCore import pyqtSignal + +import qutebrowser.utils.message as message +from qutebrowser.keyinput.keyparser import CommandKeyParser + + +STARTCHARS = ":/?" + + +class NormalKeyParser(CommandKeyParser): + + """KeyParser for normalmode with added STARTCHARS detection.""" + + 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) + + +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/normalmode.py b/qutebrowser/keyinput/normalmode.py deleted file mode 100644 index f0bf8317b..000000000 --- a/qutebrowser/keyinput/normalmode.py +++ /dev/null @@ -1,51 +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 qutebrowser.utils.message as message -from qutebrowser.keyinput.keyparser import CommandKeyParser - -STARTCHARS = ":/?" - - -class NormalKeyParser(CommandKeyParser): - - """KeyParser for normalmode with added STARTCHARS detection.""" - - 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/widgets/_statusbar.py b/qutebrowser/widgets/_statusbar.py index db018f178..aed35d1e4 100644 --- a/qutebrowser/widgets/_statusbar.py +++ b/qutebrowser/widgets/_statusbar.py @@ -22,9 +22,9 @@ from PyQt5.QtWidgets import (QWidget, QLineEdit, QProgressBar, QLabel, QHBoxLayout, QStackedLayout, QSizePolicy) from PyQt5.QtGui import QPainter, QValidator -import qutebrowser.keyinput.modes as modes +import qutebrowser.keyinput.modeman as modeman import qutebrowser.commands.utils as cmdutils -from qutebrowser.keyinput.normalmode import STARTCHARS +from qutebrowser.keyinput.modeparsers import STARTCHARS from qutebrowser.config.style import set_register_stylesheet, get_stylesheet from qutebrowser.utils.url import urlstring from qutebrowser.commands.managers 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 modes.manager.passthrough: + if mode in modeman.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: + if mode in modeman.manager.passthrough: self.txt.normaltext = "" def resizeEvent(self, e): @@ -333,7 +333,7 @@ class _Command(QLineEdit): } text = self.text() self.history.append(text) - modes.leave("command") + modeman.leave("command") if text[0] in signals: signals[text[0]].emit(text.lstrip(text[0])) @@ -360,7 +360,7 @@ class _Command(QLineEdit): def focusInEvent(self, e): """Extend focusInEvent to enter command mode.""" - modes.enter("command") + modeman.enter("command") super().focusInEvent(e) diff --git a/qutebrowser/widgets/browsertab.py b/qutebrowser/widgets/browsertab.py index b3993fdfe..aa7f99607 100644 --- a/qutebrowser/widgets/browsertab.py +++ b/qutebrowser/widgets/browsertab.py @@ -27,7 +27,7 @@ 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.keyinput.modeman as modeman import qutebrowser.utils.message as message import qutebrowser.utils.webelem as webelem from qutebrowser.browser.webpage import BrowserPage @@ -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: modes.maybe_leave("insert")) + self.loadStarted.connect(lambda: modeman.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(): - modes.maybe_leave("insert") + modeman.maybe_leave("insert") else: - modes.enter("insert") + modeman.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!") - modes.enter("insert") + modeman.enter("insert") else: logging.debug("Clicked non-editable element!") try: - modes.leave("insert") + modeman.leave("insert") except ValueError: pass