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