From 0f1610333324d58acafee8c0fa9d9c9bc293b219 Mon Sep 17 00:00:00 2001 From: Joris Date: Sun, 31 May 2020 13:59:35 +0200 Subject: Use defined tags for tasks --- src/gui/tasks/table/menu.py | 10 +- src/gui/tasks/table/model.py | 165 -------------------------- src/gui/tasks/table/widget.py | 263 ++++++++++++++++++++++++++++++++++++------ 3 files changed, 231 insertions(+), 207 deletions(-) delete mode 100644 src/gui/tasks/table/model.py (limited to 'src/gui/tasks/table') diff --git a/src/gui/tasks/table/menu.py b/src/gui/tasks/table/menu.py index 435ff25..f22176c 100644 --- a/src/gui/tasks/table/menu.py +++ b/src/gui/tasks/table/menu.py @@ -1,10 +1,12 @@ from PyQt5 import QtWidgets +from typing import List import db.tasks import gui.tasks.dialog from model.task import Task, ValidTaskForm +from model.tag import Tag -def open(database, table, update_task_signal, position): +def open(table, update_task_signal, position): rows = set([index.row() for index in table.selectedIndexes()]) menu = QtWidgets.QMenu(table) @@ -19,7 +21,7 @@ def open(database, table, update_task_signal, position): action = menu.exec_(table.mapToGlobal(position)) if action == modify_action and len(rows) == 1: row = list(rows)[0] - task = table.model().get_at(row) - gui.tasks.dialog.update(database, table, update_task_signal, row, task).exec_() + (task, tags) = table.get_at(row) + gui.tasks.dialog.update(table, update_task_signal, row, task, tags).exec_() elif action == delete_action: - gui.tasks.dialog.show_delete(database, table, rows) + gui.tasks.dialog.show_delete(table, rows, lambda: table.delete_rows(rows)) diff --git a/src/gui/tasks/table/model.py b/src/gui/tasks/table/model.py deleted file mode 100644 index ab969ec..0000000 --- a/src/gui/tasks/table/model.py +++ /dev/null @@ -1,165 +0,0 @@ -from PyQt5 import QtCore, QtWidgets, QtGui -from PyQt5.QtCore import Qt - -from model.task import Task -from model.difficulty import Difficulty -from model.priority import Priority -from model import difficulty, priority -import time -import math -import util.array -import util.range -import gui.tasks.duration -import gui.color - -columns = 6 - -headers = ['Age', 'Name', 'Duration', 'Difficulty', 'Priority', 'Tag'] - -default_sort = (0, Qt.AscendingOrder) - -class TableModel(QtCore.QAbstractTableModel): - def __init__(self, tasks): - super(TableModel, self).__init__() - self._tasks = tasks - - def headerData(self, section, orientation, role): - if role == Qt.DisplayRole and orientation == Qt.Horizontal: - return headers[section] - elif role == Qt.DisplayRole and orientation == Qt.Vertical: - return section + 1 - else: - return QtCore.QVariant() - - def data(self, index, role): - task = self._tasks[index.row()] - - if role == Qt.DisplayRole: - if index.column() == 0: - return age_since(task.created_at) - elif index.column() == 1: - return task.name - elif index.column() == 2: - return gui.tasks.duration.format(task.duration) - elif index.column() == 3: - return difficulty.format(task.difficulty) - elif index.column() == 4: - return priority.format(task.priority) - elif index.column() == 5: - return task.tag - elif role == Qt.ForegroundRole: - if index.column() == 2: - return QtGui.QBrush(gui.tasks.duration.color(task.duration)) - elif index.column() == 3: - return QtGui.QBrush(difficulty_color(task.difficulty)) - elif index.column() == 4: - return QtGui.QBrush(priority_color(task.priority)) - else: - return QtCore.QVariant() - - def rowCount(self, index): - return len(self._tasks) - - def columnCount(self, index): - return columns - - def get_at(self, row): - if row >= 0 and row < len(self._tasks): - return self._tasks[row] - - - def insert_task(self, header: QtWidgets.QHeaderView, task: Task) -> int: - at = self.insert_position(header, task) - self.beginInsertRows(QtCore.QModelIndex(), at, at) - self._tasks.insert(at, task) - self.endInsertRows() - return at - - def insert_position(self, header: QtWidgets.QHeaderView, task: Task) -> int: - row = header.sortIndicatorSection() - order = header.sortIndicatorOrder() - is_rev = is_reversed(row, order) - return util.array.insert_position( - sort_key(task, row, is_rev), - [sort_key(t, row, is_rev) for t in self._tasks], - is_rev) - - def update_task(self, header: QtWidgets.QHeaderView, row, task: Task) -> int: - self.delete_task_range(row, 1) - return self.insert_task(header, task) - - def delete_tasks(self, indexes): - for range in reversed(util.range.from_indexes(indexes)): - self.delete_task_range(range.start, range.length) - return True - - def delete_task_range(self, row, rows): - self.beginRemoveRows(QtCore.QModelIndex(), row, row + rows - 1) - self._tasks = self._tasks[:row] + self._tasks[row + rows:] - self.endRemoveRows() - return True - - def row_ids(self, rows): - return [task.id for i, task in enumerate(self._tasks) if i in rows] - - def sort(self, row: int, order: Qt.SortOrder): - self.layoutAboutToBeChanged.emit() - is_rev = is_reversed(row, order) - self._tasks = sorted( - self._tasks, - key = lambda task: sort_key(task, row, is_rev), - reverse = is_rev) - self.layoutChanged.emit() - -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 sort_key(task: Task, row: int, is_reversed: bool): - 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: - if is_reversed: - return task.tag.lower() - else: - return (task.tag == '', task.tag.lower()) - -def is_reversed(row: int, order: Qt.SortOrder) -> bool: - if row == 0: - return order == Qt.AscendingOrder - else: - return order == Qt.DescendingOrder - -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 diff --git a/src/gui/tasks/table/widget.py b/src/gui/tasks/table/widget.py index 95ebe44..0a8d216 100644 --- a/src/gui/tasks/table/widget.py +++ b/src/gui/tasks/table/widget.py @@ -1,54 +1,173 @@ -from PyQt5 import QtWidgets +from PyQt5 import QtWidgets, QtCore, QtGui from PyQt5.QtCore import Qt +from typing import List, Tuple +import time +import math -import db.tasks +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 gui.tasks.table.model -import gui.tasks.dialog -from model.task import Task, ValidTaskForm +import service.tasks +import util.array +import util.range -class Widget(QtWidgets.QTableView): +header_labels = ["Age", "Name", "Duration", "Difficulty", "Priority", "Tag"] - def __init__(self, database, parent, add_task_signal): - super().__init__(parent) +class Widget(QtWidgets.QTableWidget): + def __init__( + self, + parent, + on_show: gui.signal.Reload, + add_task_signal: gui.tasks.signal.AddTask): - self._database = database - self._update_task_signal = gui.tasks.signal.UpdateTask() + super().__init__(parent) - tasks = db.tasks.get(self._database.cursor()) - table_model = gui.tasks.table.model.TableModel(tasks) + self.init_state() + self.sort() - self.setModel(table_model) - self.sortByColumn( - gui.tasks.table.model.default_sort[0], - gui.tasks.table.model.default_sort[1]) - self.setSortingEnabled(True) self.setSelectionBehavior(QtWidgets.QTableView.SelectRows) - self.horizontalHeader().setStretchLastSection(True) - self.resizeColumns() + self.init_header() + self.setRowCount(len(self._tasks)) + self.setColumnCount(len(header_labels)) + self.setColumnWidth(1, 500) - self.doubleClicked.connect(lambda index: self.on_double_click(index.row())) + self.update_view() + self.horizontalHeader().setStretchLastSection(True) # Menu self.setContextMenuPolicy(Qt.CustomContextMenu) - self.customContextMenuRequested.connect(lambda position: gui.tasks.table.menu.open(self._database, self, self._update_task_signal, position)) + 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 + - add_task_signal.get().connect(lambda task: self.insert(task)) - self._update_task_signal.get().connect(lambda row, task: self.update(row, task)) + 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 insert(self, task): - self.model().insert_task(self.horizontalHeader(), task) - self.resizeColumns() + 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 update(self, row, task): - row = self.model().update_task(self.horizontalHeader(), row, task) + 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) - self.resizeColumns() - def resizeColumns(self): - for column in range(gui.tasks.table.model.columns): - self.resizeColumnToContents(column) + 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) @@ -56,16 +175,84 @@ class Widget(QtWidgets.QTableView): rows = self.get_selected_rows() if len(rows) == 1: row = rows[0] - task = self.model().get_at(row) - gui.tasks.dialog.update( - self._database, self, self._update_task_signal, row, task).exec_() + (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._database, self, 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 = self.model().get_at(row) - gui.tasks.dialog.update(self._database, self, self._update_task_signal, row, task).exec_() + (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 -- cgit v1.2.3