Merge branch 'new-hints'

This commit is contained in:
Florian Bruhin 2016-08-18 12:39:20 +02:00
commit 713201aa13
13 changed files with 276 additions and 358 deletions

View File

@ -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
~~~~~ ~~~~~

View File

@ -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

View File

@ -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."""

View File

@ -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

View File

@ -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()

View File

@ -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

View File

@ -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)

View File

@ -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',

View File

@ -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'

View File

@ -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':

View File

@ -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:

View File

@ -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."""

View File

@ -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]),