From f2cecb26484b9ffa4798b35b6016a65909395bdd Mon Sep 17 00:00:00 2001 From: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com> Date: Sat, 18 Jul 2026 02:40:43 -0700 Subject: [PATCH] refactor: code cleanup and fixes --- .../controllers/preview_panel_controller.py | 27 ++- .../qt/controllers/suggest_box_controller.py | 169 ++++++-------- .../controllers/tag_suggest_box_controller.py | 209 +++++------------- src/tagstudio/qt/global_settings.py | 1 + src/tagstudio/qt/views/main_window.py | 2 +- src/tagstudio/qt/views/preview_panel_view.py | 45 ++-- src/tagstudio/qt/views/suggest_box_view.py | 65 +++--- .../qt/views/tag_suggest_box_view.py | 16 -- tests/qt/test_field_containers.py | 57 +++-- tests/qt/test_file_path_options.py | 22 +- tests/qt/test_preview_panel.py | 13 +- 11 files changed, 235 insertions(+), 391 deletions(-) delete mode 100644 src/tagstudio/qt/views/tag_suggest_box_view.py diff --git a/src/tagstudio/qt/controllers/preview_panel_controller.py b/src/tagstudio/qt/controllers/preview_panel_controller.py index 6d236c21..16b20920 100644 --- a/src/tagstudio/qt/controllers/preview_panel_controller.py +++ b/src/tagstudio/qt/controllers/preview_panel_controller.py @@ -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) diff --git a/src/tagstudio/qt/controllers/suggest_box_controller.py b/src/tagstudio/qt/controllers/suggest_box_controller.py index da948988..15389ed0 100644 --- a/src/tagstudio/qt/controllers/suggest_box_controller.py +++ b/src/tagstudio/qt/controllers/suggest_box_controller.py @@ -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() diff --git a/src/tagstudio/qt/controllers/tag_suggest_box_controller.py b/src/tagstudio/qt/controllers/tag_suggest_box_controller.py index 687e1708..6e18aec0 100644 --- a/src/tagstudio/qt/controllers/tag_suggest_box_controller.py +++ b/src/tagstudio/qt/controllers/tag_suggest_box_controller.py @@ -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() diff --git a/src/tagstudio/qt/global_settings.py b/src/tagstudio/qt/global_settings.py index 85fe1f36..58fb7192 100644 --- a/src/tagstudio/qt/global_settings.py +++ b/src/tagstudio/qt/global_settings.py @@ -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) diff --git a/src/tagstudio/qt/views/main_window.py b/src/tagstudio/qt/views/main_window.py index e9be7398..de0516be 100644 --- a/src/tagstudio/qt/views/main_window.py +++ b/src/tagstudio/qt/views/main_window.py @@ -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): diff --git a/src/tagstudio/qt/views/preview_panel_view.py b/src/tagstudio/qt/views/preview_panel_view.py index d0f2292f..908f6788 100644 --- a/src/tagstudio/qt/views/preview_panel_view.py +++ b/src/tagstudio/qt/views/preview_panel_view.py @@ -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) diff --git a/src/tagstudio/qt/views/suggest_box_view.py b/src/tagstudio/qt/views/suggest_box_view.py index 9357b580..fc961a6d 100644 --- a/src/tagstudio/qt/views/suggest_box_view.py +++ b/src/tagstudio/qt/views/suggest_box_view.py @@ -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) diff --git a/src/tagstudio/qt/views/tag_suggest_box_view.py b/src/tagstudio/qt/views/tag_suggest_box_view.py deleted file mode 100644 index 51601bf1..00000000 --- a/src/tagstudio/qt/views/tag_suggest_box_view.py +++ /dev/null @@ -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) diff --git a/tests/qt/test_field_containers.py b/tests/qt/test_field_containers.py index 0d19e4e0..9a0a3c52 100644 --- a/tests/qt/test_field_containers.py +++ b/tests/qt/test_field_containers.py @@ -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"