eric7/Tasks/TaskViewer.py

branch
eric7
changeset 8312
800c432b34c8
parent 8280
17d03699f151
child 8318
962bce857696
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eric7/Tasks/TaskViewer.py	Sat May 15 18:45:04 2021 +0200
@@ -0,0 +1,922 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2005 - 2021 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing a task viewer and associated classes.
+
+Tasks can be defined manually or automatically. Automatically
+generated tasks are derived from a comment with a special
+introductory text. This text is configurable.
+"""
+
+import os
+import fnmatch
+import threading
+
+from PyQt5.QtCore import pyqtSignal, Qt, QThread
+from PyQt5.QtWidgets import (
+    QHeaderView, QLineEdit, QTreeWidget, QDialog, QInputDialog, QApplication,
+    QMenu, QAbstractItemView, QTreeWidgetItem
+)
+
+from E5Gui.E5Application import e5App
+from E5Gui import E5MessageBox
+from E5Gui.E5ProgressDialog import E5ProgressDialog
+
+from .Task import Task, TaskType, TaskPriority
+
+import UI.PixmapCache
+
+import Preferences
+import Utilities
+
+from Utilities.AutoSaver import AutoSaver
+    
+
+class TaskViewer(QTreeWidget):
+    """
+    Class implementing the task viewer.
+    
+    @signal displayFile(str, int) emitted to go to a file task
+    """
+    displayFile = pyqtSignal(str, int)
+    
+    def __init__(self, parent, project):
+        """
+        Constructor
+        
+        @param parent the parent (QWidget)
+        @param project reference to the project object
+        """
+        super().__init__(parent)
+        
+        self.setSortingEnabled(True)
+        self.setExpandsOnDoubleClick(False)
+        
+        self.__headerItem = QTreeWidgetItem(
+            ["", "", self.tr("Summary"), self.tr("Filename"),
+             self.tr("Line"), ""])
+        self.__headerItem.setIcon(
+            0, UI.PixmapCache.getIcon("taskCompleted"))
+        self.__headerItem.setIcon(
+            1, UI.PixmapCache.getIcon("taskPriority"))
+        self.setHeaderItem(self.__headerItem)
+        
+        self.header().setSortIndicator(2, Qt.SortOrder.AscendingOrder)
+        self.__resizeColumns()
+        
+        self.tasks = []
+        self.copyTask = None
+        self.projectOpen = False
+        self.project = project
+        self.__projectTasksScanFilter = ""
+        
+        from .TaskFilter import TaskFilter
+        self.taskFilter = TaskFilter()
+        self.taskFilter.setActive(False)
+        
+        self.__projectTasksSaveTimer = AutoSaver(self, self.saveProjectTasks)
+        self.__projectTaskExtractionThread = ProjectTaskExtractionThread()
+        self.__projectTaskExtractionThread.taskFound.connect(self.addFileTask)
+        
+        self.__projectTasksMenu = QMenu(
+            self.tr("P&roject Tasks"), self)
+        self.__projectTasksMenu.addAction(
+            self.tr("&Regenerate project tasks"),
+            self.regenerateProjectTasks)
+        self.__projectTasksMenu.addSeparator()
+        self.__projectTasksMenu.addAction(
+            self.tr("&Configure scan options"),
+            self.__configureProjectTasksScanOptions)
+        
+        self.__menu = QMenu(self)
+        self.__menu.addAction(self.tr("&New Task..."), self.__newTask)
+        self.subtaskItem = self.__menu.addAction(
+            self.tr("New &Sub-Task..."), self.__newSubTask)
+        self.__menu.addSeparator()
+        self.projectTasksMenuItem = self.__menu.addMenu(
+            self.__projectTasksMenu)
+        self.__menu.addSeparator()
+        self.gotoItem = self.__menu.addAction(
+            self.tr("&Go To"), self.__goToTask)
+        self.__menu.addSeparator()
+        self.copyItem = self.__menu.addAction(
+            self.tr("&Copy"), self.__copyTask)
+        self.pasteItem = self.__menu.addAction(
+            self.tr("&Paste"), self.__pasteTask)
+        self.pasteMainItem = self.__menu.addAction(
+            self.tr("Paste as &Main Task"), self.__pasteMainTask)
+        self.deleteItem = self.__menu.addAction(
+            self.tr("&Delete"), self.__deleteTask)
+        self.__menu.addSeparator()
+        self.markCompletedItem = self.__menu.addAction(
+            self.tr("&Mark Completed"), self.__markCompleted)
+        self.__menu.addAction(
+            self.tr("Delete Completed &Tasks"), self.__deleteCompleted)
+        self.__menu.addSeparator()
+        self.__menu.addAction(
+            self.tr("P&roperties..."), self.__editTaskProperties)
+        self.__menu.addSeparator()
+        self.__menuFilteredAct = self.__menu.addAction(
+            self.tr("&Filtered display"))
+        self.__menuFilteredAct.setCheckable(True)
+        self.__menuFilteredAct.setChecked(False)
+        self.__menuFilteredAct.triggered[bool].connect(self.__activateFilter)
+        self.__menu.addAction(
+            self.tr("Filter c&onfiguration..."), self.__configureFilter)
+        self.__menu.addSeparator()
+        self.__menu.addAction(
+            self.tr("Resi&ze columns"), self.__resizeColumns)
+        self.__menu.addSeparator()
+        self.__menu.addAction(self.tr("Configure..."), self.__configure)
+        
+        self.__backMenu = QMenu(self)
+        self.__backMenu.addAction(self.tr("&New Task..."), self.__newTask)
+        self.__backMenu.addSeparator()
+        self.backProjectTasksMenuItem = self.__backMenu.addMenu(
+            self.__projectTasksMenu)
+        self.__backMenu.addSeparator()
+        self.backPasteItem = self.__backMenu.addAction(
+            self.tr("&Paste"), self.__pasteTask)
+        self.backPasteMainItem = self.__backMenu.addAction(
+            self.tr("Paste as &Main Task"), self.__pasteMainTask)
+        self.__backMenu.addSeparator()
+        self.backDeleteCompletedItem = self.__backMenu.addAction(
+            self.tr("Delete Completed &Tasks"), self.__deleteCompleted)
+        self.__backMenu.addSeparator()
+        self.__backMenuFilteredAct = self.__backMenu.addAction(
+            self.tr("&Filtered display"))
+        self.__backMenuFilteredAct.setCheckable(True)
+        self.__backMenuFilteredAct.setChecked(False)
+        self.__backMenuFilteredAct.triggered[bool].connect(
+            self.__activateFilter)
+        self.__backMenu.addAction(
+            self.tr("Filter c&onfiguration..."), self.__configureFilter)
+        self.__backMenu.addSeparator()
+        self.__backMenu.addAction(
+            self.tr("Resi&ze columns"), self.__resizeColumns)
+        self.__backMenu.addSeparator()
+        self.__backMenu.addAction(
+            self.tr("Configure..."), self.__configure)
+        
+        self.__activating = False
+        
+        self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
+        self.customContextMenuRequested.connect(self.__showContextMenu)
+        self.itemActivated.connect(self.__taskItemActivated)
+        
+        self.setWindowIcon(UI.PixmapCache.getIcon("eric"))
+        
+        self.__generateTopLevelItems()
+    
+    def __generateTopLevelItems(self):
+        """
+        Private method to generate the 'Extracted Tasks' item.
+        """
+        self.__extractedItem = QTreeWidgetItem(self,
+                                               [self.tr("Extracted Tasks")])
+        self.__manualItem = QTreeWidgetItem(self,
+                                            [self.tr("Manual Tasks")])
+        for itm in [self.__extractedItem, self.__manualItem]:
+            itm.setFirstColumnSpanned(True)
+            itm.setExpanded(True)
+            itm.setHidden(True)
+            font = itm.font(0)
+            font.setUnderline(True)
+            itm.setFont(0, font)
+    
+    def __checkTopLevelItems(self):
+        """
+        Private slot to check the 'Extracted Tasks' item for children.
+        """
+        for itm in [self.__extractedItem, self.__manualItem]:
+            visibleCount = itm.childCount()
+            for index in range(itm.childCount()):
+                if itm.child(index).isHidden():
+                    visibleCount -= 1
+            itm.setHidden(visibleCount == 0)
+    
+    def __resort(self):
+        """
+        Private method to resort the tree.
+        """
+        self.sortItems(self.sortColumn(), self.header().sortIndicatorOrder())
+    
+    def __resizeColumns(self):
+        """
+        Private method to resize the list columns.
+        """
+        self.header().resizeSections(QHeaderView.ResizeMode.ResizeToContents)
+        self.header().setStretchLastSection(True)
+    
+    def findParentTask(self, parentUid):
+        """
+        Public method to find a parent task by its ID.
+        
+        @param parentUid uid of the parent task (string)
+        @return reference to the task (Task)
+        """
+        if not parentUid:
+            return None
+        
+        parentTask = None
+        for task in self.tasks:
+            if task.getUuid() == parentUid:
+                parentTask = task
+                break
+        
+        return parentTask
+    
+    def __refreshDisplay(self):
+        """
+        Private method to refresh the display.
+        """
+        for task in self.tasks:
+            task.setHidden(not self.taskFilter.showTask(task))
+        
+        self.__checkTopLevelItems()
+        self.__resort()
+        self.__resizeColumns()
+    
+    def __taskItemActivated(self, itm, col):
+        """
+        Private slot to handle the activation of an item.
+        
+        @param itm reference to the activated item (QTreeWidgetItem)
+        @param col column the item was activated in (integer)
+        """
+        if (
+            not self.__activating and
+            itm is not self.__extractedItem and
+            itm is not self.__manualItem
+        ):
+            self.__activating = True
+            fn = itm.getFilename()
+            if fn:
+                if os.path.exists(fn):
+                    self.displayFile.emit(fn, itm.getLineno())
+                else:
+                    if itm.isProjectTask():
+                        self.__deleteTask(itm)
+            else:
+                self.__editTaskProperties()
+            self.__activating = False
+
+    def __showContextMenu(self, coord):
+        """
+        Private slot to show the context menu of the list.
+        
+        @param coord the position of the mouse pointer (QPoint)
+        """
+        itm = self.itemAt(coord)
+        coord = self.mapToGlobal(coord)
+        if (
+            itm is None or
+            itm is self.__extractedItem or
+            itm is self.__manualItem
+        ):
+            self.backProjectTasksMenuItem.setEnabled(self.projectOpen)
+            if self.copyTask:
+                self.backPasteItem.setEnabled(True)
+                self.backPasteMainItem.setEnabled(True)
+            else:
+                self.backPasteItem.setEnabled(False)
+                self.backPasteMainItem.setEnabled(False)
+            self.backDeleteCompletedItem.setEnabled(
+                bool(self.tasks))
+            self.__backMenu.popup(coord)
+        else:
+            self.projectTasksMenuItem.setEnabled(self.projectOpen)
+            if itm.getFilename():
+                self.gotoItem.setEnabled(True)
+                self.deleteItem.setEnabled(True)
+                self.markCompletedItem.setEnabled(False)
+                self.copyItem.setEnabled(False)
+                self.subtaskItem.setEnabled(False)
+            else:
+                self.gotoItem.setEnabled(False)
+                self.deleteItem.setEnabled(True)
+                self.markCompletedItem.setEnabled(True)
+                self.copyItem.setEnabled(True)
+                self.subtaskItem.setEnabled(True)
+            if self.copyTask:
+                self.pasteItem.setEnabled(True)
+                self.pasteMainItem.setEnabled(True)
+            else:
+                self.pasteItem.setEnabled(False)
+                self.pasteMainItem.setEnabled(False)
+            
+            self.__menu.popup(coord)
+    
+    def setProjectOpen(self, o=False):
+        """
+        Public slot to set the project status.
+        
+        @param o flag indicating the project status
+        """
+        self.projectOpen = o
+    
+    def addTask(self, summary, priority=TaskPriority.NORMAL, filename="",
+                lineno=0, completed=False, _time=0, isProjectTask=False,
+                taskType=TaskType.TODO, description="", uid="",
+                parentTask=None):
+        """
+        Public slot to add a task.
+        
+        @param summary summary text of the task
+        @type str
+        @param priority priority of the task
+        @type TaskPriority
+        @param filename filename containing the task
+        @type str
+        @param lineno line number containing the task
+        @type int
+        @param completed flag indicating completion status
+        @type bool
+        @param _time creation time of the task (if 0 use current time)
+        @type float
+        @param isProjectTask flag indicating a task related to the current
+            project
+        @type bool
+        @param taskType type of the task
+        @type TaskType
+        @param description explanatory text of the task
+        @type str
+        @param uid unique id of the task
+        @type str
+        @param parentTask reference to the parent task item or the UID of the
+            parent task
+        @type Task or str
+        @return reference to the task item
+        @rtype Task
+        """
+        if isinstance(parentTask, str):
+            # UID of parent task
+            if parentTask == "":
+                parentUid = ""
+                parentTask = None
+            else:
+                parentUid = parentTask
+                parentTask = self.findParentTask(parentUid)
+        else:
+            # parent task item
+            if parentTask:
+                parentUid = parentTask.getUuid()
+            else:
+                parentUid = ""
+        task = Task(summary, priority, filename, lineno, completed,
+                    _time, isProjectTask, taskType,
+                    self.project, description, uid, parentUid)
+        self.tasks.append(task)
+        if parentTask:
+            parentTask.addChild(task)
+            parentTask.setExpanded(True)
+        elif filename:
+            self.__extractedItem.addChild(task)
+        else:
+            self.__manualItem.addChild(task)
+        task.setHidden(not self.taskFilter.showTask(task))
+        
+        self.__checkTopLevelItems()
+        self.__resort()
+        self.__resizeColumns()
+        
+        if isProjectTask:
+            self.__projectTasksSaveTimer.changeOccurred()
+        
+        return task
+    
+    def addFileTask(self, summary, filename, lineno, taskType=TaskType.TODO,
+                    description=""):
+        """
+        Public slot to add a file related task.
+        
+        @param summary summary text of the task
+        @type str
+        @param filename filename containing the task
+        @type str
+        @param lineno line number containing the task
+        @type int
+        @param taskType type of the task
+        @type TaskType
+        @param description explanatory text of the task
+        @type str
+        """
+        self.addTask(summary, filename=filename, lineno=lineno,
+                     isProjectTask=(
+                         self.project and
+                         self.project.isProjectSource(filename)),
+                     taskType=TaskType(taskType), description=description)
+    
+    def getProjectTasks(self):
+        """
+        Public method to retrieve all project related tasks.
+        
+        @return copy of tasks (list of Task)
+        """
+        tasks = [task for task in self.tasks if task.isProjectTask()]
+        return tasks[:]
+    
+    def getGlobalTasks(self):
+        """
+        Public method to retrieve all non project related tasks.
+        
+        @return copy of tasks (list of Task)
+        """
+        tasks = [task for task in self.tasks if not task.isProjectTask()]
+        return tasks[:]
+    
+    def clearTasks(self):
+        """
+        Public slot to clear all tasks from display.
+        """
+        self.tasks = []
+        self.clear()
+        self.__generateTopLevelItems()
+    
+    def clearProjectTasks(self, fileOnly=False):
+        """
+        Public slot to clear project related tasks.
+        
+        @param fileOnly flag indicating to clear only file related
+            project tasks (boolean)
+        """
+        for task in reversed(self.tasks[:]):
+            if (
+                (fileOnly and task.isProjectFileTask()) or
+                (not fileOnly and task.isProjectTask())
+            ):
+                if self.copyTask == task:
+                    self.copyTask = None
+                parent = task.parent()
+                parent.removeChild(task)
+                self.tasks.remove(task)
+                del task
+        
+        self.__checkTopLevelItems()
+        self.__resort()
+        self.__resizeColumns()
+    
+    def clearFileTasks(self, filename, conditionally=False):
+        """
+        Public slot to clear all tasks related to a file.
+        
+        @param filename name of the file (string)
+        @param conditionally flag indicating to clear the tasks of the file
+            checking some conditions (boolean)
+        """
+        if conditionally:
+            if self.project and self.project.isProjectSource(filename):
+                # project related tasks will not be cleared
+                return
+            if not Preferences.getTasks("ClearOnFileClose"):
+                return
+        for task in self.tasks[:]:
+            if task.getFilename() == filename:
+                if self.copyTask == task:
+                    self.copyTask = None
+                self.__extractedItem.removeChild(task)
+                self.tasks.remove(task)
+                if task.isProjectTask:
+                    self.__projectTasksSaveTimer.changeOccurred()
+                del task
+        
+        self.__checkTopLevelItems()
+        self.__resort()
+        self.__resizeColumns()
+    
+    def __editTaskProperties(self):
+        """
+        Private slot to handle the "Properties" context menu entry.
+        """
+        from .TaskPropertiesDialog import TaskPropertiesDialog
+        task = self.currentItem()
+        dlg = TaskPropertiesDialog(task, parent=self,
+                                   projectOpen=self.projectOpen)
+        if (
+            dlg.exec() == QDialog.DialogCode.Accepted and
+            dlg.isManualTaskMode()
+        ):
+            (summary, priority, taskType, completed, isProjectTask,
+             description) = dlg.getData()
+            task.setSummary(summary)
+            task.setPriority(priority)
+            task.setTaskType(taskType)
+            task.setCompleted(completed)
+            task.setProjectTask(isProjectTask)
+            task.setDescription(description)
+            self.__projectTasksSaveTimer.changeOccurred()
+    
+    def __newTask(self):
+        """
+        Private slot to handle the "New Task" context menu entry.
+        """
+        from .TaskPropertiesDialog import TaskPropertiesDialog
+        dlg = TaskPropertiesDialog(None, parent=self,
+                                   projectOpen=self.projectOpen)
+        if dlg.exec() == QDialog.DialogCode.Accepted:
+            (summary, priority, taskType, completed, isProjectTask,
+             description) = dlg.getData()
+            self.addTask(summary, priority, completed=completed,
+                         isProjectTask=isProjectTask, taskType=taskType,
+                         description=description)
+    
+    def __newSubTask(self):
+        """
+        Private slot to handle the "New Sub-Task" context menu entry.
+        """
+        parentTask = self.currentItem()
+        projectTask = parentTask.isProjectTask()
+        
+        from .TaskPropertiesDialog import TaskPropertiesDialog
+        dlg = TaskPropertiesDialog(None, parent=self,
+                                   projectOpen=self.projectOpen)
+        dlg.setSubTaskMode(projectTask)
+        if dlg.exec() == QDialog.DialogCode.Accepted:
+            (summary, priority, taskType, completed, isProjectTask,
+             description) = dlg.getData()
+            self.addTask(summary, priority, completed=completed,
+                         isProjectTask=isProjectTask, taskType=taskType,
+                         description=description, parentTask=parentTask)
+    
+    def __markCompleted(self):
+        """
+        Private slot to handle the "Mark Completed" context menu entry.
+        """
+        task = self.currentItem()
+        task.setCompleted(True)
+    
+    def __deleteCompleted(self):
+        """
+        Private slot to handle the "Delete Completed Tasks" context menu entry.
+        """
+        for task in reversed(self.tasks[:]):
+            if task.isCompleted():
+                if self.copyTask == task:
+                    self.copyTask = None
+                parent = task.parent()
+                parent.removeChild(task)
+                self.tasks.remove(task)
+                if task.isProjectTask:
+                    self.__projectTasksSaveTimer.changeOccurred()
+                del task
+        
+        self.__checkTopLevelItems()
+        self.__resort()
+        self.__resizeColumns()
+        
+        ci = self.currentItem()
+        if ci:
+            ind = self.indexFromItem(ci, self.currentColumn())
+            self.scrollTo(ind, QAbstractItemView.ScrollHint.PositionAtCenter)
+    
+    def __copyTask(self):
+        """
+        Private slot to handle the "Copy" context menu entry.
+        """
+        task = self.currentItem()
+        self.copyTask = task
+    
+    def __pasteTask(self):
+        """
+        Private slot to handle the "Paste" context menu entry.
+        """
+        if self.copyTask:
+            parent = self.copyTask.parent()
+            if not isinstance(parent, Task):
+                parent = None
+            
+            self.addTask(self.copyTask.summary,
+                         priority=self.copyTask.priority,
+                         completed=self.copyTask.completed,
+                         description=self.copyTask.description,
+                         isProjectTask=self.copyTask._isProjectTask,
+                         parentTask=parent)
+    
+    def __pasteMainTask(self):
+        """
+        Private slot to handle the "Paste as Main Task" context menu entry.
+        """
+        if self.copyTask:
+            self.addTask(self.copyTask.summary,
+                         priority=self.copyTask.priority,
+                         completed=self.copyTask.completed,
+                         description=self.copyTask.description,
+                         isProjectTask=self.copyTask._isProjectTask)
+    
+    def __deleteSubTasks(self, task):
+        """
+        Private method to delete all sub-tasks.
+        
+        @param task task to delete sub-tasks of (Task)
+        """
+        for subtask in task.takeChildren():
+            if self.copyTask == subtask:
+                self.copyTask = None
+            if subtask.childCount() > 0:
+                self.__deleteSubTasks(subtask)
+            self.tasks.remove(subtask)
+    
+    def __deleteTask(self, task=None):
+        """
+        Private slot to delete a task.
+        
+        @param task task to be deleted
+        @type Task
+        """
+        if task is None:
+            # called via "Delete Task" context menu entry
+            task = self.currentItem()
+        
+        if self.copyTask is task:
+            self.copyTask = None
+        if task.childCount() > 0:
+            self.__deleteSubTasks(task)
+        parent = task.parent()
+        parent.removeChild(task)
+        self.tasks.remove(task)
+        if task.isProjectTask:
+            self.__projectTasksSaveTimer.changeOccurred()
+        del task
+        
+        self.__checkTopLevelItems()
+        self.__resort()
+        self.__resizeColumns()
+        
+        ci = self.currentItem()
+        if ci:
+            ind = self.indexFromItem(ci, self.currentColumn())
+            self.scrollTo(ind, QAbstractItemView.ScrollHint.PositionAtCenter)
+    
+    def __goToTask(self):
+        """
+        Private slot to handle the "Go To" context menu entry.
+        """
+        task = self.currentItem()
+        self.displayFile.emit(task.getFilename(), task.getLineno())
+
+    def handlePreferencesChanged(self):
+        """
+        Public slot to react to changes of the preferences.
+        """
+        for task in self.tasks:
+            task.colorizeTask()
+
+    def __activateFilter(self, on):
+        """
+        Private slot to handle the "Filtered display" context menu entry.
+        
+        @param on flag indicating the filter state (boolean)
+        """
+        if on and not self.taskFilter.hasActiveFilter():
+            res = E5MessageBox.yesNo(
+                self,
+                self.tr("Activate task filter"),
+                self.tr(
+                    """The task filter doesn't have any active filters."""
+                    """ Do you want to configure the filter settings?"""),
+                yesDefault=True)
+            if not res:
+                on = False
+            else:
+                self.__configureFilter()
+                on = self.taskFilter.hasActiveFilter()
+        
+        self.taskFilter.setActive(on)
+        self.__menuFilteredAct.setChecked(on)
+        self.__backMenuFilteredAct.setChecked(on)
+        self.__refreshDisplay()
+    
+    def __configureFilter(self):
+        """
+        Private slot to handle the "Configure filter" context menu entry.
+        """
+        from .TaskFilterConfigDialog import TaskFilterConfigDialog
+        dlg = TaskFilterConfigDialog(self.taskFilter)
+        if dlg.exec() == QDialog.DialogCode.Accepted:
+            dlg.configureTaskFilter(self.taskFilter)
+            self.__refreshDisplay()
+
+    def __configureProjectTasksScanOptions(self):
+        """
+        Private slot to configure scan options for project tasks.
+        """
+        scanFilter, ok = QInputDialog.getText(
+            self,
+            self.tr("Scan Filter Patterns"),
+            self.tr("Enter filename patterns of files"
+                    " to be excluded separated by a comma:"),
+            QLineEdit.EchoMode.Normal,
+            self.__projectTasksScanFilter)
+        if ok:
+            self.__projectTasksScanFilter = scanFilter
+    
+    def regenerateProjectTasks(self, quiet=False):
+        """
+        Public slot to regenerate project related tasks.
+        
+        @param quiet flag indicating quiet operation
+        @type bool
+        """
+        markers = {
+            taskType: Preferences.getTasks(markersName).split()
+            for taskType, markersName in Task.TaskType2MarkersName.items()
+        }
+        files = self.project.pdata["SOURCES"]
+        
+        # apply file filter
+        filterList = [f.strip()
+                      for f in self.__projectTasksScanFilter.split(",")
+                      if f.strip()]
+        if filterList:
+            for scanFilter in filterList:
+                files = [f for f in files
+                         if not fnmatch.fnmatch(f, scanFilter)]
+        
+        # remove all project tasks
+        self.clearProjectTasks(fileOnly=True)
+        
+        # now process them
+        if quiet:
+            ppath = self.project.getProjectPath()
+            self.__projectTaskExtractionThread.scan(
+                markers, [os.path.join(ppath, f) for f in files])
+        else:
+            progress = E5ProgressDialog(
+                self.tr("Extracting project tasks..."),
+                self.tr("Abort"), 0, len(files), self.tr("%v/%m Files"))
+            progress.setMinimumDuration(0)
+            progress.setWindowTitle(self.tr("Tasks"))
+            
+            ppath = self.project.getProjectPath()
+            for count, file in enumerate(files):
+                progress.setLabelText(
+                    self.tr("Extracting project tasks...\n{0}").format(file))
+                progress.setValue(count)
+                QApplication.processEvents()
+                if progress.wasCanceled():
+                    break
+                
+                fn = os.path.join(ppath, file)
+                # read the file and split it into textlines
+                try:
+                    text, encoding = Utilities.readEncodedFile(fn)
+                    lines = text.splitlines()
+                except (UnicodeError, OSError):
+                    count += 1
+                    progress.setValue(count)
+                    continue
+                
+                # now search tasks and record them
+                for lineIndex, line in enumerate(lines, start=1):
+                    shouldBreak = False
+                    
+                    if line.endswith("__NO-TASK__"):
+                        # ignore potential task marker
+                        continue
+                    
+                    for taskType, taskMarkers in markers.items():
+                        for taskMarker in taskMarkers:
+                            index = line.find(taskMarker)
+                            if index > -1:
+                                task = line[index:]
+                                self.addFileTask(task, fn, lineIndex, taskType)
+                                shouldBreak = True
+                                break
+                        if shouldBreak:
+                            break
+            
+            progress.setValue(len(files))
+    
+    def __configure(self):
+        """
+        Private method to open the configuration dialog.
+        """
+        e5App().getObject("UserInterface").showPreferences("tasksPage")
+    
+    def saveProjectTasks(self):
+        """
+        Public method to write the project tasks.
+        """
+        if self.projectOpen and Preferences.getProject("TasksProjectAutoSave"):
+            self.project.writeTasks()
+    
+    def stopProjectTaskExtraction(self):
+        """
+        Public method to stop the project task extraction thread.
+        """
+        self.__projectTaskExtractionThread.requestInterrupt()
+        self.__projectTaskExtractionThread.wait()
+    
+    def getTasksScanFilter(self) -> str:
+        """
+        Public method to get the project scan filter.
+        
+        @return project scan filter
+        @rtype str
+        """
+        return self.__projectTasksScanFilter.strip()
+    
+    def setTasksScanFilter(self, filterStr: str):
+        """
+        Public method to set the project scan filter.
+        
+        @param filterStr project scan filter
+        @type str
+        """
+        self.__projectTasksScanFilter = filterStr
+
+
+class ProjectTaskExtractionThread(QThread):
+    """
+    Class implementing a thread to extract tasks related to a project.
+    
+    @signal taskFound(str, str, int, TaskType) emitted with the task
+        description, the file name, the line number and task type to signal
+        the presence of a task
+    """
+    taskFound = pyqtSignal(str, str, int, TaskType)
+    
+    def __init__(self, parent=None):
+        """
+        Constructor
+        
+        @param parent reference to the parent object (QObject)
+        """
+        super().__init__()
+        
+        self.__lock = threading.Lock()
+        self.__interrupt = False
+    
+    def requestInterrupt(self):
+        """
+        Public method to request iterruption of the thread.
+        """
+        if self.isRunning():
+            self.__interrupt = True
+    
+    def scan(self, markers, files):
+        """
+        Public method to scan the given list of files for tasks.
+        
+        @param markers dictionary of defined task markers
+        @type dict of lists of str
+        @param files list of file names to be scanned
+        @type list of str
+        """
+        with self.__lock:
+            self.__interrupt = False
+            self.__files = files[:]
+            self.__markers = {}
+            for markerType in markers:
+                self.__markers[markerType] = markers[markerType][:]
+            
+            if not self.isRunning():
+                self.start(QThread.Priority.LowPriority)
+    
+    def run(self):
+        """
+        Public thread method to scan the given files.
+        """
+        with self.__lock:
+            files = self.__files[:]
+            markers = {}
+            for markerType in self.__markers:
+                markers[markerType] = self.__markers[markerType][:]
+        
+        for fn in files:
+            if self.__interrupt:
+                break
+            
+            # read the file and split it into textlines
+            try:
+                text, encoding = Utilities.readEncodedFile(fn)
+                lines = text.splitlines()
+            except (UnicodeError, OSError):
+                continue
+            
+            # now search tasks and record them
+            for lineIndex, line in enumerate(lines, start=1):
+                if self.__interrupt:
+                    break
+                
+                found = False
+                
+                if line.endswith("__NO-TASK__"):
+                    # ignore potential task marker
+                    continue
+                
+                for taskType, taskMarkers in markers.items():
+                    for taskMarker in taskMarkers:
+                        index = line.find(taskMarker)
+                        if index > -1:
+                            task = line[index:]
+                            with self.__lock:
+                                self.taskFound.emit(task, fn, lineIndex,
+                                                    taskType)
+                            found = True
+                            break
+                    if found:
+                        break

eric ide

mercurial