eric7/E5Gui/E5TextEditSearchWidget.py

branch
eric7
changeset 8356
68ec9c3d4de5
parent 8355
8a7677a63c8d
child 8357
a081458cc57b
equal deleted inserted replaced
8355:8a7677a63c8d 8356:68ec9c3d4de5
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2012 - 2021 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, Qt, QMetaObject, QSize
13 from PyQt6.QtGui import QPalette, QBrush, QColor, 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 E5TextEditType(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 E5TextEditSearchWidget(QWidget):
33 """
34 Class implementing a horizontal search widget for QTextEdit.
35 """
36 def __init__(self, parent=None, widthForHeight=True):
37 """
38 Constructor
39
40 @param parent reference to the parent widget
41 @type QWidget
42 @param widthForHeight flag indicating to prefer width for height.
43 If this parameter is False, some widgets are shown in a third
44 line.
45 @type bool
46 """
47 super().__init__(parent)
48 self.__setupUi(widthForHeight)
49
50 self.__textedit = None
51 self.__texteditType = E5TextEditType.UNKNOWN
52 self.__findBackwards = True
53
54 self.__defaultBaseColor = (
55 self.findtextCombo.lineEdit().palette().color(
56 QPalette.ColorRole.Base)
57 )
58 self.__defaultTextColor = (
59 self.findtextCombo.lineEdit().palette().color(
60 QPalette.ColorRole.Text)
61 )
62
63 self.findHistory = []
64
65 self.findtextCombo.setCompleter(None)
66 self.findtextCombo.lineEdit().returnPressed.connect(
67 self.__findByReturnPressed)
68
69 self.__setSearchButtons(False)
70 self.infoLabel.hide()
71
72 self.setFocusProxy(self.findtextCombo)
73
74 def __setupUi(self, widthForHeight):
75 """
76 Private method to generate the UI.
77
78 @param widthForHeight flag indicating to prefer width for height
79 @type bool
80 """
81 self.setObjectName("E5TextEditSearchWidget")
82
83 self.verticalLayout = QVBoxLayout(self)
84 self.verticalLayout.setObjectName("verticalLayout")
85 self.verticalLayout.setContentsMargins(0, 0, 0, 0)
86
87 # row 1 of widgets
88 self.horizontalLayout1 = QHBoxLayout()
89 self.horizontalLayout1.setObjectName("horizontalLayout1")
90
91 self.label = QLabel(self)
92 self.label.setObjectName("label")
93 self.label.setText(self.tr("Find:"))
94 self.horizontalLayout1.addWidget(self.label)
95
96 self.findtextCombo = QComboBox(self)
97 self.findtextCombo.setEditable(True)
98 self.findtextCombo.lineEdit().setClearButtonEnabled(True)
99 sizePolicy = QSizePolicy(QSizePolicy.Policy.Expanding,
100 QSizePolicy.Policy.Fixed)
101 sizePolicy.setHorizontalStretch(0)
102 sizePolicy.setVerticalStretch(0)
103 sizePolicy.setHeightForWidth(
104 self.findtextCombo.sizePolicy().hasHeightForWidth())
105 self.findtextCombo.setSizePolicy(sizePolicy)
106 self.findtextCombo.setMinimumSize(QSize(100, 0))
107 self.findtextCombo.setEditable(True)
108 self.findtextCombo.setInsertPolicy(QComboBox.InsertPolicy.InsertAtTop)
109 self.findtextCombo.setDuplicatesEnabled(False)
110 self.findtextCombo.setObjectName("findtextCombo")
111 self.horizontalLayout1.addWidget(self.findtextCombo)
112
113 # row 2 (maybe) of widgets
114 self.horizontalLayout2 = QHBoxLayout()
115 self.horizontalLayout2.setObjectName("horizontalLayout2")
116
117 self.caseCheckBox = QCheckBox(self)
118 self.caseCheckBox.setObjectName("caseCheckBox")
119 self.caseCheckBox.setText(self.tr("Match case"))
120 self.horizontalLayout2.addWidget(self.caseCheckBox)
121
122 self.wordCheckBox = QCheckBox(self)
123 self.wordCheckBox.setObjectName("wordCheckBox")
124 self.wordCheckBox.setText(self.tr("Whole word"))
125 self.horizontalLayout2.addWidget(self.wordCheckBox)
126
127 # layout for the navigation buttons
128 self.horizontalLayout3 = QHBoxLayout()
129 self.horizontalLayout3.setSpacing(0)
130 self.horizontalLayout3.setObjectName("horizontalLayout3")
131
132 self.findPrevButton = QToolButton(self)
133 self.findPrevButton.setObjectName("findPrevButton")
134 self.findPrevButton.setToolTip(self.tr(
135 "Press to find the previous occurrence"))
136 self.findPrevButton.setIcon(UI.PixmapCache.getIcon("1leftarrow"))
137 self.horizontalLayout3.addWidget(self.findPrevButton)
138
139 self.findNextButton = QToolButton(self)
140 self.findNextButton.setObjectName("findNextButton")
141 self.findNextButton.setToolTip(self.tr(
142 "Press to find the next occurrence"))
143 self.findNextButton.setIcon(UI.PixmapCache.getIcon("1rightarrow"))
144 self.horizontalLayout3.addWidget(self.findNextButton)
145
146 self.horizontalLayout2.addLayout(self.horizontalLayout3)
147
148 # info label (in row 2 or 3)
149 self.infoLabel = QLabel(self)
150 self.infoLabel.setText("")
151 self.infoLabel.setObjectName("infoLabel")
152
153 # place everything together
154 self.verticalLayout.addLayout(self.horizontalLayout1)
155 self.__addWidthForHeightLayout(widthForHeight)
156 self.verticalLayout.addWidget(self.infoLabel)
157
158 QMetaObject.connectSlotsByName(self)
159
160 self.setTabOrder(self.findtextCombo, self.caseCheckBox)
161 self.setTabOrder(self.caseCheckBox, self.wordCheckBox)
162 self.setTabOrder(self.wordCheckBox, self.findPrevButton)
163 self.setTabOrder(self.findPrevButton, self.findNextButton)
164
165 def setWidthForHeight(self, widthForHeight):
166 """
167 Public method to set the 'width for height'.
168
169 @param widthForHeight flag indicating to prefer width
170 @type bool
171 """
172 if self.__widthForHeight:
173 self.horizontalLayout1.takeAt(self.__widthForHeightLayoutIndex)
174 else:
175 self.verticalLayout.takeAt(self.__widthForHeightLayoutIndex)
176 self.__addWidthForHeightLayout(widthForHeight)
177
178 def __addWidthForHeightLayout(self, widthForHeight):
179 """
180 Private method to set the middle part of the layout.
181
182 @param widthForHeight flag indicating to prefer width
183 @type bool
184 """
185 if widthForHeight:
186 self.horizontalLayout1.addLayout(self.horizontalLayout2)
187 self.__widthForHeightLayoutIndex = 2
188 else:
189 self.verticalLayout.insertLayout(1, self.horizontalLayout2)
190 self.__widthForHeightLayoutIndex = 1
191
192 self.__widthForHeight = widthForHeight
193
194 def attachTextEdit(self, textedit, editType=E5TextEditType.QTEXTEDIT):
195 """
196 Public method to attach a QTextEdit widget.
197
198 @param textedit reference to the edit widget to be attached
199 @type QTextEdit, QWebEngineView or QWebView
200 @param editType type of the attached edit widget
201 @type E5TextEditType
202 """
203 self.__textedit = textedit
204 self.__texteditType = editType
205
206 self.wordCheckBox.setVisible(editType == "QTextEdit")
207
208 def keyPressEvent(self, event):
209 """
210 Protected slot to handle key press events.
211
212 @param event reference to the key press event (QKeyEvent)
213 """
214 if self.__textedit and event.key() == Qt.Key.Key_Escape:
215 self.__textedit.setFocus(Qt.FocusReason.ActiveWindowFocusReason)
216 event.accept()
217
218 @pyqtSlot(str)
219 def on_findtextCombo_editTextChanged(self, txt):
220 """
221 Private slot to enable/disable the find buttons.
222
223 @param txt text of the combobox (string)
224 """
225 self.__setSearchButtons(txt != "")
226
227 self.infoLabel.hide()
228 self.__setFindtextComboBackground(False)
229
230 def __setSearchButtons(self, enabled):
231 """
232 Private slot to set the state of the search buttons.
233
234 @param enabled flag indicating the state (boolean)
235 """
236 self.findPrevButton.setEnabled(enabled)
237 self.findNextButton.setEnabled(enabled)
238
239 def __findByReturnPressed(self):
240 """
241 Private slot to handle the returnPressed signal of the findtext
242 combobox.
243 """
244 self.__find(self.__findBackwards)
245
246 @pyqtSlot()
247 def on_findPrevButton_clicked(self):
248 """
249 Private slot to find the previous occurrence.
250 """
251 self.__find(True)
252
253 @pyqtSlot()
254 def on_findNextButton_clicked(self):
255 """
256 Private slot to find the next occurrence.
257 """
258 self.__find(False)
259
260 def __find(self, backwards):
261 """
262 Private method to search the associated text edit.
263
264 @param backwards flag indicating a backwards search (boolean)
265 """
266 if not self.__textedit:
267 return
268
269 self.infoLabel.clear()
270 self.infoLabel.hide()
271 self.__setFindtextComboBackground(False)
272
273 txt = self.findtextCombo.currentText()
274 if not txt:
275 return
276 self.__findBackwards = backwards
277
278 # This moves any previous occurrence of this statement to the head
279 # of the list and updates the combobox
280 if txt in self.findHistory:
281 self.findHistory.remove(txt)
282 self.findHistory.insert(0, txt)
283 self.findtextCombo.clear()
284 self.findtextCombo.addItems(self.findHistory)
285
286 if self.__texteditType in (
287 E5TextEditType.QTEXTBROWSER, E5TextEditType.QTEXTEDIT
288 ):
289 ok = self.__findPrevNextQTextEdit(backwards)
290 self.__findNextPrevCallback(ok)
291 elif self.__texteditType == E5TextEditType.QWEBENGINEVIEW:
292 self.__findPrevNextQWebEngineView(backwards)
293
294 def __findPrevNextQTextEdit(self, backwards):
295 """
296 Private method to to search the associated edit widget of
297 type QTextEdit.
298
299 @param backwards flag indicating a backwards search
300 @type bool
301 @return flag indicating the search result
302 @rtype bool
303 """
304 flags = (
305 QTextDocument.FindFlag.FindBackward
306 if backwards else
307 QTextDocument.FindFlag(0)
308 )
309 if self.caseCheckBox.isChecked():
310 flags |= QTextDocument.FindFlag.FindCaseSensitively
311 if self.wordCheckBox.isChecked():
312 flags |= QTextDocument.FindFlag.FindWholeWords
313
314 ok = self.__textedit.find(self.findtextCombo.currentText(), flags)
315 if not ok:
316 # wrap around once
317 cursor = self.__textedit.textCursor()
318 if backwards:
319 moveOp = QTextCursor.MoveOperation.End
320 # move to end of document
321 else:
322 moveOp = QTextCursor.MoveOperation.Start
323 # move to start of document
324 cursor.movePosition(moveOp)
325 self.__textedit.setTextCursor(cursor)
326 ok = self.__textedit.find(self.findtextCombo.currentText(), flags)
327
328 return ok
329
330 def __findPrevNextQWebEngineView(self, backwards):
331 """
332 Private method to to search the associated edit widget of
333 type QWebEngineView.
334
335 @param backwards flag indicating a backwards search
336 @type bool
337 """
338 from PyQt6.QtWebEngineWidgets import QWebEnginePage
339
340 findFlags = QWebEnginePage.FindFlag(0)
341 if self.caseCheckBox.isChecked():
342 findFlags |= QWebEnginePage.FindFlag.FindCaseSensitively
343 if backwards:
344 findFlags |= QWebEnginePage.FindFlag.FindBackward
345 self.__textedit.findText(self.findtextCombo.currentText(),
346 findFlags, self.__findNextPrevCallback)
347
348 def __findNextPrevCallback(self, found):
349 """
350 Private method to process the result of the last search.
351
352 @param found flag indicating if the last search succeeded
353 @type bool
354 """
355 if not found:
356 txt = self.findtextCombo.currentText()
357 self.infoLabel.setText(
358 self.tr("'{0}' was not found.").format(txt))
359 self.infoLabel.show()
360 self.__setFindtextComboBackground(True)
361
362 def __setFindtextComboBackground(self, error):
363 """
364 Private slot to change the findtext combo background to indicate
365 errors.
366
367 @param error flag indicating an error condition (boolean)
368 """
369 le = self.findtextCombo.lineEdit()
370 p = le.palette()
371 if error:
372 p.setBrush(QPalette.ColorRole.Base, QBrush(QColor("#FF6666")))
373 p.setBrush(QPalette.ColorRole.Text, QBrush(QColor("#000000")))
374 else:
375 p.setBrush(QPalette.ColorRole.Base, self.__defaultBaseColor)
376 p.setBrush(QPalette.ColorRole.Text, self.__defaultTextColor)
377 le.setPalette(p)
378 le.update()

eric ide

mercurial