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:
parent
ea14b5bb42
commit
1753d3507c
@ -23,11 +23,13 @@ import collections
|
|||||||
import functools
|
import functools
|
||||||
import math
|
import math
|
||||||
import re
|
import re
|
||||||
|
import html
|
||||||
from string import ascii_lowercase
|
from string import ascii_lowercase
|
||||||
|
|
||||||
from PyQt5.QtCore import (pyqtSignal, pyqtSlot, QObject, QEvent, Qt, QUrl,
|
from PyQt5.QtCore import (pyqtSignal, pyqtSlot, QObject, QEvent, Qt, QUrl,
|
||||||
QTimer)
|
QTimer)
|
||||||
from PyQt5.QtGui import QMouseEvent
|
from PyQt5.QtGui import QMouseEvent
|
||||||
|
from PyQt5.QtWidgets import QLabel
|
||||||
from PyQt5.QtWebKitWidgets import QWebPage
|
from PyQt5.QtWebKitWidgets import QWebPage
|
||||||
|
|
||||||
from qutebrowser.config import config
|
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')
|
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:
|
class HintContext:
|
||||||
|
|
||||||
"""Context namespace used for hinting.
|
"""Context namespace used for hinting.
|
||||||
@ -362,18 +430,9 @@ class HintManager(QObject):
|
|||||||
|
|
||||||
def _cleanup(self):
|
def _cleanup(self):
|
||||||
"""Clean up after hinting."""
|
"""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:
|
for elem in self._context.all_elems:
|
||||||
try:
|
elem.label.cleanup()
|
||||||
elem.label.remove_from_document()
|
|
||||||
except webelem.Error:
|
|
||||||
pass
|
|
||||||
text = self._get_text()
|
text = self._get_text()
|
||||||
message_bridge = objreg.get('message-bridge', scope='window',
|
message_bridge = objreg.get('message-bridge', scope='window',
|
||||||
window=self._win_id)
|
window=self._win_id)
|
||||||
@ -513,87 +572,6 @@ class HintManager(QObject):
|
|||||||
hintstr.insert(0, chars[0])
|
hintstr.insert(0, chars[0])
|
||||||
return ''.join(hintstr)
|
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):
|
def _show_url_error(self):
|
||||||
"""Show an error because no link was found."""
|
"""Show an error because no link was found."""
|
||||||
message.error(self._win_id, "No suitable link found for this element.",
|
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.")
|
raise cmdexc.CommandError("No elements found.")
|
||||||
strings = self._hint_strings(elems)
|
strings = self._hint_strings(elems)
|
||||||
log.hints.debug("hints: {}".format(', '.join(strings)))
|
log.hints.debug("hints: {}".format(', '.join(strings)))
|
||||||
|
|
||||||
for e, string in zip(elems, 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)
|
elem = ElemTuple(e, label)
|
||||||
self._context.all_elems.append(elem)
|
self._context.all_elems.append(elem)
|
||||||
self._context.elems[string] = elem
|
self._context.elems[string] = elem
|
||||||
|
|
||||||
keyparsers = objreg.get('keyparsers', scope='window',
|
keyparsers = objreg.get('keyparsers', scope='window',
|
||||||
window=self._win_id)
|
window=self._win_id)
|
||||||
keyparser = keyparsers[usertypes.KeyMode.hint]
|
keyparser = keyparsers[usertypes.KeyMode.hint]
|
||||||
keyparser.update_bindings(strings)
|
keyparser.update_bindings(strings)
|
||||||
|
|
||||||
self._context.tab.contents_size_changed.connect(
|
|
||||||
self.on_contents_size_changed)
|
|
||||||
message_bridge = objreg.get('message-bridge', scope='window',
|
message_bridge = objreg.get('message-bridge', scope='window',
|
||||||
window=self._win_id)
|
window=self._win_id)
|
||||||
message_bridge.set_text(self._get_text())
|
message_bridge.set_text(self._get_text())
|
||||||
@ -777,21 +756,12 @@ class HintManager(QObject):
|
|||||||
|
|
||||||
return self._context.hint_mode
|
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):
|
def _handle_auto_follow(self, keystr="", filterstr="", visible=None):
|
||||||
"""Handle the auto-follow option."""
|
"""Handle the auto-follow option."""
|
||||||
if visible is None:
|
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:
|
if len(visible) != 1:
|
||||||
return
|
return
|
||||||
@ -830,19 +800,15 @@ class HintManager(QObject):
|
|||||||
if string.startswith(keystr):
|
if string.startswith(keystr):
|
||||||
matched = string[:len(keystr)]
|
matched = string[:len(keystr)]
|
||||||
rest = string[len(keystr):]
|
rest = string[len(keystr):]
|
||||||
match_color = config.get('colors', 'hints.fg.match')
|
elem.label.update_text(matched, rest)
|
||||||
elem.label.set_inner_xml(
|
# Show label again if it was hidden before
|
||||||
'<font color="{}">{}</font>{}'.format(
|
elem.label.show()
|
||||||
match_color, matched, rest))
|
|
||||||
if self._is_hidden(elem.label):
|
|
||||||
# hidden element which matches again -> show it
|
|
||||||
self._show_elem(elem.label)
|
|
||||||
else:
|
else:
|
||||||
# element doesn't match anymore -> hide it, unless in rapid
|
# element doesn't match anymore -> hide it, unless in rapid
|
||||||
# mode and hide-unmatched-rapid-hints is false (see #1799)
|
# mode and hide-unmatched-rapid-hints is false (see #1799)
|
||||||
if (not self._context.rapid or
|
if (not self._context.rapid or
|
||||||
config.get('hints', 'hide-unmatched-rapid-hints')):
|
config.get('hints', 'hide-unmatched-rapid-hints')):
|
||||||
self._hide_elem(elem.label)
|
elem.label.hide()
|
||||||
except webelem.Error:
|
except webelem.Error:
|
||||||
pass
|
pass
|
||||||
self._handle_auto_follow(keystr=keystr)
|
self._handle_auto_follow(keystr=keystr)
|
||||||
@ -866,12 +832,11 @@ class HintManager(QObject):
|
|||||||
try:
|
try:
|
||||||
if self._filter_matches(filterstr, str(elem.elem)):
|
if self._filter_matches(filterstr, str(elem.elem)):
|
||||||
visible.append(elem)
|
visible.append(elem)
|
||||||
if self._is_hidden(elem.label):
|
# Show label again if it was hidden before
|
||||||
# hidden element which matches again -> show it
|
elem.label.show()
|
||||||
self._show_elem(elem.label)
|
|
||||||
else:
|
else:
|
||||||
# element doesn't match anymore -> hide it
|
# element doesn't match anymore -> hide it
|
||||||
self._hide_elem(elem.label)
|
elem.label.hide()
|
||||||
except webelem.Error:
|
except webelem.Error:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -886,7 +851,7 @@ class HintManager(QObject):
|
|||||||
strings = self._hint_strings(visible)
|
strings = self._hint_strings(visible)
|
||||||
self._context.elems = {}
|
self._context.elems = {}
|
||||||
for elem, string in zip(visible, strings):
|
for elem, string in zip(visible, strings):
|
||||||
elem.label.set_inner_xml(string)
|
elem.label.update_text('', string)
|
||||||
self._context.elems[string] = elem
|
self._context.elems[string] = elem
|
||||||
keyparsers = objreg.get('keyparsers', scope='window',
|
keyparsers = objreg.get('keyparsers', scope='window',
|
||||||
window=self._win_id)
|
window=self._win_id)
|
||||||
@ -956,7 +921,7 @@ class HintManager(QObject):
|
|||||||
self.filter_hints(None)
|
self.filter_hints(None)
|
||||||
# Undo keystring highlighting
|
# Undo keystring highlighting
|
||||||
for string, elem in self._context.elems.items():
|
for string, elem in self._context.elems.items():
|
||||||
elem.label.set_inner_xml(string)
|
elem.label.update_text('', string)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
handler()
|
handler()
|
||||||
@ -980,20 +945,6 @@ class HintManager(QObject):
|
|||||||
raise cmdexc.CommandError("No hint {}!".format(keystring))
|
raise cmdexc.CommandError("No hint {}!".format(keystring))
|
||||||
self._fire(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)
|
@pyqtSlot(usertypes.KeyMode)
|
||||||
def on_mode_left(self, mode):
|
def on_mode_left(self, mode):
|
||||||
"""Stop hinting when hinting mode was left."""
|
"""Stop hinting when hinting mode was left."""
|
||||||
|
Loading…
Reference in New Issue
Block a user