aboutsummaryrefslogtreecommitdiff
path: root/todo/gui/tasks/table/widget.py
diff options
context:
space:
mode:
Diffstat (limited to 'todo/gui/tasks/table/widget.py')
-rw-r--r--todo/gui/tasks/table/widget.py283
1 files changed, 283 insertions, 0 deletions
diff --git a/todo/gui/tasks/table/widget.py b/todo/gui/tasks/table/widget.py
new file mode 100644
index 0000000..e06c921
--- /dev/null
+++ b/todo/gui/tasks/table/widget.py
@@ -0,0 +1,283 @@
+from PyQt5 import QtWidgets, QtCore, QtGui
+from PyQt5.QtCore import Qt
+from typing import List, Tuple
+import time
+import math
+
+from todo.model import difficulty, priority
+from todo.model.difficulty import Difficulty
+from todo.model.priority import Priority
+from todo.model.tag import Tag
+from todo.model.task import Task
+from todo.model.task_tag import TaskTag
+from todo.model.status import Status
+import todo.database
+import todo.db.tags
+import todo.db.task_tags
+import todo.gui.color
+import todo.gui.signal
+import todo.gui.tasks.dialog
+import todo.gui.tasks.duration
+import todo.gui.tasks.signal
+import todo.gui.tasks.signal
+import todo.gui.tasks.table.menu
+import todo.service.tasks
+import todo.util.array
+
+class Widget(QtWidgets.QTableWidget):
+ def __init__(
+ self,
+ parent,
+ on_show: todo.gui.signal.Reload,
+ add_task_signal: todo.gui.tasks.signal.AddTask,
+ status: Status):
+ super().__init__(parent)
+
+ self.init_state(status)
+ self.sort()
+
+ self.setSelectionBehavior(QtWidgets.QTableView.SelectRows)
+ self.init_header()
+ self.setRowCount(len(self._tasks))
+ self.setColumnCount(len(self.header_labels()))
+ self.setColumnWidth(1, 500)
+
+ self.update_view()
+ self.horizontalHeader().setStretchLastSection(True)
+
+ # Menu
+ self.setContextMenuPolicy(Qt.CustomContextMenu)
+ self.customContextMenuRequested.connect(lambda position: todo.gui.tasks.table.menu.open(self, status, self._update_task_signal, position))
+
+ self.doubleClicked.connect(lambda index: self.on_double_click(index.row()))
+ add_task_signal.connect(lambda task, tags: self.insert(task, tags))
+ self._update_task_signal.connect(lambda row, task, tags: self.update_task(row, task, tags))
+ on_show.connect(lambda: self.on_show())
+
+ def init_state(self, status: Status):
+ self._update_task_signal = todo.gui.tasks.signal.UpdateTask()
+ cursor = todo.database.cursor()
+ self._status = status
+ self._tasks = todo.service.tasks.get(cursor, self._status)
+ self._task_tags = todo.db.task_tags.get(cursor)
+ self._tags = todo.db.tags.get(cursor)
+ self._sort_column = 0
+ self._sort_is_ascending = True
+
+ def init_header(self):
+ self._header_view = QtWidgets.QHeaderView(Qt.Horizontal, self)
+ self._header_model = QtGui.QStandardItemModel()
+ self._header_model.setHorizontalHeaderLabels(self.header_labels())
+ self._header_view.setModel(self._header_model)
+ self._header_view.setSectionsClickable(True)
+ self._header_view.sectionClicked.connect(self.on_header_click)
+ self.setHorizontalHeader(self._header_view)
+
+ def on_show(self):
+ cursor = todo.database.cursor()
+ self._tasks = todo.service.tasks.get(cursor, self._status)
+ self._task_tags = todo.db.task_tags.get(cursor)
+ self._tags = todo.db.tags.get(cursor)
+ self.setRowCount(len(self._tasks))
+ self.sort()
+ self.update_view()
+
+ def on_header_click(self, column):
+ if self._sort_column == column:
+ self._sort_is_ascending = not self._sort_is_ascending
+ else:
+ self._sort_is_ascending = True
+ self._sort_column = column
+ self.sort()
+ self._header_model.setHorizontalHeaderLabels(self.header_labels())
+ self.update_view()
+
+ def sort(self):
+ is_rev = self.is_reversed()
+ self._tasks = sorted(
+ self._tasks,
+ key = lambda task: self.sort_key(task, is_rev),
+ reverse = is_rev)
+
+ def update_task(self, row, task: Task, tags: List[int]):
+ self._tasks[row] = task
+ filtred_task_tags = [tt for tt in self._task_tags if tt.task_id in [t.id for t in self._tasks if t.id != task.id]]
+ new_task_tags = [TaskTag(task_id=task.id, tag_id=tag_id) for tag_id in tags]
+ self._task_tags = filtred_task_tags + new_task_tags
+
+ # Update task in table
+ self.sort()
+ row_after_sort = [i for i in range(len(self._tasks)) if self._tasks[i].id == task.id][0]
+ if row_after_sort == row:
+ self.update_row(row)
+ else:
+ self.removeRow(row)
+ self.insertRow(row_after_sort)
+ self.update_row(row_after_sort)
+ self.selectRow(row_after_sort)
+
+ def update_view(self):
+ for row in range(len(self._tasks)):
+ self.update_row(row)
+
+ def update_row(self, row: int):
+ task = self._tasks[row]
+ self.setItem(row, 0, item(age_since(task.created_at)))
+ self.setItem(row, 1, item(task.name))
+ self.setCellWidget(row, 2, colored_label(self, todo.gui.tasks.duration.format(task.duration), todo.gui.tasks.duration.color(task.duration)))
+ self.setCellWidget(row, 3, colored_label(self, difficulty.format(task.difficulty), difficulty_color(task.difficulty)))
+ self.setCellWidget(row, 4, colored_label(self, priority.format(task.priority), priority_color(task.priority)))
+ tag_ids = [tt.tag_id for tt in self._task_tags if tt.task_id == task.id]
+ res_tags = sorted([tag for tag in self._tags if tag.id in tag_ids], key=lambda t: t.name)
+ self.setCellWidget(row, 5, render_tags(self, res_tags))
+ self.setRowHeight(row, 45)
+
+ def insert(self, task: Task, tags: List[int]) -> int:
+ is_rev = self.is_reversed()
+ row = todo.util.array.insert_position(
+ self.sort_key(task, is_rev),
+ [self.sort_key(t, is_rev) for t in self._tasks],
+ is_rev)
+ self._tasks.insert(row, task)
+ self._task_tags += [TaskTag(task_id=task.id, tag_id=tag_id) for tag_id in tags]
+ self.insertRow(row)
+ self.update_row(row)
+ self._task_tags += [TaskTag(task_id=task.id, tag_id=tag_id) for tag_id in tags]
+ self.setRowCount(len(self._tasks))
+ return row
+
+ def is_reversed(self) -> bool:
+ if self._sort_column == 0:
+ return self._sort_is_ascending
+ else:
+ return not self._sort_is_ascending
+
+ def sort_key(self, task: Task, is_reversed: bool):
+ row = self._sort_column
+ if row == 0:
+ return task.created_at
+ elif row == 1:
+ return task.name.lower()
+ elif row == 2:
+ if is_reversed:
+ return task.duration
+ else:
+ return (task.duration == 0, task.duration)
+ elif row == 3:
+ return task.difficulty
+ elif row == 4:
+ return task.priority
+ elif row == 5:
+ tag_ids = [tt.tag_id for tt in self._task_tags if tt.task_id == task.id]
+ tags = sorted([tag.name.lower() for tag in self._tags if tag.id in tag_ids])
+ key = "".join(tags)
+ if is_reversed:
+ return key
+ else:
+ return (key == "", key)
+
+ def keyPressEvent(self, event):
+ super().keyPressEvent(event)
+ if event.key() in (Qt.Key_Return, Qt.Key_Enter):
+ rows = self.get_selected_rows()
+ if len(rows) == 1:
+ row = rows[0]
+ (task, tags) = self.get_at(row)
+ todo.gui.tasks.dialog.update(self, self._update_task_signal, row, task, tags).exec_()
+ elif event.key() == Qt.Key_Delete:
+ rows = self.get_selected_rows()
+ todo.gui.tasks.dialog.show_delete(self, rows, lambda: self.delete_rows(rows))
+
+ def delete_rows(self, rows: List[int]):
+ task_ids = [task.id for i, task in enumerate(self._tasks) if i in rows]
+ todo.service.tasks.delete(todo.database.cursor(), task_ids)
+ self.remove_rows_from_table(rows, task_ids)
+
+ def update_status(self, rows: List[int], status: Status):
+ task_ids = [task.id for i, task in enumerate(self._tasks) if i in rows]
+ todo.service.tasks.update_status(todo.database.cursor(), task_ids, status)
+ self.remove_rows_from_table(rows, task_ids)
+
+ def remove_rows_from_table(self, rows: List[int], task_ids: List[int]):
+ for row in sorted(rows, reverse=True):
+ self.removeRow(row)
+ self._tasks = [t for t in self._tasks if t.id not in task_ids]
+ self._task_tags = [tt for tt in self._task_tags if tt.task_id in [t.id for t in self._tasks]]
+ self.setRowCount(len(self._tasks))
+
+ def get_selected_rows(self):
+ return list(set([index.row() for index in self.selectedIndexes()]))
+
+ def on_double_click(self, row: int):
+ (task, tags) = self.get_at(row)
+ todo.gui.tasks.dialog.update(self, self._update_task_signal, row, task, tags).exec_()
+
+ def get_at(self, row: int) -> Tuple[Task, List[int]]:
+ task = self._tasks[row]
+ tags = [tt.tag_id for tt in self._task_tags if tt.task_id == task.id]
+ return (task, tags)
+
+ def header_labels(self):
+ labels = ["Age", "Name", "Duration", "Difficulty", "Priority", "Tag"]
+ if self._sort_is_ascending:
+ sign = "▼"
+ else:
+ sign = "▲"
+ labels[self._sort_column] = labels[self._sort_column] + " " + sign
+ return labels
+
+def item(text: str):
+ item = QtWidgets.QTableWidgetItem(text)
+ item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
+ return item
+
+def age_since(timestamp):
+ diff = int(time.time()) - timestamp
+ if diff >= 60 * 60 * 24:
+ return "" + str(math.floor(diff / 60 / 60 / 24)) + "d"
+ elif diff >= 60 * 60:
+ return "" + str(math.floor(diff / 60 / 60)) + "h"
+ elif diff >= 60:
+ return "" + str(math.floor(diff / 60)) + "m"
+ else:
+ return "1m"
+
+def colored_label(parent, text: str, color: QtGui.QColor):
+ label = QtWidgets.QLabel(text)
+ palette = QtGui.QPalette()
+ palette.setColor(QtGui.QPalette.Text, color)
+ label.setPalette(palette)
+ return label
+
+def render_tags(parent, tags: List[Tag]):
+ widget = QtWidgets.QWidget(parent)
+
+ layout = QtWidgets.QHBoxLayout(widget)
+ widget.setLayout(layout)
+
+ for tag in tags:
+ label = QtWidgets.QLabel(tag.name)
+ label.setContentsMargins(3, 3, 3, 3)
+ palette = QtGui.QPalette()
+ palette.setColor(QtGui.QPalette.Base, QtGui.QColor(tag.color))
+ label.setAutoFillBackground(True)
+ label.setPalette(palette)
+ layout.addWidget(label)
+
+ return widget
+
+def difficulty_color(d: Difficulty) -> QtGui.QColor:
+ if d == Difficulty.EASY:
+ return todo.gui.color.easy_difficulty
+ elif d == Difficulty.NORMAL:
+ return todo.gui.color.normal_difficulty
+ elif d == Difficulty.HARD:
+ return todo.gui.color.hard_difficulty
+
+def priority_color(p: Priority) -> QtGui.QColor:
+ if p == Priority.LOW:
+ return todo.gui.color.low_priority
+ elif p == Priority.MIDDLE:
+ return todo.gui.color.middle_priority
+ elif p == Priority.HIGH:
+ return todo.gui.color.high_priority