eric6/Project/ProjectBaseBrowser.py

changeset 6942
2602857055c5
parent 6645
ad476851d7e0
child 7229
53054eb5b15a
diff -r f99d60d6b59b -r 2602857055c5 eric6/Project/ProjectBaseBrowser.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eric6/Project/ProjectBaseBrowser.py	Sun Apr 14 15:09:21 2019 +0200
@@ -0,0 +1,760 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2002 - 2019 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing the baseclass for the various project browsers.
+"""
+
+from __future__ import unicode_literals
+
+import os
+
+from PyQt5.QtCore import QModelIndex, pyqtSignal, Qt, QCoreApplication, \
+    QItemSelectionModel, QItemSelection, QElapsedTimer
+from PyQt5.QtGui import QCursor
+from PyQt5.QtWidgets import QTreeView, QApplication, QMenu, QDialog, \
+    QAbstractItemView
+
+from E5Gui.E5Application import e5App
+from E5Gui import E5MessageBox
+
+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.Select | QItemSelectionModel.Rows)
+        self.DeselectFlags = QItemSelectionModel.SelectionFlags(
+            QItemSelectionModel.Deselect | QItemSelectionModel.Rows)
+        
+        self._activating = False
+        
+        self.setContextMenuPolicy(Qt.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.ClearAndSelect | QItemSelectionModel.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.DescendingOrder)
+        self.sortByColumn(0, Qt.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.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.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()
+        QApplication.setOverrideCursor(QCursor(Qt.WaitCursor))
+        QApplication.processEvents()
+        index = self.model().index(0, 0)
+        while index.isValid():
+            itm = self.model().item(index)
+            if (isinstance(itm, ProjectBrowserSimpleDirectoryItem) or
+                isinstance(itm, ProjectBrowserDirectoryItem)) and \
+               not self.isExpanded(index):
+                self.expand(index)
+            index = self.indexBelow(index)
+        self.layoutDisplay()
+        self._connectExpandedCollapsed()
+        QApplication.restoreOverrideCursor()
+        
+    def _collapseAllDirs(self):
+        """
+        Protected slot to handle the 'Collapse all directories' menu action.
+        """
+        self._disconnectExpandedCollapsed()
+        QApplication.setOverrideCursor(QCursor(Qt.WaitCursor))
+        QApplication.processEvents()
+        # 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) or
+                isinstance(itm, ProjectBrowserDirectoryItem)) and \
+               self.isExpanded(index):
+                self.collapse(index)
+            index = self.indexAbove(index)
+        self.layoutDisplay()
+        self._connectExpandedCollapsed()
+        QApplication.restoreOverrideCursor()
+        
+    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
+        
+        if local:
+            compareString = \
+                QCoreApplication.translate('ProjectBaseBrowser', "local")
+        else:
+            compareString = self.project.vcs.vcsName()
+        
+        # expand all directories in order to iterate over all entries
+        self._expandAllDirs()
+        
+        QApplication.setOverrideCursor(QCursor(Qt.WaitCursor))
+        QApplication.processEvents()
+        self.selectionModel().clear()
+        QApplication.processEvents()
+        
+        # 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)
+        QApplication.restoreOverrideCursor()
+        QApplication.processEvents()
+        
+        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):
+                try:
+                    expandedNames.append(
+                        self.model().item(childIndex).name())
+                except AttributeError:
+                    # only items defining the name() method are returned
+                    pass
+            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():
+                try:
+                    if model.item(childIndex).name() == name:
+                        self.setExpanded(childIndex, True)
+                        break
+                except AttributeError:
+                    # ignore items not supporting this method
+                    pass
+                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)
+        """
+        QApplication.setOverrideCursor(QCursor(Qt.WaitCursor))
+        QApplication.processEvents()
+        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
+        QApplication.restoreOverrideCursor()
+        QApplication.processEvents()
+        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