diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index 0133e14ba..ebc215f3a 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -355,6 +355,10 @@ def data(readonly=False): "Hide the window decoration when using wayland " "(requires restart)"), + ('show-keyhints', + SettingValue(typ.Bool(), 'true'), + "Show possible keychains based on the current keystring"), + readonly=readonly )), @@ -1196,6 +1200,18 @@ def data(readonly=False): "Background color for webpages if unset (or empty to use the " "theme's color)"), + ('keyhint.fg', + SettingValue(typ.QssColor(), '#FFFFFF'), + "Text color for the keyhint widget."), + + ('keyhint.fg.suffix', + SettingValue(typ.QssColor(), '#FFFF00'), + "Highlight color for keys to complete the current keychain"), + + ('keyhint.bg', + SettingValue(typ.QssColor(), 'rgba(0, 0, 0, 80%)'), + "Background color of the keyhint widget."), + readonly=readonly )), @@ -1277,6 +1293,10 @@ def data(readonly=False): typ.Int(none_ok=True, minval=1, maxval=MAXVALS['int']), ''), "The default font size for fixed-pitch text."), + ('keyhint', + SettingValue(typ.Font(), DEFAULT_FONT_SIZE + ' ${_monospace}'), + "Font used in the keyhint widget."), + readonly=readonly )), ]) diff --git a/qutebrowser/mainwindow/mainwindow.py b/qutebrowser/mainwindow/mainwindow.py index d93e672b4..337552598 100644 --- a/qutebrowser/mainwindow/mainwindow.py +++ b/qutebrowser/mainwindow/mainwindow.py @@ -35,7 +35,7 @@ from qutebrowser.mainwindow.statusbar import bar from qutebrowser.completion import completionwidget from qutebrowser.keyinput import modeman from qutebrowser.browser import hints, downloads, downloadview, commands -from qutebrowser.misc import crashsignal +from qutebrowser.misc import crashsignal, keyhintwidget win_id_gen = itertools.count(0) @@ -160,6 +160,8 @@ class MainWindow(QWidget): self._commandrunner = runners.CommandRunner(self.win_id) + self._keyhint = keyhintwidget.KeyHintView(self.win_id, self) + log.init.debug("Initializing modes...") modeman.init(self.win_id, self) @@ -178,6 +180,7 @@ class MainWindow(QWidget): # resizing will fail. Therefore, we use singleShot QTimers to make sure # we defer this until everything else is initialized. QTimer.singleShot(0, self._connect_resize_completion) + QTimer.singleShot(0, self._connect_resize_keyhint) objreg.get('config').changed.connect(self.on_config_changed) if config.get('ui', 'hide-mouse-cursor'): @@ -252,6 +255,11 @@ class MainWindow(QWidget): self._completion.resize_completion.connect(self.resize_completion) self.resize_completion() + def _connect_resize_keyhint(self): + """Connect the reposition_keyhint signal and resize it once.""" + self._keyhint.reposition_keyhint.connect(self.reposition_keyhint) + self.reposition_keyhint() + def _set_default_geometry(self): """Set some sensible default geometry.""" self.setGeometry(QRect(50, 50, 800, 600)) @@ -286,6 +294,8 @@ class MainWindow(QWidget): # commands keyparsers[usertypes.KeyMode.normal].keystring_updated.connect( status.keystring.setText) + keyparsers[usertypes.KeyMode.normal].keystring_updated.connect( + self._keyhint.update_keyhint) cmd.got_cmd.connect(self._commandrunner.run_safely) cmd.returnPressed.connect(tabs.on_cmd_return_pressed) tabs.got_cmd.connect(self._commandrunner.run_safely) @@ -367,6 +377,24 @@ class MainWindow(QWidget): if rect.isValid(): self._completion.setGeometry(rect) + @pyqtSlot() + def reposition_keyhint(self): + """Adjust keyhint according to config.""" + if not self._keyhint.isVisible(): + return + # Shrink the window to the shown text and place it at the bottom left + width = self._keyhint.width() + height = self._keyhint.height() + topleft_y = self.height() - self.status.height() - height + topleft_y = qtutils.check_overflow(topleft_y, 'int', fatal=False) + topleft = QPoint(0, topleft_y) + bottomright = (self.status.geometry().topLeft() + + QPoint(width, 0)) + rect = QRect(topleft, bottomright) + log.misc.debug('keyhint rect: {}'.format(rect)) + if rect.isValid(): + self._keyhint.setGeometry(rect) + @cmdutils.register(instance='main-window', scope='window') @pyqtSlot() def close(self): @@ -394,6 +422,7 @@ class MainWindow(QWidget): """ super().resizeEvent(e) self.resize_completion() + self.reposition_keyhint() self._downloadview.updateGeometry() self.tabbed_browser.tabBar().refresh() diff --git a/qutebrowser/misc/keyhintwidget.py b/qutebrowser/misc/keyhintwidget.py new file mode 100644 index 000000000..7274f3f68 --- /dev/null +++ b/qutebrowser/misc/keyhintwidget.py @@ -0,0 +1,106 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2016 Ryan Roden-Corrent (rcorre) +# +# 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 . + +"""Small window that pops up to show hints for possible keystrings. + +When a user inputs a key that forms a partial match, this shows a small window +with each possible completion of that keystring and the corresponding command. +It is intended to help discoverability of keybindings. +""" + +from PyQt5.QtWidgets import QLabel, QSizePolicy +from PyQt5.QtCore import pyqtSlot, pyqtSignal + +from qutebrowser.config import config, style +from qutebrowser.utils import objreg, utils + + +class KeyHintView(QLabel): + + """The view showing hints for key bindings based on the current key string. + + Attributes: + _win_id: Window ID of parent. + _enabled: If False, do not show the window at all + _suffix_color: Highlight for completions to the current keychain. + + Signals: + reposition_keyhint: Emitted when this widget should be resized. + """ + + STYLESHEET = """ + QLabel { + font: {{ font['keyhint'] }}; + color: {{ color['keyhint.fg'] }}; + background-color: {{ color['keyhint.bg'] }}; + } + """ + + reposition_keyhint = pyqtSignal() + + def __init__(self, win_id, parent=None): + super().__init__(parent) + self._win_id = win_id + self.set_enabled() + config = objreg.get('config') + config.changed.connect(self.set_enabled) + self._suffix_color = config.get('colors', 'keyhint.fg.suffix') + style.set_register_stylesheet(self) + self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Minimum) + self.setVisible(False) + + def __repr__(self): + return utils.get_repr(self) + + @config.change_filter('ui', 'show-keyhints') + def set_enabled(self): + """Update self._enabled when the config changed.""" + self._enabled = config.get('ui', 'show-keyhints') + if not self._enabled: self.setVisible(False) + + def showEvent(self, e): + """Adjust the keyhint size when it's freshly shown.""" + self.reposition_keyhint.emit() + super().showEvent(e) + + @pyqtSlot(str) + def update_keyhint(self, prefix): + """Show hints for the given prefix (or hide if prefix is empty). + + Args: + prefix: The current partial keystring. + """ + if len(prefix) == 0 or not self._enabled: + self.setVisible(False) + return + + self.setVisible(True) + + text = '' + keyconf = objreg.get('key-config') + # this is only fired in normal mode + for key, cmd in keyconf.get_bindings_for('normal').items(): + if key.startswith(prefix): + suffix = "{}".format(self._suffix_color, + key[len(prefix):]) + text += '{}{}\t{}
'.format(prefix, suffix, cmd) + + self.setText(text) + self.adjustSize() + self.reposition_keyhint.emit()