from PyQt5 import QtWidgets, QtCore, QtGui from PyQt5.QtCore import Qt from typing import List, Tuple import time import math from model import difficulty, priority from model.difficulty import Difficulty from model.priority import Priority from model.tag import Tag from model.task import Task from model.task_tag import TaskTag import database import db.tags import db.task_tags import gui.color import gui.signal import gui.tasks.dialog import gui.tasks.duration import gui.tasks.signal import gui.tasks.signal import gui.tasks.table.menu import service.tasks import util.array import util.range header_labels = ["Age", "Name", "Duration", "Difficulty", "Priority", "Tag"] class Widget(QtWidgets.QTableWidget): def __init__( self, parent, on_show: gui.signal.Reload, add_task_signal: gui.tasks.signal.AddTask): super().__init__(parent) self.init_state() self.sort() self.setSelectionBehavior(QtWidgets.QTableView.SelectRows) self.init_header() self.setRowCount(len(self._tasks)) self.setColumnCount(len(header_labels)) self.setColumnWidth(1, 500) self.update_view() self.horizontalHeader().setStretchLastSection(True) # Menu self.setContextMenuPolicy(Qt.CustomContextMenu) self.customContextMenuRequested.connect(lambda position: gui.tasks.table.menu.open(self, 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 on_show(self): self._tags = db.tags.get(database.cursor()) self.update_view() def init_state(self): self._update_task_signal = gui.tasks.signal.UpdateTask() cursor = database.cursor() self._tasks = service.tasks.get(cursor) self._task_tags = db.task_tags.get(cursor) self._tags = db.tags.get(cursor) self._sort_column = 0 self._sort_is_ascending = True def init_header(self): h = QtWidgets.QHeaderView(Qt.Horizontal, self) self._header_model = QtGui.QStandardItemModel() self._header_model.setHorizontalHeaderLabels(header_labels) h.setModel(self._header_model) h.setSectionsClickable(True) h.sectionClicked.connect(self.on_header_click) self.setHorizontalHeader(h) # header.setSectionResizeMode(2, QtWidgets.QHeaderView.ResizeToContents) 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.setItem(column, QtGui.QStandardItem('Hey!')) 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]): # TODO: just update if sort order is not impacted # self._tasks[row] = task # task_ids = [t.id for t in self._tasks] # filtred_task_tags = [tt for tt in self._task_tags if tt.task_id in task_ids] # 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 # self.update_row(row) self.delete_rows([row]) row = self.insert(task, tags) self.selectRow(row) 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, gui.tasks.duration.format(task.duration), 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 = 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] 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) gui.tasks.dialog.update(self, self._update_task_signal, row, task, tags).exec_() elif event.key() == Qt.Key_Delete: rows = self.get_selected_rows() 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] service.tasks.delete(database.cursor(), task_ids) 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]] for row in sorted(rows, reverse=True): self.removeRow(row) 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) 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 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 gui.color.easy_difficulty elif d == Difficulty.NORMAL: return gui.color.normal_difficulty elif d == Difficulty.HARD: return gui.color.hard_difficulty def priority_color(p: Priority) -> QtGui.QColor: if p == Priority.LOW: return gui.color.low_priority elif p == Priority.MIDDLE: return gui.color.middle_priority elif p == Priority.HIGH: return gui.color.high_priority