src/eric7/QScintilla/SearchReplaceWidget.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) 2008 - 2022 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing the search and replace widget.
8 """
9
10 import re
11 import contextlib
12
13 from PyQt6.QtCore import pyqtSignal, Qt, pyqtSlot, QEvent
14 from PyQt6.QtWidgets import (
15 QWidget, QHBoxLayout, QToolButton, QScrollArea, QSizePolicy, QFrame
16 )
17
18 from .Editor import Editor
19
20 from EricGui.EricAction import EricAction
21 from EricWidgets import EricMessageBox
22
23 import Preferences
24
25 import UI.PixmapCache
26
27
28 class SearchReplaceWidget(QWidget):
29 """
30 Class implementing the search and replace widget.
31
32 @signal searchListChanged() emitted to indicate a change of the search list
33 """
34 searchListChanged = pyqtSignal()
35
36 def __init__(self, replace, vm, parent=None, sliding=False):
37 """
38 Constructor
39
40 @param replace flag indicating a replace widget is called
41 @param vm reference to the viewmanager object
42 @param parent parent widget of this widget (QWidget)
43 @param sliding flag indicating the widget is embedded in the
44 sliding widget (boolean)
45 """
46 super().__init__(parent)
47
48 self.__viewmanager = vm
49 self.__isMiniEditor = vm is parent
50 self.__replace = replace
51 self.__sliding = sliding
52 if sliding:
53 self.__topWidget = parent
54
55 self.findHistory = vm.getSRHistory('search')
56 if replace:
57 from .Ui_ReplaceWidget import Ui_ReplaceWidget
58 self.replaceHistory = vm.getSRHistory('replace')
59 self.ui = Ui_ReplaceWidget()
60 whatsThis = self.tr(
61 """<b>Find and Replace</b>
62 <p>This dialog is used to find some text and replace it with another text.
63 By checking the various checkboxes, the search can be made more specific.
64 The search string might be a regular expression. In a regular expression,
65 special characters interpreted are:</p>
66 """
67 )
68 else:
69 from .Ui_SearchWidget import Ui_SearchWidget
70 self.ui = Ui_SearchWidget()
71 whatsThis = self.tr(
72 """<b>Find</b>
73 <p>This dialog is used to find some text. By checking the various checkboxes,
74 the search can be made more specific. The search string might be a regular
75 expression. In a regular expression, special characters interpreted are:</p>
76 """
77 )
78 self.ui.setupUi(self)
79 if not replace:
80 self.ui.wrapCheckBox.setChecked(True)
81
82 whatsThis += self.tr(
83 """<table border="0">
84 <tr><td><code>.</code></td><td>Matches any character</td></tr>
85 <tr><td><code>(</code></td><td>This marks the start of a region for tagging a
86 match.</td></tr>
87 <tr><td><code>)</code></td><td>This marks the end of a tagged region.
88 </td></tr>
89 <tr><td><code>\\n</code></td>
90 <td>Where <code>n</code> is 1 through 9 refers to the first through ninth
91 tagged region when replacing. For example, if the search string was
92 <code>Fred([1-9])XXX</code> and the replace string was
93 <code>Sam\\1YYY</code>, when applied to <code>Fred2XXX</code> this would
94 generate <code>Sam2YYY</code>.</td></tr>
95 <tr><td><code>\\&lt;</code></td>
96 <td>This matches the start of a word using Scintilla's definitions of words.
97 </td></tr>
98 <tr><td><code>\\&gt;</code></td>
99 <td>This matches the end of a word using Scintilla's definition of words.
100 </td></tr>
101 <tr><td><code>\\x</code></td>
102 <td>This allows you to use a character x that would otherwise have a special
103 meaning. For example, \\[ would be interpreted as [ and not as the start of a
104 character set.</td></tr>
105 <tr><td><code>[...]</code></td>
106 <td>This indicates a set of characters, for example, [abc] means any of the
107 characters a, b or c. You can also use ranges, for example [a-z] for any lower
108 case character.</td></tr>
109 <tr><td><code>[^...]</code></td>
110 <td>The complement of the characters in the set. For example, [^A-Za-z] means
111 any character except an alphabetic character.</td></tr>
112 <tr><td><code>^</code></td>
113 <td>This matches the start of a line (unless used inside a set, see above).
114 </td></tr>
115 <tr><td><code>$</code></td> <td>This matches the end of a line.</td></tr>
116 <tr><td><code>*</code></td>
117 <td>This matches 0 or more times. For example, <code>Sa*m</code> matches
118 <code>Sm</code>, <code>Sam</code>, <code>Saam</code>, <code>Saaam</code>
119 and so on.</td></tr>
120 <tr><td><code>+</code></td>
121 <td>This matches 1 or more times. For example, <code>Sa+m</code> matches
122 <code>Sam</code>, <code>Saam</code>, <code>Saaam</code> and so on.</td></tr>
123 </table>
124 <p>When using the Extended (C++11) regular expression mode more features are
125 available, generally similar to regular expression support in JavaScript. See
126 the documentation of your C++ runtime for details on what is supported.<p>
127 """
128 )
129 self.setWhatsThis(whatsThis)
130
131 # set icons
132 self.ui.closeButton.setIcon(UI.PixmapCache.getIcon("close"))
133 self.ui.findPrevButton.setIcon(
134 UI.PixmapCache.getIcon("1leftarrow"))
135 self.ui.findNextButton.setIcon(
136 UI.PixmapCache.getIcon("1rightarrow"))
137 self.ui.extendButton.setIcon(
138 UI.PixmapCache.getIcon("2rightarrow"))
139
140 if replace:
141 self.ui.replaceButton.setIcon(
142 UI.PixmapCache.getIcon("editReplace"))
143 self.ui.replaceSearchButton.setIcon(
144 UI.PixmapCache.getIcon("editReplaceSearch"))
145 self.ui.replaceAllButton.setIcon(
146 UI.PixmapCache.getIcon("editReplaceAll"))
147
148 # set line edit completers
149 self.ui.findtextCombo.setCompleter(None)
150 self.ui.findtextCombo.lineEdit().returnPressed.connect(
151 self.__findByReturnPressed)
152 if replace:
153 self.ui.replacetextCombo.setCompleter(None)
154 self.ui.replacetextCombo.lineEdit().returnPressed.connect(
155 self.on_replaceButton_clicked)
156
157 self.ui.findtextCombo.lineEdit().textEdited.connect(self.__quickSearch)
158 self.ui.caseCheckBox.toggled.connect(
159 self.__updateQuickSearchMarkers)
160 self.ui.wordCheckBox.toggled.connect(
161 self.__updateQuickSearchMarkers)
162 self.ui.regexpCheckBox.toggled.connect(
163 self.__updateQuickSearchMarkers)
164
165 self.__findtextComboStyleSheet = (
166 self.ui.findtextCombo.styleSheet()
167 )
168
169 # define actions
170 self.findNextAct = EricAction(
171 self.tr('Find Next'),
172 self.tr('Find Next'),
173 0, 0, self, 'search_widget_find_next')
174 self.findNextAct.triggered.connect(self.on_findNextButton_clicked)
175 self.findNextAct.setShortcutContext(
176 Qt.ShortcutContext.WidgetWithChildrenShortcut)
177
178 self.findPrevAct = EricAction(
179 self.tr('Find Prev'),
180 self.tr('Find Prev'),
181 0, 0, self, 'search_widget_find_prev')
182 self.findPrevAct.triggered.connect(self.on_findPrevButton_clicked)
183 self.findPrevAct.setShortcutContext(
184 Qt.ShortcutContext.WidgetWithChildrenShortcut)
185
186 if replace:
187 self.replaceAndSearchAct = EricAction(
188 self.tr("Replace and Search"),
189 self.tr("Replace and Search"),
190 0, 0, self, "replace_widget_replace_search")
191 self.replaceAndSearchAct.triggered.connect(
192 self.on_replaceSearchButton_clicked)
193 self.replaceAndSearchAct.setEnabled(False)
194 self.replaceAndSearchAct.setShortcutContext(
195 Qt.ShortcutContext.WidgetWithChildrenShortcut)
196
197 self.replaceSelectionAct = EricAction(
198 self.tr("Replace Occurrence"),
199 self.tr("Replace Occurrence"),
200 0, 0, self, "replace_widget_replace_occurrence")
201 self.replaceSelectionAct.triggered.connect(
202 self.on_replaceButton_clicked)
203 self.replaceSelectionAct.setEnabled(False)
204 self.replaceSelectionAct.setShortcutContext(
205 Qt.ShortcutContext.WidgetWithChildrenShortcut)
206
207 self.replaceAllAct = EricAction(
208 self.tr("Replace All"),
209 self.tr("Replace All"),
210 0, 0, self, "replace_widget_replace_all")
211 self.replaceAllAct.triggered.connect(
212 self.on_replaceAllButton_clicked)
213 self.replaceAllAct.setEnabled(False)
214 self.replaceAllAct.setShortcutContext(
215 Qt.ShortcutContext.WidgetWithChildrenShortcut)
216
217 self.addAction(self.findNextAct)
218 self.addAction(self.findPrevAct)
219 if replace:
220 self.addAction(self.replaceAndSearchAct)
221 self.addAction(self.replaceSelectionAct)
222 self.addAction(self.replaceAllAct)
223
224 # disable search and replace buttons and actions
225 self.__setFindNextEnabled(False)
226 self.__setFindPrevEnabled(False)
227 if replace:
228 self.__setReplaceAndSearchEnabled(False)
229 self.__setReplaceSelectionEnabled(False)
230 self.__setReplaceAllEnabled(False)
231
232 self.adjustSize()
233
234 self.havefound = False
235 self.__pos = None
236 self.__findBackwards = False
237 self.__selections = []
238 self.__finding = False
239
240 def __setShortcuts(self):
241 """
242 Private method to set the local action's shortcuts to the same key
243 sequences as in the view manager.
244 """
245 if not self.__isMiniEditor:
246 self.findNextAct.setShortcuts(
247 self.__viewmanager.searchNextAct.shortcuts())
248 self.findPrevAct.setShortcuts(
249 self.__viewmanager.searchPrevAct.shortcuts())
250
251 if self.__replace:
252 self.replaceAndSearchAct.setShortcuts(
253 self.__viewmanager.replaceAndSearchAct.shortcuts())
254 self.replaceSelectionAct.setShortcuts(
255 self.__viewmanager.replaceSelectionAct.shortcuts())
256 self.replaceAllAct.setShortcuts(
257 self.__viewmanager.replaceAllAct.shortcuts())
258
259 def __setFindNextEnabled(self, enable):
260 """
261 Private method to set the enabled state of "Find Next".
262
263 @param enable flag indicating the enable state to be set
264 @type bool
265 """
266 self.ui.findNextButton.setEnabled(enable)
267 self.findNextAct.setEnabled(enable)
268
269 def __setFindPrevEnabled(self, enable):
270 """
271 Private method to set the enabled state of "Find Prev".
272
273 @param enable flag indicating the enable state to be set
274 @type bool
275 """
276 self.ui.findPrevButton.setEnabled(enable)
277 self.findPrevAct.setEnabled(enable)
278
279 def __setReplaceAndSearchEnabled(self, enable):
280 """
281 Private method to set the enabled state of "Replace And Search".
282
283 @param enable flag indicating the enable state to be set
284 @type bool
285 """
286 self.ui.replaceSearchButton.setEnabled(enable)
287 self.replaceAndSearchAct.setEnabled(enable)
288
289 def __setReplaceSelectionEnabled(self, enable):
290 """
291 Private method to set the enabled state of "Replace Occurrence".
292
293 @param enable flag indicating the enable state to be set
294 @type bool
295 """
296 self.ui.replaceButton.setEnabled(enable)
297 self.replaceSelectionAct.setEnabled(enable)
298
299 def __setReplaceAllEnabled(self, enable):
300 """
301 Private method to set the enabled state of "Replace All".
302
303 @param enable flag indicating the enable state to be set
304 @type bool
305 """
306 self.ui.replaceAllButton.setEnabled(enable)
307 self.replaceAllAct.setEnabled(enable)
308
309 def changeEvent(self, evt):
310 """
311 Protected method handling state changes.
312
313 @param evt event containing the state change (QEvent)
314 """
315 if evt.type() == QEvent.Type.FontChange:
316 self.adjustSize()
317
318 def __selectionBoundary(self, selections=None):
319 """
320 Private method to calculate the current selection boundary.
321
322 @param selections optional parameter giving the selections to
323 calculate the boundary for (list of tuples of four integer)
324 @return tuple of start line and index and end line and index
325 (tuple of four integer)
326 """
327 if selections is None:
328 selections = self.__selections
329 if selections:
330 lineNumbers = (
331 [sel[0] for sel in selections] +
332 [sel[2] for sel in selections]
333 )
334 indexNumbers = (
335 [sel[1] for sel in selections] +
336 [sel[3] for sel in selections]
337 )
338 startLine, startIndex, endLine, endIndex = (
339 min(lineNumbers), min(indexNumbers),
340 max(lineNumbers), max(indexNumbers))
341 else:
342 startLine, startIndex, endLine, endIndex = -1, -1, -1, -1
343
344 return startLine, startIndex, endLine, endIndex
345
346 @pyqtSlot(str)
347 def on_findtextCombo_editTextChanged(self, txt):
348 """
349 Private slot to enable/disable the find buttons.
350
351 @param txt text of the find text combo
352 @type str
353 """
354 enable = bool(txt)
355
356 self.__setFindNextEnabled(enable)
357 self.__setFindPrevEnabled(enable)
358 self.ui.extendButton.setEnabled(enable)
359 if self.__replace:
360 self.__setReplaceSelectionEnabled(False)
361 self.__setReplaceAndSearchEnabled(False)
362 self.__setReplaceAllEnabled(enable)
363
364 @pyqtSlot(str)
365 def __quickSearch(self, txt):
366 """
367 Private slot to search for the entered text while typing.
368
369 @param txt text of the search edit
370 @type str
371 """
372 aw = self.__viewmanager.activeWindow()
373 aw.hideFindIndicator()
374 if Preferences.getEditor("QuickSearchMarkersEnabled"):
375 self.__quickSearchMarkOccurrences(txt)
376
377 lineFrom, indexFrom, lineTo, indexTo = aw.getSelection()
378 posixMode = (Preferences.getEditor("SearchRegexpMode") == 0 and
379 self.ui.regexpCheckBox.isChecked())
380 cxx11Mode = (Preferences.getEditor("SearchRegexpMode") == 1 and
381 self.ui.regexpCheckBox.isChecked())
382 ok = aw.findFirst(
383 txt,
384 self.ui.regexpCheckBox.isChecked(),
385 self.ui.caseCheckBox.isChecked(),
386 self.ui.wordCheckBox.isChecked(),
387 self.ui.wrapCheckBox.isChecked(),
388 not self.__findBackwards,
389 lineFrom, indexFrom,
390 posix=posixMode,
391 cxx11=cxx11Mode
392 )
393 if ok:
394 sline, sindex, eline, eindex = aw.getSelection()
395 aw.showFindIndicator(sline, sindex, eline, eindex)
396
397 if not txt:
398 ok = True # reset the color in case of an empty text
399
400 self.__setSearchEditColors(ok)
401
402 def __quickSearchMarkOccurrences(self, txt):
403 """
404 Private method to mark all occurrences of the search text.
405
406 @param txt text to search for (string)
407 """
408 aw = self.__viewmanager.activeWindow()
409
410 lineFrom = 0
411 indexFrom = 0
412 lineTo = -1
413 indexTo = -1
414
415 aw.clearSearchIndicators()
416 posixMode = (Preferences.getEditor("SearchRegexpMode") == 0 and
417 self.ui.regexpCheckBox.isChecked())
418 cxx11Mode = (Preferences.getEditor("SearchRegexpMode") == 1 and
419 self.ui.regexpCheckBox.isChecked())
420 ok = aw.findFirstTarget(
421 txt,
422 self.ui.regexpCheckBox.isChecked(),
423 self.ui.caseCheckBox.isChecked(),
424 self.ui.wordCheckBox.isChecked(),
425 lineFrom, indexFrom, lineTo, indexTo,
426 posix=posixMode,
427 cxx11=cxx11Mode
428 )
429 while ok:
430 tgtPos, tgtLen = aw.getFoundTarget()
431 aw.setSearchIndicator(tgtPos, tgtLen)
432 ok = aw.findNextTarget()
433
434 def __setSearchEditColors(self, ok):
435 """
436 Private method to set the search edit colors.
437
438 @param ok flag indicating a match
439 @type bool
440 """
441 if not ok:
442 self.ui.findtextCombo.setStyleSheet(
443 "color: #000000; background-color: #ff6666;"
444 )
445 else:
446 self.ui.findtextCombo.setStyleSheet(
447 self.__findtextComboStyleSheet)
448
449 @pyqtSlot()
450 def on_extendButton_clicked(self):
451 """
452 Private slot to handle the quicksearch extend action.
453 """
454 aw = self.__viewmanager.activeWindow()
455 if aw is None:
456 return
457
458 txt = self.ui.findtextCombo.currentText()
459 if not txt:
460 return
461
462 line, index = aw.getCursorPosition()
463 text = aw.text(line)
464
465 rx = re.compile(r'[^\w_]')
466 match = rx.search(text, index)
467 if match:
468 end = match.start()
469 if end > index:
470 ext = text[index:end]
471 txt += ext
472 self.ui.findtextCombo.setEditText(txt)
473 self.ui.findtextCombo.lineEdit().selectAll()
474 self.__quickSearch(txt)
475
476 @pyqtSlot(bool)
477 def __updateQuickSearchMarkers(self, on):
478 """
479 Private slot to handle the selection of the various check boxes.
480
481 @param on status of the check box (ignored)
482 @type bool
483 """
484 txt = self.ui.findtextCombo.currentText()
485 self.__quickSearch(txt)
486
487 @pyqtSlot()
488 def on_findNextButton_clicked(self):
489 """
490 Private slot to find the next occurrence of text.
491 """
492 self.findNext()
493
494 def findNext(self):
495 """
496 Public slot to find the next occurrence of text.
497 """
498 if not self.havefound or not self.ui.findtextCombo.currentText():
499 if self.__replace:
500 self.__viewmanager.showReplaceWidget()
501 else:
502 self.__viewmanager.showSearchWidget()
503 return
504
505 self.__findBackwards = False
506 txt = self.ui.findtextCombo.currentText()
507
508 # This moves any previous occurrence of this statement to the head
509 # of the list and updates the combobox
510 if txt in self.findHistory:
511 self.findHistory.remove(txt)
512 self.findHistory.insert(0, txt)
513 self.ui.findtextCombo.clear()
514 self.ui.findtextCombo.addItems(self.findHistory)
515 self.searchListChanged.emit()
516
517 ok = self.__findNextPrev(txt, False)
518 self.__setSearchEditColors(ok)
519 if ok:
520 if self.__replace:
521 self.__setReplaceSelectionEnabled(True)
522 self.__setReplaceAndSearchEnabled(True)
523 else:
524 EricMessageBox.information(
525 self, self.windowTitle(),
526 self.tr("'{0}' was not found.").format(txt))
527
528 @pyqtSlot()
529 def on_findPrevButton_clicked(self):
530 """
531 Private slot to find the previous occurrence of text.
532 """
533 self.findPrev()
534
535 def findPrev(self):
536 """
537 Public slot to find the next previous of text.
538 """
539 if not self.havefound or not self.ui.findtextCombo.currentText():
540 self.show(self.__viewmanager.textForFind())
541 return
542
543 self.__findBackwards = True
544 txt = self.ui.findtextCombo.currentText()
545
546 # This moves any previous occurrence of this statement to the head
547 # of the list and updates the combobox
548 if txt in self.findHistory:
549 self.findHistory.remove(txt)
550 self.findHistory.insert(0, txt)
551 self.ui.findtextCombo.clear()
552 self.ui.findtextCombo.addItems(self.findHistory)
553 self.searchListChanged.emit()
554
555 ok = self.__findNextPrev(txt, True)
556 self.__setSearchEditColors(ok)
557 if ok:
558 if self.__replace:
559 self.__setReplaceSelectionEnabled(True)
560 self.__setReplaceAndSearchEnabled(True)
561 else:
562 EricMessageBox.information(
563 self, self.windowTitle(),
564 self.tr("'{0}' was not found.").format(txt))
565
566 def __findByReturnPressed(self):
567 """
568 Private slot to handle the returnPressed signal of the findtext
569 combobox.
570 """
571 if self.__findBackwards:
572 self.findPrev()
573 else:
574 self.findNext()
575
576 def __markOccurrences(self, txt):
577 """
578 Private method to mark all occurrences of the search text.
579
580 @param txt text to search for (string)
581 """
582 aw = self.__viewmanager.activeWindow()
583 lineFrom = 0
584 indexFrom = 0
585 lineTo = -1
586 indexTo = -1
587 if self.ui.selectionCheckBox.isChecked():
588 lineFrom, indexFrom, lineTo, indexTo = self.__selectionBoundary()
589 posixMode = (Preferences.getEditor("SearchRegexpMode") == 0 and
590 self.ui.regexpCheckBox.isChecked())
591 cxx11Mode = (Preferences.getEditor("SearchRegexpMode") == 1 and
592 self.ui.regexpCheckBox.isChecked())
593
594 aw.clearSearchIndicators()
595 ok = aw.findFirstTarget(
596 txt,
597 self.ui.regexpCheckBox.isChecked(),
598 self.ui.caseCheckBox.isChecked(),
599 self.ui.wordCheckBox.isChecked(),
600 lineFrom, indexFrom, lineTo, indexTo,
601 posix=posixMode, cxx11=cxx11Mode)
602 while ok:
603 tgtPos, tgtLen = aw.getFoundTarget()
604 if tgtLen == 0:
605 break
606 if len(self.__selections) > 1:
607 lineFrom, indexFrom = aw.lineIndexFromPosition(tgtPos)
608 lineTo, indexTo = aw.lineIndexFromPosition(tgtPos + tgtLen)
609 for sel in self.__selections:
610 if (
611 lineFrom == sel[0] and
612 indexFrom >= sel[1] and
613 indexTo <= sel[3]
614 ):
615 indicate = True
616 break
617 else:
618 indicate = False
619 else:
620 indicate = True
621 if indicate:
622 aw.setSearchIndicator(tgtPos, tgtLen)
623 ok = aw.findNextTarget()
624 with contextlib.suppress(AttributeError):
625 aw.updateMarkerMap()
626 # ignore it for MiniEditor
627
628 def __findNextPrev(self, txt, backwards):
629 """
630 Private method to find the next occurrence of the search text.
631
632 @param txt text to search for (string)
633 @param backwards flag indicating a backwards search (boolean)
634 @return flag indicating success (boolean)
635 """
636 self.__finding = True
637
638 if Preferences.getEditor("SearchMarkersEnabled"):
639 self.__markOccurrences(txt)
640
641 aw = self.__viewmanager.activeWindow()
642 aw.hideFindIndicator()
643 cline, cindex = aw.getCursorPosition()
644
645 ok = True
646 lineFrom, indexFrom, lineTo, indexTo = aw.getSelection()
647 boundary = self.__selectionBoundary()
648 if backwards:
649 if (
650 self.ui.selectionCheckBox.isChecked() and
651 (lineFrom, indexFrom, lineTo, indexTo) == boundary
652 ):
653 # initial call
654 line, index = boundary[2:]
655 else:
656 if (lineFrom, indexFrom) == (-1, -1):
657 # no selection present
658 line = cline
659 index = cindex
660 else:
661 line = lineFrom
662 index = indexFrom
663 if (
664 self.ui.selectionCheckBox.isChecked() and
665 line == boundary[0] and
666 index >= 0 and
667 index < boundary[1]
668 ):
669 ok = False
670
671 if ok and index < 0:
672 line -= 1
673 if self.ui.selectionCheckBox.isChecked():
674 if line < boundary[0]:
675 if self.ui.wrapCheckBox.isChecked():
676 line, index = boundary[2:]
677 else:
678 ok = False
679 else:
680 index = aw.lineLength(line)
681 else:
682 if line < 0:
683 if self.ui.wrapCheckBox.isChecked():
684 line = aw.lines() - 1
685 index = aw.lineLength(line)
686 else:
687 ok = False
688 else:
689 index = aw.lineLength(line)
690 else:
691 if (
692 self.ui.selectionCheckBox.isChecked() and
693 (lineFrom, indexFrom, lineTo, indexTo) == boundary
694 ):
695 # initial call
696 line, index = boundary[:2]
697 else:
698 line = lineTo
699 index = indexTo
700
701 if ok:
702 posixMode = (Preferences.getEditor("SearchRegexpMode") == 0 and
703 self.ui.regexpCheckBox.isChecked())
704 cxx11Mode = (Preferences.getEditor("SearchRegexpMode") == 1 and
705 self.ui.regexpCheckBox.isChecked())
706 ok = aw.findFirst(
707 txt,
708 self.ui.regexpCheckBox.isChecked(),
709 self.ui.caseCheckBox.isChecked(),
710 self.ui.wordCheckBox.isChecked(),
711 self.ui.wrapCheckBox.isChecked(),
712 not backwards,
713 line, index,
714 posix=posixMode,
715 cxx11=cxx11Mode)
716
717 if ok and self.ui.selectionCheckBox.isChecked():
718 lineFrom, indexFrom, lineTo, indexTo = aw.getSelection()
719 if len(self.__selections) > 1:
720 for sel in self.__selections:
721 if (
722 lineFrom == sel[0] and
723 indexFrom >= sel[1] and
724 indexTo <= sel[3]
725 ):
726 ok = True
727 break
728 else:
729 ok = False
730 elif (
731 (lineFrom == boundary[0] and indexFrom >= boundary[1]) or
732 (lineFrom > boundary[0] and lineFrom < boundary[2]) or
733 (lineFrom == boundary[2] and indexFrom <= boundary[3])
734 ):
735 ok = True
736 else:
737 ok = False
738 if not ok and len(self.__selections) > 1:
739 # try again
740 while (
741 not ok and
742 ((backwards and lineFrom >= boundary[0]) or
743 (not backwards and lineFrom <= boundary[2]))
744 ):
745 for ind in range(len(self.__selections)):
746 if lineFrom == self.__selections[ind][0]:
747 after = indexTo > self.__selections[ind][3]
748 if backwards:
749 if after:
750 line, index = self.__selections[ind][2:]
751 else:
752 if ind > 0:
753 line, index = (
754 self.__selections[ind - 1][2:]
755 )
756 else:
757 if after:
758 if ind < len(self.__selections) - 1:
759 line, index = (
760 self.__selections[ind + 1][:2]
761 )
762 else:
763 line, index = self.__selections[ind][:2]
764 break
765 else:
766 break
767 ok = aw.findFirst(
768 txt,
769 self.ui.regexpCheckBox.isChecked(),
770 self.ui.caseCheckBox.isChecked(),
771 self.ui.wordCheckBox.isChecked(),
772 self.ui.wrapCheckBox.isChecked(),
773 not backwards,
774 line, index,
775 posix=posixMode,
776 cxx11=cxx11Mode)
777 if ok:
778 lineFrom, indexFrom, lineTo, indexTo = (
779 aw.getSelection()
780 )
781 if (
782 lineFrom < boundary[0] or
783 lineFrom > boundary[2] or
784 indexFrom < boundary[1] or
785 indexFrom > boundary[3] or
786 indexTo < boundary[1] or
787 indexTo > boundary[3]
788 ):
789 ok = False
790 break
791 if not ok:
792 if self.ui.wrapCheckBox.isChecked():
793 # try it again
794 if backwards:
795 line, index = boundary[2:]
796 else:
797 line, index = boundary[:2]
798 ok = aw.findFirst(
799 txt,
800 self.ui.regexpCheckBox.isChecked(),
801 self.ui.caseCheckBox.isChecked(),
802 self.ui.wordCheckBox.isChecked(),
803 self.ui.wrapCheckBox.isChecked(),
804 not backwards,
805 line, index,
806 posix=posixMode,
807 cxx11=cxx11Mode)
808 if ok:
809 lineFrom, indexFrom, lineTo, indexTo = (
810 aw.getSelection()
811 )
812 if len(self.__selections) > 1:
813 for sel in self.__selections:
814 if (
815 lineFrom == sel[0] and
816 indexFrom >= sel[1] and
817 indexTo <= sel[3]
818 ):
819 ok = True
820 break
821 else:
822 ok = False
823 elif (
824 (lineFrom == boundary[0] and
825 indexFrom >= boundary[1]) or
826 (lineFrom > boundary[0] and
827 lineFrom < boundary[2]) or
828 (lineFrom == boundary[2] and
829 indexFrom <= boundary[3])
830 ):
831 ok = True
832 else:
833 ok = False
834 else:
835 ok = False
836
837 if not ok:
838 aw.selectAll(False)
839 aw.setCursorPosition(cline, cindex)
840 aw.ensureCursorVisible()
841
842 if ok:
843 sline, sindex, eline, eindex = aw.getSelection()
844 aw.showFindIndicator(sline, sindex, eline, eindex)
845
846 self.__finding = False
847
848 return ok
849
850 def __showFind(self, text=''):
851 """
852 Private method to display this widget in find mode.
853
854 @param text text to be shown in the findtext edit (string)
855 """
856 self.__replace = False
857
858 self.__setSearchEditColors(True)
859 self.ui.findtextCombo.clear()
860 self.ui.findtextCombo.addItems(self.findHistory)
861 self.ui.findtextCombo.setEditText(text)
862 self.ui.findtextCombo.lineEdit().selectAll()
863 self.ui.findtextCombo.setFocus()
864 self.on_findtextCombo_editTextChanged(text)
865
866 self.ui.caseCheckBox.setChecked(False)
867 self.ui.wordCheckBox.setChecked(False)
868 self.ui.wrapCheckBox.setChecked(True)
869 self.ui.regexpCheckBox.setChecked(False)
870
871 aw = self.__viewmanager.activeWindow()
872 self.updateSelectionCheckBox(aw)
873
874 self.havefound = True
875 self.__findBackwards = False
876
877 self.__setShortcuts()
878
879 def selectionChanged(self, editor):
880 """
881 Public slot tracking changes of selected text.
882
883 @param editor reference to the editor
884 @type Editor
885 """
886 self.updateSelectionCheckBox(editor)
887
888 @pyqtSlot(Editor)
889 def updateSelectionCheckBox(self, editor):
890 """
891 Public slot to update the selection check box.
892
893 @param editor reference to the editor
894 @type Editor
895 """
896 if not self.__finding and isinstance(editor, Editor):
897 if editor.hasSelectedText():
898 selections = editor.getSelections()
899 line1, index1, line2, index2 = (
900 self.__selectionBoundary(selections)
901 )
902 if line1 != line2:
903 self.ui.selectionCheckBox.setEnabled(True)
904 self.ui.selectionCheckBox.setChecked(True)
905 self.__selections = selections
906 return
907
908 self.ui.selectionCheckBox.setEnabled(False)
909 self.ui.selectionCheckBox.setChecked(False)
910 self.__selections = []
911
912 def replace(self):
913 """
914 Public method to replace the current selection.
915 """
916 if self.ui.replaceButton.isEnabled():
917 self.__doReplace(False)
918
919 def replaceSearch(self):
920 """
921 Public method to replace the current selection and search again.
922 """
923 if self.ui.replaceSearchButton.isEnabled():
924 self.__doReplace(True)
925
926 @pyqtSlot()
927 def on_replaceButton_clicked(self):
928 """
929 Private slot to replace one occurrence of text.
930 """
931 self.__doReplace(False)
932
933 @pyqtSlot()
934 def on_replaceSearchButton_clicked(self):
935 """
936 Private slot to replace one occurrence of text and search for the next
937 one.
938 """
939 self.__doReplace(True)
940
941 def __doReplace(self, searchNext):
942 """
943 Private method to replace one occurrence of text.
944
945 @param searchNext flag indicating to search for the next occurrence
946 (boolean).
947 """
948 self.__finding = True
949
950 # Check enabled status due to dual purpose usage of this method
951 if (
952 not self.ui.replaceButton.isEnabled() and
953 not self.ui.replaceSearchButton.isEnabled()
954 ):
955 return
956
957 ftxt = self.ui.findtextCombo.currentText()
958 rtxt = self.ui.replacetextCombo.currentText()
959
960 # This moves any previous occurrence of this statement to the head
961 # of the list and updates the combobox
962 if rtxt in self.replaceHistory:
963 self.replaceHistory.remove(rtxt)
964 self.replaceHistory.insert(0, rtxt)
965 self.ui.replacetextCombo.clear()
966 self.ui.replacetextCombo.addItems(self.replaceHistory)
967
968 aw = self.__viewmanager.activeWindow()
969 aw.hideFindIndicator()
970 aw.replace(rtxt)
971
972 if searchNext:
973 ok = self.__findNextPrev(ftxt, self.__findBackwards)
974 self.__setSearchEditColors(ok)
975
976 if not ok:
977 self.__setReplaceSelectionEnabled(False)
978 self.__setReplaceAndSearchEnabled(False)
979 EricMessageBox.information(
980 self, self.windowTitle(),
981 self.tr("'{0}' was not found.").format(ftxt))
982 else:
983 self.__setReplaceSelectionEnabled(False)
984 self.__setReplaceAndSearchEnabled(False)
985 self.__setSearchEditColors(True)
986
987 self.__finding = False
988
989 def replaceAll(self):
990 """
991 Public method to replace all occurrences.
992 """
993 if self.ui.replaceAllButton.isEnabled():
994 self.on_replaceAllButton_clicked()
995
996 @pyqtSlot()
997 def on_replaceAllButton_clicked(self):
998 """
999 Private slot to replace all occurrences of text.
1000 """
1001 self.__finding = True
1002
1003 replacements = 0
1004 ftxt = self.ui.findtextCombo.currentText()
1005 rtxt = self.ui.replacetextCombo.currentText()
1006
1007 # This moves any previous occurrence of this statement to the head
1008 # of the list and updates the combobox
1009 if ftxt in self.findHistory:
1010 self.findHistory.remove(ftxt)
1011 self.findHistory.insert(0, ftxt)
1012 self.ui.findtextCombo.clear()
1013 self.ui.findtextCombo.addItems(self.findHistory)
1014
1015 if rtxt in self.replaceHistory:
1016 self.replaceHistory.remove(rtxt)
1017 self.replaceHistory.insert(0, rtxt)
1018 self.ui.replacetextCombo.clear()
1019 self.ui.replacetextCombo.addItems(self.replaceHistory)
1020
1021 aw = self.__viewmanager.activeWindow()
1022 aw.hideFindIndicator()
1023 cline, cindex = aw.getCursorPosition()
1024 boundary = self.__selectionBoundary()
1025 if self.ui.selectionCheckBox.isChecked():
1026 line, index = boundary[:2]
1027 else:
1028 line = 0
1029 index = 0
1030 posixMode = (Preferences.getEditor("SearchRegexpMode") == 0 and
1031 self.ui.regexpCheckBox.isChecked())
1032 cxx11Mode = (Preferences.getEditor("SearchRegexpMode") == 1 and
1033 self.ui.regexpCheckBox.isChecked())
1034 ok = aw.findFirst(
1035 ftxt,
1036 self.ui.regexpCheckBox.isChecked(),
1037 self.ui.caseCheckBox.isChecked(),
1038 self.ui.wordCheckBox.isChecked(),
1039 False, True, line, index,
1040 posix=posixMode,
1041 cxx11=cxx11Mode)
1042
1043 if ok and self.ui.selectionCheckBox.isChecked():
1044 lineFrom, indexFrom, lineTo, indexTo = aw.getSelection()
1045 if len(self.__selections) > 1:
1046 for sel in self.__selections:
1047 if (
1048 lineFrom == sel[0] and
1049 indexFrom >= sel[1] and
1050 indexTo <= sel[3]
1051 ):
1052 ok = True
1053 break
1054 else:
1055 ok = False
1056 elif (
1057 (lineFrom == boundary[0] and indexFrom >= boundary[1]) or
1058 (lineFrom > boundary[0] and lineFrom < boundary[2]) or
1059 (lineFrom == boundary[2] and indexFrom <= boundary[3])
1060 ):
1061 ok = True
1062 else:
1063 ok = False
1064 if not ok and len(self.__selections) > 1:
1065 # try again
1066 while not ok and lineFrom <= boundary[2]:
1067 for ind in range(len(self.__selections)):
1068 if lineFrom == self.__selections[ind][0]:
1069 after = indexTo > self.__selections[ind][3]
1070 if after:
1071 if ind < len(self.__selections) - 1:
1072 line, index = (
1073 self.__selections[ind + 1][:2]
1074 )
1075 else:
1076 line, index = self.__selections[ind][:2]
1077 break
1078 else:
1079 break
1080 ok = aw.findFirst(
1081 ftxt,
1082 self.ui.regexpCheckBox.isChecked(),
1083 self.ui.caseCheckBox.isChecked(),
1084 self.ui.wordCheckBox.isChecked(),
1085 False, True, line, index,
1086 posix=posixMode,
1087 cxx11=cxx11Mode)
1088 if ok:
1089 lineFrom, indexFrom, lineTo, indexTo = (
1090 aw.getSelection()
1091 )
1092 if (
1093 lineFrom < boundary[0] or
1094 lineFrom > boundary[2] or
1095 indexFrom < boundary[1] or
1096 indexFrom > boundary[3] or
1097 indexTo < boundary[1] or
1098 indexTo > boundary[3]
1099 ):
1100 ok = False
1101 break
1102
1103 if not ok:
1104 aw.selectAll(False)
1105 aw.setCursorPosition(cline, cindex)
1106 aw.ensureCursorVisible()
1107
1108 found = ok
1109
1110 aw.beginUndoAction()
1111 wordWrap = self.ui.wrapCheckBox.isChecked()
1112 self.ui.wrapCheckBox.setChecked(False)
1113 while ok:
1114 aw.replace(rtxt)
1115 replacements += 1
1116 ok = self.__findNextPrev(ftxt, self.__findBackwards)
1117 self.__finding = True
1118 aw.endUndoAction()
1119 if wordWrap:
1120 self.ui.wrapCheckBox.setChecked(True)
1121 self.__setReplaceSelectionEnabled(False)
1122 self.__setReplaceAndSearchEnabled(False)
1123
1124 if found:
1125 EricMessageBox.information(
1126 self, self.windowTitle(),
1127 self.tr("Replaced {0} occurrences.")
1128 .format(replacements))
1129 else:
1130 EricMessageBox.information(
1131 self, self.windowTitle(),
1132 self.tr("Nothing replaced because '{0}' was not found.")
1133 .format(ftxt))
1134
1135 aw.setCursorPosition(cline, cindex)
1136 aw.ensureCursorVisible()
1137
1138 self.__finding = False
1139
1140 def __showReplace(self, text=''):
1141 """
1142 Private slot to display this widget in replace mode.
1143
1144 @param text text to be shown in the findtext edit
1145 """
1146 self.__replace = True
1147
1148 self.__setSearchEditColors(True)
1149 self.ui.findtextCombo.clear()
1150 self.ui.findtextCombo.addItems(self.findHistory)
1151 self.ui.findtextCombo.setEditText(text)
1152 self.ui.findtextCombo.lineEdit().selectAll()
1153 self.ui.findtextCombo.setFocus()
1154 self.on_findtextCombo_editTextChanged(text)
1155
1156 self.ui.replacetextCombo.clear()
1157 self.ui.replacetextCombo.addItems(self.replaceHistory)
1158 self.ui.replacetextCombo.setEditText('')
1159
1160 self.ui.caseCheckBox.setChecked(False)
1161 self.ui.wordCheckBox.setChecked(False)
1162 self.ui.regexpCheckBox.setChecked(False)
1163
1164 self.havefound = True
1165
1166 aw = self.__viewmanager.activeWindow()
1167 self.updateSelectionCheckBox(aw)
1168 if aw.hasSelectedText():
1169 line1, index1, line2, index2 = aw.getSelection()
1170 if line1 == line2:
1171 aw.setSelection(line1, index1, line1, index1)
1172 self.findNext()
1173
1174 self.__setShortcuts()
1175
1176 def show(self, text=''):
1177 """
1178 Public slot to show the widget.
1179
1180 @param text text to be shown in the findtext edit (string)
1181 """
1182 if self.__replace:
1183 self.__showReplace(text)
1184 else:
1185 self.__showFind(text)
1186 super().show()
1187 self.activateWindow()
1188
1189 @pyqtSlot()
1190 def on_closeButton_clicked(self):
1191 """
1192 Private slot to close the widget.
1193 """
1194 aw = self.__viewmanager.activeWindow()
1195 if aw:
1196 aw.hideFindIndicator()
1197
1198 if self.__sliding:
1199 self.__topWidget.close()
1200 else:
1201 self.close()
1202
1203 def keyPressEvent(self, event):
1204 """
1205 Protected slot to handle key press events.
1206
1207 @param event reference to the key press event (QKeyEvent)
1208 """
1209 if event.key() == Qt.Key.Key_Escape:
1210 aw = self.__viewmanager.activeWindow()
1211 if aw:
1212 aw.setFocus(Qt.FocusReason.ActiveWindowFocusReason)
1213 aw.hideFindIndicator()
1214 event.accept()
1215 if self.__sliding:
1216 self.__topWidget.close()
1217 else:
1218 self.close()
1219
1220
1221 class SearchReplaceSlidingWidget(QWidget):
1222 """
1223 Class implementing the search and replace widget with sliding behavior.
1224
1225 @signal searchListChanged() emitted to indicate a change of the search list
1226 """
1227 searchListChanged = pyqtSignal()
1228
1229 def __init__(self, replace, vm, parent=None):
1230 """
1231 Constructor
1232
1233 @param replace flag indicating a replace widget is called
1234 @param vm reference to the viewmanager object
1235 @param parent parent widget of this widget (QWidget)
1236 """
1237 super().__init__(parent)
1238
1239 self.__searchReplaceWidget = SearchReplaceWidget(
1240 replace, vm, self, True)
1241
1242 self.__layout = QHBoxLayout(self)
1243 self.setLayout(self.__layout)
1244 self.__layout.setContentsMargins(0, 0, 0, 0)
1245 self.__layout.setAlignment(Qt.AlignmentFlag.AlignTop)
1246
1247 self.__leftButton = QToolButton(self)
1248 self.__leftButton.setArrowType(Qt.ArrowType.LeftArrow)
1249 self.__leftButton.setSizePolicy(
1250 QSizePolicy.Policy.Minimum, QSizePolicy.Policy.MinimumExpanding)
1251 self.__leftButton.setAutoRepeat(True)
1252
1253 self.__scroller = QScrollArea(self)
1254 self.__scroller.setWidget(self.__searchReplaceWidget)
1255 self.__scroller.setSizePolicy(
1256 QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
1257 self.__scroller.setFrameShape(QFrame.Shape.NoFrame)
1258 self.__scroller.setVerticalScrollBarPolicy(
1259 Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
1260 self.__scroller.setHorizontalScrollBarPolicy(
1261 Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
1262 self.__scroller.setWidgetResizable(False)
1263
1264 self.__rightButton = QToolButton(self)
1265 self.__rightButton.setArrowType(Qt.ArrowType.RightArrow)
1266 self.__rightButton.setSizePolicy(
1267 QSizePolicy.Policy.Minimum, QSizePolicy.Policy.MinimumExpanding)
1268 self.__rightButton.setAutoRepeat(True)
1269
1270 self.__layout.addWidget(self.__leftButton)
1271 self.__layout.addWidget(self.__scroller)
1272 self.__layout.addWidget(self.__rightButton)
1273
1274 self.setMaximumHeight(self.__searchReplaceWidget.sizeHint().height())
1275 self.adjustSize()
1276
1277 self.__searchReplaceWidget.searchListChanged.connect(
1278 self.searchListChanged)
1279 self.__leftButton.clicked.connect(self.__slideLeft)
1280 self.__rightButton.clicked.connect(self.__slideRight)
1281
1282 def changeEvent(self, evt):
1283 """
1284 Protected method handling state changes.
1285
1286 @param evt event containing the state change (QEvent)
1287 """
1288 if evt.type() == QEvent.Type.FontChange:
1289 self.setMaximumHeight(
1290 self.__searchReplaceWidget.sizeHint().height())
1291 self.adjustSize()
1292
1293 def findNext(self):
1294 """
1295 Public slot to find the next occurrence of text.
1296 """
1297 self.__searchReplaceWidget.findNext()
1298
1299 def findPrev(self):
1300 """
1301 Public slot to find the next previous of text.
1302 """
1303 self.__searchReplaceWidget.findPrev()
1304
1305 def replace(self):
1306 """
1307 Public method to replace the current selection.
1308 """
1309 self.__searchReplaceWidget.replace()
1310
1311 def replaceSearch(self):
1312 """
1313 Public method to replace the current selection and search again.
1314 """
1315 self.__searchReplaceWidget.replaceSearch()
1316
1317 def replaceAll(self):
1318 """
1319 Public method to replace all occurrences.
1320 """
1321 self.__searchReplaceWidget.replaceAll()
1322
1323 def selectionChanged(self, editor):
1324 """
1325 Public slot tracking changes of selected text.
1326
1327 @param editor reference to the editor
1328 @type Editor
1329 """
1330 self.__searchReplaceWidget.updateSelectionCheckBox(editor)
1331
1332 @pyqtSlot(Editor)
1333 def updateSelectionCheckBox(self, editor):
1334 """
1335 Public slot to update the selection check box.
1336
1337 @param editor reference to the editor (Editor)
1338 """
1339 self.__searchReplaceWidget.updateSelectionCheckBox(editor)
1340
1341 def show(self, text=''):
1342 """
1343 Public slot to show the widget.
1344
1345 @param text text to be shown in the findtext edit (string)
1346 """
1347 self.__searchReplaceWidget.show(text)
1348 super().show()
1349 self.__enableScrollerButtons()
1350
1351 def __slideLeft(self):
1352 """
1353 Private slot to move the widget to the left, i.e. show contents to the
1354 right.
1355 """
1356 self.__slide(True)
1357
1358 def __slideRight(self):
1359 """
1360 Private slot to move the widget to the right, i.e. show contents to
1361 the left.
1362 """
1363 self.__slide(False)
1364
1365 def __slide(self, toLeft):
1366 """
1367 Private method to move the sliding widget.
1368
1369 @param toLeft flag indicating to move to the left (boolean)
1370 """
1371 scrollBar = self.__scroller.horizontalScrollBar()
1372 stepSize = scrollBar.singleStep()
1373 if toLeft:
1374 stepSize = -stepSize
1375 newValue = scrollBar.value() + stepSize
1376 if newValue < 0:
1377 newValue = 0
1378 elif newValue > scrollBar.maximum():
1379 newValue = scrollBar.maximum()
1380 scrollBar.setValue(newValue)
1381 self.__enableScrollerButtons()
1382
1383 def __enableScrollerButtons(self):
1384 """
1385 Private method to set the enabled state of the scroll buttons.
1386 """
1387 scrollBar = self.__scroller.horizontalScrollBar()
1388 self.__leftButton.setEnabled(scrollBar.value() > 0)
1389 self.__rightButton.setEnabled(scrollBar.value() < scrollBar.maximum())
1390
1391 def resizeEvent(self, evt):
1392 """
1393 Protected method to handle resize events.
1394
1395 @param evt reference to the resize event (QResizeEvent)
1396 """
1397 self.__enableScrollerButtons()
1398
1399 super().resizeEvent(evt)

eric ide

mercurial