diff --git a/src/tagstudio/core/library/alchemy/library.py b/src/tagstudio/core/library/alchemy/library.py index 3bdd615c..8c28134d 100644 --- a/src/tagstudio/core/library/alchemy/library.py +++ b/src/tagstudio/core/library/alchemy/library.py @@ -1540,7 +1540,12 @@ class Library: session.commit() def update_text_field( - self, entry_ids: list[int] | int, field: TextField, value: str, is_multiline: bool + self, + entry_ids: list[int] | int, + field: TextField, + name: str, + value: str, + is_multiline: bool, ): """Update a TextField field on one or more Entries.""" if isinstance(entry_ids, int): @@ -1552,7 +1557,7 @@ class Library: update_stmt = ( update(field_type) .where(and_(field_type.id == field.id, field_type.entry_id.in_(entry_ids))) - .values(value=value, is_multiline=is_multiline) + .values(name=name, value=value, is_multiline=is_multiline) ) session.execute(update_stmt) @@ -1562,6 +1567,7 @@ class Library: self, entry_ids: list[int] | int, field: DatetimeField, + name: str, value: datetime, ): """Update a DatetimeField field on one or more Entries.""" @@ -1574,7 +1580,7 @@ class Library: update_stmt = ( update(field_type) .where(and_(field_type.id == field.id, field_type.entry_id.in_(entry_ids))) - .values(value=value) + .values(name=name, value=value) ) session.execute(update_stmt) diff --git a/src/tagstudio/qt/controllers/edit_text_controller.py b/src/tagstudio/qt/controllers/edit_text_controller.py new file mode 100644 index 00000000..3e98003e --- /dev/null +++ b/src/tagstudio/qt/controllers/edit_text_controller.py @@ -0,0 +1,67 @@ +# SPDX-FileCopyrightText: (c) TagStudio Contributors +# SPDX-License-Identifier: GPL-3.0-only + + +from typing import override + +import structlog + +from tagstudio.qt.views.edit_text_view import EditTextView + +logger = structlog.get_logger(__name__) + + +class EditText(EditTextView): + def __init__(self, name: str, text: str | None, is_multiline: bool = False): + super().__init__() + self.name_field.setText(name) + + self.text = text + self.is_multiline: bool = is_multiline + + self.multiline_checkbox.setChecked(is_multiline) + self.multiline_checkbox.clicked.connect(lambda checked: self.on_multiline_checked(checked)) + + if self.is_multiline: + self.text_line.hide() + self.text_line_stretch.hide() + self.text_box.setPlainText(self.text or "") + else: + self.text_box.hide() + self.text_line.setText(self.text or "") + + def on_multiline_checked(self, checked: bool): + was_multiline = self.is_multiline + self.is_multiline = checked + + if was_multiline: + self.text = self.text_box.toPlainText() + self.text_box.hide() + self.text_line.setText(self.text) + self.text_line.show() + self.text_line_stretch.show() + else: + self.text = self.text_line.text() + self.text_line.hide() + self.text_line_stretch.hide() + self.text_box.setPlainText(self.text) + self.text_box.show() + + @override + def parent_post_init(self): + if self.is_multiline: + self.text_box.setFocus() + else: + self.text_line.setFocus() + + @override + def saved_data(self) -> dict[str, str | bool]: + return { + "name": self.name_field.text(), + "value": self.text_box.toPlainText() if self.is_multiline else self.text_line.text(), + "is_multiline": self.is_multiline, + } + + @override + def reset(self): + self.text_box.setPlainText(self.text or "") diff --git a/src/tagstudio/qt/controllers/field_template_search_panel_controller.py b/src/tagstudio/qt/controllers/field_template_search_panel_controller.py index 9b361118..695487ff 100644 --- a/src/tagstudio/qt/controllers/field_template_search_panel_controller.py +++ b/src/tagstudio/qt/controllers/field_template_search_panel_controller.py @@ -2,7 +2,6 @@ # SPDX-License-Identifier: GPL-3.0-only -from collections.abc import Callable from typing import override from warnings import catch_warnings @@ -27,8 +26,6 @@ class FieldTemplateSearchModal(PanelModal): self, library: Library, is_field_template_chooser: bool = True, - done_callback: Callable[..., None] | None = None, - save_callback: Callable[..., None] | None = None, has_save: bool = False, ) -> None: self.search_panel: FieldTemplateSearchPanel = FieldTemplateSearchPanel( @@ -39,9 +36,7 @@ class FieldTemplateSearchModal(PanelModal): super().__init__( self.search_panel, Translations["field.add.plural"], - done_callback=done_callback, - save_callback=save_callback, - has_save=has_save, + is_savable=has_save, ) @@ -87,7 +82,7 @@ class FieldTemplateSearchPanel(SearchPanel[BaseFieldTemplate]): Translations["field_template.add"] if add_to_entry else Translations["field_template.new"], - has_save=True, + is_savable=True, ) if query.strip(): @@ -104,7 +99,7 @@ class FieldTemplateSearchPanel(SearchPanel[BaseFieldTemplate]): panel, item.name, Translations["field_template.edit"], - has_save=True, + is_savable=True, ) modal.saved.connect(lambda: self.edit_item(panel)) diff --git a/src/tagstudio/qt/controllers/tag_box_controller.py b/src/tagstudio/qt/controllers/tag_box_controller.py index 79351230..58e7919a 100644 --- a/src/tagstudio/qt/controllers/tag_box_controller.py +++ b/src/tagstudio/qt/controllers/tag_box_controller.py @@ -2,6 +2,7 @@ # SPDX-License-Identifier: GPL-3.0-only +from functools import partial from typing import TYPE_CHECKING, override import structlog @@ -77,20 +78,20 @@ class TagBoxWidget(TagBoxWidgetView): build_tag_panel, self.__driver.lib.tag_display_name(tag), "Edit Tag", - done_callback=self.on_update.emit, - has_save=True, - ) - # TODO - this was update_tag() - edit_modal.saved.connect( - lambda: self.__driver.lib.update_tag( - build_tag_panel.build_tag(), - parent_ids=set(build_tag_panel.parent_ids), - alias_names=set(build_tag_panel.alias_names), - alias_ids=set(build_tag_panel.alias_ids), - ) + is_savable=True, ) + edit_modal.saved.connect(partial(self._update_tag_callback, build_tag_panel)) edit_modal.show() + def _update_tag_callback(self, build_tag_panel: BuildTagPanel): + self.__driver.lib.update_tag( + build_tag_panel.build_tag(), + parent_ids=set(build_tag_panel.parent_ids), + alias_names=set(build_tag_panel.alias_names), + alias_ids=set(build_tag_panel.alias_ids), + ) + self.on_update.emit() + @override def _on_search(self, tag: Tag) -> None: self.__driver.main_window.search_field.setText(f"tag_id:{tag.id}") diff --git a/src/tagstudio/qt/controllers/tag_search_panel_controller.py b/src/tagstudio/qt/controllers/tag_search_panel_controller.py index 2d4aa8c0..ecd53496 100644 --- a/src/tagstudio/qt/controllers/tag_search_panel_controller.py +++ b/src/tagstudio/qt/controllers/tag_search_panel_controller.py @@ -2,7 +2,6 @@ # SPDX-License-Identifier: GPL-3.0-only -from collections.abc import Callable from typing import TYPE_CHECKING, override from warnings import catch_warnings @@ -34,8 +33,6 @@ class TagSearchModal(PanelModal): library: Library, exclude: list[int] | None = None, is_tag_chooser: bool = True, - done_callback: Callable[..., None] | None = None, - save_callback: Callable[..., None] | None = None, has_save: bool = False, ): self.tsp = TagSearchPanel( @@ -47,9 +44,7 @@ class TagSearchModal(PanelModal): super().__init__( self.tsp, Translations["tag.add.plural"], - done_callback=done_callback, - save_callback=save_callback, - has_save=has_save, + is_savable=has_save, ) @@ -94,7 +89,7 @@ class TagSearchPanel(SearchPanel[Tag]): panel, Translations["tag.new"], Translations["tag.add"] if add_to_entry else Translations["tag.new"], - has_save=True, + is_savable=True, ) if query.strip(): @@ -113,7 +108,7 @@ class TagSearchPanel(SearchPanel[Tag]): edit_tag_panel, self.__lib.tag_display_name(item), Translations["tag.edit"], - has_save=True, + is_savable=True, ) edit_tag_modal.saved.connect(lambda: self.edit_item(edit_tag_panel)) diff --git a/src/tagstudio/qt/mixed/build_color.py b/src/tagstudio/qt/mixed/build_color.py index 07b43d88..1c16f036 100644 --- a/src/tagstudio/qt/mixed/build_color.py +++ b/src/tagstudio/qt/mixed/build_color.py @@ -32,6 +32,7 @@ from tagstudio.qt.mixed.tag_widget import ( from tagstudio.qt.models.palette import ColorType, UiColor, get_tag_color, get_ui_color from tagstudio.qt.translations import Translations from tagstudio.qt.views.panel_modal import PanelWidget +from tagstudio.qt.views.stylesheets.stylesheets import checkbox_style logger = structlog.get_logger(__name__) @@ -135,37 +136,7 @@ class BuildColorPanel(PanelWidget): self.border_layout.addWidget(self.border_checkbox) self.border_label = QLabel(Translations["color.color_border"]) self.border_layout.addWidget(self.border_label) - - primary_color = QColor(get_tag_color(ColorType.PRIMARY, TagColorEnum.DEFAULT)) - border_color = get_border_color(primary_color) - highlight_color = get_highlight_color(primary_color) - text_color: QColor = get_text_color(primary_color, highlight_color) - self.border_checkbox.setStyleSheet( - f"QCheckBox{{" - f"background: rgba{primary_color.toTuple()};" - f"color: rgba{text_color.toTuple()};" - f"border-color: rgba{border_color.toTuple()};" - f"border-radius: 6px;" - f"border-style:solid;" - f"border-width: 2px;" - f"}}" - f"QCheckBox::indicator{{" - f"width: 10px;" - f"height: 10px;" - f"border-radius: 2px;" - f"margin: 4px;" - f"}}" - f"QCheckBox::indicator:checked{{" - f"background: rgba{text_color.toTuple()};" - f"}}" - f"QCheckBox::hover{{" - f"border-color: rgba{highlight_color.toTuple()};" - f"}}" - f"QCheckBox::focus{{" - f"border-color: rgba{highlight_color.toTuple()};" - f"outline:none;" - f"}}" - ) + self.border_checkbox.setStyleSheet(checkbox_style()) # Add Widgets to Layout ================================================ self.root_layout.addWidget(self.preview_widget) diff --git a/src/tagstudio/qt/mixed/build_tag.py b/src/tagstudio/qt/mixed/build_tag.py index 0f8a860a..eb17024c 100644 --- a/src/tagstudio/qt/mixed/build_tag.py +++ b/src/tagstudio/qt/mixed/build_tag.py @@ -25,7 +25,6 @@ from PySide6.QtWidgets import ( QWidget, ) -from tagstudio.core.library.alchemy.enums import TagColorEnum from tagstudio.core.library.alchemy.library import Library from tagstudio.core.library.alchemy.models import Tag, TagColorGroup from tagstudio.core.utils.types import unwrap @@ -39,9 +38,10 @@ from tagstudio.qt.mixed.tag_widget import ( get_primary_color, get_text_color, ) -from tagstudio.qt.models.palette import ColorType, UiColor, get_tag_color, get_ui_color +from tagstudio.qt.models.palette import ColorType, UiColor, get_ui_color from tagstudio.qt.translations import Translations from tagstudio.qt.views.panel_modal import PanelModal, PanelWidget +from tagstudio.qt.views.stylesheets.stylesheets import checkbox_style from tagstudio.qt.views.tag_search_panel_view import TagSearchPanelView logger = structlog.get_logger(__name__) @@ -201,9 +201,9 @@ class BuildTagPanel(PanelWidget): self.tag_color_selection, chose_tag_color_title, chose_tag_color_title, - done_callback=lambda: self.choose_color_callback( - self.tag_color_selection.selected_color - ), + ) + self.choose_color_modal.done.connect( + lambda: self.choose_color_callback(self.tag_color_selection.selected_color) ) self.color_button.button.clicked.connect(self.choose_color_modal.show) self.color_layout.addWidget(self.color_button) @@ -218,38 +218,7 @@ class BuildTagPanel(PanelWidget): self.cat_title = QLabel(Translations["tag.is_category"]) self.cat_checkbox = QCheckBox() self.cat_checkbox.setFixedSize(22, 22) - - primary_color = QColor(get_tag_color(ColorType.PRIMARY, TagColorEnum.DEFAULT)) - border_color = get_border_color(primary_color) - highlight_color = get_highlight_color(primary_color) - text_color: QColor = get_text_color(primary_color, highlight_color) - - self.cat_checkbox.setStyleSheet( - f"QCheckBox{{" - f"background: rgba{primary_color.toTuple()};" - f"color: rgba{text_color.toTuple()};" - f"border-color: rgba{border_color.toTuple()};" - f"border-radius: 6px;" - f"border-style:solid;" - f"border-width: 2px;" - f"}}" - f"QCheckBox::indicator{{" - f"width: 10px;" - f"height: 10px;" - f"border-radius: 2px;" - f"margin: 4px;" - f"}}" - f"QCheckBox::indicator:checked{{" - f"background: rgba{text_color.toTuple()};" - f"}}" - f"QCheckBox::hover{{" - f"border-color: rgba{highlight_color.toTuple()};" - f"}}" - f"QCheckBox::focus{{" - f"border-color: rgba{highlight_color.toTuple()};" - f"outline:none;" - f"}}" - ) + self.cat_checkbox.setStyleSheet(checkbox_style()) self.cat_layout.addWidget(self.cat_checkbox) self.cat_layout.addWidget(self.cat_title) @@ -263,33 +232,7 @@ class BuildTagPanel(PanelWidget): self.hidden_title = QLabel(Translations["tag.is_hidden"]) self.hidden_checkbox = QCheckBox() self.hidden_checkbox.setFixedSize(22, 22) - - self.hidden_checkbox.setStyleSheet( - f"QCheckBox{{" - f"background: rgba{primary_color.toTuple()};" - f"color: rgba{text_color.toTuple()};" - f"border-color: rgba{border_color.toTuple()};" - f"border-radius: 6px;" - f"border-style:solid;" - f"border-width: 2px;" - f"}}" - f"QCheckBox::indicator{{" - f"width: 10px;" - f"height: 10px;" - f"border-radius: 2px;" - f"margin: 4px;" - f"}}" - f"QCheckBox::indicator:checked{{" - f"background: rgba{text_color.toTuple()};" - f"}}" - f"QCheckBox::hover{{" - f"border-color: rgba{highlight_color.toTuple()};" - f"}}" - f"QCheckBox::focus{{" - f"border-color: rgba{highlight_color.toTuple()};" - f"outline:none;" - f"}}" - ) + self.hidden_checkbox.setStyleSheet(checkbox_style()) self.hidden_layout.addWidget(self.hidden_checkbox) self.hidden_layout.addWidget(self.hidden_title) diff --git a/src/tagstudio/qt/mixed/color_box.py b/src/tagstudio/qt/mixed/color_box.py index 3a031f2a..d4d7703e 100644 --- a/src/tagstudio/qt/mixed/color_box.py +++ b/src/tagstudio/qt/mixed/color_box.py @@ -134,7 +134,7 @@ class ColorBoxWidget(FieldWidget): self.edit_modal = PanelModal( build_color_panel, "Edit Color", - has_save=True, + is_savable=True, ) self.edit_modal.saved.connect( diff --git a/src/tagstudio/qt/mixed/datetime_picker.py b/src/tagstudio/qt/mixed/datetime_picker.py index 318c4768..d28bfb1b 100644 --- a/src/tagstudio/qt/mixed/datetime_picker.py +++ b/src/tagstudio/qt/mixed/datetime_picker.py @@ -3,12 +3,11 @@ import typing -from collections.abc import Callable from datetime import datetime as dt -from typing import cast +from typing import cast, override from PySide6.QtCore import QDateTime -from PySide6.QtWidgets import QDateTimeEdit, QVBoxLayout +from PySide6.QtWidgets import QDateTimeEdit, QLineEdit, QVBoxLayout from tagstudio.qt.views.panel_modal import PanelWidget @@ -40,11 +39,16 @@ def qdtf2dtf(dtf: str) -> str: class DatetimePicker(PanelWidget): - def __init__(self, driver: "QtDriver", datetime: dt | str): + def __init__(self, driver: "QtDriver", name: str, datetime: dt | str): super().__init__() + self.setMinimumSize(300, 60) self.root_layout = QVBoxLayout(self) self.root_layout.setContentsMargins(6, 0, 6, 0) + self.name_field = QLineEdit() + self.name_field.setStyleSheet("font-weight:bold;font-size:14px;padding-top:6px") + self.name_field.setText(name) + if isinstance(datetime, str): datetime = DatetimePicker.string2dt(datetime) self.datetime_edit = QDateTimeEdit() @@ -55,20 +59,24 @@ class DatetimePicker(PanelWidget): self.datetime_edit.setDisplayFormat(qdtf2dtf(driver.settings.datetime_format)) self.initial_value = datetime + self.root_layout.addWidget(self.name_field) self.root_layout.addWidget(self.datetime_edit) - def get_content(self): - return DatetimePicker.dt2string(DatetimePicker.qdt2dt(self.datetime_edit.dateTime())) + @override + def saved_data(self) -> dict[str, str]: + return { + "name": self.name_field.text(), + "value": DatetimePicker.dt2string(DatetimePicker.qdt2dt(self.datetime_edit.dateTime())), + } + @override + def parent_post_init(self): + self.datetime_edit.setFocus() + + @override 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()) diff --git a/src/tagstudio/qt/mixed/field_containers.py b/src/tagstudio/qt/mixed/field_containers.py index 978cf38e..5a60a64e 100644 --- a/src/tagstudio/qt/mixed/field_containers.py +++ b/src/tagstudio/qt/mixed/field_containers.py @@ -2,10 +2,10 @@ # SPDX-License-Identifier: GPL-3.0-only -import sys import typing from collections.abc import Callable from datetime import datetime as dt +from functools import partial from warnings import catch_warnings import structlog @@ -31,13 +31,12 @@ 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.core.utils.types import unwrap +from tagstudio.qt.controllers.edit_text_controller import EditText from tagstudio.qt.controllers.tag_box_controller import TagBoxWidget from tagstudio.qt.mixed.datetime_picker import DatetimePicker from tagstudio.qt.mixed.field_widget import FieldContainer -from tagstudio.qt.mixed.text_field import TextWidget +from tagstudio.qt.mixed.text_field import TextContainerWidget from tagstudio.qt.translations import FIELD_TYPE_KEYS, Translations -from tagstudio.qt.views.edit_text_box_modal import EditTextBox -from tagstudio.qt.views.edit_text_line_modal import EditTextLine from tagstudio.qt.views.panel_modal import PanelModal if typing.TYPE_CHECKING: @@ -47,7 +46,7 @@ logger = structlog.get_logger(__name__) class FieldContainers(QWidget): - """The Preview Panel Widget.""" + """Widget for the tag and field containers displayed inside the Preview Panel.""" def __init__(self, library: Library, driver: "QtDriver") -> None: super().__init__() @@ -102,6 +101,11 @@ class FieldContainers(QWidget): root_layout.setContentsMargins(0, 0, 0, 0) root_layout.addWidget(self.scroll_area) + @property + def top_entry_id(self) -> int: + """Get the topmost entry ID in the (cached) selected entries.""" + return self.cached_entries[0].id + def update_from_entry(self, entry_id: int, update_badges: bool = True) -> None: """Update tags and fields from a single Entry source.""" logger.warning("[FieldContainers] Updating Selection", entry_id=entry_id) @@ -130,7 +134,7 @@ class FieldContainers(QWidget): # Write field container(s) for index, field in enumerate(entry_fields, start=container_index): - self.write_container(index, field, is_mixed=False) + self.write_field_container(index, field, is_mixed=False) # Hide leftover container(s) if len(self.containers) > container_len: @@ -247,22 +251,128 @@ class FieldContainers(QWidget): ) self.driver.add_tags_to_selected_callback(tag_ids) - def write_container(self, index: int, field: BaseField, is_mixed: bool = False) -> None: - """Update/Create data for a FieldContainer. + def write_field_container(self, index: int, field: BaseField, is_mixed: bool = False) -> None: + """Update/Create data for a field FieldContainer. Args: index(int): The container index. - field(BaseField): The type of field to write to. + field(BaseField): The field to write in this container. is_mixed(bool): Relevant when multiple items are selected. - - If True, field is not present in all selected items. + If True, field is not present in all selected items. """ + + def update_text_field_callback( + field: TextField, entry_id: int, content: dict[str, str | bool] + ) -> None: + """Callback called when a text field has updated data.""" + self._update_text_field( + field, str(content["name"]), str(content["value"]), bool(content["is_multiline"]) + ) + self.update_from_entry(entry_id) + + def update_datetime_field_callback( + field: DatetimeField, entry_id: int, content: dict[str, str] + ) -> None: + """Callback called when a datetime field has updated data.""" + self.update_datetime_field(field, str(content["name"]), str(content["value"])) + self.update_from_entry(entry_id) + + def remove_field_callback(field: BaseField, entry_id: int) -> None: + """Callback called when a field needs to be removed from an entry.""" + self._remove_field(field) + self.update_from_entry(entry_id) + + def write_text_container( + container: FieldContainer, field: TextField, title: str, is_mixed: bool + ): + container.set_title(field.name) + + # Normalize line endings in any text content. + if not is_mixed: + assert isinstance(field.value, str | type(None)) + text = (field.value or "").replace("\r", "\n") + else: + text = f"{Translations['field.mixed_data']}" + + inner_widget = TextContainerWidget(title, text) + container.set_inner_widget(inner_widget) + + if not is_mixed: + edit_modal = PanelModal( + EditText(field.name, field.value, field.is_multiline), + window_title=f"{Translations['field.edit']} ({Translations[field_name_key]})", + is_savable=True, + inline_title=False, + ) + edit_modal.saved_data.connect( + partial(update_text_field_callback, field, self.top_entry_id) + ) + + container.set_edit_callback(edit_modal.show) + container.set_remove_callback( + lambda: self.remove_message_box( + prompt=self.remove_field_prompt(title), + callback=partial(remove_field_callback, field, self.top_entry_id), + ) + ) + + def write_datetime_container( + container: FieldContainer, field: DatetimeField, title: str, is_mixed: bool + ): + container.set_title(field.name) + + if not is_mixed: + try: + assert field.value is not None + text = self.driver.settings.format_datetime( + DatetimePicker.string2dt(field.value) + ) + except (ValueError, AssertionError): + text = str(field.value) + else: + text = f"{Translations['field.mixed_data']}" + + inner_widget = TextContainerWidget(title, text) + container.set_inner_widget(inner_widget) + + if not is_mixed: + edit_modal = PanelModal( + DatetimePicker(self.driver, field.name, field.value or dt.now()), + window_title=f"{Translations['field.edit']} ({Translations[field_name_key]})", + is_savable=True, + inline_title=False, + ) + edit_modal.saved_data.connect( + partial(update_datetime_field_callback, field, self.top_entry_id) + ) + + container.set_edit_callback(edit_modal.show) + container.set_remove_callback( + lambda: self.remove_message_box( + prompt=self.remove_field_prompt(field.name), + callback=partial(remove_field_callback, field, self.top_entry_id), + ) + ) + + def write_unknown_container(): + container.set_title(field.name) + inner_widget = TextContainerWidget(title, field.name) + container.set_inner_widget(inner_widget) + container.set_remove_callback( + lambda: self.remove_message_box( + prompt=self.remove_field_prompt(field.name), + callback=partial(remove_field_callback, field, self.top_entry_id), + ) + ) + logger.info( "[FieldContainers][write_container]", index=index, name=field.name, type=field.class_name, ) + + # Create new containers if necessary if len(self.containers) < (index + 1): container = FieldContainer() self.containers.append(container) @@ -274,156 +384,27 @@ class FieldContainers(QWidget): field_name_key: str = FIELD_TYPE_KEYS.get(field.class_name, "field_type.unknown") title = f"{field.name} ({Translations[field_name_key]})" - # Single-line Text - if type(field) is TextField and not field.is_multiline: - container.set_title(field.name) - container.set_inline(False) - - # Normalize line endings in any text content. - if not is_mixed: - assert isinstance(field.value, str | type(None)) - text = field.value or "" - else: - text = "Mixed Data" # TODO: Localize this - - inner_widget = TextWidget(title, text) - container.set_inner_widget(inner_widget) - if not is_mixed: - modal = PanelModal( - EditTextLine(field.value), - title=title, - window_title=f"Edit {field.name}", # TODO: Localize this - save_callback=( # pyright: ignore[reportArgumentType] - lambda content: ( - self.update_text_field(field, content, is_multiline=False), - self.update_from_entry(self.cached_entries[0].id), - ) - ), - ) - if "pytest" in sys.modules: - # for better testability - container.modal = modal # pyright: ignore[reportAttributeAccessIssue] - - container.set_edit_callback(modal.show) - container.set_remove_callback( - lambda: self.remove_message_box( - prompt=self.remove_field_prompt(title), - callback=lambda: ( - self.remove_field(field), - self.update_from_entry(self.cached_entries[0].id), - ), - ) - ) - - # Multiline Text - elif type(field) is TextField and field.is_multiline: - container.set_title(field.name) - container.set_inline(False) - # Normalize line endings in any text content. - if not is_mixed: - assert isinstance(field.value, str | type(None)) - text = (field.value or "").replace("\r", "\n") - else: - text = "Mixed Data" # TODO: Localize this - inner_widget = TextWidget(title, text) - container.set_inner_widget(inner_widget) - if not is_mixed: - modal = PanelModal( - EditTextBox(field.value), - title=title, - window_title=f"Edit {field.name}", # TODO: Localize this - save_callback=( # pyright: ignore[reportArgumentType] - lambda content: ( - self.update_text_field(field, content, is_multiline=True), - self.update_from_entry(self.cached_entries[0].id), - ) - ), - ) - container.set_edit_callback(modal.show) - container.set_remove_callback( - lambda: self.remove_message_box( - prompt=self.remove_field_prompt(field.name), - callback=lambda: ( - self.remove_field(field), - self.update_from_entry(self.cached_entries[0].id), - ), - ) - ) - + # Write containers + if type(field) is TextField: + write_text_container(container, field, title, is_mixed) elif type(field) is DatetimeField: - logger.info("[FieldContainers][write_container] Datetime Field", field=field) - if not is_mixed: - container.set_title(field.name) - container.set_inline(False) - - try: - assert field.value is not None - text = self.driver.settings.format_datetime( - DatetimePicker.string2dt(field.value) - ) - except (ValueError, AssertionError): - text = str(field.value) - - inner_widget = TextWidget(title, text) - container.set_inner_widget(inner_widget) - - modal = PanelModal( - DatetimePicker(self.driver, field.value or dt.now()), - title=f"Edit {field.name}", - save_callback=( # pyright: ignore[reportArgumentType] - lambda content: ( - self.update_datetime_field(field, content), - self.update_from_entry(self.cached_entries[0].id), - ) - ), - ) - - container.set_edit_callback(modal.show) - container.set_remove_callback( - lambda: self.remove_message_box( - prompt=self.remove_field_prompt(field.name), - callback=lambda: ( - self.remove_field(field), - self.update_from_entry(self.cached_entries[0].id), - ), - ) - ) - else: - text = "Mixed Data" # TODO: Localize this - inner_widget = TextWidget(title, text) - container.set_inner_widget(inner_widget) + write_datetime_container(container, field, title, is_mixed) else: - logger.warning( - "[FieldContainers][write_container] Unknown Field", field=field - ) # TODO: Localize this - container.set_title(field.name) - container.set_inline(False) - inner_widget = TextWidget(title, field.name) - container.set_inner_widget(inner_widget) - container.set_remove_callback( - lambda: self.remove_message_box( - prompt=self.remove_field_prompt(field.name), - callback=lambda: ( - self.remove_field(field), - self.update_from_entry(self.cached_entries[0].id), - ), - ) - ) + write_unknown_container() container.setHidden(False) def write_tag_container( self, index: int, tags: set[Tag], category_tag: Tag | None = None, is_mixed: bool = False ) -> None: - """Update/Create tag data for a FieldContainer. + """Update/Create tag data for a tag FieldContainer. Args: index(int): The container index. tags(set[Tag]): The list of tags for this container. category_tag(Tag|None): The category tag this container represents. is_mixed(bool): Relevant when multiple items are selected. - - If True, field is not present in all selected items. + If True, field is not present in all selected items. """ logger.info("[FieldContainers][write_tag_container]", index=index) if len(self.containers) < (index + 1): @@ -433,10 +414,7 @@ class FieldContainers(QWidget): else: container = self.containers[index] - container.set_title( - "Tags" if not category_tag else category_tag.name - ) # TODO: Localize this - container.set_inline(False) + container.set_title(Translations["entries.tags"] if not category_tag else category_tag.name) if not is_mixed: inner_widget = container.get_inner_widget() @@ -446,10 +424,7 @@ class FieldContainers(QWidget): inner_widget.on_update.disconnect() else: - inner_widget = TagBoxWidget( - "Tags", # TODO: Localize this - self.driver, - ) + inner_widget = TagBoxWidget(Translations["entries.tags"], self.driver) container.set_inner_widget(inner_widget) inner_widget.set_entries([e.id for e in self.cached_entries]) inner_widget.set_tags(tags) @@ -458,15 +433,15 @@ class FieldContainers(QWidget): lambda: self.update_from_entry(self.cached_entries[0].id, update_badges=True) ) else: - text = "Mixed Data" - inner_widget = TextWidget("Mixed Tags", text) + text = f"{Translations['field.mixed_data']}" + inner_widget = TextContainerWidget("Mixed Tags", text) container.set_inner_widget(inner_widget) container.set_edit_callback() container.set_remove_callback() container.setHidden(False) - def remove_field(self, field: BaseField) -> None: + def _remove_field(self, field: BaseField) -> None: """Remove a field from all selected Entries.""" logger.info( "[FieldContainers] Removing Field", @@ -476,24 +451,26 @@ class FieldContainers(QWidget): entry_ids = [e.id for e in self.cached_entries] self.lib.remove_entry_field(field, entry_ids) - def update_text_field(self, field: TextField, value: str, is_multiline: bool) -> None: + def _update_text_field( + self, field: TextField, name: str, value: str, is_multiline: bool + ) -> None: """Update a text field across selected entries.""" entry_ids = [e.id for e in self.cached_entries] assert entry_ids, "No entries selected" - self.lib.update_text_field(entry_ids, field, value, is_multiline) + self.lib.update_text_field(entry_ids, field, name, value, is_multiline) - def update_datetime_field(self, field: DatetimeField, value: str) -> None: + def update_datetime_field(self, field: DatetimeField, name: str, value: str) -> None: """Update a datetime field across selected entries.""" entry_ids = [e.id for e in self.cached_entries] assert entry_ids, "No entries selected" - self.lib.update_datetime_field(entry_ids, field, dt.fromisoformat(value)) + self.lib.update_datetime_field(entry_ids, field, name, dt.fromisoformat(value)) - def remove_message_box(self, prompt: str, callback: Callable) -> None: + def remove_message_box(self, prompt: str, callback: Callable[..., None]) -> None: remove_mb = QMessageBox() remove_mb.setText(prompt) - remove_mb.setWindowTitle("Remove Field") # TODO: Localize + remove_mb.setWindowTitle(Translations["Remove Field"]) remove_mb.setIcon(QMessageBox.Icon.Warning) cancel_button = remove_mb.addButton( Translations["generic.cancel_alt"], QMessageBox.ButtonRole.DestructiveRole diff --git a/src/tagstudio/qt/mixed/field_widget.py b/src/tagstudio/qt/mixed/field_widget.py index 04ee3c37..845ab504 100644 --- a/src/tagstudio/qt/mixed/field_widget.py +++ b/src/tagstudio/qt/mixed/field_widget.py @@ -53,7 +53,6 @@ class FieldContainer(QWidget): super().__init__() self.setObjectName("fieldContainer") self.title: str = title - self.inline: bool = inline self.copy_callback: Callable[[], None] | None = None self.edit_callback: Callable[[], None] | None = None self.remove_callback: Callable[[], None] | None = None @@ -170,9 +169,6 @@ class FieldContainer(QWidget): self.title = self.title = f"