From 502f787d36859c91322c2b25c39758ff0d8a5cc4 Mon Sep 17 00:00:00 2001 From: Bishwa Saha <77796630+kun-codes@users.noreply.github.com> Date: Fri, 5 Jun 2026 14:23:13 +0530 Subject: [PATCH 1/5] feat(taskList.py): add context menu for subtasks with new `SubTaskMenu` component --- src/prefabs/subTaskMenu.py | 19 +++++++++++++++++ src/prefabs/taskList.py | 42 +++++++++++++++++++++++++++++++++++--- 2 files changed, 58 insertions(+), 3 deletions(-) create mode 100644 src/prefabs/subTaskMenu.py diff --git a/src/prefabs/subTaskMenu.py b/src/prefabs/subTaskMenu.py new file mode 100644 index 0000000..b073604 --- /dev/null +++ b/src/prefabs/subTaskMenu.py @@ -0,0 +1,19 @@ +from typing import Optional + +from PySide6.QtWidgets import QWidget +from qfluentwidgets import Action, FluentIcon, RoundMenu + +from prefabs.customFluentIcon import CustomFluentIcon + + +class SubTaskMenu(RoundMenu): + def __init__(self, title: str = "", parent: Optional[QWidget] = None) -> None: + super().__init__(title, parent) + + self.addSubTaskAction = Action(CustomFluentIcon.ADD_SUBTASK, "Add Subtask") + self.editTimeAction = Action(FluentIcon.EDIT, "Edit Time") + self.deleteSubTaskAction = Action(FluentIcon.DELETE, "Delete Subtask") + + self.addAction(self.addSubTaskAction) + self.addAction(self.editTimeAction) + self.addAction(self.deleteSubTaskAction) diff --git a/src/prefabs/taskList.py b/src/prefabs/taskList.py index 7d836e2..83d0600 100644 --- a/src/prefabs/taskList.py +++ b/src/prefabs/taskList.py @@ -1,14 +1,27 @@ -from typing import Optional +from typing import TYPE_CHECKING, Optional, cast from PySide6.QtCore import QModelIndex, Qt -from PySide6.QtGui import QColor, QDragEnterEvent, QDropEvent, QPainter, QPaintEvent, QPen, QResizeEvent +from PySide6.QtGui import ( + QColor, + QContextMenuEvent, + QDragEnterEvent, + QDropEvent, + QPainter, + QPaintEvent, + QPen, + QResizeEvent, +) from PySide6.QtWidgets import QAbstractItemView, QProxyStyle, QStyle, QStyleOption, QWidget from qfluentwidgets import TreeView, isDarkTheme -from models.taskListModel import TaskListModel +from models.taskListModel import TaskListModel, TaskNode +from prefabs.subTaskMenu import SubTaskMenu from prefabs.taskListItemDelegate import TaskListItemDelegate from ui_py.ui_tasks_list_view import Ui_TaskView +if TYPE_CHECKING: + from views.subinterfaces.tasksView import TaskListView + # from: https://www.qtcentre.org/threads/35443-Customize-drop-indicator-in-QTreeView?p=167572#post167572 class TaskListStyle(QProxyStyle): @@ -193,3 +206,26 @@ def _onItemCollapsed(self, index: QModelIndex) -> None: model: TaskListModel = self.model() if model: model.setData(index, False, model.IsExpandedRole) + + def contextMenuEvent(self, e: QContextMenuEvent) -> None: + index: QModelIndex = self.indexAt(e.pos()) + task_id: int = self.model().data(index, TaskListModel.IDRole) + task_node: TaskNode = self.model().getTaskNodeById(task_id) + is_root_task: bool = task_node.is_root() + + parent_view = self.parentWidget() + while parent_view is not None: + if parent_view.objectName() == "task_interface": + break + parent_view = parent_view.parentWidget() + + assert parent_view is not None + parent_view = cast("TaskListView", parent_view) + + if not is_root_task: + menu = SubTaskMenu() + + menu.addSubTaskAction.triggered.connect(parent_view.addSubTaskAction.trigger) + menu.editTimeAction.triggered.connect(parent_view.editTaskTimeButton.click) + menu.deleteSubTaskAction.triggered.connect(parent_view.deleteTaskButton.click) + menu.exec(e.globalPos()) From f9d88add97f309d35563fd5d00c5d1ebcc9863cd Mon Sep 17 00:00:00 2001 From: Bishwa Saha <77796630+kun-codes@users.noreply.github.com> Date: Fri, 5 Jun 2026 18:44:34 +0530 Subject: [PATCH 2/5] style(subTaskMenu.py): add separator to context menu --- src/prefabs/subTaskMenu.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/prefabs/subTaskMenu.py b/src/prefabs/subTaskMenu.py index b073604..4a6c6e0 100644 --- a/src/prefabs/subTaskMenu.py +++ b/src/prefabs/subTaskMenu.py @@ -16,4 +16,5 @@ def __init__(self, title: str = "", parent: Optional[QWidget] = None) -> None: self.addAction(self.addSubTaskAction) self.addAction(self.editTimeAction) + self.addSeparator() self.addAction(self.deleteSubTaskAction) From 91854cb05f74ae72ebc5d3cd1253530e29dd5744 Mon Sep 17 00:00:00 2001 From: Bishwa Saha <77796630+kun-codes@users.noreply.github.com> Date: Fri, 5 Jun 2026 20:07:34 +0530 Subject: [PATCH 3/5] feat(taskList.py, taskMenu.py): add `TaskMenu` component for context menu of parent tasks --- src/prefabs/taskList.py | 15 ++++++++++++++ src/prefabs/taskMenu.py | 24 ++++++++++++++++++++++ src/views/subinterfaces/tasksView.py | 30 ++++++++++++++++++++++++++++ 3 files changed, 69 insertions(+) create mode 100644 src/prefabs/taskMenu.py diff --git a/src/prefabs/taskList.py b/src/prefabs/taskList.py index 83d0600..c017b57 100644 --- a/src/prefabs/taskList.py +++ b/src/prefabs/taskList.py @@ -17,6 +17,7 @@ from models.taskListModel import TaskListModel, TaskNode from prefabs.subTaskMenu import SubTaskMenu from prefabs.taskListItemDelegate import TaskListItemDelegate +from prefabs.taskMenu import TaskMenu from ui_py.ui_tasks_list_view import Ui_TaskView if TYPE_CHECKING: @@ -229,3 +230,17 @@ def contextMenuEvent(self, e: QContextMenuEvent) -> None: menu.editTimeAction.triggered.connect(parent_view.editTaskTimeButton.click) menu.deleteSubTaskAction.triggered.connect(parent_view.deleteTaskButton.click) menu.exec(e.globalPos()) + else: + menu = TaskMenu() + + menu.addTaskAction.triggered.connect(parent_view.addTaskAction.trigger) + menu.addSubTaskAction.triggered.connect(parent_view.addSubTaskAction.trigger) + menu.editTimeAction.triggered.connect(parent_view.editTaskTimeButton.click) + menu.markTaskAsCompletedAction.triggered.connect(parent_view.markTaskAsCompleted) + + if self.objectName() == "completedTasksList": + menu.markTaskAsCompletedAction.setDisabled(True) + + menu.deleteTaskAction.triggered.connect(parent_view.deleteTaskButton.click) + + menu.exec(e.globalPos()) diff --git a/src/prefabs/taskMenu.py b/src/prefabs/taskMenu.py new file mode 100644 index 0000000..1c4ca6b --- /dev/null +++ b/src/prefabs/taskMenu.py @@ -0,0 +1,24 @@ +from typing import Optional + +from PySide6.QtWidgets import QWidget +from qfluentwidgets import Action, FluentIcon, RoundMenu + +from prefabs.customFluentIcon import CustomFluentIcon + + +class TaskMenu(RoundMenu): + def __init__(self, title: str = "", parent: Optional[QWidget] = None) -> None: + super().__init__(title, parent) + + self.addTaskAction = Action(FluentIcon.ADD, "Add Task") + self.addSubTaskAction = Action(CustomFluentIcon.ADD_SUBTASK, "Add Subtask") + self.editTimeAction = Action(FluentIcon.EDIT, "Edit Time") + self.markTaskAsCompletedAction = Action(FluentIcon.VPN, "Mark as Completed") + self.deleteTaskAction = Action(FluentIcon.DELETE, "Delete Subtask") + + self.addAction(self.addTaskAction) + self.addAction(self.addSubTaskAction) + self.addAction(self.editTimeAction) + self.addAction(self.markTaskAsCompletedAction) + self.addSeparator() + self.addAction(self.deleteTaskAction) diff --git a/src/views/subinterfaces/tasksView.py b/src/views/subinterfaces/tasksView.py index 31cf0f1..983407a 100644 --- a/src/views/subinterfaces/tasksView.py +++ b/src/views/subinterfaces/tasksView.py @@ -331,6 +331,36 @@ def deleteTask(self) -> None: model.deleteTask(selectedIndex.row(), parent_index) + def markTaskAsCompleted(self) -> None: + """ + marks the task in todo task list as completed by simulating a drag and drop programmatically + moves it to the last of the completed task list + """ + if not self.todoTasksList.selectionModel().hasSelection(): + return + + index = self.todoTasksList.selectionModel().currentIndex() + task_id = index.data(TaskListModel.IDRole) + task_node = self.todoTasksList.model().getTaskNodeById(task_id) + + if task_node is None or not task_node.is_root(): + return + + source_model: TaskListModel = self.todoTasksList.model() + target_model: TaskListModel = self.completedTasksList.model() + + try: + mime_data = source_model.mimeData([index]) + ok = target_model.dropMimeData(mime_data, Qt.DropAction.MoveAction, -1, 0, QModelIndex()) + if not ok: + return + + source_model.finishDrag() + source_model.removeRows(index.row(), 1, QModelIndex()) + finally: + source_model.finishDrag() + target_model.finishDrag() + @restoreFocus def editTaskTime(self) -> None: row = None From ae3a14e6ed34d6fcd4bd7b7c442f96db86ba6145 Mon Sep 17 00:00:00 2001 From: Bishwa Saha <77796630+kun-codes@users.noreply.github.com> Date: Fri, 5 Jun 2026 20:26:27 +0530 Subject: [PATCH 4/5] feat(taskList.py, tasksView.py): add toggle functionality for task completion and update context menu actions --- src/prefabs/taskList.py | 10 +++++++--- src/prefabs/taskMenu.py | 2 ++ src/views/subinterfaces/tasksView.py | 22 +++++++++++++--------- 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/src/prefabs/taskList.py b/src/prefabs/taskList.py index c017b57..c9e386a 100644 --- a/src/prefabs/taskList.py +++ b/src/prefabs/taskList.py @@ -236,10 +236,14 @@ def contextMenuEvent(self, e: QContextMenuEvent) -> None: menu.addTaskAction.triggered.connect(parent_view.addTaskAction.trigger) menu.addSubTaskAction.triggered.connect(parent_view.addSubTaskAction.trigger) menu.editTimeAction.triggered.connect(parent_view.editTaskTimeButton.click) - menu.markTaskAsCompletedAction.triggered.connect(parent_view.markTaskAsCompleted) - if self.objectName() == "completedTasksList": - menu.markTaskAsCompletedAction.setDisabled(True) + menu.markTaskAsIncompleteAction.triggered.connect(parent_view.toggleTaskCompletion) + menu.markTaskAsCompletedAction.triggered.connect(parent_view.toggleTaskCompletion) + + if self.objectName() == "todoTasksList": + menu.removeAction(menu.markTaskAsIncompleteAction) + elif self.objectName() == "completedTasksList": + menu.removeAction(menu.markTaskAsCompletedAction) menu.deleteTaskAction.triggered.connect(parent_view.deleteTaskButton.click) diff --git a/src/prefabs/taskMenu.py b/src/prefabs/taskMenu.py index 1c4ca6b..72ef0c5 100644 --- a/src/prefabs/taskMenu.py +++ b/src/prefabs/taskMenu.py @@ -13,12 +13,14 @@ def __init__(self, title: str = "", parent: Optional[QWidget] = None) -> None: self.addTaskAction = Action(FluentIcon.ADD, "Add Task") self.addSubTaskAction = Action(CustomFluentIcon.ADD_SUBTASK, "Add Subtask") self.editTimeAction = Action(FluentIcon.EDIT, "Edit Time") + self.markTaskAsIncompleteAction = Action(FluentIcon.VPN, "Mark as Incomplete") self.markTaskAsCompletedAction = Action(FluentIcon.VPN, "Mark as Completed") self.deleteTaskAction = Action(FluentIcon.DELETE, "Delete Subtask") self.addAction(self.addTaskAction) self.addAction(self.addSubTaskAction) self.addAction(self.editTimeAction) + self.addAction(self.markTaskAsIncompleteAction) self.addAction(self.markTaskAsCompletedAction) self.addSeparator() self.addAction(self.deleteTaskAction) diff --git a/src/views/subinterfaces/tasksView.py b/src/views/subinterfaces/tasksView.py index 983407a..748f258 100644 --- a/src/views/subinterfaces/tasksView.py +++ b/src/views/subinterfaces/tasksView.py @@ -331,24 +331,28 @@ def deleteTask(self) -> None: model.deleteTask(selectedIndex.row(), parent_index) - def markTaskAsCompleted(self) -> None: + def toggleTaskCompletion(self) -> None: """ - marks the task in todo task list as completed by simulating a drag and drop programmatically - moves it to the last of the completed task list + toggle completion state for the selected root task by simulating a drag and drop programmatically. """ - if not self.todoTasksList.selectionModel().hasSelection(): + if self.todoTasksList.selectionModel().hasSelection(): + source_list = self.todoTasksList + target_list = self.completedTasksList + elif self.completedTasksList.selectionModel().hasSelection(): + source_list = self.completedTasksList + target_list = self.todoTasksList + else: return - index = self.todoTasksList.selectionModel().currentIndex() + index = source_list.selectionModel().currentIndex() + source_model: TaskListModel = source_list.model() + target_model: TaskListModel = target_list.model() task_id = index.data(TaskListModel.IDRole) - task_node = self.todoTasksList.model().getTaskNodeById(task_id) + task_node = source_model.getTaskNodeById(task_id) if task_node is None or not task_node.is_root(): return - source_model: TaskListModel = self.todoTasksList.model() - target_model: TaskListModel = self.completedTasksList.model() - try: mime_data = source_model.mimeData([index]) ok = target_model.dropMimeData(mime_data, Qt.DropAction.MoveAction, -1, 0, QModelIndex()) From 91dc7027e6e7281a35f055491ab7da6748d70f90 Mon Sep 17 00:00:00 2001 From: Bishwa Saha <77796630+kun-codes@users.noreply.github.com> Date: Sat, 6 Jun 2026 20:59:23 +0530 Subject: [PATCH 5/5] style(taskMenu.py): update icons for change task completion actions --- src/prefabs/taskMenu.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/prefabs/taskMenu.py b/src/prefabs/taskMenu.py index 72ef0c5..f9b5b2a 100644 --- a/src/prefabs/taskMenu.py +++ b/src/prefabs/taskMenu.py @@ -13,8 +13,8 @@ def __init__(self, title: str = "", parent: Optional[QWidget] = None) -> None: self.addTaskAction = Action(FluentIcon.ADD, "Add Task") self.addSubTaskAction = Action(CustomFluentIcon.ADD_SUBTASK, "Add Subtask") self.editTimeAction = Action(FluentIcon.EDIT, "Edit Time") - self.markTaskAsIncompleteAction = Action(FluentIcon.VPN, "Mark as Incomplete") - self.markTaskAsCompletedAction = Action(FluentIcon.VPN, "Mark as Completed") + self.markTaskAsIncompleteAction = Action(FluentIcon.CANCEL, "Mark as Incomplete") + self.markTaskAsCompletedAction = Action(FluentIcon.ACCEPT, "Mark as Completed") self.deleteTaskAction = Action(FluentIcon.DELETE, "Delete Subtask") self.addAction(self.addTaskAction)