|
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() |