import html
from PyQt5.QtWidgets import (QTreeView, QStyledItemDelegate, QStyle,
QStyleOptionViewItem)
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):
_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',
fallback=True)
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.expandAll()
self.setHeaderHidden(True)
self.setIndentation(0)
self.setItemsExpandable(False)
self.hide()
# FIXME set elidemode
def resizeEvent(self, e):
width = e.size().width()
for i in range(self.model.columnCount()):
self.setColumnWidth(i, width / 2)
super().resizeEvent(e)
def setmodel(self, model):
self.model.setSourceModel(self.completion_models[model])
self.model.pattern = ''
self.expandAll()
def resort(self, pattern):
try:
self.model.sourceModel().sort(0)
except NotImplementedError:
self.model.sort(0)
def resize_to_bar(self, geom):
bottomleft = geom.topLeft()
bottomright = geom.topRight()
delta = QPoint(0, 200)
topleft = bottomleft - delta
self.setGeometry(QRect(topleft, bottomright))
def cmd_text_changed(self, text):
if self.ignore_next:
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.mark_all_items(text)
if self.enabled:
self.show()
def first_item(self):
cat = self.model.index(0, 0)
return self.model.index(0, 0, cat)
def last_item(self):
cat = self.model.index(self.model.rowCount() - 1, 0)
return self.model.index(self.model.rowCount(cat) - 1, 0, cat)
def mark_all_items(self, needle):
for i in range(self.model.rowCount()):
cat = self.model.index(i, 0)
for k in range(self.model.rowCount(cat)):
idx = self.model.index(k, 0, cat)
old = self.model.data(idx)
marks = self.get_marks(needle, old)
self.model.setData(idx, marks, Qt.UserRole)
def get_marks(self, needle, haystack):
pos1 = pos2 = 0
marks = []
if not needle:
return marks
while True:
pos1 = haystack.find(needle, pos2)
if pos1 == -1:
break
pos2 = pos1 + len(needle)
marks.append((pos1, pos2))
return marks
def tab_handler(self, shift):
if not self.completing:
return
idx = self._next_idx(shift)
self.ignore_next = True
self.selectionModel().setCurrentIndex(
idx, QItemSelectionModel.ClearAndSelect)
data = self.model.data(idx)
if data is not None:
self.append_cmd_text.emit(self.model.data(idx) + ' ')
def _next_idx(self, shift):
idx = self.selectionModel().currentIndex()
if not idx.isValid():
# No item selected yet
return self.first_item()
while True:
idx = self.indexAbove(idx) if shift else self.indexBelow(idx)
if not idx.isValid():
# wrap around if we arrived at beginning/end
return self.last_item() if shift else self.first_item()
if idx.parent().isValid():
# Item is a real item, not a category header -> success
return idx
class CompletionItemDelegate(QStyledItemDelegate):
opt = None
style = None
painter = None
def paint(self, painter, option, index):
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):
self.style.drawPrimitive(self.style.PE_PanelItemViewItem, self.opt,
self.painter, self.opt.widget)
def _draw_icon(self):
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):
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())
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('{}'.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('{}'.format(
html.escape(txt)))
doc.drawContents(self.painter, clip)
# FIXME we probably should do eliding here. See
# qcommonstyle.cpp:viewItemDrawText
self.painter.restore()
def _draw_focus_rect(self):
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)