eric7/Project/ProjectBrowserModel.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/ProjectBrowserModel.py	Sat May 15 18:45:04 2021 +0200
@@ -0,0 +1,974 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2006 - 2021 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing the browser model.
+"""
+
+import os
+import re
+import contextlib
+
+from PyQt5.QtCore import QDir, QModelIndex, pyqtSignal, QFileSystemWatcher, Qt
+from PyQt5.QtGui import QColor
+
+from UI.BrowserModel import (
+    BrowserModel, BrowserItem, BrowserDirectoryItem, BrowserFileItem
+)
+
+import UI.PixmapCache
+import Preferences
+import Utilities
+
+import Utilities.ModuleParser
+
+ProjectBrowserItemSimpleDirectory = 100
+ProjectBrowserItemDirectory = 101
+ProjectBrowserItemFile = 102
+
+ProjectBrowserNoType = 0
+ProjectBrowserSourceType = 1
+ProjectBrowserFormType = 2
+ProjectBrowserInterfaceType = 3
+ProjectBrowserTranslationType = 4
+ProjectBrowserOthersType = 5
+ProjectBrowserResourceType = 6
+ProjectBrowserProtocolsType = 7
+
+
+class ProjectBrowserItemMixin:
+    """
+    Class implementing common methods of project browser items.
+    
+    It is meant to be used as a mixin class.
+    """
+    def __init__(self, type_, bold=False):
+        """
+        Constructor
+        
+        @param type_ type of file/directory in the project
+        @param bold flag indicating a highlighted font
+        """
+        self._projectTypes = [type_]
+        self.bold = bold
+        self.vcsState = " "
+    
+    def getTextColor(self):
+        """
+        Public method to get the items text color.
+        
+        @return text color (QColor)
+        """
+        if self.bold:
+            return Preferences.getProjectBrowserColour("Highlighted")
+        else:
+            return None
+    
+    def setVcsState(self, state):
+        """
+        Public method to set the items VCS state.
+        
+        @param state VCS state (one of A, C, M, U or " ") (string)
+        """
+        self.vcsState = state
+    
+    def addVcsStatus(self, vcsStatus):
+        """
+        Public method to add the VCS status.
+        
+        @param vcsStatus VCS status text (string)
+        """
+        self.itemData.append(vcsStatus)
+    
+    def setVcsStatus(self, vcsStatus):
+        """
+        Public method to set the VCS status.
+        
+        @param vcsStatus VCS status text (string)
+        """
+        self.itemData[1] = vcsStatus
+    
+    def getProjectTypes(self):
+        """
+        Public method to get the project type.
+        
+        @return project type
+        """
+        return self._projectTypes[:]
+    
+    def addProjectType(self, type_):
+        """
+        Public method to add a type to the list.
+        
+        @param type_ type to add to the list
+        """
+        self._projectTypes.append(type_)
+
+
+class ProjectBrowserSimpleDirectoryItem(BrowserItem, ProjectBrowserItemMixin):
+    """
+    Class implementing the data structure for project browser simple directory
+    items.
+    """
+    def __init__(self, parent, projectType, text, path=""):
+        """
+        Constructor
+        
+        @param parent parent item
+        @param projectType type of file/directory in the project
+        @param text text to be displayed (string)
+        @param path path of the directory (string)
+        """
+        BrowserItem.__init__(self, parent, text)
+        ProjectBrowserItemMixin.__init__(self, projectType)
+        
+        self._dirName = path
+        if not os.path.isdir(self._dirName):
+            self._dirName = os.path.dirname(self._dirName)
+        
+        self.type_ = ProjectBrowserItemSimpleDirectory
+        if os.path.lexists(self._dirName) and os.path.islink(self._dirName):
+            self.symlink = True
+            self.icon = UI.PixmapCache.getSymlinkIcon("dirClosed")
+        else:
+            self.icon = UI.PixmapCache.getIcon("dirClosed")
+    
+    def setName(self, dinfo, full=True):
+        """
+        Public method to set the directory name.
+        
+        @param dinfo dinfo is the string for the directory (string)
+        @param full flag indicating full pathname should be displayed (boolean)
+        """
+        self._dirName = os.path.abspath(dinfo)
+        self.itemData[0] = os.path.basename(self._dirName)
+    
+    def dirName(self):
+        """
+        Public method returning the directory name.
+        
+        @return directory name (string)
+        """
+        return self._dirName
+    
+    def name(self):
+        """
+        Public method to return the name of the item.
+        
+        @return name of the item (string)
+        """
+        return self._dirName
+    
+    def lessThan(self, other, column, order):
+        """
+        Public method to check, if the item is less than the other one.
+        
+        @param other reference to item to compare against (BrowserItem)
+        @param column column number to use for the comparison (integer)
+        @param order sort order (Qt.SortOrder) (for special sorting)
+        @return true, if this item is less than other (boolean)
+        """
+        if (
+            issubclass(other.__class__, BrowserFileItem) and
+            Preferences.getUI("BrowsersListFoldersFirst")
+        ):
+            return order == Qt.SortOrder.AscendingOrder
+        
+        return BrowserItem.lessThan(self, other, column, order)
+
+
+class ProjectBrowserDirectoryItem(BrowserDirectoryItem,
+                                  ProjectBrowserItemMixin):
+    """
+    Class implementing the data structure for project browser directory items.
+    """
+    def __init__(self, parent, dinfo, projectType, full=True, bold=False):
+        """
+        Constructor
+        
+        @param parent parent item
+        @param dinfo dinfo is the string for the directory (string)
+        @param projectType type of file/directory in the project
+        @param full flag indicating full pathname should be displayed (boolean)
+        @param bold flag indicating a highlighted font (boolean)
+        """
+        BrowserDirectoryItem.__init__(self, parent, dinfo, full)
+        ProjectBrowserItemMixin.__init__(self, projectType, bold)
+        
+        self.type_ = ProjectBrowserItemDirectory
+
+
+class ProjectBrowserFileItem(BrowserFileItem, ProjectBrowserItemMixin):
+    """
+    Class implementing the data structure for project browser file items.
+    """
+    def __init__(self, parent, finfo, projectType, full=True, bold=False,
+                 sourceLanguage=""):
+        """
+        Constructor
+        
+        @param parent parent item
+        @param finfo the string for the file (string)
+        @param projectType type of file/directory in the project
+        @param full flag indicating full pathname should be displayed (boolean)
+        @param bold flag indicating a highlighted font (boolean)
+        @param sourceLanguage source code language of the project (string)
+        """
+        BrowserFileItem.__init__(self, parent, finfo, full, sourceLanguage)
+        ProjectBrowserItemMixin.__init__(self, projectType, bold)
+        
+        self.type_ = ProjectBrowserItemFile
+
+
+class ProjectBrowserModel(BrowserModel):
+    """
+    Class implementing the project browser model.
+    
+    @signal vcsStateChanged(str) emitted after the VCS state has changed
+    """
+    vcsStateChanged = pyqtSignal(str)
+    
+    def __init__(self, parent):
+        """
+        Constructor
+        
+        @param parent reference to parent object (Project.Project)
+        """
+        super().__init__(parent, nopopulate=True)
+        
+        rootData = self.tr("Name")
+        self.rootItem = BrowserItem(None, rootData)
+        self.rootItem.itemData.append(self.tr("VCS Status"))
+        
+        self.progDir = None
+        self.project = parent
+        
+        self.watchedItems = {}
+        self.__watcherActive = True
+        self.watcher = QFileSystemWatcher(self)
+        self.watcher.directoryChanged.connect(self.directoryChanged)
+        
+        self.inRefresh = False
+        
+        self.projectBrowserTypes = {
+            "SOURCES": ProjectBrowserSourceType,
+            "FORMS": ProjectBrowserFormType,
+            "RESOURCES": ProjectBrowserResourceType,
+            "INTERFACES": ProjectBrowserInterfaceType,
+            "PROTOCOLS": ProjectBrowserProtocolsType,
+            "TRANSLATIONS": ProjectBrowserTranslationType,
+            "OTHERS": ProjectBrowserOthersType,
+        }
+        
+        self.colorNames = {
+            "A": "VcsAdded",
+            "M": "VcsModified",
+            "O": "VcsRemoved",
+            "R": "VcsReplaced",
+            "U": "VcsUpdate",
+            "Z": "VcsConflict",
+        }
+        self.itemBackgroundColors = {
+            " ": QColor(),
+            "A": Preferences.getProjectBrowserColour(self.colorNames["A"]),
+            "M": Preferences.getProjectBrowserColour(self.colorNames["M"]),
+            "O": Preferences.getProjectBrowserColour(self.colorNames["O"]),
+            "R": Preferences.getProjectBrowserColour(self.colorNames["R"]),
+            "U": Preferences.getProjectBrowserColour(self.colorNames["U"]),
+            "Z": Preferences.getProjectBrowserColour(self.colorNames["Z"]),
+        }
+        
+        self.highLightColor = Preferences.getProjectBrowserColour(
+            "Highlighted")
+        # needed by preferencesChanged()
+        
+        self.vcsStatusReport = {}
+    
+    def data(self, index, role):
+        """
+        Public method to get data of an item.
+        
+        @param index index of the data to retrieve (QModelIndex)
+        @param role role of data (Qt.ItemDataRole)
+        @return requested data
+        """
+        if not index.isValid():
+            return None
+        
+        if role == Qt.ItemDataRole.TextColorRole:
+            if index.column() == 0:
+                try:
+                    return index.internalPointer().getTextColor()
+                except AttributeError:
+                    return None
+        elif role == Qt.ItemDataRole.BackgroundColorRole:
+            try:
+                col = self.itemBackgroundColors[
+                    index.internalPointer().vcsState]
+                if col.isValid():
+                    return col
+                else:
+                    return None
+            except AttributeError:
+                return None
+            except KeyError:
+                return None
+        
+        return BrowserModel.data(self, index, role)
+    
+    def populateItem(self, parentItem, repopulate=False):
+        """
+        Public method to populate an item's subtree.
+        
+        @param parentItem reference to the item to be populated
+        @param repopulate flag indicating a repopulation (boolean)
+        """
+        if parentItem.type() == ProjectBrowserItemSimpleDirectory:
+            return  # nothing to do
+        elif parentItem.type() == ProjectBrowserItemDirectory:
+            self.populateProjectDirectoryItem(parentItem, repopulate)
+        elif parentItem.type() == ProjectBrowserItemFile:
+            self.populateFileItem(parentItem, repopulate)
+        else:
+            BrowserModel.populateItem(self, parentItem, repopulate)
+
+    def populateProjectDirectoryItem(self, parentItem, repopulate=False):
+        """
+        Public method to populate a directory item's subtree.
+        
+        @param parentItem reference to the directory item to be populated
+        @param repopulate flag indicating a repopulation (boolean)
+        """
+        self._addWatchedItem(parentItem)
+        
+        qdir = QDir(parentItem.dirName())
+        
+        fileFilter = (
+            QDir.Filters(
+                QDir.Filter.AllEntries |
+                QDir.Filter.Hidden |
+                QDir.Filter.NoDotAndDotDot
+            )
+            if Preferences.getProject("BrowsersListHiddenFiles") else
+            QDir.Filters(QDir.Filter.AllEntries | QDir.Filter.NoDotAndDotDot)
+        )
+        entryInfoList = qdir.entryInfoList(fileFilter)
+        
+        if len(entryInfoList) > 0:
+            if repopulate:
+                self.beginInsertRows(self.createIndex(
+                    parentItem.row(), 0, parentItem),
+                    0, len(entryInfoList) - 1)
+            states = {}
+            if self.project.vcs is not None:
+                for f in entryInfoList:
+                    fname = f.absoluteFilePath()
+                    states[os.path.normcase(fname)] = 0
+                dname = parentItem.dirName()
+                self.project.vcs.clearStatusCache()
+                states = self.project.vcs.vcsAllRegisteredStates(states, dname)
+            
+            for f in entryInfoList:
+                node = (
+                    ProjectBrowserDirectoryItem(
+                        parentItem,
+                        Utilities.toNativeSeparators(f.absoluteFilePath()),
+                        parentItem.getProjectTypes()[0], False)
+                    if f.isDir() else
+                    ProjectBrowserFileItem(
+                        parentItem,
+                        Utilities.toNativeSeparators(f.absoluteFilePath()),
+                        parentItem.getProjectTypes()[0])
+                )
+                if self.project.vcs is not None:
+                    fname = f.absoluteFilePath()
+                    if (
+                        states[os.path.normcase(fname)] ==
+                            self.project.vcs.canBeCommitted
+                    ):
+                        node.addVcsStatus(self.project.vcs.vcsName())
+                        self.project.clearStatusMonitorCachedState(
+                            f.absoluteFilePath())
+                    else:
+                        node.addVcsStatus(self.tr("local"))
+                else:
+                    node.addVcsStatus("")
+                self._addItem(node, parentItem)
+            if repopulate:
+                self.endInsertRows()
+
+    def projectClosed(self):
+        """
+        Public method called after a project has been closed.
+        """
+        self.__vcsStatus = {}
+        
+        self.watchedItems = {}
+        watchedDirs = self.watcher.directories()
+        if watchedDirs:
+            self.watcher.removePaths(watchedDirs)
+        
+        self.rootItem.removeChildren()
+        self.beginResetModel()
+        self.endResetModel()
+        
+        # reset the module parser cache
+        Utilities.ModuleParser.resetParsedModules()
+        
+    def projectOpened(self):
+        """
+        Public method used to populate the model after a project has been
+        opened.
+        """
+        self.__vcsStatus = {}
+        states = {}
+        keys = list(self.projectBrowserTypes.keys())[:]
+        
+        if self.project.vcs is not None:
+            for key in keys:
+                for fn in self.project.pdata[key]:
+                    states[os.path.normcase(
+                        os.path.join(self.project.ppath, fn))] = 0
+            
+            self.project.vcs.clearStatusCache()
+            states = self.project.vcs.vcsAllRegisteredStates(
+                states, self.project.ppath)
+        
+        self.inRefresh = True
+        for key in keys:
+            # Show the entry in bold in the others browser to make it more
+            # distinguishable
+            bold = key == "OTHERS"
+            sourceLanguage = (
+                self.project.getProjectLanguage() if key == "SOURCES" else ""
+            )
+            
+            for fn in self.project.pdata[key]:
+                fname = os.path.join(self.project.ppath, fn)
+                parentItem, dt = self.findParentItemByName(
+                    self.projectBrowserTypes[key], fn)
+                itm = (
+                    ProjectBrowserDirectoryItem(
+                        parentItem, fname, self.projectBrowserTypes[key],
+                        False, bold)
+                    if os.path.isdir(fname) else
+                    ProjectBrowserFileItem(
+                        parentItem, fname, self.projectBrowserTypes[key],
+                        False, bold, sourceLanguage=sourceLanguage)
+                )
+                self._addItem(itm, parentItem)
+                if self.project.vcs is not None:
+                    if (
+                        states[os.path.normcase(fname)] ==
+                            self.project.vcs.canBeCommitted
+                    ):
+                        itm.addVcsStatus(self.project.vcs.vcsName())
+                    else:
+                        itm.addVcsStatus(self.tr("local"))
+                else:
+                    itm.addVcsStatus("")
+        self.inRefresh = False
+        self.beginResetModel()
+        self.endResetModel()
+
+    def findParentItemByName(self, type_, name, dontSplit=False):
+        """
+        Public method to find an item given its name.
+        
+        <b>Note</b>: This method creates all necessary parent items, if they
+        don't exist.
+        
+        @param type_ type of the item
+        @param name name of the item (string)
+        @param dontSplit flag indicating the name should not be split (boolean)
+        @return reference to the item found and the new display name (string)
+        """
+        if dontSplit:
+            pathlist = []
+            pathlist.append(name)
+            pathlist.append("ignore_me")
+        else:
+            pathlist = re.split(r'/|\\', name)
+        
+        if len(pathlist) > 1:
+            olditem = self.rootItem
+            path = self.project.ppath
+            for p in pathlist[:-1]:
+                itm = self.findChildItem(p, 0, olditem)
+                path = os.path.join(path, p)
+                if itm is None:
+                    itm = ProjectBrowserSimpleDirectoryItem(
+                        olditem, type_, p, path)
+                    self.__addVCSStatus(itm, path)
+                    if self.inRefresh:
+                        self._addItem(itm, olditem)
+                    else:
+                        if olditem == self.rootItem:
+                            oldindex = QModelIndex()
+                        else:
+                            oldindex = self.createIndex(
+                                olditem.row(), 0, olditem)
+                        self.addItem(itm, oldindex)
+                else:
+                    if type_ and type_ not in itm.getProjectTypes():
+                        itm.addProjectType(type_)
+                        index = self.createIndex(itm.row(), 0, itm)
+                        self.dataChanged.emit(index, index)
+                olditem = itm
+            return (itm, pathlist[-1])
+        else:
+            return (self.rootItem, name)
+    
+    def findChildItem(self, text, column, parentItem=None):
+        """
+        Public method to find a child item given some text.
+        
+        @param text text to search for (string)
+        @param column column to search in (integer)
+        @param parentItem reference to parent item
+        @return reference to the item found
+        """
+        if parentItem is None:
+            parentItem = self.rootItem
+        
+        for itm in parentItem.children():
+            if itm.data(column) == text:
+                return itm
+        
+        return None
+        
+    def addNewItem(self, typeString, name, additionalTypeStrings=None):
+        """
+        Public method to add a new item to the model.
+        
+        @param typeString string denoting the type of the new item (string)
+        @param name name of the new item (string)
+        @param additionalTypeStrings names of additional types (list of string)
+        """
+        # Show the entry in bold in the others browser to make it more
+        # distinguishable
+        bold = typeString == "OTHERS"
+        
+        fname = os.path.join(self.project.ppath, name)
+        parentItem, dt = self.findParentItemByName(
+            self.projectBrowserTypes[typeString], name)
+        parentIndex = (
+            QModelIndex()
+            if parentItem == self.rootItem else
+            self.createIndex(parentItem.row(), 0, parentItem)
+        )
+        if os.path.isdir(fname):
+            itm = ProjectBrowserDirectoryItem(
+                parentItem, fname, self.projectBrowserTypes[typeString],
+                False, bold)
+        else:
+            if typeString == "SOURCES":
+                sourceLanguage = self.project.getProjectLanguage()
+            else:
+                sourceLanguage = ""
+            itm = ProjectBrowserFileItem(
+                parentItem, fname, self.projectBrowserTypes[typeString],
+                False, bold, sourceLanguage=sourceLanguage)
+        self.__addVCSStatus(itm, fname)
+        if additionalTypeStrings:
+            for additionalTypeString in additionalTypeStrings:
+                type_ = self.projectBrowserTypes[additionalTypeString]
+                itm.addProjectType(type_)
+        self.addItem(itm, parentIndex)
+    
+    def renameItem(self, name, newFilename):
+        """
+        Public method to rename an item.
+        
+        @param name the old display name (string)
+        @param newFilename new filename of the item (string)
+        """
+        itm = self.findItem(name)
+        if itm is None:
+            return
+        
+        index = self.createIndex(itm.row(), 0, itm)
+        itm.setName(newFilename, full=False)
+        self.dataChanged.emit(index, index)
+        self.repopulateItem(newFilename)
+    
+    def findItem(self, name):
+        """
+        Public method to find an item given its name.
+        
+        @param name name of the item (string)
+        @return reference to the item found
+        """
+        if QDir.isAbsolutePath(name):
+            name = self.project.getRelativePath(name)
+        pathlist = re.split(r'/|\\', name)
+        if len(pathlist) > 0:
+            olditem = self.rootItem
+            for p in pathlist:
+                itm = self.findChildItem(p, 0, olditem)
+                if itm is None:
+                    return None
+                olditem = itm
+            return itm
+        else:
+            return None
+    
+    def itemIndexByName(self, name):
+        """
+        Public method to find an item's index given its name.
+        
+        @param name name of the item (string)
+        @return index of the item found (QModelIndex)
+        """
+        itm = self.findItem(name)
+        index = self.createIndex(itm.row(), 0, itm) if itm else QModelIndex()
+        return index
+    
+    def itemIndexByNameAndLine(self, name, lineno):
+        """
+        Public method to find an item's index given its name.
+        
+        @param name name of the item (string)
+        @param lineno one based line number of the item (integer)
+        @return index of the item found (QModelIndex)
+        """
+        index = QModelIndex()
+        itm = self.findItem(name)
+        if (
+            itm is not None and
+            isinstance(itm, ProjectBrowserFileItem)
+        ):
+            olditem = itm
+            autoPopulate = Preferences.getProject("AutoPopulateItems")
+            while itm is not None:
+                if not itm.isPopulated():
+                    if itm.isLazyPopulated() and autoPopulate:
+                        self.populateItem(itm)
+                    else:
+                        break
+                for child in itm.children():
+                    with contextlib.suppress(AttributeError):
+                        start, end = child.boundaries()
+                        if end == -1:
+                            end = 1000000   # assume end of file
+                        if start <= lineno <= end:
+                            itm = child
+                            break
+                else:
+                    itm = None
+                if itm:
+                    olditem = itm
+            index = self.createIndex(olditem.row(), 0, olditem)
+        
+        return index
+    
+    def startFileSystemMonitoring(self):
+        """
+        Public method to (re)start monitoring the project file system.
+        """
+        self.__watcherActive = True
+    
+    def stopFileSystemMonitoring(self):
+        """
+        Public method to stop monitoring the project file system.
+        """
+        self.__watcherActive = False
+        
+    def directoryChanged(self, path):
+        """
+        Public slot to handle the directoryChanged signal of the watcher.
+        
+        @param path path of the directory (string)
+        """
+        if not self.__watcherActive:
+            return
+        
+        if path not in self.watchedItems:
+            # just ignore the situation we don't have a reference to the item
+            return
+        
+        fileFilter = (
+            QDir.Filters(
+                QDir.Filter.AllEntries |
+                QDir.Filter.Hidden |
+                QDir.Filter.NoDotAndDotDot
+            )
+            if Preferences.getProject("BrowsersListHiddenFiles") else
+            QDir.Filters(QDir.Filter.AllEntries | QDir.Filter.NoDotAndDotDot)
+        )
+        
+        for itm in self.watchedItems[path]:
+            oldCnt = itm.childCount()
+            
+            qdir = QDir(itm.dirName())
+            
+            entryInfoList = qdir.entryInfoList(fileFilter)
+            
+            # step 1: check for new entries
+            children = itm.children()
+            for f in entryInfoList:
+                fpath = Utilities.toNativeSeparators(f.absoluteFilePath())
+                childFound = False
+                for child in children:
+                    if child.name() == fpath:
+                        childFound = True
+                        children.remove(child)
+                        break
+                if childFound:
+                    continue
+                
+                cnt = itm.childCount()
+                self.beginInsertRows(
+                    self.createIndex(itm.row(), 0, itm), cnt, cnt)
+                node = (
+                    ProjectBrowserDirectoryItem(
+                        itm,
+                        Utilities.toNativeSeparators(f.absoluteFilePath()),
+                        itm.getProjectTypes()[0],
+                        False)
+                    if f.isDir() else
+                    ProjectBrowserFileItem(
+                        itm,
+                        Utilities.toNativeSeparators(f.absoluteFilePath()),
+                        itm.getProjectTypes()[0])
+                )
+                self._addItem(node, itm)
+                if self.project.vcs is not None:
+                    self.project.vcs.clearStatusCache()
+                    state = self.project.vcs.vcsRegisteredState(node.name())
+                    if state == self.project.vcs.canBeCommitted:
+                        node.addVcsStatus(self.project.vcs.vcsName())
+                    else:
+                        node.addVcsStatus(self.tr("local"))
+                self.endInsertRows()
+            
+            # step 2: check for removed entries
+            if len(entryInfoList) != itm.childCount():
+                for row in range(oldCnt - 1, -1, -1):
+                    child = itm.child(row)
+                    childname = Utilities.fromNativeSeparators(child.name())
+                    entryFound = False
+                    for f in entryInfoList:
+                        if f.absoluteFilePath() == childname:
+                            entryFound = True
+                            entryInfoList.remove(f)
+                            break
+                    if entryFound:
+                        continue
+                    
+                    self._removeWatchedItem(child)
+                    self.beginRemoveRows(
+                        self.createIndex(itm.row(), 0, itm), row, row)
+                    itm.removeChild(child)
+                    self.endRemoveRows()
+    
+    def __addVCSStatus(self, item, name):
+        """
+        Private method used to set the vcs status of a node.
+        
+        @param item item to work on
+        @param name filename belonging to this item (string)
+        """
+        if self.project.vcs is not None:
+            state = self.project.vcs.vcsRegisteredState(name)
+            if state == self.project.vcs.canBeCommitted:
+                item.addVcsStatus(self.project.vcs.vcsName())
+            else:
+                item.addVcsStatus(self.tr("local"))
+        else:
+            item.addVcsStatus("")
+    
+    def __updateVCSStatus(self, item, name, recursive=True):
+        """
+        Private method used to update the vcs status of a node.
+        
+        @param item item to work on
+        @param name filename belonging to this item (string)
+        @param recursive flag indicating a recursive update (boolean)
+        """
+        if self.project.vcs is not None:
+            self.project.vcs.clearStatusCache()
+            state = self.project.vcs.vcsRegisteredState(name)
+            if state == self.project.vcs.canBeCommitted:
+                item.setVcsStatus(self.project.vcs.vcsName())
+            else:
+                item.setVcsStatus(self.tr("local"))
+            if recursive:
+                name = os.path.dirname(name)
+                parentItem = item.parent()
+                if name and parentItem is not self.rootItem:
+                    self.__updateVCSStatus(parentItem, name, recursive)
+        else:
+            item.setVcsStatus("")
+        
+        index = self.createIndex(item.row(), 0, item)
+        self.dataChanged.emit(index, index)
+    
+    def updateVCSStatus(self, name, recursive=True):
+        """
+        Public method used to update the vcs status of a node.
+        
+        @param name filename belonging to this item (string)
+        @param recursive flag indicating a recursive update (boolean)
+        """
+        item = self.findItem(name)
+        if item:
+            self.__updateVCSStatus(item, name, recursive)
+    
+    def removeItem(self, name):
+        """
+        Public method to remove a named item.
+        
+        @param name file or directory name of the item (string).
+        """
+        fname = os.path.basename(name)
+        parentItem = self.findParentItemByName(0, name)[0]
+        parentIndex = (
+            QModelIndex()
+            if parentItem == self.rootItem else
+            self.createIndex(parentItem.row(), 0, parentItem)
+        )
+        childItem = self.findChildItem(fname, 0, parentItem)
+        if childItem is not None:
+            self.beginRemoveRows(parentIndex, childItem.row(), childItem.row())
+            parentItem.removeChild(childItem)
+            self.endRemoveRows()
+    
+    def repopulateItem(self, name):
+        """
+        Public method to repopulate an item.
+        
+        @param name name of the file relative to the project root (string)
+        """
+        itm = self.findItem(name)
+        if itm is None:
+            return
+        
+        if itm.isLazyPopulated():
+            if not itm.isPopulated():
+                # item is not populated yet, nothing to do
+                return
+            
+            if itm.childCount():
+                index = self.createIndex(itm.row(), 0, itm)
+                self.beginRemoveRows(index, 0, itm.childCount() - 1)
+                itm.removeChildren()
+                self.endRemoveRows()
+            Utilities.ModuleParser.resetParsedModule(
+                os.path.join(self.project.ppath, name))
+            
+            self.populateItem(itm, True)
+    
+    def projectPropertiesChanged(self):
+        """
+        Public method to react on a change of the project properties.
+        """
+        # nothing to do for now
+        return
+
+    def changeVCSStates(self, statesList):
+        """
+        Public slot to record the (non normal) VCS states.
+        
+        @param statesList list of VCS state entries (list of strings) giving
+            the states in the first column and the path relative to the project
+            directory starting with the third column. The allowed status flags
+            are:
+            <ul>
+                <li>"A" path was added but not yet comitted</li>
+                <li>"M" path has local changes</li>
+                <li>"O" path was removed</li>
+                <li>"R" path was deleted and then re-added</li>
+                <li>"U" path needs an update</li>
+                <li>"Z" path contains a conflict</li>
+                <li>" " path is back at normal</li>
+            </ul>
+        """
+        statesList.sort()
+        lastHead = ""
+        itemCache = {}
+        if len(statesList) == 1 and statesList[0] == '--RESET--':
+            statesList = []
+            for name in list(self.__vcsStatus.keys()):
+                statesList.append(" {0}".format(name))
+        
+        for name in statesList:
+            state = name[0]
+            name = name[1:].strip()
+            if state == ' ':
+                if name in self.__vcsStatus:
+                    del self.__vcsStatus[name]
+            else:
+                self.__vcsStatus[name] = state
+            
+            try:
+                itm = itemCache[name]
+            except KeyError:
+                itm = self.findItem(name)
+                if itm:
+                    itemCache[name] = itm
+            if itm:
+                itm.setVcsState(state)
+                itm.setVcsStatus(self.project.vcs.vcsName())
+                index1 = self.createIndex(itm.row(), 0, itm)
+                index2 = self.createIndex(
+                    itm.row(), self.rootItem.columnCount(), itm)
+                self.dataChanged.emit(index1, index2)
+            
+            head, tail = os.path.split(name)
+            if head != lastHead:
+                if lastHead:
+                    self.__changeParentsVCSState(lastHead, itemCache)
+                lastHead = head
+        if lastHead:
+            self.__changeParentsVCSState(lastHead, itemCache)
+        try:
+            globalVcsStatus = sorted(self.__vcsStatus.values())[-1]
+        except IndexError:
+            globalVcsStatus = ' '
+        self.vcsStateChanged.emit(globalVcsStatus)
+
+    def __changeParentsVCSState(self, path, itemCache):
+        """
+        Private method to recursively change the parents VCS state.
+        
+        @param path pathname of parent item (string)
+        @param itemCache reference to the item cache used to store
+            references to named items
+        """
+        while path:
+            try:
+                itm = itemCache[path]
+            except KeyError:
+                itm = self.findItem(path)
+                if itm:
+                    itemCache[path] = itm
+            if itm:
+                state = " "
+                for id_ in itm.children():
+                    if state < id_.vcsState:
+                        state = id_.vcsState
+                if state != itm.vcsState:
+                    itm.setVcsState(state)
+                    index1 = self.createIndex(itm.row(), 0, itm)
+                    index2 = self.createIndex(
+                        itm.row(), self.rootItem.columnCount(), itm)
+                    self.dataChanged.emit(index1, index2)
+            path, tail = os.path.split(path)
+    
+    def preferencesChanged(self):
+        """
+        Public method used to handle a change in preferences.
+        """
+        for code in list(self.colorNames.keys()):
+            color = Preferences.getProjectBrowserColour(self.colorNames[code])
+            if color.name() == self.itemBackgroundColors[code].name():
+                continue
+            
+            self.itemBackgroundColors[code] = color
+        
+        color = Preferences.getProjectBrowserColour("Highlighted")
+        if self.highLightColor.name() != color.name():
+            self.highLightColor = color

eric ide

mercurial