From df828c4b141f84f731afffbe17c80618cacf9480 Mon Sep 17 00:00:00 2001 From: Joris Date: Fri, 8 May 2020 14:12:47 +0200 Subject: Bootstrap todo-next --- src/gui/tasks/table/main.py | 46 +++++++++++++++++ src/gui/tasks/table/menu.py | 43 ++++++++++++++++ src/gui/tasks/table/model.py | 116 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 205 insertions(+) create mode 100644 src/gui/tasks/table/main.py create mode 100644 src/gui/tasks/table/menu.py create mode 100644 src/gui/tasks/table/model.py (limited to 'src/gui/tasks/table') diff --git a/src/gui/tasks/table/main.py b/src/gui/tasks/table/main.py new file mode 100644 index 0000000..a990c0e --- /dev/null +++ b/src/gui/tasks/table/main.py @@ -0,0 +1,46 @@ +from PyQt5 import QtWidgets +from PyQt5.QtCore import Qt + +import db.tasks +import gui.tasks.signal +import gui.tasks.table.menu +import gui.tasks.table.model + +def widget(database, parent, add_task_signal): + table = QtWidgets.QTableView(parent) + + tasks = db.tasks.get(database.cursor()) + table_model = gui.tasks.table.model.TableModel(tasks) + + table.setModel(table_model) + table.sortByColumn( + gui.tasks.table.model.default_sort[0], + gui.tasks.table.model.default_sort[1]) + table.setSortingEnabled(True) + table.setSelectionBehavior(QtWidgets.QTableView.SelectRows) + table.horizontalHeader().setStretchLastSection(True) + resizeColumns(table) + + update_task_signal = gui.tasks.signal.UpdateTask() + + # Menu + table.setContextMenuPolicy(Qt.CustomContextMenu) + table.customContextMenuRequested.connect(lambda position: gui.tasks.table.menu.open(database, table, update_task_signal, position)) + + add_task_signal.get().connect(lambda task: insert(table, task)) + update_task_signal.get().connect(lambda row, task: update(table, row, task)) + + return table + +def insert(table, task): + table.model().insert_task(table.horizontalHeader(), task) + resizeColumns(table) + +def update(table, row, task): + row = table.model().update_task(table.horizontalHeader(), row, task) + table.selectRow(row) + resizeColumns(table) + +def resizeColumns(table): + for column in range(gui.tasks.table.model.columns): + table.resizeColumnToContents(column) diff --git a/src/gui/tasks/table/menu.py b/src/gui/tasks/table/menu.py new file mode 100644 index 0000000..4366c25 --- /dev/null +++ b/src/gui/tasks/table/menu.py @@ -0,0 +1,43 @@ +from PyQt5 import QtWidgets + +import db.tasks +import gui.tasks.modal +from model.task import Task, TaskForm + +def open(database, table, update_task_signal, position): + rows = set([index.row() for index in table.selectedIndexes()]) + + menu = QtWidgets.QMenu(table) + + if len(rows) == 1: + modify_action = menu.addAction(gui.icons.dialog_open(menu.style()), 'modify') + else: + modify_action = QtWidgets.QAction(menu) + + delete_action = menu.addAction(gui.icons.trash(menu.style()), 'Delete') + + action = menu.exec_(table.mapToGlobal(position)) + if action == modify_action and len(rows) == 1: + row = list(rows)[0] + task = table.model().get_at(row) + show_update_dialog(database, table, update_task_signal, row, task) + elif action == delete_action: + confirm = QtWidgets.QMessageBox.question(table, 'Task deletion', 'Do you really want to delete the selected tasks ?', QtWidgets.QMessageBox.No | QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.Yes) + if confirm == QtWidgets.QMessageBox.Yes: + db.tasks.delete(database.cursor(), table.model().row_ids(rows)) + database.commit() + table.model().delete_tasks(rows) + +def show_update_dialog(database, parent_widget, update_task_signal, row, task): + dialog = gui.tasks.modal.dialog( + parent_widget, + 'Modify a task', + 'modify', + task, + lambda taskForm: on_update(database, update_task_signal, row, task, taskForm)) + dialog.exec_() + +def on_update(database, update_task_signal, row, task: Task, taskForm: TaskForm): + task = db.tasks.update(database.cursor(), task, taskForm) + update_task_signal.emit(row, task) + database.commit() diff --git a/src/gui/tasks/table/model.py b/src/gui/tasks/table/model.py new file mode 100644 index 0000000..90bcc4c --- /dev/null +++ b/src/gui/tasks/table/model.py @@ -0,0 +1,116 @@ +from PyQt5 import QtCore, QtWidgets +from PyQt5.QtCore import Qt + +from model.task import Task +import time +import math +import util.array +import util.range + +columns = 3 + +headers = ['Age', 'Name', '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): + if role == Qt.DisplayRole: + task = self._tasks[index.row()] + if index.column() == 0: + return age_since(task.created_at) + if index.column() == 1: + return task.name + if index.column() == 2: + return task.tag + 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() + return util.array.insert_position( + sort_key(task, row), + [sort_key(t, row) for t in self._tasks], + is_reversed(row, order)) + + 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() + self._tasks = sorted( + self._tasks, + key = lambda task: sort_key(task, row), + reverse = is_reversed(row, order)) + 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): + if row == 0: + return task.created_at + elif row == 1: + return task.name.lower() + elif row == 2: + return task.tag.lower() + +def is_reversed(row: int, order: Qt.SortOrder) -> bool: + if row == 0: + return order == Qt.AscendingOrder + else: + return order == Qt.DescendingOrder -- cgit v1.2.3