src/eric7/Tasks/TaskViewer.py

branch
eric7
changeset 9209
b99e7fd55fd3
parent 9149
1810eaf18d3f
child 9221
bf71ee032bb4
equal deleted inserted replaced
9208:3fc8dfeb6ebe 9209:b99e7fd55fd3
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2005 - 2022 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing a task viewer and associated classes.
8
9 Tasks can be defined manually or automatically. Automatically
10 generated tasks are derived from a comment with a special
11 introductory text. This text is configurable.
12 """
13
14 import os
15 import fnmatch
16 import threading
17 import time
18
19 from PyQt6.QtCore import pyqtSignal, Qt, QThread
20 from PyQt6.QtWidgets import (
21 QHeaderView, QLineEdit, QTreeWidget, QDialog, QInputDialog, QApplication,
22 QMenu, QAbstractItemView, QTreeWidgetItem
23 )
24
25 from EricWidgets.EricApplication import ericApp
26 from EricWidgets import EricMessageBox
27 from EricWidgets.EricProgressDialog import EricProgressDialog
28
29 from .Task import Task, TaskType, TaskPriority
30
31 import UI.PixmapCache
32
33 import Preferences
34 import Utilities
35
36 from Utilities.AutoSaver import AutoSaver
37
38
39 class TaskViewer(QTreeWidget):
40 """
41 Class implementing the task viewer.
42
43 @signal displayFile(str, int) emitted to go to a file task
44 """
45 displayFile = pyqtSignal(str, int)
46
47 def __init__(self, parent, project):
48 """
49 Constructor
50
51 @param parent the parent (QWidget)
52 @param project reference to the project object
53 """
54 super().__init__(parent)
55
56 self.setSortingEnabled(True)
57 self.setExpandsOnDoubleClick(False)
58
59 self.__headerItem = QTreeWidgetItem(
60 ["", "", self.tr("Summary"), self.tr("Filename"),
61 self.tr("Line"), ""])
62 self.__headerItem.setIcon(
63 0, UI.PixmapCache.getIcon("taskCompleted"))
64 self.__headerItem.setIcon(
65 1, UI.PixmapCache.getIcon("taskPriority"))
66 self.setHeaderItem(self.__headerItem)
67
68 self.header().setSortIndicator(2, Qt.SortOrder.AscendingOrder)
69 self.__resizeColumns()
70
71 self.tasks = []
72 self.copyTask = None
73 self.projectOpen = False
74 self.project = project
75 self.__projectTasksScanFilter = ""
76
77 from .TaskFilter import TaskFilter
78 self.taskFilter = TaskFilter()
79 self.taskFilter.setActive(False)
80
81 self.__projectTasksSaveTimer = AutoSaver(self, self.saveProjectTasks)
82 self.__projectTaskExtractionThread = ProjectTaskExtractionThread()
83 self.__projectTaskExtractionThread.taskFound.connect(self.addFileTask)
84
85 self.__projectTasksMenu = QMenu(
86 self.tr("P&roject Tasks"), self)
87 self.__projectTasksMenu.addAction(
88 self.tr("&Regenerate project tasks"),
89 self.regenerateProjectTasks)
90 self.__projectTasksMenu.addSeparator()
91 self.__projectTasksMenu.addAction(
92 self.tr("&Configure scan options"),
93 self.__configureProjectTasksScanOptions)
94
95 self.__menu = QMenu(self)
96 self.__menu.addAction(self.tr("&New Task..."), self.__newTask)
97 self.subtaskItem = self.__menu.addAction(
98 self.tr("New &Sub-Task..."), self.__newSubTask)
99 self.__menu.addSeparator()
100 self.projectTasksMenuItem = self.__menu.addMenu(
101 self.__projectTasksMenu)
102 self.__menu.addSeparator()
103 self.gotoItem = self.__menu.addAction(
104 self.tr("&Go To"), self.__goToTask)
105 self.__menu.addSeparator()
106 self.copyItem = self.__menu.addAction(
107 self.tr("&Copy"), self.__copyTask)
108 self.pasteItem = self.__menu.addAction(
109 self.tr("&Paste"), self.__pasteTask)
110 self.pasteMainItem = self.__menu.addAction(
111 self.tr("Paste as &Main Task"), self.__pasteMainTask)
112 self.deleteItem = self.__menu.addAction(
113 self.tr("&Delete"), self.__deleteTask)
114 self.__menu.addSeparator()
115 self.markCompletedItem = self.__menu.addAction(
116 self.tr("&Mark Completed"), self.__markCompleted)
117 self.__menu.addAction(
118 self.tr("Delete Completed &Tasks"), self.__deleteCompleted)
119 self.__menu.addSeparator()
120 self.__menu.addAction(
121 self.tr("P&roperties..."), self.__editTaskProperties)
122 self.__menu.addSeparator()
123 self.__menuFilteredAct = self.__menu.addAction(
124 self.tr("&Filtered display"))
125 self.__menuFilteredAct.setCheckable(True)
126 self.__menuFilteredAct.setChecked(False)
127 self.__menuFilteredAct.triggered[bool].connect(self.__activateFilter)
128 self.__menu.addAction(
129 self.tr("Filter c&onfiguration..."), self.__configureFilter)
130 self.__menu.addSeparator()
131 self.__menu.addAction(
132 self.tr("Resi&ze columns"), self.__resizeColumns)
133 self.__menu.addSeparator()
134 self.__menu.addAction(self.tr("Configure..."), self.__configure)
135
136 self.__backMenu = QMenu(self)
137 self.__backMenu.addAction(self.tr("&New Task..."), self.__newTask)
138 self.__backMenu.addSeparator()
139 self.backProjectTasksMenuItem = self.__backMenu.addMenu(
140 self.__projectTasksMenu)
141 self.__backMenu.addSeparator()
142 self.backPasteItem = self.__backMenu.addAction(
143 self.tr("&Paste"), self.__pasteTask)
144 self.backPasteMainItem = self.__backMenu.addAction(
145 self.tr("Paste as &Main Task"), self.__pasteMainTask)
146 self.__backMenu.addSeparator()
147 self.backDeleteCompletedItem = self.__backMenu.addAction(
148 self.tr("Delete Completed &Tasks"), self.__deleteCompleted)
149 self.__backMenu.addSeparator()
150 self.__backMenuFilteredAct = self.__backMenu.addAction(
151 self.tr("&Filtered display"))
152 self.__backMenuFilteredAct.setCheckable(True)
153 self.__backMenuFilteredAct.setChecked(False)
154 self.__backMenuFilteredAct.triggered[bool].connect(
155 self.__activateFilter)
156 self.__backMenu.addAction(
157 self.tr("Filter c&onfiguration..."), self.__configureFilter)
158 self.__backMenu.addSeparator()
159 self.__backMenu.addAction(
160 self.tr("Resi&ze columns"), self.__resizeColumns)
161 self.__backMenu.addSeparator()
162 self.__backMenu.addAction(
163 self.tr("Configure..."), self.__configure)
164
165 self.__activating = False
166
167 self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
168 self.customContextMenuRequested.connect(self.__showContextMenu)
169 self.itemActivated.connect(self.__taskItemActivated)
170
171 self.setWindowIcon(UI.PixmapCache.getIcon("eric"))
172
173 self.__generateTopLevelItems()
174
175 def __generateTopLevelItems(self):
176 """
177 Private method to generate the 'Extracted Tasks' item.
178 """
179 self.__extractedItem = QTreeWidgetItem(self,
180 [self.tr("Extracted Tasks")])
181 self.__manualItem = QTreeWidgetItem(self,
182 [self.tr("Manual Tasks")])
183 for itm in [self.__extractedItem, self.__manualItem]:
184 itm.setFirstColumnSpanned(True)
185 itm.setExpanded(True)
186 itm.setHidden(True)
187 font = itm.font(0)
188 font.setUnderline(True)
189 itm.setFont(0, font)
190
191 def __checkTopLevelItems(self):
192 """
193 Private slot to check the 'Extracted Tasks' item for children.
194 """
195 for itm in [self.__extractedItem, self.__manualItem]:
196 visibleCount = itm.childCount()
197 for index in range(itm.childCount()):
198 if itm.child(index).isHidden():
199 visibleCount -= 1
200 itm.setHidden(visibleCount == 0)
201
202 def __resort(self):
203 """
204 Private method to resort the tree.
205 """
206 self.sortItems(self.sortColumn(), self.header().sortIndicatorOrder())
207
208 def __resizeColumns(self):
209 """
210 Private method to resize the list columns.
211 """
212 self.header().resizeSections(QHeaderView.ResizeMode.ResizeToContents)
213 self.header().setStretchLastSection(True)
214
215 def findParentTask(self, parentUid):
216 """
217 Public method to find a parent task by its ID.
218
219 @param parentUid uid of the parent task (string)
220 @return reference to the task (Task)
221 """
222 if not parentUid:
223 return None
224
225 parentTask = None
226 for task in self.tasks:
227 if task.getUuid() == parentUid:
228 parentTask = task
229 break
230
231 return parentTask
232
233 def containsTask(self, taskToTest):
234 """
235 Public method to test, if a task is already in the tasks list.
236
237 @param taskToTest task to look for
238 @type Task
239 @return flag indicating the existence of the task
240 @rtype bool
241 """
242 if taskToTest is None:
243 # play it safe
244 return False
245
246 return any(
247 (task.summary == taskToTest.summary) and
248 (task.filename == taskToTest.filename) and
249 (task.lineno == taskToTest.lineno)
250 for task in self.tasks
251 )
252
253 def __refreshDisplay(self):
254 """
255 Private method to refresh the display.
256 """
257 for task in self.tasks:
258 task.setHidden(not self.taskFilter.showTask(task))
259
260 self.__checkTopLevelItems()
261 self.__resort()
262 self.__resizeColumns()
263
264 def __taskItemActivated(self, itm, col):
265 """
266 Private slot to handle the activation of an item.
267
268 @param itm reference to the activated item (QTreeWidgetItem)
269 @param col column the item was activated in (integer)
270 """
271 if (
272 not self.__activating and
273 itm is not self.__extractedItem and
274 itm is not self.__manualItem
275 ):
276 self.__activating = True
277 fn = itm.getFilename()
278 if fn:
279 if os.path.exists(fn):
280 self.displayFile.emit(fn, itm.getLineno())
281 else:
282 if itm.isProjectTask():
283 self.__deleteTask(itm)
284 else:
285 self.__editTaskProperties()
286 self.__activating = False
287
288 def __showContextMenu(self, coord):
289 """
290 Private slot to show the context menu of the list.
291
292 @param coord the position of the mouse pointer (QPoint)
293 """
294 itm = self.itemAt(coord)
295 coord = self.mapToGlobal(coord)
296 if (
297 itm is None or
298 itm is self.__extractedItem or
299 itm is self.__manualItem
300 ):
301 self.backProjectTasksMenuItem.setEnabled(self.projectOpen)
302 if self.copyTask:
303 self.backPasteItem.setEnabled(True)
304 self.backPasteMainItem.setEnabled(True)
305 else:
306 self.backPasteItem.setEnabled(False)
307 self.backPasteMainItem.setEnabled(False)
308 self.backDeleteCompletedItem.setEnabled(
309 bool(self.tasks))
310 self.__backMenu.popup(coord)
311 else:
312 self.projectTasksMenuItem.setEnabled(self.projectOpen)
313 if itm.getFilename():
314 self.gotoItem.setEnabled(True)
315 self.deleteItem.setEnabled(True)
316 self.markCompletedItem.setEnabled(False)
317 self.copyItem.setEnabled(False)
318 self.subtaskItem.setEnabled(False)
319 else:
320 self.gotoItem.setEnabled(False)
321 self.deleteItem.setEnabled(True)
322 self.markCompletedItem.setEnabled(True)
323 self.copyItem.setEnabled(True)
324 self.subtaskItem.setEnabled(True)
325 if self.copyTask:
326 self.pasteItem.setEnabled(True)
327 self.pasteMainItem.setEnabled(True)
328 else:
329 self.pasteItem.setEnabled(False)
330 self.pasteMainItem.setEnabled(False)
331
332 self.__menu.popup(coord)
333
334 def setProjectOpen(self, o=False):
335 """
336 Public slot to set the project status.
337
338 @param o flag indicating the project status
339 """
340 self.projectOpen = o
341
342 def addTask(self, summary, priority=TaskPriority.NORMAL, filename="",
343 lineno=0, completed=False, _time=0, isProjectTask=False,
344 taskType=TaskType.TODO, description="", uid="",
345 parentTask=None):
346 """
347 Public slot to add a task.
348
349 @param summary summary text of the task
350 @type str
351 @param priority priority of the task
352 @type TaskPriority
353 @param filename filename containing the task
354 @type str
355 @param lineno line number containing the task
356 @type int
357 @param completed flag indicating completion status
358 @type bool
359 @param _time creation time of the task (if 0 use current time)
360 @type float
361 @param isProjectTask flag indicating a task related to the current
362 project
363 @type bool
364 @param taskType type of the task
365 @type TaskType
366 @param description explanatory text of the task
367 @type str
368 @param uid unique id of the task
369 @type str
370 @param parentTask reference to the parent task item or the UID of the
371 parent task
372 @type Task or str
373 @return reference to the task item
374 @rtype Task
375 """
376 if isinstance(parentTask, str):
377 # UID of parent task
378 if parentTask == "":
379 parentUid = ""
380 parentTask = None
381 else:
382 parentUid = parentTask
383 parentTask = self.findParentTask(parentUid)
384 else:
385 # parent task item
386 if parentTask:
387 parentUid = parentTask.getUuid()
388 else:
389 parentUid = ""
390 task = Task(summary, priority, filename, lineno, completed,
391 _time, isProjectTask, taskType,
392 self.project, description, uid, parentUid)
393 if not self.containsTask(task):
394 self.tasks.append(task)
395 if parentTask:
396 parentTask.addChild(task)
397 parentTask.setExpanded(True)
398 elif filename:
399 self.__extractedItem.addChild(task)
400 else:
401 self.__manualItem.addChild(task)
402 task.setHidden(not self.taskFilter.showTask(task))
403
404 self.__checkTopLevelItems()
405 self.__resort()
406 self.__resizeColumns()
407
408 if isProjectTask:
409 self.__projectTasksSaveTimer.changeOccurred()
410
411 return task
412 else:
413 return None
414
415 def addFileTask(self, summary, filename, lineno, taskType=TaskType.TODO,
416 description=""):
417 """
418 Public slot to add a file related task.
419
420 @param summary summary text of the task
421 @type str
422 @param filename filename containing the task
423 @type str
424 @param lineno line number containing the task
425 @type int
426 @param taskType type of the task
427 @type TaskType
428 @param description explanatory text of the task
429 @type str
430 """
431 self.addTask(summary, filename=filename, lineno=lineno,
432 isProjectTask=(
433 self.project and
434 self.project.isProjectSource(filename)),
435 taskType=TaskType(taskType), description=description)
436
437 def getProjectTasks(self):
438 """
439 Public method to retrieve all project related tasks.
440
441 @return copy of tasks (list of Task)
442 """
443 tasks = [task for task in self.tasks if task.isProjectTask()]
444 return tasks[:]
445
446 def getGlobalTasks(self):
447 """
448 Public method to retrieve all non project related tasks.
449
450 @return copy of tasks (list of Task)
451 """
452 tasks = [task for task in self.tasks if not task.isProjectTask()]
453 return tasks[:]
454
455 def clearTasks(self):
456 """
457 Public slot to clear all tasks from display.
458 """
459 self.tasks = []
460 self.clear()
461 self.__generateTopLevelItems()
462
463 def clearProjectTasks(self, fileOnly=False):
464 """
465 Public slot to clear project related tasks.
466
467 @param fileOnly flag indicating to clear only file related
468 project tasks (boolean)
469 """
470 for task in reversed(self.tasks[:]):
471 if (
472 (fileOnly and task.isProjectFileTask()) or
473 (not fileOnly and task.isProjectTask())
474 ):
475 if self.copyTask == task:
476 self.copyTask = None
477 parent = task.parent()
478 parent.removeChild(task)
479 self.tasks.remove(task)
480 del task
481
482 self.__checkTopLevelItems()
483 self.__resort()
484 self.__resizeColumns()
485
486 def clearFileTasks(self, filename, conditionally=False):
487 """
488 Public slot to clear all tasks related to a file.
489
490 @param filename name of the file (string)
491 @param conditionally flag indicating to clear the tasks of the file
492 checking some conditions (boolean)
493 """
494 if conditionally:
495 if self.project and self.project.isProjectSource(filename):
496 # project related tasks will not be cleared
497 return
498 if not Preferences.getTasks("ClearOnFileClose"):
499 return
500 for task in self.tasks[:]:
501 if task.getFilename() == filename:
502 if self.copyTask == task:
503 self.copyTask = None
504 self.__extractedItem.removeChild(task)
505 self.tasks.remove(task)
506 if task.isProjectTask:
507 self.__projectTasksSaveTimer.changeOccurred()
508 del task
509
510 self.__checkTopLevelItems()
511 self.__resort()
512 self.__resizeColumns()
513
514 def __editTaskProperties(self):
515 """
516 Private slot to handle the "Properties" context menu entry.
517 """
518 from .TaskPropertiesDialog import TaskPropertiesDialog
519 task = self.currentItem()
520 dlg = TaskPropertiesDialog(task, parent=self,
521 projectOpen=self.projectOpen)
522 if (
523 dlg.exec() == QDialog.DialogCode.Accepted and
524 dlg.isManualTaskMode()
525 ):
526 (summary, priority, taskType, completed, isProjectTask,
527 description) = dlg.getData()
528 task.setSummary(summary)
529 task.setPriority(priority)
530 task.setTaskType(taskType)
531 task.setCompleted(completed)
532 task.setProjectTask(isProjectTask)
533 task.setDescription(description)
534 self.__projectTasksSaveTimer.changeOccurred()
535
536 def __newTask(self):
537 """
538 Private slot to handle the "New Task" context menu entry.
539 """
540 from .TaskPropertiesDialog import TaskPropertiesDialog
541 dlg = TaskPropertiesDialog(None, parent=self,
542 projectOpen=self.projectOpen)
543 if dlg.exec() == QDialog.DialogCode.Accepted:
544 (summary, priority, taskType, completed, isProjectTask,
545 description) = dlg.getData()
546 self.addTask(summary, priority, completed=completed,
547 isProjectTask=isProjectTask, taskType=taskType,
548 description=description)
549
550 def __newSubTask(self):
551 """
552 Private slot to handle the "New Sub-Task" context menu entry.
553 """
554 parentTask = self.currentItem()
555 projectTask = parentTask.isProjectTask()
556
557 from .TaskPropertiesDialog import TaskPropertiesDialog
558 dlg = TaskPropertiesDialog(None, parent=self,
559 projectOpen=self.projectOpen)
560 dlg.setSubTaskMode(projectTask)
561 if dlg.exec() == QDialog.DialogCode.Accepted:
562 (summary, priority, taskType, completed, isProjectTask,
563 description) = dlg.getData()
564 self.addTask(summary, priority, completed=completed,
565 isProjectTask=isProjectTask, taskType=taskType,
566 description=description, parentTask=parentTask)
567
568 def __markCompleted(self):
569 """
570 Private slot to handle the "Mark Completed" context menu entry.
571 """
572 task = self.currentItem()
573 task.setCompleted(True)
574
575 def __deleteCompleted(self):
576 """
577 Private slot to handle the "Delete Completed Tasks" context menu entry.
578 """
579 for task in reversed(self.tasks[:]):
580 if task.isCompleted():
581 if self.copyTask == task:
582 self.copyTask = None
583 parent = task.parent()
584 parent.removeChild(task)
585 self.tasks.remove(task)
586 if task.isProjectTask:
587 self.__projectTasksSaveTimer.changeOccurred()
588 del task
589
590 self.__checkTopLevelItems()
591 self.__resort()
592 self.__resizeColumns()
593
594 ci = self.currentItem()
595 if ci:
596 ind = self.indexFromItem(ci, self.currentColumn())
597 self.scrollTo(ind, QAbstractItemView.ScrollHint.PositionAtCenter)
598
599 def __copyTask(self):
600 """
601 Private slot to handle the "Copy" context menu entry.
602 """
603 task = self.currentItem()
604 self.copyTask = task
605
606 def __pasteTask(self):
607 """
608 Private slot to handle the "Paste" context menu entry.
609 """
610 if self.copyTask:
611 parent = self.copyTask.parent()
612 if not isinstance(parent, Task):
613 parent = None
614
615 self.addTask(self.copyTask.summary,
616 priority=self.copyTask.priority,
617 completed=self.copyTask.completed,
618 description=self.copyTask.description,
619 isProjectTask=self.copyTask._isProjectTask,
620 parentTask=parent)
621
622 def __pasteMainTask(self):
623 """
624 Private slot to handle the "Paste as Main Task" context menu entry.
625 """
626 if self.copyTask:
627 self.addTask(self.copyTask.summary,
628 priority=self.copyTask.priority,
629 completed=self.copyTask.completed,
630 description=self.copyTask.description,
631 isProjectTask=self.copyTask._isProjectTask)
632
633 def __deleteSubTasks(self, task):
634 """
635 Private method to delete all sub-tasks.
636
637 @param task task to delete sub-tasks of (Task)
638 """
639 for subtask in task.takeChildren():
640 if self.copyTask == subtask:
641 self.copyTask = None
642 if subtask.childCount() > 0:
643 self.__deleteSubTasks(subtask)
644 self.tasks.remove(subtask)
645
646 def __deleteTask(self, task=None):
647 """
648 Private slot to delete a task.
649
650 @param task task to be deleted
651 @type Task
652 """
653 if task is None:
654 # called via "Delete Task" context menu entry
655 task = self.currentItem()
656
657 if self.copyTask is task:
658 self.copyTask = None
659 if task.childCount() > 0:
660 self.__deleteSubTasks(task)
661 parent = task.parent()
662 parent.removeChild(task)
663 self.tasks.remove(task)
664 if task.isProjectTask:
665 self.__projectTasksSaveTimer.changeOccurred()
666 del task
667
668 self.__checkTopLevelItems()
669 self.__resort()
670 self.__resizeColumns()
671
672 ci = self.currentItem()
673 if ci:
674 ind = self.indexFromItem(ci, self.currentColumn())
675 self.scrollTo(ind, QAbstractItemView.ScrollHint.PositionAtCenter)
676
677 def __goToTask(self):
678 """
679 Private slot to handle the "Go To" context menu entry.
680 """
681 task = self.currentItem()
682 self.displayFile.emit(task.getFilename(), task.getLineno())
683
684 def handlePreferencesChanged(self):
685 """
686 Public slot to react to changes of the preferences.
687 """
688 for task in self.tasks:
689 task.colorizeTask()
690
691 def __activateFilter(self, on):
692 """
693 Private slot to handle the "Filtered display" context menu entry.
694
695 @param on flag indicating the filter state (boolean)
696 """
697 if on and not self.taskFilter.hasActiveFilter():
698 res = EricMessageBox.yesNo(
699 self,
700 self.tr("Activate task filter"),
701 self.tr(
702 """The task filter doesn't have any active filters."""
703 """ Do you want to configure the filter settings?"""),
704 yesDefault=True)
705 if not res:
706 on = False
707 else:
708 self.__configureFilter()
709 on = self.taskFilter.hasActiveFilter()
710
711 self.taskFilter.setActive(on)
712 self.__menuFilteredAct.setChecked(on)
713 self.__backMenuFilteredAct.setChecked(on)
714 self.__refreshDisplay()
715
716 def __configureFilter(self):
717 """
718 Private slot to handle the "Configure filter" context menu entry.
719 """
720 from .TaskFilterConfigDialog import TaskFilterConfigDialog
721 dlg = TaskFilterConfigDialog(self.taskFilter)
722 if dlg.exec() == QDialog.DialogCode.Accepted:
723 dlg.configureTaskFilter(self.taskFilter)
724 self.__refreshDisplay()
725
726 def __configureProjectTasksScanOptions(self):
727 """
728 Private slot to configure scan options for project tasks.
729 """
730 scanFilter, ok = QInputDialog.getText(
731 self,
732 self.tr("Scan Filter Patterns"),
733 self.tr("Enter filename patterns of files"
734 " to be excluded separated by a comma:"),
735 QLineEdit.EchoMode.Normal,
736 self.__projectTasksScanFilter)
737 if ok:
738 self.__projectTasksScanFilter = scanFilter
739
740 def regenerateProjectTasks(self, quiet=False):
741 """
742 Public slot to regenerate project related tasks.
743
744 @param quiet flag indicating quiet operation
745 @type bool
746 """
747 markers = {
748 taskType: Preferences.getTasks(markersName).split()
749 for taskType, markersName in Task.TaskType2MarkersName.items()
750 }
751 files = self.project.pdata["SOURCES"]
752
753 # apply file filter
754 filterList = [f.strip()
755 for f in self.__projectTasksScanFilter.split(",")
756 if f.strip()]
757 if filterList:
758 for scanFilter in filterList:
759 files = [f for f in files
760 if not fnmatch.fnmatch(f, scanFilter)]
761
762 # remove all project tasks
763 self.clearProjectTasks(fileOnly=True)
764
765 # now process them
766 if quiet:
767 ppath = self.project.getProjectPath()
768 self.__projectTaskExtractionThread.scan(
769 markers, [os.path.join(ppath, f) for f in files])
770 else:
771 progress = EricProgressDialog(
772 self.tr("Extracting project tasks..."),
773 self.tr("Abort"), 0, len(files), self.tr("%v/%m Files"), self)
774 progress.setMinimumDuration(0)
775 progress.setWindowTitle(self.tr("Tasks"))
776
777 ppath = self.project.getProjectPath()
778
779 now = time.monotonic()
780 for count, file in enumerate(files):
781 progress.setLabelText(
782 self.tr("Extracting project tasks...\n{0}").format(file))
783 progress.setValue(count)
784 if time.monotonic() - now > 0.01:
785 QApplication.processEvents()
786 now = time.monotonic()
787 if progress.wasCanceled():
788 break
789
790 fn = os.path.join(ppath, file)
791 # read the file and split it into textlines
792 try:
793 text, encoding = Utilities.readEncodedFile(fn)
794 lines = text.splitlines()
795 except (UnicodeError, OSError):
796 count += 1
797 progress.setValue(count)
798 continue
799
800 # now search tasks and record them
801 for lineIndex, line in enumerate(lines, start=1):
802 shouldBreak = False
803
804 if line.endswith("__NO-TASK__"):
805 # ignore potential task marker
806 continue
807
808 for taskType, taskMarkers in markers.items():
809 for taskMarker in taskMarkers:
810 index = line.find(taskMarker)
811 if index > -1:
812 task = line[index:]
813 self.addFileTask(task, fn, lineIndex, taskType)
814 shouldBreak = True
815 break
816 if shouldBreak:
817 break
818
819 progress.setValue(len(files))
820
821 def __configure(self):
822 """
823 Private method to open the configuration dialog.
824 """
825 ericApp().getObject("UserInterface").showPreferences("tasksPage")
826
827 def saveProjectTasks(self):
828 """
829 Public method to write the project tasks.
830 """
831 if self.projectOpen and Preferences.getProject("TasksProjectAutoSave"):
832 self.project.writeTasks()
833
834 def stopProjectTaskExtraction(self):
835 """
836 Public method to stop the project task extraction thread.
837 """
838 self.__projectTaskExtractionThread.requestInterrupt()
839 self.__projectTaskExtractionThread.wait()
840
841 def getTasksScanFilter(self) -> str:
842 """
843 Public method to get the project scan filter.
844
845 @return project scan filter
846 @rtype str
847 """
848 return self.__projectTasksScanFilter.strip()
849
850 def setTasksScanFilter(self, filterStr: str):
851 """
852 Public method to set the project scan filter.
853
854 @param filterStr project scan filter
855 @type str
856 """
857 self.__projectTasksScanFilter = filterStr
858
859
860 class ProjectTaskExtractionThread(QThread):
861 """
862 Class implementing a thread to extract tasks related to a project.
863
864 @signal taskFound(str, str, int, TaskType) emitted with the task
865 description, the file name, the line number and task type to signal
866 the presence of a task
867 """
868 taskFound = pyqtSignal(str, str, int, TaskType)
869
870 def __init__(self, parent=None):
871 """
872 Constructor
873
874 @param parent reference to the parent object (QObject)
875 """
876 super().__init__()
877
878 self.__lock = threading.Lock()
879 self.__interrupt = False
880
881 def requestInterrupt(self):
882 """
883 Public method to request iterruption of the thread.
884 """
885 if self.isRunning():
886 self.__interrupt = True
887
888 def scan(self, markers, files):
889 """
890 Public method to scan the given list of files for tasks.
891
892 @param markers dictionary of defined task markers
893 @type dict of lists of str
894 @param files list of file names to be scanned
895 @type list of str
896 """
897 with self.__lock:
898 self.__interrupt = False
899 self.__files = files[:]
900 self.__markers = {}
901 for markerType in markers:
902 self.__markers[markerType] = markers[markerType][:]
903
904 if not self.isRunning():
905 self.start(QThread.Priority.LowPriority)
906
907 def run(self):
908 """
909 Public thread method to scan the given files.
910 """
911 with self.__lock:
912 files = self.__files[:]
913 markers = {}
914 for markerType in self.__markers:
915 markers[markerType] = self.__markers[markerType][:]
916
917 for fn in files:
918 if self.__interrupt:
919 break
920
921 # read the file and split it into textlines
922 try:
923 text, encoding = Utilities.readEncodedFile(fn)
924 lines = text.splitlines()
925 except (UnicodeError, OSError):
926 continue
927
928 # now search tasks and record them
929 for lineIndex, line in enumerate(lines, start=1):
930 if self.__interrupt:
931 break
932
933 found = False
934
935 if line.endswith("__NO-TASK__"):
936 # ignore potential task marker
937 continue
938
939 for taskType, taskMarkers in markers.items():
940 for taskMarker in taskMarkers:
941 index = line.find(taskMarker)
942 if index > -1:
943 task = line[index:]
944 with self.__lock:
945 self.taskFound.emit(task, fn, lineIndex,
946 taskType)
947 found = True
948 break
949 if found:
950 break

eric ide

mercurial