diff options
author | Joris | 2020-06-06 17:44:26 +0200 |
---|---|---|
committer | Joris | 2020-06-06 19:54:03 +0200 |
commit | 1595e0de940a86a7810df0e02e43838d97c0d846 (patch) | |
tree | 9701eeec0d98baa9f6044b1911df68e4c8539819 /todo/gui/tasks/table/widget.py | |
parent | 6b9195000eb5404c247288b384d7ca2bacc1ab23 (diff) | |
download | todo-1595e0de940a86a7810df0e02e43838d97c0d846.tar.gz todo-1595e0de940a86a7810df0e02e43838d97c0d846.tar.bz2 todo-1595e0de940a86a7810df0e02e43838d97c0d846.zip |
Provide nix build
Diffstat (limited to 'todo/gui/tasks/table/widget.py')
-rw-r--r-- | todo/gui/tasks/table/widget.py | 283 |
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 |