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))) if task.description: name = task.name + " *" else: name = task.name self.setItem(row, 1, item(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, 50) 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.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