eric7/Project/ProjectBaseBrowser.py

branch
eric7
changeset 8312
800c432b34c8
parent 8259
2bbec88047dd
child 8318
962bce857696
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eric7/Project/ProjectBaseBrowser.py	Sat May 15 18:45:04 2021 +0200
@@ -0,0 +1,773 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2002 - 2021 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing the baseclass for the various project browsers.
+"""
+
+import os
+import contextlib
+
+from PyQt5.QtCore import (
+    QModelIndex, pyqtSignal, Qt, QCoreApplication, QItemSelectionModel,
+    QItemSelection, QElapsedTimer
+)
+from PyQt5.QtWidgets import (
+    QTreeView, QApplication, QMenu, QDialog, QAbstractItemView
+)
+
+from E5Gui.E5Application import e5App
+from E5Gui import E5MessageBox
+from E5Gui.E5OverrideCursor import E5OverrideCursor
+
+from UI.Browser import Browser
+from UI.BrowserModel import BrowserDirectoryItem, BrowserFileItem
+
+from .ProjectBrowserModel import (
+    ProjectBrowserSimpleDirectoryItem, ProjectBrowserDirectoryItem,
+    ProjectBrowserFileItem
+)
+from .ProjectBrowserSortFilterProxyModel import (
+    ProjectBrowserSortFilterProxyModel
+)
+
+
+class ProjectBaseBrowser(Browser):
+    """
+    Baseclass implementing common functionality for the various project
+    browsers.
+    
+    @signal closeSourceWindow(str) emitted to close a source file
+    """
+    closeSourceWindow = pyqtSignal(str)
+    
+    def __init__(self, project, type_, parent=None):
+        """
+        Constructor
+        
+        @param project reference to the project object
+        @param type_ project browser type (string)
+        @param parent parent widget of this browser
+        """
+        QTreeView.__init__(self, parent)
+        
+        self.project = project
+        
+        self._model = project.getModel()
+        self._sortModel = ProjectBrowserSortFilterProxyModel(type_)
+        self._sortModel.setSourceModel(self._model)
+        self.setModel(self._sortModel)
+        
+        self.selectedItemsFilter = [ProjectBrowserFileItem]
+        
+        # contains codes for special menu entries
+        # 1 = specials for Others browser
+        self.specialMenuEntries = []
+        self.isTranslationsBrowser = False
+        self.expandedNames = []
+        
+        self.SelectFlags = QItemSelectionModel.SelectionFlags(
+            QItemSelectionModel.SelectionFlag.Select |
+            QItemSelectionModel.SelectionFlag.Rows
+        )
+        self.DeselectFlags = QItemSelectionModel.SelectionFlags(
+            QItemSelectionModel.SelectionFlag.Deselect |
+            QItemSelectionModel.SelectionFlag.Rows
+        )
+        
+        self._activating = False
+        
+        self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
+        self.customContextMenuRequested.connect(self._contextMenuRequested)
+        self.activated.connect(self._openItem)
+        self._model.rowsInserted.connect(self.__modelRowsInserted)
+        self._connectExpandedCollapsed()
+        
+        self._createPopupMenus()
+        
+        self.currentItemName = None
+        
+        self._init()    # perform common initialization tasks
+        
+        self._keyboardSearchString = ""
+        self._keyboardSearchTimer = QElapsedTimer()
+        self._keyboardSearchTimer.invalidate()
+        
+        self._initHookMethods()     # perform initialization of the hooks
+        self.hooksMenuEntries = {}
+        
+    def _connectExpandedCollapsed(self):
+        """
+        Protected method to connect the expanded and collapsed signals.
+        """
+        self.expanded.connect(self._resizeColumns)
+        self.collapsed.connect(self._resizeColumns)
+        
+    def _disconnectExpandedCollapsed(self):
+        """
+        Protected method to disconnect the expanded and collapsed signals.
+        """
+        self.expanded.disconnect(self._resizeColumns)
+        self.collapsed.disconnect(self._resizeColumns)
+        
+    def _createPopupMenus(self):
+        """
+        Protected overloaded method to generate the popup menus.
+        """
+        # create the popup menu for source files
+        self.sourceMenu = QMenu(self)
+        self.sourceMenu.addAction(
+            QCoreApplication.translate('ProjectBaseBrowser', 'Open'),
+            self._openItem)
+        
+        # create the popup menu for general use
+        self.menu = QMenu(self)
+        self.menu.addAction(
+            QCoreApplication.translate('ProjectBaseBrowser', 'Open'),
+            self._openItem)
+
+        # create the menu for multiple selected files
+        self.multiMenu = QMenu(self)
+        self.multiMenu.addAction(
+            QCoreApplication.translate('ProjectBaseBrowser', 'Open'),
+            self._openItem)
+        
+        # create the background menu
+        self.backMenu = None
+        
+        # create the directories menu
+        self.dirMenu = None
+        
+        # create the directory for multiple selected directories
+        self.dirMultiMenu = None
+        
+        self.menuActions = []
+        self.multiMenuActions = []
+        self.dirMenuActions = []
+        self.dirMultiMenuActions = []
+        
+        self.mainMenu = None
+        
+    def _contextMenuRequested(self, coord):
+        """
+        Protected slot to show the context menu.
+        
+        @param coord the position of the mouse pointer (QPoint)
+        """
+        if not self.project.isOpen():
+            return
+        
+        cnt = self.getSelectedItemsCount()
+        if cnt > 1:
+            self.multiMenu.popup(self.mapToGlobal(coord))
+        else:
+            index = self.indexAt(coord)
+            
+            if index.isValid():
+                self.menu.popup(self.mapToGlobal(coord))
+            else:
+                self.backMenu and self.backMenu.popup(self.mapToGlobal(coord))
+        
+    def _selectSingleItem(self, index):
+        """
+        Protected method to select a single item.
+        
+        @param index index of item to be selected (QModelIndex)
+        """
+        if index.isValid():
+            self.setCurrentIndex(index)
+            flags = QItemSelectionModel.SelectionFlags(
+                QItemSelectionModel.SelectionFlag.ClearAndSelect |
+                QItemSelectionModel.SelectionFlag.Rows
+            )
+            self.selectionModel().select(index, flags)
+        
+    def _setItemSelected(self, index, selected):
+        """
+        Protected method to set the selection status of an item.
+        
+        @param index index of item to set (QModelIndex)
+        @param selected flag giving the new selection status (boolean)
+        """
+        if index.isValid():
+            self.selectionModel().select(
+                index, selected and self.SelectFlags or self.DeselectFlags)
+        
+    def _setItemRangeSelected(self, startIndex, endIndex, selected):
+        """
+        Protected method to set the selection status of a range of items.
+        
+        @param startIndex start index of range of items to set (QModelIndex)
+        @param endIndex end index of range of items to set (QModelIndex)
+        @param selected flag giving the new selection status (boolean)
+        """
+        selection = QItemSelection(startIndex, endIndex)
+        self.selectionModel().select(
+            selection, selected and self.SelectFlags or self.DeselectFlags)
+        
+    def __modelRowsInserted(self, parent, start, end):
+        """
+        Private slot called after rows have been inserted into the model.
+        
+        @param parent parent index of inserted rows (QModelIndex)
+        @param start start row number (integer)
+        @param end end row number (integer)
+        """
+        self._resizeColumns(parent)
+        
+    def _projectClosed(self):
+        """
+        Protected slot to handle the projectClosed signal.
+        """
+        self.layoutDisplay()
+        if self.backMenu is not None:
+            self.backMenu.setEnabled(False)
+        
+        self._createPopupMenus()
+        
+    def _projectOpened(self):
+        """
+        Protected slot to handle the projectOpened signal.
+        """
+        self.layoutDisplay()
+        self.sortByColumn(0, Qt.SortOrder.DescendingOrder)
+        self.sortByColumn(0, Qt.SortOrder.AscendingOrder)
+        self._initMenusAndVcs()
+        
+    def _initMenusAndVcs(self):
+        """
+        Protected slot to initialize the menus and the Vcs interface.
+        """
+        self._createPopupMenus()
+        
+        if self.backMenu is not None:
+            self.backMenu.setEnabled(True)
+        
+        if self.project.vcs is not None:
+            self.vcsHelper = self.project.vcs.vcsGetProjectBrowserHelper(
+                self, self.project, self.isTranslationsBrowser)
+            self.vcsHelper.addVCSMenus(
+                self.mainMenu, self.multiMenu, self.backMenu,
+                self.dirMenu, self.dirMultiMenu)
+    
+    def _newProject(self):
+        """
+        Protected slot to handle the newProject signal.
+        """
+        # default to perform same actions as opening a project
+        self._projectOpened()
+        
+    def _removeFile(self):
+        """
+        Protected method to remove a file or files from the project.
+        """
+        itmList = self.getSelectedItems()
+        
+        for itm in itmList[:]:
+            fn = itm.fileName()
+            self.closeSourceWindow.emit(fn)
+            self.project.removeFile(fn)
+        
+    def _removeDir(self):
+        """
+        Protected method to remove a (single) directory from the project.
+        """
+        itmList = self.getSelectedItems(
+            [ProjectBrowserSimpleDirectoryItem, ProjectBrowserDirectoryItem])
+        for itm in itmList[:]:
+            dn = itm.dirName()
+            self.project.removeDirectory(dn)
+        
+    def _deleteDirectory(self):
+        """
+        Protected method to delete the selected directory from the project
+        data area.
+        """
+        itmList = self.getSelectedItems()
+        
+        dirs = []
+        fullNames = []
+        for itm in itmList:
+            dn = itm.dirName()
+            fullNames.append(dn)
+            dn = self.project.getRelativePath(dn)
+            dirs.append(dn)
+        
+        from UI.DeleteFilesConfirmationDialog import (
+            DeleteFilesConfirmationDialog
+        )
+        dlg = DeleteFilesConfirmationDialog(
+            self.parent(),
+            QCoreApplication.translate(
+                "ProjectBaseBrowser", "Delete directories"),
+            QCoreApplication.translate(
+                "ProjectBaseBrowser",
+                "Do you really want to delete these directories from"
+                " the project?"),
+            dirs)
+        
+        if dlg.exec() == QDialog.DialogCode.Accepted:
+            for dn in fullNames:
+                self.project.deleteDirectory(dn)
+        
+    def _renameFile(self):
+        """
+        Protected method to rename a file of the project.
+        """
+        itm = self.model().item(self.currentIndex())
+        fn = itm.fileName()
+        self.project.renameFile(fn)
+        
+    def _copyToClipboard(self):
+        """
+        Protected method to copy the path of an entry to the clipboard.
+        """
+        itm = self.model().item(self.currentIndex())
+        try:
+            fn = itm.fileName()
+        except AttributeError:
+            try:
+                fn = itm.dirName()
+            except AttributeError:
+                fn = ""
+        
+        cb = QApplication.clipboard()
+        cb.setText(fn)
+        
+    def selectFile(self, fn):
+        """
+        Public method to highlight a node given its filename.
+        
+        @param fn filename of file to be highlighted (string)
+        """
+        newfn = os.path.abspath(fn)
+        newfn = self.project.getRelativePath(newfn)
+        sindex = self._model.itemIndexByName(newfn)
+        if sindex.isValid():
+            index = self.model().mapFromSource(sindex)
+            if index.isValid():
+                self._selectSingleItem(index)
+                self.scrollTo(index,
+                              QAbstractItemView.ScrollHint.PositionAtTop)
+        
+    def selectFileLine(self, fn, lineno):
+        """
+        Public method to highlight a node given its filename.
+        
+        @param fn filename of file to be highlighted (string)
+        @param lineno one based line number of the item (integer)
+        """
+        newfn = os.path.abspath(fn)
+        newfn = self.project.getRelativePath(newfn)
+        sindex = self._model.itemIndexByNameAndLine(newfn, lineno)
+        if sindex.isValid():
+            index = self.model().mapFromSource(sindex)
+            if index.isValid():
+                self._selectSingleItem(index)
+                self.scrollTo(index)
+        
+    def _expandAllDirs(self):
+        """
+        Protected slot to handle the 'Expand all directories' menu action.
+        """
+        self._disconnectExpandedCollapsed()
+        with E5OverrideCursor():
+            index = self.model().index(0, 0)
+            while index.isValid():
+                itm = self.model().item(index)
+                if (
+                    isinstance(
+                        itm,
+                        (ProjectBrowserSimpleDirectoryItem,
+                         ProjectBrowserDirectoryItem)) and
+                    not self.isExpanded(index)
+                ):
+                    self.expand(index)
+                index = self.indexBelow(index)
+            self.layoutDisplay()
+        self._connectExpandedCollapsed()
+        
+    def _collapseAllDirs(self):
+        """
+        Protected slot to handle the 'Collapse all directories' menu action.
+        """
+        self._disconnectExpandedCollapsed()
+        with E5OverrideCursor():
+            # step 1: find last valid index
+            vindex = QModelIndex()
+            index = self.model().index(0, 0)
+            while index.isValid():
+                vindex = index
+                index = self.indexBelow(index)
+            
+            # step 2: go up collapsing all directory items
+            index = vindex
+            while index.isValid():
+                itm = self.model().item(index)
+                if (
+                    isinstance(
+                        itm,
+                        (ProjectBrowserSimpleDirectoryItem,
+                         ProjectBrowserDirectoryItem)) and
+                    self.isExpanded(index)
+                ):
+                    self.collapse(index)
+                index = self.indexAbove(index)
+            self.layoutDisplay()
+        self._connectExpandedCollapsed()
+        
+    def _showContextMenu(self, menu):
+        """
+        Protected slot called before the context menu is shown.
+        
+        It enables/disables the VCS menu entries depending on the overall
+        VCS status and the file status.
+        
+        @param menu reference to the menu to be shown (QMenu)
+        """
+        if self.project.vcs is None:
+            for act in self.menuActions:
+                act.setEnabled(True)
+        else:
+            self.vcsHelper.showContextMenu(menu, self.menuActions)
+        
+    def _showContextMenuMulti(self, menu):
+        """
+        Protected slot called before the context menu (multiple selections) is
+        shown.
+        
+        It enables/disables the VCS menu entries depending on the overall
+        VCS status and the files status.
+        
+        @param menu reference to the menu to be shown (QMenu)
+        """
+        if self.project.vcs is None:
+            for act in self.multiMenuActions:
+                act.setEnabled(True)
+        else:
+            self.vcsHelper.showContextMenuMulti(menu, self.multiMenuActions)
+        
+    def _showContextMenuDir(self, menu):
+        """
+        Protected slot called before the context menu is shown.
+        
+        It enables/disables the VCS menu entries depending on the overall
+        VCS status and the directory status.
+        
+        @param menu reference to the menu to be shown (QMenu)
+        """
+        if self.project.vcs is None:
+            for act in self.dirMenuActions:
+                act.setEnabled(True)
+        else:
+            self.vcsHelper.showContextMenuDir(menu, self.dirMenuActions)
+        
+    def _showContextMenuDirMulti(self, menu):
+        """
+        Protected slot called before the context menu is shown.
+        
+        It enables/disables the VCS menu entries depending on the overall
+        VCS status and the directory status.
+        
+        @param menu reference to the menu to be shown (QMenu)
+        """
+        if self.project.vcs is None:
+            for act in self.dirMultiMenuActions:
+                act.setEnabled(True)
+        else:
+            self.vcsHelper.showContextMenuDirMulti(
+                menu, self.dirMultiMenuActions)
+        
+    def _showContextMenuBack(self, menu):
+        """
+        Protected slot called before the context menu is shown.
+        
+        @param menu reference to the menu to be shown (QMenu)
+        """
+        # nothing to do for now
+        return
+        
+    def _selectEntries(self, local=True, filterList=None):
+        """
+        Protected method to select entries based on their VCS status.
+        
+        @param local flag indicating local (i.e. non VCS controlled)
+            file/directory entries should be selected (boolean)
+        @param filterList list of classes to check against
+        """
+        if self.project.vcs is None:
+            return
+        
+        compareString = (
+            QCoreApplication.translate('ProjectBaseBrowser', "local")
+            if local else
+            self.project.vcs.vcsName()
+        )
+        
+        # expand all directories in order to iterate over all entries
+        self._expandAllDirs()
+        
+        self.selectionModel().clear()
+        
+        with E5OverrideCursor():
+            # now iterate over all entries
+            startIndex = None
+            endIndex = None
+            selectedEntries = 0
+            index = self.model().index(0, 0)
+            while index.isValid():
+                itm = self.model().item(index)
+                if (
+                    self.wantedItem(itm, filterList) and
+                    compareString == itm.data(1)
+                ):
+                    if (
+                        startIndex is not None and
+                        startIndex.parent() != index.parent()
+                    ):
+                        self._setItemRangeSelected(startIndex, endIndex, True)
+                        startIndex = None
+                    selectedEntries += 1
+                    if startIndex is None:
+                        startIndex = index
+                    endIndex = index
+                else:
+                    if startIndex is not None:
+                        self._setItemRangeSelected(startIndex, endIndex, True)
+                        startIndex = None
+                index = self.indexBelow(index)
+            if startIndex is not None:
+                self._setItemRangeSelected(startIndex, endIndex, True)
+        
+        if selectedEntries == 0:
+            E5MessageBox.information(
+                self,
+                QCoreApplication.translate(
+                    'ProjectBaseBrowser', "Select entries"),
+                QCoreApplication.translate(
+                    'ProjectBaseBrowser',
+                    """There were no matching entries found."""))
+        
+    def selectLocalEntries(self):
+        """
+        Public slot to handle the select local files context menu entries.
+        """
+        self._selectEntries(local=True, filterList=[ProjectBrowserFileItem])
+        
+    def selectVCSEntries(self):
+        """
+        Public slot to handle the select VCS files context menu entries.
+        """
+        self._selectEntries(local=False, filterList=[ProjectBrowserFileItem])
+        
+    def selectLocalDirEntries(self):
+        """
+        Public slot to handle the select local directories context menu
+        entries.
+        """
+        self._selectEntries(
+            local=True,
+            filterList=[ProjectBrowserSimpleDirectoryItem,
+                        ProjectBrowserDirectoryItem])
+        
+    def selectVCSDirEntries(self):
+        """
+        Public slot to handle the select VCS directories context menu entries.
+        """
+        self._selectEntries(
+            local=False,
+            filterList=[ProjectBrowserSimpleDirectoryItem,
+                        ProjectBrowserDirectoryItem])
+        
+    def getExpandedItemNames(self):
+        """
+        Public method to get the file/directory names of all expanded items.
+        
+        @return list of expanded items names (list of string)
+        """
+        expandedNames = []
+        
+        childIndex = self.model().index(0, 0)
+        while childIndex.isValid():
+            if self.isExpanded(childIndex):
+                with contextlib.suppress(AttributeError):
+                    expandedNames.append(
+                        self.model().item(childIndex).name())
+                    # only items defining the name() method are returned
+            childIndex = self.indexBelow(childIndex)
+        
+        return expandedNames
+        
+    def expandItemsByName(self, names):
+        """
+        Public method to expand items given their names.
+        
+        @param names list of item names to be expanded (list of string)
+        """
+        model = self.model()
+        for name in names:
+            childIndex = model.index(0, 0)
+            while childIndex.isValid():
+                with contextlib.suppress(AttributeError):
+                    if model.item(childIndex).name() == name:
+                        self.setExpanded(childIndex, True)
+                        break
+                    # ignore items not supporting this method
+                childIndex = self.indexBelow(childIndex)
+        
+    def _prepareRepopulateItem(self, name):
+        """
+        Protected slot to handle the prepareRepopulateItem signal.
+        
+        @param name relative name of file item to be repopulated (string)
+        """
+        itm = self.currentItem()
+        if itm is not None:
+            self.currentItemName = itm.data(0)
+        self.expandedNames = []
+        sindex = self._model.itemIndexByName(name)
+        if not sindex.isValid():
+            return
+        
+        index = self.model().mapFromSource(sindex)
+        if not index.isValid():
+            return
+        
+        childIndex = self.indexBelow(index)
+        while childIndex.isValid():
+            if childIndex.parent() == index.parent():
+                break
+            if self.isExpanded(childIndex):
+                self.expandedNames.append(
+                    self.model().item(childIndex).data(0))
+            childIndex = self.indexBelow(childIndex)
+        
+    def _completeRepopulateItem(self, name):
+        """
+        Protected slot to handle the completeRepopulateItem signal.
+        
+        @param name relative name of file item to be repopulated (string)
+        """
+        sindex = self._model.itemIndexByName(name)
+        if sindex.isValid():
+            index = self.model().mapFromSource(sindex)
+            if index.isValid():
+                if self.isExpanded(index):
+                    childIndex = self.indexBelow(index)
+                    while childIndex.isValid():
+                        if (
+                            not childIndex.isValid() or
+                            childIndex.parent() == index.parent()
+                        ):
+                            break
+                        itm = self.model().item(childIndex)
+                        if itm is not None:
+                            itemData = itm.data(0)
+                            if (
+                                self.currentItemName and
+                                self.currentItemName == itemData
+                            ):
+                                self._selectSingleItem(childIndex)
+                            if itemData in self.expandedNames:
+                                self.setExpanded(childIndex, True)
+                        childIndex = self.indexBelow(childIndex)
+                else:
+                    self._selectSingleItem(index)
+                self.expandedNames = []
+        self.currentItemName = None
+        self._resort()
+        
+    def currentItem(self):
+        """
+        Public method to get a reference to the current item.
+        
+        @return reference to the current item
+        """
+        itm = self.model().item(self.currentIndex())
+        return itm
+    
+    def _keyboardSearchType(self, item):
+        """
+        Protected method to check, if the item is of the correct type.
+        
+        @param item reference to the item
+        @type BrowserItem
+        @return flag indicating a correct type
+        @rtype bool
+        """
+        return isinstance(
+            item, (BrowserDirectoryItem, BrowserFileItem,
+                   ProjectBrowserSimpleDirectoryItem,
+                   ProjectBrowserDirectoryItem, ProjectBrowserFileItem))
+    
+    ###########################################################################
+    ## Support for hooks below
+    ###########################################################################
+    
+    def _initHookMethods(self):
+        """
+        Protected method to initialize the hooks dictionary.
+        
+        This method should be overridden by subclasses. All supported
+        hook methods should be initialized with a None value. The keys
+        must be strings.
+        """
+        self.hooks = {}
+        
+    def __checkHookKey(self, key):
+        """
+        Private method to check a hook key.
+        
+        @param key key of the hook to check (string)
+        @exception KeyError raised to indicate an invalid hook
+        """
+        if len(self.hooks) == 0:
+            raise KeyError("Hooks are not initialized.")
+        
+        if key not in self.hooks:
+            raise KeyError(key)
+        
+    def addHookMethod(self, key, method):
+        """
+        Public method to add a hook method to the dictionary.
+        
+        @param key for the hook method (string)
+        @param method reference to the hook method (method object)
+        """
+        self.__checkHookKey(key)
+        self.hooks[key] = method
+        
+    def addHookMethodAndMenuEntry(self, key, method, menuEntry):
+        """
+        Public method to add a hook method to the dictionary.
+        
+        @param key for the hook method (string)
+        @param method reference to the hook method (method object)
+        @param menuEntry entry to be shown in the context menu (string)
+        """
+        self.addHookMethod(key, method)
+        self.hooksMenuEntries[key] = menuEntry
+        
+    def removeHookMethod(self, key):
+        """
+        Public method to remove a hook method from the dictionary.
+        
+        @param key for the hook method (string)
+        """
+        self.__checkHookKey(key)
+        self.hooks[key] = None
+        if key in self.hooksMenuEntries:
+            del self.hooksMenuEntries[key]
+    
+    ##################################################################
+    ## Configure method below
+    ##################################################################
+    
+    def _configure(self):
+        """
+        Protected method to open the configuration dialog.
+        """
+        e5App().getObject("UserInterface").showPreferences(
+            "projectBrowserPage")

eric ide

mercurial