qutebrowser/qutebrowser/widgets/completion.py
Florian Bruhin 053ce35032 Hopefully sane default config handling.
This now reads the values from the config, and from a default config if
it's not overridden.
2014-01-30 23:05:39 +01:00

362 lines
13 KiB
Python

"""Completion view which appears when something is typed in the statusbar
command section.
Defines a CompletionView which uses CompletionFiterModel and CompletionModel
subclasses to provide completions.
"""
import html
from PyQt5.QtWidgets import (QTreeView, QStyledItemDelegate, QStyle,
QStyleOptionViewItem, QSizePolicy)
from PyQt5.QtCore import (QRectF, QRect, QPoint, pyqtSignal, Qt,
QItemSelectionModel)
from PyQt5.QtGui import (QIcon, QPalette, QTextDocument, QTextOption,
QTextCursor)
import qutebrowser.utils.config as config
from qutebrowser.utils.completion import CompletionFilterModel
from qutebrowser.commands.utils import CommandCompletionModel
class CompletionView(QTreeView):
"""The view showing available completions.
Based on QTreeView but heavily customized so root elements show as category
headers, and children show as flat list.
Highlights completions based on marks in the UserRole.
"""
_stylesheet = """
QTreeView {{
font-family: {monospace};
{color[completion.fg]}
{color[completion.bg]}
outline: 0;
}}
QTreeView::item {{
{color[completion.item.fg]}
{color[completion.item.bg]}
}}
QTreeView::item:has-children {{
font-weight: bold;
{color[completion.category.fg]}
{color[completion.category.bg]}
border-top: 1px solid {color[completion.category.border.top]};
border-bottom: 1px solid
{color[completion.category.border.bottom]};
}}
QTreeView::item:selected {{
border-top: 1px solid {color[completion.item.selected.border.top]};
border-bottom: 1px solid
{color[completion.item.selected.border.bottom]};
{color[completion.item.selected.bg]}
{color[completion.item.selected.fg]}
}}
"""
# FIXME because we use :has-children, if a category is empty, it won't look
# like one anymore
# FIXME somehow only the first column is yellow, even with
# setAllColumnsShowFocus
completion_models = {}
append_cmd_text = pyqtSignal(str)
ignore_next = False
enabled = True
completing = False
def __init__(self, parent=None):
super().__init__(parent)
self.enabled = config.config.getboolean('general', 'show_completion')
self.completion_models[''] = None
self.completion_models['command'] = CommandCompletionModel()
self.model = CompletionFilterModel()
self.setModel(self.model)
self.model.setSourceModel(self.completion_models['command'])
self.model.pattern_changed.connect(self.resort)
self.setItemDelegate(CompletionItemDelegate())
self.setStyleSheet(config.get_stylesheet(self._stylesheet))
self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Minimum)
self.expandAll()
self.setHeaderHidden(True)
self.setIndentation(0)
self.setItemsExpandable(False)
self.hide()
# FIXME set elidemode
def resizeEvent(self, e):
"""Extends resizeEvent of QTreeView.
Always adjusts the column width to the new window width.
e -- The QResizeEvent.
"""
width = e.size().width()
cols = self.model.columnCount()
assert cols >= 1
assert width / cols > 1
for i in range(cols):
self.setColumnWidth(i, width / cols)
super().resizeEvent(e)
def setmodel(self, model):
"""Switch completion to a new model.
Called from cmd_text_changed().
model -- A QAbstractItemModel with available completions.
"""
self.model.setSourceModel(self.completion_models[model])
self.model.pattern = ''
self.expandAll()
def resort(self, pattern): # pylint: disable=unused-argument
"""Sort the available completions.
If the current completion model overrides sort(), it is used.
If not, the default implementation in QCompletionFilterModel is called.
"""
sortcol = 0
try:
self.model.sourceModel().sort(sortcol)
except NotImplementedError:
self.model.sort(sortcol)
def resize_to_bar(self, geom):
"""Resize the completion area to the statusbar geometry.
Slot for the resized signal of the statusbar.
geom -- A QRect containing the statusbar geometry.
"""
bottomleft = geom.topLeft()
bottomright = geom.topRight()
delta = QPoint(0, 200)
topleft = bottomleft - delta
assert topleft.x() < bottomright.x()
assert topleft.y() < bottomright.y()
self.setGeometry(QRect(topleft, bottomright))
def cmd_text_changed(self, text):
"""Check if completions are available and activate them.
Slot for the textChanged signal of the statusbar command widget.
text -- The new text
"""
if self.ignore_next:
# Text changed by a completion, so we don't have to complete again.
self.ignore_next = False
return
# FIXME more sophisticated completions
if ' ' in text or not text.startswith(':'):
self.hide()
self.completing = False
return
self.completing = True
self.setmodel('command')
text = text.lstrip(':')
self.model.pattern = text
self.model.sourceModel().mark_all_items(text)
if self.enabled:
self.show()
def tab_handler(self, shift):
"""Handle a tab press for the CompletionView.
Selects the previous/next item and writes the new text to the
statusbar. Called by key_(s)tab_handler in statusbar.command.
shift -- Whether shift is pressed or not.
"""
if not self.completing:
# No completion running at the moment, ignore keypress
return
idx = self._next_idx(shift)
self.selectionModel().setCurrentIndex(
idx, QItemSelectionModel.ClearAndSelect)
data = self.model.data(idx)
if data is not None:
self.ignore_next = True
self.append_cmd_text.emit(self.model.data(idx) + ' ')
def _next_idx(self, upwards):
"""Get the previous/next QModelIndex displayed in the view.
Used by tab_handler.
upwards -- Get previous item, not next.
"""
idx = self.selectionModel().currentIndex()
if not idx.isValid():
# No item selected yet
return self.model.first_item()
while True:
idx = self.indexAbove(idx) if upwards else self.indexBelow(idx)
# wrap around if we arrived at beginning/end
if not idx.isValid() and upwards:
return self.model.last_item()
elif not idx.isValid() and not upwards:
return self.model.first_item()
elif idx.parent().isValid():
# Item is a real item, not a category header -> success
return idx
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
"""
opt = None
style = None
painter = None
def paint(self, painter, option, index):
"""Overrides the QStyledItemDelegate paint function."""
painter.save()
self.painter = painter
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()
painter.restore()
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.
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)
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))
# FIXME this is a dirty fix for the text jumping by one pixel...
# we really should do this properly somehow
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._draw_textdoc(index, text_rect)
self.painter.restore()
def _draw_textdoc(self, index, text_rect):
"""Draw the QTextDocument of an item.
index -- The QModelIndex of the item to draw.
text_rect -- The QRect to clip the drawing to.
"""
# FIXME we probably should do eliding here. See
# qcommonstyle.cpp:viewItemDrawText
clip = QRectF(0, 0, text_rect.width(), text_rect.height())
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))
doc = QTextDocument()
if index.parent().isValid():
doc.setPlainText(self.opt.text)
else:
doc.setHtml('<b>{}</b>'.format(html.escape(self.opt.text)))
doc.setDefaultFont(self.opt.font)
doc.setDefaultTextOption(text_option)
doc.setDefaultStyleSheet(config.get_stylesheet("""
.highlight {{
{color[completion.match.fg]}
}}
"""))
doc.setDocumentMargin(0)
if index.column() == 0:
marks = index.data(Qt.UserRole)
for mark in marks:
cur = QTextCursor(doc)
cur.setPosition(mark[0])
cur.setPosition(mark[1], QTextCursor.KeepAnchor)
txt = cur.selectedText()
cur.removeSelectedText()
cur.insertHtml('<span class="highlight">{}</span>'.format(
html.escape(txt)))
doc.drawContents(self.painter, clip)
def _draw_focus_rect(self):
"""Draws 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)