Merge remote-tracking branch 'origin/pr/4220'

This commit is contained in:
Florian Bruhin 2018-10-04 19:28:01 +02:00
commit 923b726e38
6 changed files with 179 additions and 113 deletions

View File

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

View File

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

View File

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

View File

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

View 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
])

View File

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