diff --git a/qutebrowser/browser/hints.py b/qutebrowser/browser/hints.py
index 8dac93e03..c74a6dff7 100644
--- a/qutebrowser/browser/hints.py
+++ b/qutebrowser/browser/hints.py
@@ -23,11 +23,13 @@ import collections
import functools
import math
import re
+import html
from string import ascii_lowercase
from PyQt5.QtCore import (pyqtSignal, pyqtSlot, QObject, QEvent, Qt, QUrl,
QTimer)
from PyQt5.QtGui import QMouseEvent
+from PyQt5.QtWidgets import QLabel
from PyQt5.QtWebKitWidgets import QWebPage
from qutebrowser.config import config
@@ -57,6 +59,72 @@ def on_mode_entered(mode, win_id):
modeman.maybe_leave(win_id, usertypes.KeyMode.hint, 'insert mode')
+class HintLabel(QLabel):
+
+ """A label for a link.
+
+ Attributes:
+ elem: The element this label belongs to.
+ _context: The current hinting context.
+ """
+
+ def __init__(self, elem, context):
+ super().__init__(parent=context.tab)
+ self._context = context
+ self.elem = elem
+ self._context.tab.contents_size_changed.connect(
+ self._on_contents_size_changed)
+ self._move_to_elem()
+ self.show()
+
+ # FIXME styling
+ # ('color', config.get('colors', 'hints.fg') + ' !important'),
+ # ('background', config.get('colors', 'hints.bg') + ' !important'),
+ # ('font', config.get('fonts', 'hints') + ' !important'),
+ # ('border', config.get('hints', 'border') + ' !important'),
+ # ('opacity', str(config.get('hints', 'opacity')) + ' !important'),
+
+ def update_text(self, matched, unmatched):
+ """Set the text for the hint.
+
+ Args:
+ matched: The part of the text which was typed.
+ unmatched: The part of the text which was not typed yet.
+ """
+ if (config.get('hints', 'uppercase') and
+ self._context.hint_mode == 'letter'):
+ matched = html.escape(matched.upper())
+ unmatched = html.escape(unmatched.upper())
+ else:
+ matched = html.escape(matched)
+ unmatched = html.escape(unmatched)
+
+ match_color = html.escape(config.get('colors', 'hints.fg.match'))
+ self.setText('{}{}'.format(
+ match_color, matched, unmatched))
+
+ def _move_to_elem(self):
+ """Reposition the label to its element."""
+ no_js = config.get('hints', 'find-implementation') != 'javascript'
+ rect = self.elem.rect_on_view(adjust_zoom=False, no_js=no_js)
+ self.move(rect.x(), rect.y())
+
+ @pyqtSlot()
+ def _on_contents_size_changed(self):
+ """Reposition hints if contents size changed."""
+ log.hints.debug("Contents size changed...!")
+ if self.elem.frame() is None:
+ # This sometimes happens for some reason...
+ self.cleanup()
+ else:
+ self._move_to_elem()
+
+ def cleanup(self):
+ """Clean up this element and hide it."""
+ self.hide()
+ self.deleteLater()
+
+
class HintContext:
"""Context namespace used for hinting.
@@ -362,18 +430,9 @@ class HintManager(QObject):
def _cleanup(self):
"""Clean up after hinting."""
- try:
- self._context.tab.contents_size_changed.disconnect(
- self.on_contents_size_changed)
- except TypeError:
- # For some reason, this can fail sometimes...
- pass
-
for elem in self._context.all_elems:
- try:
- elem.label.remove_from_document()
- except webelem.Error:
- pass
+ elem.label.cleanup()
+
text = self._get_text()
message_bridge = objreg.get('message-bridge', scope='window',
window=self._win_id)
@@ -513,87 +572,6 @@ class HintManager(QObject):
hintstr.insert(0, chars[0])
return ''.join(hintstr)
- def _is_hidden(self, elem):
- """Check if the element is hidden via display=none."""
- display = elem.style_property('display', strategy='inline')
- return display == 'none'
-
- def _show_elem(self, elem):
- """Show a given element."""
- elem.set_style_property('display', 'inline !important')
-
- def _hide_elem(self, elem):
- """Hide a given element."""
- elem.set_style_property('display', 'none !important')
-
- def _set_style_properties(self, elem, label):
- """Set the hint CSS on the element given.
-
- Args:
- elem: The QWebElement to set the style attributes for.
- label: The label QWebElement.
- """
- attrs = [
- ('display', 'inline !important'),
- ('z-index', '{} !important'.format(int(2 ** 32 / 2 - 1))),
- ('pointer-events', 'none !important'),
- ('position', 'fixed !important'),
- ('color', config.get('colors', 'hints.fg') + ' !important'),
- ('background', config.get('colors', 'hints.bg') + ' !important'),
- ('font', config.get('fonts', 'hints') + ' !important'),
- ('border', config.get('hints', 'border') + ' !important'),
- ('opacity', str(config.get('hints', 'opacity')) + ' !important'),
- ]
-
- # Make text uppercase if set in config
- if (config.get('hints', 'uppercase') and
- self._context.hint_mode == 'letter'):
- attrs.append(('text-transform', 'uppercase !important'))
- else:
- attrs.append(('text-transform', 'none !important'))
-
- for k, v in attrs:
- label.set_style_property(k, v)
- self._set_style_position(elem, label)
-
- def _set_style_position(self, elem, label):
- """Set the CSS position of the label element.
-
- Args:
- elem: The QWebElement to set the style attributes for.
- label: The label QWebElement.
- """
- no_js = config.get('hints', 'find-implementation') != 'javascript'
- rect = elem.rect_on_view(adjust_zoom=False, no_js=no_js)
- left = rect.x()
- top = rect.y()
- log.hints.vdebug("Drawing label '{!r}' at {}/{} for element '{!r}' "
- "(no_js: {})".format(label, left, top, elem, no_js))
- label.set_style_property('left', '{}px !important'.format(left))
- label.set_style_property('top', '{}px !important'.format(top))
-
- def _draw_label(self, elem, string):
- """Draw a hint label over an element.
-
- Args:
- elem: The QWebElement to use.
- string: The hint string to print.
-
- Return:
- The newly created label element
- """
- doc = elem.document_element()
- body = doc.find_first('body')
- if body is None:
- parent = doc
- else:
- parent = body
- label = parent.create_inside('span')
- label['class'] = 'qutehint'
- self._set_style_properties(elem, label)
- label.set_text(string)
- return label
-
def _show_url_error(self):
"""Show an error because no link was found."""
message.error(self._win_id, "No suitable link found for this element.",
@@ -646,18 +624,19 @@ class HintManager(QObject):
raise cmdexc.CommandError("No elements found.")
strings = self._hint_strings(elems)
log.hints.debug("hints: {}".format(', '.join(strings)))
+
for e, string in zip(elems, strings):
- label = self._draw_label(e, string)
+ label = HintLabel(e, self._context)
+ label.update_text('', string)
elem = ElemTuple(e, label)
self._context.all_elems.append(elem)
self._context.elems[string] = elem
+
keyparsers = objreg.get('keyparsers', scope='window',
window=self._win_id)
keyparser = keyparsers[usertypes.KeyMode.hint]
keyparser.update_bindings(strings)
- self._context.tab.contents_size_changed.connect(
- self.on_contents_size_changed)
message_bridge = objreg.get('message-bridge', scope='window',
window=self._win_id)
message_bridge.set_text(self._get_text())
@@ -777,21 +756,12 @@ class HintManager(QObject):
return self._context.hint_mode
- def _get_visible_hints(self):
- """Get elements which are currently visible."""
- visible = {}
- for string, elem in self._context.elems.items():
- try:
- if not self._is_hidden(elem.label):
- visible[string] = elem
- except webelem.Error:
- pass
- return visible
-
def _handle_auto_follow(self, keystr="", filterstr="", visible=None):
"""Handle the auto-follow option."""
if visible is None:
- visible = self._get_visible_hints()
+ visible = {string: elem
+ for string, elem in self._context.elems.items()
+ if elem.label.isVisible()}
if len(visible) != 1:
return
@@ -830,19 +800,15 @@ class HintManager(QObject):
if string.startswith(keystr):
matched = string[:len(keystr)]
rest = string[len(keystr):]
- match_color = config.get('colors', 'hints.fg.match')
- elem.label.set_inner_xml(
- '{}{}'.format(
- match_color, matched, rest))
- if self._is_hidden(elem.label):
- # hidden element which matches again -> show it
- self._show_elem(elem.label)
+ elem.label.update_text(matched, rest)
+ # Show label again if it was hidden before
+ elem.label.show()
else:
# element doesn't match anymore -> hide it, unless in rapid
# mode and hide-unmatched-rapid-hints is false (see #1799)
if (not self._context.rapid or
config.get('hints', 'hide-unmatched-rapid-hints')):
- self._hide_elem(elem.label)
+ elem.label.hide()
except webelem.Error:
pass
self._handle_auto_follow(keystr=keystr)
@@ -866,12 +832,11 @@ class HintManager(QObject):
try:
if self._filter_matches(filterstr, str(elem.elem)):
visible.append(elem)
- if self._is_hidden(elem.label):
- # hidden element which matches again -> show it
- self._show_elem(elem.label)
+ # Show label again if it was hidden before
+ elem.label.show()
else:
# element doesn't match anymore -> hide it
- self._hide_elem(elem.label)
+ elem.label.hide()
except webelem.Error:
pass
@@ -886,7 +851,7 @@ class HintManager(QObject):
strings = self._hint_strings(visible)
self._context.elems = {}
for elem, string in zip(visible, strings):
- elem.label.set_inner_xml(string)
+ elem.label.update_text('', string)
self._context.elems[string] = elem
keyparsers = objreg.get('keyparsers', scope='window',
window=self._win_id)
@@ -956,7 +921,7 @@ class HintManager(QObject):
self.filter_hints(None)
# Undo keystring highlighting
for string, elem in self._context.elems.items():
- elem.label.set_inner_xml(string)
+ elem.label.update_text('', string)
try:
handler()
@@ -980,20 +945,6 @@ class HintManager(QObject):
raise cmdexc.CommandError("No hint {}!".format(keystring))
self._fire(keystring)
- @pyqtSlot()
- def on_contents_size_changed(self):
- """Reposition hints if contents size changed."""
- log.hints.debug("Contents size changed...!")
- for e in self._context.all_elems:
- try:
- if e.elem.frame() is None:
- # This sometimes happens for some reason...
- e.label.remove_from_document()
- continue
- self._set_style_position(e.elem, e.label)
- except webelem.Error:
- pass
-
@pyqtSlot(usertypes.KeyMode)
def on_mode_left(self, mode):
"""Stop hinting when hinting mode was left."""