|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2023 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing a Search widget. |
|
8 """ |
|
9 |
|
10 from PyQt6.QtCore import Qt, pyqtSlot, QModelIndex, pyqtSignal |
|
11 from PyQt6.QtPdf import QPdfSearchModel, QPdfDocument, QPdfLink |
|
12 from PyQt6.QtWidgets import ( |
|
13 QWidget, QVBoxLayout, QLabel, QLineEdit, QHBoxLayout, QToolButton, |
|
14 QAbstractItemView, QTreeWidget, QTreeWidgetItem |
|
15 ) |
|
16 |
|
17 from eric7 import Preferences |
|
18 from eric7.EricGui import EricPixmapCache |
|
19 |
|
20 |
|
21 class PdfSearchResultsWidget(QTreeWidget): |
|
22 """ |
|
23 Class implementing a widget to show the search results. |
|
24 |
|
25 @signal rowCountChanged() emitted to indicate a change of the number |
|
26 of items |
|
27 """ |
|
28 |
|
29 rowCountChanged = pyqtSignal() |
|
30 |
|
31 def __init__(self, parent=None): |
|
32 """ |
|
33 Constructor |
|
34 |
|
35 @param parent reference to the parent widget (defaults to None) |
|
36 @type QWidget (optional) |
|
37 """ |
|
38 super().__init__(parent) |
|
39 |
|
40 self.setColumnCount(2) |
|
41 self.setHeaderHidden(True) |
|
42 self.setAlternatingRowColors(True) |
|
43 self.setSortingEnabled(False) |
|
44 self.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection) |
|
45 self.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) |
|
46 |
|
47 self.__searchModel = QPdfSearchModel(self) |
|
48 self.__searchModel.modelReset.connect(self.__clear) |
|
49 self.__searchModel.rowsInserted.connect(self.__rowsInserted) |
|
50 |
|
51 def setSearchString(self, searchString): |
|
52 """ |
|
53 Public method to set the search string. |
|
54 |
|
55 @param searchString search string |
|
56 @type str |
|
57 """ |
|
58 self.__searchModel.setSearchString(searchString) |
|
59 |
|
60 def searchString(self): |
|
61 """ |
|
62 Public method to get the current search string. |
|
63 |
|
64 @return search string |
|
65 @rtype str |
|
66 """ |
|
67 return self.__searchModel.searchString() |
|
68 |
|
69 def setDocument(self, document): |
|
70 """ |
|
71 Public method to set the PDF document object to be searched. |
|
72 |
|
73 @param document reference to the PDF document object |
|
74 @type QPdfDocument |
|
75 """ |
|
76 self.__searchModel.setDocument(document) |
|
77 |
|
78 def document(self): |
|
79 """ |
|
80 Public method to get the reference to the PDF document object. |
|
81 |
|
82 @return reference to the PDF document object |
|
83 @rtype QPdfDocument |
|
84 """ |
|
85 return self.__searchModel.document() |
|
86 |
|
87 @pyqtSlot() |
|
88 def __clear(self): |
|
89 """ |
|
90 Private slot to clear the list of search results. |
|
91 """ |
|
92 self.clear() |
|
93 self.rowCountChanged.emit() |
|
94 |
|
95 @pyqtSlot(QModelIndex, int, int) |
|
96 def __rowsInserted(self, parent, first, last): |
|
97 """ |
|
98 Private slot to handle the insertion of rows of the search model. |
|
99 |
|
100 @param parent reference to the parent index |
|
101 @type QModelIndex |
|
102 @param first first row inserted |
|
103 @type int |
|
104 @param last last row inserted |
|
105 @type int |
|
106 """ |
|
107 contextLength = Preferences.getPdfViewer("PdfSearchContextLength") |
|
108 |
|
109 for row in range(first, last + 1): |
|
110 index = self.__searchModel.index(row, 0) |
|
111 itm = QTreeWidgetItem( |
|
112 self, |
|
113 [ |
|
114 self.tr("Page {0}").format( |
|
115 self.__searchModel.document().pageLabel( |
|
116 self.__searchModel.data( |
|
117 index, QPdfSearchModel.Role.Page.value |
|
118 ) |
|
119 ) |
|
120 ), |
|
121 "", |
|
122 ] |
|
123 ) |
|
124 contextBefore = self.__searchModel.data( |
|
125 index, QPdfSearchModel.Role.ContextBefore.value |
|
126 ) |
|
127 if len(contextBefore) > contextLength: |
|
128 contextBefore = "... {0}".format(contextBefore[-contextLength:]) |
|
129 contextAfter = self.__searchModel.data( |
|
130 index, QPdfSearchModel.Role.ContextAfter.value |
|
131 ) |
|
132 if len(contextAfter) > contextLength: |
|
133 contextAfter = "{0} ...".format(contextAfter[:contextLength]) |
|
134 resultLabel = QLabel( |
|
135 self.tr( |
|
136 "{0}<b>{1}</b>{2}", |
|
137 "context before, search string, context after" |
|
138 ).format(contextBefore, self.searchString(), contextAfter) |
|
139 ) |
|
140 self.setItemWidget(itm, 1, resultLabel) |
|
141 |
|
142 for column in range(self.columnCount()): |
|
143 self.resizeColumnToContents(column) |
|
144 |
|
145 self.rowCountChanged.emit() |
|
146 |
|
147 def rowCount(self): |
|
148 """ |
|
149 Public method to get the number of rows. |
|
150 |
|
151 @return number of rows |
|
152 @rtype int |
|
153 """ |
|
154 return self.topLevelItemCount() |
|
155 |
|
156 def currentRow(self): |
|
157 """ |
|
158 Public method to get the current row. |
|
159 |
|
160 @return current row |
|
161 @rtype int |
|
162 """ |
|
163 curItem = self.currentItem() |
|
164 if curItem is None: |
|
165 return -1 |
|
166 else: |
|
167 return self.indexOfTopLevelItem(curItem) |
|
168 |
|
169 def setCurrentRow(self, row): |
|
170 """ |
|
171 Public method to set the current row. |
|
172 |
|
173 @param row row number to make the current row |
|
174 @type int |
|
175 """ |
|
176 if 0 <= row < self.topLevelItemCount(): |
|
177 self.setCurrentItem(self.topLevelItem(row)) |
|
178 |
|
179 def searchResultData(self, item, role): |
|
180 """ |
|
181 Public method to get data of a search result item. |
|
182 |
|
183 @param item reference to the search result item |
|
184 @type QTreeWidgetItem |
|
185 @param role item data role |
|
186 @type QPdfSearchModel.Role or Qt.ItemDataRole |
|
187 @return requested data |
|
188 @rtype Any |
|
189 """ |
|
190 row = self.indexOfTopLevelItem(item) |
|
191 index = self.__searchModel.index(row, 0) |
|
192 return self.__searchModel.data(index, role) |
|
193 |
|
194 def getPdfLink(self, item): |
|
195 """ |
|
196 Public method to get the PDF link associated with a search result item. |
|
197 |
|
198 @param item reference to the search result item |
|
199 @type QTreeWidgetItem |
|
200 @return associated PDF link |
|
201 @rtype QPdfLink |
|
202 """ |
|
203 row = self.indexOfTopLevelItem(item) |
|
204 return self.__searchModel.resultAtIndex(row) |
|
205 |
|
206 |
|
207 class PdfSearchWidget(QWidget): |
|
208 """ |
|
209 Class implementing a Search widget. |
|
210 """ |
|
211 |
|
212 searchResultActivated = pyqtSignal(QPdfLink) |
|
213 |
|
214 def __init__(self, document, parent=None): |
|
215 """ |
|
216 Constructor |
|
217 |
|
218 @param document reference to the PDF document object |
|
219 @type QPdfDocument |
|
220 @param parent reference to the parent widget (defaults to None) |
|
221 @type QWidget (optional) |
|
222 """ |
|
223 super().__init__(parent) |
|
224 |
|
225 self.__layout = QVBoxLayout(self) |
|
226 |
|
227 # Line 1: a header label |
|
228 self.__header = QLabel("<h2>{0}</h2>".format(self.tr("Search"))) |
|
229 self.__header.setAlignment(Qt.AlignmentFlag.AlignCenter) |
|
230 self.__layout.addWidget(self.__header) |
|
231 |
|
232 # Line 2: search entry and navigation buttons |
|
233 self.__searchLineLayout = QHBoxLayout() |
|
234 |
|
235 self.__searchEdit = QLineEdit(self) |
|
236 self.__searchEdit.setPlaceholderText(self.tr("Search ...")) |
|
237 self.__searchEdit.setClearButtonEnabled(True) |
|
238 self.__searchLineLayout.addWidget(self.__searchEdit) |
|
239 |
|
240 # layout for the navigation buttons |
|
241 self.__buttonsLayout = QHBoxLayout() |
|
242 self.__buttonsLayout.setSpacing(0) |
|
243 |
|
244 self.__findPrevButton = QToolButton(self) |
|
245 self.__findPrevButton.setToolTip( |
|
246 self.tr("Press to move to the previous occurrence") |
|
247 ) |
|
248 self.__findPrevButton.setIcon(EricPixmapCache.getIcon("1leftarrow")) |
|
249 self.__buttonsLayout.addWidget(self.__findPrevButton) |
|
250 |
|
251 self.__findNextButton = QToolButton(self) |
|
252 self.__findNextButton.setToolTip(self.tr("Press to move to the next occurrence")) |
|
253 self.__findNextButton.setIcon(EricPixmapCache.getIcon("1rightarrow")) |
|
254 self.__buttonsLayout.addWidget(self.__findNextButton) |
|
255 |
|
256 self.__searchLineLayout.addLayout(self.__buttonsLayout) |
|
257 self.__layout.addLayout(self.__searchLineLayout) |
|
258 |
|
259 self.__resultsWidget = PdfSearchResultsWidget(self) |
|
260 self.__resultsWidget.setDocument(document) |
|
261 self.__layout.addWidget(self.__resultsWidget) |
|
262 |
|
263 self.setLayout(self.__layout) |
|
264 |
|
265 self.__searchEdit.setEnabled(False) |
|
266 self.__resultsWidget.setEnabled(False) |
|
267 self.__findPrevButton.setEnabled(False) |
|
268 self.__findNextButton.setEnabled(False) |
|
269 |
|
270 self.__resultsWidget.itemActivated.connect(self.__entrySelected) |
|
271 document.statusChanged.connect(self.__handleDocumentStatus) |
|
272 self.__searchEdit.returnPressed.connect(self.__search) |
|
273 self.__searchEdit.textChanged.connect(self.__searchTextChanged) |
|
274 self.__resultsWidget.rowCountChanged.connect(self.__updateButtons) |
|
275 self.__resultsWidget.currentItemChanged.connect( |
|
276 self.__updateButtons |
|
277 ) |
|
278 self.__findNextButton.clicked.connect(self.__nextResult) |
|
279 self.__findPrevButton.clicked.connect(self.__previousResult) |
|
280 |
|
281 @pyqtSlot(QPdfDocument.Status) |
|
282 def __handleDocumentStatus(self, status): |
|
283 """ |
|
284 Private slot to handle a change of the document status. |
|
285 |
|
286 @param status document status |
|
287 @type QPdfDocument.Status |
|
288 """ |
|
289 ready = status == QPdfDocument.Status.Ready |
|
290 |
|
291 self.__searchEdit.setEnabled(ready) |
|
292 self.__resultsWidget.setEnabled(ready) |
|
293 |
|
294 if not ready: |
|
295 self.__searchEdit.clear() |
|
296 |
|
297 @pyqtSlot(str) |
|
298 def __searchTextChanged(self, text): |
|
299 """ |
|
300 Private slot to handle a change of the search string. |
|
301 |
|
302 @param text search string |
|
303 @type str |
|
304 """ |
|
305 if not text: |
|
306 self.__resultsWidget.setSearchString("") |
|
307 |
|
308 @pyqtSlot() |
|
309 def __search(self): |
|
310 """ |
|
311 Private slot to initiate a new search. |
|
312 """ |
|
313 searchString = self.__searchEdit.text() |
|
314 self.__resultsWidget.setSearchString(searchString) |
|
315 |
|
316 @pyqtSlot() |
|
317 def __updateButtons(self): |
|
318 """ |
|
319 Private slot to update the state of the navigation buttons. |
|
320 """ |
|
321 hasSearchResults = bool(self.__resultsWidget.rowCount()) |
|
322 currentRow = self.__resultsWidget.currentRow() |
|
323 self.__findPrevButton.setEnabled(hasSearchResults and currentRow > 0) |
|
324 self.__findNextButton.setEnabled( |
|
325 hasSearchResults and currentRow < self.__resultsWidget.rowCount() - 2 |
|
326 ) |
|
327 |
|
328 @pyqtSlot(QTreeWidgetItem) |
|
329 def __entrySelected(self, item): |
|
330 """ |
|
331 Private slot to handle the selection of a search result entry. |
|
332 |
|
333 @param index index of the activated entry |
|
334 @type QModelIndex |
|
335 """ |
|
336 link = self.__resultsWidget.getPdfLink(item) |
|
337 self.searchResultActivated.emit(link) |
|
338 |
|
339 @pyqtSlot() |
|
340 def __nextResult(self): |
|
341 """ |
|
342 Private slot to activate the next result. |
|
343 """ |
|
344 row = self.__resultsWidget.currentRow() |
|
345 if row < self.__resultsWidget.rowCount() - 2: |
|
346 nextItem = self.__resultsWidget.topLevelItem(row + 1) |
|
347 self.__resultsWidget.setCurrentItem(nextItem) |
|
348 self.__entrySelected(nextItem) |
|
349 |
|
350 @pyqtSlot() |
|
351 def __previousResult(self): |
|
352 """ |
|
353 Private slot to activate the previous result. |
|
354 """ |
|
355 row = self.__resultsWidget.currentRow() |
|
356 if row > 0: |
|
357 prevItem = self.__resultsWidget.topLevelItem(row - 1) |
|
358 self.__resultsWidget.setCurrentItem(prevItem) |
|
359 self.__entrySelected(prevItem) |