Use native QLabels for hints

This will make labels work easily with QtWebEngine, and make sure
they're not affected by the page's contents.

Fixes #925.
Fixes #1126.
This commit is contained in:
Florian Bruhin 2016-08-16 17:07:38 +02:00
parent ea14b5bb42
commit 1753d3507c

View File

@ -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('<font color="{}">{}</font>{}'.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(
'<font color="{}">{}</font>{}'.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."""