src/eric7/UI/FindFileWidget.py

branch
eric7
changeset 9209
b99e7fd55fd3
parent 9039
3c8aa997bad8
child 9221
bf71ee032bb4
equal deleted inserted replaced
9208:3fc8dfeb6ebe 9209:b99e7fd55fd3
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2002 - 2022 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing a dialog to search for text in files.
8 """
9
10 import os
11 import re
12 import time
13
14 from PyQt6.QtCore import pyqtSignal, pyqtSlot, Qt, QPoint, QUrl
15 from PyQt6.QtGui import QCursor, QDesktopServices, QImageReader
16 from PyQt6.QtWidgets import (
17 QWidget, QApplication, QMenu, QTreeWidgetItem, QComboBox, QDialog,
18 QDialogButtonBox, QVBoxLayout
19 )
20
21 from EricWidgets.EricApplication import ericApp
22 from EricWidgets import EricMessageBox
23 from EricWidgets.EricPathPicker import EricPathPickerModes
24
25 from .Ui_FindFileWidget import Ui_FindFileWidget
26
27 import Preferences
28 import UI.PixmapCache
29 import Utilities
30
31
32 class FindFileWidget(QWidget, Ui_FindFileWidget):
33 """
34 Class implementing a widget to search for text in files and replace it
35 with some other text.
36
37 The occurrences found are displayed in a tree showing the file name,
38 the line number and the text found. The file will be opened upon a double
39 click onto the respective entry of the list. If the widget is in replace
40 mode the line below shows the text after replacement. Replacements can
41 be authorized by ticking them on. Pressing the replace button performs
42 all ticked replacement operations.
43
44 @signal sourceFile(str, int, str, int, int) emitted to open a source file
45 at a specificline
46 @signal designerFile(str) emitted to open a Qt-Designer file
47 @signal linguistFile(str) emitted to open a Qt-Linguist (*.ts) file
48 @signal trpreview([str]) emitted to preview Qt-Linguist (*.qm) files
49 @signal pixmapFile(str) emitted to open a pixmap file
50 @signal svgFile(str) emitted to open a SVG file
51 @signal umlFile(str) emitted to open an eric UML file
52 """
53 sourceFile = pyqtSignal(str, int, str, int, int)
54 designerFile = pyqtSignal(str)
55 linguistFile = pyqtSignal(str)
56 trpreview = pyqtSignal(list)
57 pixmapFile = pyqtSignal(str)
58 svgFile = pyqtSignal(str)
59 umlFile = pyqtSignal(str)
60
61 lineRole = Qt.ItemDataRole.UserRole + 1
62 startRole = Qt.ItemDataRole.UserRole + 2
63 endRole = Qt.ItemDataRole.UserRole + 3
64 replaceRole = Qt.ItemDataRole.UserRole + 4
65 md5Role = Qt.ItemDataRole.UserRole + 5
66
67 def __init__(self, project, parent=None):
68 """
69 Constructor
70
71 @param project reference to the project object
72 @type Project
73 @param parent parent widget of this dialog (defaults to None)
74 @type QWidget (optional)
75 """
76 super().__init__(parent)
77 self.setupUi(self)
78
79 self.layout().setContentsMargins(0, 3, 0, 0)
80
81 self.caseToolButton.setIcon(UI.PixmapCache.getIcon("caseSensitive"))
82 self.wordToolButton.setIcon(UI.PixmapCache.getIcon("wholeWord"))
83 self.regexpToolButton.setIcon(UI.PixmapCache.getIcon("regexp"))
84
85 self.dirPicker.setMode(EricPathPickerModes.DIRECTORY_MODE)
86 self.dirPicker.setInsertPolicy(QComboBox.InsertPolicy.InsertAtTop)
87 self.dirPicker.setSizeAdjustPolicy(
88 QComboBox.SizeAdjustPolicy.AdjustToMinimumContentsLengthWithIcon)
89
90 self.stopButton.setEnabled(False)
91 self.stopButton.clicked.connect(self.__stopSearch)
92 self.stopButton.setIcon(UI.PixmapCache.getIcon("stopLoading"))
93 self.stopButton.setAutoDefault(False)
94
95 self.findButton.setEnabled(False)
96 self.findButton.clicked.connect(self.__doSearch)
97 self.findButton.setIcon(UI.PixmapCache.getIcon("find"))
98 self.findButton.setAutoDefault(False)
99
100 self.clearButton.setEnabled(False)
101 self.clearButton.clicked.connect(self.__clearResults)
102 self.clearButton.setIcon(UI.PixmapCache.getIcon("clear"))
103 self.clearButton.setAutoDefault(False)
104
105 self.replaceButton.setIcon(UI.PixmapCache.getIcon("editReplace"))
106 self.replaceButton.setAutoDefault(False)
107
108 self.modeToggleButton.clicked.connect(self.__toggleReplaceMode)
109
110 self.findProgressLabel.setMaximumWidth(550)
111
112 self.searchHistory = Preferences.toList(
113 Preferences.getSettings().value(
114 "FindFileWidget/SearchHistory"))
115 self.findtextCombo.lineEdit().setClearButtonEnabled(True)
116 self.findtextCombo.lineEdit().returnPressed.connect(self.__doSearch)
117 self.findtextCombo.setCompleter(None)
118 self.findtextCombo.addItems(self.searchHistory)
119 self.findtextCombo.setEditText("")
120
121 self.replaceHistory = Preferences.toList(
122 Preferences.getSettings().value(
123 "FindFileWidget/ReplaceHistory"))
124 self.replacetextCombo.lineEdit().setClearButtonEnabled(True)
125 self.replacetextCombo.lineEdit().returnPressed.connect(self.__doSearch)
126 self.replacetextCombo.setCompleter(None)
127 self.replacetextCombo.addItems(self.replaceHistory)
128 self.replacetextCombo.setEditText("")
129
130 self.dirHistory = Preferences.toList(
131 Preferences.getSettings().value(
132 "FindFileWidget/DirectoryHistory"))
133 self.dirPicker.addItems(self.dirHistory)
134 self.dirPicker.setText("")
135
136 self.excludeHiddenCheckBox.setChecked(Preferences.toBool(
137 Preferences.getSettings().value(
138 "FindFileWidget/ExcludeHidden", True)
139 ))
140
141 # ensure the file type tab is the current one
142 self.fileOptionsWidget.setCurrentWidget(self.fileTypeTab)
143
144 self.project = project
145 self.project.projectOpened.connect(self.__projectOpened)
146 self.project.projectClosed.connect(self.__projectClosed)
147
148 self.__standardListFont = self.findList.font()
149 self.findList.headerItem().setText(self.findList.columnCount(), "")
150 self.findList.header().setSortIndicator(0, Qt.SortOrder.AscendingOrder)
151 self.__section0Size = self.findList.header().sectionSize(0)
152 self.findList.setExpandsOnDoubleClick(False)
153
154 # Qt Designer form files
155 self.filterForms = r'.*\.ui$'
156 self.formsExt = ['*.ui']
157
158 # Corba interface files
159 self.filterInterfaces = r'.*\.idl$'
160 self.interfacesExt = ['*.idl']
161
162 # Protobuf protocol files
163 self.filterProtocols = r'.*\.proto$'
164 self.protocolsExt = ['*.proto']
165
166 # Qt resources files
167 self.filterResources = r'.*\.qrc$'
168 self.resourcesExt = ['*.qrc']
169
170 self.__cancelSearch = False
171 self.__lastFileItem = None
172 self.__populating = False
173
174 self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
175 self.customContextMenuRequested.connect(self.__contextMenuRequested)
176
177 self.__replaceMode = True
178 self.__toggleReplaceMode()
179
180 def __createItem(self, file, line, text, start, end, replTxt="", md5=""):
181 """
182 Private method to create an entry in the file list.
183
184 @param file filename of file
185 @type str
186 @param line line number
187 @type int
188 @param text text found
189 @type str
190 @param start start position of match
191 @type int
192 @param end end position of match
193 @type int
194 @param replTxt text with replacements applied (defaults to "")
195 @type str (optional)
196 @param md5 MD5 hash of the file (defaults to "")
197 @type str (optional)
198 """
199 if self.__lastFileItem is None:
200 # It's a new file
201 self.__lastFileItem = QTreeWidgetItem(self.findList, [file])
202 self.__lastFileItem.setFirstColumnSpanned(True)
203 self.__lastFileItem.setExpanded(True)
204 if self.__replaceMode:
205 self.__lastFileItem.setFlags(
206 self.__lastFileItem.flags() |
207 Qt.ItemFlag.ItemIsUserCheckable |
208 Qt.ItemFlag.ItemIsAutoTristate)
209 self.__lastFileItem.setData(0, self.md5Role, md5)
210
211 itm = QTreeWidgetItem(self.__lastFileItem)
212 itm.setTextAlignment(0, Qt.AlignmentFlag.AlignRight)
213 itm.setData(0, Qt.ItemDataRole.DisplayRole, line)
214 itm.setData(1, Qt.ItemDataRole.DisplayRole, text)
215 itm.setData(0, self.lineRole, line)
216 itm.setData(0, self.startRole, start)
217 itm.setData(0, self.endRole, end)
218 itm.setData(0, self.replaceRole, replTxt)
219 if self.__replaceMode:
220 itm.setFlags(itm.flags() |
221 Qt.ItemFlag.ItemIsUserCheckable)
222 itm.setCheckState(0, Qt.CheckState.Checked)
223 self.replaceButton.setEnabled(True)
224
225 def activate(self, replaceMode=False, txt="", searchDir="",
226 openFiles=False):
227 """
228 Public method to activate the widget with a given mode, a text
229 to search for and some search parameters.
230
231 @param replaceMode flag indicating replacement mode (defaults to False)
232 @type bool (optional)
233 @param txt text to be searched for (defaults to "")
234 @type str (optional)
235 @param searchDir directory to search in (defaults to "")
236 @type str (optional)
237 @param openFiles flag indicating to operate on open files only
238 (defaults to False)
239 @type bool (optional)
240 """
241 if self.project.isOpen():
242 self.projectButton.setEnabled(True)
243 self.projectButton.setChecked(True)
244 else:
245 self.projectButton.setEnabled(False)
246 self.dirButton.setChecked(True)
247
248 self.findtextCombo.setEditText(txt)
249 self.findtextCombo.lineEdit().selectAll()
250 self.findtextCombo.setFocus()
251
252 if self.__replaceMode != replaceMode:
253 self.__toggleReplaceMode()
254
255 if searchDir:
256 self.__setSearchDirectory(searchDir)
257 if openFiles:
258 self.__setOpenFiles()
259
260 @pyqtSlot()
261 def __toggleReplaceMode(self):
262 """
263 Private slot to toggle the dialog mode.
264 """
265 self.__replaceMode = not self.__replaceMode
266
267 # change some interface elements and properties
268 self.findList.clear()
269 self.clearButton.setEnabled(False)
270
271 if self.__replaceMode:
272 self.replaceButton.show()
273 self.replaceLabel.show()
274 self.replacetextCombo.show()
275
276 self.replaceButton.setEnabled(False)
277 self.replacetextCombo.setEditText("")
278
279 font = Preferences.getEditorOtherFonts("MonospacedFont")
280 self.findList.setFont(font)
281
282 self.modeToggleButton.setIcon(UI.PixmapCache.getIcon("1uparrow"))
283 else:
284 self.replaceLabel.hide()
285 self.replacetextCombo.hide()
286 self.replaceButton.hide()
287
288 self.findList.setFont(self.__standardListFont)
289
290 self.modeToggleButton.setIcon(UI.PixmapCache.getIcon("1downarrow"))
291
292 @pyqtSlot()
293 def __projectOpened(self):
294 """
295 Private slot to react to the opening of a project.
296 """
297 self.projectButton.setEnabled(True)
298 self.projectButton.setChecked(True)
299
300 @pyqtSlot()
301 def __projectClosed(self):
302 """
303 Private slot to react to the closing of a project.
304 """
305 self.projectButton.setEnabled(False)
306 if self.projectButton.isChecked():
307 self.dirButton.setChecked(True)
308
309 @pyqtSlot(str)
310 def on_findtextCombo_editTextChanged(self, text):
311 """
312 Private slot to handle the editTextChanged signal of the find
313 text combo.
314
315 @param text (ignored)
316 """
317 self.__enableFindButton()
318
319 @pyqtSlot(str)
320 def on_replacetextCombo_editTextChanged(self, text):
321 """
322 Private slot to handle the editTextChanged signal of the replace
323 text combo.
324
325 @param text (ignored)
326 """
327 self.__enableFindButton()
328
329 @pyqtSlot(str)
330 def on_dirPicker_editTextChanged(self, text):
331 """
332 Private slot to handle the textChanged signal of the directory
333 picker.
334
335 @param text (ignored)
336 """
337 self.__enableFindButton()
338
339 @pyqtSlot()
340 def on_projectButton_clicked(self):
341 """
342 Private slot to handle the selection of the 'Project' radio button.
343 """
344 self.__enableFindButton()
345
346 @pyqtSlot()
347 def on_dirButton_clicked(self):
348 """
349 Private slot to handle the selection of the 'Directory' radio button.
350 """
351 self.__enableFindButton()
352
353 @pyqtSlot()
354 def on_openFilesButton_clicked(self):
355 """
356 Private slot to handle the selection of the 'Open Files' radio button.
357 """
358 self.__enableFindButton()
359
360 @pyqtSlot()
361 def on_filterCheckBox_clicked(self):
362 """
363 Private slot to handle the selection of the file filter check box.
364 """
365 self.__enableFindButton()
366
367 @pyqtSlot(str)
368 def on_filterEdit_textEdited(self, text):
369 """
370 Private slot to handle the textChanged signal of the file filter edit.
371
372 @param text (ignored)
373 """
374 self.__enableFindButton()
375
376 @pyqtSlot()
377 def __enableFindButton(self):
378 """
379 Private slot called to enable the find button.
380 """
381 if (
382 self.findtextCombo.currentText() == "" or
383 (self.dirButton.isChecked() and
384 (self.dirPicker.currentText() == "" or
385 not os.path.exists(os.path.abspath(
386 self.dirPicker.currentText())))) or
387 (self.filterCheckBox.isChecked() and
388 self.filterEdit.text() == "")
389 ):
390 self.findButton.setEnabled(False)
391 else:
392 self.findButton.setEnabled(True)
393
394 def __stripEol(self, txt):
395 """
396 Private method to strip the eol part.
397
398 @param txt line of text that should be treated
399 @type str
400 @return text with eol stripped
401 @rtype str
402 """
403 return txt.replace("\r", "").replace("\n", "")
404
405 @pyqtSlot()
406 def __stopSearch(self):
407 """
408 Private slot to handle the stop button being pressed.
409 """
410 self.__cancelSearch = True
411
412 @pyqtSlot()
413 def __doSearch(self):
414 """
415 Private slot to handle the find button being pressed.
416 """
417 if (
418 self.__replaceMode and
419 not ericApp().getObject("ViewManager").checkAllDirty()
420 ):
421 return
422
423 self.__cancelSearch = False
424
425 if self.filterCheckBox.isChecked():
426 fileFilter = self.filterEdit.text()
427 fileFilterList = [
428 "^{0}$".format(filter.replace(".", r"\.").replace("*", ".*"))
429 for filter in fileFilter.split(";")
430 ]
431 filterRe = re.compile("|".join(fileFilterList))
432
433 if self.projectButton.isChecked():
434 if self.filterCheckBox.isChecked():
435 files = [
436 self.project.getRelativePath(file)
437 for file in
438 self.__getFileList(
439 self.project.getProjectPath(),
440 filterRe,
441 excludeHiddenDirs=self.excludeHiddenCheckBox
442 .isChecked(),
443 )
444 ]
445 else:
446 files = []
447 if self.sourcesCheckBox.isChecked():
448 files += self.project.pdata["SOURCES"]
449 if self.formsCheckBox.isChecked():
450 files += self.project.pdata["FORMS"]
451 if self.interfacesCheckBox.isChecked():
452 files += self.project.pdata["INTERFACES"]
453 if self.protocolsCheckBox.isChecked():
454 files += self.project.pdata["PROTOCOLS"]
455 if self.resourcesCheckBox.isChecked():
456 files += self.project.pdata["RESOURCES"]
457 elif self.dirButton.isChecked():
458 if not self.filterCheckBox.isChecked():
459 filters = []
460 if (
461 self.project.isOpen() and
462 os.path.abspath(self.dirPicker.currentText()).startswith(
463 self.project.getProjectPath())
464 ):
465 if self.sourcesCheckBox.isChecked():
466 filters.extend([
467 "^{0}$".format(
468 assoc.replace(".", r"\.").replace("*", ".*")
469 ) for assoc in
470 self.project.getFiletypeAssociations("SOURCES")
471 ])
472 if self.formsCheckBox.isChecked():
473 filters.extend([
474 "^{0}$".format(
475 assoc.replace(".", r"\.").replace("*", ".*")
476 ) for assoc in
477 self.project.getFiletypeAssociations("FORMS")
478 ])
479 if self.interfacesCheckBox.isChecked():
480 filters.extend([
481 "^{0}$".format(
482 assoc.replace(".", r"\.").replace("*", ".*")
483 ) for assoc in
484 self.project.getFiletypeAssociations("INTERFACES")
485 ])
486 if self.protocolsCheckBox.isChecked():
487 filters.extend([
488 "^{0}$".format(
489 assoc.replace(".", r"\.").replace("*", ".*")
490 ) for assoc in
491 self.project.getFiletypeAssociations("PROTOCOLS")
492 ])
493 if self.resourcesCheckBox.isChecked():
494 filters.extend([
495 "^{0}$".format(
496 assoc.replace(".", r"\.").replace("*", ".*")
497 ) for assoc in
498 self.project.getFiletypeAssociations("RESOURCES")
499 ])
500 else:
501 if self.sourcesCheckBox.isChecked():
502 filters.extend([
503 "^{0}$".format(
504 assoc.replace(".", r"\.").replace("*", ".*"))
505 for assoc in list(
506 Preferences.getEditorLexerAssocs().keys())
507 if assoc not in
508 self.formsExt + self.interfacesExt +
509 self.protocolsExt + self.resourcesExt
510 ])
511 if self.formsCheckBox.isChecked():
512 filters.append(self.filterForms)
513 if self.interfacesCheckBox.isChecked():
514 filters.append(self.filterInterfaces)
515 if self.protocolsCheckBox.isChecked():
516 filters.append(self.filterProtocols)
517 if self.resourcesCheckBox.isChecked():
518 filters.append(self.filterResources)
519 filterString = "|".join(filters)
520 filterRe = re.compile(filterString)
521 files = self.__getFileList(
522 os.path.abspath(self.dirPicker.currentText()),
523 filterRe,
524 excludeHiddenDirs=self.excludeHiddenCheckBox.isChecked(),
525 excludeHiddenFiles=self.excludeHiddenCheckBox.isChecked(),
526 )
527 elif self.openFilesButton.isChecked():
528 vm = ericApp().getObject("ViewManager")
529 vm.checkAllDirty()
530 files = vm.getOpenFilenames()
531
532 self.findList.clear()
533 QApplication.processEvents()
534 self.findProgress.setMaximum(len(files))
535
536 # retrieve the values
537 reg = self.regexpToolButton.isChecked()
538 wo = self.wordToolButton.isChecked()
539 cs = self.caseToolButton.isChecked()
540 ct = self.findtextCombo.currentText()
541 txt = ct if reg else re.escape(ct)
542 if wo:
543 txt = "\\b{0}\\b".format(txt)
544 flags = re.UNICODE
545 if not cs:
546 flags |= re.IGNORECASE
547 try:
548 search = re.compile(txt, flags)
549 except re.error as why:
550 EricMessageBox.critical(
551 self,
552 self.tr("Invalid search expression"),
553 self.tr("""<p>The search expression is not valid.</p>"""
554 """<p>Error: {0}</p>""").format(str(why)))
555 self.stopButton.setEnabled(False)
556 self.findButton.setEnabled(True)
557 return
558 # reset the findtextCombo
559 if ct in self.searchHistory:
560 self.searchHistory.remove(ct)
561 self.searchHistory.insert(0, ct)
562 self.findtextCombo.clear()
563 self.findtextCombo.addItems(self.searchHistory)
564 Preferences.getSettings().setValue(
565 "FindFileWidget/SearchHistory",
566 self.searchHistory[:30])
567 Preferences.getSettings().setValue(
568 "FindFileWidget/ExcludeHidden",
569 self.excludeHiddenCheckBox.isChecked())
570
571 if self.__replaceMode:
572 replTxt = self.replacetextCombo.currentText()
573 if replTxt in self.replaceHistory:
574 self.replaceHistory.remove(replTxt)
575 self.replaceHistory.insert(0, replTxt)
576 self.replacetextCombo.clear()
577 self.replacetextCombo.addItems(self.replaceHistory)
578 Preferences.getSettings().setValue(
579 "FindFileWidget/ReplaceHistory",
580 self.replaceHistory[:30])
581
582 if self.dirButton.isChecked():
583 searchDir = self.dirPicker.currentText()
584 if searchDir in self.dirHistory:
585 self.dirHistory.remove(searchDir)
586 self.dirHistory.insert(0, searchDir)
587 self.dirPicker.clear()
588 self.dirPicker.addItems(self.dirHistory)
589 self.dirPicker.setText(self.dirHistory[0])
590 Preferences.getSettings().setValue(
591 "FindFileWidget/DirectoryHistory",
592 self.dirHistory[:30])
593
594 # set the button states
595 self.stopButton.setEnabled(True)
596 self.findButton.setEnabled(False)
597 self.clearButton.setEnabled(False)
598
599 # now go through all the files
600 self.__populating = True
601 self.findList.setUpdatesEnabled(False)
602 occurrences = 0
603 fileOccurrences = 0
604 for progress, file in enumerate(files, start=1):
605 self.__lastFileItem = None
606 found = False
607 if self.__cancelSearch:
608 break
609
610 fn = (
611 os.path.join(self.project.getProjectPath(), file)
612 if self.projectButton.isChecked() else
613 file
614 )
615 # read the file and split it into textlines
616 try:
617 text, encoding, hashStr = Utilities.readEncodedFileWithHash(fn)
618 lines = text.splitlines(True)
619 except (UnicodeError, OSError):
620 self.findProgress.setValue(progress)
621 continue
622
623 now = time.monotonic()
624 # now perform the search and display the lines found
625 for count, line in enumerate(lines, start=1):
626 if self.__cancelSearch:
627 break
628
629 contains = search.search(line)
630 if contains:
631 occurrences += 1
632 found = True
633 start = contains.start()
634 end = contains.end()
635 if self.__replaceMode:
636 rline = search.sub(replTxt, line)
637 else:
638 rline = ""
639 line = self.__stripEol(line)
640 if len(line) > 1024:
641 line = "{0} ...".format(line[:1024])
642 if self.__replaceMode:
643 if len(rline) > 1024:
644 rline = "{0} ...".format(line[:1024])
645 line = "- {0}\n+ {1}".format(
646 line, self.__stripEol(rline))
647 self.__createItem(file, count, line, start, end,
648 rline, hashStr)
649
650 if time.monotonic() - now > 0.01:
651 QApplication.processEvents()
652 now = time.monotonic()
653
654 if found:
655 fileOccurrences += 1
656 self.findProgress.setValue(progress)
657
658 if not files:
659 self.findProgress.setMaximum(1)
660 self.findProgress.setValue(1)
661
662 resultFormat = self.tr("{0} / {1}", "occurrences / files")
663 self.findProgressLabel.setPath(resultFormat.format(
664 self.tr("%n occurrence(s)", "", occurrences),
665 self.tr("%n file(s)", "", fileOccurrences)))
666
667 self.findList.setUpdatesEnabled(True)
668 self.findList.sortItems(self.findList.sortColumn(),
669 self.findList.header().sortIndicatorOrder())
670 self.findList.resizeColumnToContents(1)
671 if self.__replaceMode:
672 self.findList.header().resizeSection(0, self.__section0Size + 30)
673 self.findList.header().setStretchLastSection(True)
674 self.__populating = False
675
676 self.stopButton.setEnabled(False)
677 self.findButton.setEnabled(True)
678 self.clearButton.setEnabled(self.findList.topLevelItemCount() != 0)
679
680 @pyqtSlot()
681 def __clearResults(self):
682 """
683 Private slot to clear the current search results.
684 """
685 self.findList.clear()
686 self.replaceButton.setEnabled(False)
687 self.clearButton.setEnabled(False)
688 self.findProgressLabel.setPath("")
689 self.findProgress.setValue(0)
690
691 @pyqtSlot(QTreeWidgetItem, int)
692 def on_findList_itemDoubleClicked(self, itm, column):
693 """
694 Private slot to handle the double click on a file item.
695
696 It emits a signal depending on the file extension.
697
698 @param itm the double clicked tree item
699 @type QTreeWidgetItem
700 @param column column that was double clicked (ignored)
701 @type int
702 """
703 if itm.parent():
704 file = itm.parent().text(0)
705 line = itm.data(0, self.lineRole)
706 start = itm.data(0, self.startRole)
707 end = itm.data(0, self.endRole)
708 else:
709 file = itm.text(0)
710 line = 1
711 start = 0
712 end = 0
713
714 fileName = (
715 os.path.join(self.project.getProjectPath(), file)
716 if self.project.isOpen() else
717 file
718 )
719 fileExt = os.path.splitext(fileName)[1]
720
721 if fileExt == ".ui":
722 self.designerFile.emit(fileName)
723 elif fileExt == ".ts":
724 self.linguistFile.emit(fileName)
725 elif fileExt == ".qm":
726 self.trpreview.emit([fileName])
727 elif fileExt in (".egj", ".e5g"):
728 self.umlFile.emit(fileName)
729 elif fileExt == ".svg":
730 self.svgFile.emit(fileName)
731 elif fileExt[1:] in QImageReader.supportedImageFormats():
732 self.pixmapFile.emit(fileName)
733 else:
734 if Utilities.MimeTypes.isTextFile(fileName):
735 self.sourceFile.emit(fileName, line, "", start, end)
736 else:
737 QDesktopServices.openUrl(QUrl(fileName))
738
739 def __getFileList(self, path, filterRe, excludeHiddenDirs=False,
740 excludeHiddenFiles=False):
741 """
742 Private method to get a list of files to search.
743
744 @param path the root directory to search in
745 @type str
746 @param filterRe regular expression defining the filter
747 criteria
748 @type regexp object
749 @param excludeHiddenDirs flag indicating to exclude hidden directories
750 @type bool
751 @param excludeHiddenFiles flag indicating to exclude hidden files
752 @type bool
753 @return list of files to be processed
754 @rtype list of str
755 """
756 path = os.path.abspath(path)
757 files = []
758 for dirname, dirs, filenames in os.walk(path):
759 files.extend([
760 os.path.join(dirname, f) for f in filenames
761 if (not (excludeHiddenFiles and f.startswith(".")) and
762 re.match(filterRe, f))
763 ])
764 if excludeHiddenDirs:
765 for d in dirs[:]:
766 if d .startswith("."):
767 dirs.remove(d)
768 return files
769
770 def __setSearchDirectory(self, searchDir):
771 """
772 Private slot to set the name of the directory to search in.
773
774 @param searchDir name of the directory to search in
775 @type str
776 """
777 self.dirButton.setChecked(True)
778 self.dirPicker.setEditText(Utilities.toNativeSeparators(searchDir))
779
780 @pyqtSlot()
781 def __setOpenFiles(self):
782 """
783 Private slot to set the mode to search in open files.
784 """
785 self.openFilesButton.setChecked(True)
786
787 @pyqtSlot()
788 def on_replaceButton_clicked(self):
789 """
790 Private slot to perform the requested replace actions.
791 """
792 self.findProgress.setMaximum(self.findList.topLevelItemCount())
793 self.findProgress.setValue(0)
794
795 for index in range(self.findList.topLevelItemCount()):
796 itm = self.findList.topLevelItem(index)
797 if itm.checkState(0) in [Qt.CheckState.PartiallyChecked,
798 Qt.CheckState.Checked]:
799 file = itm.text(0)
800 origHash = itm.data(0, self.md5Role)
801
802 if self.projectButton.isChecked():
803 fn = os.path.join(self.project.getProjectPath(), file)
804 else:
805 fn = file
806
807 # read the file and split it into textlines
808 try:
809 text, encoding, hashStr = (
810 Utilities.readEncodedFileWithHash(fn)
811 )
812 lines = text.splitlines(True)
813 except (UnicodeError, OSError) as err:
814 EricMessageBox.critical(
815 self,
816 self.tr("Replace in Files"),
817 self.tr(
818 """<p>Could not read the file <b>{0}</b>."""
819 """ Skipping it.</p><p>Reason: {1}</p>""")
820 .format(fn, str(err))
821 )
822 self.findProgress.setValue(index)
823 continue
824
825 # Check the original and the current hash. Skip the file,
826 # if hashes are different.
827 if origHash != hashStr:
828 EricMessageBox.critical(
829 self,
830 self.tr("Replace in Files"),
831 self.tr(
832 """<p>The current and the original hash of the"""
833 """ file <b>{0}</b> are different. Skipping it."""
834 """</p><p>Hash 1: {1}</p><p>Hash 2: {2}</p>""")
835 .format(fn, origHash, hashStr)
836 )
837 self.findProgress.setValue(index)
838 continue
839
840 # replace the lines authorized by the user
841 for cindex in range(itm.childCount()):
842 citm = itm.child(cindex)
843 if citm.checkState(0) == Qt.CheckState.Checked:
844 line = citm.data(0, self.lineRole)
845 rline = citm.data(0, self.replaceRole)
846 lines[line - 1] = rline
847
848 # write the file
849 txt = "".join(lines)
850 try:
851 Utilities.writeEncodedFile(fn, txt, encoding)
852 except (OSError, Utilities.CodingError, UnicodeError) as err:
853 EricMessageBox.critical(
854 self,
855 self.tr("Replace in Files"),
856 self.tr(
857 """<p>Could not save the file <b>{0}</b>."""
858 """ Skipping it.</p><p>Reason: {1}</p>""")
859 .format(fn, str(err))
860 )
861
862 self.findProgress.setValue(index + 1)
863
864 self.findProgressLabel.setPath("")
865
866 self.findList.clear()
867 self.replaceButton.setEnabled(False)
868 self.findButton.setEnabled(True)
869 self.clearButton.setEnabled(False)
870
871 @pyqtSlot(QPoint)
872 def __contextMenuRequested(self, pos):
873 """
874 Private slot to handle the context menu request.
875
876 @param pos position the context menu shall be shown
877 @type QPoint
878 """
879 menu = QMenu(self)
880
881 menu.addAction(self.tr("Open"), self.__openFile)
882 menu.addAction(self.tr("Copy Path to Clipboard"),
883 self.__copyToClipboard)
884
885 menu.exec(QCursor.pos())
886
887 @pyqtSlot()
888 def __openFile(self):
889 """
890 Private slot to open the currently selected entry.
891 """
892 itm = self.findList.selectedItems()[0]
893 self.on_findList_itemDoubleClicked(itm, 0)
894
895 @pyqtSlot()
896 def __copyToClipboard(self):
897 """
898 Private slot to copy the path of an entry to the clipboard.
899 """
900 itm = self.findList.selectedItems()[0]
901 fn = itm.parent().text(0) if itm.parent() else itm.text(0)
902
903 cb = QApplication.clipboard()
904 cb.setText(fn)
905
906
907 class FindFileDialog(QDialog):
908 """
909 Class implementing a dialog to search for text in files and replace it
910 with some other text.
911
912 The occurrences found are displayed in a tree showing the file name,
913 the line number and the text found. The file will be opened upon a double
914 click onto the respective entry of the list. If the widget is in replace
915 mode the line below shows the text after replacement. Replacements can
916 be authorized by ticking them on. Pressing the replace button performs
917 all ticked replacement operations.
918
919 @signal sourceFile(str, int, str, int, int) emitted to open a source file
920 at a specificline
921 @signal designerFile(str) emitted to open a Qt-Designer file
922 @signal linguistFile(str) emitted to open a Qt-Linguist (*.ts) file
923 @signal trpreview([str]) emitted to preview Qt-Linguist (*.qm) files
924 @signal pixmapFile(str) emitted to open a pixmap file
925 @signal svgFile(str) emitted to open a SVG file
926 @signal umlFile(str) emitted to open an eric UML file
927 """
928 sourceFile = pyqtSignal(str, int, str, int, int)
929 designerFile = pyqtSignal(str)
930 linguistFile = pyqtSignal(str)
931 trpreview = pyqtSignal(list)
932 pixmapFile = pyqtSignal(str)
933 svgFile = pyqtSignal(str)
934 umlFile = pyqtSignal(str)
935
936 def __init__(self, project, parent=None):
937 """
938 Constructor
939
940 @param project reference to the project object
941 @type Project
942 @param parent parent widget of this dialog (defaults to None)
943 @type QWidget (optional)
944 """
945 super().__init__(parent)
946 self.setWindowFlags(Qt.WindowType.Window)
947
948 self.__layout = QVBoxLayout()
949
950 self.__findWidget = FindFileWidget(project, self)
951 self.__layout.addWidget(self.__findWidget)
952
953 self.__buttonBox = QDialogButtonBox(
954 QDialogButtonBox.StandardButton.Close,
955 Qt.Orientation.Horizontal,
956 self
957 )
958 self.__buttonBox.button(
959 QDialogButtonBox.StandardButton.Close).setAutoDefault(False)
960 self.__layout.addWidget(self.__buttonBox)
961
962 self.setLayout(self.__layout)
963 self.resize(600, 800)
964
965 # connect the widgets
966 self.__findWidget.sourceFile.connect(self.sourceFile)
967 self.__findWidget.designerFile.connect(self.designerFile)
968 self.__findWidget.linguistFile.connect(self.linguistFile)
969 self.__findWidget.trpreview.connect(self.trpreview)
970 self.__findWidget.pixmapFile.connect(self.pixmapFile)
971 self.__findWidget.svgFile.connect(self.svgFile)
972 self.__findWidget.umlFile.connect(self.umlFile)
973
974 self.__buttonBox.accepted.connect(self.accept)
975 self.__buttonBox.rejected.connect(self.reject)
976
977 def activate(self, replaceMode=False, txt="", searchDir="",
978 openFiles=False):
979 """
980 Public method to activate the dialog with a given mode, a text
981 to search for and some search parameters.
982
983 @param replaceMode flag indicating replacement mode (defaults to False)
984 @type bool (optional)
985 @param txt text to be searched for (defaults to "")
986 @type str (optional)
987 @param searchDir directory to search in (defaults to "")
988 @type str (optional)
989 @param openFiles flag indicating to operate on open files only
990 (defaults to False)
991 @type bool (optional)
992 """
993 self.__findWidget.activate(replaceMode=replaceMode, txt=txt,
994 searchDir=searchDir, openFiles=openFiles)
995
996 self.raise_()
997 self.activateWindow()
998 self.show()

eric ide

mercurial