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