src/eric7/EricWidgets/EricTextEditSearchWidget.py

branch
eric7
changeset 9209
b99e7fd55fd3
parent 8881
54e42bc2437a
child 9221
bf71ee032bb4
equal deleted inserted replaced
9208:3fc8dfeb6ebe 9209:b99e7fd55fd3
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2012 - 2022 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing a horizontal search widget for QTextEdit.
8 """
9
10 import enum
11
12 from PyQt6.QtCore import pyqtSlot, pyqtSignal, Qt, QMetaObject, QSize
13 from PyQt6.QtGui import QPalette, QTextDocument, QTextCursor
14 from PyQt6.QtWidgets import (
15 QWidget, QVBoxLayout, QHBoxLayout, QLabel, QComboBox, QCheckBox,
16 QToolButton, QSizePolicy
17 )
18
19 import UI.PixmapCache
20
21
22 class EricTextEditType(enum.Enum):
23 """
24 Class defining the supported text edit types.
25 """
26 UNKNOWN = 0
27 QTEXTEDIT = 1
28 QTEXTBROWSER = 2
29 QWEBENGINEVIEW = 3
30
31
32 class EricTextEditSearchWidget(QWidget):
33 """
34 Class implementing a horizontal search widget for QTextEdit.
35
36 @signal closePressed() emitted to indicate the closing of the widget via
37 the close button
38 """
39 closePressed = pyqtSignal()
40
41 def __init__(self, parent=None, widthForHeight=True, enableClose=False):
42 """
43 Constructor
44
45 @param parent reference to the parent widget
46 @type QWidget
47 @param widthForHeight flag indicating to prefer width for height.
48 If this parameter is False, some widgets are shown in a third
49 line.
50 @type bool
51 @param enableClose flag indicating to show a close button
52 @type bool
53 """
54 super().__init__(parent)
55 self.__setupUi(widthForHeight, enableClose)
56
57 self.__textedit = None
58 self.__texteditType = EricTextEditType.UNKNOWN
59 self.__findBackwards = False
60
61 self.__defaultBaseColor = (
62 self.findtextCombo.lineEdit().palette().color(
63 QPalette.ColorRole.Base)
64 )
65 self.__defaultTextColor = (
66 self.findtextCombo.lineEdit().palette().color(
67 QPalette.ColorRole.Text)
68 )
69
70 self.findHistory = []
71
72 self.findtextCombo.setCompleter(None)
73 self.findtextCombo.lineEdit().returnPressed.connect(
74 self.__findByReturnPressed)
75
76 self.__setSearchButtons(False)
77 self.infoLabel.hide()
78
79 self.setFocusProxy(self.findtextCombo)
80
81 def __setupUi(self, widthForHeight, enableClose):
82 """
83 Private method to generate the UI.
84
85 @param widthForHeight flag indicating to prefer width for height
86 @type bool
87 @param enableClose flag indicating to show a close button
88 @type bool
89 """
90 self.setObjectName("EricTextEditSearchWidget")
91
92 self.verticalLayout = QVBoxLayout(self)
93 self.verticalLayout.setObjectName("verticalLayout")
94 self.verticalLayout.setContentsMargins(0, 0, 0, 0)
95
96 # row 1 of widgets
97 self.horizontalLayout1 = QHBoxLayout()
98 self.horizontalLayout1.setObjectName("horizontalLayout1")
99
100 if enableClose:
101 self.closeButton = QToolButton(self)
102 self.closeButton.setIcon(UI.PixmapCache.getIcon("close"))
103 self.closeButton.clicked.connect(self.__closeButtonClicked)
104 self.horizontalLayout1.addWidget(self.closeButton)
105 else:
106 self.closeButton = None
107
108 self.label = QLabel(self)
109 self.label.setObjectName("label")
110 self.label.setText(self.tr("Find:"))
111 self.horizontalLayout1.addWidget(self.label)
112
113 self.findtextCombo = QComboBox(self)
114 self.findtextCombo.setEditable(True)
115 self.findtextCombo.lineEdit().setClearButtonEnabled(True)
116 sizePolicy = QSizePolicy(QSizePolicy.Policy.Expanding,
117 QSizePolicy.Policy.Fixed)
118 sizePolicy.setHorizontalStretch(0)
119 sizePolicy.setVerticalStretch(0)
120 sizePolicy.setHeightForWidth(
121 self.findtextCombo.sizePolicy().hasHeightForWidth())
122 self.findtextCombo.setSizePolicy(sizePolicy)
123 self.findtextCombo.setMinimumSize(QSize(100, 0))
124 self.findtextCombo.setEditable(True)
125 self.findtextCombo.setInsertPolicy(QComboBox.InsertPolicy.InsertAtTop)
126 self.findtextCombo.setDuplicatesEnabled(False)
127 self.findtextCombo.setObjectName("findtextCombo")
128 self.horizontalLayout1.addWidget(self.findtextCombo)
129
130 # row 2 (maybe) of widgets
131 self.horizontalLayout2 = QHBoxLayout()
132 self.horizontalLayout2.setObjectName("horizontalLayout2")
133
134 self.caseCheckBox = QCheckBox(self)
135 self.caseCheckBox.setObjectName("caseCheckBox")
136 self.caseCheckBox.setText(self.tr("Match case"))
137 self.horizontalLayout2.addWidget(self.caseCheckBox)
138
139 self.wordCheckBox = QCheckBox(self)
140 self.wordCheckBox.setObjectName("wordCheckBox")
141 self.wordCheckBox.setText(self.tr("Whole word"))
142 self.horizontalLayout2.addWidget(self.wordCheckBox)
143
144 # layout for the navigation buttons
145 self.horizontalLayout3 = QHBoxLayout()
146 self.horizontalLayout3.setSpacing(0)
147 self.horizontalLayout3.setObjectName("horizontalLayout3")
148
149 self.findPrevButton = QToolButton(self)
150 self.findPrevButton.setObjectName("findPrevButton")
151 self.findPrevButton.setToolTip(self.tr(
152 "Press to find the previous occurrence"))
153 self.findPrevButton.setIcon(UI.PixmapCache.getIcon("1leftarrow"))
154 self.horizontalLayout3.addWidget(self.findPrevButton)
155
156 self.findNextButton = QToolButton(self)
157 self.findNextButton.setObjectName("findNextButton")
158 self.findNextButton.setToolTip(self.tr(
159 "Press to find the next occurrence"))
160 self.findNextButton.setIcon(UI.PixmapCache.getIcon("1rightarrow"))
161 self.horizontalLayout3.addWidget(self.findNextButton)
162
163 self.horizontalLayout2.addLayout(self.horizontalLayout3)
164
165 # info label (in row 2 or 3)
166 self.infoLabel = QLabel(self)
167 self.infoLabel.setText("")
168 self.infoLabel.setObjectName("infoLabel")
169
170 # place everything together
171 self.verticalLayout.addLayout(self.horizontalLayout1)
172 self.__addWidthForHeightLayout(widthForHeight)
173 self.verticalLayout.addWidget(self.infoLabel)
174
175 QMetaObject.connectSlotsByName(self)
176
177 self.setTabOrder(self.findtextCombo, self.caseCheckBox)
178 self.setTabOrder(self.caseCheckBox, self.wordCheckBox)
179 self.setTabOrder(self.wordCheckBox, self.findPrevButton)
180 self.setTabOrder(self.findPrevButton, self.findNextButton)
181
182 def setWidthForHeight(self, widthForHeight):
183 """
184 Public method to set the 'width for height'.
185
186 @param widthForHeight flag indicating to prefer width
187 @type bool
188 """
189 if self.__widthForHeight:
190 self.horizontalLayout1.takeAt(self.__widthForHeightLayoutIndex)
191 else:
192 self.verticalLayout.takeAt(self.__widthForHeightLayoutIndex)
193 self.__addWidthForHeightLayout(widthForHeight)
194
195 def __addWidthForHeightLayout(self, widthForHeight):
196 """
197 Private method to set the middle part of the layout.
198
199 @param widthForHeight flag indicating to prefer width
200 @type bool
201 """
202 if widthForHeight:
203 self.horizontalLayout1.addLayout(self.horizontalLayout2)
204 self.__widthForHeightLayoutIndex = 2
205 else:
206 self.verticalLayout.insertLayout(1, self.horizontalLayout2)
207 self.__widthForHeightLayoutIndex = 1
208
209 self.__widthForHeight = widthForHeight
210
211 def attachTextEdit(self, textedit, editType=EricTextEditType.QTEXTEDIT):
212 """
213 Public method to attach a QTextEdit or QWebEngineView widget.
214
215 @param textedit reference to the edit widget to be attached
216 @type QTextEdit, QTextBrowser or QWebEngineView
217 @param editType type of the attached edit widget
218 @type EricTextEditType
219 """
220 if self.__textedit is not None:
221 self.detachTextEdit()
222
223 self.__textedit = textedit
224 self.__texteditType = editType
225
226 self.wordCheckBox.setVisible(editType in (
227 EricTextEditType.QTEXTEDIT, EricTextEditType.QTEXTBROWSER
228 ))
229 self.infoLabel.setVisible(editType == EricTextEditType.QWEBENGINEVIEW)
230 if editType == EricTextEditType.QWEBENGINEVIEW:
231 self.__textedit.page().findTextFinished.connect(
232 self.__findTextFinished)
233
234 def detachTextEdit(self):
235 """
236 Public method to detach the current text edit.
237 """
238 if self.__texteditType == EricTextEditType.QWEBENGINEVIEW:
239 self.__textedit.page().findTextFinished.disconnect(
240 self.__findTextFinished)
241
242 self.__textedit = None
243 self.__texteditType = EricTextEditType.UNKNOWN
244
245 @pyqtSlot()
246 def activate(self):
247 """
248 Public slot to activate the widget.
249 """
250 self.show()
251 self.findtextCombo.setFocus(
252 Qt.FocusReason.ActiveWindowFocusReason)
253 self.findtextCombo.lineEdit().selectAll()
254
255 @pyqtSlot()
256 def deactivate(self):
257 """
258 Public slot to deactivate the widget.
259 """
260 if self.__textedit:
261 self.__textedit.setFocus(Qt.FocusReason.ActiveWindowFocusReason)
262 if self.__texteditType == EricTextEditType.QWEBENGINEVIEW:
263 self.__textedit.findText("")
264 if self.closeButton is not None:
265 self.hide()
266 self.closePressed.emit()
267
268 @pyqtSlot()
269 def __closeButtonClicked(self):
270 """
271 Private slot to close the widget.
272
273 Note: The widget is just hidden.
274 """
275 self.deactivate()
276
277 def keyPressEvent(self, event):
278 """
279 Protected slot to handle key press events.
280
281 @param event reference to the key press event
282 @type QKeyEvent
283 """
284 if self.__textedit:
285 key = event.key()
286 modifiers = event.modifiers()
287
288 if key == Qt.Key.Key_Escape:
289 self.deactivate()
290 event.accept()
291
292 elif key == Qt.Key.Key_F3:
293 if modifiers == Qt.KeyboardModifier.NoModifier:
294 # search forward
295 self.on_findNextButton_clicked()
296 event.accept()
297 elif modifiers == Qt.KeyboardModifier.ShiftModifier:
298 # search backward
299 self.on_findPrevButton_clicked()
300 event.accept()
301
302 @pyqtSlot(str)
303 def on_findtextCombo_editTextChanged(self, txt):
304 """
305 Private slot to enable/disable the find buttons.
306
307 @param txt text of the combobox
308 @type str
309 """
310 self.__setSearchButtons(txt != "")
311
312 if self.__texteditType == EricTextEditType.QWEBENGINEVIEW:
313 self.infoLabel.clear()
314 else:
315 self.infoLabel.hide()
316 self.__setFindtextComboBackground(False)
317
318 def __setSearchButtons(self, enabled):
319 """
320 Private slot to set the state of the search buttons.
321
322 @param enabled flag indicating the state
323 @type bool
324 """
325 self.findPrevButton.setEnabled(enabled)
326 self.findNextButton.setEnabled(enabled)
327
328 def __findByReturnPressed(self):
329 """
330 Private slot to handle the returnPressed signal of the findtext
331 combobox.
332 """
333 self.__find(self.__findBackwards)
334
335 @pyqtSlot()
336 def on_findPrevButton_clicked(self):
337 """
338 Private slot to find the previous occurrence.
339 """
340 self.__find(True)
341
342 @pyqtSlot()
343 def on_findNextButton_clicked(self):
344 """
345 Private slot to find the next occurrence.
346 """
347 self.__find(False)
348
349 @pyqtSlot()
350 def findPrev(self):
351 """
352 Public slot to find the previous occurrence of the current search term.
353 """
354 self.on_findPrevButton_clicked()
355
356 @pyqtSlot()
357 def findNext(self):
358 """
359 Public slot to find the next occurrence of the current search term.
360 """
361 self.on_findNextButton_clicked()
362
363 def __find(self, backwards):
364 """
365 Private method to search the associated text edit.
366
367 @param backwards flag indicating a backwards search
368 @type bool
369 """
370 if not self.__textedit:
371 return
372
373 self.infoLabel.clear()
374 if self.__texteditType != EricTextEditType.QWEBENGINEVIEW:
375 self.infoLabel.hide()
376 self.__setFindtextComboBackground(False)
377
378 txt = self.findtextCombo.currentText()
379 if not txt:
380 return
381 self.__findBackwards = backwards
382
383 # This moves any previous occurrence of this statement to the head
384 # of the list and updates the combobox
385 if txt in self.findHistory:
386 self.findHistory.remove(txt)
387 self.findHistory.insert(0, txt)
388 self.findtextCombo.clear()
389 self.findtextCombo.addItems(self.findHistory)
390
391 if self.__texteditType in (
392 EricTextEditType.QTEXTBROWSER, EricTextEditType.QTEXTEDIT
393 ):
394 self.__findPrevNextQTextEdit(backwards)
395 elif self.__texteditType == EricTextEditType.QWEBENGINEVIEW:
396 self.__findPrevNextQWebEngineView(backwards)
397
398 def __findPrevNextQTextEdit(self, backwards):
399 """
400 Private method to to search the associated edit widget of
401 type QTextEdit.
402
403 @param backwards flag indicating a backwards search
404 @type bool
405 """
406 flags = (
407 QTextDocument.FindFlag.FindBackward
408 if backwards else
409 QTextDocument.FindFlag(0)
410 )
411 if self.caseCheckBox.isChecked():
412 flags |= QTextDocument.FindFlag.FindCaseSensitively
413 if self.wordCheckBox.isChecked():
414 flags |= QTextDocument.FindFlag.FindWholeWords
415
416 ok = self.__textedit.find(self.findtextCombo.currentText(), flags)
417 if not ok:
418 # wrap around once
419 cursor = self.__textedit.textCursor()
420 if backwards:
421 moveOp = QTextCursor.MoveOperation.End
422 # move to end of document
423 else:
424 moveOp = QTextCursor.MoveOperation.Start
425 # move to start of document
426 cursor.movePosition(moveOp)
427 self.__textedit.setTextCursor(cursor)
428 ok = self.__textedit.find(self.findtextCombo.currentText(), flags)
429
430 if not ok:
431 self.infoLabel.setText(
432 self.tr("'{0}' was not found.").format(
433 self.findtextCombo.currentText())
434 )
435 self.infoLabel.show()
436 self.__setFindtextComboBackground(True)
437
438 def __findPrevNextQWebEngineView(self, backwards):
439 """
440 Private method to to search the associated edit widget of
441 type QWebEngineView.
442
443 @param backwards flag indicating a backwards search
444 @type bool
445 """
446 from PyQt6.QtWebEngineCore import QWebEnginePage
447
448 findFlags = QWebEnginePage.FindFlag(0)
449 if self.caseCheckBox.isChecked():
450 findFlags |= QWebEnginePage.FindFlag.FindCaseSensitively
451 if backwards:
452 findFlags |= QWebEnginePage.FindFlag.FindBackward
453 self.__textedit.findText(self.findtextCombo.currentText(), findFlags)
454
455 def __setFindtextComboBackground(self, error):
456 """
457 Private slot to change the findtext combo background to indicate
458 errors.
459
460 @param error flag indicating an error condition
461 @type bool
462 """
463 styleSheet = (
464 "color: #000000; background-color: #ff6666"
465 if error else
466 f"color: {self.__defaultTextColor};"
467 f" background-color: {self.__defaultBaseColor}"
468 )
469 self.findtextCombo.setStyleSheet(styleSheet)
470
471 def __findTextFinished(self, result):
472 """
473 Private slot handling the findTextFinished signal of the web page.
474
475 @param result reference to the QWebEngineFindTextResult object of the
476 last search
477 @type QWebEngineFindTextResult
478 """
479 if result.numberOfMatches() == 0:
480 self.infoLabel.setText(
481 self.tr("'{0}' was not found.").format(
482 self.findtextCombo.currentText())
483 )
484 self.__setFindtextComboBackground(True)
485 else:
486 self.infoLabel.setText(self.tr("Match {0} of {1}").format(
487 result.activeMatch(), result.numberOfMatches())
488 )
489
490 def showInfo(self, info):
491 """
492 Public method to show some information in the info label.
493
494 @param info informational text to be shown
495 @type str
496 """
497 self.infoLabel.setText(info)
498 self.infoLabel.show()

eric ide

mercurial