mirror of
https://github.com/TagStudioDev/TagStudio.git
synced 2026-07-21 21:16:20 +02:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a1f9d34629 | |||
| 9780d68d2c |
@@ -2,7 +2,6 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
|
||||
import typing
|
||||
from typing import override
|
||||
from warnings import catch_warnings
|
||||
|
||||
@@ -18,19 +17,18 @@ from tagstudio.qt.controllers.modal import Modal
|
||||
from tagstudio.qt.controllers.modal_content import ModalContent
|
||||
from tagstudio.qt.controllers.suggest_box import SuggestBox
|
||||
from tagstudio.qt.controllers.underlined_widget import UnderlinedWidget
|
||||
from tagstudio.qt.global_settings import GlobalSettings
|
||||
from tagstudio.qt.translations import Translations
|
||||
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 FieldSuggestBox(SuggestBox[BaseFieldTemplate]):
|
||||
def __init__(self, driver: "QtDriver", view: SuggestBoxView | None = None):
|
||||
super().__init__(driver, view=view or SuggestBoxView())
|
||||
self._lib = self._driver.lib
|
||||
def __init__(
|
||||
self, library: Library, settings: GlobalSettings, view: SuggestBoxView | None = None
|
||||
):
|
||||
super().__init__(library, settings, view or SuggestBoxView())
|
||||
|
||||
# Context Menu Actions
|
||||
edit_field_on_add_action = QAction(Translations["settings.edit_field_on_add"], self)
|
||||
@@ -39,15 +37,15 @@ class FieldSuggestBox(SuggestBox[BaseFieldTemplate]):
|
||||
self.addAction(edit_field_on_add_action)
|
||||
self.layout().search_field.setContextMenuPolicy(Qt.ContextMenuPolicy.ActionsContextMenu)
|
||||
self.layout().search_field.addAction(edit_field_on_add_action)
|
||||
edit_field_on_add_action.setChecked(self._driver.settings.edit_field_on_add)
|
||||
edit_field_on_add_action.setChecked(self._settings.edit_field_on_add)
|
||||
edit_field_on_add_action.triggered.connect(
|
||||
lambda checked: self.toggle_edit_on_field_add(checked)
|
||||
)
|
||||
|
||||
def toggle_edit_on_field_add(self, checked: bool) -> None:
|
||||
"""Toggle the setting for opening the edit window after adding a field."""
|
||||
self._driver.settings.edit_field_on_add = checked
|
||||
self._driver.settings.save()
|
||||
self._settings.edit_field_on_add = checked
|
||||
self._settings.save()
|
||||
|
||||
@override
|
||||
def _on_item_create(self) -> None:
|
||||
|
||||
@@ -7,7 +7,7 @@ from warnings import catch_warnings
|
||||
|
||||
import structlog
|
||||
from PySide6.QtCore import Signal
|
||||
from PySide6.QtWidgets import QMessageBox
|
||||
from PySide6.QtWidgets import QMessageBox, QWidget
|
||||
|
||||
from tagstudio.core.library.alchemy.fields import BaseFieldTemplate
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
@@ -17,44 +17,30 @@ from tagstudio.qt.controllers.modal import Modal
|
||||
from tagstudio.qt.controllers.modal_content import ModalContent
|
||||
from tagstudio.qt.controllers.search_panel_controller import SearchPanel
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.qt.views.field_template_search_panel_view import FieldTemplateSearchPanelView
|
||||
from tagstudio.qt.views.search_panel_view import SearchPanelView
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
class FieldTemplateSearchModal(Modal):
|
||||
def __init__(
|
||||
self,
|
||||
library: Library,
|
||||
is_field_template_chooser: bool = True,
|
||||
has_save: bool = False,
|
||||
) -> None:
|
||||
self.search_panel: FieldTemplateSearchPanel = FieldTemplateSearchPanel(
|
||||
library,
|
||||
is_field_template_chooser,
|
||||
view=FieldTemplateSearchPanelView(is_field_template_chooser),
|
||||
)
|
||||
super().__init__(self.search_panel, Translations["field.add.plural"], is_savable=has_save)
|
||||
|
||||
|
||||
class FieldTemplateSearchPanel(SearchPanel[BaseFieldTemplate]):
|
||||
field_template_chosen = Signal(object)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
library: Library,
|
||||
is_field_template_chooser: bool = True,
|
||||
view: FieldTemplateSearchPanelView | None = None,
|
||||
is_chooser: bool = True,
|
||||
view: SearchPanelView | None = None,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
view=view or FieldTemplateSearchPanelView(is_field_template_chooser),
|
||||
view=view
|
||||
or SearchPanelView(Translations["home.search_field_templates"], is_chooser=is_chooser),
|
||||
exclude=[],
|
||||
is_chooser=is_field_template_chooser,
|
||||
is_chooser=is_chooser,
|
||||
)
|
||||
self.__lib = library
|
||||
|
||||
self._unlimited_limit_item_label = Translations["field_template.all_field_templates"]
|
||||
self._create_and_add_button_label_key = "field_template.create_add"
|
||||
self._create_and_add_button_key = "field_template.create_add"
|
||||
|
||||
@override
|
||||
def _get_max_limit(self) -> int:
|
||||
@@ -102,7 +88,7 @@ class FieldTemplateSearchPanel(SearchPanel[BaseFieldTemplate]):
|
||||
|
||||
@override
|
||||
def _on_item_remove(self, item: BaseFieldTemplate) -> None:
|
||||
if self.is_chooser:
|
||||
if self._is_chooser:
|
||||
return
|
||||
|
||||
message_box = QMessageBox(
|
||||
@@ -138,7 +124,7 @@ class FieldTemplateSearchPanel(SearchPanel[BaseFieldTemplate]):
|
||||
if item is None:
|
||||
return
|
||||
|
||||
field_template_widget.has_remove = not self.is_chooser
|
||||
field_template_widget.has_remove = not self._is_chooser
|
||||
|
||||
# Disconnect previous callbacks
|
||||
with catch_warnings(record=True):
|
||||
@@ -175,4 +161,18 @@ class FieldTemplateSearchPanel(SearchPanel[BaseFieldTemplate]):
|
||||
self.__lib.update_field_template(
|
||||
edit_item_panel.old_field_type, edit_item_panel.build_field_template()
|
||||
)
|
||||
self.update_items(self.search_field.text())
|
||||
self.update_items(self.layout().search_field.text())
|
||||
|
||||
@override
|
||||
def get_item_widget(self, index: int, library: Library | None) -> FieldTemplateWidget:
|
||||
"""Gets the item widget at a specific index."""
|
||||
# Create any new item widgets needed up to the given index
|
||||
if self.layout().scroll_layout.count() <= index:
|
||||
while self.layout().scroll_layout.count() <= index:
|
||||
pad_field_template_widget = FieldTemplateWidget()
|
||||
pad_field_template_widget.setHidden(True)
|
||||
self.layout().scroll_layout.addWidget(pad_field_template_widget)
|
||||
|
||||
field_template_widget: QWidget = self.layout().scroll_layout.itemAt(index).widget()
|
||||
assert isinstance(field_template_widget, FieldTemplateWidget)
|
||||
return field_template_widget
|
||||
|
||||
@@ -8,7 +8,7 @@ import structlog
|
||||
from PySide6 import QtCore, QtGui
|
||||
from PySide6.QtCore import Signal
|
||||
from PySide6.QtGui import QShowEvent
|
||||
from PySide6.QtWidgets import QVBoxLayout, QWidget
|
||||
from PySide6.QtWidgets import QWidget
|
||||
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.qt.controllers.modal_content import ModalContent
|
||||
@@ -50,18 +50,18 @@ class SearchPanel[T](ModalContent):
|
||||
is_chooser: bool = True,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.view = view
|
||||
self.is_chooser = is_chooser
|
||||
self.setLayout(QVBoxLayout(self))
|
||||
self.layout().setContentsMargins(0, 0, 0, 0)
|
||||
self.layout().addWidget(self.view)
|
||||
self.view.connect_callbacks(self)
|
||||
self._driver: QtDriver | None = None
|
||||
self.exclude: list[int] = exclude or []
|
||||
self._is_chooser = is_chooser
|
||||
self._create_and_add_button_in_layout = False
|
||||
self._create_and_add_button_key: str = ""
|
||||
|
||||
# Items
|
||||
self._excluded: list[int] = exclude or []
|
||||
self._search_results: list[T] = []
|
||||
|
||||
# Limits
|
||||
self._unlimited_limit_item_label: str = "All Items"
|
||||
self.__limit_items: list[tuple[str, int]] = [
|
||||
self._limit_items: list[tuple[str, int]] = [
|
||||
("25", 25),
|
||||
("50", 50),
|
||||
("100", 100),
|
||||
@@ -69,66 +69,102 @@ class SearchPanel[T](ModalContent):
|
||||
("500", 500),
|
||||
(self._unlimited_limit_item_label, -1),
|
||||
]
|
||||
self.__default_limit_index: int = 0 # 25 Limit (Default)
|
||||
self.__previous_limit_index: int = self.__default_limit_index
|
||||
self._default_limit_index: int = 0 # 25 Limit (Default)
|
||||
self._previous_limit_index: int = self._default_limit_index
|
||||
|
||||
self.view.set_limit_items(self.__limit_items)
|
||||
self.view.set_limit_index(self.__default_limit_index)
|
||||
self.setLayout(view)
|
||||
self.set_limit_items(self._limit_items)
|
||||
self.set_limit_index(self._default_limit_index)
|
||||
self.setMinimumSize(300, 400)
|
||||
self.connect_callbacks(self)
|
||||
|
||||
# Items
|
||||
self._search_results: list[T] = []
|
||||
def connect_callbacks(self, controller: "SearchPanel[Any]") -> None: # pyright: ignore[reportExplicitAny]
|
||||
self.layout().limit_combobox.currentIndexChanged.connect(controller.on_limit_changed)
|
||||
self.layout().search_field.textChanged.connect(controller.on_search_query_changed)
|
||||
self.layout().search_field.returnPressed.connect(
|
||||
lambda: controller.on_search_query_submitted(self.get_search_query())
|
||||
)
|
||||
self.layout().create_button.clicked.connect(controller.on_item_create)
|
||||
self.layout().create_and_add_button.clicked.connect(
|
||||
lambda: controller.on_item_create(add_to_entry=True)
|
||||
)
|
||||
|
||||
self._create_and_add_button_label_key: str = ""
|
||||
def set_limit_items(self, limit_items: list[tuple[str, int]]) -> None:
|
||||
# Remove existing limit items
|
||||
for i in reversed(range(self.layout().limit_combobox.count())):
|
||||
self.layout().limit_combobox.removeItem(i)
|
||||
|
||||
@property
|
||||
def search_field(self):
|
||||
return self.view.search_field
|
||||
# Add new limit items
|
||||
self.layout().limit_combobox.addItems([limit_item[0] for limit_item in limit_items])
|
||||
|
||||
@property
|
||||
def create_and_add_button(self):
|
||||
return self.view.create_and_add_button
|
||||
def get_limit_index(self) -> int:
|
||||
return self.layout().limit_combobox.currentIndex()
|
||||
|
||||
def set_limit_index(self, index: int) -> None:
|
||||
self.layout().limit_combobox.setCurrentIndex(index)
|
||||
|
||||
def focus_search_box(self, select_all: bool = False) -> None:
|
||||
self.layout().search_field.setFocus()
|
||||
if select_all:
|
||||
self.layout().search_field.selectAll()
|
||||
|
||||
def get_search_query(self) -> str:
|
||||
return self.view.get_search_query()
|
||||
return self.layout().search_field.text()
|
||||
|
||||
def clear_search_query(self) -> None:
|
||||
self.view.clear_search_query()
|
||||
self.layout().search_field.setText("")
|
||||
self.focus_search_box()
|
||||
|
||||
def get_item_widget(self, index: int, library: Library):
|
||||
return self.view.get_item_widget(index, library)
|
||||
# Item list
|
||||
def scroll_to(self, position: int) -> None:
|
||||
self.layout().scroll_area.verticalScrollBar().setValue(position)
|
||||
|
||||
def set_driver(self, driver: "QtDriver") -> None:
|
||||
self._driver = driver
|
||||
def add_create_and_add_button(self) -> None:
|
||||
if self._create_and_add_button_in_layout:
|
||||
return
|
||||
self.layout().scroll_layout.addWidget(self.layout().create_and_add_button)
|
||||
self.layout().create_and_add_button.show()
|
||||
self._create_and_add_button_in_layout = True
|
||||
|
||||
def remove_create_and_add_button(self) -> None:
|
||||
if not self._create_and_add_button_in_layout:
|
||||
return
|
||||
self.layout().scroll_layout.removeWidget(self.layout().create_and_add_button)
|
||||
self.layout().create_and_add_button.hide()
|
||||
self._create_and_add_button_in_layout = False
|
||||
|
||||
def get_item_widget(self, index: int, library: Library) -> Any: # pyright: ignore[reportExplicitAny]
|
||||
return self.get_item_widget(index, library)
|
||||
|
||||
def on_limit_changed(self, index: int) -> None:
|
||||
# Method was called outside the limit_combobox callback
|
||||
if index != self.view.get_limit_index():
|
||||
self.view.set_limit_index(index)
|
||||
if index != self.get_limit_index():
|
||||
self.set_limit_index(index)
|
||||
|
||||
if self.__previous_limit_index == index:
|
||||
if self._previous_limit_index == index:
|
||||
return
|
||||
|
||||
self.update_items(self.search_field.text())
|
||||
self.update_items(self.layout().search_field.text())
|
||||
|
||||
def _get_limit(self) -> tuple[str, int]:
|
||||
return self.__limit_items[self.view.get_limit_index()]
|
||||
return self._limit_items[self.get_limit_index()]
|
||||
|
||||
def _get_previous_limit(self) -> tuple[str, int]:
|
||||
return self.__limit_items[self.__previous_limit_index]
|
||||
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.create_and_add_button.setText(
|
||||
Translations.format(self._create_and_add_button_label_key, query=query)
|
||||
self.layout().create_and_add_button.setText(
|
||||
Translations.format(self._create_and_add_button_key, query=query)
|
||||
)
|
||||
self.update_items(query)
|
||||
|
||||
def on_search_query_submitted(self, query: str) -> None:
|
||||
# Focus search field if no query
|
||||
if not query:
|
||||
self.search_field.setFocus()
|
||||
self.layout().search_field.setFocus()
|
||||
parent: QWidget | None = self.parentWidget()
|
||||
if parent is not None: # pyright: ignore[reportUnnecessaryComparison]
|
||||
parent.hide()
|
||||
@@ -137,7 +173,7 @@ class SearchPanel[T](ModalContent):
|
||||
# Create and add item if no search results
|
||||
if len(self._search_results) <= 0:
|
||||
self.on_item_create(add_to_entry=True)
|
||||
elif self.is_chooser:
|
||||
elif self._is_chooser:
|
||||
self._on_item_chosen(self._search_results[0])
|
||||
|
||||
self.clear_search_query()
|
||||
@@ -156,14 +192,14 @@ class SearchPanel[T](ModalContent):
|
||||
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._get_limit()[1])
|
||||
|
||||
# Remove the "Create & Add" button if one exists
|
||||
self.view.remove_create_and_add_button()
|
||||
self.remove_create_and_add_button()
|
||||
|
||||
# Get results for the search query
|
||||
query_lower = "" if not query else query.lower()
|
||||
@@ -205,11 +241,11 @@ class SearchPanel[T](ModalContent):
|
||||
item: T | None = all_results[i] if i < len(all_results) else None
|
||||
self.set_item_widget(item=item, index=i)
|
||||
|
||||
self.__previous_limit_index = self.view.get_limit_index()
|
||||
self._previous_limit_index = self.get_limit_index()
|
||||
|
||||
# Add back the "Create & Add" button
|
||||
if query and query.strip():
|
||||
self.view.add_create_and_add_button()
|
||||
self.add_create_and_add_button()
|
||||
|
||||
def search_items(self, query: str) -> tuple[list[T], list[T]]: # pyright: ignore[reportUnusedParameter]
|
||||
raise NotImplementedError()
|
||||
@@ -217,11 +253,16 @@ class SearchPanel[T](ModalContent):
|
||||
def set_item_widget(self, item: T | None, index: int) -> None: # pyright: ignore[reportUnusedParameter]
|
||||
raise NotImplementedError()
|
||||
|
||||
@override
|
||||
def layout(self) -> SearchPanelView:
|
||||
"""Return the typed layout for this widget."""
|
||||
return super().layout() # pyright: ignore[reportReturnType]
|
||||
|
||||
@override
|
||||
def showEvent(self, event: QShowEvent) -> None: # noqa N802
|
||||
self.update_items()
|
||||
self.view.scroll_to(0)
|
||||
self.view.clear_search_query()
|
||||
self.scroll_to(0)
|
||||
self.clear_search_query()
|
||||
return super().showEvent(event)
|
||||
|
||||
@override
|
||||
@@ -229,10 +270,10 @@ class SearchPanel[T](ModalContent):
|
||||
# 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():
|
||||
if self.layout().search_field.hasFocus():
|
||||
super().keyPressEvent(event)
|
||||
else:
|
||||
self.view.focus_search_box(select_all=True)
|
||||
self.focus_search_box(select_all=True)
|
||||
|
||||
def create_item(self, edit_item_panel: ModalContent, choose_item: bool = False) -> None: # pyright: ignore[reportUnusedParameter]
|
||||
raise NotImplementedError()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# SPDX-FileCopyrightText: (c) TagStudio Contributors
|
||||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
from typing import TYPE_CHECKING, Any, override
|
||||
from typing import Any, override
|
||||
|
||||
import structlog
|
||||
from PySide6.QtCore import Signal
|
||||
@@ -12,6 +12,7 @@ from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.qt.controllers.autofill_line_edit import QtCore, QtGui
|
||||
from tagstudio.qt.controllers.modal_content import ModalContent
|
||||
from tagstudio.qt.controllers.underlined_widget import UnderlinedWidget
|
||||
from tagstudio.qt.global_settings import GlobalSettings
|
||||
from tagstudio.qt.views.stylesheets.stylesheets import (
|
||||
autofill_line_edit_style,
|
||||
autofill_line_edit_top_style,
|
||||
@@ -20,9 +21,6 @@ from tagstudio.qt.views.suggest_box_view import SuggestBoxView
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from tagstudio.qt.ts_qt import QtDriver
|
||||
|
||||
|
||||
def _item_id(item: object) -> int:
|
||||
item_id: Any = getattr(item, "id") # noqa: B009 # pyright: ignore[reportExplicitAny]
|
||||
@@ -46,9 +44,10 @@ class SuggestBox[T](QWidget):
|
||||
item_chosen = Signal(object)
|
||||
done = Signal()
|
||||
|
||||
def __init__(self, driver: "QtDriver", view: SuggestBoxView) -> None:
|
||||
def __init__(self, library: Library, settings: GlobalSettings, view: SuggestBoxView) -> None:
|
||||
super().__init__()
|
||||
self._driver = driver
|
||||
self._lib = library
|
||||
self._settings = settings
|
||||
self._limit = 5
|
||||
self._is_shift_held = False
|
||||
self._search_results: list[T] = []
|
||||
|
||||
@@ -6,10 +6,10 @@ from typing import override
|
||||
from warnings import catch_warnings
|
||||
|
||||
import structlog
|
||||
from PySide6.QtWidgets import QMessageBox
|
||||
from PySide6.QtCore import Signal
|
||||
from PySide6.QtWidgets import QMessageBox, 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.modal import Modal
|
||||
@@ -17,47 +17,33 @@ from tagstudio.qt.controllers.modal_content import ModalContent
|
||||
from tagstudio.qt.controllers.search_panel_controller import SearchPanel
|
||||
from tagstudio.qt.mixed.tag_widget import TagWidget
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.qt.views.tag_search_panel_view import TagSearchPanelView
|
||||
from tagstudio.qt.views.search_panel_view import SearchPanelView
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
class TagSearchModal(Modal):
|
||||
def __init__(
|
||||
self,
|
||||
library: Library,
|
||||
title: str,
|
||||
exclude: list[int] | None = None,
|
||||
is_tag_chooser: bool = True,
|
||||
has_save: bool = False,
|
||||
):
|
||||
self.tsp = TagSearchPanel(
|
||||
library, exclude, is_tag_chooser, view=TagSearchPanelView(is_tag_chooser)
|
||||
)
|
||||
super().__init__(content_widget=self.tsp, title=title, is_savable=has_save)
|
||||
|
||||
|
||||
class TagSearchPanel(SearchPanel[Tag]):
|
||||
search_for_tag = Signal(int)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
library: Library,
|
||||
exclude: list[int] | None = None,
|
||||
is_tag_chooser: bool = True,
|
||||
view: TagSearchPanelView | None = None,
|
||||
is_chooser: bool = True,
|
||||
view: SearchPanelView | None = None,
|
||||
):
|
||||
super().__init__(
|
||||
view=view or TagSearchPanelView(is_tag_chooser),
|
||||
view=view or SearchPanelView(Translations["home.search_tags"], is_chooser=is_chooser),
|
||||
exclude=exclude,
|
||||
is_chooser=is_tag_chooser,
|
||||
is_chooser=is_chooser,
|
||||
)
|
||||
self.__lib = library
|
||||
|
||||
self._lib = library
|
||||
self._unlimited_limit_item_label = Translations["tag.all_tags"]
|
||||
self._create_and_add_button_label_key = "tag.create_add"
|
||||
self._create_and_add_button_key = "tag.create_add"
|
||||
|
||||
@override
|
||||
def _get_max_limit(self) -> int:
|
||||
return len(self.__lib.tags)
|
||||
return len(self._lib.tags)
|
||||
|
||||
@override
|
||||
def on_item_create(self, add_to_entry: bool = False) -> None:
|
||||
@@ -72,8 +58,7 @@ class TagSearchPanel(SearchPanel[Tag]):
|
||||
from tagstudio.qt.mixed.build_tag import BuildTagPanel # here due to circular imports
|
||||
|
||||
query: str = self.get_search_query()
|
||||
|
||||
panel: BuildTagPanel = BuildTagPanel(self.__lib)
|
||||
panel: BuildTagPanel = BuildTagPanel(self._lib)
|
||||
modal: Modal = Modal(
|
||||
panel,
|
||||
Translations["tag.new"],
|
||||
@@ -92,10 +77,10 @@ class TagSearchPanel(SearchPanel[Tag]):
|
||||
# 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: Modal = Modal(
|
||||
edit_tag_panel,
|
||||
self.__lib.tag_display_name(item),
|
||||
self._lib.tag_display_name(item),
|
||||
Translations["tag.edit"],
|
||||
is_savable=True,
|
||||
)
|
||||
@@ -104,7 +89,7 @@ class TagSearchPanel(SearchPanel[Tag]):
|
||||
|
||||
@override
|
||||
def _on_item_remove(self, item: Tag) -> None:
|
||||
if self.is_chooser:
|
||||
if self._is_chooser:
|
||||
return
|
||||
|
||||
if item.id in range(RESERVED_TAG_START, RESERVED_TAG_END):
|
||||
@@ -113,7 +98,7 @@ class TagSearchPanel(SearchPanel[Tag]):
|
||||
message_box = QMessageBox(
|
||||
QMessageBox.Icon.Question,
|
||||
Translations["tag.remove"],
|
||||
Translations.format("tag.confirm_delete", tag_name=self.__lib.tag_display_name(item)),
|
||||
Translations.format("tag.confirm_delete", tag_name=self._lib.tag_display_name(item)),
|
||||
QMessageBox.StandardButton.Ok | QMessageBox.StandardButton.Cancel,
|
||||
)
|
||||
|
||||
@@ -122,7 +107,7 @@ class TagSearchPanel(SearchPanel[Tag]):
|
||||
if result != QMessageBox.StandardButton.Ok:
|
||||
return
|
||||
|
||||
self.__lib.remove_tag(item.id)
|
||||
self._lib.remove_tag(item.id)
|
||||
self.update_items(self.get_search_query())
|
||||
|
||||
@override
|
||||
@@ -131,12 +116,12 @@ class TagSearchPanel(SearchPanel[Tag]):
|
||||
|
||||
@override
|
||||
def search_items(self, query: str) -> tuple[list[Tag], list[Tag]]:
|
||||
return self.__lib.search_tags(name=query, limit=self._get_limit()[1])
|
||||
return self._lib.search_tags(name=query, limit=self._get_limit()[1])
|
||||
|
||||
@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.set_tag(item)
|
||||
tag_widget.setHidden(item is None)
|
||||
|
||||
@@ -144,7 +129,7 @@ class TagSearchPanel(SearchPanel[Tag]):
|
||||
return
|
||||
assert item is not None
|
||||
|
||||
tag_widget.has_remove = not self.is_chooser and item.id not in range(
|
||||
tag_widget.has_remove = not self._is_chooser and item.id not in range(
|
||||
RESERVED_TAG_START, RESERVED_TAG_END
|
||||
)
|
||||
|
||||
@@ -158,7 +143,7 @@ class TagSearchPanel(SearchPanel[Tag]):
|
||||
# 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:
|
||||
if self._is_chooser:
|
||||
tag_widget.bg_button.clicked.connect(
|
||||
lambda checked=False, tag=item: self._on_item_chosen(tag)
|
||||
)
|
||||
@@ -168,16 +153,10 @@ class TagSearchPanel(SearchPanel[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.search_for_tag_action.triggered.connect(
|
||||
lambda checked=False, tag_id=item.id: self.search_for_tag.emit(tag_id)
|
||||
)
|
||||
tag_widget.search_for_tag_action.setEnabled(True)
|
||||
|
||||
@override
|
||||
def create_item(self, edit_item_panel: ModalContent, choose_item: bool = False) -> None:
|
||||
@@ -186,7 +165,7 @@ class TagSearchPanel(SearchPanel[Tag]):
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
@@ -205,18 +184,25 @@ class TagSearchPanel(SearchPanel[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.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
|
||||
@override
|
||||
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.layout().scroll_layout.count() <= index:
|
||||
while self.layout().scroll_layout.count() <= index:
|
||||
pad_tag_widget = TagWidget(
|
||||
tag=None, has_edit=True, has_remove=True, library=library
|
||||
)
|
||||
pad_tag_widget.setHidden(True)
|
||||
self.layout().scroll_layout.addWidget(pad_tag_widget)
|
||||
|
||||
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)
|
||||
)
|
||||
tag_widget: QWidget = self.layout().scroll_layout.itemAt(index).widget()
|
||||
assert isinstance(tag_widget, TagWidget)
|
||||
return tag_widget
|
||||
|
||||
@@ -2,37 +2,36 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
|
||||
import typing
|
||||
from typing import override
|
||||
from warnings import catch_warnings
|
||||
|
||||
import structlog
|
||||
from PySide6.QtCore import Signal
|
||||
from PySide6.QtGui import QAction, Qt
|
||||
from PySide6.QtWidgets import QGraphicsOpacityEffect, QWidget
|
||||
|
||||
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.modal import Modal
|
||||
from tagstudio.qt.controllers.modal_content import ModalContent
|
||||
from tagstudio.qt.controllers.suggest_box import SuggestBox
|
||||
from tagstudio.qt.controllers.underlined_widget import UnderlinedWidget
|
||||
from tagstudio.qt.global_settings import GlobalSettings
|
||||
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.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, driver: "QtDriver", view: SuggestBoxView | None = None):
|
||||
super().__init__(driver, view=view or SuggestBoxView())
|
||||
self._driver = driver
|
||||
self._lib = self._driver.lib
|
||||
search_for_tag = Signal(int)
|
||||
|
||||
def __init__(
|
||||
self, library: Library, settings: GlobalSettings, view: SuggestBoxView | None = None
|
||||
):
|
||||
super().__init__(library, settings, view or SuggestBoxView())
|
||||
|
||||
# Context Menu Actions
|
||||
edit_tag_on_create_action = QAction(Translations["settings.edit_tag_on_create"], self)
|
||||
@@ -41,21 +40,15 @@ class TagSuggestBox(SuggestBox[Tag]):
|
||||
self.addAction(edit_tag_on_create_action)
|
||||
self.layout().search_field.setContextMenuPolicy(Qt.ContextMenuPolicy.ActionsContextMenu)
|
||||
self.layout().search_field.addAction(edit_tag_on_create_action)
|
||||
edit_tag_on_create_action.setChecked(self._driver.settings.edit_tag_on_create)
|
||||
edit_tag_on_create_action.setChecked(self._settings.edit_tag_on_create)
|
||||
edit_tag_on_create_action.triggered.connect(
|
||||
lambda checked: self.toggle_edit_on_tag_create(checked)
|
||||
)
|
||||
|
||||
def _search_for_tag_callback(self, tag_id: int) -> None:
|
||||
self._driver.main_window.search_field.setText(f"tag_id:{tag_id}")
|
||||
self._driver.update_browsing_state(
|
||||
BrowsingState.from_tag_id(tag_id, self._driver.browsing_history.current)
|
||||
)
|
||||
|
||||
def toggle_edit_on_tag_create(self, checked: bool) -> None:
|
||||
"""Toggle the setting for opening the edit window after creating a tag."""
|
||||
self._driver.settings.edit_tag_on_create = checked
|
||||
self._driver.settings.save()
|
||||
self._settings.edit_tag_on_create = checked
|
||||
self._settings.save()
|
||||
|
||||
@override
|
||||
def _on_item_create(self) -> None:
|
||||
@@ -68,7 +61,7 @@ class TagSuggestBox(SuggestBox[Tag]):
|
||||
"""
|
||||
query: str = self.layout().search_field.text()
|
||||
|
||||
if self._driver.settings.edit_tag_on_create:
|
||||
if self._settings.edit_tag_on_create:
|
||||
panel: BuildTagPanel = BuildTagPanel(self._lib)
|
||||
modal: Modal = Modal(
|
||||
panel, Translations["tag.new"], Translations["tag.new"], is_savable=True
|
||||
@@ -142,7 +135,7 @@ class TagSuggestBox(SuggestBox[Tag]):
|
||||
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_callback(tag_id)
|
||||
lambda checked=False, tag_id=item.id: self.search_for_tag.emit(tag_id)
|
||||
)
|
||||
tag_widget.search_for_tag_action.setEnabled(True)
|
||||
|
||||
|
||||
@@ -30,11 +30,12 @@ from tagstudio.core.library.alchemy.models import Tag, TagAlias, TagColorGroup
|
||||
from tagstudio.core.utils.types import unwrap
|
||||
from tagstudio.qt.controllers.modal import Modal
|
||||
from tagstudio.qt.controllers.modal_content import ModalContent
|
||||
from tagstudio.qt.controllers.tag_search_panel_controller import TagSearchModal
|
||||
from tagstudio.qt.controllers.tag_search_panel_controller import TagSearchPanel
|
||||
from tagstudio.qt.mixed.tag_color_preview import TagColorPreview
|
||||
from tagstudio.qt.mixed.tag_color_selection import TagColorSelection
|
||||
from tagstudio.qt.mixed.tag_widget import TagWidget
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.qt.views.search_panel_view import SearchPanelView
|
||||
from tagstudio.qt.views.stylesheets.stylesheets import (
|
||||
checkbox_style,
|
||||
colored_radio_button_style,
|
||||
@@ -79,7 +80,7 @@ class BuildTagPanel(ModalContent):
|
||||
|
||||
def __init__(self, library: Library, tag: Tag | None = None) -> None:
|
||||
super().__init__()
|
||||
self.lib = library
|
||||
self._lib = library
|
||||
self.tag: Tag # NOTE: This gets set at the end of the init.
|
||||
self.tag_color_namespace: str | None
|
||||
self.tag_color_slug: str | None
|
||||
@@ -154,7 +155,6 @@ class BuildTagPanel(ModalContent):
|
||||
|
||||
self.parent_tags_title = QLabel(Translations["tag.parent_tags"])
|
||||
self.parent_tags_layout.addWidget(self.parent_tags_title)
|
||||
|
||||
self.scroll_contents = QWidget()
|
||||
self.parent_tags_scroll_layout = QVBoxLayout(self.scroll_contents)
|
||||
self.parent_tags_scroll_layout.setContentsMargins(6, 6, 6, 0)
|
||||
@@ -177,10 +177,11 @@ class BuildTagPanel(ModalContent):
|
||||
if tag is not None:
|
||||
exclude_ids.append(tag.id)
|
||||
|
||||
self.add_tag_modal = TagSearchModal(
|
||||
self.lib, title=Translations["tag.add.plural"], exclude=exclude_ids
|
||||
)
|
||||
self.add_tag_modal.tsp.item_chosen.connect(lambda x: self._add_parent_tag_callback(x))
|
||||
tsp_view = SearchPanelView(placeholder_text=Translations["home.search_tags"])
|
||||
tsp = TagSearchPanel(self._lib, exclude=exclude_ids, view=tsp_view)
|
||||
self.add_tag_modal = Modal(tsp, title=Translations["tag.add.plural"])
|
||||
tsp.item_chosen.connect(lambda x: self._add_parent_tag_callback(x))
|
||||
|
||||
self.parent_tags_add_button.clicked.connect(self.add_tag_modal.show)
|
||||
|
||||
# Color ----------------------------------------------------------------
|
||||
@@ -195,12 +196,12 @@ class BuildTagPanel(ModalContent):
|
||||
self.color_button: TagColorPreview
|
||||
try:
|
||||
assert tag is not None
|
||||
self.color_button = TagColorPreview(self.lib, tag.color)
|
||||
self.color_button = TagColorPreview(self._lib, tag.color)
|
||||
except Exception as e:
|
||||
# TODO: Investigate why this happens during tests
|
||||
logger.error("[BuildTag] Could not access Tag member attributes", error=e)
|
||||
self.color_button = TagColorPreview(self.lib, None)
|
||||
self.tag_color_selection = TagColorSelection(self.lib)
|
||||
self.color_button = TagColorPreview(self._lib, None)
|
||||
self.tag_color_selection = TagColorSelection(self._lib)
|
||||
chose_tag_color_title = Translations["tag.choose_color"]
|
||||
self.choose_color_modal = Modal(
|
||||
self.tag_color_selection, chose_tag_color_title, chose_tag_color_title
|
||||
@@ -326,7 +327,7 @@ class BuildTagPanel(ModalContent):
|
||||
next_tab: QWidget = last_tab
|
||||
|
||||
for parent_id in self.parent_ids:
|
||||
tag = self.lib.get_tag(parent_id)
|
||||
tag = self._lib.get_tag(parent_id)
|
||||
if not tag:
|
||||
continue
|
||||
is_disam = parent_id == self.disambiguation_id
|
||||
@@ -364,7 +365,7 @@ class BuildTagPanel(ModalContent):
|
||||
text_color = get_tag_text_color(primary_color, highlight_color)
|
||||
|
||||
def update_parent_tag_callback(build_tag_panel: BuildTagPanel):
|
||||
self.lib.update_tag(
|
||||
self._lib.update_tag(
|
||||
build_tag_panel.build_tag(),
|
||||
parent_ids=set(build_tag_panel.parent_ids),
|
||||
aliases=set(build_tag_panel.aliases),
|
||||
@@ -372,10 +373,10 @@ class BuildTagPanel(ModalContent):
|
||||
self.set_parent_tags()
|
||||
|
||||
def on_parent_tag_edit(tag: Tag) -> None:
|
||||
build_tag_panel = BuildTagPanel(self.lib, tag=tag)
|
||||
build_tag_panel = BuildTagPanel(self._lib, tag=tag)
|
||||
edit_modal = Modal(
|
||||
build_tag_panel,
|
||||
self.lib.tag_display_name(tag),
|
||||
self._lib.tag_display_name(tag),
|
||||
"Edit Tag",
|
||||
is_savable=True,
|
||||
)
|
||||
@@ -383,7 +384,7 @@ class BuildTagPanel(ModalContent):
|
||||
edit_modal.show()
|
||||
|
||||
# Add Tag Widget
|
||||
tag_widget = TagWidget(tag, library=self.lib, has_edit=True, has_remove=True)
|
||||
tag_widget = TagWidget(tag, library=self._lib, has_edit=True, has_remove=True)
|
||||
tag_widget.on_remove.connect(lambda t=parent_id: self._remove_parent_tag_callback(t))
|
||||
tag_widget.on_edit.connect(partial(on_parent_tag_edit, tag))
|
||||
|
||||
|
||||
@@ -141,9 +141,6 @@ class TagWidget(QWidget):
|
||||
self.search_for_tag_action = QAction(self)
|
||||
self.search_for_tag_action.setText(Translations["tag.search_for_tag"])
|
||||
self.bg_button.addAction(self.search_for_tag_action)
|
||||
# add_to_search_action = QAction(self)
|
||||
# add_to_search_action.setText(Translations.translate_formatted("tag.add_to_search"))
|
||||
# self.bg_button.addAction(add_to_search_action)
|
||||
|
||||
self.inner_layout = QHBoxLayout()
|
||||
self.inner_layout.setObjectName("innerLayout")
|
||||
|
||||
+24
-16
@@ -62,7 +62,7 @@ from tagstudio.qt.controllers.fix_ignored_modal_controller import FixIgnoredEntr
|
||||
from tagstudio.qt.controllers.ignore_modal_controller import IgnoreModal
|
||||
from tagstudio.qt.controllers.library_info_window_controller import LibraryInfoWindow
|
||||
from tagstudio.qt.controllers.modal import Modal
|
||||
from tagstudio.qt.controllers.tag_search_panel_controller import TagSearchModal
|
||||
from tagstudio.qt.controllers.tag_search_panel_controller import TagSearchPanel
|
||||
from tagstudio.qt.controllers.update_available_message_box import UpdateAvailableMessageBox
|
||||
from tagstudio.qt.global_settings import DEFAULT_GLOBAL_SETTINGS_PATH, GlobalSettings, Theme
|
||||
from tagstudio.qt.mixed.about_modal import AboutModal
|
||||
@@ -83,8 +83,8 @@ from tagstudio.qt.translations import Translations
|
||||
from tagstudio.qt.utils.custom_runnable import CustomRunnable
|
||||
from tagstudio.qt.utils.file_deleter import delete_file
|
||||
from tagstudio.qt.utils.function_iterator import FunctionIterator
|
||||
from tagstudio.qt.views.field_template_search_panel_view import FieldTemplateSearchPanelView
|
||||
from tagstudio.qt.views.main_window import MainWindow
|
||||
from tagstudio.qt.views.search_panel_view import SearchPanelView
|
||||
from tagstudio.qt.views.splash import SplashScreen
|
||||
from tagstudio.qt.views.stylesheets.stylesheets import header
|
||||
|
||||
@@ -364,14 +364,14 @@ class QtDriver(DriverMixin, QObject):
|
||||
if platform.system() != "Windows":
|
||||
self.app.setDesktopFileName("tagstudio")
|
||||
|
||||
# Initialize the Tag Manager panel
|
||||
self.tag_manager = TagSearchModal(
|
||||
self.lib,
|
||||
title=Translations["tag_manager.title"],
|
||||
is_tag_chooser=False,
|
||||
tsp_view = SearchPanelView(
|
||||
placeholder_text=Translations["home.search_tags"],
|
||||
create_text=Translations["tag.create"],
|
||||
is_chooser=False,
|
||||
)
|
||||
self.tag_manager.tsp.set_driver(self)
|
||||
|
||||
tsp = TagSearchPanel(self.lib, is_chooser=False, view=tsp_view)
|
||||
tsp.search_for_tag.connect(self.search_for_tag_callback)
|
||||
self.tag_manager = Modal(tsp, Translations["tag_manager.title"])
|
||||
self.tag_manager.done.connect(
|
||||
lambda checked=False: self.main_window.preview_panel.set_selection(
|
||||
self.selected, update_preview=False
|
||||
@@ -385,8 +385,8 @@ class QtDriver(DriverMixin, QObject):
|
||||
self.field_template_manager = Modal(
|
||||
content_widget=FieldTemplateSearchPanel(
|
||||
self.lib,
|
||||
is_field_template_chooser=False,
|
||||
view=FieldTemplateSearchPanelView(is_field_template_chooser=False),
|
||||
is_chooser=False,
|
||||
view=SearchPanelView(Translations["home.search_field_templates"], is_chooser=False),
|
||||
),
|
||||
title=Translations["field_template_manager.title"],
|
||||
is_savable=False,
|
||||
@@ -397,12 +397,15 @@ class QtDriver(DriverMixin, QObject):
|
||||
)
|
||||
)
|
||||
|
||||
# TODO: Remove/replace this with a suite of persistently visible tagging panels, including
|
||||
# tag search. These will be docked on the left-hand side of the main window.
|
||||
|
||||
# Initialize the "Add Tag" panel
|
||||
self.add_tag_modal = TagSearchModal(
|
||||
self.lib, title=Translations["tag.add.plural"], is_tag_chooser=True
|
||||
)
|
||||
self.add_tag_modal.tsp.set_driver(self)
|
||||
self.add_tag_modal.tsp.item_chosen.connect(
|
||||
add_tag_tsp_view = SearchPanelView(placeholder_text=Translations["home.search_tags"])
|
||||
add_tag_tsp = TagSearchPanel(self.lib, view=add_tag_tsp_view)
|
||||
add_tag_tsp.search_for_tag.connect(self.search_for_tag_callback)
|
||||
self.add_tag_modal = Modal(add_tag_tsp, Translations["tag.add.plural"])
|
||||
add_tag_tsp.item_chosen.connect(
|
||||
lambda chosen_tag: (
|
||||
self.add_tags_to_selected_callback([chosen_tag]),
|
||||
self.main_window.preview_panel.set_selection(self.selected),
|
||||
@@ -890,6 +893,11 @@ class QtDriver(DriverMixin, QObject):
|
||||
)
|
||||
self.modal.show()
|
||||
|
||||
def search_for_tag_callback(self, tag_id: int) -> None:
|
||||
"""Callback to search for a given Tag ID."""
|
||||
self.main_window.search_field.setText(f"tag_id:{tag_id}")
|
||||
self.update_browsing_state(BrowsingState.from_tag_id(tag_id, self.browsing_history.current))
|
||||
|
||||
def select_all_action_callback(self):
|
||||
"""Set the selection to all visible items."""
|
||||
self.select_all()
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
# SPDX-FileCopyrightText: (c) TagStudio Contributors
|
||||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
from typing import override
|
||||
|
||||
from PySide6.QtWidgets import QWidget
|
||||
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.qt.controllers.field_template_widget_controller import FieldTemplateWidget
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.qt.views.search_panel_view import SearchPanelView
|
||||
|
||||
|
||||
class FieldTemplateSearchPanelView(SearchPanelView):
|
||||
def __init__(self, is_field_template_chooser: bool) -> None:
|
||||
super().__init__(is_field_template_chooser)
|
||||
|
||||
self.search_field.setPlaceholderText(Translations["home.search_field_templates"])
|
||||
self.create_button.setText(Translations["field_template.create"])
|
||||
|
||||
@override
|
||||
def get_item_widget(self, index: int, library: Library | None) -> FieldTemplateWidget:
|
||||
"""Gets the item widget at a specific index."""
|
||||
# Create any new item widgets needed up to the given index
|
||||
if self._scroll_layout.count() <= index:
|
||||
while self._scroll_layout.count() <= index:
|
||||
pad_field_template_widget = FieldTemplateWidget()
|
||||
pad_field_template_widget.setHidden(True)
|
||||
self._scroll_layout.addWidget(pad_field_template_widget)
|
||||
|
||||
field_template_widget: QWidget = self._scroll_layout.itemAt(index).widget()
|
||||
assert isinstance(field_template_widget, FieldTemplateWidget)
|
||||
return field_template_widget
|
||||
@@ -40,10 +40,14 @@ class PreviewPanelView(QVBoxLayout):
|
||||
|
||||
# Search/Create Boxes
|
||||
self.field_search_box = FieldSuggestBox(
|
||||
driver, view=SuggestBoxView(placeholder_text=ph_text("home.search_or_create_fields"))
|
||||
driver.lib,
|
||||
driver.settings,
|
||||
SuggestBoxView(placeholder_text=ph_text("home.search_or_create_fields")),
|
||||
)
|
||||
self.tag_search_box = TagSuggestBox(
|
||||
driver, view=SuggestBoxView(placeholder_text=ph_text("home.search_or_create_tags"))
|
||||
driver.lib,
|
||||
driver.settings,
|
||||
SuggestBoxView(placeholder_text=ph_text("home.search_or_create_tags")),
|
||||
)
|
||||
|
||||
self.preview_thumb = PreviewThumb(driver.lib, driver)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# SPDX-FileCopyrightText: (c) TagStudio Contributors
|
||||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from PySide6.QtCore import QSize, Qt
|
||||
from PySide6.QtWidgets import (
|
||||
@@ -16,144 +16,65 @@ from PySide6.QtWidgets import (
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.qt.controllers.modal_content import ModalContent
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.qt.views.stylesheets.stylesheets import list_button_style
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from tagstudio.qt.controllers.search_panel_controller import SearchPanel
|
||||
pass
|
||||
|
||||
|
||||
class SearchPanelView(ModalContent):
|
||||
def __init__(self, is_chooser: bool) -> None:
|
||||
class SearchPanelView(QVBoxLayout):
|
||||
def __init__(
|
||||
self, placeholder_text: str, create_text: str = "", is_chooser: bool = True
|
||||
) -> None:
|
||||
self.is_chooser: bool = is_chooser
|
||||
super().__init__()
|
||||
|
||||
self.__root_layout = QVBoxLayout(self)
|
||||
self.__root_layout.setContentsMargins(6, 0, 6, 0)
|
||||
self.setMinimumSize(300, 400)
|
||||
self.setContentsMargins(6, 0, 6, 0)
|
||||
|
||||
# Limit container
|
||||
self.__limit_container = QWidget()
|
||||
self.__root_layout.addWidget(self.__limit_container)
|
||||
|
||||
self.__limit_layout = QHBoxLayout(self.__limit_container)
|
||||
self.__limit_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.__limit_layout.setSpacing(12)
|
||||
self.__limit_layout.addStretch(1)
|
||||
|
||||
self.__limit_title = QLabel(Translations["home.search.view_limit"])
|
||||
self.__limit_layout.addWidget(self.__limit_title)
|
||||
self.limit_container = QWidget()
|
||||
self.limit_layout = QHBoxLayout(self.limit_container)
|
||||
self.limit_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.limit_layout.setSpacing(12)
|
||||
self.limit_layout.addStretch(1)
|
||||
self.limit_title = QLabel(Translations["home.search.view_limit"])
|
||||
self.limit_layout.addWidget(self.limit_title)
|
||||
self.addWidget(self.limit_container)
|
||||
|
||||
# Limit dropdown
|
||||
self.limit_combobox = QComboBox()
|
||||
self.__limit_layout.addWidget(self.limit_combobox)
|
||||
self.__limit_layout.addStretch(1)
|
||||
|
||||
self.limit_layout.addWidget(self.limit_combobox)
|
||||
self.limit_layout.addStretch(1)
|
||||
self.limit_combobox.setEditable(False)
|
||||
|
||||
# Search field
|
||||
self.search_field = QLineEdit()
|
||||
self.search_field.setPlaceholderText(placeholder_text)
|
||||
self.search_field.setObjectName("search_field")
|
||||
self.__root_layout.addWidget(self.search_field)
|
||||
|
||||
self.search_field.setMinimumSize(QSize(0, 32))
|
||||
self.addWidget(self.search_field)
|
||||
|
||||
# Scroll area
|
||||
self.__scroll_contents = QWidget()
|
||||
|
||||
self._scroll_layout = QVBoxLayout(self.__scroll_contents)
|
||||
self._scroll_layout.setContentsMargins(6, 0, 6, 0)
|
||||
self._scroll_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
|
||||
|
||||
self.__scroll_area = QScrollArea()
|
||||
self.__scroll_area.setWidget(self.__scroll_contents)
|
||||
self.__root_layout.addWidget(self.__scroll_area)
|
||||
|
||||
self.__scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOn)
|
||||
self.__scroll_area.setWidgetResizable(True)
|
||||
self.__scroll_area.setFrameShadow(QFrame.Shadow.Plain)
|
||||
self.__scroll_area.setFrameShape(QFrame.Shape.NoFrame)
|
||||
self.scroll_contents = QWidget()
|
||||
self.scroll_layout = QVBoxLayout(self.scroll_contents)
|
||||
self.scroll_layout.setContentsMargins(6, 0, 6, 0)
|
||||
self.scroll_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
|
||||
self.scroll_area = QScrollArea()
|
||||
self.scroll_area.setWidget(self.scroll_contents)
|
||||
self.scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOn)
|
||||
self.scroll_area.setWidgetResizable(True)
|
||||
self.scroll_area.setFrameShadow(QFrame.Shadow.Plain)
|
||||
self.scroll_area.setFrameShape(QFrame.Shape.NoFrame)
|
||||
self.addWidget(self.scroll_area)
|
||||
|
||||
# Create button
|
||||
self.create_button = QPushButton("")
|
||||
|
||||
self.create_button = QPushButton(create_text)
|
||||
if not self.is_chooser:
|
||||
self.__root_layout.addWidget(self.create_button)
|
||||
self.addWidget(self.create_button)
|
||||
|
||||
# Create and add button
|
||||
self.create_and_add_button_in_layout: bool = False
|
||||
|
||||
self.create_and_add_button = QPushButton()
|
||||
self.create_and_add_button.setFlat(True)
|
||||
self.create_and_add_button.setMinimumSize(22, 22)
|
||||
self.create_and_add_button.setStyleSheet(list_button_style(border_style="dashed"))
|
||||
|
||||
@property
|
||||
def scroll_layout(self) -> QVBoxLayout:
|
||||
return self._scroll_layout
|
||||
|
||||
@property
|
||||
def scroll_area(self) -> QScrollArea:
|
||||
return self.__scroll_area
|
||||
|
||||
def connect_callbacks(self, controller: "SearchPanel[Any]") -> None: # pyright: ignore[reportExplicitAny]
|
||||
self.limit_combobox.currentIndexChanged.connect(controller.on_limit_changed)
|
||||
|
||||
self.search_field.textChanged.connect(controller.on_search_query_changed)
|
||||
self.search_field.returnPressed.connect(
|
||||
lambda: controller.on_search_query_submitted(self.get_search_query())
|
||||
)
|
||||
|
||||
self.create_button.clicked.connect(controller.on_item_create)
|
||||
self.create_and_add_button.clicked.connect(
|
||||
lambda: controller.on_item_create(add_to_entry=True)
|
||||
)
|
||||
|
||||
def set_limit_items(self, limit_items: list[tuple[str, int]]) -> None:
|
||||
# Remove existing limit items
|
||||
for i in reversed(range(self.limit_combobox.count())):
|
||||
self.limit_combobox.removeItem(i)
|
||||
|
||||
# Add new limit items
|
||||
self.limit_combobox.addItems([limit_item[0] for limit_item in limit_items])
|
||||
|
||||
def get_limit_index(self) -> int:
|
||||
return self.limit_combobox.currentIndex()
|
||||
|
||||
def set_limit_index(self, index: int) -> None:
|
||||
self.limit_combobox.setCurrentIndex(index)
|
||||
|
||||
def focus_search_box(self, select_all: bool = False) -> None:
|
||||
self.search_field.setFocus()
|
||||
if select_all:
|
||||
self.search_field.selectAll()
|
||||
|
||||
def get_search_query(self) -> str:
|
||||
return self.search_field.text()
|
||||
|
||||
def clear_search_query(self) -> None:
|
||||
self.search_field.setText("")
|
||||
self.focus_search_box()
|
||||
|
||||
# Item list
|
||||
def scroll_to(self, position: int) -> None:
|
||||
self.__scroll_area.verticalScrollBar().setValue(position)
|
||||
|
||||
def get_item_widget(self, index: int, library: Library | None) -> Any: # pyright: ignore[reportUnusedParameter, reportExplicitAny]
|
||||
raise NotImplementedError()
|
||||
|
||||
def add_create_and_add_button(self) -> None:
|
||||
if self.create_and_add_button_in_layout:
|
||||
return
|
||||
self._scroll_layout.addWidget(self.create_and_add_button)
|
||||
self.create_and_add_button.show()
|
||||
self.create_and_add_button_in_layout = True
|
||||
|
||||
def remove_create_and_add_button(self) -> None:
|
||||
if not self.create_and_add_button_in_layout:
|
||||
return
|
||||
self._scroll_layout.removeWidget(self.create_and_add_button)
|
||||
self.create_and_add_button.hide()
|
||||
self.create_and_add_button_in_layout = False
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
# SPDX-FileCopyrightText: (c) TagStudio Contributors
|
||||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
from typing import override
|
||||
|
||||
from PySide6.QtWidgets import QWidget
|
||||
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.qt.mixed.tag_widget import TagWidget
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.qt.views.search_panel_view import SearchPanelView
|
||||
|
||||
|
||||
class TagSearchPanelView(SearchPanelView):
|
||||
def __init__(self, is_tag_chooser: bool) -> None:
|
||||
super().__init__(is_tag_chooser)
|
||||
|
||||
self.search_field.setPlaceholderText(Translations["home.search_tags"])
|
||||
self.create_button.setText(Translations["tag.create"])
|
||||
|
||||
@override
|
||||
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._scroll_layout.count() <= index:
|
||||
while self._scroll_layout.count() <= index:
|
||||
pad_tag_widget = TagWidget(
|
||||
tag=None, has_edit=True, has_remove=True, library=library
|
||||
)
|
||||
pad_tag_widget.setHidden(True)
|
||||
self._scroll_layout.addWidget(pad_tag_widget)
|
||||
|
||||
tag_widget: QWidget = self._scroll_layout.itemAt(index).widget()
|
||||
assert isinstance(tag_widget, TagWidget)
|
||||
return tag_widget
|
||||
@@ -1,6 +1,7 @@
|
||||
# SPDX-FileCopyrightText: (c) TagStudio Contributors
|
||||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
# pyright: reportPrivateUsage = false
|
||||
|
||||
from collections.abc import Callable
|
||||
|
||||
@@ -22,7 +23,7 @@ def test_build_tag_panel_add_sub_tag_callback(
|
||||
panel: BuildTagPanel = BuildTagPanel(library, child)
|
||||
qtbot.addWidget(panel)
|
||||
|
||||
panel._add_parent_tag_callback(parent.id) # pyright: ignore[reportPrivateUsage]
|
||||
panel._add_parent_tag_callback(parent.id)
|
||||
|
||||
assert len(panel.parent_ids) == 1
|
||||
|
||||
@@ -40,7 +41,7 @@ def test_build_tag_panel_remove_subtag_callback(
|
||||
panel: BuildTagPanel = BuildTagPanel(library, child)
|
||||
qtbot.addWidget(panel)
|
||||
|
||||
panel._remove_parent_tag_callback(parent.id) # pyright: ignore[reportPrivateUsage]
|
||||
panel._remove_parent_tag_callback(parent.id)
|
||||
|
||||
assert len(panel.parent_ids) == 0
|
||||
|
||||
@@ -58,7 +59,7 @@ def test_build_tag_panel_add_alias_callback(
|
||||
panel: BuildTagPanel = BuildTagPanel(library, tag)
|
||||
qtbot.addWidget(panel)
|
||||
|
||||
panel._create_alias_callback() # pyright: ignore[reportPrivateUsage]
|
||||
panel._create_alias_callback()
|
||||
|
||||
assert panel.aliases_table.rowCount() == 1
|
||||
|
||||
|
||||
@@ -8,13 +8,12 @@ from pytestqt.qtbot import QtBot
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.qt.controllers.tag_search_panel_controller import TagSearchPanel
|
||||
from tagstudio.qt.mixed.tag_widget import TagWidget
|
||||
from tagstudio.qt.ts_qt import QtDriver
|
||||
from tagstudio.qt.views.tag_search_panel_view import TagSearchPanelView
|
||||
from tagstudio.qt.views.search_panel_view import SearchPanelView
|
||||
|
||||
|
||||
def test_update_tags(qtbot: QtBot, library: Library):
|
||||
# Given
|
||||
panel = TagSearchPanel(library, view=TagSearchPanelView(is_tag_chooser=True))
|
||||
panel = TagSearchPanel(library, view=SearchPanelView("", is_chooser=True))
|
||||
|
||||
qtbot.addWidget(panel)
|
||||
|
||||
@@ -22,10 +21,9 @@ def test_update_tags(qtbot: QtBot, library: Library):
|
||||
panel.update_items()
|
||||
|
||||
|
||||
def test_tag_widget_actions_replaced_correctly(qtbot: QtBot, qt_driver: QtDriver, library: Library):
|
||||
panel = TagSearchPanel(library, view=TagSearchPanelView(is_tag_chooser=True))
|
||||
def test_tag_widget_actions_replaced_correctly(qtbot: QtBot, library: Library):
|
||||
panel = TagSearchPanel(library, view=SearchPanelView(""))
|
||||
qtbot.addWidget(panel)
|
||||
panel.set_driver(qt_driver)
|
||||
|
||||
# Set the widget
|
||||
tags = library.tags
|
||||
|
||||
Reference in New Issue
Block a user