mirror of
https://github.com/TagStudioDev/TagStudio.git
synced 2026-09-15 06:59:34 +02:00
refactor(ui): rename FieldContainers to Tiles
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
+16
-15
@@ -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()
|
||||
+41
-29
@@ -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)
|
||||
)
|
||||
@@ -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'<a href="{url.group(0)}">{url.group(0)}</a>',
|
||||
text,
|
||||
flags=re.IGNORECASE,
|
||||
)
|
||||
+30
-25
@@ -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)
|
||||
@@ -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)
|
||||
+48
-48
@@ -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"<i>{Translations['field.mixed_data']}</i>"
|
||||
|
||||
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"<i>{Translations['field.mixed_data']}</i>"
|
||||
|
||||
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"<i>{Translations['field.mixed_data']}</i>"
|
||||
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],
|
||||
)
|
||||
@@ -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
|
||||
@@ -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()
|
||||
|
||||
@@ -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'<a href="{url.group(0)}">{url.group(0)}</a>',
|
||||
text,
|
||||
flags=re.IGNORECASE,
|
||||
)
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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};
|
||||
}}
|
||||
"""
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
+4
-4
@@ -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)
|
||||
+3
-1
@@ -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)
|
||||
@@ -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 == "<h4>Tags</h4>"
|
||||
assert len(panel.containers._tiles) == 1
|
||||
assert panel.containers._tiles[0].title == "<h4>Tags</h4>"
|
||||
Reference in New Issue
Block a user