eric6/UI/FindFileDialog.py

changeset 6942
2602857055c5
parent 6645
ad476851d7e0
child 7109
6191abce8002
equal deleted inserted replaced
6941:f99d60d6b59b 6942:2602857055c5
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)

eric ide

mercurial