eric7/Tasks/TaskViewer.py

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

eric ide

mercurial