From e4d2229b8aec5231d50c43ce538c01837e92eadb Mon Sep 17 00:00:00 2001 From: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com> Date: Mon, 20 Jul 2026 00:21:15 -0700 Subject: [PATCH] refactor(ui): refactor PanelModal and PanelWidget into MVC Modal, ModalView, and ModalContent classes --- .../controllers/edit_field_template_modal.py | 4 +- .../qt/controllers/field_suggest_box.py | 13 +- .../field_template_search_panel_controller.py | 19 ++- src/tagstudio/qt/controllers/modal.py | 85 ++++++++++++ src/tagstudio/qt/controllers/modal_content.py | 47 +++++++ .../controllers/preview_panel_controller.py | 6 +- .../qt/controllers/search_panel_controller.py | 8 +- src/tagstudio/qt/controllers/suggest_box.py | 6 +- .../qt/controllers/tag_box_controller.py | 4 +- .../tag_search_panel_controller.py | 26 ++-- .../qt/controllers/tag_suggest_box.py | 11 +- src/tagstudio/qt/mixed/add_field.py | 98 -------------- src/tagstudio/qt/mixed/build_color.py | 8 +- src/tagstudio/qt/mixed/build_namespace.py | 8 +- src/tagstudio/qt/mixed/build_tag.py | 21 +-- src/tagstudio/qt/mixed/color_box.py | 4 +- src/tagstudio/qt/mixed/datetime_picker.py | 4 +- src/tagstudio/qt/mixed/field_containers.py | 6 +- src/tagstudio/qt/mixed/settings_panel.py | 13 +- src/tagstudio/qt/mixed/tag_color_manager.py | 6 +- src/tagstudio/qt/mixed/tag_color_selection.py | 17 +-- src/tagstudio/qt/ts_qt.py | 32 ++--- .../views/edit_field_template_modal_view.py | 4 +- src/tagstudio/qt/views/edit_text_view.py | 4 +- src/tagstudio/qt/views/ignore_modal_view.py | 4 +- src/tagstudio/qt/views/modal_view.py | 64 +++++++++ src/tagstudio/qt/views/panel_modal.py | 127 ------------------ src/tagstudio/qt/views/search_panel_view.py | 4 +- src/tagstudio/qt/views/suggest_box_view.py | 9 +- tests/qt/test_tag_panel.py | 5 +- 30 files changed, 312 insertions(+), 355 deletions(-) create mode 100644 src/tagstudio/qt/controllers/modal.py create mode 100644 src/tagstudio/qt/controllers/modal_content.py delete mode 100644 src/tagstudio/qt/mixed/add_field.py create mode 100644 src/tagstudio/qt/views/modal_view.py delete mode 100644 src/tagstudio/qt/views/panel_modal.py diff --git a/src/tagstudio/qt/controllers/edit_field_template_modal.py b/src/tagstudio/qt/controllers/edit_field_template_modal.py index 5ac7cf30..4a2a1174 100644 --- a/src/tagstudio/qt/controllers/edit_field_template_modal.py +++ b/src/tagstudio/qt/controllers/edit_field_template_modal.py @@ -73,8 +73,8 @@ class EditFieldTemplateModal(EditFieldTemplateModalView): self.name_field.setStyleSheet(line_edit_style() if is_empty else "") - if self.panel_save_button is not None: - self.panel_save_button.setDisabled(is_empty) + if self.save_button is not None: + self.save_button.setDisabled(is_empty) def __on_type_changed(self, index: int): old_type = self.__field_type diff --git a/src/tagstudio/qt/controllers/field_suggest_box.py b/src/tagstudio/qt/controllers/field_suggest_box.py index a0c7380e..2a20ab76 100644 --- a/src/tagstudio/qt/controllers/field_suggest_box.py +++ b/src/tagstudio/qt/controllers/field_suggest_box.py @@ -14,10 +14,11 @@ from tagstudio.core.library.alchemy.fields import BaseFieldTemplate from tagstudio.core.library.alchemy.library import Library from tagstudio.qt.controllers.edit_field_template_modal import EditFieldTemplateModal from tagstudio.qt.controllers.field_template_widget_controller import FieldTemplateWidget +from tagstudio.qt.controllers.modal import Modal +from tagstudio.qt.controllers.modal_content import ModalContent from tagstudio.qt.controllers.suggest_box import SuggestBox from tagstudio.qt.controllers.underlined_widget import UnderlinedWidget from tagstudio.qt.translations import Translations -from tagstudio.qt.views.panel_modal import PanelModal, PanelWidget if typing.TYPE_CHECKING: from tagstudio.qt.ts_qt import QtDriver @@ -58,7 +59,7 @@ class FieldSuggestBox(SuggestBox[BaseFieldTemplate]): # since the user needs to decide what type of field it should be before it's created. query: str = self.layout().search_field.text() panel = EditFieldTemplateModal() - modal = PanelModal( + modal = Modal( panel, Translations["field_template.new"], Translations["field_template.new"], @@ -73,9 +74,7 @@ class FieldSuggestBox(SuggestBox[BaseFieldTemplate]): @override def _on_item_edit(self, item: BaseFieldTemplate) -> None: panel: EditFieldTemplateModal = EditFieldTemplateModal(item) - modal: PanelModal = PanelModal( - panel, item.name, Translations["field_template.edit"], is_savable=True - ) + modal: Modal = Modal(panel, item.name, Translations["field_template.edit"], is_savable=True) modal.saved.connect(lambda: self._edit_item(panel)) modal.show() @@ -121,7 +120,7 @@ class FieldSuggestBox(SuggestBox[BaseFieldTemplate]): ) @override - def _create_item_from_modal(self, edit_item_panel: PanelWidget) -> None: + def _create_item_from_modal(self, edit_item_panel: ModalContent) -> None: if isinstance(edit_item_panel, EditFieldTemplateModal): template: BaseFieldTemplate = edit_item_panel.build_field_template() self._lib.add_field_template(template) @@ -132,7 +131,7 @@ class FieldSuggestBox(SuggestBox[BaseFieldTemplate]): self._on_search_query_changed(self.layout().search_field.text()) @override - def _edit_item(self, edit_item_panel: PanelWidget) -> None: + def _edit_item(self, edit_item_panel: ModalContent) -> None: if not isinstance(edit_item_panel, EditFieldTemplateModal): return 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 45c126ac..36d45e7b 100644 --- a/src/tagstudio/qt/controllers/field_template_search_panel_controller.py +++ b/src/tagstudio/qt/controllers/field_template_search_panel_controller.py @@ -13,15 +13,16 @@ from tagstudio.core.library.alchemy.fields import BaseFieldTemplate from tagstudio.core.library.alchemy.library import Library from tagstudio.qt.controllers.edit_field_template_modal import EditFieldTemplateModal from tagstudio.qt.controllers.field_template_widget_controller import FieldTemplateWidget +from tagstudio.qt.controllers.modal import Modal +from tagstudio.qt.controllers.modal_content import ModalContent from tagstudio.qt.controllers.search_panel_controller import SearchPanel from tagstudio.qt.translations import Translations from tagstudio.qt.views.field_template_search_panel_view import FieldTemplateSearchPanelView -from tagstudio.qt.views.panel_modal import PanelModal, PanelWidget logger = structlog.get_logger(__name__) -class FieldTemplateSearchModal(PanelModal): +class FieldTemplateSearchModal(Modal): def __init__( self, library: Library, @@ -33,11 +34,7 @@ class FieldTemplateSearchModal(PanelModal): is_field_template_chooser, view=FieldTemplateSearchPanelView(is_field_template_chooser), ) - super().__init__( - self.search_panel, - Translations["field.add.plural"], - is_savable=has_save, - ) + super().__init__(self.search_panel, Translations["field.add.plural"], is_savable=has_save) class FieldTemplateSearchPanel(SearchPanel[BaseFieldTemplate]): @@ -76,7 +73,7 @@ class FieldTemplateSearchPanel(SearchPanel[BaseFieldTemplate]): logger.info("[FieldTemplateSearch] Create and Add Field Template", name=query) panel: EditFieldTemplateModal = EditFieldTemplateModal() - modal: PanelModal = PanelModal( + modal: Modal = Modal( panel, Translations["field_template.new"], Translations["field_template.new"], @@ -93,7 +90,7 @@ class FieldTemplateSearchPanel(SearchPanel[BaseFieldTemplate]): def on_item_edit(self, item: BaseFieldTemplate) -> None: panel: EditFieldTemplateModal = EditFieldTemplateModal(item) - modal: PanelModal = PanelModal( + modal: Modal = Modal( panel, item.name, Translations["field_template.edit"], @@ -157,7 +154,7 @@ class FieldTemplateSearchPanel(SearchPanel[BaseFieldTemplate]): ) @override - def create_item(self, edit_item_panel: PanelWidget, choose_item: bool = False) -> None: + def create_item(self, edit_item_panel: ModalContent, choose_item: bool = False) -> None: if isinstance(edit_item_panel, EditFieldTemplateModal): template: BaseFieldTemplate = edit_item_panel.build_field_template() @@ -171,7 +168,7 @@ class FieldTemplateSearchPanel(SearchPanel[BaseFieldTemplate]): self.on_search_query_changed(self.get_search_query()) @override - def edit_item(self, edit_item_panel: PanelWidget) -> None: + def edit_item(self, edit_item_panel: ModalContent) -> None: if not isinstance(edit_item_panel, EditFieldTemplateModal): return diff --git a/src/tagstudio/qt/controllers/modal.py b/src/tagstudio/qt/controllers/modal.py new file mode 100644 index 00000000..f5417bba --- /dev/null +++ b/src/tagstudio/qt/controllers/modal.py @@ -0,0 +1,85 @@ +# SPDX-FileCopyrightText: (c) TagStudio Contributors +# SPDX-License-Identifier: GPL-3.0-only + + +import contextlib +from typing import Any, override + +import structlog +from PySide6 import QtGui +from PySide6.QtCore import Qt, Signal +from PySide6.QtWidgets import QWidget + +from tagstudio.qt.controllers.modal_content import ModalContent +from tagstudio.qt.views.modal_view import ModalView + +logger = structlog.get_logger(__name__) + + +class Modal(QWidget): + """A generic modal window widget with common signals and styling.""" + + done = Signal() + saved = Signal() + saved_data = Signal(type(Any)) + + def __init__( + self, + content_widget: ModalContent, + title: str = "", + window_title: str | None = None, + is_savable: bool = False, + inline_title: bool = True, + ): + super().__init__() + self.setWindowTitle(title if window_title is None else window_title) + self.setWindowModality(Qt.WindowModality.ApplicationModal) + self.setLayout( + ModalView( + content_widget=content_widget, + title=title, + is_savable=is_savable, + inline_title=inline_title, + ) + ) + + # [Done] + # - OR - + # [Cancel] [Save] + if not is_savable: + done_button = self.layout().content_widget.done_button + if done_button: + done_button.clicked.connect(self.hide) + done_button.clicked.connect(self.done.emit) + else: + cancel_button = self.layout().content_widget.cancel_button + if cancel_button: + cancel_button.clicked.connect(self.hide) + cancel_button.clicked.connect(content_widget.reset) + + save_button = self.layout().content_widget.save_button + if save_button: + save_button.clicked.connect(self.hide) + save_button.clicked.connect(self.saved.emit) + save_button.clicked.connect( + lambda: self.saved_data.emit(content_widget.saved_data()) + ) + + content_widget.parent_post_init() + + @override + def closeEvent(self, event: QtGui.QCloseEvent) -> None: + with contextlib.suppress(AttributeError): + cancel_button = self.layout().content_widget.cancel_button + if cancel_button: + cancel_button.click() + with contextlib.suppress(AttributeError): + done_button = self.layout().content_widget.done_button + if done_button: + done_button.click() + event.accept() + + @override + def layout(self) -> ModalView: + """Return the typed layout for this widget.""" + return super().layout() # pyright: ignore[reportReturnType] diff --git a/src/tagstudio/qt/controllers/modal_content.py b/src/tagstudio/qt/controllers/modal_content.py new file mode 100644 index 00000000..0a4348d9 --- /dev/null +++ b/src/tagstudio/qt/controllers/modal_content.py @@ -0,0 +1,47 @@ +# SPDX-FileCopyrightText: (c) TagStudio Contributors +# SPDX-License-Identifier: GPL-3.0-only + + +from typing import Any, override + +import structlog +from PySide6 import QtCore, QtGui +from PySide6.QtCore import Qt +from PySide6.QtWidgets import QPushButton, QWidget + +logger = structlog.get_logger(__name__) + + +class ModalContent(QWidget): + """Base class for widgets that go inside a Modal widget.""" + + save_button: QPushButton | None = None + cancel_button: QPushButton | None = None + done_button: QPushButton | None = None + + def __init__(self): + super().__init__() + + def saved_data(self) -> Any: # pyright: ignore[reportExplicitAny] + return None + + def reset(self) -> None: + pass + + def parent_post_init(self) -> None: + pass + + @override + def keyPressEvent(self, event: QtGui.QKeyEvent) -> None: + if event.key() == QtCore.Qt.Key.Key_Escape: + if self.cancel_button: + self.cancel_button.click() + elif self.done_button: + self.done_button.click() + elif event.key() == Qt.Key.Key_Return or event.key() == Qt.Key.Key_Enter: + if self.save_button: + self.save_button.click() + elif self.done_button: + self.done_button.click() + else: # Other key presses + super().keyPressEvent(event) diff --git a/src/tagstudio/qt/controllers/preview_panel_controller.py b/src/tagstudio/qt/controllers/preview_panel_controller.py index 4de14e41..2967f843 100644 --- a/src/tagstudio/qt/controllers/preview_panel_controller.py +++ b/src/tagstudio/qt/controllers/preview_panel_controller.py @@ -27,11 +27,11 @@ from tagstudio.core.library.alchemy.models import Entry from tagstudio.core.utils.ffmpeg_status import FfmpegStatus, FfprobeStatus from tagstudio.core.utils.types import unwrap from tagstudio.qt.controllers.edit_text_controller import EditText +from tagstudio.qt.controllers.modal import Modal from tagstudio.qt.mixed.datetime_picker import DatetimePicker from tagstudio.qt.mixed.field_containers import FieldContainers from tagstudio.qt.mixed.file_attributes import FileAttributeData from tagstudio.qt.translations import FIELD_TYPE_KEYS, Translations -from tagstudio.qt.views.panel_modal import PanelModal from tagstudio.qt.views.preview_panel_view import PreviewPanelView if typing.TYPE_CHECKING: @@ -183,7 +183,7 @@ class PreviewPanel(QWidget): field_name_key: str = FIELD_TYPE_KEYS.get(field.class_name, "field_type.unknown") if type(field) is TextField: - edit_modal = PanelModal( + edit_modal = Modal( EditText(field.name, field.value, field.is_multiline), window_title=f"{Translations['field.edit']} ({Translations[field_name_key]})", is_savable=True, @@ -194,7 +194,7 @@ class PreviewPanel(QWidget): ) edit_modal.show() elif type(field) is DatetimeField: - edit_modal = PanelModal( + edit_modal = Modal( DatetimePicker(self._driver, field.name, field.value or dt.now()), window_title=f"{Translations['field.edit']} ({Translations[field_name_key]})", is_savable=True, diff --git a/src/tagstudio/qt/controllers/search_panel_controller.py b/src/tagstudio/qt/controllers/search_panel_controller.py index bcf535ec..f26f3e33 100644 --- a/src/tagstudio/qt/controllers/search_panel_controller.py +++ b/src/tagstudio/qt/controllers/search_panel_controller.py @@ -11,8 +11,8 @@ from PySide6.QtGui import QShowEvent from PySide6.QtWidgets import QVBoxLayout, QWidget from tagstudio.core.library.alchemy.library import Library +from tagstudio.qt.controllers.modal_content import ModalContent from tagstudio.qt.translations import Translations -from tagstudio.qt.views.panel_modal import PanelWidget from tagstudio.qt.views.search_panel_view import SearchPanelView logger = structlog.get_logger(__name__) @@ -40,7 +40,7 @@ def _item_name(item: object) -> str: raise AttributeError() -class SearchPanel[T](PanelWidget): +class SearchPanel[T](ModalContent): item_chosen = Signal(int) def __init__( @@ -234,8 +234,8 @@ class SearchPanel[T](PanelWidget): else: self.view.focus_search_box(select_all=True) - def create_item(self, edit_item_panel: PanelWidget, choose_item: bool = False) -> None: # pyright: ignore[reportUnusedParameter] + def create_item(self, edit_item_panel: ModalContent, choose_item: bool = False) -> None: # pyright: ignore[reportUnusedParameter] raise NotImplementedError() - def edit_item(self, edit_item_panel: PanelWidget) -> None: # pyright: ignore[reportUnusedParameter] + def edit_item(self, edit_item_panel: ModalContent) -> None: # pyright: ignore[reportUnusedParameter] raise NotImplementedError() diff --git a/src/tagstudio/qt/controllers/suggest_box.py b/src/tagstudio/qt/controllers/suggest_box.py index 1f80ff4e..34346e45 100644 --- a/src/tagstudio/qt/controllers/suggest_box.py +++ b/src/tagstudio/qt/controllers/suggest_box.py @@ -10,8 +10,8 @@ from PySide6.QtWidgets import QGraphicsOpacityEffect, QWidget from tagstudio.core.library.alchemy.library import Library from tagstudio.qt.controllers.autofill_line_edit import QtCore, QtGui +from tagstudio.qt.controllers.modal_content import ModalContent from tagstudio.qt.controllers.underlined_widget import UnderlinedWidget -from tagstudio.qt.views.panel_modal import PanelWidget from tagstudio.qt.views.stylesheets.stylesheets import ( autofill_line_edit_style, autofill_line_edit_top_style, @@ -200,10 +200,10 @@ class SuggestBox[T](QWidget): self.done.emit() self.hide_and_reset() - def _create_item_from_modal(self, edit_item_panel: PanelWidget) -> None: # pyright: ignore[reportUnusedParameter] + def _create_item_from_modal(self, edit_item_panel: ModalContent) -> None: # pyright: ignore[reportUnusedParameter] raise NotImplementedError() - def _edit_item(self, edit_item_panel: PanelWidget) -> None: # pyright: ignore[reportUnusedParameter] + def _edit_item(self, edit_item_panel: ModalContent) -> None: # pyright: ignore[reportUnusedParameter] raise NotImplementedError() @override diff --git a/src/tagstudio/qt/controllers/tag_box_controller.py b/src/tagstudio/qt/controllers/tag_box_controller.py index 313f19cb..11dc2a91 100644 --- a/src/tagstudio/qt/controllers/tag_box_controller.py +++ b/src/tagstudio/qt/controllers/tag_box_controller.py @@ -12,8 +12,8 @@ from tagstudio.core.enums import TagClickActionOption from tagstudio.core.library.alchemy.enums import BrowsingState from tagstudio.core.library.alchemy.models import Tag from tagstudio.core.utils.types import unwrap +from tagstudio.qt.controllers.modal import Modal 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 if TYPE_CHECKING: @@ -74,7 +74,7 @@ class TagBoxWidget(TagBoxWidgetView): def _on_edit(self, tag: Tag) -> None: build_tag_panel = BuildTagPanel(self.__driver.lib, tag=tag) - edit_modal = PanelModal( + edit_modal = Modal( build_tag_panel, self.__driver.lib.tag_display_name(tag), "Edit Tag", diff --git a/src/tagstudio/qt/controllers/tag_search_panel_controller.py b/src/tagstudio/qt/controllers/tag_search_panel_controller.py index 5a0e01e3..242e1241 100644 --- a/src/tagstudio/qt/controllers/tag_search_panel_controller.py +++ b/src/tagstudio/qt/controllers/tag_search_panel_controller.py @@ -12,18 +12,17 @@ from tagstudio.core.constants import RESERVED_TAG_END, RESERVED_TAG_START from tagstudio.core.library.alchemy.enums import BrowsingState from tagstudio.core.library.alchemy.library import Library from tagstudio.core.library.alchemy.models import Tag +from tagstudio.qt.controllers.modal import Modal +from tagstudio.qt.controllers.modal_content import ModalContent from tagstudio.qt.controllers.search_panel_controller import SearchPanel from tagstudio.qt.mixed.tag_widget import TagWidget from tagstudio.qt.translations import Translations -from tagstudio.qt.views.panel_modal import PanelModal, PanelWidget from tagstudio.qt.views.tag_search_panel_view import TagSearchPanelView logger = structlog.get_logger(__name__) -class TagSearchModal(PanelModal): - tsp: "TagSearchPanel" - +class TagSearchModal(Modal): def __init__( self, library: Library, @@ -33,16 +32,9 @@ class TagSearchModal(PanelModal): has_save: bool = False, ): self.tsp = TagSearchPanel( - library, - exclude, - is_tag_chooser, - view=TagSearchPanelView(is_tag_chooser), - ) - super().__init__( - widget=self.tsp, - title=title, - is_savable=has_save, + library, exclude, is_tag_chooser, view=TagSearchPanelView(is_tag_chooser) ) + super().__init__(content_widget=self.tsp, title=title, is_savable=has_save) class TagSearchPanel(SearchPanel[Tag]): @@ -82,7 +74,7 @@ class TagSearchPanel(SearchPanel[Tag]): query: str = self.get_search_query() panel: BuildTagPanel = BuildTagPanel(self.__lib) - modal: PanelModal = PanelModal( + modal: Modal = Modal( panel, Translations["tag.new"], Translations["tag.add"] if add_to_entry else Translations["tag.new"], @@ -101,7 +93,7 @@ class TagSearchPanel(SearchPanel[Tag]): from tagstudio.qt.mixed.build_tag import BuildTagPanel # here due to circular imports edit_tag_panel: BuildTagPanel = BuildTagPanel(self.__lib, tag=item) - edit_tag_modal: PanelModal = PanelModal( + edit_tag_modal: Modal = Modal( edit_tag_panel, self.__lib.tag_display_name(item), Translations["tag.edit"], @@ -188,7 +180,7 @@ class TagSearchPanel(SearchPanel[Tag]): tag_widget.search_for_tag_action.setEnabled(False) @override - def create_item(self, edit_item_panel: PanelWidget, choose_item: bool = False) -> None: + def create_item(self, edit_item_panel: ModalContent, choose_item: bool = False) -> None: # TODO: Move this to a top-level import from tagstudio.qt.mixed.build_tag import BuildTagPanel # here due to circular imports @@ -206,7 +198,7 @@ class TagSearchPanel(SearchPanel[Tag]): self.on_search_query_changed(self.get_search_query()) @override - def edit_item(self, edit_item_panel: PanelWidget) -> None: + def edit_item(self, edit_item_panel: ModalContent) -> None: # TODO: Move this to a top-level import from tagstudio.qt.mixed.build_tag import BuildTagPanel # here due to circular imports diff --git a/src/tagstudio/qt/controllers/tag_suggest_box.py b/src/tagstudio/qt/controllers/tag_suggest_box.py index d0db7d04..8ff0cd06 100644 --- a/src/tagstudio/qt/controllers/tag_suggest_box.py +++ b/src/tagstudio/qt/controllers/tag_suggest_box.py @@ -13,12 +13,13 @@ from PySide6.QtWidgets import QGraphicsOpacityEffect, QWidget from tagstudio.core.library.alchemy.enums import BrowsingState from tagstudio.core.library.alchemy.library import Library from tagstudio.core.library.alchemy.models import Tag +from tagstudio.qt.controllers.modal import Modal +from tagstudio.qt.controllers.modal_content import ModalContent from tagstudio.qt.controllers.suggest_box import SuggestBox from tagstudio.qt.controllers.underlined_widget import UnderlinedWidget from tagstudio.qt.mixed.build_tag import BuildTagPanel from tagstudio.qt.mixed.tag_widget import TagWidget from tagstudio.qt.translations import Translations -from tagstudio.qt.views.panel_modal import PanelModal, PanelWidget if typing.TYPE_CHECKING: from tagstudio.qt.ts_qt import QtDriver @@ -66,7 +67,7 @@ class TagSuggestBox(SuggestBox[Tag]): if self._driver.settings.edit_tag_on_create: panel: BuildTagPanel = BuildTagPanel(self._lib) - modal: PanelModal = PanelModal( + modal: Modal = Modal( panel, Translations["tag.new"], Translations["tag.new"], is_savable=True ) if query.strip(): @@ -83,7 +84,7 @@ class TagSuggestBox(SuggestBox[Tag]): @override def _on_item_edit(self, item: Tag) -> None: edit_tag_panel: BuildTagPanel = BuildTagPanel(self._lib, tag=item) - edit_tag_modal: PanelModal = PanelModal( + edit_tag_modal: Modal = Modal( edit_tag_panel, self._lib.tag_display_name(item), Translations["tag.edit"], @@ -143,7 +144,7 @@ class TagSuggestBox(SuggestBox[Tag]): tag_widget.search_for_tag_action.setEnabled(True) @override - def _create_item_from_modal(self, edit_item_panel: PanelWidget) -> None: + def _create_item_from_modal(self, edit_item_panel: ModalContent) -> None: if isinstance(edit_item_panel, BuildTagPanel): tag: Tag = edit_item_panel.build_tag() self._lib.add_tag( @@ -156,7 +157,7 @@ class TagSuggestBox(SuggestBox[Tag]): self._on_search_query_changed(self.layout().search_field.text()) @override - def _edit_item(self, edit_item_panel: PanelWidget) -> None: + def _edit_item(self, edit_item_panel: ModalContent) -> None: if not isinstance(edit_item_panel, BuildTagPanel): return diff --git a/src/tagstudio/qt/mixed/add_field.py b/src/tagstudio/qt/mixed/add_field.py deleted file mode 100644 index 946f4d74..00000000 --- a/src/tagstudio/qt/mixed/add_field.py +++ /dev/null @@ -1,98 +0,0 @@ -# SPDX-FileCopyrightText: (c) TagStudio Contributors -# SPDX-License-Identifier: GPL-3.0-only - - -from typing import override - -import structlog -from PySide6 import QtCore, QtGui -from PySide6.QtCore import Qt, Signal -from PySide6.QtWidgets import ( - QHBoxLayout, - QLabel, - QListWidget, - QListWidgetItem, - QPushButton, - QVBoxLayout, - QWidget, -) - -from tagstudio.core.library.alchemy.library import Library -from tagstudio.qt.translations import FIELD_TYPE_KEYS, Translations -from tagstudio.qt.views.stylesheets.stylesheets import header - -logger = structlog.get_logger(__name__) - - -# NOTE: This class doesn't inherit from PanelWidget? Seems like it predates that system? -class AddFieldModal(QWidget): - done = Signal(list) - - def __init__(self, library: Library): - # [Done] - # - OR - - # [Cancel] [Save] - super().__init__() - self.lib = library - self.setWindowTitle(Translations["field.add"]) - self.setWindowModality(Qt.WindowModality.ApplicationModal) - self.setMinimumSize(400, 300) - self.root_layout = QVBoxLayout(self) - self.root_layout.setContentsMargins(6, 6, 6, 6) - - self.title_widget = QLabel(header(Translations["field.add"], 3)) - self.title_widget.setObjectName("fieldTitle") - self.title_widget.setWordWrap(True) - self.title_widget.setAlignment(Qt.AlignmentFlag.AlignCenter) - - self.list_widget = QListWidget() - - 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(Translations["generic.cancel"]) - self.cancel_button.clicked.connect(self.hide) - self.button_layout.addWidget(self.cancel_button) - - self.save_button = QPushButton(Translations["generic.add"]) - self.save_button.setDefault(True) - self.save_button.clicked.connect(self.hide) - self.save_button.clicked.connect( - lambda: ( - # get userData for each selected item - self.done.emit(self.list_widget.selectedItems()) - ) - ) - self.button_layout.addWidget(self.save_button) - - self.root_layout.addWidget(self.title_widget) - self.root_layout.addWidget(self.list_widget) - self.root_layout.addStretch(1) - self.root_layout.addWidget(self.button_container) - - @override - def show(self): - self.list_widget.clear() - for field_template in self.lib.field_templates: - field_name_key: str = FIELD_TYPE_KEYS.get( - field_template.class_name, "field_type.unknown" - ) - item = QListWidgetItem(f"{field_template.name} ({Translations[field_name_key]})") - item.setData(Qt.ItemDataRole.UserRole, field_template) - self.list_widget.addItem(item) - self.list_widget.setFocus() - self.list_widget.setCurrentRow(0) - - super().show() - - @override - def keyPressEvent(self, event: QtGui.QKeyEvent) -> None: # noqa N802 - if event.key() == QtCore.Qt.Key.Key_Escape: - self.cancel_button.click() - elif event.key() in (QtCore.Qt.Key.Key_Enter, QtCore.Qt.Key.Key_Return): - self.save_button.click() - else: # Other key presses - pass - return super().keyPressEvent(event) diff --git a/src/tagstudio/qt/mixed/build_color.py b/src/tagstudio/qt/mixed/build_color.py index 9791aaa7..b8590dfd 100644 --- a/src/tagstudio/qt/mixed/build_color.py +++ b/src/tagstudio/qt/mixed/build_color.py @@ -24,10 +24,10 @@ from tagstudio.core.library.alchemy.enums import TagColorEnum from tagstudio.core.library.alchemy.library import Library, slugify from tagstudio.core.library.alchemy.models import TagColorGroup from tagstudio.core.utils.types import unwrap +from tagstudio.qt.controllers.modal_content import ModalContent from tagstudio.qt.mixed.tag_color_preview import TagColorPreview from tagstudio.qt.models.palette import ColorType, get_tag_color from tagstudio.qt.translations import Translations -from tagstudio.qt.views.panel_modal import PanelWidget from tagstudio.qt.views.stylesheets.stylesheets import ( checkbox_style, line_edit_style, @@ -37,7 +37,7 @@ from tagstudio.qt.views.stylesheets.stylesheets import ( logger = structlog.get_logger(__name__) -class BuildColorPanel(PanelWidget): +class BuildColorPanel(ModalContent): on_edit = Signal(TagColorGroup) def __init__(self, library: Library, color_group: TagColorGroup): @@ -254,8 +254,8 @@ class BuildColorPanel(PanelWidget): self.slug_field.setText(slug) self.update_preview_text() - if self.panel_save_button is not None: - self.panel_save_button.setDisabled(is_name_empty) + if self.save_button is not None: + self.save_button.setDisabled(is_name_empty) def build_color(self) -> tuple[TagColorGroup, TagColorGroup]: name = self.name_field.text() diff --git a/src/tagstudio/qt/mixed/build_namespace.py b/src/tagstudio/qt/mixed/build_namespace.py index 36487a7b..0fc8bc9d 100644 --- a/src/tagstudio/qt/mixed/build_namespace.py +++ b/src/tagstudio/qt/mixed/build_namespace.py @@ -13,14 +13,14 @@ from PySide6.QtWidgets import QLabel, QLineEdit, QVBoxLayout, QWidget from tagstudio.core.constants import RESERVED_NAMESPACE_PREFIX from tagstudio.core.library.alchemy.library import Library, ReservedNamespaceError, slugify from tagstudio.core.library.alchemy.models import Namespace +from tagstudio.qt.controllers.modal_content import ModalContent from tagstudio.qt.translations import Translations -from tagstudio.qt.views.panel_modal import PanelWidget from tagstudio.qt.views.stylesheets.stylesheets import line_edit_style logger = structlog.get_logger(__name__) -class BuildNamespacePanel(PanelWidget): +class BuildNamespacePanel(ModalContent): on_edit = Signal(Namespace) def __init__(self, library: Library, namespace: Namespace | None = None): @@ -118,8 +118,8 @@ class BuildNamespacePanel(PanelWidget): self.slug_field.setText(slug) - if self.panel_save_button is not None: - self.panel_save_button.setDisabled(is_name_empty) + if self.save_button is not None: + self.save_button.setDisabled(is_name_empty) def no_collide(self, slug: str) -> str: """Return a slug name that's verified not to collide with other known namespace slugs.""" diff --git a/src/tagstudio/qt/mixed/build_tag.py b/src/tagstudio/qt/mixed/build_tag.py index d590902b..1a73976f 100644 --- a/src/tagstudio/qt/mixed/build_tag.py +++ b/src/tagstudio/qt/mixed/build_tag.py @@ -28,12 +28,13 @@ from PySide6.QtWidgets import ( from tagstudio.core.library.alchemy.library import Library from tagstudio.core.library.alchemy.models import Tag, TagAlias, TagColorGroup from tagstudio.core.utils.types import unwrap +from tagstudio.qt.controllers.modal import Modal +from tagstudio.qt.controllers.modal_content import ModalContent from tagstudio.qt.controllers.tag_search_panel_controller import TagSearchModal from tagstudio.qt.mixed.tag_color_preview import TagColorPreview from tagstudio.qt.mixed.tag_color_selection import TagColorSelection from tagstudio.qt.mixed.tag_widget import TagWidget from tagstudio.qt.translations import Translations -from tagstudio.qt.views.panel_modal import PanelModal, PanelWidget from tagstudio.qt.views.stylesheets.stylesheets import ( checkbox_style, colored_radio_button_style, @@ -73,7 +74,7 @@ class CustomTableItem(QLineEdit): super().keyPressEvent(arg__1) -class BuildTagPanel(PanelWidget): +class BuildTagPanel(ModalContent): on_edit = Signal(Tag) def __init__(self, library: Library, tag: Tag | None = None) -> None: @@ -201,7 +202,7 @@ class BuildTagPanel(PanelWidget): self.color_button = TagColorPreview(self.lib, None) self.tag_color_selection = TagColorSelection(self.lib) chose_tag_color_title = Translations["tag.choose_color"] - self.choose_color_modal = PanelModal( + self.choose_color_modal = Modal( self.tag_color_selection, chose_tag_color_title, chose_tag_color_title ) self.choose_color_modal.done.connect( @@ -372,7 +373,7 @@ class BuildTagPanel(PanelWidget): def on_parent_tag_edit(tag: Tag) -> None: build_tag_panel = BuildTagPanel(self.lib, tag=tag) - edit_modal = PanelModal( + edit_modal = Modal( build_tag_panel, self.lib.tag_display_name(tag), "Edit Tag", @@ -422,7 +423,7 @@ class BuildTagPanel(PanelWidget): while self.aliases_table.rowCount() > 0: self.aliases_table.removeRow(0) - last: QWidget | None = self.panel_save_button + last: QWidget | None = self.save_button aliases = list(self.aliases) alias_names = [a.name for a in aliases] sorted_aliases = sorted(aliases, key=lambda x: alias_names[aliases.index(x)]) @@ -493,8 +494,8 @@ class BuildTagPanel(PanelWidget): is_empty = not self.name_field.text().strip() self.name_field.setStyleSheet(line_edit_style() if is_empty else "") - if self.panel_save_button is not None: - self.panel_save_button.setDisabled(is_empty) + if self.save_button is not None: + self.save_button.setDisabled(is_empty) def build_tag(self) -> Tag: tag = self.tag @@ -515,8 +516,8 @@ class BuildTagPanel(PanelWidget): self.setTabOrder(self.shorthand_field, self.aliases_add_button) self.setTabOrder(self.aliases_add_button, self.parent_tags_add_button) self.setTabOrder(self.parent_tags_add_button, self.color_button) - self.setTabOrder(self.color_button, unwrap(self.panel_cancel_button)) - self.setTabOrder(unwrap(self.panel_cancel_button), unwrap(self.panel_save_button)) - self.setTabOrder(unwrap(self.panel_save_button), self.aliases_table.cellWidget(0, 1)) + self.setTabOrder(self.color_button, unwrap(self.cancel_button)) + self.setTabOrder(unwrap(self.cancel_button), unwrap(self.save_button)) + self.setTabOrder(unwrap(self.save_button), self.aliases_table.cellWidget(0, 1)) self.name_field.selectAll() self.name_field.setFocus() diff --git a/src/tagstudio/qt/mixed/color_box.py b/src/tagstudio/qt/mixed/color_box.py index 88771bbd..ffa427d4 100644 --- a/src/tagstudio/qt/mixed/color_box.py +++ b/src/tagstudio/qt/mixed/color_box.py @@ -12,12 +12,12 @@ from PySide6.QtWidgets import QMessageBox, QPushButton from tagstudio.core.constants import RESERVED_NAMESPACE_PREFIX from tagstudio.core.library.alchemy.models import TagColorGroup from tagstudio.core.utils.types import unwrap +from tagstudio.qt.controllers.modal import Modal 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.translations import Translations from tagstudio.qt.views.layouts.flow_layout import FlowLayout -from tagstudio.qt.views.panel_modal import PanelModal from tagstudio.qt.views.stylesheets.stylesheets import add_button_style if typing.TYPE_CHECKING: @@ -102,7 +102,7 @@ class ColorBoxWidget(FieldWidget): def edit_color(self, color_group: TagColorGroup): build_color_panel = BuildColorPanel(self.lib, color_group) - self.edit_modal = PanelModal( + self.edit_modal = Modal( build_color_panel, "Edit Color", is_savable=True, diff --git a/src/tagstudio/qt/mixed/datetime_picker.py b/src/tagstudio/qt/mixed/datetime_picker.py index 1706eaa5..aa2a7a4e 100644 --- a/src/tagstudio/qt/mixed/datetime_picker.py +++ b/src/tagstudio/qt/mixed/datetime_picker.py @@ -9,7 +9,7 @@ from typing import cast, override from PySide6.QtCore import QDateTime from PySide6.QtWidgets import QDateTimeEdit, QLineEdit, QVBoxLayout -from tagstudio.qt.views.panel_modal import PanelWidget +from tagstudio.qt.controllers.modal_content import ModalContent from tagstudio.qt.views.stylesheets.stylesheets import title_line_edit_style if typing.TYPE_CHECKING: @@ -39,7 +39,7 @@ def qdtf2dtf(dtf: str) -> str: return out -class DatetimePicker(PanelWidget): +class DatetimePicker(ModalContent): def __init__(self, driver: "QtDriver", name: str, datetime: dt | str): super().__init__() self.setMinimumSize(300, 60) diff --git a/src/tagstudio/qt/mixed/field_containers.py b/src/tagstudio/qt/mixed/field_containers.py index a71fd385..1a6ae897 100644 --- a/src/tagstudio/qt/mixed/field_containers.py +++ b/src/tagstudio/qt/mixed/field_containers.py @@ -30,12 +30,12 @@ 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.modal import Modal 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 TextContainerWidget from tagstudio.qt.translations import FIELD_TYPE_KEYS, Translations -from tagstudio.qt.views.panel_modal import PanelModal from tagstudio.qt.views.stylesheets.stylesheets import inset_container_style if typing.TYPE_CHECKING: @@ -291,7 +291,7 @@ class FieldContainers(QWidget): container.set_inner_widget(inner_widget) if not is_mixed: - edit_modal = PanelModal( + edit_modal = Modal( EditText(field.name, field.value, field.is_multiline), window_title=f"{Translations['field.edit']} ({Translations[field_name_key]})", is_savable=True, @@ -329,7 +329,7 @@ class FieldContainers(QWidget): container.set_inner_widget(inner_widget) if not is_mixed: - edit_modal = PanelModal( + edit_modal = Modal( DatetimePicker(self.driver, field.name, field.value or dt.now()), window_title=f"{Translations['field.edit']} ({Translations[field_name_key]})", is_savable=True, diff --git a/src/tagstudio/qt/mixed/settings_panel.py b/src/tagstudio/qt/mixed/settings_panel.py index 380e848e..c43f843c 100644 --- a/src/tagstudio/qt/mixed/settings_panel.py +++ b/src/tagstudio/qt/mixed/settings_panel.py @@ -20,6 +20,8 @@ from PySide6.QtWidgets import ( ) from tagstudio.core.enums import ShowFilepathOption, TagClickActionOption +from tagstudio.qt.controllers.modal import Modal +from tagstudio.qt.controllers.modal_content import ModalContent from tagstudio.qt.global_settings import ( DEFAULT_CACHED_THUMB_RES, DEFAULT_THUMB_CACHE_SIZE, @@ -30,7 +32,6 @@ from tagstudio.qt.global_settings import ( Theme, ) from tagstudio.qt.translations import DEFAULT_TRANSLATION, LANGUAGES, Translations -from tagstudio.qt.views.panel_modal import PanelModal, PanelWidget if TYPE_CHECKING: from tagstudio.qt.ts_qt import QtDriver @@ -38,7 +39,7 @@ if TYPE_CHECKING: logger = structlog.get_logger(__name__) -class SettingsPanel(PanelWidget): +class SettingsPanel(ModalContent): driver: "QtDriver" filepath_option_map: dict[ShowFilepathOption, str] = { @@ -427,15 +428,15 @@ class SettingsPanel(PanelWidget): ) @classmethod - def build_modal(cls, driver: "QtDriver") -> PanelModal: + def build_modal(cls, driver: "QtDriver") -> Modal: settings_panel = cls(driver) - modal = PanelModal( - widget=settings_panel, + modal = Modal( + content_widget=settings_panel, window_title=Translations["settings.title"], is_savable=True, ) modal.saved.connect(lambda: settings_panel.update_settings(driver)) - modal.title_widget.setVisible(False) + modal.layout().title_label.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 6b51b92c..20d78915 100644 --- a/src/tagstudio/qt/mixed/tag_color_manager.py +++ b/src/tagstudio/qt/mixed/tag_color_manager.py @@ -23,11 +23,11 @@ from PySide6.QtWidgets import ( from tagstudio.core.constants import RESERVED_NAMESPACE_PREFIX from tagstudio.core.enums import Theme +from tagstudio.qt.controllers.modal import Modal from tagstudio.qt.mixed.build_namespace import BuildNamespacePanel from tagstudio.qt.mixed.color_box import ColorBoxWidget from tagstudio.qt.mixed.field_widget import FieldContainer from tagstudio.qt.translations import Translations -from tagstudio.qt.views.panel_modal import PanelModal from tagstudio.qt.views.stylesheets.stylesheets import header logger = structlog.get_logger(__name__) @@ -38,7 +38,7 @@ if TYPE_CHECKING: class TagColorManager(QWidget): - create_namespace_modal: PanelModal | None = None + create_namespace_modal: Modal | None = None def __init__( self, @@ -174,7 +174,7 @@ class TagColorManager(QWidget): def create_namespace(self): build_namespace_panel = BuildNamespacePanel(self.lib) - self.create_namespace_modal = PanelModal( + self.create_namespace_modal = Modal( build_namespace_panel, Translations["namespace.create.title"], is_savable=True, diff --git a/src/tagstudio/qt/mixed/tag_color_selection.py b/src/tagstudio/qt/mixed/tag_color_selection.py index 35bc2c2e..cbc823d7 100644 --- a/src/tagstudio/qt/mixed/tag_color_selection.py +++ b/src/tagstudio/qt/mixed/tag_color_selection.py @@ -20,21 +20,22 @@ from PySide6.QtWidgets import ( from tagstudio.core.library.alchemy.enums import TagColorEnum from tagstudio.core.library.alchemy.library import Library from tagstudio.core.library.alchemy.models import TagColorGroup -from tagstudio.qt.mixed.tag_widget import ( - get_tag_border_color, - get_tag_highlight_color, - get_tag_text_color, -) +from tagstudio.qt.controllers.modal_content import ModalContent 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 PanelWidget -from tagstudio.qt.views.stylesheets.stylesheets import color_swatch_style, header +from tagstudio.qt.views.stylesheets.stylesheets import ( + color_swatch_style, + get_tag_border_color, + get_tag_highlight_color, + get_tag_text_color, + header, +) logger = structlog.get_logger(__name__) -class TagColorSelection(PanelWidget): +class TagColorSelection(ModalContent): def __init__(self, library: Library): super().__init__() self.lib = library diff --git a/src/tagstudio/qt/ts_qt.py b/src/tagstudio/qt/ts_qt.py index 52a25c79..8a7d356d 100644 --- a/src/tagstudio/qt/ts_qt.py +++ b/src/tagstudio/qt/ts_qt.py @@ -61,6 +61,7 @@ from tagstudio.qt.controllers.field_template_search_panel_controller import Fiel 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 +from tagstudio.qt.controllers.modal import Modal from tagstudio.qt.controllers.tag_search_panel_controller import TagSearchModal from tagstudio.qt.controllers.update_available_message_box import UpdateAvailableMessageBox from tagstudio.qt.global_settings import DEFAULT_GLOBAL_SETTINGS_PATH, GlobalSettings, Theme @@ -84,7 +85,6 @@ from tagstudio.qt.utils.file_deleter import delete_file from tagstudio.qt.utils.function_iterator import FunctionIterator from tagstudio.qt.views.field_template_search_panel_view import FieldTemplateSearchPanelView from tagstudio.qt.views.main_window import MainWindow -from tagstudio.qt.views.panel_modal import PanelModal from tagstudio.qt.views.splash import SplashScreen from tagstudio.qt.views.stylesheets.stylesheets import header @@ -167,12 +167,12 @@ class QtDriver(DriverMixin, QObject): favorite_updated = Signal(bool) archived_updated = Signal(bool) - tag_manager_panel: PanelModal | None = None + tag_manager: Modal | None = None color_manager_panel: TagColorManager | None = None - field_template_manager_panel: PanelModal | None = None - ignore_modal: PanelModal | None = None - add_tag_modal: PanelModal | None = None - add_field_modal: PanelModal | None = None + field_template_manager: Modal | None = None + ignore_modal: Modal | None = None + add_tag_modal: Modal | None = None + add_field_modal: Modal | None = None folders_modal: FoldersToTagsModal about_modal: AboutModal unlinked_modal: FixUnlinkedEntriesModal @@ -365,14 +365,14 @@ class QtDriver(DriverMixin, QObject): self.app.setDesktopFileName("tagstudio") # Initialize the Tag Manager panel - self.tag_manager_panel = TagSearchModal( + self.tag_manager = TagSearchModal( self.lib, title=Translations["tag_manager.title"], is_tag_chooser=False, ) - self.tag_manager_panel.tsp.set_driver(self) + self.tag_manager.tsp.set_driver(self) - self.tag_manager_panel.done.connect( + self.tag_manager.done.connect( lambda checked=False: self.main_window.preview_panel.set_selection( self.selected, update_preview=False ) @@ -382,8 +382,8 @@ class QtDriver(DriverMixin, QObject): self.color_manager_panel = TagColorManager(self) # Initialize the Field Template Manager panel - self.field_template_manager_panel = PanelModal( - widget=FieldTemplateSearchPanel( + self.field_template_manager = Modal( + content_widget=FieldTemplateSearchPanel( self.lib, is_field_template_chooser=False, view=FieldTemplateSearchPanelView(is_field_template_chooser=False), @@ -391,7 +391,7 @@ class QtDriver(DriverMixin, QObject): title=Translations["field_template_manager.title"], is_savable=False, ) - self.field_template_manager_panel.done.connect( + self.field_template_manager.done.connect( lambda checked=False: self.main_window.preview_panel.set_selection( self.selected, update_preview=False ) @@ -486,14 +486,14 @@ class QtDriver(DriverMixin, QObject): lambda f="": self.delete_files_callback(f) ) - self.main_window.menu_bar.tag_manager_action.triggered.connect(self.tag_manager_panel.show) + self.main_window.menu_bar.tag_manager_action.triggered.connect(self.tag_manager.show) self.main_window.menu_bar.color_manager_action.triggered.connect( self.color_manager_panel.show ) self.main_window.menu_bar.field_template_manager_action.triggered.connect( - self.field_template_manager_panel.show + self.field_template_manager.show ) # endregion @@ -733,7 +733,7 @@ class QtDriver(DriverMixin, QObject): self.ignore_modal = None panel = IgnoreModal(self.lib) - self.ignore_modal = PanelModal( + self.ignore_modal = Modal( panel, Translations["menu.edit.ignore_files"], is_savable=True, @@ -871,7 +871,7 @@ class QtDriver(DriverMixin, QObject): def add_tag_action_callback(self): panel = BuildTagPanel(self.lib) - self.modal = PanelModal( + self.modal = Modal( panel, Translations["tag.new"], Translations["tag.create"], 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 e4dafd67..944dffa0 100644 --- a/src/tagstudio/qt/views/edit_field_template_modal_view.py +++ b/src/tagstudio/qt/views/edit_field_template_modal_view.py @@ -15,14 +15,14 @@ from PySide6.QtWidgets import ( ) from tagstudio.qt.controllers.clickable_label import ClickableLabel +from tagstudio.qt.controllers.modal_content import ModalContent 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__) -class EditFieldTemplateModalView(PanelWidget): +class EditFieldTemplateModalView(ModalContent): def __init__(self) -> None: super().__init__() diff --git a/src/tagstudio/qt/views/edit_text_view.py b/src/tagstudio/qt/views/edit_text_view.py index 1ec7ed33..7fbc53e0 100644 --- a/src/tagstudio/qt/views/edit_text_view.py +++ b/src/tagstudio/qt/views/edit_text_view.py @@ -14,12 +14,12 @@ from PySide6.QtWidgets import ( ) from tagstudio.qt.controllers.clickable_label import ClickableLabel +from tagstudio.qt.controllers.modal_content import ModalContent from tagstudio.qt.translations import Translations -from tagstudio.qt.views.panel_modal import PanelWidget from tagstudio.qt.views.stylesheets.stylesheets import checkbox_style, title_line_edit_style -class EditTextView(PanelWidget): +class EditTextView(ModalContent): def __init__(self): super().__init__() self.setMinimumSize(480, 240) diff --git a/src/tagstudio/qt/views/ignore_modal_view.py b/src/tagstudio/qt/views/ignore_modal_view.py index 277f5eea..dac325d4 100644 --- a/src/tagstudio/qt/views/ignore_modal_view.py +++ b/src/tagstudio/qt/views/ignore_modal_view.py @@ -14,13 +14,13 @@ from PySide6.QtWidgets import ( from tagstudio.core.constants import IGNORE_NAME from tagstudio.core.library.alchemy.library import Library from tagstudio.core.library.alchemy.models import Tag +from tagstudio.qt.controllers.modal_content import ModalContent from tagstudio.qt.translations import Translations -from tagstudio.qt.views.panel_modal import PanelWidget logger = structlog.get_logger(__name__) -class IgnoreModalView(PanelWidget): +class IgnoreModalView(ModalContent): on_edit = Signal(Tag) def __init__(self, library: Library) -> None: diff --git a/src/tagstudio/qt/views/modal_view.py b/src/tagstudio/qt/views/modal_view.py new file mode 100644 index 00000000..b9fc1aba --- /dev/null +++ b/src/tagstudio/qt/views/modal_view.py @@ -0,0 +1,64 @@ +# SPDX-FileCopyrightText: (c) TagStudio Contributors +# SPDX-License-Identifier: GPL-3.0-only + + +import structlog +from PySide6.QtCore import Qt +from PySide6.QtWidgets import QHBoxLayout, QLabel, QPushButton, QVBoxLayout, QWidget + +from tagstudio.qt.controllers.modal_content import ModalContent +from tagstudio.qt.translations import Translations +from tagstudio.qt.views.stylesheets.stylesheets import header + +logger = structlog.get_logger(__name__) + + +class ModalView(QVBoxLayout): + """A generic reusable modal panel widget.""" + + def __init__( + self, + content_widget: ModalContent, + title: str = "", + is_savable: bool = False, + inline_title: bool = True, + ): + super().__init__() + self.content_widget = content_widget + self.setContentsMargins(6, 6 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) + + # [Done] + # - OR - + # [Cancel] [Save] + if not is_savable: + done_button = QPushButton(Translations["generic.done"]) + done_button.setAutoDefault(True) + self.content_widget.done_button = done_button + self.button_layout.addWidget(done_button) + else: + cancel_button = QPushButton(Translations["generic.cancel"]) + self.content_widget.cancel_button = cancel_button + self.button_layout.addWidget(cancel_button) + + save_button = QPushButton(Translations["generic.save"]) + save_button.setAutoDefault(True) + self.content_widget.save_button = save_button + self.button_layout.addWidget(save_button) + + if inline_title: + self.title_label = QLabel() + self.title_label.setObjectName("fieldTitle") + self.title_label.setWordWrap(True) + self.title_label.setText(header(title, 3)) + self.title_label.setAlignment(Qt.AlignmentFlag.AlignCenter) + self.addWidget(self.title_label) + + self.addWidget(content_widget) + self.setStretch(1, 2) + self.addWidget(self.button_container) + content_widget.parent_post_init() diff --git a/src/tagstudio/qt/views/panel_modal.py b/src/tagstudio/qt/views/panel_modal.py deleted file mode 100644 index 87d476c7..00000000 --- a/src/tagstudio/qt/views/panel_modal.py +++ /dev/null @@ -1,127 +0,0 @@ -# SPDX-FileCopyrightText: (c) TagStudio Contributors -# SPDX-License-Identifier: GPL-3.0-only - - -import contextlib -from typing import Any, override - -import structlog -from PySide6 import QtCore, QtGui -from PySide6.QtCore import Qt, Signal -from PySide6.QtWidgets import QHBoxLayout, QLabel, QPushButton, QVBoxLayout, QWidget - -from tagstudio.qt.translations import Translations - -logger = structlog.get_logger(__name__) - - -class PanelModal(QWidget): - """A generic reusable modal panel widget.""" - - done = Signal() - saved = Signal() - saved_data = Signal(type(Any)) - - def __init__( - self, - widget: "PanelWidget", - title: str = "", - window_title: str | None = None, - is_savable: bool = False, - inline_title: bool = True, - ): - # [Done] - # - OR - - # [Cancel] [Save] - super().__init__() - self.widget = widget - 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 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) - - if not is_savable: - self.done_button = QPushButton(Translations["generic.done"]) - self.done_button.setAutoDefault(True) - self.done_button.clicked.connect(self.hide) - self.done_button.clicked.connect(self.done.emit) - self.widget.panel_done_button = self.done_button - self.button_layout.addWidget(self.done_button) - else: - self.cancel_button = QPushButton(Translations["generic.cancel"]) - self.cancel_button.clicked.connect(self.hide) - self.cancel_button.clicked.connect(widget.reset) - self.widget.panel_cancel_button = self.cancel_button - self.button_layout.addWidget(self.cancel_button) - - self.save_button = QPushButton(Translations["generic.save"]) - 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 - self.button_layout.addWidget(self.save_button) - - 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) - - self.root_layout.addWidget(widget) - widget.parent_modal = self - self.root_layout.setStretch(1, 2) - self.root_layout.addWidget(self.button_container) - widget.parent_post_init() - - @override - def closeEvent(self, event: QtGui.QCloseEvent) -> None: - with contextlib.suppress(AttributeError): - self.cancel_button.click() - with contextlib.suppress(AttributeError): - self.done_button.click() - event.accept() - - -class PanelWidget(QWidget): - """Used for widgets that go in a modal panel, ex. for editing or searching.""" - - parent_modal: PanelModal | None = None - panel_save_button: QPushButton | None = None - panel_cancel_button: QPushButton | None = None - panel_done_button: QPushButton | None = None - - def __init__(self): - super().__init__() - - def saved_data(self) -> Any: # pyright: ignore[reportExplicitAny] - return None - - def reset(self) -> None: - pass - - def parent_post_init(self) -> None: - pass - - @override - def keyPressEvent(self, event: QtGui.QKeyEvent) -> None: - if event.key() == QtCore.Qt.Key.Key_Escape: - if self.panel_cancel_button: - self.panel_cancel_button.click() - elif self.panel_done_button: - self.panel_done_button.click() - elif event.key() == Qt.Key.Key_Return or event.key() == Qt.Key.Key_Enter: - if self.panel_save_button: - self.panel_save_button.click() - elif self.panel_done_button: - self.panel_done_button.click() - else: # Other key presses - super().keyPressEvent(event) diff --git a/src/tagstudio/qt/views/search_panel_view.py b/src/tagstudio/qt/views/search_panel_view.py index 175828af..60ccd1e5 100644 --- a/src/tagstudio/qt/views/search_panel_view.py +++ b/src/tagstudio/qt/views/search_panel_view.py @@ -17,15 +17,15 @@ from PySide6.QtWidgets import ( ) from tagstudio.core.library.alchemy.library import Library +from tagstudio.qt.controllers.modal_content import ModalContent from tagstudio.qt.translations import Translations -from tagstudio.qt.views.panel_modal import PanelWidget from tagstudio.qt.views.stylesheets.stylesheets import list_button_style if TYPE_CHECKING: from tagstudio.qt.controllers.search_panel_controller import SearchPanel -class SearchPanelView(PanelWidget): +class SearchPanelView(ModalContent): def __init__(self, is_chooser: bool) -> None: self.is_chooser: bool = is_chooser super().__init__() diff --git a/src/tagstudio/qt/views/suggest_box_view.py b/src/tagstudio/qt/views/suggest_box_view.py index 6cd85b32..39184e61 100644 --- a/src/tagstudio/qt/views/suggest_box_view.py +++ b/src/tagstudio/qt/views/suggest_box_view.py @@ -4,14 +4,7 @@ import structlog from PySide6.QtCore import Qt -from PySide6.QtWidgets import ( - QFrame, - QHBoxLayout, - QScrollArea, - QSizePolicy, - QVBoxLayout, - QWidget, -) +from PySide6.QtWidgets import QFrame, QHBoxLayout, QScrollArea, QSizePolicy, QVBoxLayout, QWidget from tagstudio.qt.controllers.autofill_line_edit import AutofillLineEdit from tagstudio.qt.views.stylesheets.stylesheets import ( diff --git a/tests/qt/test_tag_panel.py b/tests/qt/test_tag_panel.py index d5c70564..48197372 100644 --- a/tests/qt/test_tag_panel.py +++ b/tests/qt/test_tag_panel.py @@ -22,8 +22,9 @@ def test_add_tag_callback(qt_driver: QtDriver): qt_driver.add_tag_action_callback() # When - assert isinstance(qt_driver.modal.widget, BuildTagPanel) - qt_driver.modal.widget.name_field.setText("xxx") + build_tag_panel = qt_driver.modal.layout().content_widget + assert isinstance(build_tag_panel, BuildTagPanel) + build_tag_panel.name_field.setText("xxx") # qt_driver.modal.widget.color_field.setCurrentIndex(1) qt_driver.modal.saved.emit()