Merge branch 'new-hints'
This commit is contained in:
commit
713201aa13
@ -42,6 +42,20 @@ Added
|
||||
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
|
||||
already exists.
|
||||
- `: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.
|
||||
- The `ui -> hide-mouse-cursor` setting since it was completely broken and
|
||||
nobody seemed to care.
|
||||
- The `hints -> opacity` setting - see the "Changed" section for details.
|
||||
|
||||
Fixed
|
||||
~~~~~
|
||||
|
@ -178,7 +178,6 @@
|
||||
|==============
|
||||
|Setting|Description
|
||||
|<<hints-border,border>>|CSS border value for hints.
|
||||
|<<hints-opacity,opacity>>|Opacity for hints.
|
||||
|<<hints-mode,mode>>|Mode to use for hints.
|
||||
|<<hints-chars,chars>>|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.system,tabs.indicator.system>>|Color gradient interpolation system for the tab indicator.
|
||||
|<<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-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.
|
||||
@ -1601,12 +1600,6 @@ CSS border value for hints.
|
||||
|
||||
Default: +pass:[1px solid #E3BE23]+
|
||||
|
||||
[[hints-opacity]]
|
||||
=== opacity
|
||||
Opacity for hints.
|
||||
|
||||
Default: +pass:[0.7]+
|
||||
|
||||
[[hints-mode]]
|
||||
=== mode
|
||||
Mode to use for hints.
|
||||
@ -2052,9 +2045,9 @@ Default: +pass:[black]+
|
||||
|
||||
[[colors-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]]
|
||||
=== hints.fg.match
|
||||
@ -2201,7 +2194,7 @@ Default: +pass:[8pt ${_monospace}]+
|
||||
=== hints
|
||||
Font used for the hints.
|
||||
|
||||
Default: +pass:[bold 13px Monospace]+
|
||||
Default: +pass:[bold 13px ${_monospace}]+
|
||||
|
||||
[[fonts-debug-console]]
|
||||
=== debug-console
|
||||
|
@ -23,23 +23,22 @@ import collections
|
||||
import functools
|
||||
import math
|
||||
import re
|
||||
import html
|
||||
from string import ascii_lowercase
|
||||
|
||||
from PyQt5.QtCore import (pyqtSignal, pyqtSlot, QObject, QEvent, Qt, QUrl,
|
||||
QTimer)
|
||||
from PyQt5.QtGui import QMouseEvent
|
||||
from PyQt5.QtWidgets import QLabel
|
||||
from PyQt5.QtWebKitWidgets import QWebPage
|
||||
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.config import config, style
|
||||
from qutebrowser.keyinput import modeman, modeparsers
|
||||
from qutebrowser.browser import webelem
|
||||
from qutebrowser.commands import userscripts, cmdexc, cmdutils, runners
|
||||
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',
|
||||
'tab_bg', 'window', 'yank', 'yank_primary',
|
||||
'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')
|
||||
|
||||
|
||||
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:
|
||||
|
||||
"""Context namespace used for hinting.
|
||||
|
||||
Attributes:
|
||||
all_elems: A list of all (elem, label) namedtuples ever created.
|
||||
elems: A mapping from key strings to (elem, label) namedtuples.
|
||||
May contain less elements than `all_elems` due to filtering.
|
||||
all_labels: A list of all HintLabel objects ever created.
|
||||
labels: A mapping from key strings to HintLabel objects.
|
||||
May contain less elements than `all_labels` due to filtering.
|
||||
baseurl: The URL of the current page.
|
||||
target: What to do with the opened links.
|
||||
normal/current/tab/tab_fg/tab_bg/window: Get passed to
|
||||
@ -84,8 +160,8 @@ class HintContext:
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.all_elems = []
|
||||
self.elems = {}
|
||||
self.all_labels = []
|
||||
self.labels = {}
|
||||
self.target = None
|
||||
self.baseurl = None
|
||||
self.to_follow = None
|
||||
@ -362,18 +438,9 @@ class HintManager(QObject):
|
||||
|
||||
def _cleanup(self):
|
||||
"""Clean up after hinting."""
|
||||
try:
|
||||
self._context.tab.contents_size_changed.disconnect(
|
||||
self.on_contents_size_changed)
|
||||
except TypeError:
|
||||
# For some reason, this can fail sometimes...
|
||||
pass
|
||||
for label in self._context.all_labels:
|
||||
label.cleanup()
|
||||
|
||||
for elem in self._context.all_elems:
|
||||
try:
|
||||
elem.label.remove_from_document()
|
||||
except webelem.Error:
|
||||
pass
|
||||
text = self._get_text()
|
||||
message_bridge = objreg.get('message-bridge', scope='window',
|
||||
window=self._win_id)
|
||||
@ -513,87 +580,6 @@ class HintManager(QObject):
|
||||
hintstr.insert(0, chars[0])
|
||||
return ''.join(hintstr)
|
||||
|
||||
def _is_hidden(self, elem):
|
||||
"""Check if the element is hidden via display=none."""
|
||||
display = elem.style_property('display', strategy='inline')
|
||||
return display == 'none'
|
||||
|
||||
def _show_elem(self, elem):
|
||||
"""Show a given element."""
|
||||
elem.set_style_property('display', 'inline !important')
|
||||
|
||||
def _hide_elem(self, elem):
|
||||
"""Hide a given element."""
|
||||
elem.set_style_property('display', 'none !important')
|
||||
|
||||
def _set_style_properties(self, elem, label):
|
||||
"""Set the hint CSS on the element given.
|
||||
|
||||
Args:
|
||||
elem: The QWebElement to set the style attributes for.
|
||||
label: The label QWebElement.
|
||||
"""
|
||||
attrs = [
|
||||
('display', 'inline !important'),
|
||||
('z-index', '{} !important'.format(int(2 ** 32 / 2 - 1))),
|
||||
('pointer-events', 'none !important'),
|
||||
('position', 'fixed !important'),
|
||||
('color', config.get('colors', 'hints.fg') + ' !important'),
|
||||
('background', config.get('colors', 'hints.bg') + ' !important'),
|
||||
('font', config.get('fonts', 'hints') + ' !important'),
|
||||
('border', config.get('hints', 'border') + ' !important'),
|
||||
('opacity', str(config.get('hints', 'opacity')) + ' !important'),
|
||||
]
|
||||
|
||||
# Make text uppercase if set in config
|
||||
if (config.get('hints', 'uppercase') and
|
||||
self._context.hint_mode == 'letter'):
|
||||
attrs.append(('text-transform', 'uppercase !important'))
|
||||
else:
|
||||
attrs.append(('text-transform', 'none !important'))
|
||||
|
||||
for k, v in attrs:
|
||||
label.set_style_property(k, v)
|
||||
self._set_style_position(elem, label)
|
||||
|
||||
def _set_style_position(self, elem, label):
|
||||
"""Set the CSS position of the label element.
|
||||
|
||||
Args:
|
||||
elem: The QWebElement to set the style attributes for.
|
||||
label: The label QWebElement.
|
||||
"""
|
||||
no_js = config.get('hints', 'find-implementation') != 'javascript'
|
||||
rect = elem.rect_on_view(adjust_zoom=False, no_js=no_js)
|
||||
left = rect.x()
|
||||
top = rect.y()
|
||||
log.hints.vdebug("Drawing label '{!r}' at {}/{} for element '{!r}' "
|
||||
"(no_js: {})".format(label, left, top, elem, no_js))
|
||||
label.set_style_property('left', '{}px !important'.format(left))
|
||||
label.set_style_property('top', '{}px !important'.format(top))
|
||||
|
||||
def _draw_label(self, elem, string):
|
||||
"""Draw a hint label over an element.
|
||||
|
||||
Args:
|
||||
elem: The QWebElement to use.
|
||||
string: The hint string to print.
|
||||
|
||||
Return:
|
||||
The newly created label element
|
||||
"""
|
||||
doc = elem.document_element()
|
||||
body = doc.find_first('body')
|
||||
if body is None:
|
||||
parent = doc
|
||||
else:
|
||||
parent = body
|
||||
label = parent.create_inside('span')
|
||||
label['class'] = 'qutehint'
|
||||
self._set_style_properties(elem, label)
|
||||
label.set_text(string)
|
||||
return label
|
||||
|
||||
def _show_url_error(self):
|
||||
"""Show an error because no link was found."""
|
||||
message.error(self._win_id, "No suitable link found for this element.",
|
||||
@ -646,18 +632,18 @@ class HintManager(QObject):
|
||||
raise cmdexc.CommandError("No elements found.")
|
||||
strings = self._hint_strings(elems)
|
||||
log.hints.debug("hints: {}".format(', '.join(strings)))
|
||||
for e, string in zip(elems, strings):
|
||||
label = self._draw_label(e, string)
|
||||
elem = ElemTuple(e, label)
|
||||
self._context.all_elems.append(elem)
|
||||
self._context.elems[string] = elem
|
||||
|
||||
for elem, string in zip(elems, strings):
|
||||
label = HintLabel(elem, self._context)
|
||||
label.update_text('', string)
|
||||
self._context.all_labels.append(label)
|
||||
self._context.labels[string] = label
|
||||
|
||||
keyparsers = objreg.get('keyparsers', scope='window',
|
||||
window=self._win_id)
|
||||
keyparser = keyparsers[usertypes.KeyMode.hint]
|
||||
keyparser.update_bindings(strings)
|
||||
|
||||
self._context.tab.contents_size_changed.connect(
|
||||
self.on_contents_size_changed)
|
||||
message_bridge = objreg.get('message-bridge', scope='window',
|
||||
window=self._win_id)
|
||||
message_bridge.set_text(self._get_text())
|
||||
@ -777,21 +763,12 @@ class HintManager(QObject):
|
||||
|
||||
return self._context.hint_mode
|
||||
|
||||
def _get_visible_hints(self):
|
||||
"""Get elements which are currently visible."""
|
||||
visible = {}
|
||||
for string, elem in self._context.elems.items():
|
||||
try:
|
||||
if not self._is_hidden(elem.label):
|
||||
visible[string] = elem
|
||||
except webelem.Error:
|
||||
pass
|
||||
return visible
|
||||
|
||||
def _handle_auto_follow(self, keystr="", filterstr="", visible=None):
|
||||
"""Handle the auto-follow option."""
|
||||
if visible is None:
|
||||
visible = self._get_visible_hints()
|
||||
visible = {string: label
|
||||
for string, label in self._context.labels.items()
|
||||
if label.isVisible()}
|
||||
|
||||
if len(visible) != 1:
|
||||
return
|
||||
@ -825,24 +802,20 @@ class HintManager(QObject):
|
||||
def handle_partial_key(self, keystr):
|
||||
"""Handle a new partial keypress."""
|
||||
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:
|
||||
if string.startswith(keystr):
|
||||
matched = string[:len(keystr)]
|
||||
rest = string[len(keystr):]
|
||||
match_color = config.get('colors', 'hints.fg.match')
|
||||
elem.label.set_inner_xml(
|
||||
'<font color="{}">{}</font>{}'.format(
|
||||
match_color, matched, rest))
|
||||
if self._is_hidden(elem.label):
|
||||
# hidden element which matches again -> show it
|
||||
self._show_elem(elem.label)
|
||||
label.update_text(matched, rest)
|
||||
# Show label again if it was hidden before
|
||||
label.show()
|
||||
else:
|
||||
# element doesn't match anymore -> hide it, unless in rapid
|
||||
# mode and hide-unmatched-rapid-hints is false (see #1799)
|
||||
if (not self._context.rapid or
|
||||
config.get('hints', 'hide-unmatched-rapid-hints')):
|
||||
self._hide_elem(elem.label)
|
||||
label.hide()
|
||||
except webelem.Error:
|
||||
pass
|
||||
self._handle_auto_follow(keystr=keystr)
|
||||
@ -862,16 +835,15 @@ class HintManager(QObject):
|
||||
self._context.filterstr = filterstr
|
||||
|
||||
visible = []
|
||||
for elem in self._context.all_elems:
|
||||
for label in self._context.all_labels:
|
||||
try:
|
||||
if self._filter_matches(filterstr, str(elem.elem)):
|
||||
visible.append(elem)
|
||||
if self._is_hidden(elem.label):
|
||||
# hidden element which matches again -> show it
|
||||
self._show_elem(elem.label)
|
||||
if self._filter_matches(filterstr, str(label.elem)):
|
||||
visible.append(label)
|
||||
# Show label again if it was hidden before
|
||||
label.show()
|
||||
else:
|
||||
# element doesn't match anymore -> hide it
|
||||
self._hide_elem(elem.label)
|
||||
label.hide()
|
||||
except webelem.Error:
|
||||
pass
|
||||
|
||||
@ -884,10 +856,10 @@ class HintManager(QObject):
|
||||
if self._context.hint_mode == 'number':
|
||||
# renumber filtered hints
|
||||
strings = self._hint_strings(visible)
|
||||
self._context.elems = {}
|
||||
for elem, string in zip(visible, strings):
|
||||
elem.label.set_inner_xml(string)
|
||||
self._context.elems[string] = elem
|
||||
self._context.labels = {}
|
||||
for label, string in zip(visible, strings):
|
||||
label.update_text('', string)
|
||||
self._context.labels[string] = label
|
||||
keyparsers = objreg.get('keyparsers', scope='window',
|
||||
window=self._win_id)
|
||||
keyparser = keyparsers[usertypes.KeyMode.hint]
|
||||
@ -896,9 +868,9 @@ class HintManager(QObject):
|
||||
# Note: filter_hints can be called with non-None filterstr only
|
||||
# when number mode is active
|
||||
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,
|
||||
visible=self._context.elems)
|
||||
visible=self._context.labels)
|
||||
|
||||
def _fire(self, keystr):
|
||||
"""Fire a completed hint.
|
||||
@ -927,7 +899,7 @@ class HintManager(QObject):
|
||||
Target.fill: self._actions.preset_cmd_text,
|
||||
Target.spawn: self._actions.spawn,
|
||||
}
|
||||
elem = self._context.elems[keystr].elem
|
||||
elem = self._context.labels[keystr].elem
|
||||
|
||||
if elem.frame() is None:
|
||||
message.error(self._win_id,
|
||||
@ -955,8 +927,8 @@ class HintManager(QObject):
|
||||
# Reset filtering
|
||||
self.filter_hints(None)
|
||||
# Undo keystring highlighting
|
||||
for string, elem in self._context.elems.items():
|
||||
elem.label.set_inner_xml(string)
|
||||
for string, label in self._context.labels.items():
|
||||
label.update_text('', string)
|
||||
|
||||
try:
|
||||
handler()
|
||||
@ -976,24 +948,10 @@ class HintManager(QObject):
|
||||
raise cmdexc.CommandError("No hint to follow")
|
||||
else:
|
||||
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))
|
||||
self._fire(keystring)
|
||||
|
||||
@pyqtSlot()
|
||||
def on_contents_size_changed(self):
|
||||
"""Reposition hints if contents size changed."""
|
||||
log.hints.debug("Contents size changed...!")
|
||||
for e in self._context.all_elems:
|
||||
try:
|
||||
if e.elem.frame() is None:
|
||||
# This sometimes happens for some reason...
|
||||
e.label.remove_from_document()
|
||||
continue
|
||||
self._set_style_position(e.elem, e.label)
|
||||
except webelem.Error:
|
||||
pass
|
||||
|
||||
@pyqtSlot(usertypes.KeyMode)
|
||||
def on_mode_left(self, mode):
|
||||
"""Stop hinting when hinting mode was left."""
|
||||
|
@ -112,21 +112,6 @@ class AbstractWebElement(collections.abc.MutableMapping):
|
||||
"""Get the geometry for this element."""
|
||||
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):
|
||||
"""Get the element style resolved with the given strategy."""
|
||||
raise NotImplementedError
|
||||
@ -166,21 +151,6 @@ class AbstractWebElement(collections.abc.MutableMapping):
|
||||
# FIXME:qtwebengine what to do about use_js with WebEngine?
|
||||
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):
|
||||
"""Run the given JS snippet async on the element."""
|
||||
# FIXME:qtwebengine get rid of this?
|
||||
@ -191,8 +161,7 @@ class AbstractWebElement(collections.abc.MutableMapping):
|
||||
# FIXME:qtwebengine get rid of this?
|
||||
raise NotImplementedError
|
||||
|
||||
def rect_on_view(self, *, elem_geometry=None, adjust_zoom=True,
|
||||
no_js=False):
|
||||
def rect_on_view(self, *, elem_geometry=None, no_js=False):
|
||||
"""Get the geometry of the element relative to the webview.
|
||||
|
||||
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.
|
||||
Calling QWebElement::geometry is rather expensive so
|
||||
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
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
@ -66,18 +66,6 @@ class WebEngineElement(webelem.AbstractWebElement):
|
||||
log.stub()
|
||||
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):
|
||||
log.stub()
|
||||
return ''
|
||||
@ -121,21 +109,6 @@ class WebEngineElement(webelem.AbstractWebElement):
|
||||
js_code = javascript.assemble('webelem', 'set_text', self._id, text)
|
||||
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):
|
||||
"""Run the given JS snippet async on the element."""
|
||||
# FIXME:qtwebengine get rid of this?
|
||||
@ -147,8 +120,7 @@ class WebEngineElement(webelem.AbstractWebElement):
|
||||
log.stub()
|
||||
return None
|
||||
|
||||
def rect_on_view(self, *, elem_geometry=None, adjust_zoom=True,
|
||||
no_js=False):
|
||||
def rect_on_view(self, *, elem_geometry=None, no_js=False):
|
||||
"""Get the geometry of the element relative to the webview.
|
||||
|
||||
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.
|
||||
Calling QWebElement::geometry is rather expensive so
|
||||
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
|
||||
"""
|
||||
log.stub()
|
||||
|
@ -91,28 +91,6 @@ class WebKitElement(webelem.AbstractWebElement):
|
||||
self._check_vanished()
|
||||
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):
|
||||
self._check_vanished()
|
||||
strategies = {
|
||||
@ -156,18 +134,6 @@ class WebKitElement(webelem.AbstractWebElement):
|
||||
text = javascript.string_escape(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):
|
||||
"""Run the given JS snippet async on the element."""
|
||||
self._check_vanished()
|
||||
@ -182,7 +148,7 @@ class WebKitElement(webelem.AbstractWebElement):
|
||||
return None
|
||||
return WebKitElement(elem)
|
||||
|
||||
def _rect_on_view_js(self, adjust_zoom):
|
||||
def _rect_on_view_js(self):
|
||||
"""Javascript implementation for rect_on_view."""
|
||||
# FIXME:qtwebengine maybe we can reuse this?
|
||||
rects = self._elem.evaluateJavaScript("this.getClientRects()")
|
||||
@ -203,7 +169,7 @@ class WebKitElement(webelem.AbstractWebElement):
|
||||
if width > 1 and height > 1:
|
||||
# fix coordinates according to zoom level
|
||||
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["top"] *= zoom
|
||||
width *= zoom
|
||||
@ -231,18 +197,9 @@ class WebKitElement(webelem.AbstractWebElement):
|
||||
rect.translate(frame.geometry().topLeft())
|
||||
rect.translate(frame.scrollPosition() * -1)
|
||||
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
|
||||
|
||||
def rect_on_view(self, *, elem_geometry=None, adjust_zoom=True,
|
||||
no_js=False):
|
||||
def rect_on_view(self, *, elem_geometry=None, no_js=False):
|
||||
"""Get the geometry of the element relative to the webview.
|
||||
|
||||
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.
|
||||
Calling QWebElement::geometry is rather expensive so
|
||||
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
|
||||
"""
|
||||
# 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
|
||||
# accurate
|
||||
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:
|
||||
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.
|
||||
"""
|
||||
|
||||
import re
|
||||
import os
|
||||
import sys
|
||||
import os.path
|
||||
@ -34,6 +35,7 @@ import collections
|
||||
import collections.abc
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, QObject, QUrl, QSettings
|
||||
from PyQt5.QtGui import QColor
|
||||
|
||||
from qutebrowser.config import configdata, configexc, textwrapper
|
||||
from qutebrowser.config.parsers import keyconf
|
||||
@ -286,6 +288,50 @@ def _transform_position(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):
|
||||
|
||||
"""Configuration manager for qutebrowser.
|
||||
@ -353,6 +399,7 @@ class ConfigManager(QObject):
|
||||
('ui', 'display-statusbar-messages'),
|
||||
('ui', 'hide-mouse-cursor'),
|
||||
('general', 'wrap-search'),
|
||||
('hints', 'opacity'),
|
||||
]
|
||||
CHANGED_OPTIONS = {
|
||||
('content', 'cookies-accept'):
|
||||
@ -367,6 +414,10 @@ class ConfigManager(QObject):
|
||||
_get_value_transformer({'false': '*', 'true': ''}),
|
||||
('hints', 'auto-follow'):
|
||||
_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)
|
||||
@ -525,7 +576,15 @@ class ConfigManager(QObject):
|
||||
k = self.RENAMED_OPTIONS[sectname, k]
|
||||
if (sectname, k) in self.CHANGED_OPTIONS:
|
||||
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:
|
||||
self.set('conf', sectname, k, v, validate=False)
|
||||
|
@ -897,10 +897,6 @@ def data(readonly=False):
|
||||
SettingValue(typ.String(), '1px solid #E3BE23'),
|
||||
"CSS border value for hints."),
|
||||
|
||||
('opacity',
|
||||
SettingValue(typ.Float(minval=0.0, maxval=1.0), '0.7'),
|
||||
"Opacity for hints."),
|
||||
|
||||
('mode',
|
||||
SettingValue(typ.String(
|
||||
valid_values=typ.ValidValues(
|
||||
@ -1209,18 +1205,18 @@ def data(readonly=False):
|
||||
"Color gradient interpolation system for the tab indicator."),
|
||||
|
||||
('hints.fg',
|
||||
SettingValue(typ.CssColor(), 'black'),
|
||||
SettingValue(typ.QssColor(), 'black'),
|
||||
"Font color for hints."),
|
||||
|
||||
('hints.bg',
|
||||
SettingValue(
|
||||
typ.CssColor(), '-webkit-gradient(linear, left top, '
|
||||
'left bottom, color-stop(0%,#FFF785), '
|
||||
'color-stop(100%,#FFC542))'),
|
||||
"Background color for hints."),
|
||||
SettingValue(typ.QssColor(), '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))'),
|
||||
"Background color for hints. Note that you can use a `rgba(...)` "
|
||||
"value for transparency."),
|
||||
|
||||
('hints.fg.match',
|
||||
SettingValue(typ.CssColor(), 'green'),
|
||||
SettingValue(typ.QssColor(), 'green'),
|
||||
"Font color for the matched part of hints."),
|
||||
|
||||
('downloads.bg.bar',
|
||||
@ -1309,7 +1305,7 @@ def data(readonly=False):
|
||||
"Font used for the downloadbar."),
|
||||
|
||||
('hints',
|
||||
SettingValue(typ.Font(), 'bold 13px Monospace'),
|
||||
SettingValue(typ.Font(), 'bold 13px ${_monospace}'),
|
||||
"Font used for the hints."),
|
||||
|
||||
('debug-console',
|
||||
|
@ -61,6 +61,8 @@ def whitelist_generator():
|
||||
yield 'qutebrowser.utils.debug.qflags_key'
|
||||
yield 'qutebrowser.utils.qtutils.QtOSError.qt_errno'
|
||||
yield 'scripts.utils.bg_colors'
|
||||
yield 'qutebrowser.browser.webelem.AbstractWebElement.style_property'
|
||||
yield 'qutebrowser.config.configtypes.Float'
|
||||
|
||||
# Qt attributes
|
||||
yield 'PyQt5.QtWebKit.QWebPage.ErrorPageExtensionReturn().baseUrl'
|
||||
|
@ -80,7 +80,7 @@ class FakeWebFrame:
|
||||
"""
|
||||
|
||||
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.
|
||||
|
||||
Args:
|
||||
@ -89,7 +89,6 @@ class FakeWebFrame:
|
||||
plaintext: Return value of toPlainText
|
||||
html: Return value of tohtml.
|
||||
zoom: The zoom factor.
|
||||
document_element: The documentElement() to return
|
||||
parent: The parent frame.
|
||||
"""
|
||||
if scroll is None:
|
||||
@ -101,7 +100,6 @@ class FakeWebFrame:
|
||||
self.toPlainText = mock.Mock(return_value=plaintext)
|
||||
self.toHtml = mock.Mock(return_value=html)
|
||||
self.zoomFactor = mock.Mock(return_value=zoom)
|
||||
self.documentElement = mock.Mock(return_value=document_element)
|
||||
|
||||
def findFirstElement(self, selector):
|
||||
if selector == '*:focus':
|
||||
|
@ -67,11 +67,13 @@ def get_webelem(geometry=None, frame=None, *, null=False, style=None,
|
||||
else:
|
||||
scroll_x = frame.scrollPosition().x()
|
||||
scroll_y = frame.scrollPosition().y()
|
||||
|
||||
if js_rect_return is None:
|
||||
if frame is None or zoom_text_only:
|
||||
zoom = 1.0
|
||||
else:
|
||||
zoom = frame.zoomFactor()
|
||||
|
||||
elem.evaluateJavaScript.return_value = {
|
||||
"length": 1,
|
||||
"0": {
|
||||
@ -255,15 +257,9 @@ class TestWebKitElement:
|
||||
len,
|
||||
lambda e: e.frame(),
|
||||
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.text(),
|
||||
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_content_editable(),
|
||||
lambda e: e.is_editable(),
|
||||
@ -276,9 +272,7 @@ class TestWebKitElement:
|
||||
lambda e: e.rect_on_view(),
|
||||
lambda e: e.is_visible(None),
|
||||
], ids=['str', 'getitem', 'setitem', 'delitem', 'contains', 'iter', 'len',
|
||||
'frame', 'geometry', 'document_element', 'create_inside',
|
||||
'find_first', 'style_property', 'text', 'set_text',
|
||||
'set_inner_xml', 'remove_from_document', 'set_style_property',
|
||||
'frame', 'geometry', 'style_property', 'text', 'set_text',
|
||||
'is_writable', 'is_content_editable', 'is_editable',
|
||||
'is_text_input', 'remove_blank_target', 'debug_text', 'outer_xml',
|
||||
'tag_name', 'run_js_async', 'rect_on_view', 'is_visible'])
|
||||
@ -410,17 +404,6 @@ class TestWebKitElement:
|
||||
setattr(mock, 'return_value', 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):
|
||||
elem._elem.tagName.return_value = 'SPAN'
|
||||
assert elem.tag_name() == 'span'
|
||||
@ -428,34 +411,6 @@ class TestWebKitElement:
|
||||
def test_style_property(self, elem):
|
||||
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', [
|
||||
(True, 'false', 'js'),
|
||||
(True, 'true', 'nojs'),
|
||||
@ -802,20 +757,14 @@ class TestRectOnView:
|
||||
|
||||
@pytest.mark.parametrize('js_rect', [None, {}])
|
||||
@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,
|
||||
adjust_zoom):
|
||||
def test_zoomed(self, stubs, config_stub, js_rect, zoom_text_only):
|
||||
"""Make sure the coordinates are adjusted when zoomed."""
|
||||
config_stub.data = {'ui': {'zoom-text-only': zoom_text_only}}
|
||||
geometry = QRect(10, 10, 4, 4)
|
||||
frame = stubs.FakeWebFrame(QRect(0, 0, 100, 100), zoom=0.5)
|
||||
elem = get_webelem(geometry, frame, js_rect_return=js_rect,
|
||||
zoom_text_only=zoom_text_only)
|
||||
rect = elem.rect_on_view(adjust_zoom=adjust_zoom)
|
||||
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)
|
||||
assert elem.rect_on_view() == QRect(10, 10, 4, 4)
|
||||
|
||||
|
||||
class TestGetChildFrames:
|
||||
|
@ -210,6 +210,62 @@ class TestConfigParser:
|
||||
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:
|
||||
|
||||
"""Test config.parsers.keyconf.KeyConfigParser."""
|
||||
|
@ -871,7 +871,7 @@ class ColorTests:
|
||||
('hsva(359, 255, 255, 255)', [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]),
|
||||
('qconicalgradient(cx:0.5, cy:0.5, angle:30, stop:0 white, '
|
||||
'stop:1 #00FF00)', [configtypes.QssColor]),
|
||||
|
Loading…
Reference in New Issue
Block a user