Merge branch 'rcorre-keystring'

This commit is contained in:
Florian Bruhin 2016-05-18 07:38:08 +02:00
commit db0e29ae1d
7 changed files with 301 additions and 4 deletions

View File

@ -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
~~~~~~~

View File

@ -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
)),
@ -475,7 +479,7 @@ def data(readonly=False):
"match, the complete match will be executed after this time."),
('partial-timeout',
SettingValue(typ.Int(minval=0, maxval=MAXVALS['int']), '1000'),
SettingValue(typ.Int(minval=0, maxval=MAXVALS['int']), '5000'),
"Timeout for partially typed key bindings.\n\n"
"If the current input forms only partial matches, the keystring "
"will be cleared after this time."),
@ -1200,6 +1204,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.CssColor(), '#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
)),
@ -1281,6 +1297,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
)),
])

View File

@ -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))
@ -290,6 +298,11 @@ class MainWindow(QWidget):
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)
@ -367,6 +380,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 +425,7 @@ class MainWindow(QWidget):
"""
super().resizeEvent(e)
self.resize_completion()
self.reposition_keyhint()
self._downloadview.updateGeometry()
self.tabbed_browser.tabBar().refresh()

View File

@ -0,0 +1,123 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2016 Ryan Roden-Corrent (rcorre) <ryan@rcorre.net>
#
# 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 <http://www.gnu.org/licenses/>.
"""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.
"""
import html
from PyQt5.QtWidgets import QLabel, QSizePolicy
from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt
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
Signals:
reposition_keyhint: Emitted when this widget should be resized.
"""
STYLESHEET = """
QLabel {
font: {{ font['keyhint'] }};
color: {{ color['keyhint.fg'] }};
background-color: {{ color['keyhint.bg'] }};
padding: 6px;
border-top-right-radius: 6px;
}
"""
reposition_keyhint = pyqtSignal()
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')
cfg.changed.connect(self.set_enabled)
style.set_register_stylesheet(self)
self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Minimum)
self.hide()
def __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.hide()
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, modename, prefix):
"""Show hints for the given prefix (or hide if prefix is empty).
Args:
prefix: The current partial keystring.
"""
if not prefix or not self._enabled:
self.hide()
return
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():
# 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:
text += (
"<tr>"
"<td>{}</td>"
"<td style='color: {}'>{}</td>"
"<td style='padding-left: 2ex'>{}</td>"
"</tr>"
).format(
html.escape(prefix),
suffix_color,
html.escape(key[len(prefix):]),
html.escape(cmd)
)
text = '<table>{}</table>'.format(text)
self.setText(text)
self.adjustSize()
self.reposition_keyhint.emit()

View File

@ -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'),

View File

@ -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:

View File

@ -0,0 +1,112 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2016 Ryan Roden-Corrent (rcorre) <ryan@rcorre.net>
#
# 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 <http://www.gnu.org/licenses/>.
"""Test the keyhint widget."""
from collections import OrderedDict
import pytest
from qutebrowser.misc.keyhintwidget import KeyHintView
def expected_text(*args):
"""Helper to format text we expect the KeyHintView to generate.
Args:
args: One tuple for each row in the expected output.
Tuples are of the form: (prefix, color, suffix, command).
"""
text = '<table>'
for group in args:
print("group = {}".format(group))
text += ("<tr>"
"<td>{}</td>"
"<td style='color: {}'>{}</td>"
"<td style='padding-left: 2ex'>{}</td>"
"</tr>").format(*group)
return text + '</table>'
@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
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')]))
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([
('<a', 'cmd-<a'),
('<b', 'cmd-<b'),
('<ctrl-a>', 'cmd-ctrla')]))
keyhint.update_keyhint('normal', '<')
assert keyhint.text() == expected_text(
('&lt;', 'yellow', 'a', 'cmd-&lt;a'),
('&lt;', 'yellow', 'b', 'cmd-&lt;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'))