Merge branch 'new-hints'
This commit is contained in:
commit
713201aa13
@ -42,6 +42,20 @@ Added
|
|||||||
Changed
|
Changed
|
||||||
~~~~~~~
|
~~~~~~~
|
||||||
|
|
||||||
|
- Hints are now drawn natively in Qt instead of using web elements. This has a
|
||||||
|
few implications for users:
|
||||||
|
* The `hints -> opacity` setting does not exist anymore, but you can use
|
||||||
|
`rgba(r, g, b, alpha)` colors instead for `colors -> hints.bg`.
|
||||||
|
* The `hints -> font` setting is not affected by
|
||||||
|
`fonts -> web-family-fixed` anymore. Thus, a transformer got added to
|
||||||
|
change `Monospace` to `${_monospace}`.
|
||||||
|
* Gradients in hint colors can now be configured by using `qlineargradient`
|
||||||
|
and friends instead of `-webkit-gradient`. The most common cases get
|
||||||
|
migrated automatically, but if you drastically changed the defaults,
|
||||||
|
you'll need to manually adjust your config.
|
||||||
|
* Styling hints by styling `qutehint` elements in `user-stylesheet` was
|
||||||
|
never officially supported and does not work anymore.
|
||||||
|
* Hints are now not affected by the page's stylesheet or zoom anymore.
|
||||||
- `:bookmark-add` now has a `--toggle` flag which deletes the bookmark if it
|
- `:bookmark-add` now has a `--toggle` flag which deletes the bookmark if it
|
||||||
already exists.
|
already exists.
|
||||||
- `:bookmark-load` now has a `--delete` flag which deletes the bookmark after
|
- `:bookmark-load` now has a `--delete` flag which deletes the bookmark after
|
||||||
@ -99,6 +113,7 @@ Removed
|
|||||||
into a new `:completion-focus {prev,next}` command and thus removed.
|
into a new `:completion-focus {prev,next}` command and thus removed.
|
||||||
- The `ui -> hide-mouse-cursor` setting since it was completely broken and
|
- The `ui -> hide-mouse-cursor` setting since it was completely broken and
|
||||||
nobody seemed to care.
|
nobody seemed to care.
|
||||||
|
- The `hints -> opacity` setting - see the "Changed" section for details.
|
||||||
|
|
||||||
Fixed
|
Fixed
|
||||||
~~~~~
|
~~~~~
|
||||||
|
@ -178,7 +178,6 @@
|
|||||||
|==============
|
|==============
|
||||||
|Setting|Description
|
|Setting|Description
|
||||||
|<<hints-border,border>>|CSS border value for hints.
|
|<<hints-border,border>>|CSS border value for hints.
|
||||||
|<<hints-opacity,opacity>>|Opacity for hints.
|
|
||||||
|<<hints-mode,mode>>|Mode to use for hints.
|
|<<hints-mode,mode>>|Mode to use for hints.
|
||||||
|<<hints-chars,chars>>|Chars used for hint strings.
|
|<<hints-chars,chars>>|Chars used for hint strings.
|
||||||
|<<hints-min-chars,min-chars>>|Minimum number of chars used for hint strings.
|
|<<hints-min-chars,min-chars>>|Minimum number of chars used for hint strings.
|
||||||
@ -248,7 +247,7 @@
|
|||||||
|<<colors-tabs.indicator.error,tabs.indicator.error>>|Color for the tab indicator on errors..
|
|<<colors-tabs.indicator.error,tabs.indicator.error>>|Color for the tab indicator on errors..
|
||||||
|<<colors-tabs.indicator.system,tabs.indicator.system>>|Color gradient interpolation system for the tab indicator.
|
|<<colors-tabs.indicator.system,tabs.indicator.system>>|Color gradient interpolation system for the tab indicator.
|
||||||
|<<colors-hints.fg,hints.fg>>|Font color for hints.
|
|<<colors-hints.fg,hints.fg>>|Font color for hints.
|
||||||
|<<colors-hints.bg,hints.bg>>|Background color for hints.
|
|<<colors-hints.bg,hints.bg>>|Background color for hints. Note that you can use a `rgba(...)` value for transparency.
|
||||||
|<<colors-hints.fg.match,hints.fg.match>>|Font color for the matched part of hints.
|
|<<colors-hints.fg.match,hints.fg.match>>|Font color for the matched part of hints.
|
||||||
|<<colors-downloads.bg.bar,downloads.bg.bar>>|Background color for the download bar.
|
|<<colors-downloads.bg.bar,downloads.bg.bar>>|Background color for the download bar.
|
||||||
|<<colors-downloads.fg.start,downloads.fg.start>>|Color gradient start for download text.
|
|<<colors-downloads.fg.start,downloads.fg.start>>|Color gradient start for download text.
|
||||||
@ -1601,12 +1600,6 @@ CSS border value for hints.
|
|||||||
|
|
||||||
Default: +pass:[1px solid #E3BE23]+
|
Default: +pass:[1px solid #E3BE23]+
|
||||||
|
|
||||||
[[hints-opacity]]
|
|
||||||
=== opacity
|
|
||||||
Opacity for hints.
|
|
||||||
|
|
||||||
Default: +pass:[0.7]+
|
|
||||||
|
|
||||||
[[hints-mode]]
|
[[hints-mode]]
|
||||||
=== mode
|
=== mode
|
||||||
Mode to use for hints.
|
Mode to use for hints.
|
||||||
@ -2052,9 +2045,9 @@ Default: +pass:[black]+
|
|||||||
|
|
||||||
[[colors-hints.bg]]
|
[[colors-hints.bg]]
|
||||||
=== hints.bg
|
=== hints.bg
|
||||||
Background color for hints.
|
Background color for hints. Note that you can use a `rgba(...)` value for transparency.
|
||||||
|
|
||||||
Default: +pass:[-webkit-gradient(linear, left top, left bottom, color-stop(0%,#FFF785), color-stop(100%,#FFC542))]+
|
Default: +pass:[qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 rgba(255, 247, 133, 0.8), stop:1 rgba(255, 197, 66, 0.8))]+
|
||||||
|
|
||||||
[[colors-hints.fg.match]]
|
[[colors-hints.fg.match]]
|
||||||
=== hints.fg.match
|
=== hints.fg.match
|
||||||
@ -2201,7 +2194,7 @@ Default: +pass:[8pt ${_monospace}]+
|
|||||||
=== hints
|
=== hints
|
||||||
Font used for the hints.
|
Font used for the hints.
|
||||||
|
|
||||||
Default: +pass:[bold 13px Monospace]+
|
Default: +pass:[bold 13px ${_monospace}]+
|
||||||
|
|
||||||
[[fonts-debug-console]]
|
[[fonts-debug-console]]
|
||||||
=== debug-console
|
=== debug-console
|
||||||
|
@ -23,23 +23,22 @@ 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, style
|
||||||
from qutebrowser.keyinput import modeman, modeparsers
|
from qutebrowser.keyinput import modeman, modeparsers
|
||||||
from qutebrowser.browser import webelem
|
from qutebrowser.browser import webelem
|
||||||
from qutebrowser.commands import userscripts, cmdexc, cmdutils, runners
|
from qutebrowser.commands import userscripts, cmdexc, cmdutils, runners
|
||||||
from qutebrowser.utils import usertypes, log, qtutils, message, objreg, utils
|
from qutebrowser.utils import usertypes, log, qtutils, message, objreg, utils
|
||||||
|
|
||||||
|
|
||||||
ElemTuple = collections.namedtuple('ElemTuple', ['elem', 'label'])
|
|
||||||
|
|
||||||
|
|
||||||
Target = usertypes.enum('Target', ['normal', 'current', 'tab', 'tab_fg',
|
Target = usertypes.enum('Target', ['normal', 'current', 'tab', 'tab_fg',
|
||||||
'tab_bg', 'window', 'yank', 'yank_primary',
|
'tab_bg', 'window', 'yank', 'yank_primary',
|
||||||
'run', 'fill', 'hover', 'download',
|
'run', 'fill', 'hover', 'download',
|
||||||
@ -57,14 +56,91 @@ 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.
|
||||||
|
"""
|
||||||
|
|
||||||
|
STYLESHEET = """
|
||||||
|
QLabel {
|
||||||
|
background-color: {{ color['hints.bg'] }};
|
||||||
|
color: {{ color['hints.fg'] }};
|
||||||
|
font: {{ font['hints'] }};
|
||||||
|
border: {{ config.get('hints', 'border') }};
|
||||||
|
padding-left: -3px;
|
||||||
|
padding-right: -3px;
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, elem, context):
|
||||||
|
super().__init__(parent=context.tab)
|
||||||
|
self._context = context
|
||||||
|
self.elem = elem
|
||||||
|
|
||||||
|
self.setAttribute(Qt.WA_StyledBackground, True)
|
||||||
|
style.set_register_stylesheet(self)
|
||||||
|
|
||||||
|
self._context.tab.contents_size_changed.connect(self._move_to_elem)
|
||||||
|
self._move_to_elem()
|
||||||
|
self.show()
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
try:
|
||||||
|
text = self.text()
|
||||||
|
except RuntimeError:
|
||||||
|
text = '<deleted>'
|
||||||
|
return utils.get_repr(self, elem=self.elem, text=text)
|
||||||
|
|
||||||
|
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))
|
||||||
|
self.adjustSize()
|
||||||
|
|
||||||
|
@pyqtSlot()
|
||||||
|
def _move_to_elem(self):
|
||||||
|
"""Reposition the label to its element."""
|
||||||
|
if self.elem.frame() is None:
|
||||||
|
# This sometimes happens for some reason...
|
||||||
|
log.hints.debug("Frame for {!r} vanished!".format(self))
|
||||||
|
self.hide()
|
||||||
|
return
|
||||||
|
no_js = config.get('hints', 'find-implementation') != 'javascript'
|
||||||
|
rect = self.elem.rect_on_view(no_js=no_js)
|
||||||
|
self.move(rect.x(), rect.y())
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
all_elems: A list of all (elem, label) namedtuples ever created.
|
all_labels: A list of all HintLabel objects ever created.
|
||||||
elems: A mapping from key strings to (elem, label) namedtuples.
|
labels: A mapping from key strings to HintLabel objects.
|
||||||
May contain less elements than `all_elems` due to filtering.
|
May contain less elements than `all_labels` due to filtering.
|
||||||
baseurl: The URL of the current page.
|
baseurl: The URL of the current page.
|
||||||
target: What to do with the opened links.
|
target: What to do with the opened links.
|
||||||
normal/current/tab/tab_fg/tab_bg/window: Get passed to
|
normal/current/tab/tab_fg/tab_bg/window: Get passed to
|
||||||
@ -84,8 +160,8 @@ class HintContext:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.all_elems = []
|
self.all_labels = []
|
||||||
self.elems = {}
|
self.labels = {}
|
||||||
self.target = None
|
self.target = None
|
||||||
self.baseurl = None
|
self.baseurl = None
|
||||||
self.to_follow = None
|
self.to_follow = None
|
||||||
@ -362,18 +438,9 @@ class HintManager(QObject):
|
|||||||
|
|
||||||
def _cleanup(self):
|
def _cleanup(self):
|
||||||
"""Clean up after hinting."""
|
"""Clean up after hinting."""
|
||||||
try:
|
for label in self._context.all_labels:
|
||||||
self._context.tab.contents_size_changed.disconnect(
|
label.cleanup()
|
||||||
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
|
|
||||||
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 +580,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 +632,18 @@ 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):
|
|
||||||
label = self._draw_label(e, string)
|
for elem, string in zip(elems, strings):
|
||||||
elem = ElemTuple(e, label)
|
label = HintLabel(elem, self._context)
|
||||||
self._context.all_elems.append(elem)
|
label.update_text('', string)
|
||||||
self._context.elems[string] = elem
|
self._context.all_labels.append(label)
|
||||||
|
self._context.labels[string] = label
|
||||||
|
|
||||||
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 +763,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: label
|
||||||
|
for string, label in self._context.labels.items()
|
||||||
|
if label.isVisible()}
|
||||||
|
|
||||||
if len(visible) != 1:
|
if len(visible) != 1:
|
||||||
return
|
return
|
||||||
@ -825,24 +802,20 @@ class HintManager(QObject):
|
|||||||
def handle_partial_key(self, keystr):
|
def handle_partial_key(self, keystr):
|
||||||
"""Handle a new partial keypress."""
|
"""Handle a new partial keypress."""
|
||||||
log.hints.debug("Handling new keystring: '{}'".format(keystr))
|
log.hints.debug("Handling new keystring: '{}'".format(keystr))
|
||||||
for string, elem in self._context.elems.items():
|
for string, label in self._context.labels.items():
|
||||||
try:
|
try:
|
||||||
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')
|
label.update_text(matched, rest)
|
||||||
elem.label.set_inner_xml(
|
# Show label again if it was hidden before
|
||||||
'<font color="{}">{}</font>{}'.format(
|
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)
|
label.hide()
|
||||||
except webelem.Error:
|
except webelem.Error:
|
||||||
pass
|
pass
|
||||||
self._handle_auto_follow(keystr=keystr)
|
self._handle_auto_follow(keystr=keystr)
|
||||||
@ -862,16 +835,15 @@ class HintManager(QObject):
|
|||||||
self._context.filterstr = filterstr
|
self._context.filterstr = filterstr
|
||||||
|
|
||||||
visible = []
|
visible = []
|
||||||
for elem in self._context.all_elems:
|
for label in self._context.all_labels:
|
||||||
try:
|
try:
|
||||||
if self._filter_matches(filterstr, str(elem.elem)):
|
if self._filter_matches(filterstr, str(label.elem)):
|
||||||
visible.append(elem)
|
visible.append(label)
|
||||||
if self._is_hidden(elem.label):
|
# Show label again if it was hidden before
|
||||||
# hidden element which matches again -> show it
|
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)
|
label.hide()
|
||||||
except webelem.Error:
|
except webelem.Error:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -884,10 +856,10 @@ class HintManager(QObject):
|
|||||||
if self._context.hint_mode == 'number':
|
if self._context.hint_mode == 'number':
|
||||||
# renumber filtered hints
|
# renumber filtered hints
|
||||||
strings = self._hint_strings(visible)
|
strings = self._hint_strings(visible)
|
||||||
self._context.elems = {}
|
self._context.labels = {}
|
||||||
for elem, string in zip(visible, strings):
|
for label, string in zip(visible, strings):
|
||||||
elem.label.set_inner_xml(string)
|
label.update_text('', string)
|
||||||
self._context.elems[string] = elem
|
self._context.labels[string] = label
|
||||||
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]
|
||||||
@ -896,9 +868,9 @@ class HintManager(QObject):
|
|||||||
# Note: filter_hints can be called with non-None filterstr only
|
# Note: filter_hints can be called with non-None filterstr only
|
||||||
# when number mode is active
|
# when number mode is active
|
||||||
if filterstr is not None:
|
if filterstr is not None:
|
||||||
# pass self._context.elems as the dict of visible hints
|
# pass self._context.labels as the dict of visible hints
|
||||||
self._handle_auto_follow(filterstr=filterstr,
|
self._handle_auto_follow(filterstr=filterstr,
|
||||||
visible=self._context.elems)
|
visible=self._context.labels)
|
||||||
|
|
||||||
def _fire(self, keystr):
|
def _fire(self, keystr):
|
||||||
"""Fire a completed hint.
|
"""Fire a completed hint.
|
||||||
@ -927,7 +899,7 @@ class HintManager(QObject):
|
|||||||
Target.fill: self._actions.preset_cmd_text,
|
Target.fill: self._actions.preset_cmd_text,
|
||||||
Target.spawn: self._actions.spawn,
|
Target.spawn: self._actions.spawn,
|
||||||
}
|
}
|
||||||
elem = self._context.elems[keystr].elem
|
elem = self._context.labels[keystr].elem
|
||||||
|
|
||||||
if elem.frame() is None:
|
if elem.frame() is None:
|
||||||
message.error(self._win_id,
|
message.error(self._win_id,
|
||||||
@ -955,8 +927,8 @@ class HintManager(QObject):
|
|||||||
# Reset filtering
|
# Reset filtering
|
||||||
self.filter_hints(None)
|
self.filter_hints(None)
|
||||||
# Undo keystring highlighting
|
# Undo keystring highlighting
|
||||||
for string, elem in self._context.elems.items():
|
for string, label in self._context.labels.items():
|
||||||
elem.label.set_inner_xml(string)
|
label.update_text('', string)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
handler()
|
handler()
|
||||||
@ -976,24 +948,10 @@ class HintManager(QObject):
|
|||||||
raise cmdexc.CommandError("No hint to follow")
|
raise cmdexc.CommandError("No hint to follow")
|
||||||
else:
|
else:
|
||||||
keystring = self._context.to_follow
|
keystring = self._context.to_follow
|
||||||
elif keystring not in self._context.elems:
|
elif keystring not in self._context.labels:
|
||||||
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."""
|
||||||
|
@ -112,21 +112,6 @@ class AbstractWebElement(collections.abc.MutableMapping):
|
|||||||
"""Get the geometry for this element."""
|
"""Get the geometry for this element."""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def document_element(self):
|
|
||||||
"""Get the document element of this element."""
|
|
||||||
# FIXME:qtwebengine get rid of this?
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def create_inside(self, tagname):
|
|
||||||
"""Append the given element inside the current one."""
|
|
||||||
# FIXME:qtwebengine get rid of this?
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def find_first(self, selector):
|
|
||||||
"""Find the first child based on the given CSS selector."""
|
|
||||||
# FIXME:qtwebengine get rid of this?
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def style_property(self, name, *, strategy):
|
def style_property(self, name, *, strategy):
|
||||||
"""Get the element style resolved with the given strategy."""
|
"""Get the element style resolved with the given strategy."""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
@ -166,21 +151,6 @@ class AbstractWebElement(collections.abc.MutableMapping):
|
|||||||
# FIXME:qtwebengine what to do about use_js with WebEngine?
|
# FIXME:qtwebengine what to do about use_js with WebEngine?
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def set_inner_xml(self, xml):
|
|
||||||
"""Set the given inner XML."""
|
|
||||||
# FIXME:qtwebengine get rid of this?
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def remove_from_document(self):
|
|
||||||
"""Remove the node from the document."""
|
|
||||||
# FIXME:qtwebengine get rid of this?
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def set_style_property(self, name, value):
|
|
||||||
"""Set the element style."""
|
|
||||||
# FIXME:qtwebengine get rid of this?
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def run_js_async(self, code, callback=None):
|
def run_js_async(self, code, callback=None):
|
||||||
"""Run the given JS snippet async on the element."""
|
"""Run the given JS snippet async on the element."""
|
||||||
# FIXME:qtwebengine get rid of this?
|
# FIXME:qtwebengine get rid of this?
|
||||||
@ -191,8 +161,7 @@ class AbstractWebElement(collections.abc.MutableMapping):
|
|||||||
# FIXME:qtwebengine get rid of this?
|
# FIXME:qtwebengine get rid of this?
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def rect_on_view(self, *, elem_geometry=None, adjust_zoom=True,
|
def rect_on_view(self, *, elem_geometry=None, no_js=False):
|
||||||
no_js=False):
|
|
||||||
"""Get the geometry of the element relative to the webview.
|
"""Get the geometry of the element relative to the webview.
|
||||||
|
|
||||||
Uses the getClientRects() JavaScript method to obtain the collection of
|
Uses the getClientRects() JavaScript method to obtain the collection of
|
||||||
@ -208,8 +177,6 @@ class AbstractWebElement(collections.abc.MutableMapping):
|
|||||||
elem_geometry: The geometry of the element, or None.
|
elem_geometry: The geometry of the element, or None.
|
||||||
Calling QWebElement::geometry is rather expensive so
|
Calling QWebElement::geometry is rather expensive so
|
||||||
we want to avoid doing it twice.
|
we want to avoid doing it twice.
|
||||||
adjust_zoom: Whether to adjust the element position based on the
|
|
||||||
current zoom level.
|
|
||||||
no_js: Fall back to the Python implementation
|
no_js: Fall back to the Python implementation
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
@ -66,18 +66,6 @@ class WebEngineElement(webelem.AbstractWebElement):
|
|||||||
log.stub()
|
log.stub()
|
||||||
return QRect()
|
return QRect()
|
||||||
|
|
||||||
def document_element(self):
|
|
||||||
log.stub()
|
|
||||||
return None
|
|
||||||
|
|
||||||
def create_inside(self, tagname):
|
|
||||||
log.stub()
|
|
||||||
return None
|
|
||||||
|
|
||||||
def find_first(self, selector):
|
|
||||||
log.stub()
|
|
||||||
return None
|
|
||||||
|
|
||||||
def style_property(self, name, *, strategy):
|
def style_property(self, name, *, strategy):
|
||||||
log.stub()
|
log.stub()
|
||||||
return ''
|
return ''
|
||||||
@ -121,21 +109,6 @@ class WebEngineElement(webelem.AbstractWebElement):
|
|||||||
js_code = javascript.assemble('webelem', 'set_text', self._id, text)
|
js_code = javascript.assemble('webelem', 'set_text', self._id, text)
|
||||||
self._run_js(js_code)
|
self._run_js(js_code)
|
||||||
|
|
||||||
def set_inner_xml(self, xml):
|
|
||||||
"""Set the given inner XML."""
|
|
||||||
# FIXME:qtwebengine get rid of this?
|
|
||||||
log.stub()
|
|
||||||
|
|
||||||
def remove_from_document(self):
|
|
||||||
"""Remove the node from the document."""
|
|
||||||
# FIXME:qtwebengine get rid of this?
|
|
||||||
log.stub()
|
|
||||||
|
|
||||||
def set_style_property(self, name, value):
|
|
||||||
"""Set the element style."""
|
|
||||||
# FIXME:qtwebengine get rid of this?
|
|
||||||
log.stub()
|
|
||||||
|
|
||||||
def run_js_async(self, code, callback=None):
|
def run_js_async(self, code, callback=None):
|
||||||
"""Run the given JS snippet async on the element."""
|
"""Run the given JS snippet async on the element."""
|
||||||
# FIXME:qtwebengine get rid of this?
|
# FIXME:qtwebengine get rid of this?
|
||||||
@ -147,8 +120,7 @@ class WebEngineElement(webelem.AbstractWebElement):
|
|||||||
log.stub()
|
log.stub()
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def rect_on_view(self, *, elem_geometry=None, adjust_zoom=True,
|
def rect_on_view(self, *, elem_geometry=None, no_js=False):
|
||||||
no_js=False):
|
|
||||||
"""Get the geometry of the element relative to the webview.
|
"""Get the geometry of the element relative to the webview.
|
||||||
|
|
||||||
Uses the getClientRects() JavaScript method to obtain the collection of
|
Uses the getClientRects() JavaScript method to obtain the collection of
|
||||||
@ -164,8 +136,6 @@ class WebEngineElement(webelem.AbstractWebElement):
|
|||||||
elem_geometry: The geometry of the element, or None.
|
elem_geometry: The geometry of the element, or None.
|
||||||
Calling QWebElement::geometry is rather expensive so
|
Calling QWebElement::geometry is rather expensive so
|
||||||
we want to avoid doing it twice.
|
we want to avoid doing it twice.
|
||||||
adjust_zoom: Whether to adjust the element position based on the
|
|
||||||
current zoom level.
|
|
||||||
no_js: Fall back to the Python implementation
|
no_js: Fall back to the Python implementation
|
||||||
"""
|
"""
|
||||||
log.stub()
|
log.stub()
|
||||||
|
@ -91,28 +91,6 @@ class WebKitElement(webelem.AbstractWebElement):
|
|||||||
self._check_vanished()
|
self._check_vanished()
|
||||||
return self._elem.geometry()
|
return self._elem.geometry()
|
||||||
|
|
||||||
def document_element(self):
|
|
||||||
self._check_vanished()
|
|
||||||
elem = self._elem.webFrame().documentElement()
|
|
||||||
return WebKitElement(elem)
|
|
||||||
|
|
||||||
def create_inside(self, tagname):
|
|
||||||
# It seems impossible to create an empty QWebElement for which isNull()
|
|
||||||
# is false so we can work with it.
|
|
||||||
# As a workaround, we use appendInside() with markup as argument, and
|
|
||||||
# then use lastChild() to get a reference to it.
|
|
||||||
# See: http://stackoverflow.com/q/7364852/2085149
|
|
||||||
self._check_vanished()
|
|
||||||
self._elem.appendInside('<{}></{}>'.format(tagname, tagname))
|
|
||||||
return WebKitElement(self._elem.lastChild())
|
|
||||||
|
|
||||||
def find_first(self, selector):
|
|
||||||
self._check_vanished()
|
|
||||||
elem = self._elem.findFirst(selector)
|
|
||||||
if elem.isNull():
|
|
||||||
return None
|
|
||||||
return WebKitElement(elem)
|
|
||||||
|
|
||||||
def style_property(self, name, *, strategy):
|
def style_property(self, name, *, strategy):
|
||||||
self._check_vanished()
|
self._check_vanished()
|
||||||
strategies = {
|
strategies = {
|
||||||
@ -156,18 +134,6 @@ class WebKitElement(webelem.AbstractWebElement):
|
|||||||
text = javascript.string_escape(text)
|
text = javascript.string_escape(text)
|
||||||
self._elem.evaluateJavaScript("this.value='{}'".format(text))
|
self._elem.evaluateJavaScript("this.value='{}'".format(text))
|
||||||
|
|
||||||
def set_inner_xml(self, xml):
|
|
||||||
self._check_vanished()
|
|
||||||
self._elem.setInnerXml(xml)
|
|
||||||
|
|
||||||
def remove_from_document(self):
|
|
||||||
self._check_vanished()
|
|
||||||
self._elem.removeFromDocument()
|
|
||||||
|
|
||||||
def set_style_property(self, name, value):
|
|
||||||
self._check_vanished()
|
|
||||||
return self._elem.setStyleProperty(name, value)
|
|
||||||
|
|
||||||
def run_js_async(self, code, callback=None):
|
def run_js_async(self, code, callback=None):
|
||||||
"""Run the given JS snippet async on the element."""
|
"""Run the given JS snippet async on the element."""
|
||||||
self._check_vanished()
|
self._check_vanished()
|
||||||
@ -182,7 +148,7 @@ class WebKitElement(webelem.AbstractWebElement):
|
|||||||
return None
|
return None
|
||||||
return WebKitElement(elem)
|
return WebKitElement(elem)
|
||||||
|
|
||||||
def _rect_on_view_js(self, adjust_zoom):
|
def _rect_on_view_js(self):
|
||||||
"""Javascript implementation for rect_on_view."""
|
"""Javascript implementation for rect_on_view."""
|
||||||
# FIXME:qtwebengine maybe we can reuse this?
|
# FIXME:qtwebengine maybe we can reuse this?
|
||||||
rects = self._elem.evaluateJavaScript("this.getClientRects()")
|
rects = self._elem.evaluateJavaScript("this.getClientRects()")
|
||||||
@ -203,7 +169,7 @@ class WebKitElement(webelem.AbstractWebElement):
|
|||||||
if width > 1 and height > 1:
|
if width > 1 and height > 1:
|
||||||
# fix coordinates according to zoom level
|
# fix coordinates according to zoom level
|
||||||
zoom = self._elem.webFrame().zoomFactor()
|
zoom = self._elem.webFrame().zoomFactor()
|
||||||
if not config.get('ui', 'zoom-text-only') and adjust_zoom:
|
if not config.get('ui', 'zoom-text-only'):
|
||||||
rect["left"] *= zoom
|
rect["left"] *= zoom
|
||||||
rect["top"] *= zoom
|
rect["top"] *= zoom
|
||||||
width *= zoom
|
width *= zoom
|
||||||
@ -231,18 +197,9 @@ class WebKitElement(webelem.AbstractWebElement):
|
|||||||
rect.translate(frame.geometry().topLeft())
|
rect.translate(frame.geometry().topLeft())
|
||||||
rect.translate(frame.scrollPosition() * -1)
|
rect.translate(frame.scrollPosition() * -1)
|
||||||
frame = frame.parentFrame()
|
frame = frame.parentFrame()
|
||||||
# We deliberately always adjust the zoom here, even with
|
|
||||||
# adjust_zoom=False
|
|
||||||
if elem_geometry is None:
|
|
||||||
zoom = self._elem.webFrame().zoomFactor()
|
|
||||||
if not config.get('ui', 'zoom-text-only'):
|
|
||||||
rect.moveTo(rect.left() / zoom, rect.top() / zoom)
|
|
||||||
rect.setWidth(rect.width() / zoom)
|
|
||||||
rect.setHeight(rect.height() / zoom)
|
|
||||||
return rect
|
return rect
|
||||||
|
|
||||||
def rect_on_view(self, *, elem_geometry=None, adjust_zoom=True,
|
def rect_on_view(self, *, elem_geometry=None, no_js=False):
|
||||||
no_js=False):
|
|
||||||
"""Get the geometry of the element relative to the webview.
|
"""Get the geometry of the element relative to the webview.
|
||||||
|
|
||||||
Uses the getClientRects() JavaScript method to obtain the collection of
|
Uses the getClientRects() JavaScript method to obtain the collection of
|
||||||
@ -258,8 +215,6 @@ class WebKitElement(webelem.AbstractWebElement):
|
|||||||
elem_geometry: The geometry of the element, or None.
|
elem_geometry: The geometry of the element, or None.
|
||||||
Calling QWebElement::geometry is rather expensive so
|
Calling QWebElement::geometry is rather expensive so
|
||||||
we want to avoid doing it twice.
|
we want to avoid doing it twice.
|
||||||
adjust_zoom: Whether to adjust the element position based on the
|
|
||||||
current zoom level.
|
|
||||||
no_js: Fall back to the Python implementation
|
no_js: Fall back to the Python implementation
|
||||||
"""
|
"""
|
||||||
# FIXME:qtwebengine can we get rid of this with
|
# FIXME:qtwebengine can we get rid of this with
|
||||||
@ -269,7 +224,7 @@ class WebKitElement(webelem.AbstractWebElement):
|
|||||||
# First try getting the element rect via JS, as that's usually more
|
# First try getting the element rect via JS, as that's usually more
|
||||||
# accurate
|
# accurate
|
||||||
if elem_geometry is None and not no_js:
|
if elem_geometry is None and not no_js:
|
||||||
rect = self._rect_on_view_js(adjust_zoom)
|
rect = self._rect_on_view_js()
|
||||||
if rect is not None:
|
if rect is not None:
|
||||||
return rect
|
return rect
|
||||||
|
|
||||||
|
@ -24,6 +24,7 @@ are fundamentally different. This is why nothing inherits from configparser,
|
|||||||
but we borrow some methods and classes from there where it makes sense.
|
but we borrow some methods and classes from there where it makes sense.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import os.path
|
import os.path
|
||||||
@ -34,6 +35,7 @@ import collections
|
|||||||
import collections.abc
|
import collections.abc
|
||||||
|
|
||||||
from PyQt5.QtCore import pyqtSignal, QObject, QUrl, QSettings
|
from PyQt5.QtCore import pyqtSignal, QObject, QUrl, QSettings
|
||||||
|
from PyQt5.QtGui import QColor
|
||||||
|
|
||||||
from qutebrowser.config import configdata, configexc, textwrapper
|
from qutebrowser.config import configdata, configexc, textwrapper
|
||||||
from qutebrowser.config.parsers import keyconf
|
from qutebrowser.config.parsers import keyconf
|
||||||
@ -286,6 +288,50 @@ def _transform_position(val):
|
|||||||
return val
|
return val
|
||||||
|
|
||||||
|
|
||||||
|
def _transform_hint_color(val):
|
||||||
|
"""Transformer for hint colors."""
|
||||||
|
log.config.debug("Transforming hint value {}".format(val))
|
||||||
|
|
||||||
|
def to_rgba(qcolor):
|
||||||
|
"""Convert a QColor to a rgba() value."""
|
||||||
|
return 'rgba({}, {}, {}, 0.8)'.format(qcolor.red(), qcolor.green(),
|
||||||
|
qcolor.blue())
|
||||||
|
|
||||||
|
if val.startswith('-webkit-gradient'):
|
||||||
|
pattern = re.compile(r'-webkit-gradient\(linear, left top, '
|
||||||
|
r'left bottom, '
|
||||||
|
r'color-stop\(0%, *([^)]*)\), '
|
||||||
|
r'color-stop\(100%, *([^)]*)\)\)')
|
||||||
|
|
||||||
|
match = pattern.fullmatch(val)
|
||||||
|
if match:
|
||||||
|
log.config.debug('Color groups: {}'.format(match.groups()))
|
||||||
|
start_color = QColor(match.group(1))
|
||||||
|
stop_color = QColor(match.group(2))
|
||||||
|
if not start_color.isValid() or not stop_color.isValid():
|
||||||
|
return None
|
||||||
|
|
||||||
|
return ('qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 {}, '
|
||||||
|
'stop:1 {})'.format(to_rgba(start_color),
|
||||||
|
to_rgba(stop_color)))
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
elif val.startswith('-'): # Custom CSS stuff?
|
||||||
|
return None
|
||||||
|
else: # Already transformed or a named color.
|
||||||
|
return val
|
||||||
|
|
||||||
|
|
||||||
|
def _transform_hint_font(val):
|
||||||
|
"""Transformer for fonts -> hints."""
|
||||||
|
match = re.fullmatch(r'(.*\d+p[xt]) Monospace', val)
|
||||||
|
if match:
|
||||||
|
# Close enough to the old default:
|
||||||
|
return match.group(1) + ' ${_monospace}'
|
||||||
|
else:
|
||||||
|
return val
|
||||||
|
|
||||||
|
|
||||||
class ConfigManager(QObject):
|
class ConfigManager(QObject):
|
||||||
|
|
||||||
"""Configuration manager for qutebrowser.
|
"""Configuration manager for qutebrowser.
|
||||||
@ -353,6 +399,7 @@ class ConfigManager(QObject):
|
|||||||
('ui', 'display-statusbar-messages'),
|
('ui', 'display-statusbar-messages'),
|
||||||
('ui', 'hide-mouse-cursor'),
|
('ui', 'hide-mouse-cursor'),
|
||||||
('general', 'wrap-search'),
|
('general', 'wrap-search'),
|
||||||
|
('hints', 'opacity'),
|
||||||
]
|
]
|
||||||
CHANGED_OPTIONS = {
|
CHANGED_OPTIONS = {
|
||||||
('content', 'cookies-accept'):
|
('content', 'cookies-accept'):
|
||||||
@ -367,6 +414,10 @@ class ConfigManager(QObject):
|
|||||||
_get_value_transformer({'false': '*', 'true': ''}),
|
_get_value_transformer({'false': '*', 'true': ''}),
|
||||||
('hints', 'auto-follow'):
|
('hints', 'auto-follow'):
|
||||||
_get_value_transformer({'false': 'never', 'true': 'unique-match'}),
|
_get_value_transformer({'false': 'never', 'true': 'unique-match'}),
|
||||||
|
('colors', 'hints.bg'): _transform_hint_color,
|
||||||
|
('colors', 'hints.fg'): _transform_hint_color,
|
||||||
|
('colors', 'hints.fg.match'): _transform_hint_color,
|
||||||
|
('fonts', 'hints'): _transform_hint_font,
|
||||||
}
|
}
|
||||||
|
|
||||||
changed = pyqtSignal(str, str)
|
changed = pyqtSignal(str, str)
|
||||||
@ -525,7 +576,15 @@ class ConfigManager(QObject):
|
|||||||
k = self.RENAMED_OPTIONS[sectname, k]
|
k = self.RENAMED_OPTIONS[sectname, k]
|
||||||
if (sectname, k) in self.CHANGED_OPTIONS:
|
if (sectname, k) in self.CHANGED_OPTIONS:
|
||||||
func = self.CHANGED_OPTIONS[(sectname, k)]
|
func = self.CHANGED_OPTIONS[(sectname, k)]
|
||||||
v = func(v)
|
new_v = func(v)
|
||||||
|
if new_v is None:
|
||||||
|
exc = configexc.ValidationError(
|
||||||
|
v, "Could not automatically migrate the given value")
|
||||||
|
exc.section = sectname
|
||||||
|
exc.option = k
|
||||||
|
raise exc
|
||||||
|
|
||||||
|
v = new_v
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.set('conf', sectname, k, v, validate=False)
|
self.set('conf', sectname, k, v, validate=False)
|
||||||
|
@ -897,10 +897,6 @@ def data(readonly=False):
|
|||||||
SettingValue(typ.String(), '1px solid #E3BE23'),
|
SettingValue(typ.String(), '1px solid #E3BE23'),
|
||||||
"CSS border value for hints."),
|
"CSS border value for hints."),
|
||||||
|
|
||||||
('opacity',
|
|
||||||
SettingValue(typ.Float(minval=0.0, maxval=1.0), '0.7'),
|
|
||||||
"Opacity for hints."),
|
|
||||||
|
|
||||||
('mode',
|
('mode',
|
||||||
SettingValue(typ.String(
|
SettingValue(typ.String(
|
||||||
valid_values=typ.ValidValues(
|
valid_values=typ.ValidValues(
|
||||||
@ -1209,18 +1205,18 @@ def data(readonly=False):
|
|||||||
"Color gradient interpolation system for the tab indicator."),
|
"Color gradient interpolation system for the tab indicator."),
|
||||||
|
|
||||||
('hints.fg',
|
('hints.fg',
|
||||||
SettingValue(typ.CssColor(), 'black'),
|
SettingValue(typ.QssColor(), 'black'),
|
||||||
"Font color for hints."),
|
"Font color for hints."),
|
||||||
|
|
||||||
('hints.bg',
|
('hints.bg',
|
||||||
SettingValue(
|
SettingValue(typ.QssColor(), 'qlineargradient(x1:0, y1:0, x2:0, '
|
||||||
typ.CssColor(), '-webkit-gradient(linear, left top, '
|
'y2:1, stop:0 rgba(255, 247, 133, 0.8), '
|
||||||
'left bottom, color-stop(0%,#FFF785), '
|
'stop:1 rgba(255, 197, 66, 0.8))'),
|
||||||
'color-stop(100%,#FFC542))'),
|
"Background color for hints. Note that you can use a `rgba(...)` "
|
||||||
"Background color for hints."),
|
"value for transparency."),
|
||||||
|
|
||||||
('hints.fg.match',
|
('hints.fg.match',
|
||||||
SettingValue(typ.CssColor(), 'green'),
|
SettingValue(typ.QssColor(), 'green'),
|
||||||
"Font color for the matched part of hints."),
|
"Font color for the matched part of hints."),
|
||||||
|
|
||||||
('downloads.bg.bar',
|
('downloads.bg.bar',
|
||||||
@ -1309,7 +1305,7 @@ def data(readonly=False):
|
|||||||
"Font used for the downloadbar."),
|
"Font used for the downloadbar."),
|
||||||
|
|
||||||
('hints',
|
('hints',
|
||||||
SettingValue(typ.Font(), 'bold 13px Monospace'),
|
SettingValue(typ.Font(), 'bold 13px ${_monospace}'),
|
||||||
"Font used for the hints."),
|
"Font used for the hints."),
|
||||||
|
|
||||||
('debug-console',
|
('debug-console',
|
||||||
|
@ -61,6 +61,8 @@ def whitelist_generator():
|
|||||||
yield 'qutebrowser.utils.debug.qflags_key'
|
yield 'qutebrowser.utils.debug.qflags_key'
|
||||||
yield 'qutebrowser.utils.qtutils.QtOSError.qt_errno'
|
yield 'qutebrowser.utils.qtutils.QtOSError.qt_errno'
|
||||||
yield 'scripts.utils.bg_colors'
|
yield 'scripts.utils.bg_colors'
|
||||||
|
yield 'qutebrowser.browser.webelem.AbstractWebElement.style_property'
|
||||||
|
yield 'qutebrowser.config.configtypes.Float'
|
||||||
|
|
||||||
# Qt attributes
|
# Qt attributes
|
||||||
yield 'PyQt5.QtWebKit.QWebPage.ErrorPageExtensionReturn().baseUrl'
|
yield 'PyQt5.QtWebKit.QWebPage.ErrorPageExtensionReturn().baseUrl'
|
||||||
|
@ -80,7 +80,7 @@ class FakeWebFrame:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, geometry=None, *, scroll=None, plaintext=None,
|
def __init__(self, geometry=None, *, scroll=None, plaintext=None,
|
||||||
html=None, parent=None, zoom=1.0, document_element=None):
|
html=None, parent=None, zoom=1.0):
|
||||||
"""Constructor.
|
"""Constructor.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -89,7 +89,6 @@ class FakeWebFrame:
|
|||||||
plaintext: Return value of toPlainText
|
plaintext: Return value of toPlainText
|
||||||
html: Return value of tohtml.
|
html: Return value of tohtml.
|
||||||
zoom: The zoom factor.
|
zoom: The zoom factor.
|
||||||
document_element: The documentElement() to return
|
|
||||||
parent: The parent frame.
|
parent: The parent frame.
|
||||||
"""
|
"""
|
||||||
if scroll is None:
|
if scroll is None:
|
||||||
@ -101,7 +100,6 @@ class FakeWebFrame:
|
|||||||
self.toPlainText = mock.Mock(return_value=plaintext)
|
self.toPlainText = mock.Mock(return_value=plaintext)
|
||||||
self.toHtml = mock.Mock(return_value=html)
|
self.toHtml = mock.Mock(return_value=html)
|
||||||
self.zoomFactor = mock.Mock(return_value=zoom)
|
self.zoomFactor = mock.Mock(return_value=zoom)
|
||||||
self.documentElement = mock.Mock(return_value=document_element)
|
|
||||||
|
|
||||||
def findFirstElement(self, selector):
|
def findFirstElement(self, selector):
|
||||||
if selector == '*:focus':
|
if selector == '*:focus':
|
||||||
|
@ -67,11 +67,13 @@ def get_webelem(geometry=None, frame=None, *, null=False, style=None,
|
|||||||
else:
|
else:
|
||||||
scroll_x = frame.scrollPosition().x()
|
scroll_x = frame.scrollPosition().x()
|
||||||
scroll_y = frame.scrollPosition().y()
|
scroll_y = frame.scrollPosition().y()
|
||||||
|
|
||||||
if js_rect_return is None:
|
if js_rect_return is None:
|
||||||
if frame is None or zoom_text_only:
|
if frame is None or zoom_text_only:
|
||||||
zoom = 1.0
|
zoom = 1.0
|
||||||
else:
|
else:
|
||||||
zoom = frame.zoomFactor()
|
zoom = frame.zoomFactor()
|
||||||
|
|
||||||
elem.evaluateJavaScript.return_value = {
|
elem.evaluateJavaScript.return_value = {
|
||||||
"length": 1,
|
"length": 1,
|
||||||
"0": {
|
"0": {
|
||||||
@ -255,15 +257,9 @@ class TestWebKitElement:
|
|||||||
len,
|
len,
|
||||||
lambda e: e.frame(),
|
lambda e: e.frame(),
|
||||||
lambda e: e.geometry(),
|
lambda e: e.geometry(),
|
||||||
lambda e: e.document_element(),
|
|
||||||
lambda e: e.create_inside('span'),
|
|
||||||
lambda e: e.find_first('span'),
|
|
||||||
lambda e: e.style_property('visibility', strategy='computed'),
|
lambda e: e.style_property('visibility', strategy='computed'),
|
||||||
lambda e: e.text(),
|
lambda e: e.text(),
|
||||||
lambda e: e.set_text('foo'),
|
lambda e: e.set_text('foo'),
|
||||||
lambda e: e.set_inner_xml(''),
|
|
||||||
lambda e: e.remove_from_document(),
|
|
||||||
lambda e: e.set_style_property('visibility', 'hidden'),
|
|
||||||
lambda e: e.is_writable(),
|
lambda e: e.is_writable(),
|
||||||
lambda e: e.is_content_editable(),
|
lambda e: e.is_content_editable(),
|
||||||
lambda e: e.is_editable(),
|
lambda e: e.is_editable(),
|
||||||
@ -276,9 +272,7 @@ class TestWebKitElement:
|
|||||||
lambda e: e.rect_on_view(),
|
lambda e: e.rect_on_view(),
|
||||||
lambda e: e.is_visible(None),
|
lambda e: e.is_visible(None),
|
||||||
], ids=['str', 'getitem', 'setitem', 'delitem', 'contains', 'iter', 'len',
|
], ids=['str', 'getitem', 'setitem', 'delitem', 'contains', 'iter', 'len',
|
||||||
'frame', 'geometry', 'document_element', 'create_inside',
|
'frame', 'geometry', 'style_property', 'text', 'set_text',
|
||||||
'find_first', 'style_property', 'text', 'set_text',
|
|
||||||
'set_inner_xml', 'remove_from_document', 'set_style_property',
|
|
||||||
'is_writable', 'is_content_editable', 'is_editable',
|
'is_writable', 'is_content_editable', 'is_editable',
|
||||||
'is_text_input', 'remove_blank_target', 'debug_text', 'outer_xml',
|
'is_text_input', 'remove_blank_target', 'debug_text', 'outer_xml',
|
||||||
'tag_name', 'run_js_async', 'rect_on_view', 'is_visible'])
|
'tag_name', 'run_js_async', 'rect_on_view', 'is_visible'])
|
||||||
@ -410,17 +404,6 @@ class TestWebKitElement:
|
|||||||
setattr(mock, 'return_value', sentinel)
|
setattr(mock, 'return_value', sentinel)
|
||||||
assert code(elem) is sentinel
|
assert code(elem) is sentinel
|
||||||
|
|
||||||
@pytest.mark.parametrize('code, method, args', [
|
|
||||||
(lambda e: e.set_inner_xml('foo'), 'setInnerXml', ['foo']),
|
|
||||||
(lambda e: e.set_style_property('foo', 'bar'), 'setStyleProperty',
|
|
||||||
['foo', 'bar']),
|
|
||||||
(lambda e: e.remove_from_document(), 'removeFromDocument', []),
|
|
||||||
])
|
|
||||||
def test_simple_setters(self, elem, code, method, args):
|
|
||||||
code(elem)
|
|
||||||
mock = getattr(elem._elem, method)
|
|
||||||
mock.assert_called_with(*args)
|
|
||||||
|
|
||||||
def test_tag_name(self, elem):
|
def test_tag_name(self, elem):
|
||||||
elem._elem.tagName.return_value = 'SPAN'
|
elem._elem.tagName.return_value = 'SPAN'
|
||||||
assert elem.tag_name() == 'span'
|
assert elem.tag_name() == 'span'
|
||||||
@ -428,34 +411,6 @@ class TestWebKitElement:
|
|||||||
def test_style_property(self, elem):
|
def test_style_property(self, elem):
|
||||||
assert elem.style_property('foo', strategy='computed') == 'bar'
|
assert elem.style_property('foo', strategy='computed') == 'bar'
|
||||||
|
|
||||||
def test_document_element(self, stubs):
|
|
||||||
doc_elem = get_webelem()
|
|
||||||
frame = stubs.FakeWebFrame(document_element=doc_elem._elem)
|
|
||||||
elem = get_webelem(frame=frame)
|
|
||||||
|
|
||||||
doc_elem_ret = elem.document_element()
|
|
||||||
assert isinstance(doc_elem_ret, webkitelem.WebKitElement)
|
|
||||||
assert doc_elem_ret == doc_elem
|
|
||||||
|
|
||||||
def test_find_first(self, elem):
|
|
||||||
result = get_webelem()
|
|
||||||
elem._elem.findFirst.return_value = result._elem
|
|
||||||
find_result = elem.find_first('')
|
|
||||||
assert isinstance(find_result, webkitelem.WebKitElement)
|
|
||||||
assert find_result == result
|
|
||||||
|
|
||||||
def test_create_inside(self, elem):
|
|
||||||
child = get_webelem()
|
|
||||||
elem._elem.lastChild.return_value = child._elem
|
|
||||||
assert elem.create_inside('span')._elem is child._elem
|
|
||||||
elem._elem.appendInside.assert_called_with('<span></span>')
|
|
||||||
|
|
||||||
def test_find_first_null(self, elem):
|
|
||||||
nullelem = get_webelem()
|
|
||||||
nullelem._elem.isNull.return_value = True
|
|
||||||
elem._elem.findFirst.return_value = nullelem._elem
|
|
||||||
assert elem.find_first('foo') is None
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('use_js, editable, expected', [
|
@pytest.mark.parametrize('use_js, editable, expected', [
|
||||||
(True, 'false', 'js'),
|
(True, 'false', 'js'),
|
||||||
(True, 'true', 'nojs'),
|
(True, 'true', 'nojs'),
|
||||||
@ -802,20 +757,14 @@ class TestRectOnView:
|
|||||||
|
|
||||||
@pytest.mark.parametrize('js_rect', [None, {}])
|
@pytest.mark.parametrize('js_rect', [None, {}])
|
||||||
@pytest.mark.parametrize('zoom_text_only', [True, False])
|
@pytest.mark.parametrize('zoom_text_only', [True, False])
|
||||||
@pytest.mark.parametrize('adjust_zoom', [True, False])
|
def test_zoomed(self, stubs, config_stub, js_rect, zoom_text_only):
|
||||||
def test_zoomed(self, stubs, config_stub, js_rect, zoom_text_only,
|
|
||||||
adjust_zoom):
|
|
||||||
"""Make sure the coordinates are adjusted when zoomed."""
|
"""Make sure the coordinates are adjusted when zoomed."""
|
||||||
config_stub.data = {'ui': {'zoom-text-only': zoom_text_only}}
|
config_stub.data = {'ui': {'zoom-text-only': zoom_text_only}}
|
||||||
geometry = QRect(10, 10, 4, 4)
|
geometry = QRect(10, 10, 4, 4)
|
||||||
frame = stubs.FakeWebFrame(QRect(0, 0, 100, 100), zoom=0.5)
|
frame = stubs.FakeWebFrame(QRect(0, 0, 100, 100), zoom=0.5)
|
||||||
elem = get_webelem(geometry, frame, js_rect_return=js_rect,
|
elem = get_webelem(geometry, frame, js_rect_return=js_rect,
|
||||||
zoom_text_only=zoom_text_only)
|
zoom_text_only=zoom_text_only)
|
||||||
rect = elem.rect_on_view(adjust_zoom=adjust_zoom)
|
assert elem.rect_on_view() == QRect(10, 10, 4, 4)
|
||||||
if zoom_text_only or (js_rect is None and adjust_zoom):
|
|
||||||
assert rect == QRect(10, 10, 4, 4)
|
|
||||||
else:
|
|
||||||
assert rect == QRect(20, 20, 8, 8)
|
|
||||||
|
|
||||||
|
|
||||||
class TestGetChildFrames:
|
class TestGetChildFrames:
|
||||||
|
@ -210,6 +210,62 @@ class TestConfigParser:
|
|||||||
assert objects.cfg.get('general', 'save-session')
|
assert objects.cfg.get('general', 'save-session')
|
||||||
|
|
||||||
|
|
||||||
|
class TestTransformers:
|
||||||
|
|
||||||
|
"""Test value transformers in CHANGED_OPTIONS."""
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('val, expected', [('a', 'b'), ('c', 'c')])
|
||||||
|
def test_get_value_transformer(self, val, expected):
|
||||||
|
func = config._get_value_transformer({'a': 'b'})
|
||||||
|
assert func(val) == expected
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('val, expected', [
|
||||||
|
('top', 'top'),
|
||||||
|
('north', 'top'),
|
||||||
|
('south', 'bottom'),
|
||||||
|
('west', 'left'),
|
||||||
|
('east', 'right'),
|
||||||
|
])
|
||||||
|
def test_position(self, val, expected):
|
||||||
|
func = config._transform_position
|
||||||
|
assert func(val) == expected
|
||||||
|
|
||||||
|
OLD_GRADIENT = ('-webkit-gradient(linear, left top, left bottom, '
|
||||||
|
'color-stop(0%,{}), color-stop(100%,{}))')
|
||||||
|
NEW_GRADIENT = ('qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 {}, '
|
||||||
|
'stop:1 {})')
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('val, expected', [
|
||||||
|
('-unknown-stuff', None),
|
||||||
|
('blue', 'blue'),
|
||||||
|
('rgba(1, 2, 3, 4)', 'rgba(1, 2, 3, 4)'),
|
||||||
|
('-webkit-gradient(unknown)', None),
|
||||||
|
(OLD_GRADIENT.format('blah', 'blah'), None),
|
||||||
|
(OLD_GRADIENT.format('red', 'green'),
|
||||||
|
NEW_GRADIENT.format('rgba(255, 0, 0, 0.8)', 'rgba(0, 128, 0, 0.8)')),
|
||||||
|
(OLD_GRADIENT.format(' red', ' green'),
|
||||||
|
NEW_GRADIENT.format('rgba(255, 0, 0, 0.8)', 'rgba(0, 128, 0, 0.8)')),
|
||||||
|
(OLD_GRADIENT.format('#101010', ' #202020'),
|
||||||
|
NEW_GRADIENT.format('rgba(16, 16, 16, 0.8)',
|
||||||
|
'rgba(32, 32, 32, 0.8)')),
|
||||||
|
(OLD_GRADIENT.format('#666', ' #777'),
|
||||||
|
NEW_GRADIENT.format('rgba(102, 102, 102, 0.8)',
|
||||||
|
'rgba(119, 119, 119, 0.8)')),
|
||||||
|
(OLD_GRADIENT.format('red', 'green') + 'more stuff', None),
|
||||||
|
])
|
||||||
|
def test_hint_color(self, val, expected):
|
||||||
|
assert config._transform_hint_color(val) == expected
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('val, expected', [
|
||||||
|
('bold 12pt Monospace', 'bold 12pt ${_monospace}'),
|
||||||
|
('23pt Monospace', '23pt ${_monospace}'),
|
||||||
|
('bold 12pt ${_monospace}', 'bold 12pt ${_monospace}'),
|
||||||
|
('bold 12pt Comic Sans MS', 'bold 12pt Comic Sans MS'),
|
||||||
|
])
|
||||||
|
def test_hint_font(self, val, expected):
|
||||||
|
assert config._transform_hint_font(val) == expected
|
||||||
|
|
||||||
|
|
||||||
class TestKeyConfigParser:
|
class TestKeyConfigParser:
|
||||||
|
|
||||||
"""Test config.parsers.keyconf.KeyConfigParser."""
|
"""Test config.parsers.keyconf.KeyConfigParser."""
|
||||||
|
@ -871,7 +871,7 @@ class ColorTests:
|
|||||||
('hsva(359, 255, 255, 255)', [configtypes.QssColor]),
|
('hsva(359, 255, 255, 255)', [configtypes.QssColor]),
|
||||||
('hsv(10%, 10%, 10%)', [configtypes.QssColor]),
|
('hsv(10%, 10%, 10%)', [configtypes.QssColor]),
|
||||||
('hsv(10%,10%,10%)', [configtypes.QssColor]),
|
('hsv(10%,10%,10%)', [configtypes.QssColor]),
|
||||||
('qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 white, '
|
('qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 white, '
|
||||||
'stop: 0.4 gray, stop:1 green)', [configtypes.QssColor]),
|
'stop: 0.4 gray, stop:1 green)', [configtypes.QssColor]),
|
||||||
('qconicalgradient(cx:0.5, cy:0.5, angle:30, stop:0 white, '
|
('qconicalgradient(cx:0.5, cy:0.5, angle:30, stop:0 white, '
|
||||||
'stop:1 #00FF00)', [configtypes.QssColor]),
|
'stop:1 #00FF00)', [configtypes.QssColor]),
|
||||||
|
Loading…
Reference in New Issue
Block a user