Merge remote-tracking branch 'origin/pr/4220'
This commit is contained in:
commit
923b726e38
@ -59,7 +59,6 @@ except ImportError:
|
|||||||
|
|
||||||
import qutebrowser
|
import qutebrowser
|
||||||
import qutebrowser.resources
|
import qutebrowser.resources
|
||||||
from qutebrowser.completion import completiondelegate
|
|
||||||
from qutebrowser.completion.models import miscmodels
|
from qutebrowser.completion.models import miscmodels
|
||||||
from qutebrowser.commands import cmdutils, runners, cmdexc
|
from qutebrowser.commands import cmdutils, runners, cmdexc
|
||||||
from qutebrowser.config import config, websettings, configfiles, configinit
|
from qutebrowser.config import config, websettings, configfiles, configinit
|
||||||
@ -453,9 +452,6 @@ def _init_modules(args, crash_handler):
|
|||||||
pre_text='Error initializing SQL')
|
pre_text='Error initializing SQL')
|
||||||
sys.exit(usertypes.Exit.err_init)
|
sys.exit(usertypes.Exit.err_init)
|
||||||
|
|
||||||
log.init.debug("Initializing completion...")
|
|
||||||
completiondelegate.init()
|
|
||||||
|
|
||||||
log.init.debug("Initializing command history...")
|
log.init.debug("Initializing command history...")
|
||||||
cmdhistory.init()
|
cmdhistory.init()
|
||||||
|
|
||||||
|
@ -28,13 +28,27 @@ import html
|
|||||||
from PyQt5.QtWidgets import QStyle, QStyleOptionViewItem, QStyledItemDelegate
|
from PyQt5.QtWidgets import QStyle, QStyleOptionViewItem, QStyledItemDelegate
|
||||||
from PyQt5.QtCore import QRectF, QSize, Qt
|
from PyQt5.QtCore import QRectF, QSize, Qt
|
||||||
from PyQt5.QtGui import (QIcon, QPalette, QTextDocument, QTextOption,
|
from PyQt5.QtGui import (QIcon, QPalette, QTextDocument, QTextOption,
|
||||||
QAbstractTextDocumentLayout)
|
QAbstractTextDocumentLayout, QSyntaxHighlighter,
|
||||||
|
QTextCharFormat)
|
||||||
|
|
||||||
from qutebrowser.config import config
|
from qutebrowser.config import config
|
||||||
from qutebrowser.utils import qtutils, jinja
|
from qutebrowser.utils import qtutils
|
||||||
|
|
||||||
|
|
||||||
_cached_stylesheet = None
|
class _Highlighter(QSyntaxHighlighter):
|
||||||
|
|
||||||
|
def __init__(self, doc, pattern, color):
|
||||||
|
super().__init__(doc)
|
||||||
|
self._format = QTextCharFormat()
|
||||||
|
self._format.setForeground(color)
|
||||||
|
self._pattern = pattern
|
||||||
|
|
||||||
|
def highlightBlock(self, text):
|
||||||
|
"""Override highlightBlock for custom highlighting."""
|
||||||
|
for match in re.finditer(self._pattern, text):
|
||||||
|
start, end = match.span()
|
||||||
|
length = end - start
|
||||||
|
self.setFormat(start, length, self._format)
|
||||||
|
|
||||||
|
|
||||||
class CompletionItemDelegate(QStyledItemDelegate):
|
class CompletionItemDelegate(QStyledItemDelegate):
|
||||||
@ -194,21 +208,15 @@ class CompletionItemDelegate(QStyledItemDelegate):
|
|||||||
self._doc.setDefaultTextOption(text_option)
|
self._doc.setDefaultTextOption(text_option)
|
||||||
self._doc.setDocumentMargin(2)
|
self._doc.setDocumentMargin(2)
|
||||||
|
|
||||||
assert _cached_stylesheet is not None
|
|
||||||
self._doc.setDefaultStyleSheet(_cached_stylesheet)
|
|
||||||
|
|
||||||
if index.parent().isValid():
|
if index.parent().isValid():
|
||||||
view = self.parent()
|
view = self.parent()
|
||||||
pattern = view.pattern
|
pattern = view.pattern
|
||||||
columns_to_filter = index.model().columns_to_filter(index)
|
columns_to_filter = index.model().columns_to_filter(index)
|
||||||
|
self._doc.setPlainText(self._opt.text)
|
||||||
if index.column() in columns_to_filter and pattern:
|
if index.column() in columns_to_filter and pattern:
|
||||||
repl = r'<span class="highlight">\g<0></span>'
|
pat = re.escape(pattern).replace(r'\ ', r'|')
|
||||||
pat = html.escape(re.escape(pattern)).replace(r'\ ', r'|')
|
_Highlighter(self._doc, pat,
|
||||||
txt = html.escape(self._opt.text)
|
config.val.colors.completion.match.fg)
|
||||||
text = re.sub(pat, repl, txt, flags=re.IGNORECASE)
|
|
||||||
self._doc.setHtml(text)
|
|
||||||
else:
|
|
||||||
self._doc.setPlainText(self._opt.text)
|
|
||||||
else:
|
else:
|
||||||
self._doc.setHtml(
|
self._doc.setHtml(
|
||||||
'<span style="font: {};">{}</span>'.format(
|
'<span style="font: {};">{}</span>'.format(
|
||||||
@ -283,24 +291,3 @@ class CompletionItemDelegate(QStyledItemDelegate):
|
|||||||
self._draw_focus_rect()
|
self._draw_focus_rect()
|
||||||
|
|
||||||
self._painter.restore()
|
self._painter.restore()
|
||||||
|
|
||||||
|
|
||||||
@config.change_filter('colors.completion.match.fg', function=True)
|
|
||||||
def _update_stylesheet():
|
|
||||||
"""Update the cached stylesheet."""
|
|
||||||
stylesheet = """
|
|
||||||
.highlight {
|
|
||||||
color: {{ conf.colors.completion.match.fg }};
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
with jinja.environment.no_autoescape():
|
|
||||||
template = jinja.environment.from_string(stylesheet)
|
|
||||||
|
|
||||||
global _cached_stylesheet
|
|
||||||
_cached_stylesheet = template.render(conf=config.val)
|
|
||||||
|
|
||||||
|
|
||||||
def init():
|
|
||||||
"""Initialize the cached stylesheet."""
|
|
||||||
_update_stylesheet()
|
|
||||||
config.instance.changed.connect(_update_stylesheet)
|
|
||||||
|
@ -1896,7 +1896,7 @@ colors.completion.item.selected.border.bottom:
|
|||||||
|
|
||||||
colors.completion.match.fg:
|
colors.completion.match.fg:
|
||||||
default: '#ff4444'
|
default: '#ff4444'
|
||||||
type: QssColor
|
type: QtColor
|
||||||
desc: Foreground color of the matched text in the completion.
|
desc: Foreground color of the matched text in the completion.
|
||||||
|
|
||||||
colors.completion.scrollbar.fg:
|
colors.completion.scrollbar.fg:
|
||||||
|
@ -922,6 +922,23 @@ class QtColor(BaseType):
|
|||||||
* transparent (no color)
|
* transparent (no color)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def _parse_value(self, val):
|
||||||
|
try:
|
||||||
|
return int(val)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
mult = 255.0
|
||||||
|
if val.endswith('%'):
|
||||||
|
val = val[:-1]
|
||||||
|
mult = 255.0 / 100
|
||||||
|
return int(float(val) * 255.0 / 100.0)
|
||||||
|
|
||||||
|
try:
|
||||||
|
return int(float(val) * mult)
|
||||||
|
except ValueError:
|
||||||
|
raise configexc.ValidationError(val, "must be a valid color value")
|
||||||
|
|
||||||
def to_py(self, value):
|
def to_py(self, value):
|
||||||
self._basic_py_validation(value, str)
|
self._basic_py_validation(value, str)
|
||||||
if value is configutils.UNSET:
|
if value is configutils.UNSET:
|
||||||
@ -929,6 +946,20 @@ class QtColor(BaseType):
|
|||||||
elif not value:
|
elif not value:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
if value.endswith(')'):
|
||||||
|
openparen = value.index('(')
|
||||||
|
kind = value[:openparen]
|
||||||
|
vals = value[openparen+1:-1].split(',')
|
||||||
|
vals = [self._parse_value(v) for v in vals]
|
||||||
|
if kind == 'rgba' and len(vals) == 4:
|
||||||
|
return QColor.fromRgb(*vals)
|
||||||
|
if kind == 'rgb' and len(vals) == 3:
|
||||||
|
return QColor.fromRgb(*vals)
|
||||||
|
if kind == 'hsva' and len(vals) == 4:
|
||||||
|
return QColor.fromHsv(*vals)
|
||||||
|
if kind == 'hsv' and len(vals) == 3:
|
||||||
|
return QColor.fromHsv(*vals)
|
||||||
|
|
||||||
color = QColor(value)
|
color = QColor(value)
|
||||||
if color.isValid():
|
if color.isValid():
|
||||||
return color
|
return color
|
||||||
|
51
tests/unit/completion/test_completiondelegate.py
Normal file
51
tests/unit/completion/test_completiondelegate.py
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||||
|
|
||||||
|
# Copyright 2018 Ryan Roden-Corrent (rcorre) <ryan@rcorre.net>
|
||||||
|
#
|
||||||
|
# This file is part of qutebrowser.
|
||||||
|
#
|
||||||
|
# qutebrowser is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# qutebrowser is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from PyQt5.QtCore import Qt
|
||||||
|
from PyQt5.QtGui import QTextDocument
|
||||||
|
|
||||||
|
from qutebrowser.completion import completiondelegate
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('pat,txt,segments', [
|
||||||
|
('foo', 'foo', [(0, 3)]),
|
||||||
|
('foo', 'foobar', [(0, 3)]),
|
||||||
|
('foo', 'barfoo', [(3, 3)]),
|
||||||
|
('foo', 'barfoobaz', [(3, 3)]),
|
||||||
|
('foo', 'barfoobazfoo', [(3, 3), (9, 3)]),
|
||||||
|
('foo', 'foofoo', [(0, 3), (3, 3)]),
|
||||||
|
('a|b', 'cadb', [(1, 1), (3, 1)]),
|
||||||
|
('foo', '<foo>', [(1, 3)]),
|
||||||
|
('<a>', "<a>bc", [(0, 3)]),
|
||||||
|
|
||||||
|
# https://github.com/qutebrowser/qutebrowser/issues/4199
|
||||||
|
('foo', "'foo'", [(1, 3)]),
|
||||||
|
('x', "'x'", [(1, 1)]),
|
||||||
|
('lt', "<lt", [(1, 2)]),
|
||||||
|
])
|
||||||
|
def test_highlight(pat, txt, segments):
|
||||||
|
doc = QTextDocument(txt)
|
||||||
|
highlighter = completiondelegate._Highlighter(doc, pat, Qt.red)
|
||||||
|
highlighter.setFormat = mock.Mock()
|
||||||
|
highlighter.highlightBlock(txt)
|
||||||
|
highlighter.setFormat.assert_has_calls([
|
||||||
|
mock.call(s[0], s[1], mock.ANY) for s in segments
|
||||||
|
])
|
@ -1227,87 +1227,88 @@ class TestCommand:
|
|||||||
assert ('cmd2', "desc 2") in items
|
assert ('cmd2', "desc 2") in items
|
||||||
|
|
||||||
|
|
||||||
class ColorTests:
|
class TestQtColor:
|
||||||
|
|
||||||
"""Generator for tests for TestColors."""
|
"""Test QtColor."""
|
||||||
|
|
||||||
TYPES = [configtypes.QtColor, configtypes.QssColor]
|
@pytest.mark.parametrize('val, expected', [
|
||||||
|
('#123', QColor('#123')),
|
||||||
|
('#112233', QColor('#112233')),
|
||||||
|
('#111222333', QColor('#111222333')),
|
||||||
|
('#111122223333', QColor('#111122223333')),
|
||||||
|
('red', QColor('red')),
|
||||||
|
|
||||||
TESTS = [
|
('rgb(0, 0, 0)', QColor.fromRgb(0, 0, 0)),
|
||||||
('#123', TYPES),
|
('rgb(0,0,0)', QColor.fromRgb(0, 0, 0)),
|
||||||
('#112233', TYPES),
|
|
||||||
('#111222333', TYPES),
|
|
||||||
('#111122223333', TYPES),
|
|
||||||
('red', TYPES),
|
|
||||||
|
|
||||||
('#00000G', []),
|
('rgba(255, 255, 255, 1.0)', QColor.fromRgb(255, 255, 255, 255)),
|
||||||
('#123456789ABCD', []),
|
|
||||||
('#12', []),
|
|
||||||
('foobar', []),
|
|
||||||
('42', []),
|
|
||||||
('foo(1, 2, 3)', []),
|
|
||||||
('rgb(1, 2, 3', []),
|
|
||||||
|
|
||||||
('rgb(0, 0, 0)', [configtypes.QssColor]),
|
# this should be (36, 25, 25) as hue goes to 359
|
||||||
('rgb(0,0,0)', [configtypes.QssColor]),
|
# however this is consistent with Qt's CSS parser
|
||||||
|
# https://bugreports.qt.io/browse/QTBUG-70897
|
||||||
|
('hsv(10%,10%,10%)', QColor.fromHsv(25, 25, 25)),
|
||||||
|
])
|
||||||
|
def test_valid(self, val, expected):
|
||||||
|
act = configtypes.QtColor().to_py(val)
|
||||||
|
print(expected.hue(), expected.saturation(), expected.value(), expected.alpha())
|
||||||
|
print(act.hue(), act.saturation(), act.value(), act.alpha())
|
||||||
|
assert configtypes.QtColor().to_py(val) == expected
|
||||||
|
|
||||||
('rgba(255, 255, 255, 1.0)', [configtypes.QssColor]),
|
@pytest.mark.parametrize('val', [
|
||||||
('hsv(10%,10%,10%)', [configtypes.QssColor]),
|
'#00000G',
|
||||||
|
'#123456789ABCD',
|
||||||
('qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 white, '
|
'#12',
|
||||||
'stop: 0.4 gray, stop:1 green)', [configtypes.QssColor]),
|
'foobar',
|
||||||
('qconicalgradient(cx:0.5, cy:0.5, angle:30, stop:0 white, '
|
'42',
|
||||||
'stop:1 #00FF00)', [configtypes.QssColor]),
|
'foo(1, 2, 3)',
|
||||||
('qradialgradient(cx:0, cy:0, radius: 1, fx:0.5, fy:0.5, '
|
'rgb(1, 2, 3',
|
||||||
'stop:0 white, stop:1 green)', [configtypes.QssColor]),
|
])
|
||||||
]
|
def test_invalid(self, val):
|
||||||
|
|
||||||
COMBINATIONS = list(itertools.product(TESTS, TYPES))
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.valid = list(self._generate_valid())
|
|
||||||
self.invalid = list(self._generate_invalid())
|
|
||||||
|
|
||||||
def _generate_valid(self):
|
|
||||||
for (val, valid_classes), klass in self.COMBINATIONS:
|
|
||||||
if klass in valid_classes:
|
|
||||||
yield klass, val
|
|
||||||
|
|
||||||
def _generate_invalid(self):
|
|
||||||
for (val, valid_classes), klass in self.COMBINATIONS:
|
|
||||||
if klass not in valid_classes:
|
|
||||||
yield klass, val
|
|
||||||
|
|
||||||
|
|
||||||
class TestColors:
|
|
||||||
|
|
||||||
"""Test QtColor/QssColor."""
|
|
||||||
|
|
||||||
TESTS = ColorTests()
|
|
||||||
|
|
||||||
@pytest.fixture(params=ColorTests.TYPES)
|
|
||||||
def klass_fixt(self, request):
|
|
||||||
"""Fixture which provides all ColorTests classes.
|
|
||||||
|
|
||||||
Named klass_fix so it has a different name from the parametrized klass,
|
|
||||||
see https://github.com/pytest-dev/pytest/issues/979.
|
|
||||||
"""
|
|
||||||
return request.param
|
|
||||||
|
|
||||||
def test_test_generator(self):
|
|
||||||
"""Some sanity checks for ColorTests."""
|
|
||||||
assert self.TESTS.valid
|
|
||||||
assert self.TESTS.invalid
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('klass, val', TESTS.valid)
|
|
||||||
def test_to_py_valid(self, klass, val):
|
|
||||||
expected = QColor(val) if klass is configtypes.QtColor else val
|
|
||||||
assert klass().to_py(val) == expected
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('klass, val', TESTS.invalid)
|
|
||||||
def test_to_py_invalid(self, klass, val):
|
|
||||||
with pytest.raises(configexc.ValidationError):
|
with pytest.raises(configexc.ValidationError):
|
||||||
klass().to_py(val)
|
configtypes.QtColor().to_py(val)
|
||||||
|
|
||||||
|
|
||||||
|
class TestQssColor:
|
||||||
|
|
||||||
|
"""Test QssColor."""
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('val', [
|
||||||
|
'#123',
|
||||||
|
'#112233',
|
||||||
|
'#111222333',
|
||||||
|
'#111122223333',
|
||||||
|
'red',
|
||||||
|
|
||||||
|
'rgb(0, 0, 0)',
|
||||||
|
'rgb(0,0,0)',
|
||||||
|
|
||||||
|
'rgba(255, 255, 255, 1.0)',
|
||||||
|
'hsv(10%,10%,10%)',
|
||||||
|
|
||||||
|
'qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 white, '
|
||||||
|
'stop: 0.4 gray, stop:1 green)',
|
||||||
|
|
||||||
|
'qconicalgradient(cx:0.5, cy:0.5, angle:30, stop:0 white, '
|
||||||
|
'stop:1 #00FF00)',
|
||||||
|
|
||||||
|
'qradialgradient(cx:0, cy:0, radius: 1, fx:0.5, fy:0.5, '
|
||||||
|
'stop:0 white, stop:1 green)',
|
||||||
|
])
|
||||||
|
def test_valid(self, val):
|
||||||
|
assert configtypes.QssColor().to_py(val) == val
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('val', [
|
||||||
|
'#00000G',
|
||||||
|
'#123456789ABCD',
|
||||||
|
'#12',
|
||||||
|
'foobar',
|
||||||
|
'42',
|
||||||
|
'foo(1, 2, 3)',
|
||||||
|
'rgb(1, 2, 3',
|
||||||
|
])
|
||||||
|
def test_invalid(self, val):
|
||||||
|
with pytest.raises(configexc.ValidationError):
|
||||||
|
configtypes.QssColor().to_py(val)
|
||||||
|
|
||||||
|
|
||||||
@attr.s
|
@attr.s
|
||||||
|
Loading…
Reference in New Issue
Block a user