279 lines
9.6 KiB
Python
279 lines
9.6 KiB
Python
|
import logging
|
||
|
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)
|
||
|
|
||
|
from qutebrowser.utils.completion import CompletionFilterModel
|
||
|
from qutebrowser.commands.utils import CommandCompletionModel
|
||
|
|
||
|
class CompletionView(QTreeView):
|
||
|
_stylesheet = """
|
||
|
QTreeView {
|
||
|
font-family: Monospace, Courier;
|
||
|
color: #333333;
|
||
|
outline: 0;
|
||
|
}
|
||
|
QTreeView::item {
|
||
|
background: white;
|
||
|
}
|
||
|
QTreeView::item:has-children {
|
||
|
font-weight: bold;
|
||
|
|
||
|
background: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #e4e4e4,
|
||
|
stop:1 #dbdbdb);
|
||
|
border-top: 1px solid #808080;
|
||
|
border-bottom: 1px solid #bbbbbb;
|
||
|
}
|
||
|
QTreeView::item:selected {
|
||
|
border-top: 1px solid #f2f2c0;
|
||
|
border-bottom: 1px solid #e6e680;
|
||
|
background-color: #ffec8b;
|
||
|
color: #333333;
|
||
|
}
|
||
|
"""
|
||
|
# 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
|
||
|
|
||
|
def __init__(self, parent=None):
|
||
|
super().__init__(parent)
|
||
|
self.completion_models[''] = None
|
||
|
self.completion_models['command'] = CommandCompletionModel()
|
||
|
self.model = CompletionFilterModel()
|
||
|
self.setModel(self.model)
|
||
|
self.model.setSourceModel(self.completion_models['command'])
|
||
|
self.setItemDelegate(CompletionItemDelegate())
|
||
|
self.setStyleSheet(self._stylesheet.strip())
|
||
|
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 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()
|
||
|
return
|
||
|
|
||
|
self.setmodel('command')
|
||
|
text = text.lstrip(':')
|
||
|
self.model.pattern = text
|
||
|
self.mark_all_items(text)
|
||
|
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 self.isHidden():
|
||
|
return
|
||
|
selmodel = self.selectionModel()
|
||
|
cur = selmodel.currentIndex()
|
||
|
if not cur.isValid():
|
||
|
idx = self.first_item()
|
||
|
elif shift:
|
||
|
idx = self.indexAbove(cur)
|
||
|
if not idx.isValid():
|
||
|
idx = self.last_item()
|
||
|
cur = idx
|
||
|
else:
|
||
|
idx = self.indexBelow(cur)
|
||
|
if not idx.isValid():
|
||
|
idx = self.first_item()
|
||
|
cur = idx
|
||
|
self.ignore_next = True
|
||
|
selmodel.setCurrentIndex(idx, QItemSelectionModel.ClearAndSelect)
|
||
|
self.append_cmd_text.emit(self.model.data(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('<b>{}</b>'.format(html.escape(self.opt.text)))
|
||
|
doc.setDefaultFont(self.opt.font)
|
||
|
doc.setDefaultTextOption(text_option)
|
||
|
doc.setDefaultStyleSheet("""
|
||
|
.highlight {
|
||
|
color: blue;
|
||
|
}
|
||
|
""")
|
||
|
doc.setDocumentMargin(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()
|
||
|
# FIXME escape html in txt
|
||
|
cur.insertHtml('<span class="highlight">{}</span>'.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)
|