# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: # Copyright 2014-2016 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 pyqtSlot, Qt from qutebrowser.utils import message from qutebrowser.config import config from qutebrowser.keyinput import keyparser from qutebrowser.utils import usertypes, log, objreg, utils STARTCHARS = ":/?" LastPress = usertypes.enum('LastPress', ['none', 'filtertext', 'keystring']) class NormalKeyParser(keyparser.CommandKeyParser): """KeyParser for normal mode with added STARTCHARS detection and more. Attributes: _partial_timer: Timer to clear partial keypresses. """ def __init__(self, win_id, parent=None): super().__init__(win_id, parent, supports_count=True, supports_chains=True) self.read_config('normal') self._partial_timer = usertypes.Timer(self, 'partial-match') self._partial_timer.setSingleShot(True) def __repr__(self): return utils.get_repr(self) 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: A self.Match member. """ txt = e.text().strip() if not self._keystring and any(txt == c for c in STARTCHARS): message.set_cmd_text(self._win_id, txt) return self.Match.definitive match = super()._handle_single_key(e) if match == self.Match.partial: timeout = config.get('input', 'partial-timeout') if timeout != 0: self._partial_timer.setInterval(timeout) self._partial_timer.timeout.connect(self._clear_partial_match) self._partial_timer.start() return match @pyqtSlot() def _clear_partial_match(self): """Clear a partial keystring after a timeout.""" self._debug_log("Clearing partial keystring {}".format( self._keystring)) self._keystring = '' self.keystring_updated.emit(self._keystring) @pyqtSlot() def _stop_timers(self): super()._stop_timers() self._partial_timer.stop() try: self._partial_timer.timeout.disconnect(self._clear_partial_match) except TypeError: # no connections pass class PromptKeyParser(keyparser.CommandKeyParser): """KeyParser for yes/no prompts.""" def __init__(self, win_id, parent=None): super().__init__(win_id, parent, supports_count=False, supports_chains=True) # We don't want an extra section for this in the config, so we just # abuse the prompt section. self.read_config('prompt') def __repr__(self): return utils.get_repr(self) class HintKeyParser(keyparser.CommandKeyParser): """KeyChainParser for hints. Attributes: _filtertext: The text to filter with. _last_press: The nature of the last keypress, a LastPress member. """ def __init__(self, win_id, parent=None): super().__init__(win_id, parent, supports_count=False, supports_chains=True) self._filtertext = '' self._last_press = LastPress.none self.read_config('hint') self.keystring_updated.connect(self.on_keystring_updated) def _handle_special_key(self, e): """Override _handle_special_key to handle string filtering. 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. """ log.keyboard.debug("Got special key 0x{:x} text {}".format( e.key(), e.text())) hintmanager = objreg.get('hintmanager', scope='tab', window=self._win_id, tab='current') if e.key() == Qt.Key_Backspace: log.keyboard.debug("Got backspace, mode {}, filtertext '{}', " "keystring '{}'".format(self._last_press, self._filtertext, self._keystring)) if self._last_press == LastPress.filtertext and self._filtertext: self._filtertext = self._filtertext[:-1] hintmanager.filter_hints(self._filtertext) return True elif self._last_press == LastPress.keystring and self._keystring: self._keystring = self._keystring[:-1] self.keystring_updated.emit(self._keystring) return True else: return super()._handle_special_key(e) elif config.get('hints', 'mode') != 'number': return super()._handle_special_key(e) elif not e.text(): return super()._handle_special_key(e) else: self._filtertext += e.text() hintmanager.filter_hints(self._filtertext) self._last_press = LastPress.filtertext return True def handle(self, e): """Handle a new keypress and call the respective handlers. Args: e: the KeyPressEvent from Qt Returns: True if the match has been handled, False otherwise. """ match = self._handle_single_key(e) if match == self.Match.partial: self.keystring_updated.emit(self._keystring) self._last_press = LastPress.keystring return True elif match == self.Match.definitive: self._last_press = LastPress.none return True elif match == self.Match.other: pass elif match == self.Match.none: # We couldn't find a keychain so we check if it's a special key. return self._handle_special_key(e) else: raise ValueError("Got invalid match type {}!".format(match)) def execute(self, cmdstr, keytype, count=None): """Handle a completed keychain.""" if not isinstance(keytype, self.Type): raise TypeError("Type {} is no Type member!".format(keytype)) if keytype == self.Type.chain: hintmanager = objreg.get('hintmanager', scope='tab', window=self._win_id, tab='current') hintmanager.fire(cmdstr) else: # execute as command super().execute(cmdstr, keytype, count) def update_bindings(self, strings): """Update bindings when the hint strings changed. Args: strings: A list of hint strings. """ self.bindings = {s: s for s in strings} self._filtertext = '' @pyqtSlot(str) def on_keystring_updated(self, keystr): """Update hintmanager when the keystring was updated.""" hintmanager = objreg.get('hintmanager', scope='tab', window=self._win_id, tab='current') hintmanager.handle_partial_key(keystr) class CaretKeyParser(keyparser.CommandKeyParser): """KeyParser for caret mode.""" passthrough = True def __init__(self, win_id, parent=None): super().__init__(win_id, parent, supports_count=True, supports_chains=True) self.read_config('caret') class MarkKeyParser(keyparser.BaseKeyParser): """KeyParser for set_mark and jump_mark mode. Attributes: _mode: Either KeyMode.set_mark or KeyMode.jump_mark. """ def __init__(self, win_id, mode, parent=None): super().__init__(win_id, parent, supports_count=False, supports_chains=False) self._mode = mode def handle(self, e): """Override handle to always match the next key and create a mark. Args: e: the KeyPressEvent from Qt. Return: True if event has been handled, False otherwise. """ if utils.keyevent_to_string(e) is None: # this is a modifier key, let it pass and keep going return True key = e.text() if not key.isalpha() and key != "'": # only valid mark names are [a-zA-Z'] message.error(self._win_id, key + " isn't a valid mark") return True tabbed_browser = objreg.get('tabbed-browser', scope='window', window=self._win_id) if self._mode == usertypes.KeyMode.set_mark: tabbed_browser.set_mark(key) elif self._mode == usertypes.KeyMode.jump_mark: tabbed_browser.jump_mark(key) else: raise ValueError("{} is not a valid mark mode".format(self._mode)) self.request_leave.emit(self._mode, "valid mark key") return True def on_keyconfig_changed(self, mode): """MarkKeyParser has no config section (no bindable keys).""" pass def execute(self, cmdstr, _keytype, count=None): """Should never be called on MarkKeyParser.""" raise NotImplementedError