Compare commits

..

3 Commits

Author SHA1 Message Date
Travis Abendshien a1f9d34629 fix: remove deprecated methods 2026-07-20 04:33:07 -07:00
Travis Abendshien 9780d68d2c refactor(ui): refactor SearchPanel classes to use newer MVC pattern 2026-07-20 04:25:42 -07:00
Travis Abendshien 74f77de962 refactor(ui): refactor PanelModal and PanelWidget into MVC Modal, ModalView, and ModalContent classes 2026-07-20 00:21:15 -07:00
36 changed files with 570 additions and 734 deletions
@@ -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
@@ -2,7 +2,6 @@
# SPDX-License-Identifier: GPL-3.0-only
import typing
from typing import override
from warnings import catch_warnings
@@ -14,22 +13,22 @@ 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.global_settings import GlobalSettings
from tagstudio.qt.translations import Translations
from tagstudio.qt.views.panel_modal import PanelModal, PanelWidget
from tagstudio.qt.views.suggest_box_view import SuggestBoxView
if typing.TYPE_CHECKING:
from tagstudio.qt.ts_qt import QtDriver
logger = structlog.get_logger(__name__)
class FieldSuggestBox(SuggestBox[BaseFieldTemplate]):
def __init__(self, driver: "QtDriver", view: SuggestBoxView | None = None):
super().__init__(driver, view=view or SuggestBoxView())
self._lib = self._driver.lib
def __init__(
self, library: Library, settings: GlobalSettings, view: SuggestBoxView | None = None
):
super().__init__(library, settings, view or SuggestBoxView())
# Context Menu Actions
edit_field_on_add_action = QAction(Translations["settings.edit_field_on_add"], self)
@@ -38,15 +37,15 @@ class FieldSuggestBox(SuggestBox[BaseFieldTemplate]):
self.addAction(edit_field_on_add_action)
self.layout().search_field.setContextMenuPolicy(Qt.ContextMenuPolicy.ActionsContextMenu)
self.layout().search_field.addAction(edit_field_on_add_action)
edit_field_on_add_action.setChecked(self._driver.settings.edit_field_on_add)
edit_field_on_add_action.setChecked(self._settings.edit_field_on_add)
edit_field_on_add_action.triggered.connect(
lambda checked: self.toggle_edit_on_field_add(checked)
)
def toggle_edit_on_field_add(self, checked: bool) -> None:
"""Toggle the setting for opening the edit window after adding a field."""
self._driver.settings.edit_field_on_add = checked
self._driver.settings.save()
self._settings.edit_field_on_add = checked
self._settings.save()
@override
def _on_item_create(self) -> None:
@@ -61,7 +60,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"],
@@ -76,9 +75,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()
@@ -124,7 +121,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)
@@ -135,7 +132,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
@@ -7,57 +7,40 @@ from warnings import catch_warnings
import structlog
from PySide6.QtCore import Signal
from PySide6.QtWidgets import QMessageBox
from PySide6.QtWidgets import QMessageBox, QWidget
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
from tagstudio.qt.views.search_panel_view import SearchPanelView
logger = structlog.get_logger(__name__)
class FieldTemplateSearchModal(PanelModal):
def __init__(
self,
library: Library,
is_field_template_chooser: bool = True,
has_save: bool = False,
) -> None:
self.search_panel: FieldTemplateSearchPanel = FieldTemplateSearchPanel(
library,
is_field_template_chooser,
view=FieldTemplateSearchPanelView(is_field_template_chooser),
)
super().__init__(
self.search_panel,
Translations["field.add.plural"],
is_savable=has_save,
)
class FieldTemplateSearchPanel(SearchPanel[BaseFieldTemplate]):
field_template_chosen = Signal(object)
def __init__(
self,
library: Library,
is_field_template_chooser: bool = True,
view: FieldTemplateSearchPanelView | None = None,
is_chooser: bool = True,
view: SearchPanelView | None = None,
) -> None:
super().__init__(
view=view or FieldTemplateSearchPanelView(is_field_template_chooser),
view=view
or SearchPanelView(Translations["home.search_field_templates"], is_chooser=is_chooser),
exclude=[],
is_chooser=is_field_template_chooser,
is_chooser=is_chooser,
)
self.__lib = library
self._unlimited_limit_item_label = Translations["field_template.all_field_templates"]
self._create_and_add_button_label_key = "field_template.create_add"
self._create_and_add_button_key = "field_template.create_add"
@override
def _get_max_limit(self) -> int:
@@ -76,7 +59,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 +76,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"],
@@ -105,7 +88,7 @@ class FieldTemplateSearchPanel(SearchPanel[BaseFieldTemplate]):
@override
def _on_item_remove(self, item: BaseFieldTemplate) -> None:
if self.is_chooser:
if self._is_chooser:
return
message_box = QMessageBox(
@@ -141,7 +124,7 @@ class FieldTemplateSearchPanel(SearchPanel[BaseFieldTemplate]):
if item is None:
return
field_template_widget.has_remove = not self.is_chooser
field_template_widget.has_remove = not self._is_chooser
# Disconnect previous callbacks
with catch_warnings(record=True):
@@ -157,7 +140,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,11 +154,25 @@ 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
self.__lib.update_field_template(
edit_item_panel.old_field_type, edit_item_panel.build_field_template()
)
self.update_items(self.search_field.text())
self.update_items(self.layout().search_field.text())
@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
if self.layout().scroll_layout.count() <= index:
while self.layout().scroll_layout.count() <= index:
pad_field_template_widget = FieldTemplateWidget()
pad_field_template_widget.setHidden(True)
self.layout().scroll_layout.addWidget(pad_field_template_widget)
field_template_widget: QWidget = self.layout().scroll_layout.itemAt(index).widget()
assert isinstance(field_template_widget, FieldTemplateWidget)
return field_template_widget
+85
View File
@@ -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]
@@ -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)
@@ -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,
@@ -8,11 +8,11 @@ import structlog
from PySide6 import QtCore, QtGui
from PySide6.QtCore import Signal
from PySide6.QtGui import QShowEvent
from PySide6.QtWidgets import QVBoxLayout, QWidget
from PySide6.QtWidgets import 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__(
@@ -50,18 +50,18 @@ class SearchPanel[T](PanelWidget):
is_chooser: bool = True,
) -> None:
super().__init__()
self.view = view
self.is_chooser = is_chooser
self.setLayout(QVBoxLayout(self))
self.layout().setContentsMargins(0, 0, 0, 0)
self.layout().addWidget(self.view)
self.view.connect_callbacks(self)
self._driver: QtDriver | None = None
self.exclude: list[int] = exclude or []
self._is_chooser = is_chooser
self._create_and_add_button_in_layout = False
self._create_and_add_button_key: str = ""
# Items
self._excluded: list[int] = exclude or []
self._search_results: list[T] = []
# Limits
self._unlimited_limit_item_label: str = "All Items"
self.__limit_items: list[tuple[str, int]] = [
self._limit_items: list[tuple[str, int]] = [
("25", 25),
("50", 50),
("100", 100),
@@ -69,66 +69,102 @@ class SearchPanel[T](PanelWidget):
("500", 500),
(self._unlimited_limit_item_label, -1),
]
self.__default_limit_index: int = 0 # 25 Limit (Default)
self.__previous_limit_index: int = self.__default_limit_index
self._default_limit_index: int = 0 # 25 Limit (Default)
self._previous_limit_index: int = self._default_limit_index
self.view.set_limit_items(self.__limit_items)
self.view.set_limit_index(self.__default_limit_index)
self.setLayout(view)
self.set_limit_items(self._limit_items)
self.set_limit_index(self._default_limit_index)
self.setMinimumSize(300, 400)
self.connect_callbacks(self)
# Items
self._search_results: list[T] = []
def connect_callbacks(self, controller: "SearchPanel[Any]") -> None: # pyright: ignore[reportExplicitAny]
self.layout().limit_combobox.currentIndexChanged.connect(controller.on_limit_changed)
self.layout().search_field.textChanged.connect(controller.on_search_query_changed)
self.layout().search_field.returnPressed.connect(
lambda: controller.on_search_query_submitted(self.get_search_query())
)
self.layout().create_button.clicked.connect(controller.on_item_create)
self.layout().create_and_add_button.clicked.connect(
lambda: controller.on_item_create(add_to_entry=True)
)
self._create_and_add_button_label_key: str = ""
def set_limit_items(self, limit_items: list[tuple[str, int]]) -> None:
# Remove existing limit items
for i in reversed(range(self.layout().limit_combobox.count())):
self.layout().limit_combobox.removeItem(i)
@property
def search_field(self):
return self.view.search_field
# Add new limit items
self.layout().limit_combobox.addItems([limit_item[0] for limit_item in limit_items])
@property
def create_and_add_button(self):
return self.view.create_and_add_button
def get_limit_index(self) -> int:
return self.layout().limit_combobox.currentIndex()
def set_limit_index(self, index: int) -> None:
self.layout().limit_combobox.setCurrentIndex(index)
def focus_search_box(self, select_all: bool = False) -> None:
self.layout().search_field.setFocus()
if select_all:
self.layout().search_field.selectAll()
def get_search_query(self) -> str:
return self.view.get_search_query()
return self.layout().search_field.text()
def clear_search_query(self) -> None:
self.view.clear_search_query()
self.layout().search_field.setText("")
self.focus_search_box()
def get_item_widget(self, index: int, library: Library):
return self.view.get_item_widget(index, library)
# Item list
def scroll_to(self, position: int) -> None:
self.layout().scroll_area.verticalScrollBar().setValue(position)
def set_driver(self, driver: "QtDriver") -> None:
self._driver = driver
def add_create_and_add_button(self) -> None:
if self._create_and_add_button_in_layout:
return
self.layout().scroll_layout.addWidget(self.layout().create_and_add_button)
self.layout().create_and_add_button.show()
self._create_and_add_button_in_layout = True
def remove_create_and_add_button(self) -> None:
if not self._create_and_add_button_in_layout:
return
self.layout().scroll_layout.removeWidget(self.layout().create_and_add_button)
self.layout().create_and_add_button.hide()
self._create_and_add_button_in_layout = False
def get_item_widget(self, index: int, library: Library) -> Any: # pyright: ignore[reportExplicitAny]
return self.get_item_widget(index, library)
def on_limit_changed(self, index: int) -> None:
# Method was called outside the limit_combobox callback
if index != self.view.get_limit_index():
self.view.set_limit_index(index)
if index != self.get_limit_index():
self.set_limit_index(index)
if self.__previous_limit_index == index:
if self._previous_limit_index == index:
return
self.update_items(self.search_field.text())
self.update_items(self.layout().search_field.text())
def _get_limit(self) -> tuple[str, int]:
return self.__limit_items[self.view.get_limit_index()]
return self._limit_items[self.get_limit_index()]
def _get_previous_limit(self) -> tuple[str, int]:
return self.__limit_items[self.__previous_limit_index]
return self._limit_items[self._previous_limit_index]
def _get_max_limit(self) -> int:
raise NotImplementedError()
def on_search_query_changed(self, query: str) -> None:
self.create_and_add_button.setText(
Translations.format(self._create_and_add_button_label_key, query=query)
self.layout().create_and_add_button.setText(
Translations.format(self._create_and_add_button_key, query=query)
)
self.update_items(query)
def on_search_query_submitted(self, query: str) -> None:
# Focus search field if no query
if not query:
self.search_field.setFocus()
self.layout().search_field.setFocus()
parent: QWidget | None = self.parentWidget()
if parent is not None: # pyright: ignore[reportUnnecessaryComparison]
parent.hide()
@@ -137,7 +173,7 @@ class SearchPanel[T](PanelWidget):
# Create and add item if no search results
if len(self._search_results) <= 0:
self.on_item_create(add_to_entry=True)
elif self.is_chooser:
elif self._is_chooser:
self._on_item_chosen(self._search_results[0])
self.clear_search_query()
@@ -156,14 +192,14 @@ class SearchPanel[T](PanelWidget):
raise NotImplementedError()
def _is_excluded(self, item: T) -> bool:
return _item_id(item) in self.exclude
return _item_id(item) in self._excluded
def update_items(self, query: str | None = None) -> None:
"""Update the item list given a search query."""
logger.info("[SearchPanel] Updating items", limit=self._get_limit()[1])
# Remove the "Create & Add" button if one exists
self.view.remove_create_and_add_button()
self.remove_create_and_add_button()
# Get results for the search query
query_lower = "" if not query else query.lower()
@@ -205,11 +241,11 @@ class SearchPanel[T](PanelWidget):
item: T | None = all_results[i] if i < len(all_results) else None
self.set_item_widget(item=item, index=i)
self.__previous_limit_index = self.view.get_limit_index()
self._previous_limit_index = self.get_limit_index()
# Add back the "Create & Add" button
if query and query.strip():
self.view.add_create_and_add_button()
self.add_create_and_add_button()
def search_items(self, query: str) -> tuple[list[T], list[T]]: # pyright: ignore[reportUnusedParameter]
raise NotImplementedError()
@@ -217,11 +253,16 @@ class SearchPanel[T](PanelWidget):
def set_item_widget(self, item: T | None, index: int) -> None: # pyright: ignore[reportUnusedParameter]
raise NotImplementedError()
@override
def layout(self) -> SearchPanelView:
"""Return the typed layout for this widget."""
return super().layout() # pyright: ignore[reportReturnType]
@override
def showEvent(self, event: QShowEvent) -> None: # noqa N802
self.update_items()
self.view.scroll_to(0)
self.view.clear_search_query()
self.scroll_to(0)
self.clear_search_query()
return super().showEvent(event)
@override
@@ -229,13 +270,13 @@ class SearchPanel[T](PanelWidget):
# When Escape is pressed, focus back on the search box.
# If focus is already on the search box, close the modal.
if event.key() == QtCore.Qt.Key.Key_Escape:
if self.search_field.hasFocus():
if self.layout().search_field.hasFocus():
super().keyPressEvent(event)
else:
self.view.focus_search_box(select_all=True)
self.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()
+8 -9
View File
@@ -1,7 +1,7 @@
# SPDX-FileCopyrightText: (c) TagStudio Contributors
# SPDX-License-Identifier: GPL-3.0-only
from typing import TYPE_CHECKING, Any, override
from typing import Any, override
import structlog
from PySide6.QtCore import Signal
@@ -10,8 +10,9 @@ 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.global_settings import GlobalSettings
from tagstudio.qt.views.stylesheets.stylesheets import (
autofill_line_edit_style,
autofill_line_edit_top_style,
@@ -20,9 +21,6 @@ from tagstudio.qt.views.suggest_box_view import SuggestBoxView
logger = structlog.get_logger(__name__)
if TYPE_CHECKING:
from tagstudio.qt.ts_qt import QtDriver
def _item_id(item: object) -> int:
item_id: Any = getattr(item, "id") # noqa: B009 # pyright: ignore[reportExplicitAny]
@@ -46,9 +44,10 @@ class SuggestBox[T](QWidget):
item_chosen = Signal(object)
done = Signal()
def __init__(self, driver: "QtDriver", view: SuggestBoxView) -> None:
def __init__(self, library: Library, settings: GlobalSettings, view: SuggestBoxView) -> None:
super().__init__()
self._driver = driver
self._lib = library
self._settings = settings
self._limit = 5
self._is_shift_held = False
self._search_results: list[T] = []
@@ -197,10 +196,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
@@ -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",
@@ -6,66 +6,44 @@ from typing import override
from warnings import catch_warnings
import structlog
from PySide6.QtWidgets import QMessageBox
from PySide6.QtCore import Signal
from PySide6.QtWidgets import QMessageBox, QWidget
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
from tagstudio.qt.views.search_panel_view import SearchPanelView
logger = structlog.get_logger(__name__)
class TagSearchModal(PanelModal):
tsp: "TagSearchPanel"
def __init__(
self,
library: Library,
title: str,
exclude: list[int] | None = None,
is_tag_chooser: bool = True,
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,
)
class TagSearchPanel(SearchPanel[Tag]):
search_for_tag = Signal(int)
def __init__(
self,
library: Library,
exclude: list[int] | None = None,
is_tag_chooser: bool = True,
view: TagSearchPanelView | None = None,
is_chooser: bool = True,
view: SearchPanelView | None = None,
):
super().__init__(
view=view or TagSearchPanelView(is_tag_chooser),
view=view or SearchPanelView(Translations["home.search_tags"], is_chooser=is_chooser),
exclude=exclude,
is_chooser=is_tag_chooser,
is_chooser=is_chooser,
)
self.__lib = library
self._lib = library
self._unlimited_limit_item_label = Translations["tag.all_tags"]
self._create_and_add_button_label_key = "tag.create_add"
self._create_and_add_button_key = "tag.create_add"
@override
def _get_max_limit(self) -> int:
return len(self.__lib.tags)
return len(self._lib.tags)
@override
def on_item_create(self, add_to_entry: bool = False) -> None:
@@ -80,9 +58,8 @@ class TagSearchPanel(SearchPanel[Tag]):
from tagstudio.qt.mixed.build_tag import BuildTagPanel # here due to circular imports
query: str = self.get_search_query()
panel: BuildTagPanel = BuildTagPanel(self.__lib)
modal: PanelModal = PanelModal(
panel: BuildTagPanel = BuildTagPanel(self._lib)
modal: Modal = Modal(
panel,
Translations["tag.new"],
Translations["tag.add"] if add_to_entry else Translations["tag.new"],
@@ -100,10 +77,10 @@ class TagSearchPanel(SearchPanel[Tag]):
# TODO: Move this to a top-level import
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_panel: BuildTagPanel = BuildTagPanel(self._lib, tag=item)
edit_tag_modal: Modal = Modal(
edit_tag_panel,
self.__lib.tag_display_name(item),
self._lib.tag_display_name(item),
Translations["tag.edit"],
is_savable=True,
)
@@ -112,7 +89,7 @@ class TagSearchPanel(SearchPanel[Tag]):
@override
def _on_item_remove(self, item: Tag) -> None:
if self.is_chooser:
if self._is_chooser:
return
if item.id in range(RESERVED_TAG_START, RESERVED_TAG_END):
@@ -121,7 +98,7 @@ class TagSearchPanel(SearchPanel[Tag]):
message_box = QMessageBox(
QMessageBox.Icon.Question,
Translations["tag.remove"],
Translations.format("tag.confirm_delete", tag_name=self.__lib.tag_display_name(item)),
Translations.format("tag.confirm_delete", tag_name=self._lib.tag_display_name(item)),
QMessageBox.StandardButton.Ok | QMessageBox.StandardButton.Cancel,
)
@@ -130,7 +107,7 @@ class TagSearchPanel(SearchPanel[Tag]):
if result != QMessageBox.StandardButton.Ok:
return
self.__lib.remove_tag(item.id)
self._lib.remove_tag(item.id)
self.update_items(self.get_search_query())
@override
@@ -139,12 +116,12 @@ class TagSearchPanel(SearchPanel[Tag]):
@override
def search_items(self, query: str) -> tuple[list[Tag], list[Tag]]:
return self.__lib.search_tags(name=query, limit=self._get_limit()[1])
return self._lib.search_tags(name=query, limit=self._get_limit()[1])
@override
def set_item_widget(self, item: Tag | None, index: int) -> None:
"""Set the tag of a tag widget at a specific index."""
tag_widget: TagWidget = self.get_item_widget(index, self.__lib)
tag_widget: TagWidget = self.get_item_widget(index, self._lib)
tag_widget.set_tag(item)
tag_widget.setHidden(item is None)
@@ -152,7 +129,7 @@ class TagSearchPanel(SearchPanel[Tag]):
return
assert item is not None
tag_widget.has_remove = not self.is_chooser and item.id not in range(
tag_widget.has_remove = not self._is_chooser and item.id not in range(
RESERVED_TAG_START, RESERVED_TAG_END
)
@@ -166,7 +143,7 @@ class TagSearchPanel(SearchPanel[Tag]):
# Connect callbacks
tag_widget.on_edit.connect(lambda edit_tag=item: self.on_item_edit(edit_tag))
tag_widget.on_remove.connect(lambda remove_tag=item: self._on_item_remove(remove_tag))
if self.is_chooser:
if self._is_chooser:
tag_widget.bg_button.clicked.connect(
lambda checked=False, tag=item: self._on_item_chosen(tag)
)
@@ -176,25 +153,19 @@ class TagSearchPanel(SearchPanel[Tag]):
)
# Connect search action
if self._driver is not None:
tag_widget.search_for_tag_action.triggered.connect(
lambda checked=False, tag_id=item.id: self.search_for_tag(tag_id)
)
tag_widget.search_for_tag_action.setEnabled(True)
else:
logger.warning(
"[TagSearchPanel] No driver was set for this TagSearchPanel. Was this on purpose?"
)
tag_widget.search_for_tag_action.setEnabled(False)
tag_widget.search_for_tag_action.triggered.connect(
lambda checked=False, tag_id=item.id: self.search_for_tag.emit(tag_id)
)
tag_widget.search_for_tag_action.setEnabled(True)
@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
if isinstance(edit_item_panel, BuildTagPanel):
tag: Tag = edit_item_panel.build_tag()
self.__lib.add_tag(
self._lib.add_tag(
tag, parent_ids=edit_item_panel.parent_ids, aliases=edit_item_panel.aliases
)
@@ -206,25 +177,32 @@ 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
if not isinstance(edit_item_panel, BuildTagPanel):
return
self.__lib.update_tag(
self._lib.update_tag(
tag=edit_item_panel.build_tag(),
parent_ids=edit_item_panel.parent_ids,
aliases=edit_item_panel.aliases,
)
self.update_items(self.search_field.text())
self.update_items(self.layout().search_field.text())
def search_for_tag(self, tag_id: int) -> None:
if self._driver is None:
return
@override
def get_item_widget(self, index: int, library: Library | None) -> TagWidget:
"""Gets the item widget at a specific index."""
# Create any new item widgets needed up to the given index
if self.layout().scroll_layout.count() <= index:
while self.layout().scroll_layout.count() <= index:
pad_tag_widget = TagWidget(
tag=None, has_edit=True, has_remove=True, library=library
)
pad_tag_widget.setHidden(True)
self.layout().scroll_layout.addWidget(pad_tag_widget)
self._driver.main_window.search_field.setText(f"tag_id:{tag_id}")
self._driver.update_browsing_state(
BrowsingState.from_tag_id(tag_id, self._driver.browsing_history.current)
)
tag_widget: QWidget = self.layout().scroll_layout.itemAt(index).widget()
assert isinstance(tag_widget, TagWidget)
return tag_widget
+19 -25
View File
@@ -2,36 +2,36 @@
# SPDX-License-Identifier: GPL-3.0-only
import typing
from typing import override
from warnings import catch_warnings
import structlog
from PySide6.QtCore import Signal
from PySide6.QtGui import QAction, Qt
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.global_settings import GlobalSettings
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
from tagstudio.qt.views.suggest_box_view import SuggestBoxView
if typing.TYPE_CHECKING:
from tagstudio.qt.ts_qt import QtDriver
logger = structlog.get_logger(__name__)
class TagSuggestBox(SuggestBox[Tag]):
def __init__(self, driver: "QtDriver", view: SuggestBoxView | None = None):
super().__init__(driver, view=view or SuggestBoxView())
self._driver = driver
self._lib = self._driver.lib
search_for_tag = Signal(int)
def __init__(
self, library: Library, settings: GlobalSettings, view: SuggestBoxView | None = None
):
super().__init__(library, settings, view or SuggestBoxView())
# Context Menu Actions
edit_tag_on_create_action = QAction(Translations["settings.edit_tag_on_create"], self)
@@ -40,21 +40,15 @@ class TagSuggestBox(SuggestBox[Tag]):
self.addAction(edit_tag_on_create_action)
self.layout().search_field.setContextMenuPolicy(Qt.ContextMenuPolicy.ActionsContextMenu)
self.layout().search_field.addAction(edit_tag_on_create_action)
edit_tag_on_create_action.setChecked(self._driver.settings.edit_tag_on_create)
edit_tag_on_create_action.setChecked(self._settings.edit_tag_on_create)
edit_tag_on_create_action.triggered.connect(
lambda checked: self.toggle_edit_on_tag_create(checked)
)
def _search_for_tag_callback(self, tag_id: int) -> None:
self._driver.main_window.search_field.setText(f"tag_id:{tag_id}")
self._driver.update_browsing_state(
BrowsingState.from_tag_id(tag_id, self._driver.browsing_history.current)
)
def toggle_edit_on_tag_create(self, checked: bool) -> None:
"""Toggle the setting for opening the edit window after creating a tag."""
self._driver.settings.edit_tag_on_create = checked
self._driver.settings.save()
self._settings.edit_tag_on_create = checked
self._settings.save()
@override
def _on_item_create(self) -> None:
@@ -67,9 +61,9 @@ class TagSuggestBox(SuggestBox[Tag]):
"""
query: str = self.layout().search_field.text()
if self._driver.settings.edit_tag_on_create:
if self._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():
@@ -86,7 +80,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"],
@@ -141,12 +135,12 @@ class TagSuggestBox(SuggestBox[Tag]):
lambda checked=False, tag=item: self._on_item_chosen(tag)
)
tag_widget.search_for_tag_action.triggered.connect(
lambda checked=False, tag_id=item.id: self._search_for_tag_callback(tag_id)
lambda checked=False, tag_id=item.id: self.search_for_tag.emit(tag_id)
)
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(
@@ -159,7 +153,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
-98
View File
@@ -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)
+4 -4
View File
@@ -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()
+4 -4
View File
@@ -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."""
+27 -25
View File
@@ -28,12 +28,14 @@ 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.tag_search_panel_controller import TagSearchModal
from tagstudio.qt.controllers.modal import Modal
from tagstudio.qt.controllers.modal_content import ModalContent
from tagstudio.qt.controllers.tag_search_panel_controller import TagSearchPanel
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.search_panel_view import SearchPanelView
from tagstudio.qt.views.stylesheets.stylesheets import (
checkbox_style,
colored_radio_button_style,
@@ -73,12 +75,12 @@ 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:
super().__init__()
self.lib = library
self._lib = library
self.tag: Tag # NOTE: This gets set at the end of the init.
self.tag_color_namespace: str | None
self.tag_color_slug: str | None
@@ -153,7 +155,6 @@ class BuildTagPanel(PanelWidget):
self.parent_tags_title = QLabel(Translations["tag.parent_tags"])
self.parent_tags_layout.addWidget(self.parent_tags_title)
self.scroll_contents = QWidget()
self.parent_tags_scroll_layout = QVBoxLayout(self.scroll_contents)
self.parent_tags_scroll_layout.setContentsMargins(6, 6, 6, 0)
@@ -176,10 +177,11 @@ class BuildTagPanel(PanelWidget):
if tag is not None:
exclude_ids.append(tag.id)
self.add_tag_modal = TagSearchModal(
self.lib, title=Translations["tag.add.plural"], exclude=exclude_ids
)
self.add_tag_modal.tsp.item_chosen.connect(lambda x: self._add_parent_tag_callback(x))
tsp_view = SearchPanelView(placeholder_text=Translations["home.search_tags"])
tsp = TagSearchPanel(self._lib, exclude=exclude_ids, view=tsp_view)
self.add_tag_modal = Modal(tsp, title=Translations["tag.add.plural"])
tsp.item_chosen.connect(lambda x: self._add_parent_tag_callback(x))
self.parent_tags_add_button.clicked.connect(self.add_tag_modal.show)
# Color ----------------------------------------------------------------
@@ -194,14 +196,14 @@ class BuildTagPanel(PanelWidget):
self.color_button: TagColorPreview
try:
assert tag is not None
self.color_button = TagColorPreview(self.lib, tag.color)
self.color_button = TagColorPreview(self._lib, tag.color)
except Exception as e:
# TODO: Investigate why this happens during tests
logger.error("[BuildTag] Could not access Tag member attributes", error=e)
self.color_button = TagColorPreview(self.lib, None)
self.tag_color_selection = TagColorSelection(self.lib)
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(
@@ -325,7 +327,7 @@ class BuildTagPanel(PanelWidget):
next_tab: QWidget = last_tab
for parent_id in self.parent_ids:
tag = self.lib.get_tag(parent_id)
tag = self._lib.get_tag(parent_id)
if not tag:
continue
is_disam = parent_id == self.disambiguation_id
@@ -363,7 +365,7 @@ class BuildTagPanel(PanelWidget):
text_color = get_tag_text_color(primary_color, highlight_color)
def update_parent_tag_callback(build_tag_panel: BuildTagPanel):
self.lib.update_tag(
self._lib.update_tag(
build_tag_panel.build_tag(),
parent_ids=set(build_tag_panel.parent_ids),
aliases=set(build_tag_panel.aliases),
@@ -371,10 +373,10 @@ class BuildTagPanel(PanelWidget):
self.set_parent_tags()
def on_parent_tag_edit(tag: Tag) -> None:
build_tag_panel = BuildTagPanel(self.lib, tag=tag)
edit_modal = PanelModal(
build_tag_panel = BuildTagPanel(self._lib, tag=tag)
edit_modal = Modal(
build_tag_panel,
self.lib.tag_display_name(tag),
self._lib.tag_display_name(tag),
"Edit Tag",
is_savable=True,
)
@@ -382,7 +384,7 @@ class BuildTagPanel(PanelWidget):
edit_modal.show()
# Add Tag Widget
tag_widget = TagWidget(tag, library=self.lib, has_edit=True, has_remove=True)
tag_widget = TagWidget(tag, library=self._lib, has_edit=True, has_remove=True)
tag_widget.on_remove.connect(lambda t=parent_id: self._remove_parent_tag_callback(t))
tag_widget.on_edit.connect(partial(on_parent_tag_edit, tag))
@@ -422,7 +424,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 +495,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 +517,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()
+2 -2
View File
@@ -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,
+2 -2
View File
@@ -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)
+3 -3
View File
@@ -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,
+7 -6
View File
@@ -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
+3 -3
View File
@@ -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,
@@ -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
-3
View File
@@ -141,9 +141,6 @@ class TagWidget(QWidget):
self.search_for_tag_action = QAction(self)
self.search_for_tag_action.setText(Translations["tag.search_for_tag"])
self.bg_button.addAction(self.search_for_tag_action)
# add_to_search_action = QAction(self)
# add_to_search_action.setText(Translations.translate_formatted("tag.add_to_search"))
# self.bg_button.addAction(add_to_search_action)
self.inner_layout = QHBoxLayout()
self.inner_layout.setObjectName("innerLayout")
+38 -30
View File
@@ -61,7 +61,8 @@ 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.tag_search_panel_controller import TagSearchModal
from tagstudio.qt.controllers.modal import Modal
from tagstudio.qt.controllers.tag_search_panel_controller import TagSearchPanel
from tagstudio.qt.controllers.update_available_message_box import UpdateAvailableMessageBox
from tagstudio.qt.global_settings import DEFAULT_GLOBAL_SETTINGS_PATH, GlobalSettings, Theme
from tagstudio.qt.mixed.about_modal import AboutModal
@@ -82,9 +83,8 @@ from tagstudio.qt.translations import Translations
from tagstudio.qt.utils.custom_runnable import CustomRunnable
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.search_panel_view import SearchPanelView
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
@@ -364,15 +364,15 @@ class QtDriver(DriverMixin, QObject):
if platform.system() != "Windows":
self.app.setDesktopFileName("tagstudio")
# Initialize the Tag Manager panel
self.tag_manager_panel = TagSearchModal(
self.lib,
title=Translations["tag_manager.title"],
is_tag_chooser=False,
tsp_view = SearchPanelView(
placeholder_text=Translations["home.search_tags"],
create_text=Translations["tag.create"],
is_chooser=False,
)
self.tag_manager_panel.tsp.set_driver(self)
self.tag_manager_panel.done.connect(
tsp = TagSearchPanel(self.lib, is_chooser=False, view=tsp_view)
tsp.search_for_tag.connect(self.search_for_tag_callback)
self.tag_manager = Modal(tsp, Translations["tag_manager.title"])
self.tag_manager.done.connect(
lambda checked=False: self.main_window.preview_panel.set_selection(
self.selected, update_preview=False
)
@@ -382,27 +382,30 @@ 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),
is_chooser=False,
view=SearchPanelView(Translations["home.search_field_templates"], is_chooser=False),
),
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
)
)
# TODO: Remove/replace this with a suite of persistently visible tagging panels, including
# tag search. These will be docked on the left-hand side of the main window.
# Initialize the "Add Tag" panel
self.add_tag_modal = TagSearchModal(
self.lib, title=Translations["tag.add.plural"], is_tag_chooser=True
)
self.add_tag_modal.tsp.set_driver(self)
self.add_tag_modal.tsp.item_chosen.connect(
add_tag_tsp_view = SearchPanelView(placeholder_text=Translations["home.search_tags"])
add_tag_tsp = TagSearchPanel(self.lib, view=add_tag_tsp_view)
add_tag_tsp.search_for_tag.connect(self.search_for_tag_callback)
self.add_tag_modal = Modal(add_tag_tsp, Translations["tag.add.plural"])
add_tag_tsp.item_chosen.connect(
lambda chosen_tag: (
self.add_tags_to_selected_callback([chosen_tag]),
self.main_window.preview_panel.set_selection(self.selected),
@@ -486,14 +489,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 +736,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 +874,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"],
@@ -890,6 +893,11 @@ class QtDriver(DriverMixin, QObject):
)
self.modal.show()
def search_for_tag_callback(self, tag_id: int) -> None:
"""Callback to search for a given Tag ID."""
self.main_window.search_field.setText(f"tag_id:{tag_id}")
self.update_browsing_state(BrowsingState.from_tag_id(tag_id, self.browsing_history.current))
def select_all_action_callback(self):
"""Set the selection to all visible items."""
self.select_all()
@@ -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__()
+2 -2
View File
@@ -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)
@@ -1,33 +0,0 @@
# 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
from tagstudio.qt.controllers.field_template_widget_controller import FieldTemplateWidget
from tagstudio.qt.translations import Translations
from tagstudio.qt.views.search_panel_view import SearchPanelView
class FieldTemplateSearchPanelView(SearchPanelView):
def __init__(self, is_field_template_chooser: bool) -> None:
super().__init__(is_field_template_chooser)
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
if self._scroll_layout.count() <= index:
while self._scroll_layout.count() <= index:
pad_field_template_widget = FieldTemplateWidget()
pad_field_template_widget.setHidden(True)
self._scroll_layout.addWidget(pad_field_template_widget)
field_template_widget: QWidget = self._scroll_layout.itemAt(index).widget()
assert isinstance(field_template_widget, FieldTemplateWidget)
return field_template_widget
+2 -2
View File
@@ -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:
+64
View File
@@ -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()
-127
View File
@@ -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)
+6 -2
View File
@@ -40,10 +40,14 @@ class PreviewPanelView(QVBoxLayout):
# Search/Create Boxes
self.field_search_box = FieldSuggestBox(
driver, view=SuggestBoxView(placeholder_text=ph_text("home.search_or_create_fields"))
driver.lib,
driver.settings,
SuggestBoxView(placeholder_text=ph_text("home.search_or_create_fields")),
)
self.tag_search_box = TagSuggestBox(
driver, view=SuggestBoxView(placeholder_text=ph_text("home.search_or_create_tags"))
driver.lib,
driver.settings,
SuggestBoxView(placeholder_text=ph_text("home.search_or_create_tags")),
)
self.preview_thumb = PreviewThumb(driver.lib, driver)
+32 -111
View File
@@ -1,7 +1,7 @@
# SPDX-FileCopyrightText: (c) TagStudio Contributors
# SPDX-License-Identifier: GPL-3.0-only
from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING
from PySide6.QtCore import QSize, Qt
from PySide6.QtWidgets import (
@@ -16,144 +16,65 @@ from PySide6.QtWidgets import (
QWidget,
)
from tagstudio.core.library.alchemy.library import Library
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
pass
class SearchPanelView(PanelWidget):
def __init__(self, is_chooser: bool) -> None:
class SearchPanelView(QVBoxLayout):
def __init__(
self, placeholder_text: str, create_text: str = "", is_chooser: bool = True
) -> None:
self.is_chooser: bool = is_chooser
super().__init__()
self.__root_layout = QVBoxLayout(self)
self.__root_layout.setContentsMargins(6, 0, 6, 0)
self.setMinimumSize(300, 400)
self.setContentsMargins(6, 0, 6, 0)
# Limit container
self.__limit_container = QWidget()
self.__root_layout.addWidget(self.__limit_container)
self.__limit_layout = QHBoxLayout(self.__limit_container)
self.__limit_layout.setContentsMargins(0, 0, 0, 0)
self.__limit_layout.setSpacing(12)
self.__limit_layout.addStretch(1)
self.__limit_title = QLabel(Translations["home.search.view_limit"])
self.__limit_layout.addWidget(self.__limit_title)
self.limit_container = QWidget()
self.limit_layout = QHBoxLayout(self.limit_container)
self.limit_layout.setContentsMargins(0, 0, 0, 0)
self.limit_layout.setSpacing(12)
self.limit_layout.addStretch(1)
self.limit_title = QLabel(Translations["home.search.view_limit"])
self.limit_layout.addWidget(self.limit_title)
self.addWidget(self.limit_container)
# Limit dropdown
self.limit_combobox = QComboBox()
self.__limit_layout.addWidget(self.limit_combobox)
self.__limit_layout.addStretch(1)
self.limit_layout.addWidget(self.limit_combobox)
self.limit_layout.addStretch(1)
self.limit_combobox.setEditable(False)
# Search field
self.search_field = QLineEdit()
self.search_field.setPlaceholderText(placeholder_text)
self.search_field.setObjectName("search_field")
self.__root_layout.addWidget(self.search_field)
self.search_field.setMinimumSize(QSize(0, 32))
self.addWidget(self.search_field)
# Scroll area
self.__scroll_contents = QWidget()
self._scroll_layout = QVBoxLayout(self.__scroll_contents)
self._scroll_layout.setContentsMargins(6, 0, 6, 0)
self._scroll_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
self.__scroll_area = QScrollArea()
self.__scroll_area.setWidget(self.__scroll_contents)
self.__root_layout.addWidget(self.__scroll_area)
self.__scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOn)
self.__scroll_area.setWidgetResizable(True)
self.__scroll_area.setFrameShadow(QFrame.Shadow.Plain)
self.__scroll_area.setFrameShape(QFrame.Shape.NoFrame)
self.scroll_contents = QWidget()
self.scroll_layout = QVBoxLayout(self.scroll_contents)
self.scroll_layout.setContentsMargins(6, 0, 6, 0)
self.scroll_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
self.scroll_area = QScrollArea()
self.scroll_area.setWidget(self.scroll_contents)
self.scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOn)
self.scroll_area.setWidgetResizable(True)
self.scroll_area.setFrameShadow(QFrame.Shadow.Plain)
self.scroll_area.setFrameShape(QFrame.Shape.NoFrame)
self.addWidget(self.scroll_area)
# Create button
self.create_button = QPushButton("")
self.create_button = QPushButton(create_text)
if not self.is_chooser:
self.__root_layout.addWidget(self.create_button)
self.addWidget(self.create_button)
# Create and add button
self.create_and_add_button_in_layout: bool = False
self.create_and_add_button = QPushButton()
self.create_and_add_button.setFlat(True)
self.create_and_add_button.setMinimumSize(22, 22)
self.create_and_add_button.setStyleSheet(list_button_style(border_style="dashed"))
@property
def scroll_layout(self) -> QVBoxLayout:
return self._scroll_layout
@property
def scroll_area(self) -> QScrollArea:
return self.__scroll_area
def connect_callbacks(self, controller: "SearchPanel[Any]") -> None: # pyright: ignore[reportExplicitAny]
self.limit_combobox.currentIndexChanged.connect(controller.on_limit_changed)
self.search_field.textChanged.connect(controller.on_search_query_changed)
self.search_field.returnPressed.connect(
lambda: controller.on_search_query_submitted(self.get_search_query())
)
self.create_button.clicked.connect(controller.on_item_create)
self.create_and_add_button.clicked.connect(
lambda: controller.on_item_create(add_to_entry=True)
)
def set_limit_items(self, limit_items: list[tuple[str, int]]) -> None:
# Remove existing limit items
for i in reversed(range(self.limit_combobox.count())):
self.limit_combobox.removeItem(i)
# Add new limit items
self.limit_combobox.addItems([limit_item[0] for limit_item in limit_items])
def get_limit_index(self) -> int:
return self.limit_combobox.currentIndex()
def set_limit_index(self, index: int) -> None:
self.limit_combobox.setCurrentIndex(index)
def focus_search_box(self, select_all: bool = False) -> None:
self.search_field.setFocus()
if select_all:
self.search_field.selectAll()
def get_search_query(self) -> str:
return self.search_field.text()
def clear_search_query(self) -> None:
self.search_field.setText("")
self.focus_search_box()
# Item list
def scroll_to(self, position: int) -> None:
self.__scroll_area.verticalScrollBar().setValue(position)
def get_item_widget(self, index: int, library: Library | None) -> Any: # pyright: ignore[reportUnusedParameter, reportExplicitAny]
raise NotImplementedError()
def add_create_and_add_button(self) -> None:
if self.create_and_add_button_in_layout:
return
self._scroll_layout.addWidget(self.create_and_add_button)
self.create_and_add_button.show()
self.create_and_add_button_in_layout = True
def remove_create_and_add_button(self) -> None:
if not self.create_and_add_button_in_layout:
return
self._scroll_layout.removeWidget(self.create_and_add_button)
self.create_and_add_button.hide()
self.create_and_add_button_in_layout = False
+1 -8
View File
@@ -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 (
@@ -1,35 +0,0 @@
# 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
from tagstudio.qt.mixed.tag_widget import TagWidget
from tagstudio.qt.translations import Translations
from tagstudio.qt.views.search_panel_view import SearchPanelView
class TagSearchPanelView(SearchPanelView):
def __init__(self, is_tag_chooser: bool) -> None:
super().__init__(is_tag_chooser)
self.search_field.setPlaceholderText(Translations["home.search_tags"])
self.create_button.setText(Translations["tag.create"])
@override
def get_item_widget(self, index: int, library: Library | None) -> TagWidget:
"""Gets the item widget at a specific index."""
# Create any new item widgets needed up to the given index
if self._scroll_layout.count() <= index:
while self._scroll_layout.count() <= index:
pad_tag_widget = TagWidget(
tag=None, has_edit=True, has_remove=True, library=library
)
pad_tag_widget.setHidden(True)
self._scroll_layout.addWidget(pad_tag_widget)
tag_widget: QWidget = self._scroll_layout.itemAt(index).widget()
assert isinstance(tag_widget, TagWidget)
return tag_widget
+4 -3
View File
@@ -1,6 +1,7 @@
# SPDX-FileCopyrightText: (c) TagStudio Contributors
# SPDX-License-Identifier: GPL-3.0-only
# pyright: reportPrivateUsage = false
from collections.abc import Callable
@@ -22,7 +23,7 @@ def test_build_tag_panel_add_sub_tag_callback(
panel: BuildTagPanel = BuildTagPanel(library, child)
qtbot.addWidget(panel)
panel._add_parent_tag_callback(parent.id) # pyright: ignore[reportPrivateUsage]
panel._add_parent_tag_callback(parent.id)
assert len(panel.parent_ids) == 1
@@ -40,7 +41,7 @@ def test_build_tag_panel_remove_subtag_callback(
panel: BuildTagPanel = BuildTagPanel(library, child)
qtbot.addWidget(panel)
panel._remove_parent_tag_callback(parent.id) # pyright: ignore[reportPrivateUsage]
panel._remove_parent_tag_callback(parent.id)
assert len(panel.parent_ids) == 0
@@ -58,7 +59,7 @@ def test_build_tag_panel_add_alias_callback(
panel: BuildTagPanel = BuildTagPanel(library, tag)
qtbot.addWidget(panel)
panel._create_alias_callback() # pyright: ignore[reportPrivateUsage]
panel._create_alias_callback()
assert panel.aliases_table.rowCount() == 1
+3 -2
View File
@@ -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()
+4 -6
View File
@@ -8,13 +8,12 @@ from pytestqt.qtbot import QtBot
from tagstudio.core.library.alchemy.library import Library
from tagstudio.qt.controllers.tag_search_panel_controller import TagSearchPanel
from tagstudio.qt.mixed.tag_widget import TagWidget
from tagstudio.qt.ts_qt import QtDriver
from tagstudio.qt.views.tag_search_panel_view import TagSearchPanelView
from tagstudio.qt.views.search_panel_view import SearchPanelView
def test_update_tags(qtbot: QtBot, library: Library):
# Given
panel = TagSearchPanel(library, view=TagSearchPanelView(is_tag_chooser=True))
panel = TagSearchPanel(library, view=SearchPanelView("", is_chooser=True))
qtbot.addWidget(panel)
@@ -22,10 +21,9 @@ def test_update_tags(qtbot: QtBot, library: Library):
panel.update_items()
def test_tag_widget_actions_replaced_correctly(qtbot: QtBot, qt_driver: QtDriver, library: Library):
panel = TagSearchPanel(library, view=TagSearchPanelView(is_tag_chooser=True))
def test_tag_widget_actions_replaced_correctly(qtbot: QtBot, library: Library):
panel = TagSearchPanel(library, view=SearchPanelView(""))
qtbot.addWidget(panel)
panel.set_driver(qt_driver)
# Set the widget
tags = library.tags