From a54b7776320ef5aa02e6ef7378c2a011dc454885 Mon Sep 17 00:00:00 2001 From: Joris Date: Sat, 16 Oct 2021 20:09:55 +0200 Subject: Introduce due date Also: - Remove duration, difficulty and priority, - Translate to french. --- default.nix | 12 +++--- todo/db/init.py | 4 +- todo/db/tasks.py | 36 ++++++---------- todo/gui/color.py | 12 ------ todo/gui/icon.py | 5 ++- todo/gui/tags/panel/dialog.py | 8 ++-- todo/gui/tags/panel/form/widget.py | 38 ++--------------- todo/gui/tags/panel/table/menu.py | 2 +- todo/gui/tags/panel/table/model.py | 2 +- todo/gui/tags/panel/table/widget.py | 2 +- todo/gui/tags/panel/widget.py | 2 +- todo/gui/tasks/dialog.py | 8 ++-- todo/gui/tasks/duration.py | 50 ---------------------- todo/gui/tasks/form/state.py | 45 ++++++-------------- todo/gui/tasks/form/widget.py | 82 +++++++++++++------------------------ todo/gui/tasks/table/menu.py | 12 +++--- todo/gui/tasks/table/widget.py | 55 ++++++++++--------------- todo/gui/tasks/test_duration.py | 21 ---------- todo/gui/tasks/widget.py | 2 +- todo/gui/window.py | 6 +-- todo/model/difficulty.py | 30 -------------- todo/model/priority.py | 30 -------------- todo/model/task.py | 13 ++---- todo/util/gui/__init__.py | 0 todo/util/gui/color_input.py | 31 ++++++++++++++ todo/util/gui/date_input.py | 79 +++++++++++++++++++++++++++++++++++ 26 files changed, 226 insertions(+), 361 deletions(-) delete mode 100644 todo/gui/tasks/duration.py delete mode 100644 todo/gui/tasks/test_duration.py delete mode 100644 todo/model/difficulty.py delete mode 100644 todo/model/priority.py create mode 100644 todo/util/gui/__init__.py create mode 100644 todo/util/gui/color_input.py create mode 100644 todo/util/gui/date_input.py diff --git a/default.nix b/default.nix index 4430918..d040783 100644 --- a/default.nix +++ b/default.nix @@ -1,9 +1,9 @@ -with (import (builtins.fetchGit { - name = "nixpkgs-20.03"; - url = "git@github.com:nixos/nixpkgs.git"; - rev = "5272327b81ed355bbed5659b8d303cf2979b6953"; - ref = "refs/tags/20.03"; -}) {}); +with import (builtins.fetchGit { + name = "nixos-21.05-2021-08-02"; + url = "https://github.com/nixos/nixpkgs/"; + ref = "refs/heads/nixos-21.05"; + rev = "d4590d21006387dcb190c516724cb1e41c0f8fdf"; +}) {}; python38Packages.buildPythonApplication rec { pname = "todo"; diff --git a/todo/db/init.py b/todo/db/init.py index 5d847a3..0fb9ec1 100644 --- a/todo/db/init.py +++ b/todo/db/init.py @@ -18,9 +18,7 @@ def init(path): " created_at INTEGER NOT NULL," " updated_at INTEGER NOT NULL," " name TEXT NOT NULL," - " duration INTEGER," - " difficulty INT," - " priority INT," + " due_date TEXT," " description TEXT," " status TEXT" " )") diff --git a/todo/db/tasks.py b/todo/db/tasks.py index fc23bf0..1abbb51 100644 --- a/todo/db/tasks.py +++ b/todo/db/tasks.py @@ -1,10 +1,12 @@ from sqlite3 import Cursor import time from typing import List +from PyQt5 import QtCore +from datetime import date from todo.model.task import Task, ValidTaskForm from todo.model.status import Status -from todo.model import difficulty, priority, status +from todo.model import status def get(cursor: Cursor, s: Status) -> List[Task]: cursor.execute( @@ -13,9 +15,7 @@ def get(cursor: Cursor, s: Status) -> List[Task]: " created_at," " updated_at," " name," - " duration," - " difficulty," - " priority," + " due_date," " description" " FROM" " tasks" @@ -31,10 +31,8 @@ def get(cursor: Cursor, s: Status) -> List[Task]: created_at = task[1], updated_at = task[2], name = task[3], - duration = task[4], - difficulty = difficulty.parse(task[5]), - priority = priority.parse(task[6]), - description = task[7] + due_date = date.fromisoformat(task[4]) if task[4] else None, + description = task[5] )) return res @@ -46,22 +44,18 @@ def insert(cursor: Cursor, s: Status, form: ValidTaskForm): " created_at," " updated_at," " name," - " duration," - " difficulty," - " priority," + " due_date," " description," " status" - " ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", - (now, now, form.name, form.duration, difficulty.format(form.difficulty), priority.format(form.priority), form.description, status.format(s))) + " ) VALUES (?, ?, ?, ?, ?, ?)", + (now, now, form.name, form.due_date.isoformat() if form.due_date else "", form.description, status.format(s))) return Task( id = cursor.lastrowid, created_at = now, updated_at = now, name = form.name, - duration = form.duration, - difficulty = form.difficulty, - priority = form.priority, + due_date = form.due_date, description = form.description ) @@ -72,21 +66,17 @@ def update(cursor: Cursor, task: Task, form: ValidTaskForm): " UPDATE tasks SET" " updated_at = ?," " name = ?," - " duration = ?," - " difficulty = ?," - " priority = ?," + " due_date = ?," " description = ?" " WHERE id = ?", - (now, form.name, form.duration, difficulty.format(form.difficulty), priority.format(form.priority), form.description, task.id)) + (now, form.name, form.due_date.isoformat() if form.due_date else "", form.description, task.id)) return Task( id = task.id, created_at = task.created_at, updated_at = now, name = form.name, - duration = form.duration, - difficulty = form.difficulty, - priority = form.priority, + due_date = form.due_date, description = form.description ) diff --git a/todo/gui/color.py b/todo/gui/color.py index cc7e5a8..7ce74e5 100644 --- a/todo/gui/color.py +++ b/todo/gui/color.py @@ -7,15 +7,3 @@ green = QtGui.QColor(30, 180, 30) blue = QtGui.QColor(30, 30, 200) text = black - -easy_difficulty = green -normal_difficulty = orange -hard_difficulty = red - -low_priority = green -middle_priority = orange -high_priority = red - -short_duration = green -medium_duration = orange -long_duration = red diff --git a/todo/gui/icon.py b/todo/gui/icon.py index 7e2156d..4c9f017 100644 --- a/todo/gui/icon.py +++ b/todo/gui/icon.py @@ -18,7 +18,10 @@ def dialog_open(style): return style.standardIcon(QtWidgets.QStyle.SP_DialogOpenButton) def dialog_ok(style): - return style.standardIcon(QtWidgets.QStyle.SP_DialogOkButton) + return style.standardIcon(QtWidgets.QStyle.SP_DialogApplyButton) + +def dialog_reset(style): + return style.standardIcon(QtWidgets.QStyle.SP_DialogResetButton) def dialog_cancel(style): return style.standardIcon(QtWidgets.QStyle.SP_DialogCancelButton) diff --git a/todo/gui/tags/panel/dialog.py b/todo/gui/tags/panel/dialog.py index 6a0e5c4..fde1f1f 100644 --- a/todo/gui/tags/panel/dialog.py +++ b/todo/gui/tags/panel/dialog.py @@ -12,7 +12,7 @@ def add(parent_widget, add_tag_signal): todo.database.commit() add_tag_signal.emit(tag) - return widget(parent_widget, "Add a tag", "add", None, on_add) + return widget(parent_widget, "Ajouter une étiquette", "Ajouter", None, on_add) def update(parent_widget, update_tag_signal, row, tag): @@ -21,13 +21,13 @@ def update(parent_widget, update_tag_signal, row, tag): update_tag_signal.emit(row, updated_tag) todo.database.commit() - return widget(parent_widget, "Modify a tag", "modify", tag, on_update) + return widget(parent_widget, "Modifier une étiquette", "Modifier", tag, on_update) def confirm_delete(table, rows): confirm = QtWidgets.QMessageBox.question( table, - "Tag deletion", - "Do you really want to delete the selected tags ?", + "Suppression des étiquettes", + "Voulez-vous vraiment supprimer les étiquettes sélectionnées ?", QtWidgets.QMessageBox.No | QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.Yes) diff --git a/todo/gui/tags/panel/form/widget.py b/todo/gui/tags/panel/form/widget.py index 9ac4eb1..1b1744d 100644 --- a/todo/gui/tags/panel/form/widget.py +++ b/todo/gui/tags/panel/form/widget.py @@ -2,10 +2,10 @@ from PyQt5 import QtWidgets, QtCore, QtGui from typing import Optional, Tuple, List, Any from todo.model.tag import Tag, ValidTagForm -from todo.model import difficulty, priority import todo.gui.icon import todo.gui.tags.panel.form.state import todo.gui.color +from todo.util.gui.color_input import ColorInput def widget( parent: QtWidgets.QWidget, @@ -24,10 +24,10 @@ def widget( grid.setLayout(grid_layout) init_name = tag.name if tag is not None else "" - name_input = line_edit(grid, grid_layout, 0, "Name", init_name) + name_input = line_edit(grid, grid_layout, 0, "Nom", init_name) init_color = tag.color if tag is not None else "#FFFFFF" - color_input = color_edit(grid, grid_layout, 1, "Color", QtGui.QColor(init_color)) + color_input = color_edit(grid, grid_layout, 1, "Couleur", QtGui.QColor(init_color)) tag_form_edition = todo.gui.tags.panel.form.state.TagFormEdition( init_name, @@ -81,36 +81,6 @@ def color_edit( return edit -class ColorInput(QtWidgets.QLineEdit): - - def __init__(self, init_color: QtGui.QColor, parent): - super().__init__(parent) - self.setReadOnly(True) - self.installEventFilter(self) - self._color = init_color - self.update(init_color) - self._is_editing = False - - def eventFilter(self, source, event): - if source is self and event.type() == QtCore.QEvent.FocusIn: - if not self._is_editing: - self._is_editing = True - color = QtWidgets.QColorDialog.getColor(self._color, self) - if color.isValid(): - self.update(color) - else: - self._is_editing = False - self.clearFocus() - return super(ColorInput, self).eventFilter(source, event) - - def update(self, color: QtGui.QColor): - self._color = color - self.setText(color.name().upper()) - palette = QtGui.QPalette() - palette.setColor(QtGui.QPalette.Base, color) - palette.setColor(QtGui.QPalette.Text, color) - self.setPalette(palette) - def buttons(parent, action_title, tag_form_signal, on_validate, on_cancel): widget = QtWidgets.QWidget(parent) layout = QtWidgets.QHBoxLayout(widget) @@ -129,7 +99,7 @@ def buttons(parent, action_title, tag_form_signal, on_validate, on_cancel): tag_form_signal.connect(on_tag_form_signal) - cancel = QtWidgets.QPushButton("cancel", widget) + cancel = QtWidgets.QPushButton("Annuler", widget) cancel.setIcon(todo.gui.icon.dialog_cancel(cancel.style())) cancel.clicked.connect(on_cancel) layout.addWidget(cancel) diff --git a/todo/gui/tags/panel/table/menu.py b/todo/gui/tags/panel/table/menu.py index 0446841..7ceded7 100644 --- a/todo/gui/tags/panel/table/menu.py +++ b/todo/gui/tags/panel/table/menu.py @@ -31,5 +31,5 @@ def open(table, update_tag_signal, position): row = list(rows)[0] tag = table.model().get_at(row) todo.gui.tags.panel.dialog.update(table, update_tag_signal, row, tag).exec_() - elif action == delete_action: + elif action == delete_action and len(rows) > 0: todo.gui.tags.panel.dialog.confirm_delete(table, rows) diff --git a/todo/gui/tags/panel/table/model.py b/todo/gui/tags/panel/table/model.py index 6f9d71a..0112b03 100644 --- a/todo/gui/tags/panel/table/model.py +++ b/todo/gui/tags/panel/table/model.py @@ -10,7 +10,7 @@ import todo.util.range columns = 1 -headers = ["Name", "Color"] +headers = ["Nom", "Couleur"] default_sort = (0, Qt.AscendingOrder) diff --git a/todo/gui/tags/panel/table/widget.py b/todo/gui/tags/panel/table/widget.py index 1d10c73..dc2f1c5 100644 --- a/todo/gui/tags/panel/table/widget.py +++ b/todo/gui/tags/panel/table/widget.py @@ -62,7 +62,7 @@ class Widget(QtWidgets.QTableView): elif event.key() == Qt.Key_Delete: rows = self.get_selected_rows() tags = self.model().row_ids(rows) - if not todo.db.task_tags.one_is_used(todo.database.cursor(), tags): + if not todo.db.task_tags.one_is_used(todo.database.cursor(), tags) and len(rows) > 0: todo.gui.tags.panel.dialog.confirm_delete(self, rows) def get_selected_rows(self): diff --git a/todo/gui/tags/panel/widget.py b/todo/gui/tags/panel/widget.py index 071442e..675b0ff 100644 --- a/todo/gui/tags/panel/widget.py +++ b/todo/gui/tags/panel/widget.py @@ -15,7 +15,7 @@ def widget(parent): add_tag_signal = todo.gui.tags.panel.signal.AddTag() - add_tag_button = QtWidgets.QPushButton(" Add a tag", widget) + add_tag_button = QtWidgets.QPushButton(" Ajouter une étiquette", widget) add_tag_button.setFixedHeight(30) add_tag_button.setIcon(todo.gui.icon.new_folder(widget.style())) diff --git a/todo/gui/tasks/dialog.py b/todo/gui/tasks/dialog.py index 2bed6af..9cfa2be 100644 --- a/todo/gui/tasks/dialog.py +++ b/todo/gui/tasks/dialog.py @@ -14,7 +14,7 @@ def add(parent_widget, status: Status, add_task_signal): task = todo.service.tasks.create(todo.database.cursor(), status, task_form) add_task_signal.emit(task, task_form.tags) - return widget(parent_widget, "Add a task", "add", None, [], on_add) + return widget(parent_widget, "Ajouter une tâche", "ajouter", None, [], on_add) def update(parent_widget, update_task_signal, row: int, task: Task, tags: List[int]): @@ -22,13 +22,13 @@ def update(parent_widget, update_task_signal, row: int, task: Task, tags: List[i updated_task = todo.service.tasks.update(todo.database.cursor(), task, tags, task_form) update_task_signal.emit(row, updated_task, task_form.tags) - return widget(parent_widget, "Modify a task", "modify", task, tags, on_update) + return widget(parent_widget, "Modifier une tâche", "modifier", task, tags, on_update) def confirm_delete(parent, rows: List[int], on_confirm): confirm = QtWidgets.QMessageBox.question( parent, - "Task deletion", - "Do you really want to delete the selected tasks ?", + "Suppression des tâches", + "Voulez-vous vraiment supprimer les tâches selectionnées ?", QtWidgets.QMessageBox.No | QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.Yes) diff --git a/todo/gui/tasks/duration.py b/todo/gui/tasks/duration.py deleted file mode 100644 index 81db661..0000000 --- a/todo/gui/tasks/duration.py +++ /dev/null @@ -1,50 +0,0 @@ -from PyQt5 import QtGui -from typing import Optional -import math -import re - -import todo.gui.color - -def format(minutes: int): - if minutes >= 60 * 24: - return "" + format_decimal(minutes / 60 / 24) + "d" - elif minutes >= 60: - return "" + format_decimal(minutes / 60) + "h" - elif minutes > 0: - return "" + str(minutes) + "m" - else: - return "" - -def format_decimal(d: float) -> str: - return "{0:.2g}".format(d) - -def parse(duration: str) -> Optional[int]: - duration = duration.strip() - if duration: - result = re.match("^(\d+)(\.(\d+))?([mhd])$", duration.strip()) - if result: - n = int(result.group(1)) - if result.group(3): - d = int(result.group(3)) * pow(10, -1 * len(result.group(3))) - else: - d = 0 - num = n + d - unit = result.group(4) - if unit == "m": - return math.floor(num) - elif unit == "h": - return math.floor(num * 60) - elif unit == "d": - return math.floor(num * 60 * 24) - else: - return None - else: - return 0 - -def color(minutes: int): - if minutes <= 15: - return todo.gui.color.short_duration - elif minutes < 60: - return todo.gui.color.medium_duration - else: - return todo.gui.color.long_duration diff --git a/todo/gui/tasks/form/state.py b/todo/gui/tasks/form/state.py index c073214..ed64747 100644 --- a/todo/gui/tasks/form/state.py +++ b/todo/gui/tasks/form/state.py @@ -1,10 +1,8 @@ from PyQt5 import QtCore from typing import Optional, List +from datetime import date, datetime from todo.model.task import ValidTaskForm -from todo.model.difficulty import Difficulty -from todo.model.priority import Priority -import todo.gui.tasks.duration import todo.gui.tags.list class TaskFormEdition: @@ -12,45 +10,33 @@ class TaskFormEdition: self, name, name_signal, - duration, - duration_signal, - difficulty, - difficulty_signal, - priority, - priority_signal, + due_date, + due_date_signal, tags: List[int], tags_signal: todo.gui.tags.list.SelectionSignal, description, description_signal): self._name = name - self._duration = duration - self._difficulty = difficulty - self._priority = priority + self._due_date = parse_date(due_date) self._tags = tags self._description = description self._signal = ValidTaskFormSignal() name_signal.connect(lambda n: self.on_name_signal(n)) - duration_signal.connect(lambda d: self.on_duration_signal(d)) - difficulty_signal.connect(lambda d: self.on_difficulty_signal(d)) - priority_signal.connect(lambda p: self.on_priority_signal(p)) + due_date_signal.connect(lambda d: self.on_due_date_signal(d)) tags_signal.connect(lambda ts: self.on_tags_signal(ts)) description_signal.connect(lambda d: self.on_description_signal(d)) def get(self) -> Optional[ValidTaskForm]: name = self._name.strip() - duration = todo.gui.tasks.duration.parse(self._duration) - difficulty = self._difficulty - priority = self._priority + due_date = self._due_date description = self._description.strip() - if name and duration != None: + if name: return ValidTaskForm( name = name, - duration = duration, - difficulty = difficulty, - priority = priority, + due_date = due_date, tags = self._tags, description = description) else: @@ -60,16 +46,8 @@ class TaskFormEdition: self._name = name self.emit() - def on_duration_signal(self, duration: str): - self._duration = duration - self.emit() - - def on_difficulty_signal(self, index: int): - self._difficulty = Difficulty(index) - self.emit() - - def on_priority_signal(self, index: int): - self._priority = Priority(index) + def on_due_date_signal(self, due_date: str): + self._due_date = parse_date(due_date) self.emit() def on_tags_signal(self, tags: List[int]): @@ -99,3 +77,6 @@ class ValidTaskFormSignal(QtCore.QObject): def connect(self, f): self._signal.connect(f) + +def parse_date(d: str) -> date: + return datetime.strptime(d, "%d/%m/%Y").date() if d else None diff --git a/todo/gui/tasks/form/widget.py b/todo/gui/tasks/form/widget.py index a6ce488..a401eaa 100644 --- a/todo/gui/tasks/form/widget.py +++ b/todo/gui/tasks/form/widget.py @@ -3,11 +3,10 @@ from typing import Optional, Tuple, List, Any from todo.model.task import Task, ValidTaskForm from todo.model.tag import Tag -from todo.model import difficulty, priority import todo.gui.icon import todo.gui.tasks.form.state -import todo.gui.tasks.duration import todo.gui.tags.list +from todo.util.gui.date_input import DateInput class TextEditSignal(QtCore.QObject): _signal = QtCore.pyqtSignal(str, name = "textEdit") @@ -34,33 +33,11 @@ def widget( widget.setLayout(layout) init_name = task.name if task is not None else "" - name_input = name_edit(widget, layout, "Name", init_name) - - grid = QtWidgets.QWidget(widget) - layout.addWidget(grid) - grid_layout = QtWidgets.QGridLayout(grid) - grid.setLayout(grid_layout) - - init_duration = todo.gui.tasks.duration.format(task.duration) if task is not None else "" - duration_input = line_edit(grid, grid_layout, 1, "Duration", init_duration) - - init_difficulty = task.difficulty if task is not None else difficulty.Difficulty.NORMAL - difficulty_input = combo_box( - grid, - grid_layout, - 3, - "Difficulty", - [difficulty.format(d) for d in difficulty.values], - int(init_difficulty)) - - init_priority = task.priority if task is not None else priority.Priority.MIDDLE - priority_input = combo_box( - grid, - grid_layout, - 4, - "Priority", - [priority.format(d) for d in priority.values], - int(init_priority)) + name_input = name_edit(widget, layout, "Nom", init_name) + + init_due_date = (task.due_date.strftime("%d/%m/%Y") if task.due_date else None) if task else None + (due_date_input, due_date_signal) = date_edit(widget, "Échéance", init_due_date) + layout.addWidget(due_date_input) (tags_list_widget, tags_signal) = tags_selection(widget, tags) layout.addWidget(tags_list_widget) @@ -72,12 +49,8 @@ def widget( task_form_edition = todo.gui.tasks.form.state.TaskFormEdition( init_name, name_input.textChanged, - init_duration, - duration_input.textChanged, - init_difficulty, - difficulty_input.currentIndexChanged, - init_priority, - priority_input.currentIndexChanged, + init_due_date, + due_date_signal, tags, tags_signal, init_description, @@ -119,23 +92,6 @@ def name_edit( return edit -def line_edit( - parent, - layout: QtWidgets.QGridLayout, - n: int, - label: str, - default_value: str) -> QtWidgets.QLineEdit: - - label = QtWidgets.QLabel(label, parent) - layout.addWidget(label, n, 0) - - edit = QtWidgets.QLineEdit(parent) - if default_value != None: - edit.setText(default_value) - layout.addWidget(edit, n, 1) - - return edit - def combo_box( parent, layout: QtWidgets.QGridLayout, @@ -162,7 +118,8 @@ def tags_selection(parent, init_tags: List[int]) -> QtWidgets.QWidget: def text_edit( parent, label: str, - default_value: str) -> Tuple[QtWidgets.QWidget, TextEditSignal]: + default_value: str + ) -> Tuple[QtWidgets.QWidget, TextEditSignal]: widget = QtWidgets.QWidget(parent) layout = QtWidgets.QVBoxLayout(widget) @@ -181,6 +138,23 @@ def text_edit( return (widget, signal) +def date_edit( + parent, + label: str, + init_date: str + ) -> QtWidgets.QLineEdit: + + widget = QtWidgets.QWidget(parent) + layout = QtWidgets.QVBoxLayout(widget) + + label = QtWidgets.QLabel(label, parent) + layout.addWidget(label) + + date_input = DateInput(init_date, parent) + layout.addWidget(date_input) + + return (widget, date_input.textChanged) + def buttons(parent, action_title, task_form_signal, on_validate, on_cancel): widget = QtWidgets.QWidget(parent) layout = QtWidgets.QHBoxLayout(widget) @@ -199,7 +173,7 @@ def buttons(parent, action_title, task_form_signal, on_validate, on_cancel): task_form_signal.connect(on_task_form_signal) - cancel = QtWidgets.QPushButton("cancel", widget) + cancel = QtWidgets.QPushButton("Annuler", widget) cancel.setIcon(todo.gui.icon.dialog_cancel(cancel.style())) cancel.clicked.connect(on_cancel) layout.addWidget(cancel) diff --git a/todo/gui/tasks/table/menu.py b/todo/gui/tasks/table/menu.py index 78c85ac..ad8415b 100644 --- a/todo/gui/tasks/table/menu.py +++ b/todo/gui/tasks/table/menu.py @@ -12,24 +12,24 @@ def open(table: QtWidgets.QTableWidget, status: Status, update_task_signal, posi menu = QtWidgets.QMenu(table) if len(rows) == 1: - modify_action = menu.addAction(todo.gui.icon.dialog_open(menu.style()), "modify") + modify_action = menu.addAction(todo.gui.icon.dialog_open(menu.style()), "Modifier") else: modify_action = QtWidgets.QAction(menu) - delete_action = menu.addAction(todo.gui.icon.trash(menu.style()), "delete") + delete_action = menu.addAction(todo.gui.icon.trash(menu.style()), "Supprimer") if status != Status.READY: - move_to_ready = menu.addAction(todo.gui.icon.task_ready(menu.style()), "move to ready") + move_to_ready = menu.addAction(todo.gui.icon.task_ready(menu.style()), "Mettre à prêt") else: move_to_ready = QtWidgets.QAction(menu) if status != Status.WAITING: - move_to_waiting = menu.addAction(todo.gui.icon.task_waiting(menu.style()), "move to waiting") + move_to_waiting = menu.addAction(todo.gui.icon.task_waiting(menu.style()), "Mettre en attente") else: move_to_waiting = QtWidgets.QAction(menu) if status != Status.MAYBE: - move_to_maybe = menu.addAction(todo.gui.icon.task_maybe(menu.style()), "move to maybe") + move_to_maybe = menu.addAction(todo.gui.icon.task_maybe(menu.style()), "Mettre à peut-être") else: move_to_maybe = QtWidgets.QAction(menu) @@ -38,7 +38,7 @@ def open(table: QtWidgets.QTableWidget, status: Status, update_task_signal, posi row = list(rows)[0] (task, tags) = table.get_at(row) todo.gui.tasks.dialog.update(table, update_task_signal, row, task, tags).exec_() - elif action == delete_action: + elif action == delete_action and len(rows) > 0: todo.gui.tasks.dialog.confirm_delete(table, rows, lambda: table.delete_rows(rows)) elif action == move_to_ready: table.update_status(rows, Status.READY) diff --git a/todo/gui/tasks/table/widget.py b/todo/gui/tasks/table/widget.py index b379abc..e374c90 100644 --- a/todo/gui/tasks/table/widget.py +++ b/todo/gui/tasks/table/widget.py @@ -1,12 +1,10 @@ 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 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 @@ -17,7 +15,6 @@ 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 @@ -128,12 +125,10 @@ class Widget(QtWidgets.QTableWidget): 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))) + 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, 5, render_tags(self, res_tags)) + self.setCellWidget(row, 3, render_tags(self, res_tags)) self.setRowHeight(row, 50) def insert(self, task: Task, tags: List[int]) -> int: @@ -163,14 +158,10 @@ class Widget(QtWidgets.QTableWidget): return task.name.lower() elif row == 2: if is_reversed: - return task.duration + return task.due_date.isoformat() if task.due_date else "" else: - return (task.duration == 0, task.duration) + return (task.due_date == None, task.due_date.isoformat() if task.due_date else "") 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) @@ -181,13 +172,13 @@ class Widget(QtWidgets.QTableWidget): def keyPressEvent(self, event): super().keyPressEvent(event) + rows = self.get_selected_rows() 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: + 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)) @@ -221,7 +212,7 @@ class Widget(QtWidgets.QTableWidget): return (task, tags) def header_labels(self): - labels = ["Age", "Name", "Duration", "Difficulty", "Priority", "Tag"] + labels = ["Âge", "Nom", "Échéance", "Étiquettes"] if self._sort_is_ascending: sign = "▼" else: @@ -237,7 +228,7 @@ def item(text: str): def age_since(timestamp): diff = int(time.time()) - timestamp if diff >= 60 * 60 * 24: - return "" + str(math.floor(diff / 60 / 60 / 24)) + "d" + return "" + str(math.floor(diff / 60 / 60 / 24)) + "j" elif diff >= 60 * 60: return "" + str(math.floor(diff / 60 / 60)) + "h" elif diff >= 60: @@ -245,6 +236,18 @@ def age_since(timestamp): 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() @@ -268,19 +271,3 @@ def render_tags(parent, tags: List[Tag]): 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 diff --git a/todo/gui/tasks/test_duration.py b/todo/gui/tasks/test_duration.py deleted file mode 100644 index 1435e2d..0000000 --- a/todo/gui/tasks/test_duration.py +++ /dev/null @@ -1,21 +0,0 @@ -from todo.gui.tasks.duration import format, parse - -def test_format(): - assert format(0) == "" - assert format(0.5) == "0.5m" - assert format(35) == "35m" - assert format(60) == "1h" - assert format(61) == "1h" - assert format(90) == "1.5h" - assert format(1440) == "1d" - -def test_parse(): - assert parse("") == 0 - assert parse("42") == None - assert parse("hey") == None - assert parse("1h30") == None - assert parse("1h30m") == None - assert parse("17m") == 17 - assert parse("90m") == 90 - assert parse("1.5h") == 90 - assert parse("2d") == 2880 diff --git a/todo/gui/tasks/widget.py b/todo/gui/tasks/widget.py index cc7fe96..7c8f80b 100644 --- a/todo/gui/tasks/widget.py +++ b/todo/gui/tasks/widget.py @@ -16,7 +16,7 @@ def widget(parent, on_show: todo.gui.signal.Reload, status: Status): add_task_signal = todo.gui.tasks.signal.AddTask() - add_task_button = QtWidgets.QPushButton(" Add a task", widget) + add_task_button = QtWidgets.QPushButton(" Ajouter une tâche", widget) add_task_button.setFixedHeight(30) add_task_button.setIcon(todo.gui.icon.new_folder(widget.style())) add_task_button.clicked.connect(lambda: todo.gui.tasks.dialog.add(widget, status, add_task_signal).exec_()) diff --git a/todo/gui/window.py b/todo/gui/window.py index 0391ee9..18518e6 100644 --- a/todo/gui/window.py +++ b/todo/gui/window.py @@ -28,9 +28,9 @@ def get(): tabs.currentChanged.connect(on_current_tab_changed) - tabs.addTab(todo.gui.tasks.widget.widget(tabs, show_ready, Status.READY), "Ready") - tabs.addTab(todo.gui.tasks.widget.widget(tabs, show_waiting, Status.WAITING), "Waiting") - tabs.addTab(todo.gui.tasks.widget.widget(tabs, show_maybe, Status.MAYBE), "Maybe") + tabs.addTab(todo.gui.tasks.widget.widget(tabs, show_ready, Status.READY), "Prêt") + tabs.addTab(todo.gui.tasks.widget.widget(tabs, show_waiting, Status.WAITING), "En attente") + tabs.addTab(todo.gui.tasks.widget.widget(tabs, show_maybe, Status.MAYBE), "Peut-être") tabs.addTab(todo.gui.tags.panel.widget.widget(tabs), "Tags") return window diff --git a/todo/model/difficulty.py b/todo/model/difficulty.py deleted file mode 100644 index 526cdb9..0000000 --- a/todo/model/difficulty.py +++ /dev/null @@ -1,30 +0,0 @@ -from enum import IntEnum -from typing import Optional - -class Difficulty(IntEnum): - EASY = 0 - NORMAL = 1 - HARD = 2 - -values = [ - Difficulty.EASY, - Difficulty.NORMAL, - Difficulty.HARD] - -def format(difficulty: Difficulty) -> str: - if difficulty == Difficulty.EASY: - return "Easy" - elif difficulty == Difficulty.NORMAL: - return "Normal" - elif difficulty == Difficulty.HARD: - return "Hard" - -def parse(string: str) -> Optional[Difficulty]: - if string == "Easy": - return Difficulty.EASY - elif string == "Normal": - return Difficulty.NORMAL - elif string == "Hard": - return Difficulty.HARD - else: - return None diff --git a/todo/model/priority.py b/todo/model/priority.py deleted file mode 100644 index 5948104..0000000 --- a/todo/model/priority.py +++ /dev/null @@ -1,30 +0,0 @@ -from enum import IntEnum -from typing import Optional - -class Priority(IntEnum): - LOW = 0 - MIDDLE = 1 - HIGH = 2 - -values = [ - Priority.LOW, - Priority.MIDDLE, - Priority.HIGH] - -def format(priority: Priority) -> str: - if priority == Priority.LOW: - return "Low" - elif priority == Priority.MIDDLE: - return "Middle" - elif priority == Priority.HIGH: - return "High" - -def parse(string: str) -> Optional[Priority]: - if string == "Low": - return Priority.LOW - elif string == "Middle": - return Priority.MIDDLE - elif string == "High": - return Priority.HIGH - else: - return None diff --git a/todo/model/task.py b/todo/model/task.py index f20cbc9..086a84b 100644 --- a/todo/model/task.py +++ b/todo/model/task.py @@ -1,22 +1,17 @@ from typing import NamedTuple, List - -from todo.model.difficulty import Difficulty -from todo.model.priority import Priority +from PyQt5 import QtCore +from datetime import date class Task(NamedTuple): id: int created_at: int updated_at: int name: str - duration: int - difficulty: Difficulty - priority: Priority + due_date: date description: str class ValidTaskForm(NamedTuple): name: str - duration: int - difficulty: Difficulty - priority: Priority + due_date: date tags: List[int] description: str diff --git a/todo/util/gui/__init__.py b/todo/util/gui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/todo/util/gui/color_input.py b/todo/util/gui/color_input.py new file mode 100644 index 0000000..032aea1 --- /dev/null +++ b/todo/util/gui/color_input.py @@ -0,0 +1,31 @@ +from PyQt5 import QtWidgets, QtCore, QtGui + +class ColorInput(QtWidgets.QLineEdit): + + def __init__(self, init_color: QtGui.QColor, parent): + super().__init__(parent) + self.setReadOnly(True) + self.installEventFilter(self) + self._color = init_color + self.update(init_color) + self._is_editing = False + + def eventFilter(self, source, event): + if source is self and event.type() == QtCore.QEvent.FocusIn: + if not self._is_editing: + self._is_editing = True + color = QtWidgets.QColorDialog.getColor(self._color, self) + if color.isValid(): + self.update(color) + else: + self._is_editing = False + self.clearFocus() + return super(ColorInput, self).eventFilter(source, event) + + def update(self, color: QtGui.QColor): + self._color = color + self.setText(color.name().upper()) + palette = QtGui.QPalette() + palette.setColor(QtGui.QPalette.Base, color) + palette.setColor(QtGui.QPalette.Text, color) + self.setPalette(palette) diff --git a/todo/util/gui/date_input.py b/todo/util/gui/date_input.py new file mode 100644 index 0000000..06536c7 --- /dev/null +++ b/todo/util/gui/date_input.py @@ -0,0 +1,79 @@ +from PyQt5 import QtWidgets, QtCore, QtGui + +import todo.gui.icon + +class DateInput(QtWidgets.QLineEdit): + + def __init__(self, init_date: str, parent): + super().__init__(parent) + self.setReadOnly(True) + self.installEventFilter(self) + self._date = init_date + self.update(init_date) + self._is_editing = False + + def eventFilter(self, source, event): + if source is self and event.type() == QtCore.QEvent.FocusIn: + if not self._is_editing: + self._is_editing = True + Calendar(self, self._date, self.update).exec_() + else: + self._is_editing = False + self.clearFocus() + return super(DateInput, self).eventFilter(source, event) + + def update(self, date: str): + self._date = date + self.setText(date) + +class Calendar(QtWidgets.QDialog): + + def __init__(self, parent: QtWidgets.QWidget, init_date: str, update_date): + super().__init__(parent) + + self._update_date = update_date + self._calendar = QtWidgets.QCalendarWidget(self) + + self.setWindowTitle("Sélectionnez une date") + self.setMinimumSize(QtCore.QSize(300, 400)) + + layout = QtWidgets.QVBoxLayout(self) + self.setLayout(layout) + + self._calendar.setMinimumDate(QtCore.QDate.currentDate()) + self._calendar.activated.connect(self.select) + if init_date: + self._calendar.setSelectedDate(QtCore.QDate.fromString(init_date, "dd/MM/yyyy")) + layout.addWidget(self._calendar) + + buttons = QtWidgets.QWidget(self) + buttons_layout = QtWidgets.QHBoxLayout(buttons) + layout.addWidget(buttons) + + accept_button = QtWidgets.QPushButton("Sélectionner", self) + accept_button.setIcon(todo.gui.icon.dialog_ok(accept_button.style())) + accept_button.clicked.connect(self.select) + buttons_layout.addWidget(accept_button) + + reset_button = QtWidgets.QPushButton("Effacer", self) + reset_button.setIcon(todo.gui.icon.dialog_reset(reset_button.style())) + reset_button.clicked.connect(self.reset) + buttons_layout.addWidget(reset_button) + + cancel_button = QtWidgets.QPushButton("Annuler", self) + cancel_button.setIcon(todo.gui.icon.dialog_cancel(cancel_button.style())) + cancel_button.clicked.connect(lambda: self.reject()) + buttons_layout.addWidget(cancel_button) + + def select(self): + self.accept() + self._update_date(self._calendar.selectedDate().toString("dd/MM/yyyy")) + + def reset(self): + self.accept() + self._update_date("") + + def keyPressEvent(self, event): + super().keyPressEvent(event) + if event.key() in (QtCore.Qt.Key_Delete, QtCore.Qt.Key_Backspace): + self.reset() -- cgit v1.2.3