src/eric7/PdfViewer/PdfSearchWidget.py

branch
pdf_viewer
changeset 9704
6e1650b9b3b5
child 9706
c0ff0b4d5657
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)

eric ide

mercurial