diff --git a/qutebrowser/widgets/_completion.py b/qutebrowser/widgets/_completion.py index 0f58dfcee..55a95083d 100644 --- a/qutebrowser/widgets/_completion.py +++ b/qutebrowser/widgets/_completion.py @@ -22,20 +22,16 @@ subclasses to provide completions. """ import logging -import html -from PyQt5.QtWidgets import (QStyle, QStyleOptionViewItem, QTreeView, - QStyledItemDelegate, QSizePolicy) -from PyQt5.QtCore import (pyqtSlot, pyqtSignal, Qt, QRectF, QSize, - QItemSelectionModel) -from PyQt5.QtGui import (QIcon, QPalette, QTextDocument, QTextOption, - QTextCursor, QColor, QAbstractTextDocumentLayout) +from PyQt5.QtWidgets import QStyle, QTreeView, QSizePolicy +from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QItemSelectionModel import qutebrowser.config.config as config import qutebrowser.commands.utils as cmdutils import qutebrowser.config.configdata as configdata -from qutebrowser.models.basecompletion import ROLE_MARKS, NoCompletionsError -from qutebrowser.config.style import set_register_stylesheet, get_stylesheet +from qutebrowser.widgets._completiondelegate import CompletionItemDelegate +from qutebrowser.models.basecompletion import NoCompletionsError +from qutebrowser.config.style import set_register_stylesheet from qutebrowser.commands.managers import split_cmdline from qutebrowser.models.completionfilter import CompletionFilterModel as CFM from qutebrowser.models.completion import ( @@ -119,7 +115,7 @@ class CompletionView(QTreeView): self._init_setting_completions() self._completing = False - self._delegate = _CompletionItemDelegate(self) + self._delegate = CompletionItemDelegate(self) self.setItemDelegate(self._delegate) set_register_stylesheet(self) self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Minimum) @@ -376,240 +372,3 @@ class CompletionView(QTreeView): """Extend resizeEvent to adjust column size.""" super().resizeEvent(e) self._resize_columns() - - -class _CompletionItemDelegate(QStyledItemDelegate): - - """Delegate used by CompletionView to draw individual items. - - Mainly a cleaned up port of Qt's way to draw a TreeView item, except it - uses a QTextDocument to draw the text and add marking. - - Original implementation: - qt/src/gui/styles/qcommonstyle.cpp:drawControl:2153 - - Attributes: - _opt: The QStyleOptionViewItem which is used. - _style: The style to be used. - _painter: The QPainter to be used. - _doc: The QTextDocument to be used. - """ - - # FIXME this is horribly slow when resizing. - # We should probably cache something in _get_textdoc or so, but as soon as - # we implement eliding that cache probably isn't worth much anymore... - - def __init__(self, parent=None): - self._painter = None - self._opt = None - self._doc = None - self._style = None - super().__init__(parent) - - def _draw_background(self): - """Draw the background of an ItemViewItem.""" - self._style.drawPrimitive(self._style.PE_PanelItemViewItem, self._opt, - self._painter, self._opt.widget) - - def _draw_icon(self): - """Draw the icon of an ItemViewItem.""" - icon_rect = self._style.subElementRect( - self._style.SE_ItemViewItemDecoration, self._opt, self._opt.widget) - - mode = QIcon.Normal - if not self._opt.state & QStyle.State_Enabled: - mode = QIcon.Disabled - elif self._opt.state & QStyle.State_Selected: - mode = QIcon.Selected - state = QIcon.On if self._opt.state & QStyle.State_Open else QIcon.Off - self._opt.icon.paint(self._painter, icon_rect, - self._opt.decorationAlignment, mode, state) - - def _draw_text(self, index): - """Draw the text of an ItemViewItem. - - This is the main part where we differ from the original implementation - in Qt: We use a QTextDocument to draw text. - - Args: - index: The QModelIndex of the item to draw. - """ - if not self._opt.text: - return - - text_rect_ = self._style.subElementRect( - self._style.SE_ItemViewItemText, self._opt, self._opt.widget) - margin = self._style.pixelMetric(QStyle.PM_FocusFrameHMargin, - self._opt, self._opt.widget) + 1 - # remove width padding - text_rect = text_rect_.adjusted(margin, 0, -margin, 0) - # move text upwards a bit - if index.parent().isValid(): - text_rect.adjust(0, -1, 0, -1) - else: - text_rect.adjust(0, -2, 0, -2) - self._painter.save() - state = self._opt.state - if state & QStyle.State_Enabled and state & QStyle.State_Active: - cg = QPalette.Normal - elif state & QStyle.State_Enabled: - cg = QPalette.Inactive - else: - cg = QPalette.Disabled - - if state & QStyle.State_Selected: - self._painter.setPen(self._opt.palette.color( - cg, QPalette.HighlightedText)) - # This is a dirty fix for the text jumping by one pixel for - # whatever reason. - text_rect.adjust(0, -1, 0, 0) - else: - self._painter.setPen(self._opt.palette.color(cg, QPalette.Text)) - - if state & QStyle.State_Editing: - self._painter.setPen(self._opt.palette.color(cg, QPalette.Text)) - self._painter.drawRect(text_rect_.adjusted(0, 0, -1, -1)) - - self._painter.translate(text_rect.left(), text_rect.top()) - self._get_textdoc(index) - self._draw_textdoc(text_rect) - self._painter.restore() - - def _draw_textdoc(self, rect): - """Draw the QTextDocument of an item. - - Args: - rect: The QRect to clip the drawing to. - """ - # We can't use drawContents because then the color would be ignored. - # See: https://qt-project.org/forums/viewthread/21492 - clip = QRectF(0, 0, rect.width(), rect.height()) - self._painter.save() - if self._opt.state & QStyle.State_Selected: - option = 'completion.item.selected.fg' - elif not self._opt.state & QStyle.State_Enabled: - option = 'completion.category.fg' - else: - option = 'completion.fg' - try: - self._painter.setPen(QColor(config.get('colors', option))) - except config.NoOptionError: - self._painter.setPen(QColor(config.get('colors', 'completion.fg'))) - ctx = QAbstractTextDocumentLayout.PaintContext() - ctx.palette.setColor(QPalette.Text, self._painter.pen().color()) - if clip.isValid(): - self._painter.setClipRect(clip) - ctx.clip = clip - self._doc.documentLayout().draw(self._painter, ctx) - self._painter.restore() - - def _get_textdoc(self, index): - """Create the QTextDocument of an item. - - Args: - index: The QModelIndex of the item to draw. - """ - # FIXME we probably should do eliding here. See - # qcommonstyle.cpp:viewItemDrawText - text_option = QTextOption() - if self._opt.features & QStyleOptionViewItem.WrapText: - text_option.setWrapMode(QTextOption.WordWrap) - else: - text_option.setWrapMode(QTextOption.ManualWrap) - text_option.setTextDirection(self._opt.direction) - text_option.setAlignment(QStyle.visualAlignment( - self._opt.direction, self._opt.displayAlignment)) - - self._doc = QTextDocument(self) - if index.parent().isValid(): - self._doc.setPlainText(self._opt.text) - else: - self._doc.setHtml('{}'.format(html.escape(self._opt.text))) - self._doc.setDefaultFont(self._opt.font) - self._doc.setDefaultTextOption(text_option) - self._doc.setDefaultStyleSheet(get_stylesheet(""" - .highlight {{ - {color[completion.match.fg]} - }} - """)) - self._doc.setDocumentMargin(2) - - if index.column() == 0: - marks = index.data(ROLE_MARKS) - if marks is None: - return - for mark in marks: - cur = QTextCursor(self._doc) - cur.setPosition(mark[0]) - cur.setPosition(mark[1], QTextCursor.KeepAnchor) - txt = cur.selectedText() - cur.removeSelectedText() - cur.insertHtml('{}'.format( - html.escape(txt))) - - def _draw_focus_rect(self): - """Draw the focus rectangle of an ItemViewItem.""" - state = self._opt.state - if not state & QStyle.State_HasFocus: - return - o = self._opt - o.rect = self._style.subElementRect( - self._style.SE_ItemViewItemFocusRect, self._opt, self._opt.widget) - o.state |= QStyle.State_KeyboardFocusChange | QStyle.State_Item - if state & QStyle.State_Enabled: - cg = QPalette.Normal - else: - cg = QPalette.Disabled - if state & QStyle.State_Selected: - role = QPalette.Highlight - else: - role = QPalette.Window - o.backgroundColor = self._opt.palette.color(cg, role) - self._style.drawPrimitive(QStyle.PE_FrameFocusRect, o, self._painter, - self._opt.widget) - - def sizeHint(self, option, index): - """Override sizeHint of QStyledItemDelegate. - - Return the cell size based on the QTextDocument size, but might not - work correctly yet. - - Args: - option: const QStyleOptionViewItem & option - index: const QModelIndex & index - - Return: - A QSize with the recommended size. - """ - value = index.data(Qt.SizeHintRole) - if value is not None: - return value - self._opt = QStyleOptionViewItem(option) - self.initStyleOption(self._opt, index) - self._style = self._opt.widget.style() - self._get_textdoc(index) - docsize = self._doc.size().toSize() - size = self._style.sizeFromContents(QStyle.CT_ItemViewItem, self._opt, - docsize, self._opt.widget) - return size + QSize(10, 3) - - def paint(self, painter, option, index): - """Override the QStyledItemDelegate paint function. - - Args: - painter: QPainter * painter - option: const QStyleOptionViewItem & option - index: const QModelIndex & index - """ - self._painter = painter - self._painter.save() - self._opt = QStyleOptionViewItem(option) - self.initStyleOption(self._opt, index) - self._style = self._opt.widget.style() - - self._draw_background() - self._draw_icon() - self._draw_text(index) - self._draw_focus_rect() - - self._painter.restore() diff --git a/qutebrowser/widgets/_completiondelegate.py b/qutebrowser/widgets/_completiondelegate.py new file mode 100644 index 000000000..6760f5184 --- /dev/null +++ b/qutebrowser/widgets/_completiondelegate.py @@ -0,0 +1,268 @@ +# Copyright 2014 Florian Bruhin (The Compiler) +# +# 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 . + +"""Completion item delegate for CompletionView. + +We use this to be able to highlight parts of the text. +""" + +import html +from PyQt5.QtWidgets import QStyle, QStyleOptionViewItem, QStyledItemDelegate +from PyQt5.QtCore import QRectF, QSize, Qt +from PyQt5.QtGui import (QIcon, QPalette, QTextDocument, QTextOption, + QTextCursor, QColor, QAbstractTextDocumentLayout) + +import qutebrowser.config.config as config +from qutebrowser.models.basecompletion import ROLE_MARKS +from qutebrowser.config.style import get_stylesheet + + +class CompletionItemDelegate(QStyledItemDelegate): + + """Delegate used by CompletionView to draw individual items. + + Mainly a cleaned up port of Qt's way to draw a TreeView item, except it + uses a QTextDocument to draw the text and add marking. + + Original implementation: + qt/src/gui/styles/qcommonstyle.cpp:drawControl:2153 + + Attributes: + _opt: The QStyleOptionViewItem which is used. + _style: The style to be used. + _painter: The QPainter to be used. + _doc: The QTextDocument to be used. + """ + + # FIXME this is horribly slow when resizing. + # We should probably cache something in _get_textdoc or so, but as soon as + # we implement eliding that cache probably isn't worth much anymore... + + def __init__(self, parent=None): + self._painter = None + self._opt = None + self._doc = None + self._style = None + super().__init__(parent) + + def _draw_background(self): + """Draw the background of an ItemViewItem.""" + self._style.drawPrimitive(self._style.PE_PanelItemViewItem, self._opt, + self._painter, self._opt.widget) + + def _draw_icon(self): + """Draw the icon of an ItemViewItem.""" + icon_rect = self._style.subElementRect( + self._style.SE_ItemViewItemDecoration, self._opt, self._opt.widget) + + mode = QIcon.Normal + if not self._opt.state & QStyle.State_Enabled: + mode = QIcon.Disabled + elif self._opt.state & QStyle.State_Selected: + mode = QIcon.Selected + state = QIcon.On if self._opt.state & QStyle.State_Open else QIcon.Off + self._opt.icon.paint(self._painter, icon_rect, + self._opt.decorationAlignment, mode, state) + + def _draw_text(self, index): + """Draw the text of an ItemViewItem. + + This is the main part where we differ from the original implementation + in Qt: We use a QTextDocument to draw text. + + Args: + index: The QModelIndex of the item to draw. + """ + if not self._opt.text: + return + + text_rect_ = self._style.subElementRect( + self._style.SE_ItemViewItemText, self._opt, self._opt.widget) + margin = self._style.pixelMetric(QStyle.PM_FocusFrameHMargin, + self._opt, self._opt.widget) + 1 + # remove width padding + text_rect = text_rect_.adjusted(margin, 0, -margin, 0) + # move text upwards a bit + if index.parent().isValid(): + text_rect.adjust(0, -1, 0, -1) + else: + text_rect.adjust(0, -2, 0, -2) + self._painter.save() + state = self._opt.state + if state & QStyle.State_Enabled and state & QStyle.State_Active: + cg = QPalette.Normal + elif state & QStyle.State_Enabled: + cg = QPalette.Inactive + else: + cg = QPalette.Disabled + + if state & QStyle.State_Selected: + self._painter.setPen(self._opt.palette.color( + cg, QPalette.HighlightedText)) + # This is a dirty fix for the text jumping by one pixel for + # whatever reason. + text_rect.adjust(0, -1, 0, 0) + else: + self._painter.setPen(self._opt.palette.color(cg, QPalette.Text)) + + if state & QStyle.State_Editing: + self._painter.setPen(self._opt.palette.color(cg, QPalette.Text)) + self._painter.drawRect(text_rect_.adjusted(0, 0, -1, -1)) + + self._painter.translate(text_rect.left(), text_rect.top()) + self._get_textdoc(index) + self._draw_textdoc(text_rect) + self._painter.restore() + + def _draw_textdoc(self, rect): + """Draw the QTextDocument of an item. + + Args: + rect: The QRect to clip the drawing to. + """ + # We can't use drawContents because then the color would be ignored. + # See: https://qt-project.org/forums/viewthread/21492 + clip = QRectF(0, 0, rect.width(), rect.height()) + self._painter.save() + if self._opt.state & QStyle.State_Selected: + option = 'completion.item.selected.fg' + elif not self._opt.state & QStyle.State_Enabled: + option = 'completion.category.fg' + else: + option = 'completion.fg' + try: + self._painter.setPen(QColor(config.get('colors', option))) + except config.NoOptionError: + self._painter.setPen(QColor(config.get('colors', 'completion.fg'))) + ctx = QAbstractTextDocumentLayout.PaintContext() + ctx.palette.setColor(QPalette.Text, self._painter.pen().color()) + if clip.isValid(): + self._painter.setClipRect(clip) + ctx.clip = clip + self._doc.documentLayout().draw(self._painter, ctx) + self._painter.restore() + + def _get_textdoc(self, index): + """Create the QTextDocument of an item. + + Args: + index: The QModelIndex of the item to draw. + """ + # FIXME we probably should do eliding here. See + # qcommonstyle.cpp:viewItemDrawText + text_option = QTextOption() + if self._opt.features & QStyleOptionViewItem.WrapText: + text_option.setWrapMode(QTextOption.WordWrap) + else: + text_option.setWrapMode(QTextOption.ManualWrap) + text_option.setTextDirection(self._opt.direction) + text_option.setAlignment(QStyle.visualAlignment( + self._opt.direction, self._opt.displayAlignment)) + + self._doc = QTextDocument(self) + if index.parent().isValid(): + self._doc.setPlainText(self._opt.text) + else: + self._doc.setHtml('{}'.format(html.escape(self._opt.text))) + self._doc.setDefaultFont(self._opt.font) + self._doc.setDefaultTextOption(text_option) + self._doc.setDefaultStyleSheet(get_stylesheet(""" + .highlight {{ + {color[completion.match.fg]} + }} + """)) + self._doc.setDocumentMargin(2) + + if index.column() == 0: + marks = index.data(ROLE_MARKS) + if marks is None: + return + for mark in marks: + cur = QTextCursor(self._doc) + cur.setPosition(mark[0]) + cur.setPosition(mark[1], QTextCursor.KeepAnchor) + txt = cur.selectedText() + cur.removeSelectedText() + cur.insertHtml('{}'.format( + html.escape(txt))) + + def _draw_focus_rect(self): + """Draw the focus rectangle of an ItemViewItem.""" + state = self._opt.state + if not state & QStyle.State_HasFocus: + return + o = self._opt + o.rect = self._style.subElementRect( + self._style.SE_ItemViewItemFocusRect, self._opt, self._opt.widget) + o.state |= QStyle.State_KeyboardFocusChange | QStyle.State_Item + if state & QStyle.State_Enabled: + cg = QPalette.Normal + else: + cg = QPalette.Disabled + if state & QStyle.State_Selected: + role = QPalette.Highlight + else: + role = QPalette.Window + o.backgroundColor = self._opt.palette.color(cg, role) + self._style.drawPrimitive(QStyle.PE_FrameFocusRect, o, self._painter, + self._opt.widget) + + def sizeHint(self, option, index): + """Override sizeHint of QStyledItemDelegate. + + Return the cell size based on the QTextDocument size, but might not + work correctly yet. + + Args: + option: const QStyleOptionViewItem & option + index: const QModelIndex & index + + Return: + A QSize with the recommended size. + """ + value = index.data(Qt.SizeHintRole) + if value is not None: + return value + self._opt = QStyleOptionViewItem(option) + self.initStyleOption(self._opt, index) + self._style = self._opt.widget.style() + self._get_textdoc(index) + docsize = self._doc.size().toSize() + size = self._style.sizeFromContents(QStyle.CT_ItemViewItem, self._opt, + docsize, self._opt.widget) + return size + QSize(10, 3) + + def paint(self, painter, option, index): + """Override the QStyledItemDelegate paint function. + + Args: + painter: QPainter * painter + option: const QStyleOptionViewItem & option + index: const QModelIndex & index + """ + self._painter = painter + self._painter.save() + self._opt = QStyleOptionViewItem(option) + self.initStyleOption(self._opt, index) + self._style = self._opt.widget.style() + + self._draw_background() + self._draw_icon() + self._draw_text(index) + self._draw_focus_rect() + + self._painter.restore()