#!/usr/bin/env python # --!-- coding: utf8 --!-- import re from PyQt5.QtCore import Qt, QRect, QEvent, QCoreApplication from PyQt5.QtGui import QPalette, QFontMetrics, QKeySequence from PyQt5.QtWidgets import QWidget, qApp, QListWidgetItem, QStyledItemDelegate, QStyle, QLabel, QToolTip, QShortcut from manuskript.functions import mainWindow from manuskript.ui import style from manuskript.ui.search_ui import Ui_search from manuskript.enums import Model from manuskript.models.flatDataModelWrapper import flatDataModelWrapper from manuskript.ui.searchMenu import searchMenu from manuskript.ui.highlighters.searchResultHighlighters.searchResultHighlighter import searchResultHighlighter import logging LOGGER = logging.getLogger(__name__) class search(QWidget, Ui_search): def __init__(self, parent=None): _translate = QCoreApplication.translate QWidget.__init__(self, parent) self.setupUi(self) self.searchTextInput.returnPressed.connect(self.search) self.searchTextInput.textChanged.connect(self.updateSearchFeedback) self.searchMenu = searchMenu() self.btnOptions.setMenu(self.searchMenu) self.searchMenu.triggered.connect(self.onSearchMenuChange) self.delegate = listResultDelegate(self) self.result.setItemDelegate(self.delegate) self.result.setMouseTracking(True) self.result.itemClicked.connect(self.openItem) self.result.setStyleSheet(style.searchResultSS()) self.searchTextInput.setStyleSheet(style.lineEditSS()) self.searchResultHighlighter = searchResultHighlighter() self.noResultsLabel = QLabel(_translate("Search", "No results found"), self.result) self.noResultsLabel.setVisible(False) self.noResultsLabel.setStyleSheet("QLabel {color: gray;}") # Add shortcuts for navigating through search results QShortcut(QKeySequence(_translate("MainWindow", "F3")), self.searchTextInput, self.nextSearchResult) QShortcut(QKeySequence(_translate("MainWindow", "Shift+F3")), self.searchTextInput, self.previousSearchResult) # These texts are already included in translation files but including ":" at the end. We force here the # translation for them without ":" _translate("MainWindow", "Situation") _translate("MainWindow", "Status") def nextSearchResult(self): if self.result.currentRow() < self.result.count() - 1: self.result.setCurrentRow(self.result.currentRow() + 1) else: self.result.setCurrentRow(0) if 0 < self.result.currentRow() < self.result.count(): self.openItem(self.result.currentItem()) def previousSearchResult(self): if self.result.currentRow() > 0: self.result.setCurrentRow(self.result.currentRow() - 1) else: self.result.setCurrentRow(self.result.count() - 1) if 0 < self.result.currentRow() < self.result.count(): self.openItem(self.result.currentItem()) def onSearchMenuChange(self): search_string = self.searchTextInput.text() self.updateSearchFeedback(search_string) def updateSearchFeedback(self, search_string): palette = QPalette() try: self.compileRegex(search_string) except Exception as e: # From https://stackoverflow.com/questions/27432456/python-qlineedit-text-color palette.setColor(QPalette.Text, Qt.red) self.searchTextInput.setPalette(palette) def prepareRegex(self, searchText): rtn = None try: rtn = self.compileRegex(searchText) except re.error as e: LOGGER.info("Problem preparing regular expression: " + e.msg) rtn = None except Exception as e: LOGGER.info("Problem preparing regular expression") rtn = None return rtn def compileRegex(self, searchText): # Intentionally throws exceptions for use elsewhere flags = re.UNICODE if self.searchMenu.caseSensitive() is False: flags |= re.IGNORECASE if self.searchMenu.regex() is False: searchText = re.escape(searchText) if self.searchMenu.matchWords() is True: # Source: https://stackoverflow.com/a/15863102 searchText = r'\b' + searchText + r'\b' return re.compile(searchText, flags) def search(self): self.result.clear() self.result.setCurrentRow(0) searchText = self.searchTextInput.text() if len(searchText) > 0: results = list() searchRegex = self.prepareRegex(searchText) if searchRegex is not None: # Set override cursor qApp.setOverrideCursor(Qt.WaitCursor) for model, modelName in [ (mainWindow().mdlOutline, Model.Outline), (mainWindow().mdlCharacter, Model.Character), (flatDataModelWrapper(mainWindow().mdlFlatData), Model.FlatData), (mainWindow().mdlWorld, Model.World), (mainWindow().mdlPlots, Model.Plot) ]: filteredColumns = self.searchMenu.columns(modelName) # Searching if len(filteredColumns): results += model.searchOccurrences(searchRegex, filteredColumns) # Showing results self.generateResultsLists(results) # Remove override cursor qApp.restoreOverrideCursor() else: # No results to generate if there is a problem with the regex self.generateResultsLists(list()) def generateResultsLists(self, results): self.noResultsLabel.setVisible(len(results) == 0) for result in results: item = QListWidgetItem(result.title(), self.result) item.setData(Qt.UserRole, result) item.setData(Qt.UserRole + 1, ' > '.join(result.path())) item.setData(Qt.UserRole + 2, result.context()) self.result.addItem(item) def openItem(self, item): self.searchResultHighlighter.highlightSearchResult(item.data(Qt.UserRole)) def leaveEvent(self, event): self.delegate.mouseLeave() class listResultDelegate(QStyledItemDelegate): def __init__(self, parent=None): QStyledItemDelegate.__init__(self, parent) self._tooltipRowIndex = -1 def paint(self, painter, option, index): extra = index.data(Qt.UserRole + 1) if not extra: return QStyledItemDelegate.paint(self, painter, option, index) else: if option.state & QStyle.State_Selected: painter.fillRect(option.rect, option.palette.color(QPalette.Highlight)) title = index.data() painter.drawText(option.rect.adjusted(2, 1, 0, 0), Qt.AlignLeft, title) fm = QFontMetrics(option.font) w = fm.width(title) r = QRect(option.rect) r.setLeft(r.left() + w) painter.save() if option.state & QStyle.State_Selected: painter.setPen(Qt.white) else: painter.setPen(Qt.gray) painter.drawText(r.adjusted(2, 1, 0, 0), Qt.AlignLeft, " - {}".format(extra)) painter.restore() def editorEvent(self, event, model, option, index): if event.type() == QEvent.MouseMove and self._tooltipRowIndex != index.row(): self._tooltipRowIndex = index.row() context = index.data(Qt.UserRole + 2) extra = index.data(Qt.UserRole + 1) QToolTip.showText(event.globalPos(), "

#" + str(index.row()) + " - " + extra + "

" + context + "

") return True return False def mouseLeave(self): self._tooltipRowIndex = -1