From 3999d5d39b3ab87b5d1245790ced6a3bbd8faddb Mon Sep 17 00:00:00 2001 From: Jann Stute <46534683+Computerdores@users.noreply.github.com> Date: Fri, 6 Jun 2025 21:41:52 +0200 Subject: [PATCH] feat(ui): improved datetime field modal using QDateTimeEdit (#946) * feat: custom modal making use of QDateTimeEdit * fix: add back license notice * refactor: remove unnecessary line * feat: use date and hour format from settings for date time picker --- src/tagstudio/core/global_settings.py | 7 +- src/tagstudio/qt/ts_qt.py | 3 - src/tagstudio/qt/widgets/datetime_picker.py | 82 +++++++++++++++++++ .../qt/widgets/preview/field_containers.py | 17 ++-- src/tagstudio/qt/widgets/text_line_edit.py | 2 - 5 files changed, 97 insertions(+), 14 deletions(-) create mode 100644 src/tagstudio/qt/widgets/datetime_picker.py diff --git a/src/tagstudio/core/global_settings.py b/src/tagstudio/core/global_settings.py index a30fdeec..56cb4a6a 100644 --- a/src/tagstudio/core/global_settings.py +++ b/src/tagstudio/core/global_settings.py @@ -79,7 +79,8 @@ class GlobalSettings(BaseModel): with open(path, "w") as f: toml.dump(self.model_dump(), f, encoder=TomlEnumEncoder()) - def format_datetime(self, dt: datetime) -> str: + @property + def datetime_format(self) -> str: date_format = self.date_format is_24h = self.hour_format hour_format = "%H:%M:%S" if is_24h else "%I:%M:%S %p" @@ -94,5 +95,7 @@ class GlobalSettings(BaseModel): hour_format = hour_format.replace("%H", f"%{zero_padding_symbol}H").replace( "%I", f"%{zero_padding_symbol}I" ) + return f"{date_format}, {hour_format}" - return datetime.strftime(dt, f"{date_format}, {hour_format}") + def format_datetime(self, dt: datetime) -> str: + return datetime.strftime(dt, self.datetime_format) diff --git a/src/tagstudio/qt/ts_qt.py b/src/tagstudio/qt/ts_qt.py index d25789d3..036dac1c 100644 --- a/src/tagstudio/qt/ts_qt.py +++ b/src/tagstudio/qt/ts_qt.py @@ -37,7 +37,6 @@ from PySide6.QtGui import ( QMouseEvent, QPalette, ) -from PySide6.QtUiTools import QUiLoader from PySide6.QtWidgets import ( QApplication, QFileDialog, @@ -298,8 +297,6 @@ class QtDriver(DriverMixin, QObject): def start(self) -> None: """Launch the main Qt window.""" - _ = QUiLoader() - if self.settings.theme == Theme.SYSTEM and platform.system() == "Windows": sys.argv += ["-platform", "windows:darkmode=2"] self.app = QApplication(sys.argv) diff --git a/src/tagstudio/qt/widgets/datetime_picker.py b/src/tagstudio/qt/widgets/datetime_picker.py new file mode 100644 index 00000000..3ac991da --- /dev/null +++ b/src/tagstudio/qt/widgets/datetime_picker.py @@ -0,0 +1,82 @@ +import typing +from collections.abc import Callable +from datetime import datetime as dt +from typing import cast + +from PySide6.QtCore import QDateTime +from PySide6.QtWidgets import QDateTimeEdit, QVBoxLayout + +from tagstudio.qt.widgets.panel import PanelWidget + +if typing.TYPE_CHECKING: + from tagstudio.qt.ts_qt import QtDriver + +DATETIME_FORMAT = "%Y-%m-%d %H:%M:%S" + + +QDTF2DTF = { + "%d": "dd", + "%m": "MM", + "%y": "yy", + "%H": "HH", + "%M": "mm", + "%S": "ss", + "%Y": "yyyy", + "%I": "hh", + "%p": "AP", + "%x": "MM/dd/yy", +} + + +def qdtf2dtf(dtf: str) -> str: + out = dtf + for old, new in QDTF2DTF.items(): + out = out.replace(old, new) + return out + + +class DatetimePicker(PanelWidget): + def __init__(self, driver: "QtDriver", datetime: dt | str): + super().__init__() + self.root_layout = QVBoxLayout(self) + self.root_layout.setContentsMargins(6, 0, 6, 0) + + if isinstance(datetime, str): + datetime = DatetimePicker.string2dt(datetime) + self.datetime_edit = QDateTimeEdit() + self.datetime_edit.setCalendarPopup(True) + self.datetime_edit.setDateTime(DatetimePicker.dt2qdt(datetime)) + # sketchy way to show seconds without showing the day of the week; + # while also still having localisation + self.datetime_edit.setDisplayFormat(qdtf2dtf(driver.settings.datetime_format)) + + self.initial_value = datetime + self.root_layout.addWidget(self.datetime_edit) + + def get_content(self): + return DatetimePicker.dt2string(DatetimePicker.qdt2dt(self.datetime_edit.dateTime())) + + def reset(self): + self.datetime_edit.setDateTime(DatetimePicker.dt2qdt(self.initial_value)) + + def add_callback(self, callback: Callable, event: str = "returnPressed"): + if event == "returnPressed": + pass + else: + raise ValueError(f"unknown event type: {event}") + + @staticmethod + def qdt2dt(qdt: QDateTime) -> dt: + return cast(dt, qdt.toPython()) + + @staticmethod + def dt2qdt(datetime: dt) -> QDateTime: + return QDateTime.fromSecsSinceEpoch(int(datetime.timestamp())) + + @staticmethod + def string2dt(datetime_str: str) -> dt: + return dt.strptime(datetime_str, DATETIME_FORMAT) + + @staticmethod + def dt2string(datetime: dt) -> str: + return dt.strftime(datetime, DATETIME_FORMAT) diff --git a/src/tagstudio/qt/widgets/preview/field_containers.py b/src/tagstudio/qt/widgets/preview/field_containers.py index f93b5432..2d8394c5 100644 --- a/src/tagstudio/qt/widgets/preview/field_containers.py +++ b/src/tagstudio/qt/widgets/preview/field_containers.py @@ -33,6 +33,7 @@ from tagstudio.core.library.alchemy.fields import ( from tagstudio.core.library.alchemy.library import Library from tagstudio.core.library.alchemy.models import Entry, Tag from tagstudio.qt.translations import Translations +from tagstudio.qt.widgets.datetime_picker import DatetimePicker from tagstudio.qt.widgets.fields import FieldContainer from tagstudio.qt.widgets.panel import PanelModal from tagstudio.qt.widgets.tag_box import TagBoxWidget @@ -390,21 +391,23 @@ class FieldContainers(QWidget): if not is_mixed: container.set_title(field.type.name) container.set_inline(False) + + title = f"{field.type.name} (Date)" try: - title = f"{field.type.name} (Date)" + assert field.value is not None text = self.driver.settings.format_datetime( - dt.strptime(field.value or "", "%Y-%m-%d %H:%M:%S") + DatetimePicker.string2dt(field.value) ) - except ValueError: - title = f"{field.type.name} (Date) (Unknown Format)" + except (ValueError, AssertionError): + title += " (Unknown Format)" text = str(field.value) inner_widget = TextWidget(title, text) container.set_inner_widget(inner_widget) - modal = PanelModal( # TODO Replace with proper date picker including timezone etc. - EditTextLine(field.value), - title=f"Edit {field.type.name} in 'YYYY-MM-DD HH:MM:SS' format", + modal = PanelModal( + DatetimePicker(self.driver, field.value or dt.now()), + title=f"Edit {field.type.name}", window_title=f"Edit {field.type.name}", save_callback=( lambda content: ( diff --git a/src/tagstudio/qt/widgets/text_line_edit.py b/src/tagstudio/qt/widgets/text_line_edit.py index 1c719fab..a737d9c6 100644 --- a/src/tagstudio/qt/widgets/text_line_edit.py +++ b/src/tagstudio/qt/widgets/text_line_edit.py @@ -1,8 +1,6 @@ -# Copyright (C) 2025 Travis Abendshien (CyanVoxel). # Licensed under the GPL-3.0 License. # Created for TagStudio: https://github.com/CyanVoxel/TagStudio - from collections.abc import Callable from PySide6.QtWidgets import QLineEdit, QVBoxLayout