eric7/UI/FindFileWidget.py

branch
eric7
changeset 8614
4a3a68e51b92
parent 8358
144a6b854f70
child 8632
f25cd4b94eb0
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eric7/UI/FindFileWidget.py	Sat Sep 18 18:51:58 2021 +0200
@@ -0,0 +1,846 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2002 - 2021 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing a dialog to search for text in files.
+"""
+
+import os
+import re
+
+from PyQt6.QtCore import pyqtSignal, pyqtSlot, Qt, QPoint
+from PyQt6.QtGui import QCursor
+from PyQt6.QtWidgets import (
+    QWidget, QApplication, QMenu, QTreeWidgetItem, QComboBox
+)
+
+from EricWidgets.EricApplication import ericApp
+from EricWidgets import EricMessageBox
+from EricWidgets.EricPathPicker import EricPathPickerModes
+
+from .Ui_FindFileWidget import Ui_FindFileWidget
+
+import Preferences
+import UI.PixmapCache
+import Utilities
+
+
+class FindFileWidget(QWidget, Ui_FindFileWidget):
+    """
+    Class implementing a widget to search for text in files and replace it
+    with some other text.
+    
+    The occurrences found are displayed in a tree showing the file name,
+    the line number and the text found. The file will be opened upon a double
+    click onto the respective entry of the list. If the widget is in replace
+    mode the line below shows the text after replacement. Replacements can
+    be authorized by ticking them on. Pressing the replace button performs
+    all ticked replacement operations.
+    
+    @signal sourceFile(str, int, str, int, int) emitted to open a source file
+        at a specificline
+    @signal designerFile(str) emitted to open a Qt-Designer file
+    """
+    sourceFile = pyqtSignal(str, int, str, int, int)
+    designerFile = pyqtSignal(str)
+    
+    lineRole = Qt.ItemDataRole.UserRole + 1
+    startRole = Qt.ItemDataRole.UserRole + 2
+    endRole = Qt.ItemDataRole.UserRole + 3
+    replaceRole = Qt.ItemDataRole.UserRole + 4
+    md5Role = Qt.ItemDataRole.UserRole + 5
+    
+    def __init__(self, project, parent=None):
+        """
+        Constructor
+        
+        @param project reference to the project object
+        @type Project
+        @param parent parent widget of this dialog (defaults to None)
+        @type QWidget (optional)
+        """
+        super().__init__(parent)
+        self.setupUi(self)
+        self.setWindowFlags(Qt.WindowType.Window)
+        
+        self.layout().setContentsMargins(0, 3, 0, 0)
+        
+        self.dirPicker.setMode(EricPathPickerModes.DIRECTORY_MODE)
+        self.dirPicker.setInsertPolicy(QComboBox.InsertPolicy.InsertAtTop)
+        self.dirPicker.setSizeAdjustPolicy(
+            QComboBox.SizeAdjustPolicy.AdjustToMinimumContentsLengthWithIcon)
+        
+        self.stopButton.setEnabled(False)
+        self.stopButton.clicked.connect(self.__stopSearch)
+        self.stopButton.setIcon(UI.PixmapCache.getIcon("stopLoading"))
+        
+        self.findButton.setEnabled(False)
+        self.findButton.clicked.connect(self.__doSearch)
+        self.findButton.setIcon(UI.PixmapCache.getIcon("find"))
+        
+        self.replaceButton.setIcon(UI.PixmapCache.getIcon("editReplace"))
+        
+        self.modeToggleButton.clicked.connect(self.__toggleReplaceMode)
+        
+        self.findProgressLabel.setMaximumWidth(550)
+        
+        self.searchHistory = Preferences.toList(
+            Preferences.Prefs.settings.value(
+                "FindFileWidget/SearchHistory"))
+        self.findtextCombo.lineEdit().setClearButtonEnabled(True)
+        self.findtextCombo.lineEdit().returnPressed.connect(self.__doSearch)
+        self.findtextCombo.setCompleter(None)
+        self.findtextCombo.addItems(self.searchHistory)
+        self.findtextCombo.setEditText("")
+        
+        self.replaceHistory = Preferences.toList(
+            Preferences.Prefs.settings.value(
+                "FindFileWidget/ReplaceHistory"))
+        self.replacetextCombo.lineEdit().setClearButtonEnabled(True)
+        self.replacetextCombo.lineEdit().returnPressed.connect(self.__doSearch)
+        self.replacetextCombo.setCompleter(None)
+        self.replacetextCombo.addItems(self.replaceHistory)
+        self.replacetextCombo.setEditText("")
+        
+        self.dirHistory = Preferences.toList(
+            Preferences.Prefs.settings.value(
+                "FindFileWidget/DirectoryHistory"))
+        self.dirPicker.addItems(self.dirHistory)
+        self.dirPicker.setText("")
+        
+        self.excludeHiddenCheckBox.setChecked(Preferences.toBool(
+            Preferences.Prefs.settings.value(
+                "FindFileWidget/ExcludeHidden", True)
+        ))
+        
+        self.project = project
+        self.project.projectOpened.connect(self.__projectOpened)
+        self.project.projectClosed.connect(self.__projectClosed)
+        
+        self.__standardListFont = self.findList.font()
+        self.findList.headerItem().setText(self.findList.columnCount(), "")
+        self.findList.header().setSortIndicator(0, Qt.SortOrder.AscendingOrder)
+        self.__section0Size = self.findList.header().sectionSize(0)
+        self.findList.setExpandsOnDoubleClick(False)
+
+        # Qt Designer form files
+        self.filterForms = r'.*\.ui$'
+        self.formsExt = ['*.ui']
+        
+        # Corba interface files
+        self.filterInterfaces = r'.*\.idl$'
+        self.interfacesExt = ['*.idl']
+        
+        # Protobuf protocol files
+        self.filterProtocols = r'.*\.proto$'
+        self.protocolsExt = ['*.proto']
+        
+        # Qt resources files
+        self.filterResources = r'.*\.qrc$'
+        self.resourcesExt = ['*.qrc']
+        
+        self.__cancelSearch = False
+        self.__lastFileItem = None
+        self.__populating = False
+        
+        self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
+        self.customContextMenuRequested.connect(self.__contextMenuRequested)
+        
+        self.__replaceMode = True
+        self.__toggleReplaceMode()
+    
+    def __createItem(self, file, line, text, start, end, replTxt="", md5=""):
+        """
+        Private method to create an entry in the file list.
+        
+        @param file filename of file
+        @type str
+        @param line line number
+        @type int
+        @param text text found
+        @type str
+        @param start start position of match
+        @type int
+        @param end end position of match
+        @type int
+        @param replTxt text with replacements applied (defaults to "")
+        @type str (optional)
+        @param md5 MD5 hash of the file (defaults to "")
+        @type str (optional)
+        """
+        if self.__lastFileItem is None:
+            # It's a new file
+            self.__lastFileItem = QTreeWidgetItem(self.findList, [file])
+            self.__lastFileItem.setFirstColumnSpanned(True)
+            self.__lastFileItem.setExpanded(True)
+            if self.__replaceMode:
+                self.__lastFileItem.setFlags(
+                    self.__lastFileItem.flags() |
+                    Qt.ItemFlag.ItemIsUserCheckable |
+                    Qt.ItemFlag.ItemIsAutoTristate)
+            self.__lastFileItem.setData(0, self.md5Role, md5)
+        
+        itm = QTreeWidgetItem(self.__lastFileItem)
+        itm.setTextAlignment(0, Qt.AlignmentFlag.AlignRight)
+        itm.setData(0, Qt.ItemDataRole.DisplayRole, line)
+        itm.setData(1, Qt.ItemDataRole.DisplayRole, text)
+        itm.setData(0, self.lineRole, line)
+        itm.setData(0, self.startRole, start)
+        itm.setData(0, self.endRole, end)
+        itm.setData(0, self.replaceRole, replTxt)
+        if self.__replaceMode:
+            itm.setFlags(itm.flags() |
+                         Qt.ItemFlag.ItemIsUserCheckable)
+            itm.setCheckState(0, Qt.CheckState.Checked)
+            self.replaceButton.setEnabled(True)
+    
+    def activate(self, replaceMode=False, txt="", searchDir="",
+                 openFiles=False):
+        """
+        Public method to activate the widget with a given mode, a text
+        to search for and some search parameters.
+        
+        @param replaceMode flag indicating replacement mode (defaults to False)
+        @type bool (optional)
+        @param txt text to be searched for (defaults to "")
+        @type str (optional)
+        @param searchDir directory to search in (defaults to "")
+        @type str (optional)
+        @param openFiles flag indicating to operate on open files only
+            (defaults to False)
+        @type bool (optional)
+        """
+        if self.project.isOpen():
+            self.projectButton.setEnabled(True)
+            self.projectButton.setChecked(True)
+        else:
+            self.projectButton.setEnabled(False)
+            self.dirButton.setChecked(True)
+            
+        self.findtextCombo.setEditText(txt)
+        self.findtextCombo.lineEdit().selectAll()
+        self.findtextCombo.setFocus()
+        
+        if self.__replaceMode != replaceMode:
+            self.__toggleReplaceMode()
+        
+        if searchDir:
+            self.setSearchDirectory(searchDir)
+        if openFiles:
+            self.setOpenFiles()
+    
+    @pyqtSlot()
+    def __toggleReplaceMode(self):
+        """
+        Private slot to toggle the dialog mode.
+        """
+        self.__replaceMode = not self.__replaceMode
+        
+        # change some interface elements and properties
+        self.findList.clear()
+        if self.__replaceMode:
+            self.replaceButton.show()
+            self.replaceLabel.show()
+            self.replacetextCombo.show()
+            
+            self.replaceButton.setEnabled(False)
+            self.replacetextCombo.setEditText("")
+            
+            font = Preferences.getEditorOtherFonts("MonospacedFont")
+            self.findList.setFont(font)
+            
+            self.modeToggleButton.setIcon(UI.PixmapCache.getIcon("1uparrow"))
+        else:
+            self.replaceLabel.hide()
+            self.replacetextCombo.hide()
+            self.replaceButton.hide()
+            
+            self.findList.setFont(self.__standardListFont)
+            
+            self.modeToggleButton.setIcon(UI.PixmapCache.getIcon("1downarrow"))
+    
+    @pyqtSlot()
+    def __projectOpened(self):
+        """
+        Private slot to react to the opening of a project.
+        """
+        self.projectButton.setEnabled(True)
+        self.projectButton.setChecked(True)
+    
+    @pyqtSlot()
+    def __projectClosed(self):
+        """
+        Private slot to react to the closing of a project.
+        """
+        self.projectButton.setEnabled(False)
+        if self.projectButton.isChecked():
+            self.dirButton.setChecked(True)
+    
+    @pyqtSlot(str)
+    def on_findtextCombo_editTextChanged(self, text):
+        """
+        Private slot to handle the editTextChanged signal of the find
+        text combo.
+        
+        @param text (ignored)
+        """
+        self.__enableFindButton()
+    
+    @pyqtSlot(str)
+    def on_replacetextCombo_editTextChanged(self, text):
+        """
+        Private slot to handle the editTextChanged signal of the replace
+        text combo.
+        
+        @param text (ignored)
+        """
+        self.__enableFindButton()
+    
+    @pyqtSlot(str)
+    def on_dirPicker_editTextChanged(self, text):
+        """
+        Private slot to handle the textChanged signal of the directory
+        picker.
+        
+        @param text (ignored)
+        """
+        self.__enableFindButton()
+    
+    @pyqtSlot()
+    def on_projectButton_clicked(self):
+        """
+        Private slot to handle the selection of the 'Project' radio button.
+        """
+        self.__enableFindButton()
+    
+    @pyqtSlot()
+    def on_dirButton_clicked(self):
+        """
+        Private slot to handle the selection of the 'Directory' radio button.
+        """
+        self.__enableFindButton()
+    
+    @pyqtSlot()
+    def on_openFilesButton_clicked(self):
+        """
+        Private slot to handle the selection of the 'Open Files' radio button.
+        """
+        self.__enableFindButton()
+    
+    @pyqtSlot()
+    def on_filterCheckBox_clicked(self):
+        """
+        Private slot to handle the selection of the file filter check box.
+        """
+        self.__enableFindButton()
+    
+    @pyqtSlot(str)
+    def on_filterEdit_textEdited(self, text):
+        """
+        Private slot to handle the textChanged signal of the file filter edit.
+        
+        @param text (ignored)
+        """
+        self.__enableFindButton()
+    
+    @pyqtSlot()
+    def __enableFindButton(self):
+        """
+        Private slot called to enable the find button.
+        """
+        if (
+            self.findtextCombo.currentText() == "" or
+            (self.dirButton.isChecked() and
+             (self.dirPicker.currentText() == "" or
+              not os.path.exists(os.path.abspath(
+                self.dirPicker.currentText())))) or
+            (self.filterCheckBox.isChecked() and
+             self.filterEdit.text() == "")
+        ):
+            self.findButton.setEnabled(False)
+        else:
+            self.findButton.setEnabled(True)
+    
+    def __stripEol(self, txt):
+        """
+        Private method to strip the eol part.
+        
+        @param txt line of text that should be treated
+        @type str
+        @return text with eol stripped
+        @rtype str
+        """
+        return txt.replace("\r", "").replace("\n", "")
+        
+    @pyqtSlot()
+    def __stopSearch(self):
+        """
+        Private slot to handle the stop button being pressed.
+        """
+        self.__cancelSearch = True
+    
+    @pyqtSlot()
+    def __doSearch(self):
+        """
+        Private slot to handle the find button being pressed.
+        """
+        if (
+            self.__replaceMode and
+            not ericApp().getObject("ViewManager").checkAllDirty()
+        ):
+            return
+        
+        self.__cancelSearch = False
+        
+        if self.filterCheckBox.isChecked():
+            fileFilter = self.filterEdit.text()
+            fileFilterList = [
+                "^{0}$".format(filter.replace(".", r"\.").replace("*", ".*"))
+                for filter in fileFilter.split(";")
+            ]
+            filterRe = re.compile("|".join(fileFilterList))
+        
+        if self.projectButton.isChecked():
+            if self.filterCheckBox.isChecked():
+                files = [
+                    self.project.getRelativePath(file)
+                    for file in
+                    self.__getFileList(
+                        self.project.getProjectPath(),
+                        filterRe,
+                        excludeHiddenDirs=self.excludeHiddenCheckBox
+                        .isChecked(),
+                    )
+                ]
+            else:
+                files = []
+                if self.sourcesCheckBox.isChecked():
+                    files += self.project.pdata["SOURCES"]
+                if self.formsCheckBox.isChecked():
+                    files += self.project.pdata["FORMS"]
+                if self.interfacesCheckBox.isChecked():
+                    files += self.project.pdata["INTERFACES"]
+                if self.protocolsCheckBox.isChecked():
+                    files += self.project.pdata["PROTOCOLS"]
+                if self.resourcesCheckBox.isChecked():
+                    files += self.project.pdata["RESOURCES"]
+        elif self.dirButton.isChecked():
+            if not self.filterCheckBox.isChecked():
+                filters = []
+                if (
+                    self.project.isOpen() and
+                    os.path.abspath(self.dirPicker.currentText()).startswith(
+                        self.project.getProjectPath())
+                ):
+                    if self.sourcesCheckBox.isChecked():
+                        filters.extend([
+                            "^{0}$".format(
+                                assoc.replace(".", r"\.").replace("*", ".*")
+                            ) for assoc in
+                            self.project.getFiletypeAssociations("SOURCES")
+                        ])
+                    if self.formsCheckBox.isChecked():
+                        filters.extend([
+                            "^{0}$".format(
+                                assoc.replace(".", r"\.").replace("*", ".*")
+                            ) for assoc in
+                            self.project.getFiletypeAssociations("FORMS")
+                        ])
+                    if self.interfacesCheckBox.isChecked():
+                        filters.extend([
+                            "^{0}$".format(
+                                assoc.replace(".", r"\.").replace("*", ".*")
+                            ) for assoc in
+                            self.project.getFiletypeAssociations("INTERFACES")
+                        ])
+                    if self.protocolsCheckBox.isChecked():
+                        filters.extend([
+                            "^{0}$".format(
+                                assoc.replace(".", r"\.").replace("*", ".*")
+                            ) for assoc in
+                            self.project.getFiletypeAssociations("PROTOCOLS")
+                        ])
+                    if self.resourcesCheckBox.isChecked():
+                        filters.extend([
+                            "^{0}$".format(
+                                assoc.replace(".", r"\.").replace("*", ".*")
+                            ) for assoc in
+                            self.project.getFiletypeAssociations("RESOURCES")
+                        ])
+                else:
+                    if self.sourcesCheckBox.isChecked():
+                        filters.extend([
+                            "^{0}$".format(
+                                assoc.replace(".", r"\.").replace("*", ".*"))
+                            for assoc in list(
+                                Preferences.getEditorLexerAssocs().keys())
+                            if assoc not in
+                            self.formsExt + self.interfacesExt +
+                            self.protocolsExt + self.resourcesExt
+                        ])
+                    if self.formsCheckBox.isChecked():
+                        filters.append(self.filterForms)
+                    if self.interfacesCheckBox.isChecked():
+                        filters.append(self.filterInterfaces)
+                    if self.protocolsCheckBox.isChecked():
+                        filters.append(self.filterProtocols)
+                    if self.resourcesCheckBox.isChecked():
+                        filters.append(self.filterResources)
+                filterString = "|".join(filters)
+                filterRe = re.compile(filterString)
+            files = self.__getFileList(
+                os.path.abspath(self.dirPicker.currentText()),
+                filterRe,
+                excludeHiddenDirs=self.excludeHiddenCheckBox.isChecked(),
+                excludeHiddenFiles=self.excludeHiddenCheckBox.isChecked(),
+            )
+        elif self.openFilesButton.isChecked():
+            vm = ericApp().getObject("ViewManager")
+            vm.checkAllDirty()
+            files = vm.getOpenFilenames()
+        
+        self.findList.clear()
+        QApplication.processEvents()
+        QApplication.processEvents()
+        self.findProgress.setMaximum(len(files))
+        
+        # retrieve the values
+        reg = self.regexpCheckBox.isChecked()
+        wo = self.wordCheckBox.isChecked()
+        cs = self.caseCheckBox.isChecked()
+        ct = self.findtextCombo.currentText()
+        txt = ct if reg else re.escape(ct)
+        if wo:
+            txt = "\\b{0}\\b".format(txt)
+        flags = re.UNICODE
+        if not cs:
+            flags |= re.IGNORECASE
+        try:
+            search = re.compile(txt, flags)
+        except re.error as why:
+            EricMessageBox.critical(
+                self,
+                self.tr("Invalid search expression"),
+                self.tr("""<p>The search expression is not valid.</p>"""
+                        """<p>Error: {0}</p>""").format(str(why)))
+            self.stopButton.setEnabled(False)
+            self.findButton.setEnabled(True)
+            return
+        # reset the findtextCombo
+        if ct in self.searchHistory:
+            self.searchHistory.remove(ct)
+        self.searchHistory.insert(0, ct)
+        self.findtextCombo.clear()
+        self.findtextCombo.addItems(self.searchHistory)
+        Preferences.Prefs.settings.setValue(
+            "FindFileWidget/SearchHistory",
+            self.searchHistory[:30])
+        Preferences.Prefs.settings.setValue(
+            "FindFileWidget/ExcludeHidden",
+            self.excludeHiddenCheckBox.isChecked())
+        
+        if self.__replaceMode:
+            replTxt = self.replacetextCombo.currentText()
+            if replTxt in self.replaceHistory:
+                self.replaceHistory.remove(replTxt)
+            self.replaceHistory.insert(0, replTxt)
+            self.replacetextCombo.clear()
+            self.replacetextCombo.addItems(self.replaceHistory)
+            Preferences.Prefs.settings.setValue(
+                "FindFileWidget/ReplaceHistory",
+                self.replaceHistory[:30])
+        
+        if self.dirButton.isChecked():
+            searchDir = self.dirPicker.currentText()
+            if searchDir in self.dirHistory:
+                self.dirHistory.remove(searchDir)
+            self.dirHistory.insert(0, searchDir)
+            self.dirPicker.clear()
+            self.dirPicker.addItems(self.dirHistory)
+            self.dirPicker.setText(self.dirHistory[0])
+            Preferences.Prefs.settings.setValue(
+                "FindFileWidget/DirectoryHistory",
+                self.dirHistory[:30])
+        
+        # set the button states
+        self.stopButton.setEnabled(True)
+        self.findButton.setEnabled(False)
+        
+        # now go through all the files
+        self.__populating = True
+        self.findList.setUpdatesEnabled(False)
+        occurrences = 0
+        fileOccurrences = 0
+        for progress, file in enumerate(files, start=1):
+            self.__lastFileItem = None
+            found = False
+            if self.__cancelSearch:
+                break
+            
+            self.findProgressLabel.setPath(file)
+            
+            fn = (
+                os.path.join(self.project.ppath, file)
+                if self.projectButton.isChecked() else
+                file
+            )
+            # read the file and split it into textlines
+            try:
+                text, encoding, hashStr = Utilities.readEncodedFileWithHash(fn)
+                lines = text.splitlines(True)
+            except (UnicodeError, OSError):
+                self.findProgress.setValue(progress)
+                continue
+            
+            # now perform the search and display the lines found
+            for count, line in enumerate(lines, start=1):
+                if self.__cancelSearch:
+                    break
+                
+                contains = search.search(line)
+                if contains:
+                    occurrences += 1
+                    found = True
+                    start = contains.start()
+                    end = contains.end()
+                    if self.__replaceMode:
+                        rline = search.sub(replTxt, line)
+                    else:
+                        rline = ""
+                    line = self.__stripEol(line)
+                    if len(line) > 1024:
+                        line = "{0} ...".format(line[:1024])
+                    if self.__replaceMode:
+                        if len(rline) > 1024:
+                            rline = "{0} ...".format(line[:1024])
+                        line = "- {0}\n+ {1}".format(
+                            line, self.__stripEol(rline))
+                    self.__createItem(file, count, line, start, end,
+                                      rline, hashStr)
+                
+                QApplication.processEvents()
+            
+            if found:
+                fileOccurrences += 1
+            self.findProgress.setValue(progress)
+        
+        if not files:
+            self.findProgress.setMaximum(1)
+            self.findProgress.setValue(1)
+        
+        resultFormat = self.tr("{0} / {1}", "occurrences / files")
+        self.findProgressLabel.setPath(resultFormat.format(
+            self.tr("%n occurrence(s)", "", occurrences),
+            self.tr("%n file(s)", "", fileOccurrences)))
+        
+        self.findList.setUpdatesEnabled(True)
+        self.findList.sortItems(self.findList.sortColumn(),
+                                self.findList.header().sortIndicatorOrder())
+        self.findList.resizeColumnToContents(1)
+        if self.__replaceMode:
+            self.findList.header().resizeSection(0, self.__section0Size + 30)
+        self.findList.header().setStretchLastSection(True)
+        self.__populating = False
+        
+        self.stopButton.setEnabled(False)
+        self.findButton.setEnabled(True)
+    
+    @pyqtSlot(QTreeWidgetItem, int)
+    def on_findList_itemDoubleClicked(self, itm, column):
+        """
+        Private slot to handle the double click on a file item.
+        
+        It emits the signal sourceFile or designerFile depending on the file
+        extension.
+        
+        @param itm the double clicked tree item
+        @type QTreeWidgetItem
+        @param column column that was double clicked (ignored)
+        @type int
+        """
+        if itm.parent():
+            file = itm.parent().text(0)
+            line = itm.data(0, self.lineRole)
+            start = itm.data(0, self.startRole)
+            end = itm.data(0, self.endRole)
+        else:
+            file = itm.text(0)
+            line = 1
+            start = 0
+            end = 0
+        
+        fn = os.path.join(self.project.ppath, file) if self.project else file
+        if fn.endswith('.ui'):
+            self.designerFile.emit(fn)
+        else:
+            self.sourceFile.emit(fn, line, "", start, end)
+    
+    def __getFileList(self, path, filterRe, excludeHiddenDirs=False,
+                      excludeHiddenFiles=False):
+        """
+        Private method to get a list of files to search.
+        
+        @param path the root directory to search in
+        @type str
+        @param filterRe regular expression defining the filter
+            criteria
+        @type regexp object
+        @param excludeHiddenDirs flag indicating to exclude hidden directories
+        @type bool
+        @param excludeHiddenFiles flag indicating to exclude hidden files
+        @type bool
+        @return list of files to be processed
+        @rtype list of str
+        """
+        path = os.path.abspath(path)
+        files = []
+        for dirname, dirs, filenames in os.walk(path):
+            files.extend([
+                os.path.join(dirname, f) for f in filenames
+                if (not (excludeHiddenFiles and f.startswith(".")) and
+                    re.match(filterRe, f))
+            ])
+            if excludeHiddenDirs:
+                for d in dirs[:]:
+                    if d .startswith("."):
+                        dirs.remove(d)
+        return files
+    
+    def setSearchDirectory(self, searchDir):
+        """
+        Public slot to set the name of the directory to search in.
+        
+        @param searchDir name of the directory to search in
+        @type str
+        """
+        self.dirButton.setChecked(True)
+        self.dirPicker.setEditText(Utilities.toNativeSeparators(searchDir))
+    
+    @pyqtSlot()
+    def setOpenFiles(self):
+        """
+        Public slot to set the mode to search in open files.
+        """
+        self.openFilesButton.setChecked(True)
+    
+    @pyqtSlot()
+    def on_replaceButton_clicked(self):
+        """
+        Private slot to perform the requested replace actions.
+        """
+        self.findProgress.setMaximum(self.findList.topLevelItemCount())
+        self.findProgress.setValue(0)
+        
+        for index in range(self.findList.topLevelItemCount()):
+            itm = self.findList.topLevelItem(index)
+            if itm.checkState(0) in [Qt.CheckState.PartiallyChecked,
+                                     Qt.CheckState.Checked]:
+                file = itm.text(0)
+                origHash = itm.data(0, self.md5Role)
+                
+                self.findProgressLabel.setPath(file)
+                
+                if self.projectButton.isChecked():
+                    fn = os.path.join(self.project.ppath, file)
+                else:
+                    fn = file
+                
+                # read the file and split it into textlines
+                try:
+                    text, encoding, hashStr = (
+                        Utilities.readEncodedFileWithHash(fn)
+                    )
+                    lines = text.splitlines(True)
+                except (UnicodeError, OSError) as err:
+                    EricMessageBox.critical(
+                        self,
+                        self.tr("Replace in Files"),
+                        self.tr(
+                            """<p>Could not read the file <b>{0}</b>."""
+                            """ Skipping it.</p><p>Reason: {1}</p>""")
+                        .format(fn, str(err))
+                    )
+                    self.findProgress.setValue(index)
+                    continue
+                
+                # Check the original and the current hash. Skip the file,
+                # if hashes are different.
+                if origHash != hashStr:
+                    EricMessageBox.critical(
+                        self,
+                        self.tr("Replace in Files"),
+                        self.tr(
+                            """<p>The current and the original hash of the"""
+                            """ file <b>{0}</b> are different. Skipping it."""
+                            """</p><p>Hash 1: {1}</p><p>Hash 2: {2}</p>""")
+                        .format(fn, origHash, hashStr)
+                    )
+                    self.findProgress.setValue(index)
+                    continue
+                
+                # replace the lines authorized by the user
+                for cindex in range(itm.childCount()):
+                    citm = itm.child(cindex)
+                    if citm.checkState(0) == Qt.CheckState.Checked:
+                        line = citm.data(0, self.lineRole)
+                        rline = citm.data(0, self.replaceRole)
+                        lines[line - 1] = rline
+                
+                # write the file
+                txt = "".join(lines)
+                try:
+                    Utilities.writeEncodedFile(fn, txt, encoding)
+                except (OSError, Utilities.CodingError, UnicodeError) as err:
+                    EricMessageBox.critical(
+                        self,
+                        self.tr("Replace in Files"),
+                        self.tr(
+                            """<p>Could not save the file <b>{0}</b>."""
+                            """ Skipping it.</p><p>Reason: {1}</p>""")
+                        .format(fn, str(err))
+                    )
+            
+            self.findProgress.setValue(index + 1)
+        
+        self.findProgressLabel.setPath("")
+        
+        self.findList.clear()
+        self.replaceButton.setEnabled(False)
+        self.findButton.setEnabled(True)
+    
+    @pyqtSlot(QPoint)
+    def __contextMenuRequested(self, pos):
+        """
+        Private slot to handle the context menu request.
+        
+        @param pos position the context menu shall be shown
+        @type QPoint
+        """
+        menu = QMenu(self)
+        
+        menu.addAction(self.tr("Open"), self.__openFile)
+        menu.addAction(self.tr("Copy Path to Clipboard"),
+                       self.__copyToClipboard)
+        
+        menu.exec(QCursor.pos())
+    
+    @pyqtSlot()
+    def __openFile(self):
+        """
+        Private slot to open the currently selected entry.
+        """
+        itm = self.findList.selectedItems()[0]
+        self.on_findList_itemDoubleClicked(itm, 0)
+    
+    @pyqtSlot()
+    def __copyToClipboard(self):
+        """
+        Private slot to copy the path of an entry to the clipboard.
+        """
+        itm = self.findList.selectedItems()[0]
+        fn = itm.parent().text(0) if itm.parent() else itm.text(0)
+        
+        cb = QApplication.clipboard()
+        cb.setText(fn)

eric ide

mercurial