hydrus/hydrus/client/gui/QLocator.py

809 lines
33 KiB
Python

# QLocator
# Copyright (C) 2020-2022 qcomixdev
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program 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 Affero General Public License for more details.
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from qtpy import QtCore as QC
from qtpy import QtWidgets as QW
from qtpy import QtGui as QG
import random
import math
import re
def elideRichText(richText: str, maxWidth: int, widget, elideFromLeft: bool):
doc = QG.QTextDocument()
opt = QG.QTextOption()
opt.setWrapMode(QG.QTextOption.NoWrap)
doc.setDefaultTextOption(opt)
doc.setDocumentMargin(0)
doc.setHtml(richText)
doc.adjustSize()
if doc.size().width() > maxWidth:
cursor = QG.QTextCursor (doc)
if elideFromLeft:
cursor.movePosition(QG.QTextCursor.Start)
else:
cursor.movePosition(QG.QTextCursor.End)
elidedPostfix = ""
metric = QG.QFontMetrics(widget.font())
postfixWidth = metric.horizontalAdvance(elidedPostfix)
while doc.size().width() > maxWidth - postfixWidth:
if elideFromLeft:
cursor.deleteChar()
else:
cursor.deletePreviousChar()
doc.adjustSize()
cursor.insertText(elidedPostfix)
return doc.toHtml()
return richText
class FocusEventFilter(QC.QObject):
focused = QC.Signal()
def __init__(self, parent = None):
super().__init__(parent)
def eventFilter(self, object, event) -> bool:
if event.type() == QC.QEvent.FocusIn:
self.focused.emit()
return False
class QLocatorSearchResult:
def __init__(self, id: int, defaultIconPath: str, selectedIconPath: str, closeOnActivated: bool, text: list, toggled: bool = False, toggledIconPath: str = "", toggledSelectedIconPath: str = ""):
self.id = id
self.defaultIconPath = defaultIconPath
self.selectedIconPath = selectedIconPath
self.closeOnActivated = closeOnActivated
self.text = text
self.toggled = toggled
self.toggledIconPath = toggledIconPath
self.toggledSelectedIconPath = toggledSelectedIconPath
class QLocatorTitleWidget(QW.QWidget):
def __init__(self, title: str, iconPath: str, height: int, shouldRemainHidden: bool, parent = None):
super().__init__(parent)
self.icon = QG.QIcon(iconPath)
self.iconHeight = height - 2
self.setLayout(QW.QHBoxLayout())
self.iconLabel = QW.QLabel()
self.iconLabel.setFixedHeight(self.iconHeight)
self.iconLabel.setPixmap(self.icon.pixmap(self.iconHeight, self.iconHeight))
self.titleLabel = QW.QLabel()
self.titleLabel.setText(title)
self.titleLabel.setTextFormat(QC.Qt.RichText)
self.countLabel = QW.QLabel()
self.layout().setContentsMargins(4, 1, 4, 1)
self.layout().addWidget(self.iconLabel)
self.layout().addWidget(self.titleLabel)
self.layout().addStretch(1)
self.layout().addWidget(self.countLabel)
self.layout().setAlignment(self.countLabel, QC.Qt.AlignVCenter)
self.layout().setAlignment(self.iconLabel, QC.Qt.AlignVCenter)
self.layout().setAlignment(self.titleLabel, QC.Qt.AlignVCenter)
titleFont = self.titleLabel.font()
titleFont.setBold(True)
self.titleLabel.setFont(titleFont)
self.setFixedHeight(height)
self.shouldRemainHidden = shouldRemainHidden
def updateData(self, count: int):
self.countLabel.setText(str(count))
def paintEvent(self, event):
opt = QW.QStyleOption()
opt.initFrom(self)
p = QG.QPainter(self)
self.style().drawPrimitive(QW.QStyle.PE_Widget, opt, p, self)
class QLocatorResultWidget(QW.QWidget):
up = QC.Signal()
down = QC.Signal()
activated = QC.Signal(int, int, bool)
entered = QC.Signal()
def __init__(self, keyEventTarget: QW.QWidget, height: int, primaryTextWidth: int, secondaryTextWidth: int, parent = None):
super().__init__(parent)
self.iconHeight = height - 2
self.setObjectName("unselectedLocatorResult")
self.keyEventTarget = keyEventTarget
self.setLayout(QW.QHBoxLayout())
self.iconLabel = QW.QLabel(self)
self.iconLabel.setFixedHeight(self.iconHeight)
self.mainTextLabel = QW.QLabel(self)
self.primaryTextWidth = primaryTextWidth
self.mainTextLabel.setMinimumWidth(primaryTextWidth)
self.mainTextLabel.setTextFormat(QC.Qt.RichText)
self.mainTextLabel.setTextInteractionFlags(QC.Qt.NoTextInteraction)
self.secondaryTextLabel = QW.QLabel(self)
self.secondaryTextWidth = secondaryTextWidth
self.secondaryTextLabel.setMaximumWidth(secondaryTextWidth)
self.secondaryTextLabel.setTextFormat(QC.Qt.RichText)
self.secondaryTextLabel.setTextInteractionFlags(QC.Qt.NoTextInteraction)
self.layout().setContentsMargins(4, 1, 4, 1)
self.layout().addWidget(self.iconLabel)
self.layout().addWidget(self.mainTextLabel)
self.layout().addStretch(1)
self.layout().addWidget(self.secondaryTextLabel)
self.layout().setAlignment(self.mainTextLabel, QC.Qt.AlignVCenter)
self.layout().setAlignment(self.iconLabel, QC.Qt.AlignVCenter)
self.layout().setAlignment(self.secondaryTextLabel, QC.Qt.AlignVCenter)
self.setFixedHeight(height)
self.setSizePolicy(QW.QSizePolicy.Expanding, QW.QSizePolicy.Fixed)
self.activateEnterShortcut = QW.QShortcut(QG.QKeySequence(QC.Qt.Key_Enter), self)
self.activateEnterShortcut.setContext(QC.Qt.WidgetShortcut)
self.activateReturnShortcut = QW.QShortcut(QG.QKeySequence(QC.Qt.Key_Return), self)
self.activateReturnShortcut.setContext(QC.Qt.WidgetShortcut)
self.upShortcut = QW.QShortcut(QG.QKeySequence(QC.Qt.Key_Up), self)
self.upShortcut.setContext(QC.Qt.WidgetShortcut)
self.downShortcut = QW.QShortcut(QG.QKeySequence(QC.Qt.Key_Down), self)
self.downShortcut.setContext(QC.Qt.WidgetShortcut)
self.selectedPalette = self.palette()
self.selectedPalette.setColor(QG.QPalette.Window, QG.QPalette().color(QG.QPalette.WindowText))
self.selectedPalette.setColor(QG.QPalette.WindowText, QG.QPalette().color(QG.QPalette.Window))
self.id = -1
self.providerIndex = -1
self.closeOnActivated = False
self.selected = False
self.defaultStylingEnabled = True
self.currentIcon = QG.QIcon()
self.currentToggledIcon = QG.QIcon()
self.toggled = False
self.activateEnterShortcut.activated.connect(self.activate)
self.activateReturnShortcut.activated.connect(self.activate)
self.upShortcut.activated.connect(self.up)
self.downShortcut.activated.connect(self.down)
def paintEvent(self, event):
opt = QW.QStyleOption()
opt.initFrom(self)
p = QG.QPainter(self)
self.style().drawPrimitive(QW.QStyle.PE_Widget, opt, p, self)
def enterEvent(self, event):
self.entered.emit()
def mousePressEvent(self, event):
self.entered.emit()
def mouseReleaseEvent(self, event):
self.activate()
def activate(self):
if not self.closeOnActivated:
self.toggled = not self.toggled
iconToUse = self.currentIcon if not self.toggled else self.currentToggledIcon
self.iconLabel.setPixmap(iconToUse.pixmap(self.iconHeight, self.iconHeight, QG.QIcon.Selected if self.selected else QG.QIcon.Normal))
self.activated.emit(self.providerIndex, self.id, self.closeOnActivated)
def updateData(self, providerIndex: int, data: QLocatorSearchResult):
self.toggled = data.toggled
self.currentIcon = QG.QIcon()
self.currentIcon.addFile(data.defaultIconPath, QC.QSize(), QG.QIcon.Normal)
self.currentIcon.addFile(data.selectedIconPath, QC.QSize(), QG.QIcon.Selected)
self.currentToggledIcon = QG.QIcon()
self.currentToggledIcon.addFile(data.toggledIconPath, QC.QSize(), QG.QIcon.Normal)
self.currentToggledIcon.addFile(data.toggledSelectedIconPath, QC.QSize(), QG.QIcon.Selected)
iconToUse = self.currentIcon if not self.toggled else self.currentToggledIcon
self.iconLabel.setPixmap(iconToUse.pixmap(self.iconHeight, self.iconHeight, QG.QIcon.Selected if self.selected else QG.QIcon.Normal))
self.mainTextLabel.clear()
self.secondaryTextLabel.clear()
if len(data.text) > 0:
self.mainTextLabel.setText(elideRichText(data.text[0], self.primaryTextWidth, self.mainTextLabel, False))
if len(data.text) > 1:
self.secondaryTextLabel.setText(elideRichText(data.text[1], self.secondaryTextWidth, self.secondaryTextLabel, True))
self.id = data.id
self.closeOnActivated = data.closeOnActivated
self.providerIndex = providerIndex
def setDefaultStylingEnabled(self, enabled: bool):
if self.defaultStylingEnabled and not enabled: self.setPalette(QG.QPalette())
self.defaultStylingEnabled = enabled
def setSelected(self, selected: bool):
self.selected = selected
if selected:
self.setObjectName("selectedLocatorResult")
if self.defaultStylingEnabled: self.setPalette(self.selectedPalette)
self.style().unpolish(self)
self.style().polish(self)
self.setFocus()
else:
self.setObjectName("unselectedLocatorResult")
if self.defaultStylingEnabled: self.setPalette(QG.QPalette())
self.style().unpolish(self)
self.style().polish(self)
iconToUse = self.currentIcon if not self.toggled else self.currentToggledIcon
self.iconLabel.setPixmap(iconToUse.pixmap(self.iconHeight, self.iconHeight, QG.QIcon.Selected if self.selected else QG.QIcon.Normal))
def keyPressEvent(self, ev: QG.QKeyEvent):
if ev.key() != QC.Qt.Key_Up and ev.key() != QC.Qt.Key_Down and ev.key() != QC.Qt.Key_Enter and ev.key() != QC.Qt.Key_Return:
QW.QApplication.postEvent(self.keyEventTarget, QG.QKeyEvent(ev.type(), ev.key(), ev.modifiers(), ev.text(), ev.isAutoRepeat()))
self.keyEventTarget.setFocus()
else:
super().keyPressEvent(self, ev)
class QAbstractLocatorSearchProvider(QC.QObject):
resultsAvailable = QC.Signal(int, list)
def __init__(self, parent = None):
super().__init__(parent)
class QExampleSearchProvider(QAbstractLocatorSearchProvider):
def __init__(self, parent = None):
super().__init__(parent)
def title(self):
return "Example search provider"
def suggestedReservedItemCount(self):
return 32
def resultSelected(self, resultID: int):
pass
def processQuery(self, query: str, context, jobID: int):
resCount = random.randint(0, 50)
results = []
for i in range(resCount):
randomStr = str()
for j in range(5):
randomStr += str(chr(97+random.randint(0, 26)))
txt = []
txt.append("Result <b>text</b> #" + str(i) + randomStr)
txt.append("Secondary result text")
results.append(QLocatorSearchResult(0, "icon.svg", "icon.svg", True, txt, False, "icon.svg", "icon.svg"))
self.resultsAvailable.emit(jobID, results)
def stopJobs(self, jobs):
pass
def hideTitle(self):
return False
def titleIconPath(self):
return "icon.svg"
class QCalculatorSearchProvider(QAbstractLocatorSearchProvider):
def __init__(self, parent = None):
super().__init__(parent)
self.safeEnv = {
'ceil': math.ceil,
'abs': abs,
'floor': math.floor,
'gcd': math.gcd,
'exp': math.exp,
'log': math.log,
'log2': math.log2,
'log10': math.log10,
'pow': math.pow,
'sqrt': math.sqrt,
'acos': math.acos,
'asin': math.asin,
'atan': math.atan,
'atan2': math.atan2,
'cos': math.cos,
'hypot': math.hypot,
'sin': math.sin,
'tan': math.tan,
'degrees': math.degrees,
'radians': math.radians,
'acosh': math.acosh,
'asinh': math.asinh,
'atanh': math.atanh,
'cosh': math.cosh,
'sinh': math.sinh,
'tanh': math.tanh,
'erf': math.erf,
'erfc': math.erfc,
'gamma': math.gamma,
'lgamma': math.lgamma,
'pi': math.pi,
'e': math.e,
'inf': math.inf,
'randint': random.randint,
'random': random.random,
'factorial': math.factorial
}
self.safePattern = re.compile("^("+"|".join(self.safeEnv.keys())+r"|[0-9.*+\-%/()]|\s" + ")+$")
def processQuery(self, query: str, context, jobID: int):
try:
if len(query.strip()) and self.safePattern.match(query):
result = str(eval(query, {"__builtins__": {}}, self.safeEnv))
try:
int(result)
except:
result = str(float(result))
self.resultsAvailable.emit(jobID, [QLocatorSearchResult(0, self.iconPath(), self.selectedIconPath(), False, [result,"Calculator"], False, self.iconPath(), self.selectedIconPath())])
except:
pass
def title(self):
return str()
def suggestedReservedItemCount(self):
return 1
def resultSelected(self, resultID: int):
pass
def stopJobs(self, jobs):
pass
def hideTitle(self):
return True
def titleIconPath(self):
return str()
def selectedIconPath(self):
return str()
def iconPath(self):
return str()
class QLocatorWidget(QW.QWidget):
finished = QC.Signal()
def __init__(self, parent = None, width: int = 600, resultHeight: int = 36, titleHeight: int = 36, primaryTextWidth: int = 320, secondaryTextWidth: int = 200, maxVisibleItemCount: int = 8):
super().__init__(parent)
self.alignment = QC.Qt.AlignCenter
self.resultHeight = resultHeight
self.titleHeight = titleHeight
self.primaryTextWidth = primaryTextWidth
self.locator = None
self.secondaryTextWidth = secondaryTextWidth
self.maxVisibleItemCount = maxVisibleItemCount
self.reservedItemCounts = []
self.visibleResultItemCounts = []
self.currentJobIds = []
self.titleItems = []
self.resultItems = []
self.escapeShortcuts = []
self.selectedLayoutItemIndex = 0
self.defaultStylingEnabled = True
self.context = None
self.lastQuery = str()
self.setVisible(False)
self.setLayout(QW.QVBoxLayout())
self.searchEdit = QW.QLineEdit()
self.resultList = QW.QScrollArea()
self.resultLayout = QW.QVBoxLayout()
self.resultList.setWidget(QW.QWidget())
self.resultList.widget().setLayout(self.resultLayout)
self.resultList.setWidgetResizable(True)
self.resultLayout.setSizeConstraint(QW.QLayout.SetMinAndMaxSize)
self.layout().addWidget(self.searchEdit)
self.layout().addWidget(self.resultList)
self.layout().setContentsMargins(0, 0, 0, 0)
self.layout().setSpacing(0)
self.resultLayout.setContentsMargins(0, 0, 0, 0)
self.resultLayout.setSpacing(0)
self.setFixedWidth(width)
self.setWindowFlags(QC.Qt.FramelessWindowHint | QC.Qt.WindowStaysOnTopHint | QC.Qt.CustomizeWindowHint | QC.Qt.Popup)
self.resultList.setSizeAdjustPolicy(QW.QAbstractScrollArea.AdjustToContents)
self.setSizePolicy(QW.QSizePolicy.Fixed, QW.QSizePolicy.Maximum)
self.setEscapeShortcuts([QC.Qt.Key_Escape])
self.editorDownShortcut = QW.QShortcut(QG.QKeySequence(QC.Qt.Key_Down), self.searchEdit)
self.editorDownShortcut.setContext(QC.Qt.WidgetShortcut)
self.editorDownShortcut.activated.connect(self.handleEditorDown)
def handleTextEdited():
for i in range(len(self.resultItems)):
for it in self.resultItems[i]: self.setResultVisible(it, False)
self.setResultVisible(self.titleItems[i], False)
self.selectedLayoutItemIndex = 0
self.updateResultListHeight()
self.searchEdit.textEdited.connect(handleTextEdited)
def handleSearchFocused():
if self.selectedLayoutItemIndex < self.resultLayout.count():
widget = self.resultLayout.itemAt(self.selectedLayoutItemIndex).widget()
if widget:
if isinstance(widget, QLocatorResultWidget):
widget.setSelected(False)
self.selectedLayoutItemIndex = 0
filter = FocusEventFilter()
self.searchEdit.installEventFilter(filter)
filter.focused.connect(handleSearchFocused)
self.queryTimer = QC.QTimer(self)
self.queryTimer.setInterval(0)
self.queryTimer.setSingleShot(True)
def handleQueryTimeout():
if not self.locator: return
self.currentJobIds = self.locator.query(self.lastQuery, self.context)
self.queryTimer.timeout.connect(handleQueryTimeout)
self.searchEdit.textEdited.connect(self.queryLocator)
self.updateResultListHeight()
def setAlignment( self, alignment ):
if alignment == QC.Qt.AlignCenter:
self.alignment = alignment
self.updateAlignment()
elif alignment == QC.Qt.AlignTop:
self.alignment = alignment
self.updateAlignment()
def updateAlignment( self ):
widget = self
while True:
parent = widget.parentWidget()
if not parent:
break
else:
widget = parent
screenRect = QW.QApplication.primaryScreen().availableGeometry()
if widget != self: # there is a parent
screenRect = widget.geometry()
if self.alignment == QC.Qt.AlignCenter:
centerRect = QW.QStyle.alignedRect(QC.Qt.LeftToRight, QC.Qt.AlignCenter, self.size(), screenRect)
centerRect.setY(max(0, centerRect.y() - self.resultHeight * 4))
self.setGeometry(centerRect)
elif self.alignment == QC.Qt.AlignTop:
rect = QW.QStyle.alignedRect(QC.Qt.LeftToRight, QC.Qt.AlignHCenter | QC.Qt.AlignTop, self.size(), screenRect)
self.setGeometry(rect)
def paintEvent(self, event):
opt = QW.QStyleOption()
opt.initFrom(self)
p = QG.QPainter(self)
self.style().drawPrimitive(QW.QStyle.PE_Widget, opt, p, self)
def providerAdded(self, title: str, titleIconPath: str, suggestedReservedItemCount: int, hideTitle: bool):
newTitleWidget = QLocatorTitleWidget(title, titleIconPath, self.titleHeight, hideTitle)
self.visibleResultItemCounts.append(0)
self.reservedItemCounts.append(suggestedReservedItemCount)
self.titleItems.append(newTitleWidget)
self.resultLayout.addWidget(newTitleWidget)
newTitleWidget.setVisible(False)
self.resultItems.append([])
for i in range(suggestedReservedItemCount):
newWidget = QLocatorResultWidget(self.searchEdit, self.resultHeight, self.primaryTextWidth, self.secondaryTextWidth, self)
self.setupResultWidget(newWidget)
self.resultItems[-1].append(newWidget)
self.resultLayout.addWidget(newWidget)
newWidget.setVisible(False)
def setEscapeShortcuts(self, shortcuts):
for escapeShortcut in self.escapeShortcuts:
escapeShortcut.deleteLater()
self.escapeShortcuts = []
for shortcut in shortcuts:
newShortcut = QW.QShortcut(QG.QKeySequence(shortcut), self)
self.escapeShortcuts.append(newShortcut)
newShortcut.activated.connect(self.finish)
def setLocator(self, locator):
if self.locator:
self.locator.providerAdded.disconnect(self.providerAdded)
self.locator.resultsAvailable.disconnect(self.handleResultsAvailable)
self.reset()
self.locator = locator
if self.locator:
for provider in self.locator.providers:
self.providerAdded(provider.title(), self.locator.iconBasePath + provider.titleIconPath(), provider.suggestedReservedItemCount(), provider.hideTitle())
self.locator.providerAdded.connect(self.providerAdded)
self.locator.resultsAvailable.connect(self.handleResultsAvailable)
def setDefaultStylingEnabled(self, enabled: bool) -> None:
self.defaultStylingEnabled = enabled
for i in range(len(self.resultItems)):
for it in self.resultItems[i]: it.setDefaultStylingEnabled(enabled)
def __del__(self):
self.queryTimer.stop()
if self.locator:
if self.currentJobIds: self.locator.stopJobs(self.currentJobIds)
self.locator.providerAdded.disconnect(self.providerAdded)
self.locator.resultsAvailable.disconnect(self.handleResultsAvailable)
for it in self.titleItems:
it.deleteLater()
for i in range(len(self.resultItems)):
for it in self.resultItems[i]: it.deleteLater()
def setContext(self, context):
self.context = context
def setQueryTimeout(self, msec: int):
self.queryTimer.setInterval(msec)
def start(self):
self.clear()
self.updateAlignment()
self.show()
self.searchEdit.setFocus()
self.searchEdit.textEdited.emit("") # pylint: disable=E1101
def finish(self, doNotStopJobs: bool = False):
self.queryTimer.stop()
if self.locator and self.currentJobIds and not doNotStopJobs: self.locator.stopJobs(self.currentJobIds)
self.clear()
self.hide()
self.finished.emit()
def reset(self):
if self.locator and self.currentJobIds: self.locator.stopJobs(self.currentJobIds)
self.currentJobIds.clear()
self.reservedItemCounts.clear()
self.visibleResultItemCounts.clear()
for it in self.titleItems:
it.deleteLater()
self.titleItems = []
for i in range(len(self.resultItems)):
for it in self.resultItems[i]: it.deleteLater()
self.resultItems = []
if self.locator:
for provider in self.locator.providers:
self.providerAdded(provider.title(), self.locator.iconBasePath + provider.titleIconPath(), provider.suggestedReservedItemCount(), provider.hideTitle())
def handleResultsAvailable(self, providerIndex: int, jobID: int) -> None:
data = self.locator.getResult(jobID)
self.titleItems[providerIndex].updateData(len(data))
if not self.titleItems[providerIndex].shouldRemainHidden:
self.setResultVisible(self.titleItems[providerIndex], len(data) != 0)
if len(data): self.resultList.setVisible(True)
if len(data) > len(self.resultItems[providerIndex]):
titleItemIdx = 0
for i in range(self.resultLayout.count()):
if self.resultLayout.itemAt(i).widget() == self.titleItems[providerIndex]: break
titleItemIdx += 1
i = len(self.resultItems[providerIndex])
while i < len(data):
newWidget = QLocatorResultWidget(self.searchEdit, self.resultHeight, self.primaryTextWidth, self.secondaryTextWidth, self)
self.setupResultWidget(newWidget)
self.resultLayout.insertWidget(titleItemIdx + i + 1, newWidget)
self.resultItems[providerIndex].append(newWidget)
i += 1
for i in range(len(self.resultItems[providerIndex])):
if i < len(data):
self.resultItems[providerIndex][i].updateData(providerIndex, data[i])
if not self.resultItems[providerIndex][i].isVisible():
self.setResultVisible(self.resultItems[providerIndex][i], True)
else:
if self.resultItems[providerIndex][i].isVisible():
self.setResultVisible(self.resultItems[providerIndex][i], False)
self.updateResultListHeight()
def queryLocator(self, query: str) -> None:
if not self.locator: return
if self.currentJobIds:
self.locator.stopJobs(self.currentJobIds)
self.lastQuery = query
self.queryTimer.start()
def handleResultUp(self):
resultWidget = self.sender()
resultWidget.setSelected(False)
i = self.selectedLayoutItemIndex - 1
while i > 0:
widget = self.resultLayout.itemAt(i).widget()
if widget and widget.isVisible():
if isinstance(widget, QLocatorResultWidget):
self.selectedLayoutItemIndex = i
widget.setSelected(True)
self.resultList.ensureVisible(0, widget.pos().y(), 0, 0)
return
i = i - 1
self.searchEdit.setFocus()
self.resultList.ensureVisible(0, 0, 0, 0)
def handleResultDown(self):
resultWidget = self.sender()
i = self.selectedLayoutItemIndex + 1
while i < self.resultLayout.count():
widget = self.resultLayout.itemAt(i).widget()
if widget and widget.isVisible():
if isinstance(widget, QLocatorResultWidget):
self.selectedLayoutItemIndex = i
resultWidget.setSelected(False)
widget.setSelected(True)
self.resultList.ensureVisible(0, widget.pos().y() + widget.height(), 0, 0)
break
i = i + 1
def handleEditorDown(self):
for i in range(self.resultLayout.count()):
widget = self.resultLayout.itemAt(i).widget()
if widget and widget.isVisible():
if isinstance(widget, QLocatorResultWidget):
self.selectedLayoutItemIndex = i
widget.setSelected(True)
break
def handleEntered(self):
resultWidget = self.sender()
i = 0
while i < self.resultLayout.count():
widget = self.resultLayout.itemAt(i).widget()
if widget and widget.isVisible() and isinstance(widget, QLocatorResultWidget):
if widget == resultWidget:
self.selectedLayoutItemIndex = i
widget.setSelected(True)
else:
widget.setSelected(False)
i = i + 1
def handleResultActivated(self, provider: int, id: int, closeOnSelected: bool):
currJobIdsTmp = self.currentJobIds[:]
if closeOnSelected: self.finish(True)
if self.locator:
self.locator.activateResult(provider, id)
if closeOnSelected and currJobIdsTmp: self.locator.stopJobs(currJobIdsTmp)
def clear(self):
self.searchEdit.clear()
self.queryTimer.stop()
self.lastQuery = str()
self.context = None
for i in range(len(self.titleItems)):
self.titleItems[i].setVisible(False)
for it in self.resultItems[i]:
it.setVisible(False)
it.setSelected(False)
self.visibleResultItemCounts[i] = 0
self.deleteAdditionalItems()
self.updateResultListHeight()
def deleteAdditionalItems(self):
titleIndex = 0
for i in range(len(self.titleItems)):
resultItemCount = len(self.resultItems[i])
j = titleIndex
while j < self.resultLayout.count():
if self.resultLayout.itemAt(j).widget() == self.titleItems[i]: break
titleIndex += 1
j += 1
k = self.reservedItemCounts[i]
while k < resultItemCount:
self.resultLayout.takeAt(titleIndex + self.reservedItemCounts[i] + 1).widget().deleteLater()
k += 1
self.resultItems[i] = self.resultItems[i][:self.reservedItemCounts[i]]
def getVisibleHeight(self) -> int:
itemCount = 0
height = 0
for i in range(len(self.titleItems)):
if itemCount >= self.maxVisibleItemCount: break
if self.visibleResultItemCounts[i] > 0:
if not self.titleItems[i].shouldRemainHidden:
itemCount += 1
height += self.titleHeight
if itemCount >= self.maxVisibleItemCount: break
visibleItems = min(self.visibleResultItemCounts[i], self.maxVisibleItemCount - itemCount)
itemCount += visibleItems
height += self.resultHeight * visibleItems
return height
def setResultVisible(self, widget: QW.QWidget, visible: bool):
if widget.isVisible() and not visible:
widget.setVisible(False)
if isinstance(widget, QLocatorResultWidget):
self.visibleResultItemCounts[widget.providerIndex] -= 1
widget.setSelected(False)
elif not widget.isVisible() and visible:
if isinstance(widget, QLocatorResultWidget):
self.visibleResultItemCounts[widget.providerIndex] += 1
widget.setVisible(True)
def updateResultListHeight(self):
pos = self.pos()
visibleHeight = self.getVisibleHeight()
if visibleHeight == 0:
self.resultList.setVisible(False)
else:
self.resultList.setVisible(True)
self.resultList.widget().setMaximumHeight(visibleHeight)
if self.resultList.isVisible():
self.setFixedHeight(self.searchEdit.sizeHint().height() + (self.resultList.height() - self.resultList.contentsRect().height()) + visibleHeight)
else:
self.setFixedHeight(self.searchEdit.sizeHint().height())
self.move(pos)
def setupResultWidget(self, widget):
widget.up.connect(self.handleResultUp)
widget.down.connect(self.handleResultDown)
widget.activated.connect(self.handleResultActivated)
widget.entered.connect(self.handleEntered)
widget.setDefaultStylingEnabled(self.defaultStylingEnabled)
class QLocator(QC.QObject):
iconBasePathChanged = QC.Signal(str)
providerAdded = QC.Signal(str, str, int, bool)
resultsAvailable = QC.Signal(int, int)
def __init__(self, parent):
super().__init__(parent)
self.providers = []
self.currentJobs = {}
self.savedProviderData = {}
self.jobIDCounter = 0
self.iconBasePath = str()
def addProvider(self, provider) -> None:
self.providers.append(provider)
provider.setParent(self)
provider.resultsAvailable.connect(self.handleItemUpdate)
self.providerAdded.emit(provider.title(), self.iconBasePath + provider.titleIconPath(), provider.suggestedReservedItemCount(), provider.hideTitle())
def provider(self, idx: int):
if idx >= 0 and idx <= len(self.providers):
return self.providers[idx]
return None
def getResult(self, jobID: int):
return self.savedProviderData.pop(jobID, None)
def __del__(self):
self.stopJobs()
def setIconBasePath(self, iconBasePath: str) -> None:
if self.iconBasePath != iconBasePath:
self.iconBasePath = iconBasePath
self.iconBasePathChanged.emit(self.iconBasePath)
def query(self, queryText: str, context) -> list:
jobIDs = []
for i in range(len(self.providers)):
self.currentJobs[self.jobIDCounter] = i
jobIDs.append(self.jobIDCounter)
self.providers[i].processQuery(queryText, context, self.jobIDCounter)
self.jobIDCounter += 1
return jobIDs
def activateResult(self, provider: int, id: int) -> None:
if provider >= 0 and provider < len(self.providers):
self.providers[provider].resultSelected(id)
def handleItemUpdate(self, jobID: int, data) -> None:
if jobID in self.currentJobs:
providerIndex = self.currentJobs[jobID]
del self.currentJobs[jobID]
self.savedProviderData[jobID] = data
for dataItem in self.savedProviderData[jobID]:
dataItem.defaultIconPath = self.iconBasePath + dataItem.defaultIconPath
dataItem.selectedIconPath = self.iconBasePath + dataItem.selectedIconPath
dataItem.toggledIconPath = self.iconBasePath + dataItem.toggledIconPath
dataItem.toggledSelectedIconPath = self.iconBasePath + dataItem.toggledSelectedIconPath
self.resultsAvailable.emit(providerIndex, jobID)
def stopJobs(self, ids = []) -> None:
if not len(ids):
self.currentJobs = {}
for provider in self.providers:
provider.stopJobs([])
self.savedProviderData = {}
else:
for provider in self.providers:
provider.stopJobs(ids)
for id in ids:
if id in self.currentJobs:
del self.currentJobs[id]
if id in self.savedProviderData:
del self.savedProviderData[id]