refactor(preview_panel): mvc split (#952)

* refactor: basic split

* fix: renaming and usage test didn't work for the tests

* fix: tests

* refactor: restructuring

* refactor: further separation and lots of related changes

* refactor: remove last reference to a widget from controller

* refactor: address todo

* fix: failing tests and mypy compaint

* refactor: move control logic to controller

* refactor: more readable button style

* fix: set_selection was called with invalid argument
This commit is contained in:
Jann Stute
2025-08-01 07:53:32 +02:00
committed by GitHub
parent 7176908274
commit 192af25f6f
18 changed files with 355 additions and 287 deletions

View File

@@ -84,7 +84,8 @@ qt_api = "pyside6"
[tool.pyright]
ignore = [".venv/**"]
include = ["src/tagstudio/**"]
include = ["src/tagstudio", "tests"]
extraPaths = ["src/tagstudio", "tests"]
reportAny = false
reportIgnoreCommentWithoutRule = false
reportImplicitStringConcatenation = false

View File

@@ -674,7 +674,7 @@ class Library:
start_time = time.time()
entry = session.scalar(entry_stmt)
if with_tags:
tags = set(session.scalars(tag_stmt)) # pyright: ignore [reportPossiblyUnboundVariable]
tags = set(session.scalars(tag_stmt)) # pyright: ignore[reportPossiblyUnboundVariable]
end_time = time.time()
logger.info(
f"[Library] Time it took to get entry: "

View File

@@ -0,0 +1,44 @@
import typing
from warnings import catch_warnings
from PySide6.QtWidgets import QListWidgetItem
from tagstudio.core.library.alchemy.library import Library
from tagstudio.qt.modals.add_field import AddFieldModal
from tagstudio.qt.modals.tag_search import TagSearchModal
from tagstudio.qt.view.widgets.preview_panel_view import PreviewPanelView
if typing.TYPE_CHECKING:
from tagstudio.qt.ts_qt import QtDriver
class PreviewPanel(PreviewPanelView):
def __init__(self, library: Library, driver: "QtDriver"):
super().__init__(library, driver)
self.__add_field_modal = AddFieldModal(self.lib)
self.__add_tag_modal = TagSearchModal(self.lib, is_tag_chooser=True)
def _add_field_button_callback(self):
self.__add_field_modal.show()
def _add_tag_button_callback(self):
self.__add_tag_modal.show()
def _set_selection_callback(self):
with catch_warnings(record=True):
self.__add_field_modal.done.disconnect()
self.__add_tag_modal.tsp.tag_chosen.disconnect()
self.__add_field_modal.done.connect(self._add_field_to_selected)
self.__add_tag_modal.tsp.tag_chosen.connect(self._add_tag_to_selected)
def _add_field_to_selected(self, field_list: list[QListWidgetItem]):
self._fields.add_field_to_selected(field_list)
if len(self._selected) == 1:
self._fields.update_from_entry(self._selected[0])
def _add_tag_to_selected(self, tag_id: int):
self._fields.add_tags_to_selected(tag_id)
if len(self._selected) == 1:
self._fields.update_from_entry(self._selected[0])

View File

@@ -33,12 +33,12 @@ from PySide6.QtWidgets import (
from tagstudio.core.enums import ShowFilepathOption
from tagstudio.core.library.alchemy.enums import SortingModeEnum
from tagstudio.qt.controller.widgets.preview_panel_controller import PreviewPanel
from tagstudio.qt.flowlayout import FlowLayout
from tagstudio.qt.pagination import Pagination
from tagstudio.qt.platform_strings import trash_term
from tagstudio.qt.translations import Translations
from tagstudio.qt.widgets.landing import LandingWidget
from tagstudio.qt.widgets.preview_panel import PreviewPanel
# Only import for type checking/autocompletion, will not be imported at runtime.
if typing.TYPE_CHECKING:

View File

@@ -30,7 +30,7 @@ from tagstudio.core.library.alchemy.library import Library
from tagstudio.core.library.alchemy.models import Tag, TagColorGroup
from tagstudio.core.palette import ColorType, UiColor, get_tag_color, get_ui_color
from tagstudio.qt.modals.tag_color_selection import TagColorSelection
from tagstudio.qt.modals.tag_search import TagSearchPanel
from tagstudio.qt.modals.tag_search import TagSearchModal
from tagstudio.qt.translations import Translations
from tagstudio.qt.widgets.panel import PanelModal, PanelWidget
from tagstudio.qt.widgets.tag import (
@@ -166,9 +166,8 @@ class BuildTagPanel(PanelWidget):
if tag is not None:
exclude_ids.append(tag.id)
tsp = TagSearchPanel(self.lib, exclude_ids)
tsp.tag_chosen.connect(lambda x: self.add_parent_tag_callback(x))
self.add_tag_modal = PanelModal(tsp, Translations["tag.parent_tags.add"])
self.add_tag_modal = TagSearchModal(self.lib, exclude_ids)
self.add_tag_modal.tsp.tag_chosen.connect(lambda x: self.add_parent_tag_callback(x))
self.parent_tags_add_button.clicked.connect(self.add_tag_modal.show)
# Color ----------------------------------------------------------------

View File

@@ -227,7 +227,9 @@ class FoldersToTagsModal(QWidget):
def on_apply(self):
folders_to_tags(self.library)
self.close()
self.driver.main_window.preview_panel.update_widgets(update_preview=False)
self.driver.main_window.preview_panel.set_selection(
self.driver.selected, update_preview=False
)
@override
def showEvent(self, event: QtGui.QShowEvent):

View File

@@ -87,7 +87,7 @@ class MirrorEntriesModal(QWidget):
pw.from_iterable_function(
self.mirror_entries_runnable,
displayed_text,
self.driver.main_window.preview_panel.update_widgets,
lambda s=self.driver.selected: self.driver.main_window.preview_panel.set_selection(s),
self.done.emit,
)

View File

@@ -263,7 +263,7 @@ class SettingsPanel(PanelWidget):
# Apply changes
# Show File Path
driver.update_recent_lib_menu()
driver.main_window.preview_panel.update_widgets()
driver.main_window.preview_panel.set_selection(self.driver.selected)
library_directory = driver.lib.library_dir
if settings["show_filepath"] == ShowFilepathOption.SHOW_FULL_PATHS:
display_path = library_directory or ""

View File

@@ -124,7 +124,7 @@ class TagColorManager(QWidget):
self.setup_color_groups(),
()
if len(self.driver.selected) < 1
else self.driver.main_window.preview_panel.fields.update_from_entry(
else self.driver.main_window.preview_panel.field_containers_widget.update_from_entry( # noqa: E501
self.driver.selected[0], update_badges=False
),
)
@@ -141,7 +141,7 @@ class TagColorManager(QWidget):
self.setup_color_groups(),
()
if len(self.driver.selected) < 1
else self.driver.main_window.preview_panel.fields.update_from_entry(
else self.driver.main_window.preview_panel.field_containers_widget.update_from_entry( # noqa: E501
self.driver.selected[0], update_badges=False
),
),

View File

@@ -4,7 +4,7 @@
import contextlib
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Union
from warnings import catch_warnings
import structlog
@@ -39,10 +39,32 @@ if TYPE_CHECKING:
from tagstudio.qt.ts_qt import QtDriver
class TagSearchModal(PanelModal):
tsp: "TagSearchPanel"
def __init__(
self,
library: Library,
exclude: list[int] | None = None,
is_tag_chooser: bool = True,
done_callback=None,
save_callback=None,
has_save=False,
):
self.tsp = TagSearchPanel(library, exclude, is_tag_chooser)
super().__init__(
self.tsp,
Translations["tag.add.plural"],
done_callback=done_callback,
save_callback=save_callback,
has_save=has_save,
)
class TagSearchPanel(PanelWidget):
tag_chosen = Signal(int)
lib: Library
driver: "QtDriver"
driver: Union["QtDriver", None]
is_initialized: bool = False
first_tag_id: int | None = None
is_tag_chooser: bool
@@ -56,7 +78,7 @@ class TagSearchPanel(PanelWidget):
def __init__(
self,
library: Library,
exclude: list[int] = None,
exclude: list[int] | None = None,
is_tag_chooser: bool = True,
):
super().__init__()
@@ -194,6 +216,7 @@ class TagSearchPanel(PanelWidget):
create_button: QPushButton | None = None
if self.create_button_in_layout and self.scroll_layout.count():
create_button = self.scroll_layout.takeAt(self.scroll_layout.count() - 1).widget() # type: ignore
assert create_button is not None
create_button.deleteLater()
self.create_button_in_layout = False
@@ -264,7 +287,8 @@ class TagSearchPanel(PanelWidget):
self.scroll_layout.addWidget(new_tw)
# Assign the tag to the widget at the given index.
tag_widget: TagWidget = self.scroll_layout.itemAt(index).widget()
tag_widget: TagWidget = self.scroll_layout.itemAt(index).widget() # pyright: ignore[reportAssignmentType]
assert isinstance(tag_widget, TagWidget)
tag_widget.set_tag(tag)
# Set tag widget viability and potentially return early
@@ -288,11 +312,11 @@ class TagSearchPanel(PanelWidget):
tag_widget.on_remove.connect(lambda t=tag: self.delete_tag(t))
tag_widget.bg_button.clicked.connect(lambda: self.tag_chosen.emit(tag_id))
if self.driver:
if self.driver is not None:
tag_widget.search_for_tag_action.triggered.connect(
lambda checked=False, tag_id=tag.id: (
self.driver.main_window.search_field.setText(f"tag_id:{tag_id}"),
self.driver.update_browsing_state(BrowsingState.from_tag_id(tag_id)),
lambda checked=False, tag_id=tag.id, driver=self.driver: (
driver.main_window.search_field.setText(f"tag_id:{tag_id}"),
driver.update_browsing_state(BrowsingState.from_tag_id(tag_id)),
)
)
tag_widget.search_for_tag_action.setEnabled(True)

View File

@@ -83,7 +83,7 @@ from tagstudio.qt.modals.folders_to_tags import FoldersToTagsModal
from tagstudio.qt.modals.settings_panel import SettingsPanel
from tagstudio.qt.modals.tag_color_manager import TagColorManager
from tagstudio.qt.modals.tag_database import TagDatabasePanel
from tagstudio.qt.modals.tag_search import TagSearchPanel
from tagstudio.qt.modals.tag_search import TagSearchModal
from tagstudio.qt.platform_strings import trash_term
from tagstudio.qt.resource_manager import ResourceManager
from tagstudio.qt.splash import Splash
@@ -173,7 +173,6 @@ class QtDriver(DriverMixin, QObject):
tag_manager_panel: PanelModal | None = None
color_manager_panel: TagColorManager | None = None
file_extension_panel: PanelModal | None = None
tag_search_panel: TagSearchPanel | None = None
add_tag_modal: PanelModal | None = None
folders_modal: FoldersToTagsModal
about_modal: AboutModal
@@ -364,8 +363,8 @@ class QtDriver(DriverMixin, QObject):
self.tag_manager_panel = PanelModal(
widget=TagDatabasePanel(self, self.lib),
title=Translations["tag_manager.title"],
done_callback=lambda: self.main_window.preview_panel.update_widgets(
update_preview=False
done_callback=lambda s=self.selected: self.main_window.preview_panel.set_selection(
s, update_preview=False
),
has_save=False,
)
@@ -374,16 +373,12 @@ class QtDriver(DriverMixin, QObject):
self.color_manager_panel = TagColorManager(self)
# Initialize the Tag Search panel
self.tag_search_panel = TagSearchPanel(self.lib, is_tag_chooser=True)
self.tag_search_panel.set_driver(self)
self.add_tag_modal = PanelModal(
widget=self.tag_search_panel,
title=Translations["tag.add.plural"],
)
self.tag_search_panel.tag_chosen.connect(
lambda t: (
self.add_tag_modal = TagSearchModal(self.lib, is_tag_chooser=True)
self.add_tag_modal.tsp.set_driver(self)
self.add_tag_modal.tsp.tag_chosen.connect(
lambda t, s=self.selected: (
self.add_tags_to_selected_callback(t),
self.main_window.preview_panel.update_widgets(),
self.main_window.preview_panel.set_selection(s),
)
)
@@ -536,12 +531,12 @@ class QtDriver(DriverMixin, QObject):
self.main_window.search_field.textChanged.connect(self.update_completions_list)
self.main_window.preview_panel.fields.archived_updated.connect(
self.main_window.preview_panel.field_containers_widget.archived_updated.connect(
lambda hidden: self.update_badges(
{BadgeType.ARCHIVED: hidden}, origin_id=0, add_tags=False
)
)
self.main_window.preview_panel.fields.favorite_updated.connect(
self.main_window.preview_panel.field_containers_widget.favorite_updated.connect(
lambda hidden: self.update_badges(
{BadgeType.FAVORITE: hidden}, origin_id=0, add_tags=False
)
@@ -709,7 +704,7 @@ class QtDriver(DriverMixin, QObject):
self.cached_values.sync()
# Reset library state
self.main_window.preview_panel.update_widgets()
self.main_window.preview_panel.set_selection(self.selected)
self.main_window.search_field.setText("")
scrollbar: QScrollArea = self.main_window.entry_scroll_area
scrollbar.verticalScrollBar().setValue(0)
@@ -733,7 +728,7 @@ class QtDriver(DriverMixin, QObject):
self.set_clipboard_menu_viability()
self.set_select_actions_visibility()
self.main_window.preview_panel.update_widgets()
self.main_window.preview_panel.set_selection(self.selected)
self.main_window.toggle_landing_page(enabled=True)
self.main_window.pagination.setHidden(True)
try:
@@ -811,7 +806,7 @@ class QtDriver(DriverMixin, QObject):
self.set_clipboard_menu_viability()
self.set_select_actions_visibility()
self.main_window.preview_panel.update_widgets(update_preview=False)
self.main_window.preview_panel.set_selection(self.selected, update_preview=False)
def select_inverse_action_callback(self):
"""Invert the selection of all visible items."""
@@ -830,7 +825,7 @@ class QtDriver(DriverMixin, QObject):
self.set_clipboard_menu_viability()
self.set_select_actions_visibility()
self.main_window.preview_panel.update_widgets(update_preview=False)
self.main_window.preview_panel.set_selection(self.selected, update_preview=False)
def clear_select_action_callback(self):
self.selected.clear()
@@ -839,7 +834,7 @@ class QtDriver(DriverMixin, QObject):
item.thumb_button.set_selected(False)
self.set_clipboard_menu_viability()
self.main_window.preview_panel.update_widgets()
self.main_window.preview_panel.set_selection(self.selected)
def add_tags_to_selected_callback(self, tag_ids: list[int]):
self.lib.add_tags_to_entries(self.selected, tag_ids)
@@ -884,7 +879,7 @@ class QtDriver(DriverMixin, QObject):
for i, tup in enumerate(pending):
e_id, f = tup
if (origin_path == f) or (not origin_path):
self.main_window.preview_panel.thumb.media_player.stop()
self.main_window.preview_panel.thumb_media_player_stop()
if delete_file(self.lib.library_dir / f):
self.main_window.status_bar.showMessage(
Translations.format(
@@ -899,7 +894,7 @@ class QtDriver(DriverMixin, QObject):
if deleted_count > 0:
self.update_browsing_state()
self.main_window.preview_panel.update_widgets()
self.main_window.preview_panel.set_selection(self.selected)
if len(self.selected) <= 1 and deleted_count == 0:
self.main_window.status_bar.showMessage(Translations["status.deleted_none"])
@@ -1224,7 +1219,7 @@ class QtDriver(DriverMixin, QObject):
if TAG_FAVORITE in self.copy_buffer["tags"]:
self.update_badges({BadgeType.FAVORITE: True}, origin_id=0, add_tags=False)
else:
self.main_window.preview_panel.update_widgets()
self.main_window.preview_panel.set_selection(self.selected)
def toggle_item_selection(self, item_id: int, append: bool, bridge: bool):
"""Toggle the selection of an item in the Thumbnail Grid.
@@ -1298,7 +1293,7 @@ class QtDriver(DriverMixin, QObject):
self.set_clipboard_menu_viability()
self.set_select_actions_visibility()
self.main_window.preview_panel.update_widgets()
self.main_window.preview_panel.set_selection(self.selected)
def set_clipboard_menu_viability(self):
if len(self.selected) == 1:
@@ -1742,7 +1737,7 @@ class QtDriver(DriverMixin, QObject):
self.main_window.menu_bar.clear_thumb_cache_action.setEnabled(True)
self.main_window.menu_bar.folders_to_tags_action.setEnabled(True)
self.main_window.preview_panel.update_widgets()
self.main_window.preview_panel.set_selection(self.selected)
# page (re)rendering, extract eventually
self.update_browsing_state()

View File

@@ -0,0 +1,208 @@
import traceback
import typing
from pathlib import Path
import structlog
from PySide6.QtCore import Qt
from PySide6.QtWidgets import (
QHBoxLayout,
QPushButton,
QSplitter,
QVBoxLayout,
QWidget,
)
from tagstudio.core.enums import Theme
from tagstudio.core.library.alchemy.library import Library
from tagstudio.core.library.alchemy.models import Entry
from tagstudio.core.palette import ColorType, UiColor, get_ui_color
from tagstudio.qt.translations import Translations
from tagstudio.qt.widgets.preview.field_containers import FieldContainers
from tagstudio.qt.widgets.preview.file_attributes import FileAttributes
from tagstudio.qt.widgets.preview.preview_thumb import PreviewThumb
if typing.TYPE_CHECKING:
from tagstudio.qt.ts_qt import QtDriver
logger = structlog.get_logger(__name__)
BUTTON_STYLE = f"""
QPushButton{{
background-color: {Theme.COLOR_BG.value};
border-radius: 6px;
font-weight: 500;
text-align: center;
}}
QPushButton::hover{{
background-color: {Theme.COLOR_HOVER.value};
border-color: {get_ui_color(ColorType.BORDER, UiColor.THEME_DARK)};
border-style: solid;
border-width: 2px;
}}
QPushButton::pressed{{
background-color: {Theme.COLOR_PRESSED.value};
border-color: {get_ui_color(ColorType.LIGHT_ACCENT, UiColor.THEME_DARK)};
border-style: solid;
border-width: 2px;
}}
QPushButton::disabled{{
background-color: {Theme.COLOR_DISABLED_BG.value};
}}
"""
class PreviewPanelView(QWidget):
lib: Library
_selected: list[int]
def __init__(self, library: Library, driver: "QtDriver"):
super().__init__()
self.lib = library
self.__thumb = PreviewThumb(self.lib, driver)
self.__file_attrs = FileAttributes(self.lib, driver)
self._fields = FieldContainers(
self.lib, driver
) # TODO: this should be name mangled, but is still needed on the controller side atm
preview_section = QWidget()
preview_layout = QVBoxLayout(preview_section)
preview_layout.setContentsMargins(0, 0, 0, 0)
preview_layout.setSpacing(6)
info_section = QWidget()
info_layout = QVBoxLayout(info_section)
info_layout.setContentsMargins(0, 0, 0, 0)
info_layout.setSpacing(6)
splitter = QSplitter()
splitter.setOrientation(Qt.Orientation.Vertical)
splitter.setHandleWidth(12)
add_buttons_container = QWidget()
add_buttons_layout = QHBoxLayout(add_buttons_container)
add_buttons_layout.setContentsMargins(0, 0, 0, 0)
add_buttons_layout.setSpacing(6)
self.__add_tag_button = QPushButton(Translations["tag.add"])
self.__add_tag_button.setEnabled(False)
self.__add_tag_button.setCursor(Qt.CursorShape.PointingHandCursor)
self.__add_tag_button.setMinimumHeight(28)
self.__add_tag_button.setStyleSheet(BUTTON_STYLE)
self.__add_field_button = QPushButton(Translations["library.field.add"])
self.__add_field_button.setEnabled(False)
self.__add_field_button.setCursor(Qt.CursorShape.PointingHandCursor)
self.__add_field_button.setMinimumHeight(28)
self.__add_field_button.setStyleSheet(BUTTON_STYLE)
add_buttons_layout.addWidget(self.__add_tag_button)
add_buttons_layout.addWidget(self.__add_field_button)
preview_layout.addWidget(self.__thumb)
info_layout.addWidget(self.__file_attrs)
info_layout.addWidget(self._fields)
splitter.addWidget(preview_section)
splitter.addWidget(info_section)
splitter.setStretchFactor(1, 2)
root_layout = QVBoxLayout(self)
root_layout.setContentsMargins(0, 0, 0, 0)
root_layout.addWidget(splitter)
root_layout.addWidget(add_buttons_container)
self.__connect_callbacks()
def __connect_callbacks(self):
self.__add_field_button.clicked.connect(self._add_field_button_callback)
self.__add_tag_button.clicked.connect(self._add_tag_button_callback)
def _add_field_button_callback(self):
raise NotImplementedError()
def _add_tag_button_callback(self):
raise NotImplementedError()
def _set_selection_callback(self):
raise NotImplementedError()
def thumb_media_player_stop(self):
self.__thumb.media_player.stop()
def set_selection(self, selected: list[int], update_preview: bool = True):
"""Render the panel widgets with the newest data from the Library.
Args:
selected (list[int]): List of the IDs of the selected entries.
update_preview (bool): Should the file preview be updated?
(Only works with one or more items selected)
"""
self._selected = selected
try:
# No Items Selected
if len(selected) == 0:
self.__thumb.hide_preview()
self.__file_attrs.update_stats()
self.__file_attrs.update_date_label()
self._fields.hide_containers()
self.add_buttons_enabled = False
# One Item Selected
elif len(selected) == 1:
entry_id = selected[0]
entry: Entry | None = self.lib.get_entry(entry_id)
assert entry is not None
assert self.lib.library_dir is not None
filepath: Path = self.lib.library_dir / entry.path
if update_preview:
stats: dict = self.__thumb.update_preview(filepath)
self.__file_attrs.update_stats(filepath, stats)
self.__file_attrs.update_date_label(filepath)
self._fields.update_from_entry(entry_id)
self._set_selection_callback()
self.add_buttons_enabled = True
# Multiple Selected Items
elif len(selected) > 1:
# items: list[Entry] = [self.lib.get_entry_full(x) for x in self.driver.selected]
self.__thumb.hide_preview() # TODO: Render mixed selection
self.__file_attrs.update_multi_selection(len(selected))
self.__file_attrs.update_date_label()
self._fields.hide_containers() # TODO: Allow for mixed editing
self._set_selection_callback()
self.add_buttons_enabled = True
except Exception as e:
logger.error("[Preview Panel] Error updating selection", error=e)
traceback.print_exc()
@property
def add_buttons_enabled(self) -> bool: # needed for the tests
field = self.__add_field_button.isEnabled()
tag = self.__add_tag_button.isEnabled()
assert field == tag
return field
@add_buttons_enabled.setter
def add_buttons_enabled(self, enabled: bool):
self.__add_field_button.setEnabled(enabled)
self.__add_tag_button.setEnabled(enabled)
@property
def _file_attributes_widget(self) -> FileAttributes: # needed for the tests
"""Getter for the file attributes widget."""
return self.__file_attrs
@property
def field_containers_widget(self) -> FieldContainers: # needed for the tests
"""Getter for the field containers widget."""
return self._fields

View File

@@ -502,9 +502,9 @@ class ItemThumb(FlowWidget):
toggle_value: bool,
tag_id: int,
):
if entry_id in self.driver.selected and self.driver.main_window.preview_panel.is_open:
if entry_id in self.driver.selected:
if len(self.driver.selected) == 1:
self.driver.main_window.preview_panel.fields.update_toggled_tag(
self.driver.main_window.preview_panel.field_containers_widget.update_toggled_tag(
tag_id, toggle_value
)
else:

View File

@@ -1,203 +0,0 @@
# Copyright (C) 2025 Travis Abendshien (CyanVoxel).
# Licensed under the GPL-3.0 License.
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
import traceback
import typing
from pathlib import Path
from warnings import catch_warnings
import structlog
from PySide6.QtCore import Qt
from PySide6.QtWidgets import QHBoxLayout, QPushButton, QSplitter, QVBoxLayout, QWidget
from tagstudio.core.enums import Theme
from tagstudio.core.library.alchemy.library import Library
from tagstudio.core.library.alchemy.models import Entry
from tagstudio.core.palette import ColorType, UiColor, get_ui_color
from tagstudio.qt.modals.add_field import AddFieldModal
from tagstudio.qt.modals.tag_search import TagSearchPanel
from tagstudio.qt.translations import Translations
from tagstudio.qt.widgets.panel import PanelModal
from tagstudio.qt.widgets.preview.field_containers import FieldContainers
from tagstudio.qt.widgets.preview.file_attributes import FileAttributes
from tagstudio.qt.widgets.preview.preview_thumb import PreviewThumb
if typing.TYPE_CHECKING:
from tagstudio.qt.ts_qt import QtDriver
logger = structlog.get_logger(__name__)
class PreviewPanel(QWidget):
"""The Preview Panel Widget."""
# TODO: There should be a global button theme somewhere.
button_style = (
f"QPushButton{{"
f"background-color:{Theme.COLOR_BG.value};"
"border-radius:6px;"
"font-weight: 500;"
"text-align: center;"
f"}}"
f"QPushButton::hover{{"
f"background-color:{Theme.COLOR_HOVER.value};"
f"border-color:{get_ui_color(ColorType.BORDER, UiColor.THEME_DARK)};"
f"border-style:solid;"
f"border-width: 2px;"
f"}}"
f"QPushButton::pressed{{"
f"background-color:{Theme.COLOR_PRESSED.value};"
f"border-color:{get_ui_color(ColorType.LIGHT_ACCENT, UiColor.THEME_DARK)};"
f"border-style:solid;"
f"border-width: 2px;"
f"}}"
f"QPushButton::disabled{{"
f"background-color:{Theme.COLOR_DISABLED_BG.value};"
f"}}"
)
def __init__(self, library: Library, driver: "QtDriver"):
super().__init__()
self.lib = library
self.driver: QtDriver = driver
self.initialized = False
self.is_open: bool = True
self.thumb = PreviewThumb(library, driver)
self.file_attrs = FileAttributes(library, driver)
self.fields = FieldContainers(library, driver)
self.tag_search_panel = TagSearchPanel(self.driver.lib, is_tag_chooser=True)
self.add_tag_modal = PanelModal(self.tag_search_panel, Translations["tag.add.plural"])
self.add_field_modal = AddFieldModal(self.lib)
preview_section = QWidget()
preview_layout = QVBoxLayout(preview_section)
preview_layout.setContentsMargins(0, 0, 0, 0)
preview_layout.setSpacing(6)
info_section = QWidget()
info_layout = QVBoxLayout(info_section)
info_layout.setContentsMargins(0, 0, 0, 0)
info_layout.setSpacing(6)
splitter = QSplitter()
splitter.setOrientation(Qt.Orientation.Vertical)
splitter.setHandleWidth(12)
add_buttons_container = QWidget()
add_buttons_layout = QHBoxLayout(add_buttons_container)
add_buttons_layout.setContentsMargins(0, 0, 0, 0)
add_buttons_layout.setSpacing(6)
self.add_tag_button = QPushButton(Translations["tag.add"])
self.add_tag_button.setEnabled(False)
self.add_tag_button.setCursor(Qt.CursorShape.PointingHandCursor)
self.add_tag_button.setMinimumHeight(28)
self.add_tag_button.setStyleSheet(PreviewPanel.button_style)
self.add_field_button = QPushButton(Translations["library.field.add"])
self.add_field_button.setEnabled(False)
self.add_field_button.setCursor(Qt.CursorShape.PointingHandCursor)
self.add_field_button.setMinimumHeight(28)
self.add_field_button.setStyleSheet(PreviewPanel.button_style)
add_buttons_layout.addWidget(self.add_tag_button)
add_buttons_layout.addWidget(self.add_field_button)
preview_layout.addWidget(self.thumb)
info_layout.addWidget(self.file_attrs)
info_layout.addWidget(self.fields)
splitter.addWidget(preview_section)
splitter.addWidget(info_section)
splitter.setStretchFactor(1, 2)
root_layout = QVBoxLayout(self)
root_layout.setContentsMargins(0, 0, 0, 0)
root_layout.addWidget(splitter)
root_layout.addWidget(add_buttons_container)
def update_widgets(self, update_preview: bool = True) -> bool:
"""Render the panel widgets with the newest data from the Library.
Args:
update_preview(bool): Should the file preview be updated?
(Only works with one or more items selected)
"""
# No Items Selected
try:
if len(self.driver.selected) == 0:
self.thumb.hide_preview()
self.file_attrs.update_stats()
self.file_attrs.update_date_label()
self.fields.hide_containers()
self.add_tag_button.setEnabled(False)
self.add_field_button.setEnabled(False)
# One Item Selected
elif len(self.driver.selected) == 1:
entry: Entry = self.lib.get_entry(self.driver.selected[0])
entry_id = self.driver.selected[0]
filepath: Path = self.lib.library_dir / entry.path
if update_preview:
stats: dict = self.thumb.update_preview(filepath)
self.file_attrs.update_stats(filepath, stats)
self.file_attrs.update_date_label(filepath)
self.fields.update_from_entry(entry_id)
self.update_add_tag_button(entry_id)
self.update_add_field_button(entry_id)
self.add_tag_button.setEnabled(True)
self.add_field_button.setEnabled(True)
# Multiple Selected Items
elif len(self.driver.selected) > 1:
# items: list[Entry] = [self.lib.get_entry_full(x) for x in self.driver.selected]
self.thumb.hide_preview() # TODO: Render mixed selection
self.file_attrs.update_multi_selection(len(self.driver.selected))
self.file_attrs.update_date_label()
self.fields.hide_containers() # TODO: Allow for mixed editing
self.update_add_tag_button()
self.update_add_field_button()
self.add_tag_button.setEnabled(True)
self.add_field_button.setEnabled(True)
return True
except Exception as e:
logger.error("[Preview Panel] Error updating selection", error=e)
traceback.print_exc()
return False
def update_add_field_button(self, entry_id: int | None = None):
with catch_warnings(record=True):
self.add_field_modal.done.disconnect()
self.add_field_button.clicked.disconnect()
self.add_field_modal.done.connect(
lambda f: (
self.fields.add_field_to_selected(f),
(self.fields.update_from_entry(entry_id) if entry_id else ()),
)
)
self.add_field_button.clicked.connect(self.add_field_modal.show)
def update_add_tag_button(self, entry_id: int = None):
with catch_warnings(record=True):
self.tag_search_panel.tag_chosen.disconnect()
self.add_tag_button.clicked.disconnect()
self.tag_search_panel.tag_chosen.connect(
lambda t: (
self.fields.add_tags_to_selected(t),
(self.fields.update_from_entry(entry_id) if entry_id else ()),
)
)
self.add_tag_button.clicked.connect(self.add_tag_modal.show)

View File

@@ -63,9 +63,9 @@ class TagBoxWidget(FieldWidget):
tag_widget.on_click.connect(lambda t=tag: self.__on_tag_clicked(t))
tag_widget.on_remove.connect(
lambda tag_id=tag.id: (
lambda tag_id=tag.id, s=self.driver.selected: (
self.remove_tag(tag_id),
self.driver.main_window.preview_panel.update_widgets(update_preview=False),
self.driver.main_window.preview_panel.set_selection(s, update_preview=False),
)
)
tag_widget.on_edit.connect(lambda t=tag: self.edit_tag(t))
@@ -107,8 +107,9 @@ class TagBoxWidget(FieldWidget):
build_tag_panel,
self.driver.lib.tag_display_name(tag.id),
"Edit Tag",
done_callback=lambda: self.driver.main_window.preview_panel.update_widgets(
update_preview=False
done_callback=lambda _=None,
s=self.driver.selected: self.driver.main_window.preview_panel.set_selection( # noqa: E501
s, update_preview=False
),
has_save=True,
)

View File

@@ -1,4 +1,4 @@
from tagstudio.qt.widgets.preview_panel import PreviewPanel
from tagstudio.qt.controller.widgets.preview_panel_controller import PreviewPanel
def test_update_selection_empty(qt_driver, library):
@@ -7,10 +7,10 @@ def test_update_selection_empty(qt_driver, library):
# Clear the library selection (selecting 1 then unselecting 1)
qt_driver.toggle_item_selection(1, append=False, bridge=False)
qt_driver.toggle_item_selection(1, append=True, bridge=False)
panel.update_widgets()
panel.set_selection(qt_driver.selected)
# FieldContainer should hide all containers
for container in panel.fields.containers:
for container in panel.field_containers_widget.containers:
assert container.isHidden()
@@ -19,10 +19,10 @@ def test_update_selection_single(qt_driver, library, entry_full):
# Select the single entry
qt_driver.toggle_item_selection(entry_full.id, append=False, bridge=False)
panel.update_widgets()
panel.set_selection(qt_driver.selected)
# FieldContainer should show all applicable tags and field containers
for container in panel.fields.containers:
for container in panel.field_containers_widget.containers:
assert not container.isHidden()
@@ -34,10 +34,10 @@ def test_update_selection_multiple(qt_driver, library):
# Select the multiple entries
qt_driver.toggle_item_selection(1, append=False, bridge=False)
qt_driver.toggle_item_selection(2, append=True, bridge=False)
panel.update_widgets()
panel.set_selection(qt_driver.selected)
# FieldContainer should show mixed field editing
for container in panel.fields.containers:
for container in panel.field_containers_widget.containers:
assert container.isHidden()
@@ -48,10 +48,10 @@ def test_add_tag_to_selection_single(qt_driver, library, entry_full):
# Select the single entry
qt_driver.toggle_item_selection(entry_full.id, append=False, bridge=False)
panel.update_widgets()
panel.set_selection(qt_driver.selected)
# Add new tag
panel.fields.add_tags_to_selected(2000)
panel.field_containers_widget.add_tags_to_selected(2000)
# Then reload entry
refreshed_entry = next(library.get_entries(with_joins=True))
@@ -65,10 +65,10 @@ def test_add_same_tag_to_selection_single(qt_driver, library, entry_full):
# Select the single entry
qt_driver.toggle_item_selection(entry_full.id, append=False, bridge=False)
panel.update_widgets()
panel.set_selection(qt_driver.selected)
# Add an existing tag
panel.fields.add_tags_to_selected(1000)
panel.field_containers_widget.add_tags_to_selected(1000)
# Then reload entry
refreshed_entry = next(library.get_entries(with_joins=True))
@@ -95,10 +95,10 @@ def test_add_tag_to_selection_multiple(qt_driver, library):
# Select the multiple entries
for i, e in enumerate(library.get_entries(with_joins=True), start=0):
qt_driver.toggle_item_selection(e.id, append=(True if i == 0 else False), bridge=False) # noqa: SIM210
panel.update_widgets()
panel.set_selection(qt_driver.selected)
# Add new tag
panel.fields.add_tags_to_selected(1000)
panel.field_containers_widget.add_tags_to_selected(1000)
# Then reload all entries and recheck the presence of tag 1000
refreshed_entries = library.get_entries(with_joins=True)
@@ -123,11 +123,11 @@ def test_meta_tag_category(qt_driver, library, entry_full):
# Select the single entry
qt_driver.toggle_item_selection(entry_full.id, append=False, bridge=False)
panel.update_widgets()
panel.set_selection(qt_driver.selected)
# FieldContainer should hide all containers
assert len(panel.fields.containers) == 3
for i, container in enumerate(panel.fields.containers):
assert len(panel.field_containers_widget.containers) == 3
for i, container in enumerate(panel.field_containers_widget.containers):
match i:
case 0:
# Check if the container is the Meta Tags category
@@ -155,11 +155,11 @@ def test_custom_tag_category(qt_driver, library, entry_full):
# Select the single entry
qt_driver.toggle_item_selection(entry_full.id, append=False, bridge=False)
panel.update_widgets()
panel.set_selection(qt_driver.selected)
# FieldContainer should hide all containers
assert len(panel.fields.containers) == 3
for i, container in enumerate(panel.fields.containers):
assert len(panel.field_containers_widget.containers) == 3
for i, container in enumerate(panel.field_containers_widget.containers):
match i:
case 0:
# Check if the container is the Meta Tags category

View File

@@ -12,9 +12,9 @@ from pytestqt.qtbot import QtBot
from tagstudio.core.enums import ShowFilepathOption
from tagstudio.core.library.alchemy.library import Library, LibraryStatus
from tagstudio.core.library.alchemy.models import Entry
from tagstudio.qt.controller.widgets.preview_panel_controller import PreviewPanel
from tagstudio.qt.modals.settings_panel import SettingsPanel
from tagstudio.qt.ts_qt import QtDriver
from tagstudio.qt.widgets.preview_panel import PreviewPanel
# Tests to see if the file path setting is applied correctly
@@ -59,7 +59,7 @@ def test_file_path_display(
# Select 2
qt_driver.toggle_item_selection(2, append=False, bridge=False)
panel.update_widgets()
panel.set_selection(qt_driver.selected)
qt_driver.settings.show_filepath = filepath_option
@@ -68,7 +68,7 @@ def test_file_path_display(
assert isinstance(entry, Entry)
filename = entry.path
assert library.library_dir is not None
panel.file_attrs.update_stats(filepath=library.library_dir / filename)
panel._file_attributes_widget.update_stats(filepath=library.library_dir / filename)
# Generate the expected file string.
# This is copied directly from the file_attributes.py file
@@ -86,7 +86,7 @@ def test_file_path_display(
file_str += f"<b>{'\u200b'.join(part_)}</b>"
# Assert the file path is displayed correctly
assert panel.file_attrs.file_label.text() == file_str
assert panel._file_attributes_widget.file_label.text() == file_str
@pytest.mark.parametrize(

View File

@@ -1,4 +1,4 @@
from tagstudio.qt.widgets.preview_panel import PreviewPanel
from tagstudio.qt.controller.widgets.preview_panel_controller import PreviewPanel
def test_update_selection_empty(qt_driver, library):
@@ -7,11 +7,10 @@ def test_update_selection_empty(qt_driver, library):
# Clear the library selection (selecting 1 then unselecting 1)
qt_driver.toggle_item_selection(1, append=False, bridge=False)
qt_driver.toggle_item_selection(1, append=True, bridge=False)
panel.update_widgets()
panel.set_selection(qt_driver.selected)
# Panel should disable UI that allows for entry modification
assert not panel.add_tag_button.isEnabled()
assert not panel.add_field_button.isEnabled()
assert not panel.add_buttons_enabled
def test_update_selection_single(qt_driver, library, entry_full):
@@ -19,11 +18,10 @@ def test_update_selection_single(qt_driver, library, entry_full):
# Select the single entry
qt_driver.toggle_item_selection(entry_full.id, append=False, bridge=False)
panel.update_widgets()
panel.set_selection(qt_driver.selected)
# Panel should enable UI that allows for entry modification
assert panel.add_tag_button.isEnabled()
assert panel.add_field_button.isEnabled()
assert panel.add_buttons_enabled
def test_update_selection_multiple(qt_driver, library):
@@ -32,8 +30,7 @@ def test_update_selection_multiple(qt_driver, library):
# Select the multiple entries
qt_driver.toggle_item_selection(1, append=False, bridge=False)
qt_driver.toggle_item_selection(2, append=True, bridge=False)
panel.update_widgets()
panel.set_selection(qt_driver.selected)
# Panel should enable UI that allows for entry modification
assert panel.add_tag_button.isEnabled()
assert panel.add_field_button.isEnabled()
assert panel.add_buttons_enabled