Ejemplo n.º 1
0
class Watcher( QObject ):
    " Filesystem watcher implementation "

    fsChanged = pyqtSignal( list )

    def __init__( self, excludeFilters, dirToWatch ):

        QObject.__init__( self )
        self.__dirWatcher = QFileSystemWatcher( self )

        # data members
        self.__excludeFilter = []       # Files exclude filter
        self.__srcDirsToWatch = set()   # Came from the user

        self.__fsTopLevelSnapshot = {}  # Current snapshot
        self.__fsSnapshot = {}          # Current snapshot

        # Sets of dirs which are currently watched
        self.__dirsToWatch = set()
        self.__topLevelDirsToWatch = set()      # Generated till root


        # precompile filters
        for flt in excludeFilters:
            self.__excludeFilter.append( re.compile( flt ) )

        # Initialise the list of dirs to watch
        self.__srcDirsToWatch.add( dirToWatch )

        self.__topLevelDirsToWatch = self.__buildTopDirsList(
                                        self.__srcDirsToWatch )
        self.__fsTopLevelSnapshot = self.__buildTopLevelSnapshot(
                                        self.__topLevelDirsToWatch,
                                        self.__srcDirsToWatch )
        self.__dirsToWatch = self.__buildSnapshot()

        # Here __dirsToWatch and __topLevelDirsToWatch have a complete
        # set of what should be watched

        # Add the dirs to the watcher
        dirs = []
        for path in self.__dirsToWatch | self.__topLevelDirsToWatch:
            dirs.append( path )
        self.__dirWatcher.addPaths( dirs )
        self.__dirWatcher.directoryChanged.connect( self.__onDirChanged )

        # self.debug()
        return

    @staticmethod
    def __buildTopDirsList( srcDirs ):
        " Takes a list of dirs to be watched and builds top dirs set "
        topDirsList = set()
        for path in srcDirs:
            parts = path.split( os.path.sep )
            for index in xrange( 1, len( parts ) - 1 ):
                candidate = os.path.sep.join( parts[ 0:index ] ) + os.path.sep
                if os.path.exists( candidate ):
                    if os.access( candidate, os.R_OK ):
                        topDirsList.add( candidate )
        return topDirsList

    @staticmethod
    def __buildTopLevelSnapshot( topLevelDirs, srcDirs ):
        " Takes top level dirs and builds their snapshot "
        snapshot = {}
        for path in topLevelDirs:
            itemsSet = set()
            # search for all the dirs to be watched
            for candidate in topLevelDirs | srcDirs:
                if len( candidate ) <= len( path ):
                    continue
                if candidate.startswith( path ):
                    candidate = candidate[ len( path ) : ]
                    slashIndex = candidate.find( os.path.sep ) + 1
                    item = candidate[ : slashIndex ]
                    if os.path.exists( path + item ):
                        itemsSet.add( item )
            snapshot[ path ] = itemsSet
        return snapshot

    def __buildSnapshot( self ):
        " Builds the filesystem snapshot "
        snapshotDirs = set()
        for path in self.__srcDirsToWatch:
            self.__addSnapshotPath( path, snapshotDirs )
        return snapshotDirs

    def __addSnapshotPath( self, path, snapshotDirs, itemsToReport = None ):
        " Adds one path to the FS snapshot "
        if not os.path.exists( path ):
            return

        snapshotDirs.add( path )
        dirItems = set()
        for item in os.listdir( path ):
            if self.__shouldExclude( item ):
                continue
            if os.path.isdir( path + item ):
                dirName = path + item + os.path.sep
                dirItems.add( item + os.path.sep )
                if itemsToReport is not None:
                    itemsToReport.append( "+" + dirName )
                self.__addSnapshotPath( dirName, snapshotDirs, itemsToReport )
                continue
            dirItems.add( item )
            if itemsToReport is not None:
                itemsToReport.append( "+" + path + item )
        self.__fsSnapshot[ path ] = dirItems
        return

    def __onDirChanged( self, path ):
        " Triggered when the dir is changed "

        path = str( path )
        if not path.endswith( os.path.sep ):
            path = path + os.path.sep

        # Check if it is a top level dir
        try:
            oldSet = self.__fsTopLevelSnapshot[ path ]

            # Build a new set of what is in that top level dir
            newSet = set()
            for item in os.listdir( path ):
                if not os.path.isdir( path + item ):
                    continue    # Only dirs are of interest for the top level
                item = item + os.path.sep
                if item in oldSet:
                    newSet.add( item )
            # Now we have an old set and a new one with those from the old
            # which actually exist
            diff = oldSet - newSet

            # diff are those which disappeared. We need to do the following:
            # - build a list of all the items in the fs snapshot which start
            #   from this dir
            # - build a list of dirs which should be deregistered from the
            #   watcher. This list includes both top level and project level
            # - deregister dirs from the watcher
            # - emit a signal of what disappeared
            if not diff:
                return  # no changes

            self.__fsTopLevelSnapshot[ path ] = newSet

            dirsToBeRemoved = []
            itemsToReport = []

            for item in diff:
                self.__processRemoveTopDir( path + item, dirsToBeRemoved,
                                            itemsToReport )

            # Here: it is possible that the last dir to watch disappeared
            if not newSet:
                # There is nothing to watch here anymore
                dirsToBeRemoved.append( path )
                del self.__fsTopLevelSnapshot[ path ]

                parts = path[ 1:-1 ].split( os.path.sep )
                for index in xrange( len( parts ) - 2, 0, -1 ):
                    candidate = os.path.sep + \
                                os.path.sep.join( parts[ 0 : index ] ) + \
                                os.path.sep
                    dirSet = self.__fsTopLevelSnapshot[ candidate ]
                    dirSet.remove( parts[ index + 1 ] + os.path.sep )
                    if not dirSet:
                        dirsToBeRemoved.append( candidate )
                        del self.__fsTopLevelSnapshot[ candidate ]
                        continue
                    break   # it is not the last item in the set

            # Update the watcher
            if dirsToBeRemoved:
                self.__dirWatcher.removePaths( dirsToBeRemoved )

            # Report
            if itemsToReport:
                self.fsChanged.emit( itemsToReport )
            return
        except:
            # it is not a top level dir - no key
            pass

        # Here: the change is in the project level dir
        try:
            oldSet = self.__fsSnapshot[ path ]

            # Build a new set of what is in that top level dir
            newSet = set()
            for item in os.listdir( path ):
                if self.__shouldExclude( item ):
                    continue
                if os.path.isdir( path + item ):
                    newSet.add( item + os.path.sep )
                else:
                    newSet.add( item )

            # Here: we have a new and old snapshots
            # Lets calculate the difference
            deletedItems = oldSet - newSet
            addedItems = newSet - oldSet

            if not deletedItems and not addedItems:
                return  # No changes

            # Update the changed dir set
            self.__fsSnapshot[ path ] = newSet

            # We need to build some lists:
            # - list of files which were added
            # - list of dirs which were added
            # - list of files which were deleted
            # - list of dirs which were deleted
            # The deleted dirs must be unregistered in the watcher
            # The added dirs must be registered
            itemsToReport = []
            dirsToBeAdded = []
            dirsToBeRemoved = []

            for item in addedItems:
                if item.endswith( os.path.sep ):
                    # directory was added
                    self.__processAddedDir( path + item,
                                            dirsToBeAdded, itemsToReport )
                else:
                    itemsToReport.append( "+" + path + item )

            for item in deletedItems:
                if item.endswith( os.path.sep ):
                    # directory was deleted
                    self.__processRemovedDir( path + item,
                                              dirsToBeRemoved, itemsToReport )
                else:
                    itemsToReport.append( "-" + path + item )

            # Update the watcher
            if dirsToBeRemoved:
                self.__dirWatcher.removePaths( dirsToBeRemoved )
            if dirsToBeAdded:
                self.__dirWatcher.addPaths( dirsToBeAdded )

            # Report
            self.fsChanged.emit( itemsToReport )

        except:
            # It could be a queued signal about what was already reported
            pass

        # self.debug()
        return

    def __shouldExclude( self, name ):
        " Tests if a file must be excluded "
        for excl in self.__excludeFilter:
            if excl.match( name ):
                return True
        return False

    def __processAddedDir( self, path, dirsToBeAdded, itemsToReport ):
        " called for an appeared dir in the project tree "
        dirsToBeAdded.append( path )
        itemsToReport.append( "+" + path )

        # it should add dirs recursively into the snapshot and care
        # of the items to report
        dirItems = set()
        for item in os.listdir( path ):
            if self.__shouldExclude( item ):
                continue
            if os.path.isdir( path + item ):
                dirName = path + item + os.path.sep
                dirItems.add( item + os.path.sep )
                self.__processAddedDir( dirName, dirsToBeAdded, itemsToReport )
                continue
            itemsToReport.append( "+" + path + item )
            dirItems.add( item )
        self.__fsSnapshot[ path ] = dirItems
        return

    def __processRemovedDir( self, path, dirsToBeRemoved, itemsToReport ):
        " called for a disappeared dir in the project tree "

        # it should remove the dirs recursively from the fs snapshot
        # and care of items to report
        dirsToBeRemoved.append( path )
        itemsToReport.append( "-" + path )

        oldSet = self.__fsSnapshot[ path ]
        for item in oldSet:
            if item.endswith( os.path.sep ):
                # Nested dir
                self.__processRemovedDir( path + item, dirsToBeRemoved,
                                          itemsToReport )
            else:
                # a file
                itemsToReport.append( "-" + path + item )
        del self.__fsSnapshot[ path ]
        return

    def __processRemoveTopDir( self, path, dirsToBeRemoved, itemsToReport ):
        " Called for a disappeared top level dir "

        if path in self.__fsTopLevelSnapshot:
            # It is still a top level dir
            dirsToBeRemoved.append( path )
            for item in self.__fsTopLevelSnapshot[ path ]:
                self.__processRemoveTopDir( path + item, dirsToBeRemoved,
                                            itemsToReport )
            del self.__fsTopLevelSnapshot[ path ]
        else:
            # This is a project level dir
            self.__processRemovedDir( path, dirsToBeRemoved,
                                      itemsToReport )
        return

    def reset( self ):
        " Resets the watcher (it does not report any changes) "
        self.__dirWatcher.removePaths( self.__dirWatcher.directories() )

        self.__srcDirsToWatch = set()

        self.__fsTopLevelSnapshot = {}
        self.__fsSnapshot = {}

        self.__dirsToWatch = set()
        self.__topLevelDirsToWatch = set()
        return

    def registerDir( self, path ):
        " Adds a directory to the list of watched ones "
        if not path.endswith( os.path.sep ):
            path = path + os.path.sep

        if path in self.__srcDirsToWatch:
            return  # It is there already


        # It is necessary to do the following:
        # - add the dir to the fs snapshot
        # - collect dirs to add to the watcher
        # - collect items to report
        self.__srcDirsToWatch.add( path )

        dirsToWatch = set()
        itemsToReport = []
        self.__registerDir( path, dirsToWatch, itemsToReport )


        # It might be that top level dirs should be updated too
        newTopLevelDirsToWatch = self.__buildTopDirsList(self.__srcDirsToWatch)
        addedDirs = newTopLevelDirsToWatch - self.__topLevelDirsToWatch

        for item in addedDirs:
            dirsToWatch.add( item )

            # Identify items to be watched by this dir
            dirItems = set()
            for candidate in newTopLevelDirsToWatch | self.__srcDirsToWatch:
                if len( candidate ) <= len( item ):
                    continue
                if candidate.startswith( item ):
                    candidate = candidate[ len( item ) : ]
                    slashIndex = candidate.find( os.path.sep ) + 1
                    dirName = candidate[ : slashIndex ]
                    if os.path.exists( item + dirName ):
                        dirItems.add( dirName )
            # Update the top level dirs snapshot
            self.__fsTopLevelSnapshot[ item ] = dirItems

        # Update the top level snapshot with the added dir
        upperDir = os.path.dirname( path[ :-1 ] ) + os.path.sep
        dirName = path.replace( upperDir, '' )
        self.__fsTopLevelSnapshot[ upperDir ].add( dirName )

        # Update the list of top level dirs to watch
        self.__topLevelDirsToWatch = newTopLevelDirsToWatch

        # Update the watcher
        if dirsToWatch:
            dirs = []
            for item in dirsToWatch:
                dirs.append( item )
            self.__dirWatcher.addPaths( dirs )

        # Report the changes
        if itemsToReport:
            self.fsChanged.emit( itemsToReport )

        # self.debug()
        return

    def __registerDir( self, path, dirsToWatch, itemsToReport ):
        " Adds one path to the FS snapshot "
        if not os.path.exists( path ):
            return

        dirsToWatch.add( path )
        itemsToReport.append( "+" + path )

        dirItems = set()
        for item in os.listdir( path ):
            if self.__shouldExclude( item ):
                continue
            if os.path.isdir( path + item ):
                dirName = path + item + os.path.sep
                dirItems.add( item + os.path.sep )
                itemsToReport.append( "+" + path + item + os.path.sep )
                self.__addSnapshotPath( dirName, dirsToWatch, itemsToReport )
                continue
            dirItems.add( item )
            itemsToReport.append( "+" + path + item )
        self.__fsSnapshot[ path ] = dirItems
        return

    def deregisterDir( self, path ):
        " Removes the directory from the list of the watched ones "

        if not path.endswith( os.path.sep ):
            path = path + os.path.sep

        if path not in self.__srcDirsToWatch:
            return  # It is not there already
        self.__srcDirsToWatch.remove( path )

        # It is necessary to do the following:
        # - remove the dir from the fs snapshot
        # - collect the dirs to be removed from watching
        # - collect item to report

        itemsToReport = []
        dirsToBeRemoved = []

        self.__deregisterDir( path, dirsToBeRemoved, itemsToReport )

        # It is possible that some of the top level watched dirs should be
        # removed as well
        newTopLevelDirsToWatch = self.__buildTopDirsList(self.__srcDirsToWatch)
        deletedDirs = self.__topLevelDirsToWatch - newTopLevelDirsToWatch

        for item in deletedDirs:
            dirsToBeRemoved.append( item )
            del self.__fsTopLevelSnapshot[ item ]

        # It might be the case that some of the items should be deleted in the
        # top level dirs sets
        for dirName in self.__fsTopLevelSnapshot:
            itemsSet = self.__fsTopLevelSnapshot[ dirName ]
            for item in itemsSet:
                candidate = dirName + item
                if candidate == path or candidate in deletedDirs:
                    itemsSet.remove( item )
                    self.__fsTopLevelSnapshot[ dirName ] = itemsSet
                    break

        # Update the list of dirs to be watched
        self.__topLevelDirsToWatch = newTopLevelDirsToWatch

        # Update the watcher
        if dirsToBeRemoved:
            self.__dirWatcher.removePaths( dirsToBeRemoved )

        # Report the changes
        if itemsToReport:
            self.fsChanged.emit( itemsToReport )

        # self.debug()
        return

    def __deregisterDir( self, path, dirsToBeRemoved, itemsToReport ):
        " Deregisters a directory recursively "
        dirsToBeRemoved.append( path )
        itemsToReport.append( "-" + path )
        if path in self.__fsTopLevelSnapshot:
            # This is a top level dir
            for item in self.__fsTopLevelSnapshot[ path ]:
                if item.endswith( os.path.sep ):
                    # It's a dir
                    self.__deregisterDir( path + item, dirsToBeRemoved,
                                          itemsToReport )
                else:
                    # It's a file
                    itemsToReport.append( "-" + path + item )
            del self.__fsTopLevelSnapshot[ path ]
            return

        # It is from an a project level snapshot
        if path in self.__fsSnapshot:
            for item in self.__fsSnapshot[ path ]:
                if item.endswith( os.path.sep ):
                    # It's a dir
                    self.__deregisterDir( path + item, dirsToBeRemoved,
                                          itemsToReport )
                else:
                    # It's a file
                    itemsToReport.append( "-" + path + item )
            del self.__fsSnapshot[ path ]
        return


    def debug( self ):
        print "Top level dirs to watch: " + str( self.__topLevelDirsToWatch )
        print "Project dirs to watch: " + str( self.__dirsToWatch )

        print "Top level snapshot: " + str( self.__fsTopLevelSnapshot )
        print "Project snapshot: " + str( self.__fsSnapshot )
Ejemplo n.º 2
0
class TreeProjectsWidget(QTreeWidget):

###############################################################################
# TreeProjectsWidget SIGNALS
###############################################################################

    """
    runProject()
    closeProject(QString)
    closeFilesFromProjectClosed(QString)
    addProjectToConsole(QString)
    removeProjectFromConsole(QString)
    """

###############################################################################

    #Extra context menu 'all' indicate a menu for ALL LANGUAGES!
    EXTRA_MENUS = {'all': []}
    images = {
        'py': resources.IMAGES['tree-python'],
        'java': resources.IMAGES['tree-java'],
        'fn': resources.IMAGES['tree-code'],
        'c': resources.IMAGES['tree-code'],
        'cs': resources.IMAGES['tree-code'],
        'jpg': resources.IMAGES['tree-image'],
        'png': resources.IMAGES['tree-image'],
        'html': resources.IMAGES['tree-html'],
        'css': resources.IMAGES['tree-css'],
        'ui': resources.IMAGES['designer']}

    def __init__(self):
        QTreeWidget.__init__(self)

        self.header().setHidden(True)
        self.setSelectionMode(QTreeWidget.SingleSelection)
        self.setAnimated(True)

        self._actualProject = None
        #self._projects -> key: [Item, folderStructure]
        self._projects = {}
        self.__enableCloseNotification = True
        self._fileWatcher = QFileSystemWatcher()

        self.header().setHorizontalScrollMode(QAbstractItemView.ScrollPerPixel)
        self.header().setResizeMode(0, QHeaderView.ResizeToContents)
        self.header().setStretchLastSection(False)

        self.setContextMenuPolicy(Qt.CustomContextMenu)
        self.connect(self, SIGNAL(
            "customContextMenuRequested(const QPoint &)"),
            self._menu_context_tree)
        self.connect(self, SIGNAL("itemClicked(QTreeWidgetItem *, int)"),
            self._open_file)
        self.connect(self._fileWatcher, SIGNAL("directoryChanged(QString)"),
            self._refresh_project_by_path)

    def add_extra_menu(self, menu, lang='all'):
        '''
        Add an extra menu for the given language
        @lang: string with the form 'py', 'php', 'json', etc
        '''
        #remove blanks and replace dots Example(.py => py)
        lang = lang.strip().replace('.', '')
        self.EXTRA_MENUS.setdefault(lang, [])
        self.EXTRA_MENUS[lang].append(menu)

    def _menu_context_tree(self, point):
        index = self.indexAt(point)
        if not index.isValid():
            return

        item = self.itemAt(point)
        handler = None
        menu = QMenu(self)
        if item.isFolder or item.parent() is None:
            action_add_file = menu.addAction(QIcon(resources.IMAGES['new']),
                self.tr("Add New File"))
            self.connect(action_add_file, SIGNAL("triggered()"),
                self._add_new_file)
            action_add_folder = menu.addAction(QIcon(
                resources.IMAGES['openProj']), self.tr("Add New Folder"))
            self.connect(action_add_folder, SIGNAL("triggered()"),
                self._add_new_folder)
            action_create_init = menu.addAction(
                self.tr("Create '__init__' Complete"))
            self.connect(action_create_init, SIGNAL("triggered()"),
                self._create_init)
            if item.isFolder and (item.parent() != None):
                action_remove_folder = menu.addAction(self.tr("Remove Folder"))
                self.connect(action_remove_folder, SIGNAL("triggered()"),
                    self._delete_folder)
        elif not item.isFolder:
            action_rename_file = menu.addAction(self.tr("Rename File"))
            action_move_file = menu.addAction(self.tr("Move File"))
            action_copy_file = menu.addAction(self.tr("Copy File"))
            action_remove_file = menu.addAction(
                self.style().standardIcon(QStyle.SP_DialogCloseButton),
                self.tr("Delete File"))
            self.connect(action_remove_file, SIGNAL("triggered()"),
                self._delete_file)
            self.connect(action_rename_file, SIGNAL("triggered()"),
                self._rename_file)
            self.connect(action_copy_file, SIGNAL("triggered()"),
                self._copy_file)
            self.connect(action_move_file, SIGNAL("triggered()"),
                self._move_file)
            #Allow to edit Qt UI files with the appropiate program
            if item.lang() == 'ui':
                action_edit_ui_file = menu.addAction(self.tr("Edit UI File"))
                self.connect(action_edit_ui_file, SIGNAL("triggered()"),
                    self._edit_ui_file)
            #menu per file language!
            for m in self.EXTRA_MENUS.get(item.lang(), ()):
                menu.addSeparator()
                menu.addMenu(m)
        if item.parent() is None:
            menu.addSeparator()
            actionRunProject = menu.addAction(QIcon(
                resources.IMAGES['play']), self.tr("Run Project"))
            self.connect(actionRunProject, SIGNAL("triggered()"),
                SIGNAL("runProject()"))
            actionMainProject = menu.addAction(self.tr("Set as Main Project"))
            self.connect(actionMainProject, SIGNAL("triggered()"),
                lambda: self.set_default_project(item))
            if item.addedToConsole:
                actionRemoveFromConsole = menu.addAction(
                    self.tr("Remove this Project from the Python Console"))
                self.connect(actionRemoveFromConsole, SIGNAL("triggered()"),
                    self._remove_project_from_console)
            else:
                actionAdd2Console = menu.addAction(
                    self.tr("Add this Project to the Python Console"))
                self.connect(actionAdd2Console, SIGNAL("triggered()"),
                    self._add_project_to_console)
            actionProperties = menu.addAction(QIcon(resources.IMAGES['pref']),
                self.tr("Project Properties"))
            self.connect(actionProperties, SIGNAL("triggered()"),
                self.open_project_properties)
            #get the extra context menu for this projectType
            handler = settings.get_project_type_handler(item.projectType)

            menu.addSeparator()
            action_refresh = menu.addAction(
                self.style().standardIcon(QStyle.SP_BrowserReload),
                self.tr("Refresh Project"))
            self.connect(action_refresh, SIGNAL("triggered()"),
                self._refresh_project)
            action_close = menu.addAction(
                self.style().standardIcon(QStyle.SP_DialogCloseButton),
                self.tr("Close Project"))
            self.connect(action_close, SIGNAL("triggered()"),
                self._close_project)

        #menu for all items!
        for m in self.EXTRA_MENUS.get('all', ()):
            menu.addSeparator()
            menu.addMenu(m)

        #menu for the Project Type(if present)
        if handler:
            for m in handler.get_context_menus():
                menu.addSeparator()
                menu.addMenu(m)
        #show the menu!
        menu.exec_(QCursor.pos())

    def _add_project_to_console(self):
        item = self.currentItem()
        if isinstance(item, ProjectTree):
            self.emit(SIGNAL("addProjectToConsole(QString)"), item.path)
            item.addedToConsole = True

    def _remove_project_from_console(self):
        item = self.currentItem()
        if isinstance(item, ProjectTree):
            self.emit(SIGNAL("removeProjectFromConsole(QString)"), item.path)
            item.addedToConsole = False

    def _open_file(self, item, column):
        if item.childCount() == 0 and not item.isFolder:
            fileName = os.path.join(item.path, unicode(item.text(column)))
            main_container.MainContainer().open_file(fileName)

    def _get_project_root(self, item=None):
        if item is None:
            item = self.currentItem()
        while item is not None and item.parent() is not None:
            item = item.parent()
        return item

    def set_default_project(self, item):
        item.setForeground(0, QBrush(QColor(0, 204, 82)))
        if self._actualProject:
            self._actualProject.setForeground(0, QBrush(Qt.darkGray))
        self._actualProject = item

    def open_project_properties(self):
        item = self._get_project_root()
        proj = project_properties_widget.ProjectProperties(item, self)
        proj.show()

    def _refresh_project_by_path(self, folder):
        item = self.get_item_for_path(unicode(folder))
        self._refresh_project(item)

    def _refresh_project(self, item=None):
        if item is None:
            item = self.currentItem()
        parentItem = self._get_project_root(item)
        if parentItem is None:
            return
        if item.parent() is None:
            path = item.path
        else:
            path = file_manager.create_path(item.path, unicode(item.text(0)))
        if parentItem.extensions != settings.SUPPORTED_EXTENSIONS:
            folderStructure = file_manager.open_project_with_extensions(
                path, parentItem.extensions)
        else:
            folderStructure = file_manager.open_project(path)
        if folderStructure.get(path, [None, None])[1] is not None:
            folderStructure[path][1].sort()
        else:
            return
        item.takeChildren()
        self._load_folder(folderStructure, path, item)
        item.setExpanded(True)

    def _close_project(self):
        item = self.currentItem()
        index = self.indexOfTopLevelItem(item)
        pathKey = item.path
        for directory in self._fileWatcher.directories():
            directory = unicode(directory)
            if file_manager.belongs_to_folder(pathKey, directory):
                self._fileWatcher.removePath(directory)
        self._fileWatcher.removePath(pathKey)
        self.takeTopLevelItem(index)
        self._projects.pop(pathKey)
        if self.__enableCloseNotification:
            self.emit(SIGNAL("closeProject(QString)"), pathKey)
        self.emit(SIGNAL("closeFilesFromProjectClosed(QString)"), pathKey)
        item = self.currentItem()
        if item:
            self.set_default_project(item)

    def _create_init(self):
        item = self.currentItem()
        if item.parent() is None:
            pathFolder = item.path
        else:
            pathFolder = os.path.join(item.path, str(item.text(0)))
        try:
            file_manager.create_init_file_complete(pathFolder)
        except file_manager.NinjaFileExistsException, ex:
            QMessageBox.information(self, self.tr("Create INIT fail"),
                ex.message)
        self._refresh_project(item)
Ejemplo n.º 3
0
class MainWindow(QMainWindow):
    workingDirectory = ''
    fileNames = []
    supportedExtensions = QStringList(('*.txt','*.csv'))
    def __init__(self):
        QMainWindow.__init__(self)

        self.settings = QSettings("greyltc", "batch-iv-analysis")

        self.rows = 0 #keep track of how many rows there are in the table

        self.cols = OrderedDict()

        thisKey = 'plotBtn'
        self.cols[thisKey] = col()
        self.cols[thisKey].header = 'Draw Plot'
        self.cols[thisKey].tooltip = 'Click this button to draw a plot for that row'        

        thisKey = 'exportBtn'
        self.cols[thisKey] = col()
        self.cols[thisKey].header = 'Export'
        self.cols[thisKey].tooltip = 'Click this button to export\ninterpolated data points from fits'        

        thisKey = 'file'
        self.cols[thisKey] = col()
        self.cols[thisKey].header = 'File'
        self.cols[thisKey].tooltip = 'File name\nHover to see header from data file'

        thisKey = 'pce'
        self.cols[thisKey] = col()
        self.cols[thisKey].header = 'PCE\n[%]'
        self.cols[thisKey].tooltip = 'Power conversion efficiency as found from spline fit\nHover for value from characteristic equation fit'

        thisKey = 'pmax'
        self.cols[thisKey] = col()
        self.cols[thisKey].header = 'P_max\n[mW/cm^2]'
        self.cols[thisKey].tooltip = 'Maximum power density as found from spline fit\nHover for value from characteristic equation fit'

        thisKey = 'jsc'
        self.cols[thisKey] = col()
        self.cols[thisKey].header = 'J_sc\n[mA/cm^2]'
        self.cols[thisKey].tooltip = 'Short-circuit current density as found from spline fit\nHover for value from characteristic equation fit'

        thisKey = 'voc'
        self.cols[thisKey] = col()
        self.cols[thisKey].header = 'V_oc\n[mV]'
        self.cols[thisKey].tooltip = 'Open-circuit voltage as found from spline fit\nHover for value from characteristic equation fit'

        thisKey = 'ff'
        self.cols[thisKey] = col()
        self.cols[thisKey].header = 'FF'
        self.cols[thisKey].tooltip = 'Fill factor as found from spline fit\nHover for value from characteristic equation fit'

        thisKey = 'rs'
        self.cols[thisKey] = col()
        self.cols[thisKey].header = 'R_s\n[ohm*cm^2]'
        self.cols[thisKey].tooltip = 'Specific series resistance as found from characteristic equation fit\nHover for 95% confidence interval'

        thisKey = 'rsh'
        self.cols[thisKey] = col()
        self.cols[thisKey].header = 'R_sh\n[ohm*cm^2]'
        self.cols[thisKey].tooltip = 'Specific shunt resistance as found from characteristic equation fit\nHover for 95% confidence interval'

        thisKey = 'jph'
        self.cols[thisKey] = col()
        self.cols[thisKey].header = 'J_ph\n[mA/cm^2]'
        self.cols[thisKey].tooltip = 'Photogenerated current density as found from characteristic equation fit\nHover for 95% confidence interval'

        thisKey = 'j0'
        self.cols[thisKey] = col()
        self.cols[thisKey].header = 'J_0\n[nA/cm^2]'
        self.cols[thisKey].tooltip = 'Reverse saturation current density as found from characteristic equation fit\nHover for 95% confidence interval'

        thisKey = 'n'
        self.cols[thisKey] = col()
        self.cols[thisKey].header = 'n'
        self.cols[thisKey].tooltip = 'Diode ideality factor as found from characteristic equation fit\nHover for 95% confidence interval'

        thisKey = 'Vmax'
        self.cols[thisKey] = col()
        self.cols[thisKey].header = 'V_max\n[mV]'
        self.cols[thisKey].tooltip = 'Voltage at maximum power point as found from spline fit\nHover for value from characteristic equation fit'

        thisKey = 'area'
        self.cols[thisKey] = col()
        self.cols[thisKey].header = 'Area\n[cm^2]'
        self.cols[thisKey].tooltip = 'Device area'

        thisKey = 'pmax2'
        self.cols[thisKey] = col()
        self.cols[thisKey].header = 'P_max\n[mW]'
        self.cols[thisKey].tooltip = 'Maximum power as found from spline fit\nHover for value from characteristic equation fit'

        thisKey = 'isc'
        self.cols[thisKey] = col()
        self.cols[thisKey].header = 'I_sc\n[mA]'
        self.cols[thisKey].tooltip = 'Short-circuit current as found from characteristic equation fit\nHover for 95% confidence interval'

        thisKey = 'iph'
        self.cols[thisKey] = col()
        self.cols[thisKey].header = 'I_ph\n[mA]'
        self.cols[thisKey].tooltip = 'Photogenerated current as found from characteristic equation fit\nHover for 95% confidence interval'

        thisKey = 'i0'
        self.cols[thisKey] = col()
        self.cols[thisKey].header = 'I_0\n[nA]'
        self.cols[thisKey].tooltip = 'Reverse saturation current as found from characteristic equation fit\nHover for 95% confidence interval'

        thisKey = 'rs2'
        self.cols[thisKey] = col()
        self.cols[thisKey].header = 'R_s\n[ohm]'
        self.cols[thisKey].tooltip = 'Series resistance as found from characteristic equation fit\nHover for 95% confidence interval'

        thisKey = 'rsh2'
        self.cols[thisKey] = col()
        self.cols[thisKey].header = 'R_sh\n[ohm]'
        self.cols[thisKey].tooltip = 'Shunt resistance as found from characteristic equation fit\nHover for 95% confidence interval'		


        #how long status messages show for
        self.messageDuration = 2500#ms

        # Set up the user interface from Designer.
        self.ui = Ui_batch_iv_analysis()
        self.ui.setupUi(self)

        #insert cols
        for item in self.cols:
            blankItem = QTableWidgetItem()
            thisCol = self.cols.keys().index(item)
            self.ui.tableWidget.insertColumn(thisCol)
            blankItem.setToolTip(self.cols[item].tooltip)
            blankItem.setText(self.cols[item].header)
            self.ui.tableWidget.setHorizontalHeaderItem(thisCol,blankItem)
        
        #file system watcher
        self.watcher = QFileSystemWatcher(self)
        self.watcher.directoryChanged.connect(self.handleWatchUpdate)

        #connect signals generated by gui elements to proper functions 
        self.ui.actionOpen.triggered.connect(self.openCall)
        self.ui.actionEnable_Watching.triggered.connect(self.watchCall)
        self.ui.actionSave.triggered.connect(self.handleSave)
        self.ui.actionWatch_2.triggered.connect(self.handleWatchAction)
        

        self.ui.actionClear_Table.triggered.connect(self.clearTableCall)

    def exportInterp(self,row):
        thisGraphData = self.ui.tableWidget.item(row,self.cols.keys().index('plotBtn')).data(Qt.UserRole).toPyObject()
        fitX = thisGraphData[QString(u'fitX')]
        modelY = thisGraphData[QString(u'modelY')]
        splineY = thisGraphData[QString(u'splineY')]
        a = np.asarray([fitX, modelY, splineY])
        a = np.transpose(a)
        destinationFolder = os.path.join(self.workingDirectory,'exports')
        QDestinationFolder = QDir(destinationFolder)
        if not QDestinationFolder.exists():
            QDir().mkdir(destinationFolder)
        saveFile = os.path.join(destinationFolder,str(self.ui.tableWidget.item(row,self.cols.keys().index('file')).text())+'.csv')
        header = 'Voltage [V],CharEqn Current [mA/cm^2],Spline Current [mA/cm^2]'
        try:
            np.savetxt(saveFile, a, delimiter=",",header=header)
            self.ui.statusbar.showMessage("Exported " + saveFile,5000)
        except:
            self.ui.statusbar.showMessage("Could not export " + saveFile,self.messageDuration)
        


    def handleButton(self):
        btn = self.sender()
        #kinda hacky:
        row = self.ui.tableWidget.indexAt(btn.pos()).row()
        col = self.ui.tableWidget.indexAt(btn.pos()).column()
        if col == 0:
            self.rowGraph(row)
        if col == 1:
            self.exportInterp(row)


    def rowGraph(self,row):
        thisGraphData = self.ui.tableWidget.item(row,self.cols.keys().index('plotBtn')).data(Qt.UserRole).toPyObject()
        filename = str(self.ui.tableWidget.item(row,self.cols.keys().index('file')).text())
        
        v = thisGraphData[QString(u'v')]
        i = thisGraphData[QString(u'i')]
        if not thisGraphData[QString(u'vsTime')]:
            plt.plot(v, i, c='b', marker='o', ls="None",label='I-V Data')
            plt.scatter(thisGraphData[QString(u'Vmax')], thisGraphData[QString(u'Imax')], c='g',marker='x',s=100)
            plt.scatter(thisGraphData[QString(u'Voc')], 0, c='g',marker='x',s=100)
            plt.scatter(0, thisGraphData[QString(u'Isc')], c='g',marker='x',s=100)
            fitX = thisGraphData[QString(u'fitX')]
            modelY = thisGraphData[QString(u'modelY')]
            splineY = thisGraphData[QString(u'splineY')]
            if not np.isnan(modelY[0]):
                plt.plot(fitX, modelY,c='k', label='CharEqn Best Fit')
            plt.plot(fitX, splineY,c='g', label='Spline Fit')
            plt.autoscale(axis='x', tight=True)
            plt.grid(b=True)
            ax = plt.gca()
            handles, labels = ax.get_legend_handles_labels()
            ax.legend(handles, labels, loc=3)
    
            plt.annotate(
                thisGraphData[QString(u'Voc')].__format__('0.4f')+ ' V', 
                xy = (thisGraphData[QString(u'Voc')], 0), xytext = (40, 20),
                textcoords = 'offset points', ha = 'right', va = 'bottom',
                bbox = dict(boxstyle = 'round,pad=0.5', fc = 'yellow', alpha = 0.5),
                arrowprops = dict(arrowstyle = '->', connectionstyle = 'arc3,rad=0'))
    
            plt.annotate(
                float(thisGraphData[QString(u'Isc')]).__format__('0.4f') + ' mA/cm^2', 
                xy = (0,thisGraphData[QString(u'Isc')]), xytext = (40, 20),
                textcoords = 'offset points', ha = 'right', va = 'bottom',
                bbox = dict(boxstyle = 'round,pad=0.5', fc = 'yellow', alpha = 0.5),
                arrowprops = dict(arrowstyle = '->', connectionstyle = 'arc3,rad=0'))
    
            plt.annotate(
                float(thisGraphData[QString(u'Imax')]*thisGraphData[QString(u'Vmax')]).__format__('0.4f') + '% @(' + float(thisGraphData[QString(u'Vmax')]).__format__('0.4f') + ',' + float(thisGraphData[QString(u'Imax')]).__format__('0.4f') + ')', 
                xy = (thisGraphData[QString(u'Vmax')],thisGraphData[QString(u'Imax')]), xytext = (80, 40),
                textcoords = 'offset points', ha = 'right', va = 'bottom',
                bbox = dict(boxstyle = 'round,pad=0.5', fc = 'yellow', alpha = 0.5),
                arrowprops = dict(arrowstyle = '->', connectionstyle = 'arc3,rad=0'))		
    
            plt.ylabel('Current [mA/cm^2]')
            plt.xlabel('Voltage [V]')
        else: #vs time
            time = thisGraphData[QString(u'time')]
            
            fig, ax1 = plt.subplots()
            ax1.plot(time, v, 'b-',label='Voltage [V]')
            ax1.set_xlabel('Time [s]')
            # Make the y-axis label and tick labels match the line color.
            ax1.set_ylabel('Voltage [V]', color='b')
            for tl in ax1.get_yticklabels():
                tl.set_color('b')
            #fdsf
            ax2 = ax1.twinx()
            ax2.plot(time, i, 'r-')
            ax2.set_ylabel('Current [mA/cm^2]', color='r')
            for tl in ax2.get_yticklabels():
                tl.set_color('r')            
        
        plt.title(filename)
        plt.draw()
        plt.show()

    def handleSave(self):
        if self.settings.contains('lastFolder'):
            saveDir = self.settings.value('lastFolder').toString()
        else:
            saveDir = '.'        
        path = QFileDialog.getSaveFileName(self, caption='Save File', directory=saveDir)
        if not str(path[0]) == '':
            with open(unicode(path), 'wb') as stream:
                writer = csv.writer(stream)
                rowdata = []
                for column in range(self.ui.tableWidget.columnCount()):
                    item = self.ui.tableWidget.horizontalHeaderItem(column)
                    if item is not None:
                        rowdata.append(unicode(item.text()).encode('utf8').replace('\n',' '))
                    else:
                        rowdata.append('')
                writer.writerow(rowdata)                
                for row in range(self.ui.tableWidget.rowCount()):
                    rowdata = []
                    for column in range(self.ui.tableWidget.columnCount()):
                        item = self.ui.tableWidget.item(row, column)
                        if item is not None:
                            rowdata.append(unicode(item.text()).encode('utf8'))
                        else:
                            rowdata.append('')
                    writer.writerow(rowdata)
                stream.close()

    def clearTableCall(self):
        for ii in range(self.rows):
            self.ui.tableWidget.removeRow(0)
        self.ui.tableWidget.clearContents()
        self.rows = 0
        self.fileNames = []

    def processFile(self,fullPath):
        fileName, fileExtension = os.path.splitext(fullPath)
        fileName = os.path.basename(fullPath)
        self.fileNames.append(fileName)
        if fileExtension == '.csv':
            delimiter = ','
        else:
            delimiter = None

        self.ui.statusbar.showMessage("processing: "+ fileName,2500)
        
        #wait here for the file to be completely written to disk and closed before trying to read it

        fi = QFileInfo(fullPath)
        while (not fi.isWritable()):
                time.sleep(0.001)
                fi.refresh()
        
        fp = open(fullPath, mode='r')
        fileBuffer = fp.read()
        fp.close()
        first10 = fileBuffer[0:10]
        nMcHeaderLines = 25 #number of header lines in mcgehee IV file format
        isMcFile = False #true if this is a McGehee iv file format
        if (not first10.__contains__('#')) and (first10.__contains__('/')) and (first10.__contains__('\t')):#the first line is not a comment
            #the first 8 chars do not contain comment symbol and do contain / and a tab, it's safe to assume mcgehee iv file format
            isMcFile = True
            #comment out the first 25 rows here
            fileBuffer = '#'+fileBuffer
            fileBuffer = fileBuffer.replace('\n', '\n#',nMcHeaderLines-1)

        splitBuffer = fileBuffer.splitlines(True)
        
        
        area = 1
        noArea = True
        vsTime = False #this is not an i,v vs t data file
        #extract header lines and search for area
        header = []
        for line in splitBuffer:
            if line.startswith('#'):
                header.append(line)
                if line.__contains__('Area'):
                    area = float(line.split(' ')[3])
                    noArea = False
                if line.__contains__('I&V vs t'):
                    if float(line.split(' ')[5]) == 1:
                        vsTime = True
            else:
                break
        
        outputScaleFactor = np.array(1000/area) #for converstion to [mA/cm^2]

        tempFile = QTemporaryFile()
        tempFile.open()
        tempFile.writeData(fileBuffer)
        tempFile.flush()

        #read in data
        try:
            data = np.loadtxt(str(tempFile.fileName()),delimiter=delimiter)
            VV = data[:,0]
            II = data[:,1]
            if vsTime:
                time = data[:,2]
        except:
            self.ui.statusbar.showMessage('Could not read' + fileName +'. Prepend # to all non-data lines and try again',2500)
            return
        tempFile.close()
        tempFile.remove()

        
        if isMcFile: #convert to amps
            II = II/1000*area

        if not vsTime:
            #sort data by ascending voltage
            newOrder = VV.argsort()
            VV=VV[newOrder]
            II=II[newOrder]
            #remove duplicate voltage entries
            VV, indices = np.unique(VV, return_index =True)
            II = II[indices]
        else:
            #sort data by ascending time
            newOrder = time.argsort()
            VV=VV[newOrder]
            II=II[newOrder]
            time=time[newOrder]
            time=time-time[0]#start time at t=0

        #catch and fix flipped current sign:
        if II[0] < II[-1]:
            II = II * -1       

        indexInQuad1 = np.logical_and(VV>0,II>0)
        if any(indexInQuad1): #enters statement if there is at least one datapoint in quadrant 1
            isDarkCurve = False
        else:
            self.ui.statusbar.showMessage("Dark curve detected",500)
            isDarkCurve = True
        
        #put items in table
        self.ui.tableWidget.insertRow(self.rows)
        for ii in range(len(self.cols)):
            self.ui.tableWidget.setItem(self.rows,ii,QTableWidgetItem())        
        
        if not vsTime:
            fitParams, fitCovariance, infodict, errmsg, ier = self.bestEffortFit(VV,II)
        
            #print errmsg
    
            I0_fit = fitParams[0]
            Iph_fit = fitParams[1]
            Rs_fit = fitParams[2]
            Rsh_fit = fitParams[3]
            n_fit = fitParams[4]
    
            
            #0 -> LS-straight line
            #1 -> cubic spline interpolant
            smoothingParameter = 1-2e-6
            iFitSpline = SmoothSpline(VV, II, p=smoothingParameter)
    
            def cellModel(voltageIn):
                #voltageIn = np.array(voltageIn)
                return vectorizedCurrent(voltageIn, I0_fit, Iph_fit, Rs_fit, Rsh_fit, n_fit)
    
            def invCellPowerSpline(voltageIn):
                if voltageIn < 0:
                    return 0
                else:
                    return -1*voltageIn*iFitSpline(voltageIn)
    
            def invCellPowerModel(voltageIn):
                if voltageIn < 0:
                    return 0
                else:
                    return -1*voltageIn*cellModel(voltageIn)
    
            if not isDarkCurve:
                VVq1 = VV[indexInQuad1]
                IIq1 = II[indexInQuad1]
                vMaxGuess = VVq1[np.array(VVq1*IIq1).argmax()]
                powerSearchResults = optimize.minimize(invCellPowerSpline,vMaxGuess)
                #catch a failed max power search:
                if not powerSearchResults.status == 0:
                    print "power search exit code = " + str(powerSearchResults.status)
                    print powerSearchResults.message
                    vMax = nan
                    iMax = nan
                    pMax = nan
                else:
                    vMax = powerSearchResults.x[0]
                    iMax = iFitSpline([vMax])[0]
                    pMax = vMax*iMax                
    
                #only do this stuff if the char eqn fit was good
                if ier < 5:
                    powerSearchResults_charEqn = optimize.minimize(invCellPowerModel,vMaxGuess)
                    #catch a failed max power search:
                    if not powerSearchResults_charEqn.status == 0:
                        print "power search exit code = " + str(powerSearchResults_charEqn.status)
                        print powerSearchResults_charEqn.message
                        vMax_charEqn = nan
                    else:
                        vMax_charEqn = powerSearchResults_charEqn.x[0]
                    #dude
                    try:
                        Voc_nn_charEqn=optimize.brentq(cellModel, VV[0], VV[-1])
                    except:
                        Voc_nn_charEqn = nan
                else:
                    Voc_nn_charEqn = nan
                    vMax_charEqn = nan
    
    
                try:
                    Voc_nn = optimize.brentq(iFitSpline, VV[0], VV[-1])
                except:
                    Voc_nn = nan
    
            else:
                Voc_nn = nan
                vMax = nan
                iMax = nan
                pMax = nan
                Voc_nn_charEqn = nan
                vMax_charEqn = nan
                iMax_charEqn = nan
                pMax_charEqn = nan
    
    
    
            if ier < 5:
                dontFindBounds = False
                iMax_charEqn = cellModel([vMax_charEqn])[0]
                pMax_charEqn = vMax_charEqn*iMax_charEqn
                Isc_nn_charEqn = cellModel(0)
                FF_charEqn = pMax_charEqn/(Voc_nn_charEqn*Isc_nn_charEqn)
            else:
                dontFindBounds = True
                iMax_charEqn = nan
                pMax_charEqn = nan
                Isc_nn_charEqn = nan
                FF_charEqn = nan
    
            #there is a maddening bug in SmoothingSpline: it can't evaluate 0 alone, so I have to do this:
            try:
                Isc_nn = iFitSpline([0,1e-55])[0]
            except:
                Isc_nn = nan
    
            FF = pMax/(Voc_nn*Isc_nn)
    
            if (ier != 7) and (ier != 6) and (not dontFindBounds) and (type(fitCovariance) is not float):
                #error estimation:
                alpha = 0.05 # 95% confidence interval = 100*(1-alpha)
    
                nn = len(VV)    # number of data points
                p = len(fitParams) # number of parameters
    
                dof = max(0, nn - p) # number of degrees of freedom
    
                # student-t value for the dof and confidence level
                tval = t.ppf(1.0-alpha/2., dof) 
    
                lowers = []
                uppers = []
                #calculate 95% confidence interval
                for a, p,var in zip(range(nn), fitParams, np.diag(fitCovariance)):
                    sigma = var**0.5
                    lower = p - sigma*tval
                    upper = p + sigma*tval
                    lowers.append(lower)
                    uppers.append(upper)
    
            else:
                uppers = [nan,nan,nan,nan,nan]
                lowers = [nan,nan,nan,nan,nan]
    
            plotPoints = 1000
            fitX = np.linspace(VV[0],VV[-1],plotPoints)
            
            if ier < 5:
                modelY = cellModel(fitX)*outputScaleFactor
            else:
                modelY = np.empty(plotPoints)*nan
            splineY = iFitSpline(fitX)*outputScaleFactor
            graphData = {'vsTime':vsTime,'origRow':self.rows,'fitX':fitX,'modelY':modelY,'splineY':splineY,'i':II*outputScaleFactor,'v':VV,'Voc':Voc_nn,'Isc':Isc_nn*outputScaleFactor,'Vmax':vMax,'Imax':iMax*outputScaleFactor}		
    
            #export button
            exportBtn = QPushButton(self.ui.tableWidget)
            exportBtn.setText('Export')
            exportBtn.clicked.connect(self.handleButton)
            self.ui.tableWidget.setCellWidget(self.rows,self.cols.keys().index('exportBtn'), exportBtn)
              
            self.ui.tableWidget.item(self.rows,self.cols.keys().index('pce')).setData(Qt.DisplayRole,round(pMax/area*1e3,3))
            self.ui.tableWidget.item(self.rows,self.cols.keys().index('pce')).setToolTip(str(round(pMax_charEqn/area*1e3,3)))
            self.ui.tableWidget.item(self.rows,self.cols.keys().index('pmax')).setData(Qt.DisplayRole,round(pMax/area*1e3,3))
            self.ui.tableWidget.item(self.rows,self.cols.keys().index('pmax')).setToolTip(str(round(pMax_charEqn/area*1e3,3)))
            self.ui.tableWidget.item(self.rows,self.cols.keys().index('jsc')).setData(Qt.DisplayRole,round(Isc_nn/area*1e3,3))
            self.ui.tableWidget.item(self.rows,self.cols.keys().index('jsc')).setToolTip(str(round(Isc_nn_charEqn/area*1e3,3)))
            self.ui.tableWidget.item(self.rows,self.cols.keys().index('voc')).setData(Qt.DisplayRole,round(Voc_nn*1e3,3))
            self.ui.tableWidget.item(self.rows,self.cols.keys().index('voc')).setToolTip(str(round(Voc_nn_charEqn*1e3,3)))
            self.ui.tableWidget.item(self.rows,self.cols.keys().index('ff')).setData(Qt.DisplayRole,round(FF,3))
            self.ui.tableWidget.item(self.rows,self.cols.keys().index('ff')).setToolTip(str(round(FF_charEqn,3)))
            self.ui.tableWidget.item(self.rows,self.cols.keys().index('rs')).setData(Qt.DisplayRole,round(Rs_fit*area,3))
            self.ui.tableWidget.item(self.rows,self.cols.keys().index('rs')).setToolTip('[{0}  {1}]'.format(lowers[2]*area, uppers[2]*area))
            self.ui.tableWidget.item(self.rows,self.cols.keys().index('rsh')).setData(Qt.DisplayRole,round(Rsh_fit*area,3))
            self.ui.tableWidget.item(self.rows,self.cols.keys().index('rsh')).setToolTip('[{0}  {1}]'.format(lowers[3]*area, uppers[3]*area))
            self.ui.tableWidget.item(self.rows,self.cols.keys().index('jph')).setData(Qt.DisplayRole,round(Iph_fit/area*1e3,3))
            self.ui.tableWidget.item(self.rows,self.cols.keys().index('jph')).setToolTip('[{0}  {1}]'.format(lowers[1]/area*1e3, uppers[1]/area*1e3))
            self.ui.tableWidget.item(self.rows,self.cols.keys().index('j0')).setData(Qt.DisplayRole,round(I0_fit/area*1e9,3))
            self.ui.tableWidget.item(self.rows,self.cols.keys().index('j0')).setToolTip('[{0}  {1}]'.format(lowers[0]/area*1e9, uppers[0]/area*1e9))
            self.ui.tableWidget.item(self.rows,self.cols.keys().index('n')).setData(Qt.DisplayRole,round(n_fit,3))
            self.ui.tableWidget.item(self.rows,self.cols.keys().index('n')).setToolTip('[{0}  {1}]'.format(lowers[4], uppers[4]))
            self.ui.tableWidget.item(self.rows,self.cols.keys().index('Vmax')).setData(Qt.DisplayRole,round(vMax*1e3,3))
            self.ui.tableWidget.item(self.rows,self.cols.keys().index('Vmax')).setToolTip(str(round(vMax_charEqn*1e3,3)))
            self.ui.tableWidget.item(self.rows,self.cols.keys().index('area')).setData(Qt.DisplayRole,round(area,3))
            self.ui.tableWidget.item(self.rows,self.cols.keys().index('pmax2')).setData(Qt.DisplayRole,round(pMax*1e3,3))
            self.ui.tableWidget.item(self.rows,self.cols.keys().index('pmax2')).setToolTip(str(round(pMax_charEqn*1e3,3)))
            self.ui.tableWidget.item(self.rows,self.cols.keys().index('isc')).setData(Qt.DisplayRole,round(Isc_nn*1e3,3))
            self.ui.tableWidget.item(self.rows,self.cols.keys().index('isc')).setToolTip(str(round(Isc_nn_charEqn*1e3,3)))
            self.ui.tableWidget.item(self.rows,self.cols.keys().index('iph')).setData(Qt.DisplayRole,round(Iph_fit*1e3,3))
            self.ui.tableWidget.item(self.rows,self.cols.keys().index('iph')).setToolTip('[{0}  {1}]'.format(lowers[1]*1e3, uppers[1]*1e3))
            self.ui.tableWidget.item(self.rows,self.cols.keys().index('i0')).setData(Qt.DisplayRole,round(I0_fit*1e9,3))
            self.ui.tableWidget.item(self.rows,self.cols.keys().index('i0')).setToolTip('[{0}  {1}]'.format(lowers[0]*1e9, uppers[0]*1e9))
            self.ui.tableWidget.item(self.rows,self.cols.keys().index('rs2')).setData(Qt.DisplayRole,round(Rs_fit,3))
            self.ui.tableWidget.item(self.rows,self.cols.keys().index('rs2')).setToolTip('[{0}  {1}]'.format(lowers[2], uppers[2]))
            self.ui.tableWidget.item(self.rows,self.cols.keys().index('rsh2')).setData(Qt.DisplayRole,round(Rsh_fit,3))
            self.ui.tableWidget.item(self.rows,self.cols.keys().index('rsh2')).setToolTip('[{0}  {1}]'.format(lowers[3], uppers[3]))
        
        else:#vs time
            graphData = {'vsTime':vsTime,'origRow':self.rows,'time':time,'i':II*outputScaleFactor,'v':VV}

        #file name
        self.ui.tableWidget.item(self.rows,self.cols.keys().index('file')).setText(fileName)
        self.ui.tableWidget.item(self.rows,self.cols.keys().index('file')).setToolTip(''.join(header))          
        
        #plot button
        plotBtn = QPushButton(self.ui.tableWidget)
        plotBtn.setText('Plot')
        plotBtn.clicked.connect(self.handleButton)
        self.ui.tableWidget.setCellWidget(self.rows,self.cols.keys().index('plotBtn'), plotBtn)
        self.ui.tableWidget.item(self.rows,self.cols.keys().index('plotBtn')).setData(Qt.UserRole,graphData)
        
        self.ui.tableWidget.resizeColumnsToContents()
        self.rows = self.rows + 1


    def bestEffortFit(self,VV,II):

        #splineTestVV=np.linspace(VV[0],VV[-1],1000)
        #splineTestII=iFitSpline(splineTestVV)
        #p1, = plt.plot(splineTestVV,splineTestII)
        #p3, = plt.plot(VV,II,ls='None',marker='o', label='Data')
        #plt.draw()
        #plt.show()            

        #data point selection:
        #lowest voltage (might be same as Isc)
        V_start_n = VV[0]
        I_start_n = II[0]

        #highest voltage
        V_end_n = VV[-1]
        I_end_n = II[-1]

        #Isc
        iFit = interpolate.interp1d(VV,II)
        V_sc_n = 0
        try:
            I_sc_n = float(iFit(V_sc_n))
        except:
            return([[nan,nan,nan,nan,nan], [nan,nan,nan,nan,nan], nan, "hard fail", 10])

        #mpp
        VVcalc = VV-VV[0]
        IIcalc = II-min(II)
        Pvirtual= np.array(VVcalc*IIcalc)
        vMaxIndex = Pvirtual.argmax()
        V_vmpp_n = VV[vMaxIndex]
        I_vmpp_n = II[vMaxIndex]

        #Vp: half way in voltage between vMpp and the start of the dataset:
        V_vp_n = (V_vmpp_n-V_start_n)/2 +V_start_n
        try:
            I_vp_n = float(iFit(V_vp_n))
        except:
            return([[nan,nan,nan,nan,nan], [nan,nan,nan,nan,nan], nan, "hard fail", 10])

        #Ip: half way in current between vMpp and the end of the dataset:
        I_ip_n = (I_vmpp_n-I_end_n)/2 + I_end_n
        iFit2 = interpolate.interp1d(VV,II-I_ip_n)
        try:
            V_ip_n = optimize.brentq(iFit2, VV[0], VV[-1])
        except:
            return([[nan,nan,nan,nan,nan], [nan,nan,nan,nan,nan], nan, "hard fail", 10])

        diaplayAllGuesses = False
        def evaluateGuessPlot(dataX, dataY, myguess):
            myguess = [float(x) for x in myguess]
            print "myguess:"
            print myguess
            vv=np.linspace(min(dataX),max(dataX),1000)
            ii=vectorizedCurrent(vv,myguess[0],myguess[1],myguess[2],myguess[3],myguess[4])
            plt.title('Guess and raw data')
            plt.plot(vv,ii)
            plt.scatter(dataX,dataY)
            plt.grid(b=True)
            plt.draw()
            plt.show()

        #phase 1 guesses:
        I_L_initial_guess = I_sc_n
        R_sh_initial_guess = 1e6

        #compute intellegent guesses for Iph, Rsh by forcing the curve through several data points and numerically solving the resulting system of eqns
        newRhs = rhs - I
        aLine = Rsh*V+Iph-I
        eqnSys1 = aLine.subs([(V,V_start_n),(I,I_start_n)])
        eqnSys2 = aLine.subs([(V,V_vp_n),(I,I_vp_n)])

        eqnSys = (eqnSys1,eqnSys2)

        try:
            nGuessSln = sympy.nsolve(eqnSys,(Iph,Rsh),(I_L_initial_guess,R_sh_initial_guess),maxsteps=10000)
        except:
            return([[nan,nan,nan,nan,nan], [nan,nan,nan,nan,nan], nan, "hard fail", 10])


        I_L_guess = nGuessSln[0]
        R_sh_guess = -1*1/nGuessSln[1]
        R_s_guess = -1*(V_end_n-V_ip_n)/(I_end_n-I_ip_n)
        n_initial_guess = 2 #TODO: maybe a more intelegant guess for n can be found using http://pvcdrom.pveducation.org/CHARACT/IDEALITY.HTM
        I0_initial_guess = eyeNot[0].evalf(subs={Vth:thermalVoltage,Rs:R_s_guess,Rsh:R_sh_guess,Iph:I_L_guess,n:n_initial_guess,I:I_ip_n,V:V_ip_n})                         

        initial_guess = [I0_initial_guess, I_L_guess, R_s_guess, R_sh_guess, n_initial_guess]
        if diaplayAllGuesses:
            evaluateGuessPlot(VV, II, initial_guess)
            
        # let's try the fit now, if it works great, we're done, otherwise we can continue
        #try:
            #guess = initial_guess
            #fitParams, fitCovariance, infodict, errmsg, ier = optimize.curve_fit(optimizeThis, VV, II,p0=guess,full_output = True,xtol=1e-13,ftol=1e-15)
            #return(fitParams, fitCovariance, infodict, errmsg, ier)
        #except:
            #pass        

        #refine guesses for I0 and Rs by forcing the curve through several data points and numerically solving the resulting system of eqns
        eqnSys1 = newRhs.subs([(Vth,thermalVoltage),(Iph,I_L_guess),(V,V_ip_n),(I,I_ip_n),(n,n_initial_guess),(Rsh,R_sh_guess)])
        eqnSys2 = newRhs.subs([(Vth,thermalVoltage),(Iph,I_L_guess),(V,V_end_n),(I,I_end_n),(n,n_initial_guess),(Rsh,R_sh_guess)])
        eqnSys = (eqnSys1,eqnSys2)
        
        try:
            nGuessSln = sympy.nsolve(eqnSys,(I0,Rs),(I0_initial_guess,R_s_guess),maxsteps=10000)
        except:
            return([[nan,nan,nan,nan,nan], [nan,nan,nan,nan,nan], nan, "hard fail", 10])

        I0_guess = nGuessSln[0]
        R_s_guess = nGuessSln[1]
        
        #Rs_initial_guess = RsEqn[0].evalf(subs={I0:I0_initial_guess,Vth:thermalVoltage,Rsh:R_sh_guess,Iph:I_L_guess,n:n_initial_guess,I:I_end_n,V:V_end_n})
        #I0_guess = I0_initial_guess
        #R_s_guess = Rs_initial_guess
        
        guess = [I0_guess, I_L_guess, R_s_guess, R_sh_guess, n_initial_guess]
        if diaplayAllGuesses:
            evaluateGuessPlot(VV, II, guess)
            
        #nidf

        #give 5x weight to data around mpp
        #nP = II*VV
        #maxIndex = np.argmax(nP)
        #weights = np.ones(len(II))
        #halfRange = (V_ip_n-VV[vMaxIndex])/2
        #upperTarget = VV[vMaxIndex] + halfRange
        #lowerTarget = VV[vMaxIndex] - halfRange
        #lowerTarget = 0
        #upperTarget = V_oc_n
        #lowerI = np.argmin(abs(VV-lowerTarget))
        #upperI = np.argmin(abs(VV-upperTarget))
        #weights[range(lowerI,upperI)] = 3
        #weights[maxnpi] = 10
        #todo: play with setting up "key points"

        guess = [float(x) for x in guess]

        #odrMod = odr.Model(odrThing)
        #myData = odr.Data(VV,II)
        #myodr = odr.ODR(myData, odrMod, beta0=guess,maxit=5000,sstol=1e-20,partol=1e-20)#
        #myoutput = myodr.run()
        #myoutput.pprint()
        #see http://docs.scipy.org/doc/external/odrpack_guide.pdf


        try:
            #myoutput = myodr.run()
            #fitParams = myoutput.beta
            #print myoutput.stopreason
            #print myoutput.info
            #ier = 1
            fitParams, fitCovariance, infodict, errmsg, ier = optimize.curve_fit(optimizeThis, VV, II,p0=guess,full_output = True,xtol=1e-13,ftol=1e-15)
            #fitParams, fitCovariance, infodict, errmsg, ier = optimize.leastsq(func=residual, args=(VV, II, np.ones(len(II))),x0=guess,full_output=1,xtol=1e-12,ftol=1e-14)#,xtol=1e-12,ftol=1e-14,maxfev=12000
            #fitParams, fitCovariance, infodict, errmsg, ier = optimize.leastsq(func=residual, args=(VV, II, weights),x0=fitParams,full_output=1,ftol=1e-15,xtol=0)#,xtol=1e-12,ftol=1e-14            
        
            alwaysShowRecap = False
            if  alwaysShowRecap:
                vv=np.linspace(VV[0],VV[-1],1000)
                print "fit:"
                print fitParams                
                print "guess:"
                print guess
                print ier
                print errmsg
                ii=vectorizedCurrent(vv,guess[0],guess[1],guess[2],guess[3],guess[4])
                ii2=vectorizedCurrent(vv,fitParams[0],fitParams[1],fitParams[2],fitParams[3],fitParams[4])
                plt.title('Fit analysis')
                p1, = plt.plot(vv,ii, label='Guess',ls='--')
                p2, = plt.plot(vv,ii2, label='Fit')
                p3, = plt.plot(VV,II,ls='None',marker='o', label='Data')
                #p4, = plt.plot(VV[range(lowerI,upperI)],II[range(lowerI,upperI)],ls="None",marker='o', label='5x Weight Data')
                ax = plt.gca()
                handles, labels = ax.get_legend_handles_labels()
                ax.legend(handles, labels, loc=3)
                plt.grid(b=True)
                plt.draw()
                plt.show()
            return(fitParams, fitCovariance, infodict, errmsg, ier)
        except:
            return([[nan,nan,nan,nan,nan], [nan,nan,nan,nan,nan], nan, "hard fail", 10])



    def openCall(self):
        #remember the last path th user opened
        if self.settings.contains('lastFolder'):
            openDir = self.settings.value('lastFolder').toString()
        else:
            openDir = '.'

        fileNames = QFileDialog.getOpenFileNamesAndFilter(directory = openDir, caption="Select one or more files to open", filter = '(*.txt *.csv);;Folders (*)')       
        #fileNames = QFileDialog.getExistingDirectory(directory = openDir, caption="Select one or more files to open")       
        
        if len(fileNames[0])>0:#check if user clicked cancel
            self.workingDirectory = os.path.dirname(str(fileNames[0][0]))
            self.settings.setValue('lastFolder',self.workingDirectory)
            for fullPath in fileNames[0]:
                fullPath = str(fullPath)
                self.processFile(fullPath)
        
            if self.ui.actionEnable_Watching.isChecked():
                watchedDirs = self.watcher.directories()
                self.watcher.removePaths(watchedDirs)
                self.watcher.addPath(self.workingDirectory)
                self.handleWatchUpdate(self.workingDirectory)
    
    #user chose file --> watch
    def handleWatchAction(self):
        #remember the last path th user opened
        if self.settings.contains('lastFolder'):
            openDir = self.settings.value('lastFolder').toString()
        else:
            openDir = '.'
        
        myDir = QFileDialog.getExistingDirectory(directory = openDir, caption="Select folder to watch")
        
        if len(myDir)>0:#check if user clicked cancel
            self.workingDirectory = str(myDir)
            self.settings.setValue('lastFolder',self.workingDirectory)
            self.ui.actionEnable_Watching.setChecked(True)
            watchedDirs = self.watcher.directories()
            self.watcher.removePaths(watchedDirs)
            self.watcher.addPath(self.workingDirectory)
            self.handleWatchUpdate(self.workingDirectory)
    
    #user toggeled Tools --> Enable Watching
    def watchCall(self):
        watchedDirs = self.watcher.directories()
        self.watcher.removePaths(watchedDirs)
        if self.ui.actionEnable_Watching.isChecked():
            if (self.workingDirectory != ''):
                self.watcher.addPath(self.workingDirectory)
                self.handleWatchUpdate(self.workingDirectory)
            
    def handleWatchUpdate(self,path):
        myDir = QDir(path)
        myDir.setNameFilters(self.supportedExtensions)
        allFilesNow = myDir.entryList()
        allFilesNow = list(allFilesNow)
        allFilesNow = [str(item) for item in allFilesNow]

        differentFiles = list(set(allFilesNow) ^ set(self.fileNames))
        if differentFiles != []:
            for aFile in differentFiles:
                if self.fileNames.__contains__(aFile):
                    #TODO: delete the file from the table
                    self.ui.statusbar.showMessage('Removed' + aFile,2500)
                else:
                    #process the new file
                    self.processFile(os.path.join(self.workingDirectory,aFile))
Ejemplo n.º 4
0
class Watcher(QObject):
    " Filesystem watcher implementation "

    fsChanged = pyqtSignal(list)

    def __init__(self, excludeFilters, dirToWatch):

        QObject.__init__(self)
        self.__dirWatcher = QFileSystemWatcher(self)

        # data members
        self.__excludeFilter = []  # Files exclude filter
        self.__srcDirsToWatch = set()  # Came from the user

        self.__fsTopLevelSnapshot = {}  # Current snapshot
        self.__fsSnapshot = {}  # Current snapshot

        # Sets of dirs which are currently watched
        self.__dirsToWatch = set()
        self.__topLevelDirsToWatch = set()  # Generated till root

        # precompile filters
        for flt in excludeFilters:
            self.__excludeFilter.append(re.compile(flt))

        # Initialise the list of dirs to watch
        self.__srcDirsToWatch.add(dirToWatch)

        self.__topLevelDirsToWatch = self.__buildTopDirsList(
            self.__srcDirsToWatch)
        self.__fsTopLevelSnapshot = self.__buildTopLevelSnapshot(
            self.__topLevelDirsToWatch, self.__srcDirsToWatch)
        self.__dirsToWatch = self.__buildSnapshot()

        # Here __dirsToWatch and __topLevelDirsToWatch have a complete
        # set of what should be watched

        # Add the dirs to the watcher
        dirs = []
        for path in self.__dirsToWatch | self.__topLevelDirsToWatch:
            dirs.append(path)
        self.__dirWatcher.addPaths(dirs)
        self.__dirWatcher.directoryChanged.connect(self.__onDirChanged)

        # self.debug()
        return

    @staticmethod
    def __buildTopDirsList(srcDirs):
        " Takes a list of dirs to be watched and builds top dirs set "
        topDirsList = set()
        for path in srcDirs:
            parts = path.split(os.path.sep)
            for index in xrange(1, len(parts) - 1):
                candidate = os.path.sep.join(parts[0:index]) + os.path.sep
                if os.path.exists(candidate):
                    if os.access(candidate, os.R_OK):
                        topDirsList.add(candidate)
        return topDirsList

    @staticmethod
    def __buildTopLevelSnapshot(topLevelDirs, srcDirs):
        " Takes top level dirs and builds their snapshot "
        snapshot = {}
        for path in topLevelDirs:
            itemsSet = set()
            # search for all the dirs to be watched
            for candidate in topLevelDirs | srcDirs:
                if len(candidate) <= len(path):
                    continue
                if candidate.startswith(path):
                    candidate = candidate[len(path):]
                    slashIndex = candidate.find(os.path.sep) + 1
                    item = candidate[:slashIndex]
                    if os.path.exists(path + item):
                        itemsSet.add(item)
            snapshot[path] = itemsSet
        return snapshot

    def __buildSnapshot(self):
        " Builds the filesystem snapshot "
        snapshotDirs = set()
        for path in self.__srcDirsToWatch:
            self.__addSnapshotPath(path, snapshotDirs)
        return snapshotDirs

    def __addSnapshotPath(self, path, snapshotDirs, itemsToReport=None):
        " Adds one path to the FS snapshot "
        if not os.path.exists(path):
            return

        snapshotDirs.add(path)
        dirItems = set()
        for item in os.listdir(path):
            if self.__shouldExclude(item):
                continue
            if os.path.isdir(path + item):
                dirName = path + item + os.path.sep
                dirItems.add(item + os.path.sep)
                if itemsToReport is not None:
                    itemsToReport.append("+" + dirName)
                self.__addSnapshotPath(dirName, snapshotDirs, itemsToReport)
                continue
            dirItems.add(item)
            if itemsToReport is not None:
                itemsToReport.append("+" + path + item)
        self.__fsSnapshot[path] = dirItems
        return

    def __onDirChanged(self, path):
        " Triggered when the dir is changed "

        path = str(path)
        if not path.endswith(os.path.sep):
            path = path + os.path.sep

        # Check if it is a top level dir
        try:
            oldSet = self.__fsTopLevelSnapshot[path]

            # Build a new set of what is in that top level dir
            newSet = set()
            for item in os.listdir(path):
                if not os.path.isdir(path + item):
                    continue  # Only dirs are of interest for the top level
                item = item + os.path.sep
                if item in oldSet:
                    newSet.add(item)
            # Now we have an old set and a new one with those from the old
            # which actually exist
            diff = oldSet - newSet

            # diff are those which disappeared. We need to do the following:
            # - build a list of all the items in the fs snapshot which start
            #   from this dir
            # - build a list of dirs which should be deregistered from the
            #   watcher. This list includes both top level and project level
            # - deregister dirs from the watcher
            # - emit a signal of what disappeared
            if not diff:
                return  # no changes

            self.__fsTopLevelSnapshot[path] = newSet

            dirsToBeRemoved = []
            itemsToReport = []

            for item in diff:
                self.__processRemoveTopDir(path + item, dirsToBeRemoved,
                                           itemsToReport)

            # Here: it is possible that the last dir to watch disappeared
            if not newSet:
                # There is nothing to watch here anymore
                dirsToBeRemoved.append(path)
                del self.__fsTopLevelSnapshot[path]

                parts = path[1:-1].split(os.path.sep)
                for index in xrange(len(parts) - 2, 0, -1):
                    candidate = os.path.sep + \
                                os.path.sep.join( parts[ 0 : index ] ) + \
                                os.path.sep
                    dirSet = self.__fsTopLevelSnapshot[candidate]
                    dirSet.remove(parts[index + 1] + os.path.sep)
                    if not dirSet:
                        dirsToBeRemoved.append(candidate)
                        del self.__fsTopLevelSnapshot[candidate]
                        continue
                    break  # it is not the last item in the set

            # Update the watcher
            if dirsToBeRemoved:
                self.__dirWatcher.removePaths(dirsToBeRemoved)

            # Report
            if itemsToReport:
                self.fsChanged.emit(itemsToReport)
            return
        except:
            # it is not a top level dir - no key
            pass

        # Here: the change is in the project level dir
        try:
            oldSet = self.__fsSnapshot[path]

            # Build a new set of what is in that top level dir
            newSet = set()
            for item in os.listdir(path):
                if self.__shouldExclude(item):
                    continue
                if os.path.isdir(path + item):
                    newSet.add(item + os.path.sep)
                else:
                    newSet.add(item)

            # Here: we have a new and old snapshots
            # Lets calculate the difference
            deletedItems = oldSet - newSet
            addedItems = newSet - oldSet

            if not deletedItems and not addedItems:
                return  # No changes

            # Update the changed dir set
            self.__fsSnapshot[path] = newSet

            # We need to build some lists:
            # - list of files which were added
            # - list of dirs which were added
            # - list of files which were deleted
            # - list of dirs which were deleted
            # The deleted dirs must be unregistered in the watcher
            # The added dirs must be registered
            itemsToReport = []
            dirsToBeAdded = []
            dirsToBeRemoved = []

            for item in addedItems:
                if item.endswith(os.path.sep):
                    # directory was added
                    self.__processAddedDir(path + item, dirsToBeAdded,
                                           itemsToReport)
                else:
                    itemsToReport.append("+" + path + item)

            for item in deletedItems:
                if item.endswith(os.path.sep):
                    # directory was deleted
                    self.__processRemovedDir(path + item, dirsToBeRemoved,
                                             itemsToReport)
                else:
                    itemsToReport.append("-" + path + item)

            # Update the watcher
            if dirsToBeRemoved:
                self.__dirWatcher.removePaths(dirsToBeRemoved)
            if dirsToBeAdded:
                self.__dirWatcher.addPaths(dirsToBeAdded)

            # Report
            self.fsChanged.emit(itemsToReport)

        except:
            # It could be a queued signal about what was already reported
            pass

        # self.debug()
        return

    def __shouldExclude(self, name):
        " Tests if a file must be excluded "
        for excl in self.__excludeFilter:
            if excl.match(name):
                return True
        return False

    def __processAddedDir(self, path, dirsToBeAdded, itemsToReport):
        " called for an appeared dir in the project tree "
        dirsToBeAdded.append(path)
        itemsToReport.append("+" + path)

        # it should add dirs recursively into the snapshot and care
        # of the items to report
        dirItems = set()
        for item in os.listdir(path):
            if self.__shouldExclude(item):
                continue
            if os.path.isdir(path + item):
                dirName = path + item + os.path.sep
                dirItems.add(item + os.path.sep)
                self.__processAddedDir(dirName, dirsToBeAdded, itemsToReport)
                continue
            itemsToReport.append("+" + path + item)
            dirItems.add(item)
        self.__fsSnapshot[path] = dirItems
        return

    def __processRemovedDir(self, path, dirsToBeRemoved, itemsToReport):
        " called for a disappeared dir in the project tree "

        # it should remove the dirs recursively from the fs snapshot
        # and care of items to report
        dirsToBeRemoved.append(path)
        itemsToReport.append("-" + path)

        oldSet = self.__fsSnapshot[path]
        for item in oldSet:
            if item.endswith(os.path.sep):
                # Nested dir
                self.__processRemovedDir(path + item, dirsToBeRemoved,
                                         itemsToReport)
            else:
                # a file
                itemsToReport.append("-" + path + item)
        del self.__fsSnapshot[path]
        return

    def __processRemoveTopDir(self, path, dirsToBeRemoved, itemsToReport):
        " Called for a disappeared top level dir "

        if path in self.__fsTopLevelSnapshot:
            # It is still a top level dir
            dirsToBeRemoved.append(path)
            for item in self.__fsTopLevelSnapshot[path]:
                self.__processRemoveTopDir(path + item, dirsToBeRemoved,
                                           itemsToReport)
            del self.__fsTopLevelSnapshot[path]
        else:
            # This is a project level dir
            self.__processRemovedDir(path, dirsToBeRemoved, itemsToReport)
        return

    def reset(self):
        " Resets the watcher (it does not report any changes) "
        self.__dirWatcher.removePaths(self.__dirWatcher.directories())

        self.__srcDirsToWatch = set()

        self.__fsTopLevelSnapshot = {}
        self.__fsSnapshot = {}

        self.__dirsToWatch = set()
        self.__topLevelDirsToWatch = set()
        return

    def registerDir(self, path):
        " Adds a directory to the list of watched ones "
        if not path.endswith(os.path.sep):
            path = path + os.path.sep

        if path in self.__srcDirsToWatch:
            return  # It is there already

        # It is necessary to do the following:
        # - add the dir to the fs snapshot
        # - collect dirs to add to the watcher
        # - collect items to report
        self.__srcDirsToWatch.add(path)

        dirsToWatch = set()
        itemsToReport = []
        self.__registerDir(path, dirsToWatch, itemsToReport)

        # It might be that top level dirs should be updated too
        newTopLevelDirsToWatch = self.__buildTopDirsList(self.__srcDirsToWatch)
        addedDirs = newTopLevelDirsToWatch - self.__topLevelDirsToWatch

        for item in addedDirs:
            dirsToWatch.add(item)

            # Identify items to be watched by this dir
            dirItems = set()
            for candidate in newTopLevelDirsToWatch | self.__srcDirsToWatch:
                if len(candidate) <= len(item):
                    continue
                if candidate.startswith(item):
                    candidate = candidate[len(item):]
                    slashIndex = candidate.find(os.path.sep) + 1
                    dirName = candidate[:slashIndex]
                    if os.path.exists(item + dirName):
                        dirItems.add(dirName)
            # Update the top level dirs snapshot
            self.__fsTopLevelSnapshot[item] = dirItems

        # Update the top level snapshot with the added dir
        upperDir = os.path.dirname(path[:-1]) + os.path.sep
        dirName = path.replace(upperDir, '')
        self.__fsTopLevelSnapshot[upperDir].add(dirName)

        # Update the list of top level dirs to watch
        self.__topLevelDirsToWatch = newTopLevelDirsToWatch

        # Update the watcher
        if dirsToWatch:
            dirs = []
            for item in dirsToWatch:
                dirs.append(item)
            self.__dirWatcher.addPaths(dirs)

        # Report the changes
        if itemsToReport:
            self.fsChanged.emit(itemsToReport)

        # self.debug()
        return

    def __registerDir(self, path, dirsToWatch, itemsToReport):
        " Adds one path to the FS snapshot "
        if not os.path.exists(path):
            return

        dirsToWatch.add(path)
        itemsToReport.append("+" + path)

        dirItems = set()
        for item in os.listdir(path):
            if self.__shouldExclude(item):
                continue
            if os.path.isdir(path + item):
                dirName = path + item + os.path.sep
                dirItems.add(item + os.path.sep)
                itemsToReport.append("+" + path + item + os.path.sep)
                self.__addSnapshotPath(dirName, dirsToWatch, itemsToReport)
                continue
            dirItems.add(item)
            itemsToReport.append("+" + path + item)
        self.__fsSnapshot[path] = dirItems
        return

    def deregisterDir(self, path):
        " Removes the directory from the list of the watched ones "

        if not path.endswith(os.path.sep):
            path = path + os.path.sep

        if path not in self.__srcDirsToWatch:
            return  # It is not there already
        self.__srcDirsToWatch.remove(path)

        # It is necessary to do the following:
        # - remove the dir from the fs snapshot
        # - collect the dirs to be removed from watching
        # - collect item to report

        itemsToReport = []
        dirsToBeRemoved = []

        self.__deregisterDir(path, dirsToBeRemoved, itemsToReport)

        # It is possible that some of the top level watched dirs should be
        # removed as well
        newTopLevelDirsToWatch = self.__buildTopDirsList(self.__srcDirsToWatch)
        deletedDirs = self.__topLevelDirsToWatch - newTopLevelDirsToWatch

        for item in deletedDirs:
            dirsToBeRemoved.append(item)
            del self.__fsTopLevelSnapshot[item]

        # It might be the case that some of the items should be deleted in the
        # top level dirs sets
        for dirName in self.__fsTopLevelSnapshot:
            itemsSet = self.__fsTopLevelSnapshot[dirName]
            for item in itemsSet:
                candidate = dirName + item
                if candidate == path or candidate in deletedDirs:
                    itemsSet.remove(item)
                    self.__fsTopLevelSnapshot[dirName] = itemsSet
                    break

        # Update the list of dirs to be watched
        self.__topLevelDirsToWatch = newTopLevelDirsToWatch

        # Update the watcher
        if dirsToBeRemoved:
            self.__dirWatcher.removePaths(dirsToBeRemoved)

        # Report the changes
        if itemsToReport:
            self.fsChanged.emit(itemsToReport)

        # self.debug()
        return

    def __deregisterDir(self, path, dirsToBeRemoved, itemsToReport):
        " Deregisters a directory recursively "
        dirsToBeRemoved.append(path)
        itemsToReport.append("-" + path)
        if path in self.__fsTopLevelSnapshot:
            # This is a top level dir
            for item in self.__fsTopLevelSnapshot[path]:
                if item.endswith(os.path.sep):
                    # It's a dir
                    self.__deregisterDir(path + item, dirsToBeRemoved,
                                         itemsToReport)
                else:
                    # It's a file
                    itemsToReport.append("-" + path + item)
            del self.__fsTopLevelSnapshot[path]
            return

        # It is from an a project level snapshot
        if path in self.__fsSnapshot:
            for item in self.__fsSnapshot[path]:
                if item.endswith(os.path.sep):
                    # It's a dir
                    self.__deregisterDir(path + item, dirsToBeRemoved,
                                         itemsToReport)
                else:
                    # It's a file
                    itemsToReport.append("-" + path + item)
            del self.__fsSnapshot[path]
        return

    def debug(self):
        print "Top level dirs to watch: " + str(self.__topLevelDirsToWatch)
        print "Project dirs to watch: " + str(self.__dirsToWatch)

        print "Top level snapshot: " + str(self.__fsTopLevelSnapshot)
        print "Project snapshot: " + str(self.__fsSnapshot)