1# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: 2 3# Copyright 2016-2021 Ryan Roden-Corrent (rcorre) <ryan@rcorre.net> 4# 5# This file is part of qutebrowser. 6# 7# qutebrowser is free software: you can redistribute it and/or modify 8# it under the terms of the GNU General Public License as published by 9# the Free Software Foundation, either version 3 of the License, or 10# (at your option) any later version. 11# 12# qutebrowser is distributed in the hope that it will be useful, 13# but WITHOUT ANY WARRANTY; without even the implied warranty of 14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15# GNU General Public License for more details. 16# 17# You should have received a copy of the GNU General Public License 18# along with qutebrowser. If not, see <https://www.gnu.org/licenses/>. 19 20"""Small window that pops up to show hints for possible keystrings. 21 22When a user inputs a key that forms a partial match, this shows a small window 23with each possible completion of that keystring and the corresponding command. 24It is intended to help discoverability of keybindings. 25""" 26 27import html 28import fnmatch 29import re 30 31from PyQt5.QtWidgets import QLabel, QSizePolicy 32from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt 33 34from qutebrowser.config import config, stylesheet 35from qutebrowser.utils import utils, usertypes 36from qutebrowser.misc import objects 37from qutebrowser.keyinput import keyutils 38 39 40class KeyHintView(QLabel): 41 42 """The view showing hints for key bindings based on the current key string. 43 44 Attributes: 45 _win_id: Window ID of parent. 46 47 Signals: 48 update_geometry: Emitted when this widget should be resized/positioned. 49 """ 50 51 STYLESHEET = """ 52 QLabel { 53 font: {{ conf.fonts.keyhint }}; 54 color: {{ conf.colors.keyhint.fg }}; 55 background-color: {{ conf.colors.keyhint.bg }}; 56 padding: 6px; 57 {% if conf.statusbar.position == 'top' %} 58 border-bottom-right-radius: {{ conf.keyhint.radius }}px; 59 {% else %} 60 border-top-right-radius: {{ conf.keyhint.radius }}px; 61 {% endif %} 62 } 63 """ 64 update_geometry = pyqtSignal() 65 66 def __init__(self, win_id, parent=None): 67 super().__init__(parent) 68 self.setTextFormat(Qt.RichText) 69 self._win_id = win_id 70 self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Minimum) 71 self.hide() 72 self._show_timer = usertypes.Timer(self, 'keyhint_show') 73 self._show_timer.timeout.connect(self.show) 74 self._show_timer.setSingleShot(True) 75 stylesheet.set_register(self) 76 77 def __repr__(self): 78 return utils.get_repr(self, win_id=self._win_id) 79 80 def showEvent(self, e): 81 """Adjust the keyhint size when it's freshly shown.""" 82 self.update_geometry.emit() 83 super().showEvent(e) 84 85 @pyqtSlot(usertypes.KeyMode, str) 86 def update_keyhint(self, mode, prefix): 87 """Show hints for the given prefix (or hide if prefix is empty). 88 89 Args: 90 prefix: The current partial keystring. 91 """ 92 match = re.fullmatch(r'(\d*)(.*)', prefix) 93 assert match is not None, prefix 94 95 countstr, prefix = match.groups() 96 if not prefix: 97 self._show_timer.stop() 98 self.hide() 99 return 100 101 def blacklisted(keychain): 102 return any(fnmatch.fnmatchcase(keychain, glob) 103 for glob in config.val.keyhint.blacklist) 104 105 def takes_count(cmdstr): 106 """Return true iff this command can take a count argument.""" 107 cmdname = cmdstr.split(' ')[0] 108 cmd = objects.commands.get(cmdname) 109 return cmd and cmd.takes_count() 110 111 bindings_dict = config.key_instance.get_bindings_for(mode.name) 112 bindings = [(k, v) for (k, v) in sorted(bindings_dict.items()) 113 if keyutils.KeySequence.parse(prefix).matches(k) and 114 not blacklisted(str(k)) and 115 (takes_count(v) or not countstr)] 116 117 if not bindings: 118 self._show_timer.stop() 119 return 120 121 # delay so a quickly typed keychain doesn't display hints 122 self._show_timer.setInterval(config.val.keyhint.delay) 123 self._show_timer.start() 124 suffix_color = html.escape(config.val.colors.keyhint.suffix.fg) 125 126 text = '' 127 for seq, cmd in bindings: 128 text += ( 129 "<tr>" 130 "<td>{}</td>" 131 "<td style='color: {}'>{}</td>" 132 "<td style='padding-left: 2ex'>{}</td>" 133 "</tr>" 134 ).format( 135 html.escape(prefix), 136 suffix_color, 137 html.escape(str(seq)[len(prefix):]), 138 html.escape(cmd) 139 ) 140 text = '<table>{}</table>'.format(text) 141 142 self.setText(text) 143 self.adjustSize() 144 self.update_geometry.emit() 145