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