from PyQt5 import QtWidgets, QtCore, QtGui from PyQt5.QtCore import Qt from typing import List, Tuple from datetime import date, timedelta import time import math 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.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, task.due_date.strftime("%d/%m/%Y") if task.due_date else "", due_date_color(task.due_date))) 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, 3, 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.due_date.isoformat() if task.due_date else "" else: return (task.due_date == None, task.due_date.isoformat() if task.due_date else "") elif row == 3: 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) rows = self.get_selected_rows() if event.key() in (Qt.Key_Return, Qt.Key_Enter): 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 and len(rows) > 0: rows = self.get_selected_rows() todo.gui.tasks.dialog.confirm_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 = ["Âge", "Nom", "Échéance", "Étiquettes"] 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)) + "j" 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 due_date_color(d: date) -> QtGui.QColor: if d != None: today = date.today() if d < today: return todo.gui.color.red elif d < today + timedelta(days = 7): return todo.gui.color.orange else: return todo.gui.color.black else: return todo.gui.color.black 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