diff -r 7c973954919d -r 6e1650b9b3b5 src/eric7/PdfViewer/PdfSearchWidget.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/eric7/PdfViewer/PdfSearchWidget.py Wed Jan 18 14:31:55 2023 +0100 @@ -0,0 +1,359 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2023 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a Search widget. +""" + +from PyQt6.QtCore import Qt, pyqtSlot, QModelIndex, pyqtSignal +from PyQt6.QtPdf import QPdfSearchModel, QPdfDocument, QPdfLink +from PyQt6.QtWidgets import ( + QWidget, QVBoxLayout, QLabel, QLineEdit, QHBoxLayout, QToolButton, + QAbstractItemView, QTreeWidget, QTreeWidgetItem +) + +from eric7 import Preferences +from eric7.EricGui import EricPixmapCache + + +class PdfSearchResultsWidget(QTreeWidget): + """ + Class implementing a widget to show the search results. + + @signal rowCountChanged() emitted to indicate a change of the number + of items + """ + + rowCountChanged = pyqtSignal() + + def __init__(self, parent=None): + """ + Constructor + + @param parent reference to the parent widget (defaults to None) + @type QWidget (optional) + """ + super().__init__(parent) + + self.setColumnCount(2) + self.setHeaderHidden(True) + self.setAlternatingRowColors(True) + self.setSortingEnabled(False) + self.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection) + self.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) + + self.__searchModel = QPdfSearchModel(self) + self.__searchModel.modelReset.connect(self.__clear) + self.__searchModel.rowsInserted.connect(self.__rowsInserted) + + def setSearchString(self, searchString): + """ + Public method to set the search string. + + @param searchString search string + @type str + """ + self.__searchModel.setSearchString(searchString) + + def searchString(self): + """ + Public method to get the current search string. + + @return search string + @rtype str + """ + return self.__searchModel.searchString() + + def setDocument(self, document): + """ + Public method to set the PDF document object to be searched. + + @param document reference to the PDF document object + @type QPdfDocument + """ + self.__searchModel.setDocument(document) + + def document(self): + """ + Public method to get the reference to the PDF document object. + + @return reference to the PDF document object + @rtype QPdfDocument + """ + return self.__searchModel.document() + + @pyqtSlot() + def __clear(self): + """ + Private slot to clear the list of search results. + """ + self.clear() + self.rowCountChanged.emit() + + @pyqtSlot(QModelIndex, int, int) + def __rowsInserted(self, parent, first, last): + """ + Private slot to handle the insertion of rows of the search model. + + @param parent reference to the parent index + @type QModelIndex + @param first first row inserted + @type int + @param last last row inserted + @type int + """ + contextLength = Preferences.getPdfViewer("PdfSearchContextLength") + + for row in range(first, last + 1): + index = self.__searchModel.index(row, 0) + itm = QTreeWidgetItem( + self, + [ + self.tr("Page {0}").format( + self.__searchModel.document().pageLabel( + self.__searchModel.data( + index, QPdfSearchModel.Role.Page.value + ) + ) + ), + "", + ] + ) + contextBefore = self.__searchModel.data( + index, QPdfSearchModel.Role.ContextBefore.value + ) + if len(contextBefore) > contextLength: + contextBefore = "... {0}".format(contextBefore[-contextLength:]) + contextAfter = self.__searchModel.data( + index, QPdfSearchModel.Role.ContextAfter.value + ) + if len(contextAfter) > contextLength: + contextAfter = "{0} ...".format(contextAfter[:contextLength]) + resultLabel = QLabel( + self.tr( + "{0}<b>{1}</b>{2}", + "context before, search string, context after" + ).format(contextBefore, self.searchString(), contextAfter) + ) + self.setItemWidget(itm, 1, resultLabel) + + for column in range(self.columnCount()): + self.resizeColumnToContents(column) + + self.rowCountChanged.emit() + + def rowCount(self): + """ + Public method to get the number of rows. + + @return number of rows + @rtype int + """ + return self.topLevelItemCount() + + def currentRow(self): + """ + Public method to get the current row. + + @return current row + @rtype int + """ + curItem = self.currentItem() + if curItem is None: + return -1 + else: + return self.indexOfTopLevelItem(curItem) + + def setCurrentRow(self, row): + """ + Public method to set the current row. + + @param row row number to make the current row + @type int + """ + if 0 <= row < self.topLevelItemCount(): + self.setCurrentItem(self.topLevelItem(row)) + + def searchResultData(self, item, role): + """ + Public method to get data of a search result item. + + @param item reference to the search result item + @type QTreeWidgetItem + @param role item data role + @type QPdfSearchModel.Role or Qt.ItemDataRole + @return requested data + @rtype Any + """ + row = self.indexOfTopLevelItem(item) + index = self.__searchModel.index(row, 0) + return self.__searchModel.data(index, role) + + def getPdfLink(self, item): + """ + Public method to get the PDF link associated with a search result item. + + @param item reference to the search result item + @type QTreeWidgetItem + @return associated PDF link + @rtype QPdfLink + """ + row = self.indexOfTopLevelItem(item) + return self.__searchModel.resultAtIndex(row) + + +class PdfSearchWidget(QWidget): + """ + Class implementing a Search widget. + """ + + searchResultActivated = pyqtSignal(QPdfLink) + + def __init__(self, document, parent=None): + """ + Constructor + + @param document reference to the PDF document object + @type QPdfDocument + @param parent reference to the parent widget (defaults to None) + @type QWidget (optional) + """ + super().__init__(parent) + + self.__layout = QVBoxLayout(self) + + # Line 1: a header label + self.__header = QLabel("<h2>{0}</h2>".format(self.tr("Search"))) + self.__header.setAlignment(Qt.AlignmentFlag.AlignCenter) + self.__layout.addWidget(self.__header) + + # Line 2: search entry and navigation buttons + self.__searchLineLayout = QHBoxLayout() + + self.__searchEdit = QLineEdit(self) + self.__searchEdit.setPlaceholderText(self.tr("Search ...")) + self.__searchEdit.setClearButtonEnabled(True) + self.__searchLineLayout.addWidget(self.__searchEdit) + + # layout for the navigation buttons + self.__buttonsLayout = QHBoxLayout() + self.__buttonsLayout.setSpacing(0) + + self.__findPrevButton = QToolButton(self) + self.__findPrevButton.setToolTip( + self.tr("Press to move to the previous occurrence") + ) + self.__findPrevButton.setIcon(EricPixmapCache.getIcon("1leftarrow")) + self.__buttonsLayout.addWidget(self.__findPrevButton) + + self.__findNextButton = QToolButton(self) + self.__findNextButton.setToolTip(self.tr("Press to move to the next occurrence")) + self.__findNextButton.setIcon(EricPixmapCache.getIcon("1rightarrow")) + self.__buttonsLayout.addWidget(self.__findNextButton) + + self.__searchLineLayout.addLayout(self.__buttonsLayout) + self.__layout.addLayout(self.__searchLineLayout) + + self.__resultsWidget = PdfSearchResultsWidget(self) + self.__resultsWidget.setDocument(document) + self.__layout.addWidget(self.__resultsWidget) + + self.setLayout(self.__layout) + + self.__searchEdit.setEnabled(False) + self.__resultsWidget.setEnabled(False) + self.__findPrevButton.setEnabled(False) + self.__findNextButton.setEnabled(False) + + self.__resultsWidget.itemActivated.connect(self.__entrySelected) + document.statusChanged.connect(self.__handleDocumentStatus) + self.__searchEdit.returnPressed.connect(self.__search) + self.__searchEdit.textChanged.connect(self.__searchTextChanged) + self.__resultsWidget.rowCountChanged.connect(self.__updateButtons) + self.__resultsWidget.currentItemChanged.connect( + self.__updateButtons + ) + self.__findNextButton.clicked.connect(self.__nextResult) + self.__findPrevButton.clicked.connect(self.__previousResult) + + @pyqtSlot(QPdfDocument.Status) + def __handleDocumentStatus(self, status): + """ + Private slot to handle a change of the document status. + + @param status document status + @type QPdfDocument.Status + """ + ready = status == QPdfDocument.Status.Ready + + self.__searchEdit.setEnabled(ready) + self.__resultsWidget.setEnabled(ready) + + if not ready: + self.__searchEdit.clear() + + @pyqtSlot(str) + def __searchTextChanged(self, text): + """ + Private slot to handle a change of the search string. + + @param text search string + @type str + """ + if not text: + self.__resultsWidget.setSearchString("") + + @pyqtSlot() + def __search(self): + """ + Private slot to initiate a new search. + """ + searchString = self.__searchEdit.text() + self.__resultsWidget.setSearchString(searchString) + + @pyqtSlot() + def __updateButtons(self): + """ + Private slot to update the state of the navigation buttons. + """ + hasSearchResults = bool(self.__resultsWidget.rowCount()) + currentRow = self.__resultsWidget.currentRow() + self.__findPrevButton.setEnabled(hasSearchResults and currentRow > 0) + self.__findNextButton.setEnabled( + hasSearchResults and currentRow < self.__resultsWidget.rowCount() - 2 + ) + + @pyqtSlot(QTreeWidgetItem) + def __entrySelected(self, item): + """ + Private slot to handle the selection of a search result entry. + + @param index index of the activated entry + @type QModelIndex + """ + link = self.__resultsWidget.getPdfLink(item) + self.searchResultActivated.emit(link) + + @pyqtSlot() + def __nextResult(self): + """ + Private slot to activate the next result. + """ + row = self.__resultsWidget.currentRow() + if row < self.__resultsWidget.rowCount() - 2: + nextItem = self.__resultsWidget.topLevelItem(row + 1) + self.__resultsWidget.setCurrentItem(nextItem) + self.__entrySelected(nextItem) + + @pyqtSlot() + def __previousResult(self): + """ + Private slot to activate the previous result. + """ + row = self.__resultsWidget.currentRow() + if row > 0: + prevItem = self.__resultsWidget.topLevelItem(row - 1) + self.__resultsWidget.setCurrentItem(prevItem) + self.__entrySelected(prevItem)