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"

{title}

" self.title_widget.setText(self.title) - def set_inline(self, inline: bool) -> None: - self.inline = inline - @override def enterEvent(self, event: QEnterEvent) -> None: # NOTE: You could pass the hover event to the FieldWidget if needed. diff --git a/src/tagstudio/qt/mixed/settings_panel.py b/src/tagstudio/qt/mixed/settings_panel.py index 5e8c022d..55d5f0e0 100644 --- a/src/tagstudio/qt/mixed/settings_panel.py +++ b/src/tagstudio/qt/mixed/settings_panel.py @@ -356,9 +356,9 @@ class SettingsPanel(PanelWidget): modal = PanelModal( widget=settings_panel, window_title=Translations["settings.title"], - done_callback=lambda: settings_panel.update_settings(driver), - has_save=True, + is_savable=True, ) + modal.done.connect(lambda: settings_panel.update_settings(driver)) modal.title_widget.setVisible(False) return modal diff --git a/src/tagstudio/qt/mixed/tag_color_manager.py b/src/tagstudio/qt/mixed/tag_color_manager.py index 92c3b42b..ad1b72fe 100644 --- a/src/tagstudio/qt/mixed/tag_color_manager.py +++ b/src/tagstudio/qt/mixed/tag_color_manager.py @@ -176,7 +176,7 @@ class TagColorManager(QWidget): self.create_namespace_modal = PanelModal( build_namespace_panel, Translations["namespace.create.title"], - has_save=True, + is_savable=True, ) self.create_namespace_modal.saved.connect( diff --git a/src/tagstudio/qt/mixed/text_field.py b/src/tagstudio/qt/mixed/text_field.py index 3d958e3c..879a0121 100644 --- a/src/tagstudio/qt/mixed/text_field.py +++ b/src/tagstudio/qt/mixed/text_field.py @@ -10,8 +10,8 @@ from PySide6.QtWidgets import QHBoxLayout, QLabel from tagstudio.qt.mixed.field_widget import FieldWidget -class TextWidget(FieldWidget): - def __init__(self, title, text: str) -> None: +class TextContainerWidget(FieldWidget): + def __init__(self, title: str, text: str) -> None: super().__init__(title) self.setObjectName("textBox") self.base_layout = QHBoxLayout() diff --git a/src/tagstudio/qt/ts_qt.py b/src/tagstudio/qt/ts_qt.py index ed6dcf58..cce250da 100644 --- a/src/tagstudio/qt/ts_qt.py +++ b/src/tagstudio/qt/ts_qt.py @@ -24,7 +24,7 @@ from typing import TypeVar from warnings import catch_warnings import structlog -from humanfriendly import format_size, format_timespan +from humanfriendly import format_size, format_timespan # pyright: ignore[reportUnknownVariableType] from PySide6.QtCore import QObject, QSettings, Qt, QThread, QThreadPool, QTimer, Signal from PySide6.QtGui import ( QColor, @@ -45,7 +45,8 @@ from PySide6.QtWidgets import ( QScrollArea, ) -import tagstudio.qt.resources_rc # noqa: F401 +# This import has side-effect of importing PySide resources +import tagstudio.qt.resources_rc # noqa: F401 # pyright: ignore[reportUnusedImport] from tagstudio.core.constants import TAG_ARCHIVED, TAG_FAVORITE, VERSION, VERSION_BRANCH from tagstudio.core.driver import DriverMixin from tagstudio.core.enums import MacroID, SettingItems, ShowFilepathOption @@ -64,11 +65,7 @@ from tagstudio.core.utils.str_formatting import is_version_outdated from tagstudio.core.utils.types import unwrap from tagstudio.qt.cache_manager import CacheManager from tagstudio.qt.controllers.ffmpeg_missing_message_box import FfmpegMissingMessageBox -from tagstudio.qt.controllers.field_template_search_panel_controller import ( - FieldTemplateSearchPanel, -) - -# this import has side-effect of import PySide resources +from tagstudio.qt.controllers.field_template_search_panel_controller import FieldTemplateSearchPanel from tagstudio.qt.controllers.fix_ignored_modal_controller import FixIgnoredEntriesModal from tagstudio.qt.controllers.ignore_modal_controller import IgnoreModal from tagstudio.qt.controllers.library_info_window_controller import LibraryInfoWindow @@ -376,10 +373,12 @@ class QtDriver(DriverMixin, QObject): view=TagSearchPanelView(is_tag_chooser=False), ), title=Translations["tag_manager.title"], - done_callback=lambda checked=False: self.main_window.preview_panel.set_selection( + is_savable=False, + ) + self.tag_manager_panel.done.connect( + lambda checked=False: self.main_window.preview_panel.set_selection( self.selected, update_preview=False - ), - has_save=False, + ) ) # Initialize the Color Group Manager panel @@ -393,10 +392,12 @@ class QtDriver(DriverMixin, QObject): view=FieldTemplateSearchPanelView(is_field_template_chooser=False), ), title=Translations["field_template_manager.title"], - done_callback=lambda checked=False: self.main_window.preview_panel.set_selection( + is_savable=False, + ) + self.field_template_manager_panel.done.connect( + lambda checked=False: self.main_window.preview_panel.set_selection( self.selected, update_preview=False - ), - has_save=False, + ) ) # Initialize the Tag Search panel @@ -741,7 +742,7 @@ class QtDriver(DriverMixin, QObject): self.ignore_modal = PanelModal( panel, Translations["menu.edit.ignore_files"], - has_save=True, + is_savable=True, ) self.ignore_modal.saved.connect(panel.save) self.main_window.menu_bar.ignore_modal_action.triggered.connect(self.ignore_modal.show) @@ -880,7 +881,7 @@ class QtDriver(DriverMixin, QObject): panel, Translations["tag.new"], Translations["tag.add"], - has_save=True, + is_savable=True, ) self.modal.saved.connect( diff --git a/src/tagstudio/qt/views/clickable_label.py b/src/tagstudio/qt/views/clickable_label.py index f382d14e..2eadda67 100644 --- a/src/tagstudio/qt/views/clickable_label.py +++ b/src/tagstudio/qt/views/clickable_label.py @@ -2,7 +2,7 @@ # SPDX-License-Identifier: GPL-3.0-only -from typing import override +from typing import Any, override from PySide6.QtCore import Signal from PySide6.QtGui import QMouseEvent @@ -14,8 +14,8 @@ class ClickableLabel(QLabel): clicked = Signal() - def __init__(self): - super().__init__() + def __init__(self, *args: Any, **kwarg: Any): # pyright: ignore[reportExplicitAny] + super().__init__(*args, **kwarg) @override def mousePressEvent(self, ev: QMouseEvent): diff --git a/src/tagstudio/qt/views/edit_field_template_modal_view.py b/src/tagstudio/qt/views/edit_field_template_modal_view.py index 71ef3414..36f7c90d 100644 --- a/src/tagstudio/qt/views/edit_field_template_modal_view.py +++ b/src/tagstudio/qt/views/edit_field_template_modal_view.py @@ -29,31 +29,31 @@ class EditFieldTemplateModalView(PanelWidget): self.root_layout.setAlignment(Qt.AlignmentFlag.AlignTop) # Field Name ------------------------------------------------------------------------------- - self.__name_widget = QWidget() - self.__name_layout = QVBoxLayout(self.__name_widget) - self.__name_layout.setStretch(1, 1) - self.__name_layout.setContentsMargins(0, 0, 0, 0) - self.__name_layout.setSpacing(0) - self.__name_layout.setAlignment(Qt.AlignmentFlag.AlignLeft) - self.__name_title = QLabel(Translations["field.name"]) - self.__name_layout.addWidget(self.__name_title) + self._name_widget = QWidget() + self._name_layout = QVBoxLayout(self._name_widget) + self._name_layout.setStretch(1, 1) + self._name_layout.setContentsMargins(0, 0, 0, 0) + self._name_layout.setSpacing(0) + self._name_layout.setAlignment(Qt.AlignmentFlag.AlignLeft) + self._name_title = QLabel(Translations["field.name"]) + self._name_layout.addWidget(self._name_title) self.name_field = QLineEdit() self.name_field.setFixedHeight(24) self.name_field.setPlaceholderText(Translations["field.field_name_required"]) - self.__name_layout.addWidget(self.name_field) + self._name_layout.addWidget(self.name_field) # Field Type ------------------------------------------------------------------------------- - self.__type_widget = QWidget() - self.__type_layout = QVBoxLayout(self.__type_widget) - self.__type_layout.setStretch(1, 1) - self.__type_layout.setContentsMargins(0, 0, 0, 0) - self.__type_layout.setSpacing(0) - self.__type_layout.setAlignment(Qt.AlignmentFlag.AlignLeft) - self.__type_title = QLabel(Translations["field.type"]) - self.__type_layout.addWidget(self.__type_title) + self._type_widget = QWidget() + self._type_layout = QVBoxLayout(self._type_widget) + self._type_layout.setStretch(1, 1) + self._type_layout.setContentsMargins(0, 0, 0, 0) + self._type_layout.setSpacing(0) + self._type_layout.setAlignment(Qt.AlignmentFlag.AlignLeft) + self._type_title = QLabel(Translations["field.type"]) + self._type_layout.addWidget(self._type_title) self._type_combobox = QComboBox() - self.__type_layout.addWidget(self._type_combobox) + self._type_layout.addWidget(self._type_combobox) # Add Widgets to Layout ==================================================================== - self.root_layout.addWidget(self.__name_widget) - self.root_layout.addWidget(self.__type_widget) + self.root_layout.addWidget(self._name_widget) + self.root_layout.addWidget(self._type_widget) diff --git a/src/tagstudio/qt/views/edit_text_box_modal.py b/src/tagstudio/qt/views/edit_text_box_modal.py deleted file mode 100644 index fd60ba21..00000000 --- a/src/tagstudio/qt/views/edit_text_box_modal.py +++ /dev/null @@ -1,25 +0,0 @@ -# SPDX-FileCopyrightText: (c) TagStudio Contributors -# SPDX-License-Identifier: GPL-3.0-only - - -from PySide6.QtWidgets import QPlainTextEdit, QVBoxLayout - -from tagstudio.qt.views.panel_modal import PanelWidget - - -class EditTextBox(PanelWidget): - def __init__(self, text): - super().__init__() - self.setMinimumSize(480, 480) - self.root_layout = QVBoxLayout(self) - self.root_layout.setContentsMargins(6, 0, 6, 0) - self.text = text - self.text_edit = QPlainTextEdit() - self.text_edit.setPlainText(text) - self.root_layout.addWidget(self.text_edit) - - def get_content(self) -> str: - return self.text_edit.toPlainText() - - def reset(self): - self.text_edit.setPlainText(self.text) diff --git a/src/tagstudio/qt/views/edit_text_line_modal.py b/src/tagstudio/qt/views/edit_text_line_modal.py deleted file mode 100644 index d7ce6d0d..00000000 --- a/src/tagstudio/qt/views/edit_text_line_modal.py +++ /dev/null @@ -1,33 +0,0 @@ -# SPDX-FileCopyrightText: (c) TagStudio Contributors -# SPDX-License-Identifier: GPL-3.0-only - - -from collections.abc import Callable - -from PySide6.QtWidgets import QLineEdit, QVBoxLayout - -from tagstudio.qt.views.panel_modal import PanelWidget - - -class EditTextLine(PanelWidget): - def __init__(self, text): - super().__init__() - self.setMinimumWidth(480) - self.root_layout = QVBoxLayout(self) - self.root_layout.setContentsMargins(6, 0, 6, 0) - self.text = text - self.text_edit = QLineEdit() - self.text_edit.setText(text) - self.root_layout.addWidget(self.text_edit) - - def get_content(self) -> str: - return self.text_edit.text() - - def reset(self): - self.text_edit.setText(self.text) - - def add_callback(self, callback: Callable, event: str = "returnPressed"): - if event == "returnPressed": - self.text_edit.returnPressed.connect(callback) - else: - raise ValueError(f"unknown event type: {event}") diff --git a/src/tagstudio/qt/views/edit_text_view.py b/src/tagstudio/qt/views/edit_text_view.py new file mode 100644 index 00000000..2fc9eeb7 --- /dev/null +++ b/src/tagstudio/qt/views/edit_text_view.py @@ -0,0 +1,60 @@ +# SPDX-FileCopyrightText: (c) TagStudio Contributors +# SPDX-License-Identifier: GPL-3.0-only + + +from PySide6.QtGui import Qt +from PySide6.QtWidgets import ( + QCheckBox, + QHBoxLayout, + QLineEdit, + QPlainTextEdit, + QSizePolicy, + QVBoxLayout, + QWidget, +) + +from tagstudio.qt.translations import Translations +from tagstudio.qt.views.clickable_label import ClickableLabel +from tagstudio.qt.views.panel_modal import PanelWidget +from tagstudio.qt.views.stylesheets.stylesheets import checkbox_style + + +class EditTextView(PanelWidget): + def __init__(self): + super().__init__() + self.setMinimumSize(480, 240) + 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.text_box = QPlainTextEdit() + self.text_line = QLineEdit() + self.text_line_stretch = QWidget() + self.text_line_stretch.setSizePolicy( + QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding + ) + + # Is Multiline + self.multiline_widget = QWidget() + self.multiline_layout = QHBoxLayout(self.multiline_widget) + self.multiline_layout.setStretch(1, 1) + self.multiline_layout.setContentsMargins(0, 0, 0, 0) + self.multiline_layout.setSpacing(6) + self.multiline_layout.setAlignment(Qt.AlignmentFlag.AlignLeft) + self.multiline_title = ClickableLabel(Translations["field.text.is_multiline"]) + self.multiline_checkbox = QCheckBox() + self.multiline_checkbox.setFixedSize(22, 22) + self.multiline_checkbox.setStyleSheet(checkbox_style()) + self.multiline_title.clicked.connect(self.multiline_checkbox.click) + self.multiline_layout.addWidget(self.multiline_checkbox) + self.multiline_layout.addWidget(self.multiline_title) + + self.root_layout.addWidget(self.name_field) + self.root_layout.addWidget(self.text_box) + self.root_layout.setStretch(2, 1) + self.root_layout.addWidget(self.text_line) + self.root_layout.addWidget(self.text_line_stretch) + self.root_layout.setStretch(4, 1) + self.root_layout.addWidget(self.multiline_widget) diff --git a/src/tagstudio/qt/views/field_template_search_panel_view.py b/src/tagstudio/qt/views/field_template_search_panel_view.py index 0c8ef6fc..4b6218a9 100644 --- a/src/tagstudio/qt/views/field_template_search_panel_view.py +++ b/src/tagstudio/qt/views/field_template_search_panel_view.py @@ -1,6 +1,8 @@ # SPDX-FileCopyrightText: (c) TagStudio Contributors # SPDX-License-Identifier: GPL-3.0-only +from typing import override + from PySide6.QtWidgets import QWidget from tagstudio.core.library.alchemy.library import Library @@ -16,6 +18,7 @@ class FieldTemplateSearchPanelView(SearchPanelView): self.search_field.setPlaceholderText(Translations["home.search_field_templates"]) self.create_button.setText(Translations["field_template.create"]) + @override def get_item_widget(self, index: int, library: Library | None) -> FieldTemplateWidget: """Gets the item widget at a specific index.""" # Create any new item widgets needed up to the given index diff --git a/src/tagstudio/qt/views/main_window.py b/src/tagstudio/qt/views/main_window.py index b9229fdd..13ead752 100644 --- a/src/tagstudio/qt/views/main_window.py +++ b/src/tagstudio/qt/views/main_window.py @@ -10,7 +10,7 @@ import structlog from PIL import Image, ImageQt from PySide6 import QtCore from PySide6.QtCore import QMetaObject, QSize, QStringListModel, Qt -from PySide6.QtGui import QAction, QColor, QPixmap +from PySide6.QtGui import QAction, QPixmap from PySide6.QtWidgets import ( QCheckBox, QComboBox, @@ -35,18 +35,17 @@ from PySide6.QtWidgets import ( ) from tagstudio.core.enums import ShowFilepathOption -from tagstudio.core.library.alchemy.enums import SortingModeEnum, TagColorEnum +from tagstudio.core.library.alchemy.enums import SortingModeEnum from tagstudio.qt.controllers.preview_panel_controller import PreviewPanel from tagstudio.qt.helpers.color_overlay import theme_fg_overlay from tagstudio.qt.mixed.landing import LandingWidget from tagstudio.qt.mixed.pagination import Pagination -from tagstudio.qt.mixed.tag_widget import get_border_color, get_highlight_color, get_text_color from tagstudio.qt.mnemonics import assign_mnemonics -from tagstudio.qt.models.palette import ColorType, get_tag_color from tagstudio.qt.platform_strings import trash_term from tagstudio.qt.resource_manager import ResourceManager from tagstudio.qt.thumb_grid_layout import ThumbGridLayout from tagstudio.qt.translations import Translations +from tagstudio.qt.views.stylesheets.stylesheets import checkbox_style # Only import for type checking/autocompletion, will not be imported at runtime. if typing.TYPE_CHECKING: @@ -589,11 +588,6 @@ class MainWindow(QMainWindow): self.extra_input_layout = QHBoxLayout() self.extra_input_layout.setObjectName("extra_input_layout") - 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) - ## Show hidden entries checkbox self.show_hidden_entries_widget = QWidget() self.show_hidden_entries_layout = QHBoxLayout(self.show_hidden_entries_widget) @@ -604,33 +598,7 @@ class MainWindow(QMainWindow): self.show_hidden_entries_title = QLabel(Translations["home.show_hidden_entries"]) self.show_hidden_entries_checkbox = QCheckBox() self.show_hidden_entries_checkbox.setFixedSize(22, 22) - - self.show_hidden_entries_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.show_hidden_entries_checkbox.setStyleSheet(checkbox_style()) self.show_hidden_entries_checkbox.setChecked(False) # Default: No diff --git a/src/tagstudio/qt/views/panel_modal.py b/src/tagstudio/qt/views/panel_modal.py index 3d9a3176..87d476c7 100644 --- a/src/tagstudio/qt/views/panel_modal.py +++ b/src/tagstudio/qt/views/panel_modal.py @@ -2,8 +2,8 @@ # SPDX-License-Identifier: GPL-3.0-only -from collections.abc import Callable -from typing import override +import contextlib +from typing import Any, override import structlog from PySide6 import QtCore, QtGui @@ -16,18 +16,19 @@ logger = structlog.get_logger(__name__) class PanelModal(QWidget): - saved = Signal() + """A generic reusable modal panel widget.""" + + done = Signal() + saved = Signal() + saved_data = Signal(type(Any)) - # TODO: Separate callbacks from the buttons you want, and just generally - # figure out what you want from this. def __init__( self, widget: "PanelWidget", title: str = "", window_title: str | None = None, - done_callback: Callable[..., None] | None = None, - save_callback: Callable[..., None] | None = None, - has_save: bool = False, + is_savable: bool = False, + inline_title: bool = True, ): # [Done] # - OR - @@ -37,37 +38,24 @@ class PanelModal(QWidget): self.setWindowTitle(title if window_title is None else window_title) self.setWindowModality(Qt.WindowModality.ApplicationModal) self.root_layout = QVBoxLayout(self) - self.root_layout.setContentsMargins(6, 0, 6, 6) - - self.title_widget = QLabel() - self.title_widget.setObjectName("fieldTitle") - self.title_widget.setWordWrap(True) - self.title_widget.setStyleSheet("font-weight:bold;font-size:14px;padding-top: 6px") - self.title_widget.setText(title) - self.title_widget.setAlignment(Qt.AlignmentFlag.AlignCenter) + self.root_layout.setContentsMargins(6, 0 if inline_title else 12, 6, 6) self.button_container = QWidget() self.button_layout = QHBoxLayout(self.button_container) self.button_layout.setContentsMargins(6, 6, 6, 6) self.button_layout.addStretch(1) - # self.cancel_button = QPushButton() - # self.cancel_button.setText('Cancel') - - if not (save_callback or has_save): + if not is_savable: self.done_button = QPushButton(Translations["generic.done"]) self.done_button.setAutoDefault(True) self.done_button.clicked.connect(self.hide) - if done_callback: - self.done_button.clicked.connect(done_callback) + self.done_button.clicked.connect(self.done.emit) self.widget.panel_done_button = self.done_button self.button_layout.addWidget(self.done_button) - - if save_callback or has_save: + else: self.cancel_button = QPushButton(Translations["generic.cancel"]) self.cancel_button.clicked.connect(self.hide) self.cancel_button.clicked.connect(widget.reset) - # self.cancel_button.clicked.connect(cancel_callback) self.widget.panel_cancel_button = self.cancel_button self.button_layout.addWidget(self.cancel_button) @@ -75,23 +63,19 @@ class PanelModal(QWidget): self.save_button.setAutoDefault(True) self.save_button.clicked.connect(self.hide) self.save_button.clicked.connect(self.saved.emit) + self.save_button.clicked.connect(lambda: self.saved_data.emit(widget.saved_data())) self.widget.panel_save_button = self.save_button - - if done_callback: - self.save_button.clicked.connect(done_callback) - - if save_callback: - self.save_button.clicked.connect(lambda: save_callback(widget.get_content())) - self.button_layout.addWidget(self.save_button) - # trigger save button actions when pressing enter in the widget - self.widget.add_callback(lambda: self.save_button.click()) + if inline_title: + self.title_widget = QLabel() + self.title_widget.setObjectName("fieldTitle") + self.title_widget.setWordWrap(True) + self.title_widget.setStyleSheet("font-weight:bold;font-size:14px;padding-top:6px") + self.title_widget.setText(title) + self.title_widget.setAlignment(Qt.AlignmentFlag.AlignCenter) + self.root_layout.addWidget(self.title_widget) - if save_callback is not None: - widget.done.connect(lambda: save_callback(widget.get_content())) - - self.root_layout.addWidget(self.title_widget) self.root_layout.addWidget(widget) widget.parent_modal = self self.root_layout.setStretch(1, 2) @@ -100,9 +84,9 @@ class PanelModal(QWidget): @override def closeEvent(self, event: QtGui.QCloseEvent) -> None: - if self.cancel_button: + with contextlib.suppress(AttributeError): self.cancel_button.click() - elif self.done_button: + with contextlib.suppress(AttributeError): self.done_button.click() event.accept() @@ -110,7 +94,6 @@ class PanelModal(QWidget): class PanelWidget(QWidget): """Used for widgets that go in a modal panel, ex. for editing or searching.""" - done = Signal() parent_modal: PanelModal | None = None panel_save_button: QPushButton | None = None panel_cancel_button: QPushButton | None = None @@ -119,8 +102,8 @@ class PanelWidget(QWidget): def __init__(self): super().__init__() - def get_content(self) -> str: - return "" + def saved_data(self) -> Any: # pyright: ignore[reportExplicitAny] + return None def reset(self) -> None: pass @@ -128,9 +111,6 @@ class PanelWidget(QWidget): def parent_post_init(self) -> None: pass - def add_callback(self, callback: Callable[..., None], event: str = "returnPressed"): # pyright: ignore[reportUnusedParameter] - logger.warning(f"[PanelModal] add_callback not implemented for {self.__class__.__name__}") - @override def keyPressEvent(self, event: QtGui.QKeyEvent) -> None: if event.key() == QtCore.Qt.Key.Key_Escape: diff --git a/src/tagstudio/qt/views/preview_thumb_view.py b/src/tagstudio/qt/views/preview_thumb_view.py index d6c032f7..381ae10b 100644 --- a/src/tagstudio/qt/views/preview_thumb_view.py +++ b/src/tagstudio/qt/views/preview_thumb_view.py @@ -19,7 +19,7 @@ from tagstudio.qt.mixed.media_player import MediaPlayer from tagstudio.qt.platform_strings import open_file_str, trash_term from tagstudio.qt.previews.renderer import ThumbRenderer from tagstudio.qt.translations import Translations -from tagstudio.qt.views.styles.rounded_pixmap_style import RoundedPixmapStyle +from tagstudio.qt.views.stylesheets.rounded_pixmap_style import RoundedPixmapStyle if TYPE_CHECKING: from tagstudio.qt.ts_qt import QtDriver diff --git a/src/tagstudio/qt/views/styles/rounded_pixmap_style.py b/src/tagstudio/qt/views/stylesheets/rounded_pixmap_style.py similarity index 100% rename from src/tagstudio/qt/views/styles/rounded_pixmap_style.py rename to src/tagstudio/qt/views/stylesheets/rounded_pixmap_style.py diff --git a/src/tagstudio/qt/views/stylesheets/stylesheets.py b/src/tagstudio/qt/views/stylesheets/stylesheets.py new file mode 100644 index 00000000..af331fc7 --- /dev/null +++ b/src/tagstudio/qt/views/stylesheets/stylesheets.py @@ -0,0 +1,41 @@ +# SPDX-FileCopyrightText: (c) TagStudio Contributors +# SPDX-License-Identifier: GPL-3.0-only + +from PySide6.QtGui import QColor + +from tagstudio.core.library.alchemy.enums import TagColorEnum +from tagstudio.qt.mixed.tag_widget import get_border_color, get_highlight_color, get_text_color +from tagstudio.qt.models.palette import ColorType, get_tag_color + + +def checkbox_style() -> str: + 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) + return ( + 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"}}" + ) diff --git a/src/tagstudio/resources/translations/en.json b/src/tagstudio/resources/translations/en.json index f847f198..e0cd5dbb 100644 --- a/src/tagstudio/resources/translations/en.json +++ b/src/tagstudio/resources/translations/en.json @@ -92,6 +92,7 @@ "field.name": "Name", "field.paste": "Paste Field", "field.remove": "Remove Field", + "field.text.is_multiline": "Multiline", "field.type": "Type", "file.date_added": "Date Added", "file.date_created": "Date Created", diff --git a/tests/test_library.py b/tests/test_library.py index 17f0d990..4cca5d8f 100644 --- a/tests/test_library.py +++ b/tests/test_library.py @@ -225,7 +225,9 @@ def test_remove_text_field_entry_with_multiple_fields(library: Library, entry_fu def test_update_entry_field(library: Library, entry_full: Entry): title_field = entry_full.text_fields[0] - library.update_text_field(entry_full.id, title_field, "new value", title_field.is_multiline) + library.update_text_field( + entry_full.id, title_field, title_field.name, "new value", title_field.is_multiline + ) entry = next(library.all_entries(with_joins=True)) assert entry.text_fields[0].value == "new value" @@ -241,7 +243,9 @@ def test_update_entry_with_multiple_identical_text_fields(library: Library, entr library.add_field_to_entries(entry_full.id, field=empty_title) # update one of the fields - library.update_text_field(entry_full.id, title_field, "new value", title_field.is_multiline) + library.update_text_field( + entry_full.id, title_field, title_field.name, "new value", title_field.is_multiline + ) # Then only one should be updated entry = next(library.all_entries(with_joins=True))