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