diff --git a/src/tagstudio/qt/controllers/preview_panel/fields/color_box_widget_controller.py b/src/tagstudio/qt/controllers/preview_panel/fields/color_box_widget_controller.py new file mode 100644 index 000000000..57e4732de --- /dev/null +++ b/src/tagstudio/qt/controllers/preview_panel/fields/color_box_widget_controller.py @@ -0,0 +1,72 @@ +from typing import TYPE_CHECKING + +import structlog +from PySide6.QtCore import Signal +from PySide6.QtWidgets import QMessageBox + +from tagstudio.core.library.alchemy.models import TagColorGroup +from tagstudio.qt.mixed.build_color import BuildColorPanel +from tagstudio.qt.translations import Translations +from tagstudio.qt.views.panel_modal import PanelModal +from tagstudio.qt.views.preview_panel.fields.color_box_widget_view import ColorBoxWidgetView + +if TYPE_CHECKING: + from tagstudio.core.library.alchemy.library import Library + +logger = structlog.get_logger(__name__) + + +class ColorBoxWidget(ColorBoxWidgetView): + """A widget holding a list of tag colors.""" + + on_update = Signal() + + def __init__(self, group: str, colors: list[TagColorGroup], library: "Library") -> None: + super().__init__(group, colors, library) + self.__lib: Library = library + + def _on_edit_color(self, color_group: TagColorGroup) -> None: + build_color_panel = BuildColorPanel(self.__lib, color_group) + + edit_color_modal = PanelModal( + build_color_panel, + "Edit Color", + has_save=True, + ) + + edit_color_modal.saved.connect( + lambda: ( + self.__lib.update_color(*build_color_panel.build_color()), + self.on_update.emit(), + ) + ) + + edit_color_modal.show() + + def _on_delete_color(self, color_group: TagColorGroup) -> None: + # Dialogue box + message_box = QMessageBox( + QMessageBox.Icon.Warning, + Translations["color.delete"], + Translations.format("color.confirm_delete", color_name=color_group.name), + ) + + # Buttons + cancel_button = message_box.addButton( + Translations["generic.cancel_alt"], QMessageBox.ButtonRole.RejectRole + ) + message_box.addButton( + Translations["generic.delete_alt"], QMessageBox.ButtonRole.DestructiveRole + ) + message_box.setEscapeButton(cancel_button) + + # Dialogue box result + result = message_box.exec_() + logger.info(QMessageBox.ButtonRole.DestructiveRole.value) + + if result != QMessageBox.ButtonRole.ActionRole.value: + return + + logger.info("[ColorBoxWidget] Removing color", color=color_group) + self.__lib.delete_color(color_group) + self.on_update.emit() diff --git a/src/tagstudio/qt/controllers/tag_box_controller.py b/src/tagstudio/qt/controllers/preview_panel/fields/tag_box_widget_controller.py similarity index 94% rename from src/tagstudio/qt/controllers/tag_box_controller.py rename to src/tagstudio/qt/controllers/preview_panel/fields/tag_box_widget_controller.py index 2a5865d8b..fce5c979a 100644 --- a/src/tagstudio/qt/controllers/tag_box_controller.py +++ b/src/tagstudio/qt/controllers/preview_panel/fields/tag_box_widget_controller.py @@ -13,7 +13,7 @@ from tagstudio.core.utils.types import unwrap from tagstudio.qt.mixed.build_tag import BuildTagPanel from tagstudio.qt.views.panel_modal import PanelModal -from tagstudio.qt.views.tag_box_view import TagBoxWidgetView +from tagstudio.qt.views.preview_panel.fields.tag_box_widget_view import TagBoxWidgetView if TYPE_CHECKING: from tagstudio.qt.ts_qt import QtDriver @@ -22,11 +22,13 @@ class TagBoxWidget(TagBoxWidgetView): + """A widget that holds a list of tags.""" + on_update = Signal() __entries: list[int] = [] - def __init__(self, title: str, driver: "QtDriver"): + def __init__(self, title: str, driver: "QtDriver") -> None: super().__init__(title, driver) self.__driver = driver diff --git a/src/tagstudio/qt/mixed/color_box.py b/src/tagstudio/qt/mixed/color_box.py deleted file mode 100644 index 20866c438..000000000 --- a/src/tagstudio/qt/mixed/color_box.py +++ /dev/null @@ -1,166 +0,0 @@ -# Copyright (C) 2025 Travis Abendshien (CyanVoxel). -# Licensed under the GPL-3.0 License. -# Created for TagStudio: https://github.com/CyanVoxel/TagStudio - - -import typing -from collections.abc import Iterable - -import structlog -from PySide6.QtCore import Signal -from PySide6.QtWidgets import QMessageBox, QPushButton - -from tagstudio.core.constants import RESERVED_NAMESPACE_PREFIX -from tagstudio.core.library.alchemy.enums import TagColorEnum -from tagstudio.core.library.alchemy.models import TagColorGroup -from tagstudio.core.utils.types import unwrap -from tagstudio.qt.mixed.build_color import BuildColorPanel -from tagstudio.qt.mixed.field_widget import FieldWidget -from tagstudio.qt.mixed.tag_color_label import TagColorLabel -from tagstudio.qt.models.palette import ColorType, get_tag_color -from tagstudio.qt.translations import Translations -from tagstudio.qt.views.layouts.flow_layout import FlowLayout -from tagstudio.qt.views.panel_modal import PanelModal - -if typing.TYPE_CHECKING: - from tagstudio.core.library.alchemy.library import Library - -logger = structlog.get_logger(__name__) - - -class ColorBoxWidget(FieldWidget): - updated = Signal() - - def __init__( - self, - group: str, - colors: list["TagColorGroup"], - library: "Library", - ) -> None: - self.namespace = group - self.colors: list[TagColorGroup] = colors - self.lib: Library = library - - title = "" if not self.lib.engine else self.lib.get_namespace_name(group) - super().__init__(title) - - self.add_button_stylesheet = ( - f"QPushButton{{" - f"background: {get_tag_color(ColorType.PRIMARY, TagColorEnum.DEFAULT)};" - f"color: {get_tag_color(ColorType.TEXT, TagColorEnum.DEFAULT)};" - f"font-weight: 600;" - f"border-color:{get_tag_color(ColorType.BORDER, TagColorEnum.DEFAULT)};" - f"border-radius: 6px;" - f"border-style:solid;" - f"border-width: 2px;" - f"padding-right: 4px;" - f"padding-bottom: 2px;" - f"padding-left: 4px;" - f"font-size: 15px" - f"}}" - f"QPushButton::hover{{" - f"border-color:{get_tag_color(ColorType.LIGHT_ACCENT, TagColorEnum.DEFAULT)};" - f"}}" - f"QPushButton::pressed{{" - f"background: {get_tag_color(ColorType.LIGHT_ACCENT, TagColorEnum.DEFAULT)};" - f"color: {get_tag_color(ColorType.PRIMARY, TagColorEnum.DEFAULT)};" - f"border-color: {get_tag_color(ColorType.PRIMARY, TagColorEnum.DEFAULT)};" - f"}}" - f"QPushButton::focus{{" - f"border-color: {get_tag_color(ColorType.LIGHT_ACCENT, TagColorEnum.DEFAULT)};" - f"outline:none;" - f"}}" - ) - - self.setObjectName("colorBox") - self.base_layout = FlowLayout() - self.base_layout.enable_grid_optimizations(value=True) - self.base_layout.setContentsMargins(0, 0, 0, 0) - self.setLayout(self.base_layout) - - self.set_colors(self.colors) - - def set_colors(self, colors: Iterable[TagColorGroup]): - colors_ = sorted( - list(colors), key=lambda color: self.lib.get_namespace_name(color.namespace) - ) - is_mutable = not self.namespace.startswith(RESERVED_NAMESPACE_PREFIX) - max_width = 60 - color_widgets: list[TagColorLabel] = [] - - while self.base_layout.itemAt(0): - unwrap(self.base_layout.takeAt(0)).widget().deleteLater() - - for color in colors_: - color_widget = TagColorLabel( - color=color, - has_edit=is_mutable, - has_remove=is_mutable, - library=self.lib, - ) - hint = color_widget.sizeHint().width() - if hint > max_width: - max_width = hint - color_widget.on_click.connect(lambda c=color: self.edit_color(c)) - color_widget.on_remove.connect(lambda c=color: self.delete_color(c)) - - color_widgets.append(color_widget) - self.base_layout.addWidget(color_widget) - - for color_widget in color_widgets: - color_widget.setFixedWidth(max_width) - - if is_mutable: - add_button = QPushButton() - add_button.setText("+") - add_button.setFlat(True) - add_button.setFixedSize(22, 22) - add_button.setStyleSheet(self.add_button_stylesheet) - add_button.clicked.connect( - lambda: self.edit_color( - TagColorGroup( - slug="slug", - namespace=self.namespace, - name="Color", - primary="#FFFFFF", - secondary=None, - ) - ) - ) - self.base_layout.addWidget(add_button) - - def edit_color(self, color_group: TagColorGroup): - build_color_panel = BuildColorPanel(self.lib, color_group) - - self.edit_modal = PanelModal( - build_color_panel, - "Edit Color", - has_save=True, - ) - - self.edit_modal.saved.connect( - lambda: (self.lib.update_color(*build_color_panel.build_color()), self.updated.emit()) # type: ignore - ) - self.edit_modal.show() - - def delete_color(self, color_group: TagColorGroup): - message_box = QMessageBox( - QMessageBox.Icon.Warning, - Translations["color.delete"], - Translations.format("color.confirm_delete", color_name=color_group.name), - ) - cancel_button = message_box.addButton( - Translations["generic.cancel_alt"], QMessageBox.ButtonRole.RejectRole - ) - message_box.addButton( - Translations["generic.delete_alt"], QMessageBox.ButtonRole.DestructiveRole - ) - message_box.setEscapeButton(cancel_button) - result = message_box.exec_() - logger.info(QMessageBox.ButtonRole.DestructiveRole.value) - if result != QMessageBox.ButtonRole.ActionRole.value: - return - - logger.info("[ColorBoxWidget] Removing color", color=color_group) - self.lib.delete_color(color_group) - self.updated.emit() diff --git a/src/tagstudio/qt/mixed/field_containers.py b/src/tagstudio/qt/mixed/field_containers.py index 7f17dbf29..b262c4c52 100644 --- a/src/tagstudio/qt/mixed/field_containers.py +++ b/src/tagstudio/qt/mixed/field_containers.py @@ -33,14 +33,14 @@ 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.tag_box_controller import TagBoxWidget +from tagstudio.qt.controllers.preview_panel.fields.tag_box_widget_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.translations import 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 +from tagstudio.qt.views.preview_panel.fields.field_container import FieldContainer +from tagstudio.qt.views.preview_panel.fields.text_field_widget import TextFieldWidget if typing.TYPE_CHECKING: from tagstudio.qt.ts_qt import QtDriver @@ -274,8 +274,8 @@ def write_container(self, index: int, field: BaseField, is_mixed: bool = False): text = "Mixed Data" title = f"{field.type.name} ({field.type.type.value})" - inner_widget = TextWidget(title, text) - container.set_inner_widget(inner_widget) + inner_widget = TextFieldWidget(title, text) + container.set_field_widget(inner_widget) if not is_mixed: modal = PanelModal( EditTextLine(field.value), @@ -313,8 +313,8 @@ def write_container(self, index: int, field: BaseField, is_mixed: bool = False): else: text = "Mixed Data" title = f"{field.type.name} (Text Box)" - inner_widget = TextWidget(title, text) - container.set_inner_widget(inner_widget) + inner_widget = TextFieldWidget(title, text) + container.set_field_widget(inner_widget) if not is_mixed: modal = PanelModal( EditTextBox(field.value), @@ -354,8 +354,8 @@ def write_container(self, index: int, field: BaseField, is_mixed: bool = False): title += " (Unknown Format)" text = str(field.value) - inner_widget = TextWidget(title, text) - container.set_inner_widget(inner_widget) + inner_widget = TextFieldWidget(title, text) + container.set_field_widget(inner_widget) modal = PanelModal( DatetimePicker(self.driver, field.value or dt.now()), @@ -381,15 +381,15 @@ def write_container(self, index: int, field: BaseField, is_mixed: bool = False): else: text = "Mixed Data" title = f"{field.type.name} (Wacky Date)" - inner_widget = TextWidget(title, text) - container.set_inner_widget(inner_widget) + inner_widget = TextFieldWidget(title, text) + container.set_field_widget(inner_widget) else: logger.warning("[FieldContainers][write_container] Unknown Field", field=field) container.set_title(field.type.name) container.set_inline(False) title = f"{field.type.name} (Unknown Field Type)" - inner_widget = TextWidget(title, field.type.name) - container.set_inner_widget(inner_widget) + inner_widget = TextFieldWidget(title, field.type.name) + container.set_field_widget(inner_widget) container.set_remove_callback( lambda: self.remove_message_box( prompt=self.remove_field_prompt(field.type.name), @@ -427,28 +427,28 @@ def write_tag_container( container.set_inline(False) if not is_mixed: - inner_widget = container.get_inner_widget() + field_widget = container.get_field_widget() - if isinstance(inner_widget, TagBoxWidget): + if isinstance(field_widget, TagBoxWidget): with catch_warnings(record=True): - inner_widget.on_update.disconnect() + field_widget.on_update.disconnect() else: - inner_widget = TagBoxWidget( + field_widget = TagBoxWidget( "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) + container.set_field_widget(field_widget) + field_widget.set_entries([e.id for e in self.cached_entries]) + field_widget.set_tags(tags) - inner_widget.on_update.connect( + field_widget.on_update.connect( lambda: (self.update_from_entry(self.cached_entries[0].id, update_badges=True)) ) else: text = "Mixed Data" - inner_widget = TextWidget("Mixed Tags", text) - container.set_inner_widget(inner_widget) + field_widget = TextFieldWidget("Mixed Tags", text) + container.set_field_widget(field_widget) container.set_edit_callback() container.set_remove_callback() diff --git a/src/tagstudio/qt/mixed/field_widget.py b/src/tagstudio/qt/mixed/field_widget.py deleted file mode 100644 index d2678b556..000000000 --- a/src/tagstudio/qt/mixed/field_widget.py +++ /dev/null @@ -1,207 +0,0 @@ -# Copyright (C) 2025 Travis Abendshien (CyanVoxel). -# Licensed under the GPL-3.0 License. -# Created for TagStudio: https://github.com/CyanVoxel/TagStudio - - -import math -from collections.abc import Callable -from pathlib import Path -from typing import override -from warnings import catch_warnings - -import structlog -from PIL import Image, ImageQt -from PySide6.QtCore import QEvent, Qt -from PySide6.QtGui import QEnterEvent, QPixmap, QResizeEvent -from PySide6.QtWidgets import QHBoxLayout, QLabel, QPushButton, QVBoxLayout, QWidget - -from tagstudio.core.enums import Theme - -logger = structlog.get_logger(__name__) - - -class FieldContainer(QWidget): - # TODO: reference a resources folder rather than path.parents[2]? - clipboard_icon_128: Image.Image = Image.open( - str(Path(__file__).parents[2] / "resources/qt/images/clipboard_icon_128.png") - ).resize((math.floor(24 * 1.25), math.floor(24 * 1.25))) - clipboard_icon_128.load() - - edit_icon_128: Image.Image = Image.open( - str(Path(__file__).parents[2] / "resources/qt/images/edit_icon_128.png") - ).resize((math.floor(24 * 1.25), math.floor(24 * 1.25))) - edit_icon_128.load() - - trash_icon_128: Image.Image = Image.open( - str(Path(__file__).parents[2] / "resources/qt/images/trash_icon_128.png") - ).resize((math.floor(24 * 1.25), math.floor(24 * 1.25))) - trash_icon_128.load() - - # TODO: There should be a global button theme somewhere. - container_style = ( - f"QWidget#fieldContainer{{" - "border-radius:4px;" - f"}}" - f"QWidget#fieldContainer::hover{{" - f"background-color:{Theme.COLOR_HOVER.value};" - f"}}" - f"QWidget#fieldContainer::pressed{{" - f"background-color:{Theme.COLOR_PRESSED.value};" - f"}}" - ) - - def __init__(self, title: str = "Field", inline: bool = True) -> None: - 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 - button_size = 24 - - self.root_layout = QVBoxLayout(self) - self.root_layout.setObjectName("baseLayout") - self.root_layout.setContentsMargins(0, 0, 0, 0) - - self.inner_layout = QVBoxLayout() - self.inner_layout.setObjectName("innerLayout") - self.inner_layout.setContentsMargins(6, 0, 6, 6) - self.inner_layout.setSpacing(0) - self.field_container = QWidget() - self.field_container.setObjectName("fieldContainer") - self.field_container.setLayout(self.inner_layout) - self.root_layout.addWidget(self.field_container) - - self.title_container = QWidget() - self.title_layout = QHBoxLayout(self.title_container) - self.title_layout.setAlignment(Qt.AlignmentFlag.AlignLeft) - self.title_layout.setObjectName("fieldLayout") - self.title_layout.setContentsMargins(0, 0, 0, 0) - self.title_layout.setSpacing(0) - self.inner_layout.addWidget(self.title_container) - - self.title_widget = QLabel() - self.title_widget.setMinimumHeight(button_size) - self.title_widget.setObjectName("fieldTitle") - self.title_widget.setWordWrap(True) - self.title_widget.setText(title) - self.title_layout.addWidget(self.title_widget) - self.title_layout.addStretch(2) - - self.copy_button = QPushButton() - self.copy_button.setObjectName("copyButton") - self.copy_button.setMinimumSize(button_size, button_size) - self.copy_button.setMaximumSize(button_size, button_size) - self.copy_button.setFlat(True) - self.copy_button.setIcon(QPixmap.fromImage(ImageQt.ImageQt(self.clipboard_icon_128))) - self.copy_button.setCursor(Qt.CursorShape.PointingHandCursor) - self.title_layout.addWidget(self.copy_button) - self.copy_button.setHidden(True) - - self.edit_button = QPushButton() - self.edit_button.setObjectName("editButton") - self.edit_button.setMinimumSize(button_size, button_size) - self.edit_button.setMaximumSize(button_size, button_size) - self.edit_button.setFlat(True) - self.edit_button.setIcon(QPixmap.fromImage(ImageQt.ImageQt(self.edit_icon_128))) - self.edit_button.setCursor(Qt.CursorShape.PointingHandCursor) - self.title_layout.addWidget(self.edit_button) - self.edit_button.setHidden(True) - - self.remove_button = QPushButton() - self.remove_button.setObjectName("removeButton") - self.remove_button.setMinimumSize(button_size, button_size) - self.remove_button.setMaximumSize(button_size, button_size) - self.remove_button.setFlat(True) - self.remove_button.setIcon(QPixmap.fromImage(ImageQt.ImageQt(self.trash_icon_128))) - self.remove_button.setCursor(Qt.CursorShape.PointingHandCursor) - self.title_layout.addWidget(self.remove_button) - self.remove_button.setHidden(True) - - self.field = QWidget() - self.field.setObjectName("field") - self.field_layout = QHBoxLayout() - self.field_layout.setObjectName("fieldLayout") - self.field_layout.setContentsMargins(0, 0, 0, 0) - self.field.setLayout(self.field_layout) - self.inner_layout.addWidget(self.field) - - self.set_title(title) - self.setStyleSheet(FieldContainer.container_style) - - def set_copy_callback(self, callback: Callable[[], None] | None = None) -> None: - with catch_warnings(record=True): - self.copy_button.clicked.disconnect() - - self.copy_callback = callback - if callback: - self.copy_button.clicked.connect(callback) - - def set_edit_callback(self, callback: Callable[[], None] | None = None) -> None: - with catch_warnings(record=True): - self.edit_button.clicked.disconnect() - - self.edit_callback = callback - if callback: - self.edit_button.clicked.connect(callback) - - def set_remove_callback(self, callback: Callable[[], None] | None = None) -> None: - with catch_warnings(record=True): - self.remove_button.clicked.disconnect() - - self.remove_callback = callback - if callback: - self.remove_button.clicked.connect(callback) - - def set_inner_widget(self, widget: "FieldWidget") -> None: - if self.field_layout.itemAt(0): - old: QWidget = self.field_layout.itemAt(0).widget() - self.field_layout.removeWidget(old) - old.deleteLater() - - self.field_layout.addWidget(widget) - - def get_inner_widget(self) -> QWidget | None: - if self.field_layout.itemAt(0): - return self.field_layout.itemAt(0).widget() - return None - - def set_title(self, title: str) -> None: - self.title = self.title = f"