refactor: code cleanup and fixes

This commit is contained in:
Travis Abendshien
2026-07-18 02:40:43 -07:00
parent 34d00d7e9a
commit f2cecb2648
11 changed files with 235 additions and 391 deletions
@@ -12,7 +12,6 @@ from PySide6 import QtCore
from PySide6.QtGui import QShortcut
from tagstudio.core.library.alchemy.fields import BaseFieldTemplate
from tagstudio.core.library.alchemy.library import Library
from tagstudio.core.utils.ffmpeg_status import FfmpegStatus, FfprobeStatus
from tagstudio.qt.mixed.file_attributes import FileAttributeData
from tagstudio.qt.views.preview_panel_view import PreviewPanelView
@@ -24,8 +23,8 @@ logger = structlog.get_logger(__name__)
class PreviewPanel(PreviewPanelView):
def __init__(self, library: Library, driver: "QtDriver") -> None:
super().__init__(library, driver)
def __init__(self, driver: "QtDriver") -> None:
super().__init__(driver)
self.__current_stats: FileAttributeData | None = None
self._thumb.check_ffmpeg.connect(self._toggle_ffmpeg_warning)
@@ -45,29 +44,29 @@ class PreviewPanel(PreviewPanelView):
self.add_tag_action.activated.connect(self._add_tag_button.setFocus)
self.add_tag_action.activated.connect(self._add_tag_button.click)
self.tag_search.done.connect(self.tag_added_callback)
self.tag_search.tags_updated.connect(self.update_added_callback)
self.tag_search_box.done.connect(self.tag_added_callback)
self.tag_search_box.tags_updated.connect(self.update_added_callback)
def _add_field_button_callback(self) -> None:
# self.__add_field_modal.show()
pass
def _add_tag_button_callback(self) -> None:
self.tag_search.added = self._containers.tags
self.tag_search.view.search_field.setDisabled(False)
self.tag_search.setHidden(False)
self.tag_search_box.added = self._containers.tags
self.tag_search_box.layout().search_field.setDisabled(False)
self.tag_search_box.setHidden(False)
self._add_tag_button.setHidden(True)
self._add_field_button.setHidden(True)
def tag_added_callback(self):
self.tag_search.setHidden(True)
self.tag_search_box.setHidden(True)
self._add_tag_button.setHidden(False)
self._add_field_button.setHidden(False)
self._add_tag_button.setFocus()
def update_added_callback(self):
self.tag_search.added = self._containers.tags
self.tag_search_box.added = self._containers.tags
def __thumb_stats_updated_callback(self, filepath: Path, stats: FileAttributeData) -> None:
if len(self._selected) != 1:
@@ -91,11 +90,11 @@ class PreviewPanel(PreviewPanelView):
@override
def _set_selection_callback(self) -> None:
with catch_warnings(record=True):
self.field_search.field_template_chosen.disconnect()
self.tag_search.item_chosen.disconnect()
self.field_search_box.field_template_chosen.disconnect()
self.tag_search_box.item_chosen.disconnect()
self.field_search.field_template_chosen.connect(self._add_field_to_selected)
self.tag_search.item_chosen.connect(self._add_tag_to_selected)
self.field_search_box.field_template_chosen.connect(self._add_field_to_selected)
self.tag_search_box.item_chosen.connect(self._add_tag_to_selected)
def _add_field_to_selected(self, template: BaseFieldTemplate) -> None:
self._containers.add_field_to_selected(template)
@@ -6,9 +6,10 @@ from typing import TYPE_CHECKING, Any, override
import structlog
from PySide6.QtCore import Signal
from PySide6.QtGui import QShowEvent
from PySide6.QtWidgets import QGraphicsOpacityEffect, QVBoxLayout
from PySide6.QtWidgets import QGraphicsOpacityEffect, QWidget
from tagstudio.core.library.alchemy.library import Library
from tagstudio.qt.controllers.autofill_line_edit import QtCore, QtGui
from tagstudio.qt.views.panel_modal import PanelWidget
from tagstudio.qt.views.stylesheets.stylesheets import (
autofill_line_edit_style,
@@ -41,93 +42,56 @@ def _item_name(item: object) -> str:
raise AttributeError()
class SuggestBox[T](PanelWidget):
class SuggestBox[T](QWidget):
item_chosen = Signal(int)
done = Signal()
tags_updated = Signal()
def __init__(
self, view: SuggestBoxView, exclude: list[int] | None = None, is_chooser: bool = True
) -> None:
def __init__(self, driver: "QtDriver", view: SuggestBoxView) -> None:
super().__init__()
self.view = view
self.is_chooser = is_chooser
self._layout = QVBoxLayout(self)
self._layout.setContentsMargins(0, 0, 0, 0)
self._layout.addWidget(self.view)
self._driver: QtDriver | None = None
self.exclude: list[int] = exclude or []
self.added: list[int] = exclude or []
self.create_and_add_button_in_layout: bool = False
self.limit = 5
self.shift_held = False
# Limits
self._unlimited_limit_item_label: str = "All Items"
self._limit_items: list[tuple[str, int]] = [
("25", 25),
("50", 50),
("100", 100),
("250", 250),
("500", 500),
(self._unlimited_limit_item_label, -1),
]
self._default_limit_index: int = 0
self._previous_limit_index: int = self._default_limit_index
# Items
self._layout = view
self._driver = driver
self._limit = 5
self._is_shift_held = False
self._search_results: list[T] = []
self.added: list[int] = []
self.excluded: list[int] = []
self._create_and_add_button_label_key: str = ""
self.connect_callbacks()
self.setLayout(self._layout)
self._connect_callbacks()
def connect_callbacks(self) -> None:
self.view.search_field.textChanged.connect(self.on_search_query_changed)
self.view.search_field.editingFinished.connect(self.test_editing_finished)
self.view.search_field.return_pressed.connect(
lambda: self.on_search_query_submitted(self.view.search_field.text())
def _connect_callbacks(self) -> None:
self._layout.search_field.textChanged.connect(self.on_search_query_changed)
self._layout.search_field.editingFinished.connect(self.test_editing_finished)
self._layout.search_field.return_pressed.connect(
lambda: self.on_search_query_submitted(self._layout.search_field.text())
)
self.view.search_field.shift_return_pressed.connect(
self._layout.search_field.shift_return_pressed.connect(
lambda: self.on_search_query_submitted(
self.view.search_field.text(), always_create=True
self._layout.search_field.text(), always_create=True
)
)
self.view.search_field.shift_holding.connect(lambda held: self.on_shift_held(held))
self._layout.search_field.shift_holding.connect(lambda held: self._on_shift_held(held))
def on_shift_held(self, held: bool):
def _on_shift_held(self, held: bool):
if held:
self.shift_held = True
self._is_shift_held = True
opacity_effect = QGraphicsOpacityEffect(self)
opacity_effect.setOpacity(0.3)
if self.view.content_layout.count() > 0:
self.view.content_layout.itemAt(0).widget().setGraphicsEffect(opacity_effect)
if self._layout.content_layout.count() > 0:
self._layout.content_layout.itemAt(0).widget().setGraphicsEffect(opacity_effect)
else:
self.shift_held = False
if self.view.content_layout.count() > 0:
self.view.content_layout.itemAt(0).widget().setGraphicsEffect(None) # pyright: ignore[reportArgumentType]
def focus_search_box(self, select_all: bool = False) -> None:
if not self.isHidden():
self.view.search_field.setFocus()
if select_all:
self.view.search_field.selectAll()
self._is_shift_held = False
if self._layout.content_layout.count() > 0:
self._layout.content_layout.itemAt(0).widget().setGraphicsEffect(None) # pyright: ignore[reportArgumentType]
def clear_search_query(self) -> None:
self.view.search_field.setText("")
self._layout.search_field.setText("")
def get_item_widget(self, index: int, library: Library) -> Any: # pyright: ignore[reportExplicitAny]
return self.get_item_widget(index, library)
def set_driver(self, driver: "QtDriver") -> None:
self._driver = driver
def _get_previous_limit(self) -> tuple[str, int]:
return self._limit_items[self._previous_limit_index]
def _get_max_limit(self) -> int:
raise NotImplementedError()
def on_search_query_changed(self, query: str) -> None:
self.update_items(query)
@@ -136,38 +100,35 @@ class SuggestBox[T](PanelWidget):
logger.info("Query submitted")
if not query:
self.done.emit()
self.disappear()
self.hide_and_reset()
return
elif not self.isHidden():
self.view.search_field.setFocus()
self._layout.search_field.setFocus()
# Create and add item if no search results
if (len(self._search_results) <= 0) or always_create:
self.on_item_create(add_to_entry=True)
elif self.is_chooser:
self.on_item_create()
else:
self._on_item_chosen(self._search_results[0])
self.clear_search_query()
self.update_items()
def on_item_create(self, add_to_entry: bool = False) -> None: # pyright: ignore[reportUnusedParameter]
def on_item_create(self) -> None:
raise NotImplementedError()
def on_item_edit(self, item: T) -> None: # pyright: ignore[reportUnusedParameter]
raise NotImplementedError()
def _on_item_remove(self, item: T) -> None: # pyright: ignore[reportUnusedParameter]
raise NotImplementedError()
def _on_item_chosen(self, item: T) -> None: # pyright: ignore[reportUnusedParameter]
raise NotImplementedError()
def _is_excluded(self, item: T) -> bool:
return _item_id(item) in self.exclude
return _item_id(item) in self.excluded
def update_items(self, query: str | None = None) -> None:
"""Update the item list given a search query."""
logger.info("[SearchPanel] Updating items", limit=self.limit)
logger.info("[SearchPanel] Updating items", limit=self._limit)
# Get results for the search query
query_lower = "" if not query else query.lower()
@@ -199,24 +160,24 @@ class SuggestBox[T](PanelWidget):
all_results.remove(item)
all_results = all_results + already_added
if self.limit > 0:
all_results = all_results[: self.limit]
if self._limit > 0:
all_results = all_results[: self._limit]
self._search_results = all_results
logger.info("[SearchPanel] Search results", results=self._search_results)
for i in range(0, self.limit):
for i in range(0, self._limit):
item: T | None = all_results[i] if i < len(all_results) else None
self.set_item_widget(item=item, index=i)
if self.view.content_layout.isEmpty():
self.view.scroll_area.setHidden(True)
self.view.content_layout.setContentsMargins(0, 0, 0, 0)
self.view.search_field.setStyleSheet(autofill_line_edit_style())
if self._layout.content_layout.isEmpty():
self._layout.scroll_area.setHidden(True)
self._layout.content_layout.setContentsMargins(0, 0, 0, 0)
self._layout.search_field.setStyleSheet(autofill_line_edit_style())
else:
self.view.scroll_area.setHidden(False)
self.view.content_layout.setContentsMargins(6, 6, 6, 6)
self.view.search_field.setStyleSheet(autofill_line_edit_top_style())
self._layout.scroll_area.setHidden(False)
self._layout.content_layout.setContentsMargins(6, 6, 6, 6)
self._layout.search_field.setStyleSheet(autofill_line_edit_top_style())
def search_items(self, query: str) -> tuple[list[T], list[T]]: # pyright: ignore[reportUnusedParameter]
raise NotImplementedError()
@@ -224,27 +185,41 @@ class SuggestBox[T](PanelWidget):
def set_item_widget(self, item: T | None, index: int) -> None: # pyright: ignore[reportUnusedParameter]
raise NotImplementedError()
@override
def showEvent(self, event: QShowEvent) -> None:
self.update_items()
self.on_shift_held(held=False)
self.clear_search_query()
return super().showEvent(event)
def test_editing_finished(self):
logger.info("Editing finished")
self.tags_updated.emit()
if self.view.search_field.text() == "":
if self._layout.search_field.text() == "":
self.done.emit()
self.disappear()
self.hide_and_reset()
def disappear(self):
def hide_and_reset(self):
self.hide()
self.view.search_field.setDisabled(True)
self.on_shift_held(held=False)
self._layout.search_field.setDisabled(True)
self._on_shift_held(held=False)
def create_item(self, edit_item_panel: PanelWidget, choose_item: bool = False) -> None: # pyright: ignore[reportUnusedParameter]
def create_item_from_modal(self, edit_item_panel: PanelWidget) -> None: # pyright: ignore[reportUnusedParameter]
raise NotImplementedError()
def edit_item(self, edit_item_panel: PanelWidget) -> None: # pyright: ignore[reportUnusedParameter]
raise NotImplementedError()
@override
def showEvent(self, event: QShowEvent) -> None:
self.update_items()
self._on_shift_held(held=False)
self.clear_search_query()
return super().showEvent(event)
@override
def layout(self) -> SuggestBoxView:
return self._layout
@override
def keyPressEvent(self, event: QtGui.QKeyEvent) -> None:
# When Escape is pressed, focus back on the search box.
if event.key() in {
QtCore.Qt.Key.Key_Escape,
QtCore.Qt.Key.Key_Enter,
QtCore.Qt.Key.Key_Return,
}:
self.hide_and_reset()
@@ -2,49 +2,38 @@
# SPDX-License-Identifier: GPL-3.0-only
import typing
from typing import override
from warnings import catch_warnings
import structlog
from PySide6.QtWidgets import QGraphicsOpacityEffect, QMessageBox, QSizePolicy, QWidget
from PySide6.QtWidgets import QGraphicsOpacityEffect, QWidget
from tagstudio.core.constants import RESERVED_TAG_END, RESERVED_TAG_START
from tagstudio.core.library.alchemy.enums import BrowsingState
from tagstudio.core.library.alchemy.library import Library
from tagstudio.core.library.alchemy.models import Tag
from tagstudio.qt.controllers.suggest_box_controller import SuggestBox
from tagstudio.qt.mixed.build_tag import BuildTagPanel
from tagstudio.qt.mixed.tag_widget import TagWidget
from tagstudio.qt.translations import Translations
from tagstudio.qt.views.panel_modal import PanelModal, PanelWidget
from tagstudio.qt.views.tag_suggest_box_view import TagSuggestBoxView
from tagstudio.qt.views.suggest_box_view import SuggestBoxView
if typing.TYPE_CHECKING:
from tagstudio.qt.ts_qt import QtDriver
logger = structlog.get_logger(__name__)
class TagSuggestBox(SuggestBox[Tag]):
def __init__(
self,
library: Library,
exclude: list[int] | None = None,
is_tag_chooser: bool = True,
view: TagSuggestBoxView | None = None,
):
super().__init__(
view=view or TagSuggestBoxView(is_tag_chooser),
exclude=exclude,
is_chooser=is_tag_chooser,
)
self.__lib = library
self._unlimited_limit_item_label = Translations["tag.all_tags"]
self._create_and_add_button_label_key = "tag.create_add"
def __init__(self, driver: "QtDriver", view: SuggestBoxView | None = None):
super().__init__(driver, view=view or SuggestBoxView())
self._driver = driver
self._lib = self._driver.lib
self._edit_on_create = self._driver.settings.edit_tag_on_create
@override
def _get_max_limit(self) -> int:
return len(self.__lib.tags)
@override
def on_item_create(self, add_to_entry: bool = False) -> None:
def on_item_create(self) -> None:
"""Opens panel to create a new tag and optionally add it to an entry.
Populates name field using current search query.
@@ -52,67 +41,36 @@ class TagSuggestBox(SuggestBox[Tag]):
Args:
add_to_entry (bool): Should this item be added to currently selected entries?
"""
# TODO: Move this to a top-level import
query: str = self._layout.search_field.text()
query: str = self.view.search_field.text()
if self._edit_on_create:
panel: BuildTagPanel = BuildTagPanel(self._lib)
modal: PanelModal = PanelModal(
panel, Translations["tag.new"], Translations["tag.new"], is_savable=True
)
if query.strip():
panel.name_field.setText(query)
# panel: BuildTagPanel = BuildTagPanel(self.__lib)
# modal: PanelModal = PanelModal(
# panel,
# Translations["tag.new"],
# Translations["tag.add"] if add_to_entry else Translations["tag.new"],
# is_savable=True,
# )
# if query.strip():
# panel.name_field.setText(query)
# modal.saved.connect(lambda: self.create_item(panel, choose_item=add_to_entry))
# modal.show()
tag = Tag(name=query)
self.__lib.add_tag(tag)
if add_to_entry:
modal.saved.connect(lambda: self.create_item_from_modal(panel))
modal.show()
else:
tag = Tag(name=query)
self._lib.add_tag(tag)
self._on_item_chosen(tag)
self.clear_search_query()
@override
def on_item_edit(self, item: Tag) -> None:
# TODO: Move this to a top-level import
from tagstudio.qt.mixed.build_tag import BuildTagPanel # here due to circular imports
edit_tag_panel: BuildTagPanel = BuildTagPanel(self.__lib, tag=item)
edit_tag_panel: BuildTagPanel = BuildTagPanel(self._lib, tag=item)
edit_tag_modal: PanelModal = PanelModal(
edit_tag_panel,
self.__lib.tag_display_name(item),
self._lib.tag_display_name(item),
Translations["tag.edit"],
is_savable=True,
)
edit_tag_modal.saved.connect(lambda: self.edit_item(edit_tag_panel))
edit_tag_modal.show()
@override
def _on_item_remove(self, item: Tag) -> None:
if self.is_chooser:
return
if item.id in range(RESERVED_TAG_START, RESERVED_TAG_END):
return
message_box = QMessageBox(
QMessageBox.Icon.Question,
Translations["tag.remove"],
Translations.format("tag.confirm_delete", tag_name=self.__lib.tag_display_name(item)),
QMessageBox.StandardButton.Ok | QMessageBox.StandardButton.Cancel,
)
result = message_box.exec()
if result != QMessageBox.StandardButton.Ok:
return
self.__lib.remove_tag(item.id)
self.update_items(self.view.search_field.text())
@override
def _on_item_chosen(self, item: Tag) -> None:
self.item_chosen.emit(item.id)
@@ -121,80 +79,55 @@ class TagSuggestBox(SuggestBox[Tag]):
@override
def search_items(self, query: str) -> tuple[list[Tag], list[Tag]]:
if query != "":
# return self.__lib.search_tags(name=query, limit=self._get_limit()[1])
return self.__lib.search_tags(name=query, limit=0)
return self._lib.search_tags(name=query, limit=0)
else:
return ([], [])
@override
def set_item_widget(self, item: Tag | None, index: int) -> None:
"""Set the tag of a tag widget at a specific index."""
tag_widget: TagWidget = self.get_item_widget(index, self.__lib)
tag_widget: TagWidget = self.get_item_widget(index, self._lib)
tag_widget.has_remove = False
tag_widget.set_tag(item)
tag_widget.setHidden(item is None)
opacity_effect = QGraphicsOpacityEffect(self)
opacity_effect.setOpacity(0.3)
if item and item.id in self.added:
opacity_effect = QGraphicsOpacityEffect(self)
opacity_effect.setOpacity(0.3)
tag_widget.setGraphicsEffect(opacity_effect)
else:
tag_widget.setGraphicsEffect(None) # pyright: ignore[reportArgumentType]
if item is None:
return
assert item is not None
tag_widget.has_remove = not self.is_chooser and item.id not in range(
RESERVED_TAG_START, RESERVED_TAG_END
)
# Disconnect previous callbacks
with catch_warnings(record=True):
tag_widget.on_edit.disconnect()
tag_widget.on_remove.disconnect()
tag_widget.bg_button.clicked.disconnect()
tag_widget.search_for_tag_action.triggered.disconnect()
# Connect callbacks
tag_widget.on_edit.connect(lambda edit_tag=item: self.on_item_edit(edit_tag))
tag_widget.on_remove.connect(lambda remove_tag=item: self._on_item_remove(remove_tag))
if self.is_chooser:
tag_widget.bg_button.clicked.connect(
lambda checked=False, tag=item: self._on_item_chosen(tag)
)
else:
tag_widget.bg_button.clicked.connect(
lambda checked=False, edit_tag=item: self.on_item_edit(edit_tag)
)
# Connect search action
if self._driver is not None:
tag_widget.search_for_tag_action.triggered.connect(
lambda checked=False, tag_id=item.id: self.search_for_tag(tag_id)
)
tag_widget.search_for_tag_action.setEnabled(True)
else:
logger.warning(
"[TagSearchPanel] No driver was set for this TagSearchPanel. Was this on purpose?"
)
tag_widget.search_for_tag_action.setEnabled(False)
tag_widget.bg_button.clicked.connect(
lambda checked=False, tag=item: self._on_item_chosen(tag)
)
tag_widget.search_for_tag_action.triggered.connect(
lambda checked=False, tag_id=item.id: self.search_for_tag(tag_id)
)
tag_widget.search_for_tag_action.setEnabled(True)
@override
def create_item(self, edit_item_panel: PanelWidget, choose_item: bool = False) -> None:
# TODO: Move this to a top-level import
from tagstudio.qt.mixed.build_tag import BuildTagPanel # here due to circular imports
def create_item_from_modal(self, edit_item_panel: PanelWidget) -> None:
if isinstance(edit_item_panel, BuildTagPanel):
tag: Tag = edit_item_panel.build_tag()
self.__lib.add_tag(
self._lib.add_tag(
tag, parent_ids=edit_item_panel.parent_ids, aliases=edit_item_panel.aliases
)
if choose_item:
self._on_item_chosen(tag)
self.clear_search_query()
self._on_item_chosen(tag)
self.clear_search_query()
edit_item_panel.hide()
self.on_search_query_changed(self.view.search_field.text())
self.on_search_query_changed(self._layout.search_field.text())
@override
def edit_item(self, edit_item_panel: PanelWidget) -> None:
@@ -204,17 +137,14 @@ class TagSuggestBox(SuggestBox[Tag]):
if not isinstance(edit_item_panel, BuildTagPanel):
return
self.__lib.update_tag(
self._lib.update_tag(
tag=edit_item_panel.build_tag(),
parent_ids=edit_item_panel.parent_ids,
aliases=edit_item_panel.aliases,
)
self.update_items(self.view.search_field.text())
self.update_items(self._layout.search_field.text())
def search_for_tag(self, tag_id: int) -> None:
if self._driver is None:
return
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)
@@ -224,50 +154,13 @@ class TagSuggestBox(SuggestBox[Tag]):
def get_item_widget(self, index: int, library: Library | None) -> TagWidget:
"""Gets the item widget at a specific index."""
# Create any new item widgets needed up to the given index
if self.view.content_layout.count() <= index:
# opacity_effect = QGraphicsOpacityEffect(self)
# opacity_effect.setOpacity(0.3)
while self.view.content_layout.count() <= index:
if self._layout.content_layout.count() <= index:
while self._layout.content_layout.count() <= index:
tag_widget = TagWidget(tag=None, has_edit=True, has_remove=True, library=library)
tag_widget.on_remove.connect(self.update_items)
tag_widget.bg_button.setSizePolicy(
QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Minimum
)
tag_widget.setHidden(True)
# if index > 0:
# tag_widget.setGraphicsEffect(opacity_effect)
self.view.content_layout.addWidget(tag_widget)
self._layout.content_layout.addWidget(tag_widget)
tag_widget: QWidget = self.view.content_layout.itemAt(index).widget()
tag_widget: QWidget = self._layout.content_layout.itemAt(index).widget()
assert isinstance(tag_widget, TagWidget)
return tag_widget
# @override
# def keyPressEvent(self, event: QtGui.QKeyEvent) -> None:
# # When Escape is pressed, focus back on the search box.
# # If focus is already on the search box, close the modal.
# pass
# # if event.key() in {QtCore.Qt.Key.Key_Escape, QtCore.Qt.Key.Key_Backspace, }:
# # if self.search_field.hasFocus():
# # self.hide()
# @override
# def keyPressEvent(self, event: QtGui.QKeyEvent) -> None:
# # When Escape is pressed, focus back on the search box.
# # If focus is already on the search box, close the modal.
# # if event.key() == QtCore.Qt.Key.Key_Escape:
# # if self.search_field.hasFocus():
# # self.hide()
# logger.info(event.key)
# if event.key() in {
# QtCore.Qt.Key.Key_Escape,
# QtCore.Qt.Key.Key_Enter,
# QtCore.Qt.Key.Key_Return,
# }:
# if self.search_field.hasFocus():
# logger.info("Hiding")
# self.hide()
# elif event.key() in {QtCore.Qt.Key.Key_Backspace, QtCore.Qt.Key.Key_Delete}:
# if self.search_field.hasFocus() and self.search_field.text() == "":
# # self.hide()
+1
View File
@@ -77,6 +77,7 @@ class GlobalSettings(BaseModel):
theme: Theme = Field(default=Theme.SYSTEM)
splash: Splash = Field(default=Splash.DEFAULT)
windows_start_command: bool = Field(default=False)
edit_tag_on_create: bool = Field(default=False)
date_format: str = Field(default="%x")
hour_format: bool = Field(default=True)
+1 -1
View File
@@ -701,7 +701,7 @@ class MainWindow(QMainWindow):
self.content_splitter.addWidget(self.entry_list_container)
def setup_preview_panel(self, driver: "QtDriver"):
self.preview_panel = PreviewPanel(driver.lib, driver)
self.preview_panel = PreviewPanel(driver)
self.content_splitter.addWidget(self.preview_panel)
def setup_status_bar(self):
+22 -23
View File
@@ -13,7 +13,6 @@ from PySide6.QtGui import QDesktopServices
from PySide6.QtWidgets import QHBoxLayout, QLabel, QSplitter, QVBoxLayout, QWidget
from tagstudio.core.constants import FFMPEG_HELP_URL
from tagstudio.core.library.alchemy.library import Library
from tagstudio.core.library.alchemy.models import Entry
from tagstudio.core.utils.types import unwrap
from tagstudio.qt.controllers.field_template_search_panel_controller import FieldTemplateSearchPanel
@@ -26,7 +25,7 @@ from tagstudio.qt.resource_manager import ResourceManager
from tagstudio.qt.translations import Translations
from tagstudio.qt.views.field_template_search_panel_view import FieldTemplateSearchPanelView
from tagstudio.qt.views.stylesheets.stylesheets import button_style, preview_warning_style
from tagstudio.qt.views.tag_suggest_box_view import TagSuggestBoxView
from tagstudio.qt.views.suggest_box_view import SuggestBoxView
if typing.TYPE_CHECKING:
from tagstudio.qt.ts_qt import QtDriver
@@ -35,31 +34,31 @@ logger = structlog.get_logger(__name__)
class PreviewPanelView(QWidget):
lib: Library
_selected: list[int]
def __init__(self, library: Library, driver: "QtDriver") -> None:
def __init__(self, driver: "QtDriver") -> None:
super().__init__()
self.lib = library
self._lib = driver.lib
rm = ResourceManager()
self.field_search: FieldTemplateSearchPanel = FieldTemplateSearchPanel(
library,
self.field_search_box: FieldTemplateSearchPanel = FieldTemplateSearchPanel(
self._lib,
is_field_template_chooser=True,
view=FieldTemplateSearchPanelView(is_field_template_chooser=True),
)
self.tag_search = TagSuggestBox(
library,
is_tag_chooser=True,
view=TagSuggestBoxView(is_tag_chooser=True),
)
self.tag_search.set_driver(driver)
self.tag_search.hide()
self._thumb = PreviewThumb(self.lib, driver)
self._file_attrs = FileAttributes(self.lib, driver)
tag_placeholder_text = (
f"{Translations['home.search_tags']} {Translations['home.search.how_to_exit']}"
)
self.tag_search_box = TagSuggestBox(
driver, view=SuggestBoxView(placeholder=tag_placeholder_text)
)
self.tag_search_box.hide()
self._thumb = PreviewThumb(self._lib, driver)
self._file_attrs = FileAttributes(self._lib, driver)
self._containers = FieldContainers(
self.lib, driver
self._lib, driver
) # TODO: this should be name mangled, but is still needed on the controller side atm
# Visual Preview
@@ -124,7 +123,7 @@ class PreviewPanelView(QWidget):
add_buttons_layout.addWidget(self._add_tag_button)
add_buttons_layout.addWidget(self._add_field_button)
add_buttons_layout.addWidget(self.tag_search)
add_buttons_layout.addWidget(self.tag_search_box)
# add_buttons_layout.addWidget(self.field_search)
preview_layout.addWidget(self._thumb)
@@ -166,14 +165,14 @@ class PreviewPanelView(QWidget):
self._add_field_button.setEnabled(False)
self._add_tag_button.setHidden(False)
self._add_field_button.setHidden(False)
self.tag_search.disappear()
self.tag_search_box.hide_and_reset()
# One Item Selected
elif len(selected) == 1:
entry_id = selected[0]
entry: Entry = unwrap(self.lib.get_entry(entry_id))
entry: Entry = unwrap(self._lib.get_entry(entry_id))
filepath: Path = unwrap(self.lib.library_dir) / entry.path
filepath: Path = unwrap(self._lib.library_dir) / entry.path
if filepath != self._thumb.current_file:
self.__current_stats = None
@@ -190,7 +189,7 @@ class PreviewPanelView(QWidget):
self._add_field_button.setEnabled(True)
self._add_tag_button.setHidden(False)
self._add_field_button.setHidden(False)
self.tag_search.disappear()
self.tag_search_box.hide_and_reset()
# Multiple Selected Items
elif len(selected) > 1:
@@ -207,7 +206,7 @@ class PreviewPanelView(QWidget):
self._add_field_button.setEnabled(True)
self._add_tag_button.setHidden(False)
self._add_field_button.setHidden(False)
self.tag_search.disappear()
self.tag_search_box.hide_and_reset()
except Exception as e:
logger.error("[Preview Panel] Error updating selection", error=e)
+31 -34
View File
@@ -26,38 +26,15 @@ if TYPE_CHECKING:
logger = structlog.get_logger(__name__)
class SuggestBoxView(QWidget):
def __init__(self, is_chooser: bool) -> None:
self.is_chooser: bool = is_chooser
class SuggestBoxView(QVBoxLayout):
def __init__(self, placeholder: str = "") -> None:
super().__init__()
self._root_layout = QVBoxLayout(self)
self._root_layout.setContentsMargins(0, 0, 0, 0)
self._root_layout.setSpacing(0)
# Scroll area
self.contents = QWidget()
self.content_layout = QHBoxLayout(self.contents)
self.content_layout.setSpacing(6)
self.content_layout.setAlignment(Qt.AlignmentFlag.AlignBottom | Qt.AlignmentFlag.AlignLeft)
self.content_layout.setContentsMargins(0, 0, 0, 0)
self.scroll_area_container = QWidget()
self.scroll_area_container.setObjectName("container")
self.scroll_area_container_layout = QHBoxLayout(self.scroll_area_container)
self.scroll_area_container_layout.setContentsMargins(0, 0, 0, 0)
self.scroll_area_container_layout.setSpacing(0)
self.scroll_area_container.setStyleSheet(autofill_scroll_top_style("container"))
# Search field
self.search_field = AutofillLineEdit(self.scroll_area_container)
self.search_field.setStyleSheet(autofill_line_edit_style())
self.search_field.setObjectName("search_field")
self.search_field.setMinimumHeight(28)
# Init layout
self.setContentsMargins(0, 0, 0, 0)
self.setSpacing(0)
# HACK: The transparent border allows for the focus border color to
# still show above the tags at the edges. Sort of.
# still show above the tags at the edges... sort of (overlaps on left when h-scrolling)
scroll_area_style = """
QScrollArea{
background: transparent;
@@ -71,11 +48,22 @@ class SuggestBoxView(QWidget):
}
"""
# Autocomplete ScrollArea
contents = QWidget()
self.content_layout = QHBoxLayout(contents)
self.content_layout.setSpacing(6)
self.content_layout.setAlignment(Qt.AlignmentFlag.AlignBottom | Qt.AlignmentFlag.AlignLeft)
self.content_layout.setContentsMargins(0, 0, 0, 0)
scroll_area_container = QWidget()
scroll_area_container.setObjectName("container")
scroll_area_container_layout = QHBoxLayout(scroll_area_container)
scroll_area_container_layout.setContentsMargins(0, 0, 0, 0)
scroll_area_container_layout.setSpacing(0)
scroll_area_container.setStyleSheet(autofill_scroll_top_style("container"))
self.scroll_area = QScrollArea()
self.scroll_area.setFocusProxy(self.search_field)
self.scroll_area.setStyleSheet(scroll_area_style)
self.scroll_area_container_layout.addWidget(self.scroll_area)
self.scroll_area.setWidget(self.contents)
scroll_area_container_layout.addWidget(self.scroll_area)
self.scroll_area.setWidget(contents)
self.scroll_area.setMaximumHeight(28)
self.scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
self.scroll_area.verticalScrollBar().setEnabled(False)
@@ -86,5 +74,14 @@ class SuggestBoxView(QWidget):
self.scroll_area.setFrameShape(QFrame.Shape.NoFrame)
self.scroll_area.setSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Minimum)
self._root_layout.addWidget(self.scroll_area_container)
self._root_layout.addWidget(self.search_field)
# Search Field
self.search_field = AutofillLineEdit(scroll_area_container)
self.search_field.setStyleSheet(autofill_line_edit_style())
self.search_field.setObjectName("search_field")
self.search_field.setMinimumHeight(28)
self.search_field.setPlaceholderText(placeholder)
self.scroll_area.setFocusProxy(self.search_field)
# Finalize layout
self.addWidget(scroll_area_container)
self.addWidget(self.search_field)
@@ -1,16 +0,0 @@
# SPDX-FileCopyrightText: (c) TagStudio Contributors
# SPDX-License-Identifier: GPL-3.0-only
from tagstudio.qt.translations import Translations
from tagstudio.qt.views.suggest_box_view import SuggestBoxView
# TODO: Get rid of this class
class TagSuggestBoxView(SuggestBoxView):
def __init__(self, is_tag_chooser: bool) -> None:
super().__init__(is_tag_chooser)
placeholder = (
f"{Translations['home.search_tags']} {Translations['home.search.how_to_exit']}"
)
self.search_field.setPlaceholderText(placeholder)
+27 -30
View File
@@ -2,15 +2,14 @@
# SPDX-License-Identifier: GPL-3.0-only
from tagstudio.core.library.alchemy.library import Library
from tagstudio.core.library.alchemy.models import Entry, Tag
from tagstudio.core.utils.types import unwrap
from tagstudio.qt.controllers.preview_panel_controller import PreviewPanel
from tagstudio.qt.ts_qt import QtDriver
def test_update_selection_empty(qt_driver: QtDriver, library: Library):
panel = PreviewPanel(library, qt_driver)
def test_update_selection_empty(qt_driver: QtDriver):
panel = PreviewPanel(qt_driver)
# Clear the library selection (selecting 1 then unselecting 1)
qt_driver.toggle_item_selection(1, append=False, bridge=False)
@@ -22,8 +21,8 @@ def test_update_selection_empty(qt_driver: QtDriver, library: Library):
assert container.isHidden()
def test_update_selection_single(qt_driver: QtDriver, library: Library, entry_full: Entry):
panel = PreviewPanel(library, qt_driver)
def test_update_selection_single(qt_driver: QtDriver, entry_full: Entry):
panel = PreviewPanel(qt_driver)
# Select the single entry
qt_driver.toggle_item_selection(entry_full.id, append=False, bridge=False)
@@ -34,10 +33,10 @@ def test_update_selection_single(qt_driver: QtDriver, library: Library, entry_fu
assert not container.isHidden()
def test_update_selection_multiple(qt_driver: QtDriver, library: Library):
def test_update_selection_multiple(qt_driver: QtDriver):
# TODO: Implement mixed field editing. Currently these containers will be hidden,
# same as the empty selection behavior.
panel = PreviewPanel(library, qt_driver)
panel = PreviewPanel(qt_driver)
# Select the multiple entries
qt_driver.toggle_item_selection(1, append=False, bridge=False)
@@ -49,8 +48,8 @@ def test_update_selection_multiple(qt_driver: QtDriver, library: Library):
assert container.isHidden()
def test_add_tag_to_selection_single(qt_driver: QtDriver, library: Library, entry_full: Entry):
panel = PreviewPanel(library, qt_driver)
def test_add_tag_to_selection_single(qt_driver: QtDriver, entry_full: Entry):
panel = PreviewPanel(qt_driver)
assert {t.id for t in entry_full.tags} == {1000}
@@ -62,12 +61,12 @@ def test_add_tag_to_selection_single(qt_driver: QtDriver, library: Library, entr
panel.field_containers_widget.add_tags_to_selected(2000)
# Then reload entry
refreshed_entry: Entry = next(library.all_entries(with_joins=True))
refreshed_entry: Entry = next(qt_driver.lib.all_entries(with_joins=True))
assert {t.id for t in refreshed_entry.tags} == {1000, 2000}
def test_add_same_tag_to_selection_single(qt_driver: QtDriver, library: Library, entry_full: Entry):
panel = PreviewPanel(library, qt_driver)
def test_add_same_tag_to_selection_single(qt_driver: QtDriver, entry_full: Entry):
panel = PreviewPanel(qt_driver)
assert {t.id for t in entry_full.tags} == {1000}
@@ -79,13 +78,13 @@ def test_add_same_tag_to_selection_single(qt_driver: QtDriver, library: Library,
panel.field_containers_widget.add_tags_to_selected(1000)
# Then reload entry
refreshed_entry = next(library.all_entries(with_joins=True))
refreshed_entry = next(qt_driver.lib.all_entries(with_joins=True))
assert {t.id for t in refreshed_entry.tags} == {1000}
def test_add_tag_to_selection_multiple(qt_driver: QtDriver, library: Library):
panel = PreviewPanel(library, qt_driver)
all_entries = library.all_entries(with_joins=True)
def test_add_tag_to_selection_multiple(qt_driver: QtDriver):
panel = PreviewPanel(qt_driver)
all_entries = qt_driver.lib.all_entries(with_joins=True)
# We want to verify that tag 1000 is on some, but not all entries already.
tag_present_on_some: bool = False
@@ -101,7 +100,7 @@ def test_add_tag_to_selection_multiple(qt_driver: QtDriver, library: Library):
assert tag_absent_on_some
# Select the multiple entries
for i, e in enumerate(library.all_entries(with_joins=True), start=0):
for i, e in enumerate(qt_driver.lib.all_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.set_selection(qt_driver.selected)
@@ -109,7 +108,7 @@ def test_add_tag_to_selection_multiple(qt_driver: QtDriver, library: Library):
panel.field_containers_widget.add_tags_to_selected(1000)
# Then reload all entries and recheck the presence of tag 1000
refreshed_entries = library.all_entries(with_joins=True)
refreshed_entries = qt_driver.lib.all_entries(with_joins=True)
tag_present_on_some = False
tag_absent_on_some = False
@@ -123,11 +122,11 @@ def test_add_tag_to_selection_multiple(qt_driver: QtDriver, library: Library):
assert not tag_absent_on_some
def test_meta_tag_category(qt_driver: QtDriver, library: Library, entry_full: Entry):
panel = PreviewPanel(library, qt_driver)
def test_meta_tag_category(qt_driver: QtDriver, entry_full: Entry):
panel = PreviewPanel(qt_driver)
# Ensure the Favorite tag is on entry_full
library.add_tags_to_entries(1, entry_full.id)
qt_driver.lib.add_tags_to_entries(1, entry_full.id)
# Select the single entry
qt_driver.toggle_item_selection(entry_full.id, append=False, bridge=False)
@@ -139,7 +138,7 @@ def test_meta_tag_category(qt_driver: QtDriver, library: Library, entry_full: En
match i:
case 0:
# Check if the container is the Meta Tags category
tag: Tag = unwrap(library.get_tag(2))
tag: Tag = unwrap(qt_driver.lib.get_tag(2))
assert container.title == f"<h4>{tag.name}</h4>"
case 1:
# Check if the container is the Tags category
@@ -151,18 +150,16 @@ def test_meta_tag_category(qt_driver: QtDriver, library: Library, entry_full: En
pass
def test_custom_tag_category(qt_driver: QtDriver, library: Library, entry_full: Entry):
panel = PreviewPanel(library, qt_driver)
def test_custom_tag_category(qt_driver: QtDriver, entry_full: Entry):
panel = PreviewPanel(qt_driver)
# Set tag 1000 (foo) as a category
tag: Tag = unwrap(library.get_tag(1000))
tag: Tag = unwrap(qt_driver.lib.get_tag(1000))
tag.is_category = True
library.update_tag(
tag,
)
qt_driver.lib.update_tag(tag)
# Ensure the Favorite tag is on entry_full
library.add_tags_to_entries(1, entry_full.id)
qt_driver.lib.add_tags_to_entries(1, entry_full.id)
# Select the single entry
qt_driver.toggle_item_selection(entry_full.id, append=False, bridge=False)
@@ -174,7 +171,7 @@ def test_custom_tag_category(qt_driver: QtDriver, library: Library, entry_full:
match i:
case 0:
# Check if the container is the Meta Tags category
tag_2: Tag = unwrap(library.get_tag(2))
tag_2: Tag = unwrap(qt_driver.lib.get_tag(2))
assert container.title == f"<h4>{tag_2.name}</h4>"
case 1:
# Check if the container is the custom "foo" category
+11 -11
View File
@@ -1,6 +1,7 @@
# SPDX-FileCopyrightText: (c) TagStudio Contributors
# SPDX-License-Identifier: GPL-3.0-only
# pyright: reportPrivateUsage=false, reportAttributeAccessIssue=false
import os
from collections.abc import Callable
@@ -8,9 +9,7 @@ from pathlib import Path
from unittest.mock import patch
import pytest
from PySide6.QtGui import (
QAction,
)
from PySide6.QtGui import QAction
from PySide6.QtWidgets import QMenu, QMenuBar
from pytestqt.qtbot import QtBot
@@ -60,11 +59,10 @@ def test_filepath_setting(qtbot: QtBot, qt_driver: QtDriver, filepath_option: Sh
)
def test_file_path_display(
qt_driver: QtDriver,
library: Library,
filepath_option: ShowFilepathOption,
expected_path: Callable[[Library], Path],
):
panel = PreviewPanel(library, qt_driver)
panel = PreviewPanel(qt_driver)
# Select 2
qt_driver.toggle_item_selection(2, append=False, bridge=False)
@@ -73,15 +71,17 @@ def test_file_path_display(
qt_driver.settings.show_filepath = filepath_option
# Apply the mock value
entry = library.get_entry(2)
entry = qt_driver.lib.get_entry(2)
assert isinstance(entry, Entry)
filename = entry.path
panel._file_attributes_widget.update_stats(filepath=unwrap(library.library_dir) / filename) # pyright: ignore[reportPrivateUsage]
panel._file_attributes_widget.update_stats(
filepath=unwrap(qt_driver.lib.library_dir) / filename
)
# Generate the expected file string.
# This is copied directly from the file_attributes.py file
# can be imported as a function in the future
display_path: Path = expected_path(library)
display_path: Path = expected_path(qt_driver.lib)
file_str: str = ""
separator: str = f"<a style='color: #777777'><b>{os.path.sep}</a>" # Gray
for i, part in enumerate(display_path.parts):
@@ -94,7 +94,7 @@ def test_file_path_display(
file_str += f"<b>{'\u200b'.join(part_)}</b>"
# Assert the file path is displayed correctly
assert panel._file_attributes_widget.file_label.text() == file_str # pyright: ignore[reportPrivateUsage]
assert panel._file_attributes_widget.file_label.text() == file_str
@pytest.mark.parametrize(
@@ -146,7 +146,7 @@ def test_title_update(
qt_driver.main_window.menu_bar.folders_to_tags_action = QAction(menu_bar)
# Trigger the update
qt_driver._init_library(library_dir, open_status) # pyright: ignore[reportPrivateUsage]
qt_driver._init_library(library_dir, open_status)
# Assert the title is updated correctly
qt_driver.main_window.setWindowTitle.assert_called_with(expected_title(library_dir, base_title)) # pyright: ignore[reportAttributeAccessIssue]
qt_driver.main_window.setWindowTitle.assert_called_with(expected_title(library_dir, base_title))
+6 -7
View File
@@ -2,14 +2,13 @@
# SPDX-License-Identifier: GPL-3.0-only
from tagstudio.core.library.alchemy.library import Library
from tagstudio.core.library.alchemy.models import Entry
from tagstudio.qt.controllers.preview_panel_controller import PreviewPanel
from tagstudio.qt.ts_qt import QtDriver
def test_update_selection_empty(qt_driver: QtDriver, library: Library):
panel = PreviewPanel(library, qt_driver)
def test_update_selection_empty(qt_driver: QtDriver):
panel = PreviewPanel(qt_driver)
# Clear the library selection (selecting 1 then unselecting 1)
qt_driver.toggle_item_selection(1, append=False, bridge=False)
@@ -20,8 +19,8 @@ def test_update_selection_empty(qt_driver: QtDriver, library: Library):
assert not panel.add_buttons_enabled
def test_update_selection_single(qt_driver: QtDriver, library: Library, entry_full: Entry):
panel = PreviewPanel(library, qt_driver)
def test_update_selection_single(qt_driver: QtDriver, entry_full: Entry):
panel = PreviewPanel(qt_driver)
# Select the single entry
qt_driver.toggle_item_selection(entry_full.id, append=False, bridge=False)
@@ -31,8 +30,8 @@ def test_update_selection_single(qt_driver: QtDriver, library: Library, entry_fu
assert panel.add_buttons_enabled
def test_update_selection_multiple(qt_driver: QtDriver, library: Library):
panel = PreviewPanel(library, qt_driver)
def test_update_selection_multiple(qt_driver: QtDriver):
panel = PreviewPanel(qt_driver)
# Select the multiple entries
qt_driver.toggle_item_selection(1, append=False, bridge=False)