From 8406746889edef6f9017fc27f5bae27b20506f00 Mon Sep 17 00:00:00 2001 From: Ryan Roden-Corrent Date: Sat, 14 May 2016 06:49:27 -0400 Subject: [PATCH 01/15] Show possible keychains from current input. When the current keystring is a partial match for one or more bindings, show the possible bindings in a small overlay. The overlay is partially transparent by default, but the background color is configurable as ui->keystring.bg. --- qutebrowser/config/configdata.py | 20 +++++ qutebrowser/mainwindow/mainwindow.py | 31 +++++++- qutebrowser/misc/keyhintwidget.py | 106 +++++++++++++++++++++++++++ 3 files changed, 156 insertions(+), 1 deletion(-) create mode 100644 qutebrowser/misc/keyhintwidget.py 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() From e7ff717d52e81279928047ff77a1d5c4da89733c Mon Sep 17 00:00:00 2001 From: Ryan Roden-Corrent Date: Sat, 14 May 2016 06:49:47 -0400 Subject: [PATCH 02/15] Show key hints for all modes, not just normal. --- qutebrowser/mainwindow/mainwindow.py | 7 +++++-- qutebrowser/misc/keyhintwidget.py | 4 ++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/qutebrowser/mainwindow/mainwindow.py b/qutebrowser/mainwindow/mainwindow.py index 337552598..461dba577 100644 --- a/qutebrowser/mainwindow/mainwindow.py +++ b/qutebrowser/mainwindow/mainwindow.py @@ -294,12 +294,15 @@ 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) + # key hint popup + for mode, parser in keyparsers.items(): + parser.keystring_updated.connect(functools.partial( + self._keyhint.update_keyhint, mode.name)) + # config for obj in keyparsers.values(): key_config.changed.connect(obj.on_keyconfig_changed) diff --git a/qutebrowser/misc/keyhintwidget.py b/qutebrowser/misc/keyhintwidget.py index 7274f3f68..4b1bde199 100644 --- a/qutebrowser/misc/keyhintwidget.py +++ b/qutebrowser/misc/keyhintwidget.py @@ -80,7 +80,7 @@ class KeyHintView(QLabel): super().showEvent(e) @pyqtSlot(str) - def update_keyhint(self, prefix): + def update_keyhint(self, modename, prefix): """Show hints for the given prefix (or hide if prefix is empty). Args: @@ -95,7 +95,7 @@ class KeyHintView(QLabel): text = '' keyconf = objreg.get('key-config') # this is only fired in normal mode - for key, cmd in keyconf.get_bindings_for('normal').items(): + for key, cmd in keyconf.get_bindings_for(modename).items(): if key.startswith(prefix): suffix = "{}".format(self._suffix_color, key[len(prefix):]) From d592a3e7648f2e2e86181547b06ca3e198d3654a Mon Sep 17 00:00:00 2001 From: Ryan Roden-Corrent Date: Sat, 14 May 2016 19:22:18 -0400 Subject: [PATCH 03/15] Clean up keyhint implementation. From code review: - escape all strings used in the keyhint html - read the prefix color each time the hint is shown - use show/hide instead of setVisible - clean up pylint/flake8 errors - use CssColor instead of QssColor for keyhint.fg.suffix - add some padding to the keyhint popup --- qutebrowser/config/configdata.py | 2 +- qutebrowser/misc/keyhintwidget.py | 33 ++++++++++++++++++++----------- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index ebc215f3a..e1695900d 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -1205,7 +1205,7 @@ def data(readonly=False): "Text color for the keyhint widget."), ('keyhint.fg.suffix', - SettingValue(typ.QssColor(), '#FFFF00'), + SettingValue(typ.CssColor(), '#FFFF00'), "Highlight color for keys to complete the current keychain"), ('keyhint.bg', diff --git a/qutebrowser/misc/keyhintwidget.py b/qutebrowser/misc/keyhintwidget.py index 4b1bde199..353054da7 100644 --- a/qutebrowser/misc/keyhintwidget.py +++ b/qutebrowser/misc/keyhintwidget.py @@ -24,6 +24,8 @@ with each possible completion of that keystring and the corresponding command. It is intended to help discoverability of keybindings. """ +import html + from PyQt5.QtWidgets import QLabel, QSizePolicy from PyQt5.QtCore import pyqtSlot, pyqtSignal @@ -49,6 +51,9 @@ class KeyHintView(QLabel): font: {{ font['keyhint'] }}; color: {{ color['keyhint.fg'] }}; background-color: {{ color['keyhint.bg'] }}; + padding: 6px; + padding-bottom: -4px; + border-radius: 6px; } """ @@ -58,21 +63,21 @@ class KeyHintView(QLabel): 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') + cfg = objreg.get('config') + cfg.changed.connect(self.set_enabled) style.set_register_stylesheet(self) self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Minimum) - self.setVisible(False) + self.hide() def __repr__(self): - return utils.get_repr(self) + return utils.get_repr(self, win_id=self._win_id) @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) + if not self._enabled: + self.hide() def showEvent(self, e): """Adjust the keyhint size when it's freshly shown.""" @@ -86,20 +91,24 @@ class KeyHintView(QLabel): Args: prefix: The current partial keystring. """ - if len(prefix) == 0 or not self._enabled: - self.setVisible(False) + if not prefix or not self._enabled: + self.hide() return - self.setVisible(True) + self.show() + suffix_color = html.escape(config.get('colors', 'keyhint.fg.suffix')) text = '' keyconf = objreg.get('key-config') # this is only fired in normal mode for key, cmd in keyconf.get_bindings_for(modename).items(): if key.startswith(prefix): - suffix = "{}".format(self._suffix_color, - key[len(prefix):]) - text += '{}{}\t{}
'.format(prefix, suffix, cmd) + suffix = "{}".format(suffix_color, + html.escape(key[len(prefix):])) + + text += '{}{}\t{}
'.format(html.escape(prefix), + suffix, + html.escape(cmd)) self.setText(text) self.adjustSize() From 231950aa880391ac814b33e6fa04899d1fb0ff85 Mon Sep 17 00:00:00 2001 From: Ryan Roden-Corrent Date: Sat, 14 May 2016 19:30:39 -0400 Subject: [PATCH 04/15] Increase default value for input.partial-timeout. Increase from 500 to 2500. This allows the user more time to read hints shown in the new keyhint popup. --- qutebrowser/config/configdata.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index e1695900d..2af418a5e 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -477,7 +477,7 @@ def data(readonly=False): "Timeout for ambiguous key bindings."), ('partial-timeout', - SettingValue(typ.Int(minval=0, maxval=MAXVALS['int']), '1000'), + SettingValue(typ.Int(minval=0, maxval=MAXVALS['int']), '2500'), "Timeout for partially typed key bindings."), ('insert-mode-on-plugins', From d506d7793f845419e06b600cbc8fec5b075bc29f Mon Sep 17 00:00:00 2001 From: Ryan Roden-Corrent Date: Sun, 15 May 2016 08:40:33 -0400 Subject: [PATCH 05/15] Further clean up keyhints. Don't show special keys in the keyhint window as these currently cannot be part of keychains. Use a rounded border on the top-right corner and square on the rest. --- qutebrowser/misc/keyhintwidget.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/qutebrowser/misc/keyhintwidget.py b/qutebrowser/misc/keyhintwidget.py index 353054da7..6e07a5b64 100644 --- a/qutebrowser/misc/keyhintwidget.py +++ b/qutebrowser/misc/keyhintwidget.py @@ -40,7 +40,6 @@ class KeyHintView(QLabel): 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. @@ -53,7 +52,7 @@ class KeyHintView(QLabel): background-color: {{ color['keyhint.bg'] }}; padding: 6px; padding-bottom: -4px; - border-radius: 6px; + border-top-right-radius: 6px; } """ @@ -102,7 +101,9 @@ class KeyHintView(QLabel): keyconf = objreg.get('key-config') # this is only fired in normal mode for key, cmd in keyconf.get_bindings_for(modename).items(): - if key.startswith(prefix): + # for now, special keys can't be part of keychains, so ignore them + is_special_binding = key.startswith('<') and key.endswith('>') + if key.startswith(prefix) and not is_special_binding: suffix = "{}".format(suffix_color, html.escape(key[len(prefix):])) From 581a521b4d269dcbd81c2e15514c5048b15ead98 Mon Sep 17 00:00:00 2001 From: Ryan Roden-Corrent Date: Sun, 15 May 2016 22:18:16 -0400 Subject: [PATCH 06/15] Allow setting mock keybindings for unit tests. Implement mock_key_config.set_bindings_for to set bindings that will be retrieved by mock_key_config.get_bindings_for. This is useful for testing the new keyhint ui. --- tests/helpers/stubs.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/helpers/stubs.py b/tests/helpers/stubs.py index f414fba74..adaebd168 100644 --- a/tests/helpers/stubs.py +++ b/tests/helpers/stubs.py @@ -385,8 +385,14 @@ class KeyConfigStub: """Stub for the key-config object.""" - def get_bindings_for(self, _section): - return {} + def __init__(self): + self.bindings = {} + + def get_bindings_for(self, section): + return self.bindings.get(section) + + def set_bindings_for(self, section, bindings): + self.bindings[section] = bindings class HostBlockerStub: From 8eee5def5d8ad4bcb5095a76fc9a17e0570c271e Mon Sep 17 00:00:00 2001 From: Ryan Roden-Corrent Date: Sun, 15 May 2016 22:20:52 -0400 Subject: [PATCH 07/15] Add unit tests for the keyhint widget. - validate keyhint text for a partial keychain - ensure special keybindings are not suggested - ensure it is not visible when disabled - ensure changes to the suffix color are picked up --- tests/unit/misc/test_keyhints.py | 96 ++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 tests/unit/misc/test_keyhints.py diff --git a/tests/unit/misc/test_keyhints.py b/tests/unit/misc/test_keyhints.py new file mode 100644 index 000000000..975ce88df --- /dev/null +++ b/tests/unit/misc/test_keyhints.py @@ -0,0 +1,96 @@ +# 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 . + +"""Test the keyhint widget.""" + +from collections import OrderedDict +from unittest import mock +from PyQt5.QtCore import Qt +from PyQt5.QtWidgets import QApplication +import pytest + +from qutebrowser.misc.keyhintwidget import KeyHintView + + +class TestKeyHintView: + """Tests for KeyHintView widget.""" + + @pytest.fixture + def keyhint(self, qtbot, config_stub, key_config_stub): + """Fixture to initialize a KeyHintView.""" + config_stub.data = { + 'colors': { + 'keyhint.fg': 'white', + 'keyhint.fg.suffix': 'yellow', + 'keyhint.bg': 'black' + }, + 'fonts': {'keyhint': 'Comic Sans'}, + 'ui': {'show-keyhints': True}, + } + keyhint = KeyHintView(0, None) + #qtbot.add_widget(keyhint) + assert keyhint.text() == '' + return keyhint + + def test_suggestions(self, qtbot, keyhint, key_config_stub): + """Test cursor position based on the prompt.""" + # we want the dict to return sorted items() for reliable testing + key_config_stub.set_bindings_for('normal', OrderedDict([ + ('aa', 'cmd-aa'), + ('ab', 'cmd-ab'), + ('aba', 'cmd-aba'), + ('abb', 'cmd-abb'), + ('xd', 'cmd-xd'), + ('xe', 'cmd-xe')])) + + keyhint.update_keyhint('normal', 'a') + line = "a{}\t{}
" + assert keyhint.text() == (line.format('yellow', 'a', 'cmd-aa') + + line.format('yellow', 'b', 'cmd-ab') + + line.format('yellow', 'ba', 'cmd-aba') + + line.format('yellow', 'bb', 'cmd-abb')) + + def test_special_bindings(self, qtbot, keyhint, key_config_stub): + """Ensure the a prefix of '<' doesn't suggest special keys""" + # we want the dict to return sorted items() for reliable testing + key_config_stub.set_bindings_for('normal', OrderedDict([ + ('', 'cmd-aba')])) + + keyhint.update_keyhint('normal', '<') + line = "<{}\t{}
" + assert keyhint.text() == (line.format('yellow', 'a', 'cmd-aa') + + line.format('yellow', 'b', 'cmd-ab')) + + def test_disable(self, qtbot, keyhint, config_stub): + """Ensure the a prefix of '<' doesn't suggest special keys""" + config_stub.set('ui', 'show-keyhints', False) + keyhint.update_keyhint('normal', 'a') + assert not keyhint.text() + assert not keyhint.isVisible() + + def test_color_switch(self, qtbot, keyhint, config_stub, key_config_stub): + """Ensure the the keyhint suffix color can be updated at runtime.""" + config_stub.set('colors', 'keyhint.fg.suffix', '#ABCDEF') + key_config_stub.set_bindings_for('normal', OrderedDict([ + ('aa', 'cmd-aa')])) + keyhint.update_keyhint('normal', 'a') + expected = "aa\tcmd-aa
" + assert keyhint.text() == expected From 3cd252ef82731334c7c15cd342139251487e6dec Mon Sep 17 00:00:00 2001 From: Ryan Roden-Corrent Date: Sun, 15 May 2016 22:28:12 -0400 Subject: [PATCH 08/15] Clean up html for keyhint text. The \t was behaving the same as a space and the was doing nothing. --- qutebrowser/misc/keyhintwidget.py | 6 +++--- tests/unit/misc/test_keyhints.py | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/qutebrowser/misc/keyhintwidget.py b/qutebrowser/misc/keyhintwidget.py index 6e07a5b64..da97eeb2b 100644 --- a/qutebrowser/misc/keyhintwidget.py +++ b/qutebrowser/misc/keyhintwidget.py @@ -107,9 +107,9 @@ class KeyHintView(QLabel): suffix = "{}".format(suffix_color, html.escape(key[len(prefix):])) - text += '{}{}\t{}
'.format(html.escape(prefix), - suffix, - html.escape(cmd)) + text += '{}{} {}
'.format(html.escape(prefix), + suffix, + html.escape(cmd)) self.setText(text) self.adjustSize() diff --git a/tests/unit/misc/test_keyhints.py b/tests/unit/misc/test_keyhints.py index 975ce88df..8023fe175 100644 --- a/tests/unit/misc/test_keyhints.py +++ b/tests/unit/misc/test_keyhints.py @@ -60,7 +60,7 @@ class TestKeyHintView: ('xe', 'cmd-xe')])) keyhint.update_keyhint('normal', 'a') - line = "a{}\t{}
" + line = "a{} {}
" assert keyhint.text() == (line.format('yellow', 'a', 'cmd-aa') + line.format('yellow', 'b', 'cmd-ab') + line.format('yellow', 'ba', 'cmd-aba') + @@ -75,12 +75,12 @@ class TestKeyHintView: ('', 'cmd-aba')])) keyhint.update_keyhint('normal', '<') - line = "<{}\t{}
" + line = "<{} {}
" assert keyhint.text() == (line.format('yellow', 'a', 'cmd-aa') + line.format('yellow', 'b', 'cmd-ab')) def test_disable(self, qtbot, keyhint, config_stub): - """Ensure the a prefix of '<' doesn't suggest special keys""" + """Ensure the widget isn't visible if disabled.""" config_stub.set('ui', 'show-keyhints', False) keyhint.update_keyhint('normal', 'a') assert not keyhint.text() @@ -92,5 +92,5 @@ class TestKeyHintView: key_config_stub.set_bindings_for('normal', OrderedDict([ ('aa', 'cmd-aa')])) keyhint.update_keyhint('normal', 'a') - expected = "aa\tcmd-aa
" + expected = "aa cmd-aa
" assert keyhint.text() == expected From acb60a1bf47f7c0f00721be8e77cb18adf6f366a Mon Sep 17 00:00:00 2001 From: Felix Van der Jeugt Date: Mon, 16 May 2016 11:38:39 +0200 Subject: [PATCH 09/15] align using a table --- qutebrowser/misc/keyhintwidget.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/qutebrowser/misc/keyhintwidget.py b/qutebrowser/misc/keyhintwidget.py index da97eeb2b..80ce0aae9 100644 --- a/qutebrowser/misc/keyhintwidget.py +++ b/qutebrowser/misc/keyhintwidget.py @@ -104,12 +104,19 @@ class KeyHintView(QLabel): # for now, special keys can't be part of keychains, so ignore them is_special_binding = key.startswith('<') and key.endswith('>') if key.startswith(prefix) and not is_special_binding: - suffix = "{}".format(suffix_color, - html.escape(key[len(prefix):])) - - text += '{}{} {}
'.format(html.escape(prefix), - suffix, - html.escape(cmd)) + text += ( + "" + "{}" + "{}" + "{}" + "" + ).format( + html.escape(prefix), + suffix_color, + html.escape(key[len(prefix):]), + html.escape(cmd) + ) + text = '{}
'.format(text) self.setText(text) self.adjustSize() From 822d14871327c7d995ec4ccaf6221ded7e5f5e7e Mon Sep 17 00:00:00 2001 From: Ryan Roden-Corrent Date: Mon, 16 May 2016 08:36:01 -0400 Subject: [PATCH 10/15] Update key hint tests for new format. Change the unit tests to expect the new tabular format. Also generally clean up the tests -- refactor from a class to module-level functions as there was no need for a class here. --- scripts/dev/check_coverage.py | 2 + tests/unit/misc/test_keyhints.py | 140 +++++++++++++++++-------------- 2 files changed, 80 insertions(+), 62 deletions(-) diff --git a/scripts/dev/check_coverage.py b/scripts/dev/check_coverage.py index e82229545..bf12628d0 100644 --- a/scripts/dev/check_coverage.py +++ b/scripts/dev/check_coverage.py @@ -136,6 +136,8 @@ PERFECT_FILES = [ 'qutebrowser/utils/jinja.py'), ('tests/unit/utils/test_error.py', 'qutebrowser/utils/error.py'), + ('tests/unit/utils/test_keyhints.py', + 'qutebrowser/misc/keyhintwidget.py'), ] diff --git a/tests/unit/misc/test_keyhints.py b/tests/unit/misc/test_keyhints.py index 8023fe175..85f6c55ce 100644 --- a/tests/unit/misc/test_keyhints.py +++ b/tests/unit/misc/test_keyhints.py @@ -20,77 +20,93 @@ """Test the keyhint widget.""" from collections import OrderedDict -from unittest import mock -from PyQt5.QtCore import Qt -from PyQt5.QtWidgets import QApplication import pytest from qutebrowser.misc.keyhintwidget import KeyHintView -class TestKeyHintView: - """Tests for KeyHintView widget.""" +def expected_text(*args): + """Helper to format text we expect the KeyHintView to generate. - @pytest.fixture - def keyhint(self, qtbot, config_stub, key_config_stub): - """Fixture to initialize a KeyHintView.""" - config_stub.data = { - 'colors': { - 'keyhint.fg': 'white', - 'keyhint.fg.suffix': 'yellow', - 'keyhint.bg': 'black' - }, - 'fonts': {'keyhint': 'Comic Sans'}, - 'ui': {'show-keyhints': True}, - } - keyhint = KeyHintView(0, None) - #qtbot.add_widget(keyhint) - assert keyhint.text() == '' - return keyhint + Args: + args: One tuple for each row in the expected output. + Tuples are of the form: (prefix, color, suffix, command). + """ + text = '' + for group in args: + print("group = {}".format(group)) + text += ("" + "" + "" + "" + "").format(*group) - def test_suggestions(self, qtbot, keyhint, key_config_stub): - """Test cursor position based on the prompt.""" - # we want the dict to return sorted items() for reliable testing - key_config_stub.set_bindings_for('normal', OrderedDict([ - ('aa', 'cmd-aa'), - ('ab', 'cmd-ab'), - ('aba', 'cmd-aba'), - ('abb', 'cmd-abb'), - ('xd', 'cmd-xd'), - ('xe', 'cmd-xe')])) + return text + '
{}{}{}
' - keyhint.update_keyhint('normal', 'a') - line = "a{} {}
" - assert keyhint.text() == (line.format('yellow', 'a', 'cmd-aa') + - line.format('yellow', 'b', 'cmd-ab') + - line.format('yellow', 'ba', 'cmd-aba') + - line.format('yellow', 'bb', 'cmd-abb')) - def test_special_bindings(self, qtbot, keyhint, key_config_stub): - """Ensure the a prefix of '<' doesn't suggest special keys""" - # we want the dict to return sorted items() for reliable testing - key_config_stub.set_bindings_for('normal', OrderedDict([ - ('', 'cmd-aba')])) +@pytest.fixture +def keyhint(qtbot, config_stub, key_config_stub): + """Fixture to initialize a KeyHintView.""" + config_stub.data = { + 'colors': { + 'keyhint.fg': 'white', + 'keyhint.fg.suffix': 'yellow', + 'keyhint.bg': 'black' + }, + 'fonts': {'keyhint': 'Comic Sans'}, + 'ui': {'show-keyhints': True}, + } + keyhint = KeyHintView(0, None) + qtbot.add_widget(keyhint) + assert keyhint.text() == '' + return keyhint - keyhint.update_keyhint('normal', '<') - line = "<{} {}
" - assert keyhint.text() == (line.format('yellow', 'a', 'cmd-aa') + - line.format('yellow', 'b', 'cmd-ab')) - def test_disable(self, qtbot, keyhint, config_stub): - """Ensure the widget isn't visible if disabled.""" - config_stub.set('ui', 'show-keyhints', False) - keyhint.update_keyhint('normal', 'a') - assert not keyhint.text() - assert not keyhint.isVisible() +def test_suggestions(keyhint, key_config_stub): + """Test cursor position based on the prompt.""" + # we want the dict to return sorted items() for reliable testing + key_config_stub.set_bindings_for('normal', OrderedDict([ + ('aa', 'cmd-aa'), + ('ab', 'cmd-ab'), + ('aba', 'cmd-aba'), + ('abb', 'cmd-abb'), + ('xd', 'cmd-xd'), + ('xe', 'cmd-xe')])) - def test_color_switch(self, qtbot, keyhint, config_stub, key_config_stub): - """Ensure the the keyhint suffix color can be updated at runtime.""" - config_stub.set('colors', 'keyhint.fg.suffix', '#ABCDEF') - key_config_stub.set_bindings_for('normal', OrderedDict([ - ('aa', 'cmd-aa')])) - keyhint.update_keyhint('normal', 'a') - expected = "aa cmd-aa
" - assert keyhint.text() == expected + keyhint.update_keyhint('normal', 'a') + assert keyhint.text() == expected_text( + ('a', 'yellow', 'a', 'cmd-aa'), + ('a', 'yellow', 'b', 'cmd-ab'), + ('a', 'yellow', 'ba', 'cmd-aba'), + ('a', 'yellow', 'bb', 'cmd-abb')) + + +def test_special_bindings(keyhint, key_config_stub): + """Ensure the a prefix of '<' doesn't suggest special keys.""" + # we want the dict to return sorted items() for reliable testing + key_config_stub.set_bindings_for('normal', OrderedDict([ + ('', 'cmd-ctrla')])) + + keyhint.update_keyhint('normal', '<') + assert keyhint.text() == expected_text( + ('<', 'yellow', 'a', 'cmd-<a'), + ('<', 'yellow', 'b', 'cmd-<b')) + + +def test_disable(keyhint, config_stub): + """Ensure the widget isn't visible if disabled.""" + config_stub.set('ui', 'show-keyhints', False) + keyhint.update_keyhint('normal', 'a') + assert not keyhint.text() + assert not keyhint.isVisible() + + +def test_color_switch(keyhint, config_stub, key_config_stub): + """Ensure the the keyhint suffix color can be updated at runtime.""" + config_stub.set('colors', 'keyhint.fg.suffix', '#ABCDEF') + key_config_stub.set_bindings_for('normal', OrderedDict([ + ('aa', 'cmd-aa')])) + keyhint.update_keyhint('normal', 'a') + assert keyhint.text() == expected_text(('a', '#ABCDEF', 'a', 'cmd-aa')) From e810317057945d280817bcd27ad85b1cb69f7c51 Mon Sep 17 00:00:00 2001 From: Ryan Roden-Corrent Date: Tue, 17 May 2016 20:10:52 -0400 Subject: [PATCH 11/15] Restore bottom padding of keyhint widget. Previously, negative bottom padding was used (probably to compensate for a trailing
). With the table format, this is no longer necessary and causes the last line to be drawn too low. --- qutebrowser/misc/keyhintwidget.py | 1 - 1 file changed, 1 deletion(-) diff --git a/qutebrowser/misc/keyhintwidget.py b/qutebrowser/misc/keyhintwidget.py index 80ce0aae9..4ee70032c 100644 --- a/qutebrowser/misc/keyhintwidget.py +++ b/qutebrowser/misc/keyhintwidget.py @@ -51,7 +51,6 @@ class KeyHintView(QLabel): color: {{ color['keyhint.fg'] }}; background-color: {{ color['keyhint.bg'] }}; padding: 6px; - padding-bottom: -4px; border-top-right-radius: 6px; } """ From acdeb0f57ca714f644b3bce587cc5b4942618b34 Mon Sep 17 00:00:00 2001 From: Ryan Roden-Corrent Date: Tue, 17 May 2016 20:13:25 -0400 Subject: [PATCH 12/15] Bump default for partial-timeout to 5000ms. Give more time to read the keyhint widget. --- qutebrowser/config/configdata.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index 2af418a5e..253735b4c 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -477,7 +477,7 @@ def data(readonly=False): "Timeout for ambiguous key bindings."), ('partial-timeout', - SettingValue(typ.Int(minval=0, maxval=MAXVALS['int']), '2500'), + SettingValue(typ.Int(minval=0, maxval=MAXVALS['int']), '5000'), "Timeout for partially typed key bindings."), ('insert-mode-on-plugins', From 5c8d1656ffeff5b6ea272b689be569119811db49 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 18 May 2016 07:34:40 +0200 Subject: [PATCH 13/15] Fix path to test_keyhints.py in check_coverage.py utils -> misc --- scripts/dev/check_coverage.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/dev/check_coverage.py b/scripts/dev/check_coverage.py index 1f33a51a4..beb92b756 100644 --- a/scripts/dev/check_coverage.py +++ b/scripts/dev/check_coverage.py @@ -91,6 +91,8 @@ PERFECT_FILES = [ 'qutebrowser/misc/cmdhistory.py'), ('tests/unit/misc/test_ipc.py', 'qutebrowser/misc/ipc.py'), + ('tests/unit/misc/test_keyhints.py', + 'qutebrowser/misc/keyhintwidget.py'), (None, 'qutebrowser/mainwindow/statusbar/keystring.py'), @@ -138,8 +140,6 @@ PERFECT_FILES = [ 'qutebrowser/utils/error.py'), ('tests/unit/utils/test_typing.py', 'qutebrowser/utils/typing.py'), - ('tests/unit/utils/test_keyhints.py', - 'qutebrowser/misc/keyhintwidget.py'), ] From 5b65ec17fd6d5d8ce9f27731c28106ce1227555e Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 18 May 2016 07:35:56 +0200 Subject: [PATCH 14/15] Set Qt.RichText textFormat for KeyHintView --- qutebrowser/misc/keyhintwidget.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/qutebrowser/misc/keyhintwidget.py b/qutebrowser/misc/keyhintwidget.py index 4ee70032c..9a4d99d95 100644 --- a/qutebrowser/misc/keyhintwidget.py +++ b/qutebrowser/misc/keyhintwidget.py @@ -27,7 +27,7 @@ It is intended to help discoverability of keybindings. import html from PyQt5.QtWidgets import QLabel, QSizePolicy -from PyQt5.QtCore import pyqtSlot, pyqtSignal +from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt from qutebrowser.config import config, style from qutebrowser.utils import objreg, utils @@ -59,6 +59,7 @@ class KeyHintView(QLabel): def __init__(self, win_id, parent=None): super().__init__(parent) + self.setTextFormat(Qt.RichText) self._win_id = win_id self.set_enabled() cfg = objreg.get('config') From 213677d30a9314f65591d8f599cb91140d335103 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 18 May 2016 07:37:20 +0200 Subject: [PATCH 15/15] Update changelog --- CHANGELOG.asciidoc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index 99fd043b9..5b4080271 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -30,6 +30,8 @@ Added - New `--force-color` argument to force colored logging even if stdout is not a terminal - New `:messages` command to show error messages +- New pop-up showing possible keybinding when the first key of a keychain is + pressed. This can be turned off using `:set ui show-keyhints false`. Changed ~~~~~~~