diff --git a/src/tagstudio/qt/controllers/inspector.py b/src/tagstudio/qt/controllers/inspector.py index 6f6ff029..60695faf 100644 --- a/src/tagstudio/qt/controllers/inspector.py +++ b/src/tagstudio/qt/controllers/inspector.py @@ -27,8 +27,8 @@ from tagstudio.core.utils.ffmpeg_status import FfmpegStatus, FfprobeStatus from tagstudio.core.utils.types import unwrap from tagstudio.i18n.translations import FIELD_TYPE_KEYS, Translations from tagstudio.qt.controllers.edit_text import EditText -from tagstudio.qt.controllers.entry_data_box_list import EntryDataBoxList from tagstudio.qt.controllers.modal import Modal +from tagstudio.qt.controllers.tiles.tile_list import TileList from tagstudio.qt.mixed.datetime_picker import DatetimePicker from tagstudio.qt.mixed.file_attributes import FileAttributeData from tagstudio.qt.views.inspector_view import InspectorView @@ -186,8 +186,8 @@ class Inspector(QWidget): self.layout().containers.update_from_entry(self._selected[0]) def _edit_field(self, entry_id: int, field: BaseField) -> None: - # TODO: A lot of this code is similar to or straight up shared with EntryDataBoxList. - # It's possible to reuse it later, after an EntryDataBoxList refactor. + # TODO: A lot of this code is similar to or straight up shared with TileList. + # It's possible to reuse it later, after a TileList refactor. field_name_key: str = FIELD_TYPE_KEYS.get(field.class_name, "field_type.unknown") if type(field) is TextField: @@ -281,7 +281,7 @@ class Inspector(QWidget): self.layout().preview_thumb.media_player.stop() @property - def containers(self) -> EntryDataBoxList: + def containers(self) -> TileList: return self.layout().containers @override diff --git a/src/tagstudio/qt/controllers/main_window.py b/src/tagstudio/qt/controllers/main_window.py index ffb0132d..356a0d26 100644 --- a/src/tagstudio/qt/controllers/main_window.py +++ b/src/tagstudio/qt/controllers/main_window.py @@ -47,7 +47,7 @@ from tagstudio.qt.mixed.landing import LandingWidget from tagstudio.qt.resource_manager import ResourceManager from tagstudio.qt.views.layouts.thumb_grid_layout import ThumbGridLayout from tagstudio.qt.views.styles.color_overlay import auto_theme_overlay -from tagstudio.qt.views.styles.stylesheets import PAD, WIN_PAD, checkbox_style +from tagstudio.qt.views.styles.stylesheets import HALF_PAD, PAD, WIN_PAD, checkbox_style # Only import for type checking/autocompletion, will not be imported at runtime. if typing.TYPE_CHECKING: @@ -662,7 +662,7 @@ class MainWindow(QMainWindow): self.content_splitter = QSplitter() self.content_splitter.setObjectName("content_splitter") - self.content_splitter.setHandleWidth(PAD * 2) + self.content_splitter.setHandleWidth(PAD + HALF_PAD) self.central_content = QWidget() self.central_content.setObjectName("central_content") diff --git a/src/tagstudio/qt/controllers/pagination.py b/src/tagstudio/qt/controllers/pagination.py index 7f46f5d5..e6a97f23 100644 --- a/src/tagstudio/qt/controllers/pagination.py +++ b/src/tagstudio/qt/controllers/pagination.py @@ -30,24 +30,27 @@ class Pagination(QWidget): self.setFixedHeight(self.HEIGHT) self.setStyleSheet(pagination_style()) - self.view = PaginationView() - self.setLayout(self.view) + self.setLayout(PaginationView()) self._connect_callbacks() if parent is not None: parent.installEventFilter(self) self._sync_geometry() + @override + def layout(self) -> PaginationView: + return super().layout() # pyright: ignore[reportReturnType] + def _connect_callbacks(self) -> None: - self.view.current_page_field.returnPressed.connect( - lambda: self._goto_page(int(self.view.current_page_field.text()) - 1) + self.layout().current_page_field.returnPressed.connect( + lambda: self._goto_page(int(self.layout().current_page_field.text()) - 1) ) def update_buttons(self, page_count: int, index: int, emit: bool = True): if index < 0: raise ValueError("Negative index detected") - view = self.view + view = self.layout() for i in range(0, 10): if button := self._get_button_at(view.start_buffer_layout, i): button.setHidden(True) diff --git a/src/tagstudio/qt/mixed/color_box.py b/src/tagstudio/qt/controllers/tiles/color_data.py similarity index 85% rename from src/tagstudio/qt/mixed/color_box.py rename to src/tagstudio/qt/controllers/tiles/color_data.py index bdefa82c..a9489208 100644 --- a/src/tagstudio/qt/mixed/color_box.py +++ b/src/tagstudio/qt/controllers/tiles/color_data.py @@ -4,6 +4,7 @@ import typing from collections.abc import Iterable +from typing import override import structlog from PySide6.QtCore import Signal @@ -13,11 +14,11 @@ from tagstudio.core.constants import RESERVED_NAMESPACE_PREFIX from tagstudio.core.library.alchemy.models import TagColorGroup from tagstudio.i18n.translations import Translations from tagstudio.qt.controllers.modal import Modal +from tagstudio.qt.controllers.tiles.tile_data import TileData from tagstudio.qt.mixed.build_color import BuildColorPanel -from tagstudio.qt.mixed.data_box import DataBox from tagstudio.qt.mixed.tag_color_label import TagColorLabel -from tagstudio.qt.views.layouts.flow_layout import FlowLayout from tagstudio.qt.views.styles.stylesheets import add_button_style +from tagstudio.qt.views.tiles.color_data_view import ColorDataView if typing.TYPE_CHECKING: from tagstudio.core.library.alchemy.library import Library @@ -25,8 +26,9 @@ if typing.TYPE_CHECKING: logger = structlog.get_logger(__name__) -# TODO: Split to use MVC guidelines. -class ColorBoxWidget(DataBox): +class ColorData(TileData): + """An inner widget for color names that goes in a Tile widget.""" + updated = Signal() def __init__( @@ -40,16 +42,15 @@ class ColorBoxWidget(DataBox): self.lib: Library = library title = "" if not self.lib.engine else self.lib.get_namespace_name(group) - super().__init__(title) - - self.setObjectName("colorBox") - self.base_layout = FlowLayout() - self.base_layout.enable_grid_optimizations(value=True) - self.base_layout.setContentsMargins(0, 0, 0, 0) - self.setLayout(self.base_layout) + super().__init__(title, ColorDataView()) + self.setObjectName("color_data") self.set_colors(self.colors) + @override + def layout(self) -> ColorDataView: + return super().layout() # pyright: ignore[reportReturnType] + def set_colors(self, colors: Iterable[TagColorGroup]): colors_ = sorted( list(colors), key=lambda color: self.lib.get_namespace_name(color.namespace) @@ -58,7 +59,7 @@ class ColorBoxWidget(DataBox): max_width = 60 color_widgets: list[TagColorLabel] = [] - while (item := self.base_layout.itemAt(0)) and (widget := item.widget()): + while (item := self.layout().itemAt(0)) and (widget := item.widget()): widget.deleteLater() for color in colors_: @@ -75,7 +76,7 @@ class ColorBoxWidget(DataBox): color_widget.on_remove.connect(lambda c=color: self.delete_color(c)) color_widgets.append(color_widget) - self.base_layout.addWidget(color_widget) + self.layout().addWidget(color_widget) for color_widget in color_widgets: color_widget.setFixedWidth(max_width) @@ -97,7 +98,7 @@ class ColorBoxWidget(DataBox): ) ) ) - self.base_layout.addWidget(add_button) + self.layout().addWidget(add_button) def edit_color(self, color_group: TagColorGroup): build_color_panel = BuildColorPanel(self.lib, color_group) @@ -131,6 +132,6 @@ class ColorBoxWidget(DataBox): if result != QMessageBox.ButtonRole.ActionRole.value: return - logger.info("[ColorBoxWidget] Removing color", color=color_group) + logger.info("[ColorData] Removing color", color=color_group) self.lib.delete_color(color_group) self.updated.emit() diff --git a/src/tagstudio/qt/controllers/tag_box.py b/src/tagstudio/qt/controllers/tiles/tag_data.py similarity index 56% rename from src/tagstudio/qt/controllers/tag_box.py rename to src/tagstudio/qt/controllers/tiles/tag_data.py index f174522e..88328522 100644 --- a/src/tagstudio/qt/controllers/tag_box.py +++ b/src/tagstudio/qt/controllers/tiles/tag_data.py @@ -2,6 +2,7 @@ # SPDX-License-Identifier: GPL-3.0-only +from collections.abc import Iterable from functools import partial from typing import TYPE_CHECKING, override @@ -13,8 +14,9 @@ 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.controllers.tiles.tile_data import TileData from tagstudio.qt.mixed.build_tag import BuildTagPanel -from tagstudio.qt.views.tag_box_view import TagBoxWidgetView +from tagstudio.qt.views.tiles.tag_data_view import TagDataView if TYPE_CHECKING: from tagstudio.qt.qt_driver import QtDriver @@ -22,62 +24,73 @@ if TYPE_CHECKING: logger = structlog.get_logger(__name__) -# TODO: Use newer MVC style guidelines -class TagBoxWidget(TagBoxWidgetView): +class TagData(TileData): + """An inner widget for tags that go in a Tile widget.""" + on_update = Signal() - __entries: list[int] = [] - def __init__(self, title: str, driver: QtDriver): - super().__init__(title, driver) - self.__driver = driver - - def set_entries(self, entries: list[int]) -> None: - self.__entries = entries + self._driver = driver + self._entries: list[int] = [] + super().__init__(title, TagDataView(driver.lib)) + self._connect_callbacks() @override + def layout(self) -> TagDataView: + return super().layout() # pyright: ignore[reportReturnType] + + def _connect_callbacks(self) -> None: + self.layout().tag_clicked.connect(self._on_click) + self.layout().tag_removed.connect(self._on_remove) + self.layout().tag_edited.connect(self._on_edit) + self.layout().tag_searched.connect(self._on_search) + + def set_entries(self, entries: list[int]) -> None: + self._entries = entries + + def set_tags(self, tags: Iterable[Tag]) -> None: + self.layout().set_tags(tags) + def _on_click(self, tag: Tag) -> None: - match self.__driver.settings.tag_click_action: + match self._driver.settings.tag_click_action: case TagClickActionOption.OPEN_EDIT: self._on_edit(tag) case TagClickActionOption.SET_SEARCH: - self.__driver.update_browsing_state( - BrowsingState.from_tag_id(tag.id, self.__driver.browsing_history.current) + self._driver.update_browsing_state( + BrowsingState.from_tag_id(tag.id, self._driver.browsing_history.current) ) case TagClickActionOption.ADD_TO_SEARCH: # NOTE: modifying the ast and then setting that would be nicer # than this string manipulation, but also much more complex, # due to needing to implement a visitor that turns an AST to a string # So if that exists when you read this, change the following accordingly. - current = self.__driver.browsing_history.current + current = self._driver.browsing_history.current suffix = unwrap( - BrowsingState.from_tag_id(tag.id, self.__driver.browsing_history.current).query + BrowsingState.from_tag_id(tag.id, self._driver.browsing_history.current).query ) - self.__driver.update_browsing_state( + self._driver.update_browsing_state( current.with_search_query( f"{current.query} {suffix}" if current.query else suffix ) ) - @override def _on_remove(self, tag: Tag) -> None: logger.info( - "[TagBoxWidget] remove_tag", - selected=self.__entries, + "[TagData] remove_tag", + selected=self._entries, ) - for entry_id in self.__entries: - self.__driver.lib.remove_tags_from_entries(entry_id, tag.id) + for entry_id in self._entries: + self._driver.lib.remove_tags_from_entries(entry_id, tag.id) self.on_update.emit() - @override def _on_edit(self, tag: Tag) -> None: - build_tag_panel = BuildTagPanel(self.__driver.lib, tag=tag) + build_tag_panel = BuildTagPanel(self._driver.lib, tag=tag) edit_modal = Modal( build_tag_panel, - self.__driver.lib.tag_display_name(tag), + self._driver.lib.tag_display_name(tag), "Edit Tag", is_savable=True, ) @@ -85,7 +98,7 @@ class TagBoxWidget(TagBoxWidgetView): edit_modal.show() def _update_tag_callback(self, build_tag_panel: BuildTagPanel): - self.__driver.lib.update_tag( + self._driver.lib.update_tag( build_tag_panel.build_tag(), parent_ids=set(build_tag_panel.parent_ids), aliases=set(build_tag_panel.aliases), @@ -93,9 +106,8 @@ class TagBoxWidget(TagBoxWidgetView): ) self.on_update.emit() - @override def _on_search(self, tag: Tag) -> 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) + 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) ) diff --git a/src/tagstudio/qt/controllers/tiles/text_data.py b/src/tagstudio/qt/controllers/tiles/text_data.py new file mode 100644 index 00000000..89ac9e09 --- /dev/null +++ b/src/tagstudio/qt/controllers/tiles/text_data.py @@ -0,0 +1,40 @@ +# SPDX-FileCopyrightText: (c) TagStudio Contributors +# SPDX-License-Identifier: GPL-3.0-only + + +import re +from typing import override + +from tagstudio.qt.controllers.tiles.tile_data import TileData +from tagstudio.qt.views.tiles.text_data_view import TextDataView + + +class TextData(TileData): + """An inner widget for any text that goes in a Tile widget. + + Can include text fields, date fields, etc. + """ + + def __init__(self, title: str, text: str) -> None: + super().__init__(title, TextDataView()) + self.setObjectName("text_data") + self.set_text(text) + + @override + def layout(self) -> TextDataView: + return super().layout() # pyright: ignore[reportReturnType] + + def set_text(self, text: str): + text = linkify(text) + self.layout().text_label.setText(text) + + +# Regex from https://stackoverflow.com/a/6041965 +def linkify(text: str): + url_pattern = r"(http|ftp|https):\/\/([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:\/~+#\-*]*[\w@?^=%&\/~+#\-*])" # noqa: E501 + return re.sub( + url_pattern, + lambda url: f'{url.group(0)}', + text, + flags=re.IGNORECASE, + ) diff --git a/src/tagstudio/qt/controllers/entry_data_box.py b/src/tagstudio/qt/controllers/tiles/tile.py similarity index 59% rename from src/tagstudio/qt/controllers/entry_data_box.py rename to src/tagstudio/qt/controllers/tiles/tile.py index 9950bab0..ef2746ca 100644 --- a/src/tagstudio/qt/controllers/entry_data_box.py +++ b/src/tagstudio/qt/controllers/tiles/tile.py @@ -11,88 +11,93 @@ from PySide6.QtGui import QEnterEvent, QResizeEvent from PySide6.QtWidgets import QWidget from tagstudio.core.utils.types import unwrap -from tagstudio.qt.views.entry_data_box_view import EntryDataBoxView from tagstudio.qt.views.styles.stylesheets import container_style, header +from tagstudio.qt.views.tiles.tile_view import TileView -class EntryDataBox(QWidget): - def __init__(self, title: str = "DATA BOX") -> None: +class Tile(QWidget): + """Wraps a title, action buttons, and data such as text or tags inside a single widget.""" + + def __init__(self, title: str = "TILE") -> None: super().__init__() - self.setObjectName("entry_data_box") + self.setObjectName("tile") self.setAttribute(Qt.WidgetAttribute.WA_StyledBackground) self.title: str = title self.copy_callback: Callable[[], None] | None = None self.edit_callback: Callable[[], None] | None = None self.remove_callback: Callable[[], None] | None = None - self.view = EntryDataBoxView() - self.setLayout(self.view) + self.setLayout(TileView()) self.set_title(title) self.setStyleSheet(container_style()) + @override + def layout(self) -> TileView: + return super().layout() # pyright: ignore[reportReturnType] + def set_copy_callback(self, callback: Callable[[], None] | None = None) -> None: with catch_warnings(record=True): - self.view.copy_button.clicked.disconnect() + self.layout().copy_button.clicked.disconnect() self.copy_callback = callback if callback: - self.view.copy_button.clicked.connect(callback) + self.layout().copy_button.clicked.connect(callback) def set_edit_callback(self, callback: Callable[[], None] | None = None) -> None: with catch_warnings(record=True): - self.view.edit_button.clicked.disconnect() + self.layout().edit_button.clicked.disconnect() self.edit_callback = callback if callback: - self.view.edit_button.clicked.connect(callback) + self.layout().edit_button.clicked.connect(callback) def set_remove_callback(self, callback: Callable[[], None] | None = None) -> None: with catch_warnings(record=True): - self.view.remove_button.clicked.disconnect() + self.layout().remove_button.clicked.disconnect() self.remove_callback = callback if callback: - self.view.remove_button.clicked.connect(callback) + self.layout().remove_button.clicked.connect(callback) def set_inner_widget(self, widget: QWidget) -> None: - if item := self.view.data_layout.itemAt(0): + if item := self.layout().data_layout.itemAt(0): old: QWidget = unwrap(item.widget()) - self.view.data_layout.removeWidget(old) + self.layout().data_layout.removeWidget(old) old.deleteLater() - self.view.data_layout.addWidget(widget) + self.layout().data_layout.addWidget(widget) def get_inner_widget(self) -> QWidget | None: - if item := self.view.data_layout.itemAt(0): + if item := self.layout().data_layout.itemAt(0): return item.widget() return None def set_title(self, title: str) -> None: self.title = header(title, 4) - self.view.title_widget.setText(self.title) + self.layout().title_widget.setText(self.title) @override def enterEvent(self, event: QEnterEvent) -> None: - # NOTE: You could pass the hover event to the EntryDataBox if needed. + # NOTE: You could pass the hover event to the inner widget if needed. if self.copy_callback: - self.view.copy_button.setHidden(False) + self.layout().copy_button.setHidden(False) if self.edit_callback: - self.view.edit_button.setHidden(False) + self.layout().edit_button.setHidden(False) if self.remove_callback: - self.view.remove_button.setHidden(False) + self.layout().remove_button.setHidden(False) return super().enterEvent(event) @override def leaveEvent(self, event: QEvent) -> None: if self.copy_callback: - self.view.copy_button.setHidden(True) + self.layout().copy_button.setHidden(True) if self.edit_callback: - self.view.edit_button.setHidden(True) + self.layout().edit_button.setHidden(True) if self.remove_callback: - self.view.remove_button.setHidden(True) + self.layout().remove_button.setHidden(True) return super().leaveEvent(event) @override def resizeEvent(self, event: QResizeEvent) -> None: - self.view.title_widget.setFixedWidth(int(event.size().width() // 1.5)) + self.layout().title_widget.setFixedWidth(int(event.size().width() // 1.5)) return super().resizeEvent(event) diff --git a/src/tagstudio/qt/controllers/tiles/tile_data.py b/src/tagstudio/qt/controllers/tiles/tile_data.py new file mode 100644 index 00000000..9198859a --- /dev/null +++ b/src/tagstudio/qt/controllers/tiles/tile_data.py @@ -0,0 +1,14 @@ +# SPDX-FileCopyrightText: (c) TagStudio Contributors +# SPDX-License-Identifier: GPL-3.0-only + + +from PySide6.QtWidgets import QLayout, QWidget + + +class TileData(QWidget): + """A base class for widgets that go in a Tile widget.""" + + def __init__(self, title: str, view: QLayout) -> None: + super().__init__() + self.title: str = title + self.setLayout(view) diff --git a/src/tagstudio/qt/controllers/entry_data_box_list.py b/src/tagstudio/qt/controllers/tiles/tile_list.py similarity index 84% rename from src/tagstudio/qt/controllers/entry_data_box_list.py rename to src/tagstudio/qt/controllers/tiles/tile_list.py index d521c896..1a5aa0cd 100644 --- a/src/tagstudio/qt/controllers/entry_data_box_list.py +++ b/src/tagstudio/qt/controllers/tiles/tile_list.py @@ -6,6 +6,7 @@ import typing from collections.abc import Callable from datetime import datetime as dt from functools import partial +from typing import override from warnings import catch_warnings import structlog @@ -23,12 +24,12 @@ from tagstudio.core.library.alchemy.models import Entry, Tag from tagstudio.core.utils.types import unwrap from tagstudio.i18n.translations import FIELD_TYPE_KEYS, Translations from tagstudio.qt.controllers.edit_text import EditText -from tagstudio.qt.controllers.entry_data_box import EntryDataBox from tagstudio.qt.controllers.modal import Modal -from tagstudio.qt.controllers.tag_box import TagBoxWidget +from tagstudio.qt.controllers.tiles.tag_data import TagData +from tagstudio.qt.controllers.tiles.text_data import TextData +from tagstudio.qt.controllers.tiles.tile import Tile from tagstudio.qt.mixed.datetime_picker import DatetimePicker -from tagstudio.qt.mixed.text_field import TextContainerWidget -from tagstudio.qt.views.entry_data_box_list_view import EntryDataBoxListView +from tagstudio.qt.views.tiles.tile_list_view import TileListView if typing.TYPE_CHECKING: from tagstudio.qt.qt_driver import QtDriver @@ -36,8 +37,8 @@ if typing.TYPE_CHECKING: logger = structlog.get_logger(__name__) -class EntryDataBoxList(QWidget): - """Widget for the tag and field boxes displayed inside the Inspector.""" +class TileList(QWidget): + """A styled list of Tile widgets.""" on_tags_update = Signal() @@ -51,10 +52,13 @@ class EntryDataBoxList(QWidget): self.common_fields: list = [] # TODO: Reimplement self.mixed_fields: list = [] # TODO: Reimplement self.cached_entries: list[Entry] = [] - self._data_boxes: list[EntryDataBox] = [] + self._tiles: list[Tile] = [] - self.view = EntryDataBoxListView() - self.setLayout(self.view) + self.setLayout(TileListView()) + + @override + def layout(self) -> TileListView: + return super().layout() # pyright: ignore[reportReturnType] @property def top_entry_id(self) -> int: @@ -63,7 +67,7 @@ class EntryDataBoxList(QWidget): def update_from_entry(self, entry_id: int, update_badges: bool = True) -> None: """Update tags and fields from a single Entry source.""" - logger.warning("[EntryDataBoxList] Updating Selection", entry_id=entry_id) + logger.warning("[TileList] Updating Selection", entry_id=entry_id) entry = unwrap(self.lib.get_entry_full(entry_id)) self.cached_entries = [entry] @@ -79,9 +83,7 @@ class EntryDataBoxList(QWidget): if entry_tags: categories = self.get_tag_categories(entry_tags) for cat, tags in sorted(categories.items(), key=lambda kv: (kv[0] is None, kv)): - self.write_tag_container( - container_index, tags=tags, category_tag=cat, is_mixed=False - ) + self.write_tag_tile(container_index, tags=tags, category_tag=cat, is_mixed=False) container_index += 1 container_len += 1 if update_badges: @@ -89,11 +91,11 @@ class EntryDataBoxList(QWidget): # Write field container(s) for index, field in enumerate(entry_fields, start=container_index): - self.write_field_container(index, field, is_mixed=False) + self.write_field_tile(index, field, is_mixed=False) # Hide leftover container(s) - if len(self._data_boxes) > container_len: - for i, c in enumerate(self._data_boxes): + if len(self._tiles) > container_len: + for i, c in enumerate(self._tiles): if i > (container_len - 1): c.setHidden(True) @@ -112,7 +114,7 @@ class EntryDataBoxList(QWidget): def hide_containers(self) -> None: """Hide all field and tag containers.""" - for c in self._data_boxes: + for c in self._tiles: c.setHidden(True) def get_tag_categories(self, tags: set[Tag]) -> dict[Tag | None, set[Tag]]: @@ -176,7 +178,7 @@ class EntryDataBoxList(QWidget): assert isinstance(field_templates, list) logger.info( - "[EntryDataBoxList][add_field_to_selected]", + "[TileList][add_field_to_selected]", selected=self.driver.selected, fields=[ (field_template.class_name, field_template.id) for field_template in field_templates @@ -186,7 +188,7 @@ class EntryDataBoxList(QWidget): for entry_id in self.driver.selected: for field_template in field_templates: logger.info( - "[EntryDataBoxList][add_field_to_selected] Adding field", + "[TileList][add_field_to_selected] Adding field", name=field_template.name, type=field_template.class_name, ) @@ -200,7 +202,7 @@ class EntryDataBoxList(QWidget): if isinstance(tag_ids, int): tag_ids = [tag_ids] logger.info( - "[EntryDataBoxList][add_tags_to_selected]", + "[TileList][add_tags_to_selected]", selected=self.driver.selected, tag_ids=tag_ids, ) @@ -227,8 +229,8 @@ class EntryDataBoxList(QWidget): self._remove_field(field) self.update_from_entry(entry_id) - def write_field_container(self, index: int, field: BaseField, is_mixed: bool = False) -> None: - """Update/Create data for a field EntryDataBox. + def write_field_tile(self, index: int, field: BaseField, is_mixed: bool = False) -> None: + """Update/Create data for a field Tile. Args: index(int): The container index. @@ -237,9 +239,7 @@ class EntryDataBoxList(QWidget): If True, field is not present in all selected items. """ - def write_text_container( - container: EntryDataBox, field: TextField, title: str, is_mixed: bool - ): + def write_text_tile(container: Tile, field: TextField, title: str, is_mixed: bool): container.set_title(field.name) # Normalize line endings in any text content. @@ -249,7 +249,7 @@ class EntryDataBoxList(QWidget): else: text = f"{Translations['field.mixed_data']}" - inner_widget = TextContainerWidget(title, text) + inner_widget = TextData(title, text) container.set_inner_widget(inner_widget) if not is_mixed: @@ -272,7 +272,7 @@ class EntryDataBoxList(QWidget): ) def write_datetime_container( - container: EntryDataBox, field: DatetimeField, title: str, is_mixed: bool + container: Tile, field: DatetimeField, title: str, is_mixed: bool ): container.set_title(field.name) @@ -287,7 +287,7 @@ class EntryDataBoxList(QWidget): else: text = f"{Translations['field.mixed_data']}" - inner_widget = TextContainerWidget(title, text) + inner_widget = TextData(title, text) container.set_inner_widget(inner_widget) if not is_mixed: @@ -311,7 +311,7 @@ class EntryDataBoxList(QWidget): def write_unknown_container(): container.set_title(field.name) - inner_widget = TextContainerWidget(title, field.name) + inner_widget = TextData(title, field.name) container.set_inner_widget(inner_widget) container.set_remove_callback( lambda: self.remove_message_box( @@ -321,19 +321,19 @@ class EntryDataBoxList(QWidget): ) logger.info( - "[EntryDataBoxList][write_container]", + "[TileList][write_container]", index=index, name=field.name, type=field.class_name, ) # Create new containers if necessary - if len(self._data_boxes) < (index + 1): - container = EntryDataBox() - self._data_boxes.append(container) - self.view.scroll_layout.addWidget(container) + if len(self._tiles) < (index + 1): + container = Tile() + self._tiles.append(container) + self.layout().scroll_layout.addWidget(container) else: - container = self._data_boxes[index] + container = self._tiles[index] # Set field title field_name_key: str = FIELD_TYPE_KEYS.get(field.class_name, "field_type.unknown") @@ -341,7 +341,7 @@ class EntryDataBoxList(QWidget): # Write containers if type(field) is TextField: - write_text_container(container, field, title, is_mixed) + write_text_tile(container, field, title, is_mixed) elif type(field) is DatetimeField: write_datetime_container(container, field, title, is_mixed) else: @@ -349,10 +349,10 @@ class EntryDataBoxList(QWidget): container.setHidden(False) - def write_tag_container( + def write_tag_tile( self, index: int, tags: set[Tag], category_tag: Tag | None = None, is_mixed: bool = False ) -> None: - """Update/Create tag data for a tag EntryDataBox. + """Update/Create tag data for a tag Tile. Args: index(int): The container index. @@ -361,25 +361,25 @@ class EntryDataBoxList(QWidget): is_mixed(bool): Relevant when multiple items are selected. If True, field is not present in all selected items. """ - logger.info("[EntryDataBoxList][write_tag_container]", index=index) - if len(self._data_boxes) < (index + 1): - container = EntryDataBox() - self._data_boxes.append(container) - self.view.scroll_layout.addWidget(container) + logger.info("[TileList][write_tag_tile]", index=index) + if len(self._tiles) < (index + 1): + container = Tile() + self._tiles.append(container) + self.layout().scroll_layout.addWidget(container) else: - container = self._data_boxes[index] + container = self._tiles[index] container.set_title(Translations["entries.tags"] if not category_tag else category_tag.name) if not is_mixed: inner_widget = container.get_inner_widget() - if isinstance(inner_widget, TagBoxWidget): + if isinstance(inner_widget, TagData): with catch_warnings(record=True): inner_widget.on_update.disconnect() else: - inner_widget = TagBoxWidget(Translations["entries.tags"], self.driver) + inner_widget = TagData(Translations["entries.tags"], self.driver) container.set_inner_widget(inner_widget) inner_widget.set_entries([e.id for e in self.cached_entries]) inner_widget.set_tags(tags) @@ -392,7 +392,7 @@ class EntryDataBoxList(QWidget): ) else: text = f"{Translations['field.mixed_data']}" - inner_widget = TextContainerWidget("Mixed Tags", text) # NOTE: Unlocalized but unused + inner_widget = TextData("Mixed Tags", text) # NOTE: Unlocalized but unused container.set_inner_widget(inner_widget) container.set_edit_callback() @@ -402,7 +402,7 @@ class EntryDataBoxList(QWidget): def _remove_field(self, field: BaseField) -> None: """Remove a field from all selected Entries.""" logger.info( - "[EntryDataBoxList] Removing Field", + "[TileList] Removing Field", field=field, selected=[x.path for x in self.cached_entries], ) diff --git a/src/tagstudio/qt/mixed/data_box.py b/src/tagstudio/qt/mixed/data_box.py deleted file mode 100644 index 78f01e2c..00000000 --- a/src/tagstudio/qt/mixed/data_box.py +++ /dev/null @@ -1,14 +0,0 @@ -# SPDX-FileCopyrightText: (c) TagStudio Contributors -# SPDX-License-Identifier: GPL-3.0-only - - -import structlog -from PySide6.QtWidgets import QWidget - -logger = structlog.get_logger(__name__) - - -class DataBox(QWidget): - def __init__(self, title: str) -> None: - super().__init__() - self.title: str = title diff --git a/src/tagstudio/qt/mixed/tag_color_manager.py b/src/tagstudio/qt/mixed/tag_color_manager.py index 88b8471e..19ef50ae 100644 --- a/src/tagstudio/qt/mixed/tag_color_manager.py +++ b/src/tagstudio/qt/mixed/tag_color_manager.py @@ -25,10 +25,10 @@ from tagstudio.core.constants import RESERVED_NAMESPACE_PREFIX from tagstudio.core.enums import ThemePalette from tagstudio.core.utils.types import unwrap from tagstudio.i18n.translations import Translations -from tagstudio.qt.controllers.entry_data_box import EntryDataBox from tagstudio.qt.controllers.modal import Modal +from tagstudio.qt.controllers.tiles.color_data import ColorData +from tagstudio.qt.controllers.tiles.tile import Tile from tagstudio.qt.mixed.build_namespace import BuildNamespacePanel -from tagstudio.qt.mixed.color_box import ColorBoxWidget from tagstudio.qt.views.styles.stylesheets import header logger = structlog.get_logger(__name__) @@ -119,8 +119,8 @@ class TagColorManager(QWidget): for group, colors in self.driver.lib.tag_color_groups.items(): if not group.startswith(RESERVED_NAMESPACE_PREFIX): all_default = False - color_box = ColorBoxWidget(group, colors, self.driver.lib) - color_box.updated.connect( + color_data = ColorData(group, colors, self.driver.lib) + color_data.updated.connect( lambda: ( self.reset(), self.setup_color_groups(), @@ -131,10 +131,10 @@ class TagColorManager(QWidget): ), ) ) - entry_data_box = EntryDataBox(self.driver.lib.get_namespace_name(group)) - entry_data_box.set_inner_widget(color_box) + tile = Tile(self.driver.lib.get_namespace_name(group)) + tile.set_inner_widget(color_data) if not group.startswith(RESERVED_NAMESPACE_PREFIX): - entry_data_box.set_remove_callback( + tile.set_remove_callback( lambda checked=False, g=group: self.delete_namespace_dialog( prompt=Translations["color.namespace.delete.prompt"], callback=lambda namespace=g: ( @@ -150,7 +150,7 @@ class TagColorManager(QWidget): ) ) - self.scroll_layout.addWidget(entry_data_box) + self.scroll_layout.addWidget(tile) if all_default: ns_container = QWidget() diff --git a/src/tagstudio/qt/mixed/text_field.py b/src/tagstudio/qt/mixed/text_field.py deleted file mode 100644 index 4ba36a3b..00000000 --- a/src/tagstudio/qt/mixed/text_field.py +++ /dev/null @@ -1,43 +0,0 @@ -# SPDX-FileCopyrightText: (c) TagStudio Contributors -# SPDX-License-Identifier: GPL-3.0-only - - -import re - -from PySide6.QtCore import Qt -from PySide6.QtWidgets import QHBoxLayout, QLabel - -from tagstudio.qt.mixed.data_box import DataBox - - -# TODO: Split to use MVC guidelines. -class TextContainerWidget(DataBox): - def __init__(self, title: str, text: str) -> None: - super().__init__(title) - self.setObjectName("textBox") - self.base_layout = QHBoxLayout() - self.base_layout.setContentsMargins(0, 0, 0, 0) - self.setLayout(self.base_layout) - self.text_label = QLabel() - self.text_label.setStyleSheet("font-size: 12px") - self.text_label.setWordWrap(True) - self.text_label.setTextFormat(Qt.TextFormat.MarkdownText) - self.text_label.setOpenExternalLinks(True) - self.text_label.setTextInteractionFlags(Qt.TextInteractionFlag.TextBrowserInteraction) - self.base_layout.addWidget(self.text_label) - self.set_text(text) - - def set_text(self, text: str): - text = linkify(text) - self.text_label.setText(text) - - -# Regex from https://stackoverflow.com/a/6041965 -def linkify(text: str): - url_pattern = r"(http|ftp|https):\/\/([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:\/~+#\-*]*[\w@?^=%&\/~+#\-*])" # noqa: E501 - return re.sub( - url_pattern, - lambda url: f'{url.group(0)}', - text, - flags=re.IGNORECASE, - ) diff --git a/src/tagstudio/qt/views/inspector_view.py b/src/tagstudio/qt/views/inspector_view.py index fde9f922..5d7609e9 100644 --- a/src/tagstudio/qt/views/inspector_view.py +++ b/src/tagstudio/qt/views/inspector_view.py @@ -12,11 +12,11 @@ from PySide6.QtWidgets import QHBoxLayout, QLabel, QSplitter, QVBoxLayout, QWidg from tagstudio.core.constants import FFMPEG_HELP_URL from tagstudio.i18n.translations import Translations -from tagstudio.qt.controllers.entry_data_box_list import EntryDataBoxList from tagstudio.qt.controllers.field_suggest_box import FieldSuggestBox from tagstudio.qt.controllers.preview_thumb import PreviewThumb from tagstudio.qt.controllers.return_button import ReturnButton from tagstudio.qt.controllers.tag_suggest_box import TagSuggestBox +from tagstudio.qt.controllers.tiles.tile_list import TileList from tagstudio.qt.mixed.file_attributes import FileAttributes from tagstudio.qt.resource_manager import ResourceManager from tagstudio.qt.views.styles.stylesheets import ( @@ -53,7 +53,7 @@ class InspectorView(QVBoxLayout): self.preview_thumb = PreviewThumb(driver.lib, driver) self.file_attrs = FileAttributes(driver.lib, driver) - self.containers = EntryDataBoxList(driver.lib, driver) + self.containers = TileList(driver.lib, driver) # Visual Preview preview_section = QWidget() diff --git a/src/tagstudio/qt/views/styles/color_overlay.py b/src/tagstudio/qt/views/styles/color_overlay.py index b5097c0b..b98da044 100644 --- a/src/tagstudio/qt/views/styles/color_overlay.py +++ b/src/tagstudio/qt/views/styles/color_overlay.py @@ -23,7 +23,7 @@ def auto_theme_overlay( Args: image (Image): The PIL Image object to apply an overlay to. - inverse (bool): Option inverse the overlay color relative to the current theme. + inverse (bool): Option to invert the overlay color relative to the current theme. use_alpha (bool): Option to retain the base image's alpha value when applying the overlay. """ dark_fg: str = _THEME_DARK_FG[:-2] if not use_alpha else _THEME_DARK_FG diff --git a/src/tagstudio/qt/views/styles/stylesheets.py b/src/tagstudio/qt/views/styles/stylesheets.py index 321514d8..4f06fcb1 100644 --- a/src/tagstudio/qt/views/styles/stylesheets.py +++ b/src/tagstudio/qt/views/styles/stylesheets.py @@ -260,15 +260,15 @@ def color_swatch_style( def container_style() -> str: - """Style used for field containers.""" + """Style used for Tile widgets.""" return f""" - QWidget#entry_data_box{{ + QWidget#tile{{ border-radius: 4px; }} - QWidget#entry_data_box::hover{{ + QWidget#tile::hover{{ background-color: {ThemePalette.COLOR_HOVER.value}; }} - QWidget#entry_data_box::pressed{{ + QWidget#tile::pressed{{ background-color: {ThemePalette.COLOR_PRESSED.value}; }} """ diff --git a/src/tagstudio/qt/views/tag_box_view.py b/src/tagstudio/qt/views/tag_box_view.py deleted file mode 100644 index 78a4b03e..00000000 --- a/src/tagstudio/qt/views/tag_box_view.py +++ /dev/null @@ -1,61 +0,0 @@ -# SPDX-FileCopyrightText: (c) TagStudio Contributors -# SPDX-License-Identifier: GPL-3.0-only - - -from collections.abc import Iterable -from typing import TYPE_CHECKING - -import structlog - -from tagstudio.core.library.alchemy.library import Library -from tagstudio.core.library.alchemy.models import Tag -from tagstudio.qt.mixed.data_box import DataBox -from tagstudio.qt.mixed.tag_widget import TagWidget -from tagstudio.qt.views.layouts.flow_layout import FlowLayout - -if TYPE_CHECKING: - from tagstudio.qt.qt_driver import QtDriver - -logger = structlog.get_logger(__name__) - - -# TODO: Use newer MVC style guidelines -class TagBoxWidgetView(DataBox): - __lib: Library - - def __init__(self, title: str, driver: QtDriver) -> None: - super().__init__(title) - self.__lib = driver.lib - - self.__root_layout = FlowLayout() - self.__root_layout.enable_grid_optimizations(value=False) - self.__root_layout.setContentsMargins(0, 0, 0, 0) - self.setLayout(self.__root_layout) - - def set_tags(self, tags: Iterable[Tag]) -> None: - tags_ = sorted(list(tags), key=lambda tag: self.__lib.tag_display_name(tag)) - logger.info("[TagBoxWidget] Tags:", tags=tags) - while self.__root_layout.itemAt(0): - self.__root_layout.takeAt(0).widget().deleteLater() # pyright: ignore[reportOptionalMemberAccess] - - for tag in tags_: - tag_widget = TagWidget(tag, library=self.__lib, has_edit=True, has_remove=True) - tag_widget.on_click.connect(lambda t=tag: self._on_click(t)) - tag_widget.on_remove.connect(lambda t=tag: self._on_remove(t)) - tag_widget.on_edit.connect(lambda t=tag: self._on_edit(t)) - tag_widget.search_for_tag_action.triggered.connect( - lambda checked=False, t=tag: self._on_search(t) - ) - self.__root_layout.addWidget(tag_widget) - - def _on_click(self, tag: Tag) -> None: - raise NotImplementedError - - def _on_remove(self, tag: Tag) -> None: - raise NotImplementedError - - def _on_edit(self, tag: Tag) -> None: - raise NotImplementedError - - def _on_search(self, tag: Tag) -> None: - raise NotImplementedError diff --git a/src/tagstudio/qt/views/tiles/color_data_view.py b/src/tagstudio/qt/views/tiles/color_data_view.py new file mode 100644 index 00000000..164cac9d --- /dev/null +++ b/src/tagstudio/qt/views/tiles/color_data_view.py @@ -0,0 +1,14 @@ +# SPDX-FileCopyrightText: (c) TagStudio Contributors +# SPDX-License-Identifier: GPL-3.0-only + + +from tagstudio.qt.views.layouts.flow_layout import FlowLayout + + +class ColorDataView(FlowLayout): + """The layout used for a ColorData widget.""" + + def __init__(self) -> None: + super().__init__() + self.enable_grid_optimizations(value=True) + self.setContentsMargins(0, 0, 0, 0) diff --git a/src/tagstudio/qt/views/tiles/tag_data_view.py b/src/tagstudio/qt/views/tiles/tag_data_view.py new file mode 100644 index 00000000..4e985a57 --- /dev/null +++ b/src/tagstudio/qt/views/tiles/tag_data_view.py @@ -0,0 +1,46 @@ +# SPDX-FileCopyrightText: (c) TagStudio Contributors +# SPDX-License-Identifier: GPL-3.0-only + + +from collections.abc import Iterable + +import structlog +from PySide6.QtCore import Signal + +from tagstudio.core.library.alchemy.library import Library +from tagstudio.core.library.alchemy.models import Tag +from tagstudio.qt.mixed.tag_widget import TagWidget +from tagstudio.qt.views.layouts.flow_layout import FlowLayout + +logger = structlog.get_logger(__name__) + + +class TagDataView(FlowLayout): + """The layout used for a TagData widget.""" + + tag_clicked = Signal(Tag) + tag_removed = Signal(Tag) + tag_edited = Signal(Tag) + tag_searched = Signal(Tag) + + def __init__(self, library: Library) -> None: + super().__init__() + self._lib = library + self.enable_grid_optimizations(value=False) + self.setContentsMargins(0, 0, 0, 0) + + def set_tags(self, tags: Iterable[Tag]) -> None: + tags_ = sorted(list(tags), key=lambda tag: self._lib.tag_display_name(tag)) + logger.info("[TagData] Tags:", tags=tags) + while self.itemAt(0): + self.takeAt(0).widget().deleteLater() # pyright: ignore[reportOptionalMemberAccess] + + for tag in tags_: + tag_widget = TagWidget(tag, library=self._lib, has_edit=True, has_remove=True) + tag_widget.on_click.connect(lambda t=tag: self.tag_clicked.emit(t)) + tag_widget.on_remove.connect(lambda t=tag: self.tag_removed.emit(t)) + tag_widget.on_edit.connect(lambda t=tag: self.tag_edited.emit(t)) + tag_widget.search_for_tag_action.triggered.connect( + lambda checked=False, t=tag: self.tag_searched.emit(t) + ) + self.addWidget(tag_widget) diff --git a/src/tagstudio/qt/views/tiles/text_data_view.py b/src/tagstudio/qt/views/tiles/text_data_view.py new file mode 100644 index 00000000..c6b1258e --- /dev/null +++ b/src/tagstudio/qt/views/tiles/text_data_view.py @@ -0,0 +1,22 @@ +# SPDX-FileCopyrightText: (c) TagStudio Contributors +# SPDX-License-Identifier: GPL-3.0-only + + +from PySide6.QtCore import Qt +from PySide6.QtWidgets import QHBoxLayout, QLabel + + +class TextDataView(QHBoxLayout): + """The layout used for a TextData widget.""" + + def __init__(self) -> None: + super().__init__() + self.setContentsMargins(0, 0, 0, 0) + + self.text_label = QLabel() + self.text_label.setStyleSheet("font-size: 12px") + self.text_label.setWordWrap(True) + self.text_label.setTextFormat(Qt.TextFormat.MarkdownText) + self.text_label.setOpenExternalLinks(True) + self.text_label.setTextInteractionFlags(Qt.TextInteractionFlag.TextBrowserInteraction) + self.addWidget(self.text_label) diff --git a/src/tagstudio/qt/views/entry_data_box_list_view.py b/src/tagstudio/qt/views/tiles/tile_list_view.py similarity index 86% rename from src/tagstudio/qt/views/entry_data_box_list_view.py rename to src/tagstudio/qt/views/tiles/tile_list_view.py index db2f2e42..5bdf7e4c 100644 --- a/src/tagstudio/qt/views/entry_data_box_list_view.py +++ b/src/tagstudio/qt/views/tiles/tile_list_view.py @@ -8,7 +8,7 @@ from PySide6.QtWidgets import QFrame, QHBoxLayout, QScrollArea, QSizePolicy, QVB from tagstudio.qt.views.styles.stylesheets import HALF_PAD, PAD, inset_container_style -class EntryDataBoxListView(QHBoxLayout): +class TileListView(QHBoxLayout): def __init__(self) -> None: super().__init__() self.setContentsMargins(0, 0, 0, 0) @@ -19,11 +19,11 @@ class EntryDataBoxListView(QHBoxLayout): self.scroll_layout.setSpacing(PAD) scroll_container = QWidget() - scroll_container.setObjectName("entryScrollContainer") + scroll_container.setObjectName("tile_scroll_container") scroll_container.setLayout(self.scroll_layout) self.scroll_area = QScrollArea() - self.scroll_area.setObjectName("entryScrollArea") + self.scroll_area.setObjectName("tile_scroll_area") self.scroll_area.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) self.scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded) self.scroll_area.setWidgetResizable(True) @@ -34,7 +34,7 @@ class EntryDataBoxListView(QHBoxLayout): # background and NOT the scroll container background, so that the # rounded corners are maintained when scrolling. I was unable to # find the right trick to only select that particular element. - self.scroll_area.setStyleSheet(inset_container_style("entryScrollContainer")) + self.scroll_area.setStyleSheet(inset_container_style("tile_scroll_container")) self.scroll_area.setWidget(scroll_container) self.addWidget(self.scroll_area) diff --git a/src/tagstudio/qt/views/entry_data_box_view.py b/src/tagstudio/qt/views/tiles/tile_view.py similarity index 97% rename from src/tagstudio/qt/views/entry_data_box_view.py rename to src/tagstudio/qt/views/tiles/tile_view.py index 8bfe994f..23c99ced 100644 --- a/src/tagstudio/qt/views/entry_data_box_view.py +++ b/src/tagstudio/qt/views/tiles/tile_view.py @@ -17,7 +17,9 @@ _ICON_MARGIN = 4 _ICON_SIZE = _BUTTON_SIZE - _ICON_MARGIN -class EntryDataBoxView(QVBoxLayout): +class TileView(QVBoxLayout): + """The layout used for a Tile widget.""" + _rm = ResourceManager() copy_icon = auto_theme_overlay(_rm.copy, inverse=True) edit_icon = auto_theme_overlay(_rm.edit, inverse=True) diff --git a/tests/qt/test_entry_data_box_list.py b/tests/qt/test_tile_list.py similarity index 93% rename from tests/qt/test_entry_data_box_list.py rename to tests/qt/test_tile_list.py index 5eaef11b..ac9fa0d8 100644 --- a/tests/qt/test_entry_data_box_list.py +++ b/tests/qt/test_tile_list.py @@ -21,7 +21,7 @@ def test_update_selection_empty(qt_driver: QtDriver): panel.set_selection(qt_driver.selected) # FieldContainer should hide all containers - for container in panel.containers._data_boxes: + for container in panel.containers._tiles: assert container.isHidden() @@ -33,7 +33,7 @@ def test_update_selection_single(qt_driver: QtDriver, entry_full: Entry): panel.set_selection(qt_driver.selected) # FieldContainer should show all applicable tags and field containers - for container in panel.containers._data_boxes: + for container in panel.containers._tiles: assert not container.isHidden() @@ -48,7 +48,7 @@ def test_update_selection_multiple(qt_driver: QtDriver): panel.set_selection(qt_driver.selected) # FieldContainer should show mixed field editing - for container in panel.containers._data_boxes: + for container in panel.containers._tiles: assert container.isHidden() @@ -137,8 +137,8 @@ def test_meta_tag_category(qt_driver: QtDriver, entry_full: Entry): panel.set_selection(qt_driver.selected) # FieldContainer should hide all containers - assert len(panel.containers._data_boxes) == 3 - for i, container in enumerate(panel.containers._data_boxes): + assert len(panel.containers._tiles) == 3 + for i, container in enumerate(panel.containers._tiles): match i: case 0: # Check if the container is the Meta Tags category @@ -170,8 +170,8 @@ def test_custom_tag_category(qt_driver: QtDriver, entry_full: Entry): panel.set_selection(qt_driver.selected) # FieldContainer should hide all containers - assert len(panel.containers._data_boxes) == 3 - for i, container in enumerate(panel.containers._data_boxes): + assert len(panel.containers._tiles) == 3 + for i, container in enumerate(panel.containers._tiles): match i: case 0: # Check if the container is the Meta Tags category @@ -206,5 +206,5 @@ def test_exclude_tag_category( qt_driver.toggle_item_selection(entry.id, append=False, bridge=False) panel.set_selection(qt_driver.selected) - assert len(panel.containers._data_boxes) == 1 - assert panel.containers._data_boxes[0].title == "

Tags

" + assert len(panel.containers._tiles) == 1 + assert panel.containers._tiles[0].title == "

Tags

"