mirror of
https://github.com/TagStudioDev/TagStudio.git
synced 2026-07-21 04:56:22 +02:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a1f9d34629 | |||
| 9780d68d2c | |||
| 74f77de962 |
@@ -73,8 +73,8 @@ class EditFieldTemplateModal(EditFieldTemplateModalView):
|
||||
|
||||
self.name_field.setStyleSheet(line_edit_style() if is_empty else "")
|
||||
|
||||
if self.panel_save_button is not None:
|
||||
self.panel_save_button.setDisabled(is_empty)
|
||||
if self.save_button is not None:
|
||||
self.save_button.setDisabled(is_empty)
|
||||
|
||||
def __on_type_changed(self, index: int):
|
||||
old_type = self.__field_type
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
|
||||
import typing
|
||||
from typing import override
|
||||
from warnings import catch_warnings
|
||||
|
||||
@@ -14,22 +13,22 @@ from tagstudio.core.library.alchemy.fields import BaseFieldTemplate
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.qt.controllers.edit_field_template_modal import EditFieldTemplateModal
|
||||
from tagstudio.qt.controllers.field_template_widget_controller import FieldTemplateWidget
|
||||
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.panel_modal import PanelModal, PanelWidget
|
||||
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)
|
||||
@@ -38,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:
|
||||
@@ -61,7 +60,7 @@ class FieldSuggestBox(SuggestBox[BaseFieldTemplate]):
|
||||
# since the user needs to decide what type of field it should be before it's created.
|
||||
query: str = self.layout().search_field.text()
|
||||
panel = EditFieldTemplateModal()
|
||||
modal = PanelModal(
|
||||
modal = Modal(
|
||||
panel,
|
||||
Translations["field_template.new"],
|
||||
Translations["field_template.new"],
|
||||
@@ -76,9 +75,7 @@ class FieldSuggestBox(SuggestBox[BaseFieldTemplate]):
|
||||
@override
|
||||
def _on_item_edit(self, item: BaseFieldTemplate) -> None:
|
||||
panel: EditFieldTemplateModal = EditFieldTemplateModal(item)
|
||||
modal: PanelModal = PanelModal(
|
||||
panel, item.name, Translations["field_template.edit"], is_savable=True
|
||||
)
|
||||
modal: Modal = Modal(panel, item.name, Translations["field_template.edit"], is_savable=True)
|
||||
|
||||
modal.saved.connect(lambda: self._edit_item(panel))
|
||||
modal.show()
|
||||
@@ -124,7 +121,7 @@ class FieldSuggestBox(SuggestBox[BaseFieldTemplate]):
|
||||
)
|
||||
|
||||
@override
|
||||
def _create_item_from_modal(self, edit_item_panel: PanelWidget) -> None:
|
||||
def _create_item_from_modal(self, edit_item_panel: ModalContent) -> None:
|
||||
if isinstance(edit_item_panel, EditFieldTemplateModal):
|
||||
template: BaseFieldTemplate = edit_item_panel.build_field_template()
|
||||
self._lib.add_field_template(template)
|
||||
@@ -135,7 +132,7 @@ class FieldSuggestBox(SuggestBox[BaseFieldTemplate]):
|
||||
self._on_search_query_changed(self.layout().search_field.text())
|
||||
|
||||
@override
|
||||
def _edit_item(self, edit_item_panel: PanelWidget) -> None:
|
||||
def _edit_item(self, edit_item_panel: ModalContent) -> None:
|
||||
if not isinstance(edit_item_panel, EditFieldTemplateModal):
|
||||
return
|
||||
|
||||
|
||||
@@ -7,57 +7,40 @@ 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
|
||||
from tagstudio.qt.controllers.edit_field_template_modal import EditFieldTemplateModal
|
||||
from tagstudio.qt.controllers.field_template_widget_controller import FieldTemplateWidget
|
||||
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.panel_modal import PanelModal, PanelWidget
|
||||
from tagstudio.qt.views.search_panel_view import SearchPanelView
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
class FieldTemplateSearchModal(PanelModal):
|
||||
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:
|
||||
@@ -76,7 +59,7 @@ class FieldTemplateSearchPanel(SearchPanel[BaseFieldTemplate]):
|
||||
logger.info("[FieldTemplateSearch] Create and Add Field Template", name=query)
|
||||
|
||||
panel: EditFieldTemplateModal = EditFieldTemplateModal()
|
||||
modal: PanelModal = PanelModal(
|
||||
modal: Modal = Modal(
|
||||
panel,
|
||||
Translations["field_template.new"],
|
||||
Translations["field_template.new"],
|
||||
@@ -93,7 +76,7 @@ class FieldTemplateSearchPanel(SearchPanel[BaseFieldTemplate]):
|
||||
def on_item_edit(self, item: BaseFieldTemplate) -> None:
|
||||
|
||||
panel: EditFieldTemplateModal = EditFieldTemplateModal(item)
|
||||
modal: PanelModal = PanelModal(
|
||||
modal: Modal = Modal(
|
||||
panel,
|
||||
item.name,
|
||||
Translations["field_template.edit"],
|
||||
@@ -105,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(
|
||||
@@ -141,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):
|
||||
@@ -157,7 +140,7 @@ class FieldTemplateSearchPanel(SearchPanel[BaseFieldTemplate]):
|
||||
)
|
||||
|
||||
@override
|
||||
def create_item(self, edit_item_panel: PanelWidget, choose_item: bool = False) -> None:
|
||||
def create_item(self, edit_item_panel: ModalContent, choose_item: bool = False) -> None:
|
||||
|
||||
if isinstance(edit_item_panel, EditFieldTemplateModal):
|
||||
template: BaseFieldTemplate = edit_item_panel.build_field_template()
|
||||
@@ -171,11 +154,25 @@ class FieldTemplateSearchPanel(SearchPanel[BaseFieldTemplate]):
|
||||
self.on_search_query_changed(self.get_search_query())
|
||||
|
||||
@override
|
||||
def edit_item(self, edit_item_panel: PanelWidget) -> None:
|
||||
def edit_item(self, edit_item_panel: ModalContent) -> None:
|
||||
if not isinstance(edit_item_panel, EditFieldTemplateModal):
|
||||
return
|
||||
|
||||
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
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
# SPDX-FileCopyrightText: (c) TagStudio Contributors
|
||||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
|
||||
import contextlib
|
||||
from typing import Any, override
|
||||
|
||||
import structlog
|
||||
from PySide6 import QtGui
|
||||
from PySide6.QtCore import Qt, Signal
|
||||
from PySide6.QtWidgets import QWidget
|
||||
|
||||
from tagstudio.qt.controllers.modal_content import ModalContent
|
||||
from tagstudio.qt.views.modal_view import ModalView
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
class Modal(QWidget):
|
||||
"""A generic modal window widget with common signals and styling."""
|
||||
|
||||
done = Signal()
|
||||
saved = Signal()
|
||||
saved_data = Signal(type(Any))
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
content_widget: ModalContent,
|
||||
title: str = "",
|
||||
window_title: str | None = None,
|
||||
is_savable: bool = False,
|
||||
inline_title: bool = True,
|
||||
):
|
||||
super().__init__()
|
||||
self.setWindowTitle(title if window_title is None else window_title)
|
||||
self.setWindowModality(Qt.WindowModality.ApplicationModal)
|
||||
self.setLayout(
|
||||
ModalView(
|
||||
content_widget=content_widget,
|
||||
title=title,
|
||||
is_savable=is_savable,
|
||||
inline_title=inline_title,
|
||||
)
|
||||
)
|
||||
|
||||
# [Done]
|
||||
# - OR -
|
||||
# [Cancel] [Save]
|
||||
if not is_savable:
|
||||
done_button = self.layout().content_widget.done_button
|
||||
if done_button:
|
||||
done_button.clicked.connect(self.hide)
|
||||
done_button.clicked.connect(self.done.emit)
|
||||
else:
|
||||
cancel_button = self.layout().content_widget.cancel_button
|
||||
if cancel_button:
|
||||
cancel_button.clicked.connect(self.hide)
|
||||
cancel_button.clicked.connect(content_widget.reset)
|
||||
|
||||
save_button = self.layout().content_widget.save_button
|
||||
if save_button:
|
||||
save_button.clicked.connect(self.hide)
|
||||
save_button.clicked.connect(self.saved.emit)
|
||||
save_button.clicked.connect(
|
||||
lambda: self.saved_data.emit(content_widget.saved_data())
|
||||
)
|
||||
|
||||
content_widget.parent_post_init()
|
||||
|
||||
@override
|
||||
def closeEvent(self, event: QtGui.QCloseEvent) -> None:
|
||||
with contextlib.suppress(AttributeError):
|
||||
cancel_button = self.layout().content_widget.cancel_button
|
||||
if cancel_button:
|
||||
cancel_button.click()
|
||||
with contextlib.suppress(AttributeError):
|
||||
done_button = self.layout().content_widget.done_button
|
||||
if done_button:
|
||||
done_button.click()
|
||||
event.accept()
|
||||
|
||||
@override
|
||||
def layout(self) -> ModalView:
|
||||
"""Return the typed layout for this widget."""
|
||||
return super().layout() # pyright: ignore[reportReturnType]
|
||||
@@ -0,0 +1,47 @@
|
||||
# SPDX-FileCopyrightText: (c) TagStudio Contributors
|
||||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
|
||||
from typing import Any, override
|
||||
|
||||
import structlog
|
||||
from PySide6 import QtCore, QtGui
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtWidgets import QPushButton, QWidget
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
class ModalContent(QWidget):
|
||||
"""Base class for widgets that go inside a Modal widget."""
|
||||
|
||||
save_button: QPushButton | None = None
|
||||
cancel_button: QPushButton | None = None
|
||||
done_button: QPushButton | None = None
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def saved_data(self) -> Any: # pyright: ignore[reportExplicitAny]
|
||||
return None
|
||||
|
||||
def reset(self) -> None:
|
||||
pass
|
||||
|
||||
def parent_post_init(self) -> None:
|
||||
pass
|
||||
|
||||
@override
|
||||
def keyPressEvent(self, event: QtGui.QKeyEvent) -> None:
|
||||
if event.key() == QtCore.Qt.Key.Key_Escape:
|
||||
if self.cancel_button:
|
||||
self.cancel_button.click()
|
||||
elif self.done_button:
|
||||
self.done_button.click()
|
||||
elif event.key() == Qt.Key.Key_Return or event.key() == Qt.Key.Key_Enter:
|
||||
if self.save_button:
|
||||
self.save_button.click()
|
||||
elif self.done_button:
|
||||
self.done_button.click()
|
||||
else: # Other key presses
|
||||
super().keyPressEvent(event)
|
||||
@@ -27,11 +27,11 @@ from tagstudio.core.library.alchemy.models import Entry
|
||||
from tagstudio.core.utils.ffmpeg_status import FfmpegStatus, FfprobeStatus
|
||||
from tagstudio.core.utils.types import unwrap
|
||||
from tagstudio.qt.controllers.edit_text_controller import EditText
|
||||
from tagstudio.qt.controllers.modal import Modal
|
||||
from tagstudio.qt.mixed.datetime_picker import DatetimePicker
|
||||
from tagstudio.qt.mixed.field_containers import FieldContainers
|
||||
from tagstudio.qt.mixed.file_attributes import FileAttributeData
|
||||
from tagstudio.qt.translations import FIELD_TYPE_KEYS, Translations
|
||||
from tagstudio.qt.views.panel_modal import PanelModal
|
||||
from tagstudio.qt.views.preview_panel_view import PreviewPanelView
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
@@ -183,7 +183,7 @@ class PreviewPanel(QWidget):
|
||||
field_name_key: str = FIELD_TYPE_KEYS.get(field.class_name, "field_type.unknown")
|
||||
|
||||
if type(field) is TextField:
|
||||
edit_modal = PanelModal(
|
||||
edit_modal = Modal(
|
||||
EditText(field.name, field.value, field.is_multiline),
|
||||
window_title=f"{Translations['field.edit']} ({Translations[field_name_key]})",
|
||||
is_savable=True,
|
||||
@@ -194,7 +194,7 @@ class PreviewPanel(QWidget):
|
||||
)
|
||||
edit_modal.show()
|
||||
elif type(field) is DatetimeField:
|
||||
edit_modal = PanelModal(
|
||||
edit_modal = Modal(
|
||||
DatetimePicker(self._driver, field.name, field.value or dt.now()),
|
||||
window_title=f"{Translations['field.edit']} ({Translations[field_name_key]})",
|
||||
is_savable=True,
|
||||
|
||||
@@ -8,11 +8,11 @@ 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
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.qt.views.panel_modal import PanelWidget
|
||||
from tagstudio.qt.views.search_panel_view import SearchPanelView
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
@@ -40,7 +40,7 @@ def _item_name(item: object) -> str:
|
||||
raise AttributeError()
|
||||
|
||||
|
||||
class SearchPanel[T](PanelWidget):
|
||||
class SearchPanel[T](ModalContent):
|
||||
item_chosen = Signal(int)
|
||||
|
||||
def __init__(
|
||||
@@ -50,18 +50,18 @@ class SearchPanel[T](PanelWidget):
|
||||
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](PanelWidget):
|
||||
("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](PanelWidget):
|
||||
# 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](PanelWidget):
|
||||
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](PanelWidget):
|
||||
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](PanelWidget):
|
||||
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,13 +270,13 @@ class SearchPanel[T](PanelWidget):
|
||||
# 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: PanelWidget, choose_item: bool = False) -> None: # pyright: ignore[reportUnusedParameter]
|
||||
def create_item(self, edit_item_panel: ModalContent, choose_item: bool = False) -> None: # pyright: ignore[reportUnusedParameter]
|
||||
raise NotImplementedError()
|
||||
|
||||
def edit_item(self, edit_item_panel: PanelWidget) -> None: # pyright: ignore[reportUnusedParameter]
|
||||
def edit_item(self, edit_item_panel: ModalContent) -> 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
|
||||
@@ -10,8 +10,9 @@ 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.controllers.modal_content import ModalContent
|
||||
from tagstudio.qt.controllers.underlined_widget import UnderlinedWidget
|
||||
from tagstudio.qt.views.panel_modal import PanelWidget
|
||||
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] = []
|
||||
@@ -197,10 +196,10 @@ class SuggestBox[T](QWidget):
|
||||
self.done.emit()
|
||||
self.hide_and_reset()
|
||||
|
||||
def _create_item_from_modal(self, edit_item_panel: PanelWidget) -> None: # pyright: ignore[reportUnusedParameter]
|
||||
def _create_item_from_modal(self, edit_item_panel: ModalContent) -> None: # pyright: ignore[reportUnusedParameter]
|
||||
raise NotImplementedError()
|
||||
|
||||
def _edit_item(self, edit_item_panel: PanelWidget) -> None: # pyright: ignore[reportUnusedParameter]
|
||||
def _edit_item(self, edit_item_panel: ModalContent) -> None: # pyright: ignore[reportUnusedParameter]
|
||||
raise NotImplementedError()
|
||||
|
||||
@override
|
||||
|
||||
@@ -12,8 +12,8 @@ from tagstudio.core.enums import TagClickActionOption
|
||||
from tagstudio.core.library.alchemy.enums import BrowsingState
|
||||
from tagstudio.core.library.alchemy.models import Tag
|
||||
from tagstudio.core.utils.types import unwrap
|
||||
from tagstudio.qt.controllers.modal import Modal
|
||||
from tagstudio.qt.mixed.build_tag import BuildTagPanel
|
||||
from tagstudio.qt.views.panel_modal import PanelModal
|
||||
from tagstudio.qt.views.tag_box_view import TagBoxWidgetView
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -74,7 +74,7 @@ class TagBoxWidget(TagBoxWidgetView):
|
||||
def _on_edit(self, tag: Tag) -> None:
|
||||
build_tag_panel = BuildTagPanel(self.__driver.lib, tag=tag)
|
||||
|
||||
edit_modal = PanelModal(
|
||||
edit_modal = Modal(
|
||||
build_tag_panel,
|
||||
self.__driver.lib.tag_display_name(tag),
|
||||
"Edit Tag",
|
||||
|
||||
@@ -6,66 +6,44 @@ 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
|
||||
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.panel_modal import PanelModal, PanelWidget
|
||||
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(PanelModal):
|
||||
tsp: "TagSearchPanel"
|
||||
|
||||
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__(
|
||||
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:
|
||||
@@ -80,9 +58,8 @@ 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)
|
||||
modal: PanelModal = PanelModal(
|
||||
panel: BuildTagPanel = BuildTagPanel(self._lib)
|
||||
modal: Modal = Modal(
|
||||
panel,
|
||||
Translations["tag.new"],
|
||||
Translations["tag.add"] if add_to_entry else Translations["tag.new"],
|
||||
@@ -100,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_modal: PanelModal = PanelModal(
|
||||
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,
|
||||
)
|
||||
@@ -112,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):
|
||||
@@ -121,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,
|
||||
)
|
||||
|
||||
@@ -130,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
|
||||
@@ -139,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)
|
||||
|
||||
@@ -152,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
|
||||
)
|
||||
|
||||
@@ -166,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)
|
||||
)
|
||||
@@ -176,25 +153,19 @@ 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: PanelWidget, choose_item: bool = False) -> None:
|
||||
def create_item(self, edit_item_panel: ModalContent, 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
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
@@ -206,25 +177,32 @@ class TagSearchPanel(SearchPanel[Tag]):
|
||||
self.on_search_query_changed(self.get_search_query())
|
||||
|
||||
@override
|
||||
def edit_item(self, edit_item_panel: PanelWidget) -> None:
|
||||
def edit_item(self, edit_item_panel: ModalContent) -> None:
|
||||
# TODO: Move this to a top-level import
|
||||
from tagstudio.qt.mixed.build_tag import BuildTagPanel # here due to circular imports
|
||||
|
||||
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,36 +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.panel_modal import PanelModal, PanelWidget
|
||||
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)
|
||||
@@ -40,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:
|
||||
@@ -67,9 +61,9 @@ 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: PanelModal = PanelModal(
|
||||
modal: Modal = Modal(
|
||||
panel, Translations["tag.new"], Translations["tag.new"], is_savable=True
|
||||
)
|
||||
if query.strip():
|
||||
@@ -86,7 +80,7 @@ class TagSuggestBox(SuggestBox[Tag]):
|
||||
@override
|
||||
def _on_item_edit(self, item: Tag) -> None:
|
||||
edit_tag_panel: BuildTagPanel = BuildTagPanel(self._lib, tag=item)
|
||||
edit_tag_modal: PanelModal = PanelModal(
|
||||
edit_tag_modal: Modal = Modal(
|
||||
edit_tag_panel,
|
||||
self._lib.tag_display_name(item),
|
||||
Translations["tag.edit"],
|
||||
@@ -141,12 +135,12 @@ 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)
|
||||
|
||||
@override
|
||||
def _create_item_from_modal(self, edit_item_panel: PanelWidget) -> None:
|
||||
def _create_item_from_modal(self, edit_item_panel: ModalContent) -> None:
|
||||
if isinstance(edit_item_panel, BuildTagPanel):
|
||||
tag: Tag = edit_item_panel.build_tag()
|
||||
self._lib.add_tag(
|
||||
@@ -159,7 +153,7 @@ class TagSuggestBox(SuggestBox[Tag]):
|
||||
self._on_search_query_changed(self.layout().search_field.text())
|
||||
|
||||
@override
|
||||
def _edit_item(self, edit_item_panel: PanelWidget) -> None:
|
||||
def _edit_item(self, edit_item_panel: ModalContent) -> None:
|
||||
if not isinstance(edit_item_panel, BuildTagPanel):
|
||||
return
|
||||
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
# SPDX-FileCopyrightText: (c) TagStudio Contributors
|
||||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
|
||||
from typing import override
|
||||
|
||||
import structlog
|
||||
from PySide6 import QtCore, QtGui
|
||||
from PySide6.QtCore import Qt, Signal
|
||||
from PySide6.QtWidgets import (
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QListWidget,
|
||||
QListWidgetItem,
|
||||
QPushButton,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.qt.translations import FIELD_TYPE_KEYS, Translations
|
||||
from tagstudio.qt.views.stylesheets.stylesheets import header
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
# NOTE: This class doesn't inherit from PanelWidget? Seems like it predates that system?
|
||||
class AddFieldModal(QWidget):
|
||||
done = Signal(list)
|
||||
|
||||
def __init__(self, library: Library):
|
||||
# [Done]
|
||||
# - OR -
|
||||
# [Cancel] [Save]
|
||||
super().__init__()
|
||||
self.lib = library
|
||||
self.setWindowTitle(Translations["field.add"])
|
||||
self.setWindowModality(Qt.WindowModality.ApplicationModal)
|
||||
self.setMinimumSize(400, 300)
|
||||
self.root_layout = QVBoxLayout(self)
|
||||
self.root_layout.setContentsMargins(6, 6, 6, 6)
|
||||
|
||||
self.title_widget = QLabel(header(Translations["field.add"], 3))
|
||||
self.title_widget.setObjectName("fieldTitle")
|
||||
self.title_widget.setWordWrap(True)
|
||||
self.title_widget.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
self.list_widget = QListWidget()
|
||||
|
||||
self.button_container = QWidget()
|
||||
self.button_layout = QHBoxLayout(self.button_container)
|
||||
self.button_layout.setContentsMargins(6, 6, 6, 6)
|
||||
self.button_layout.addStretch(1)
|
||||
|
||||
self.cancel_button = QPushButton(Translations["generic.cancel"])
|
||||
self.cancel_button.clicked.connect(self.hide)
|
||||
self.button_layout.addWidget(self.cancel_button)
|
||||
|
||||
self.save_button = QPushButton(Translations["generic.add"])
|
||||
self.save_button.setDefault(True)
|
||||
self.save_button.clicked.connect(self.hide)
|
||||
self.save_button.clicked.connect(
|
||||
lambda: (
|
||||
# get userData for each selected item
|
||||
self.done.emit(self.list_widget.selectedItems())
|
||||
)
|
||||
)
|
||||
self.button_layout.addWidget(self.save_button)
|
||||
|
||||
self.root_layout.addWidget(self.title_widget)
|
||||
self.root_layout.addWidget(self.list_widget)
|
||||
self.root_layout.addStretch(1)
|
||||
self.root_layout.addWidget(self.button_container)
|
||||
|
||||
@override
|
||||
def show(self):
|
||||
self.list_widget.clear()
|
||||
for field_template in self.lib.field_templates:
|
||||
field_name_key: str = FIELD_TYPE_KEYS.get(
|
||||
field_template.class_name, "field_type.unknown"
|
||||
)
|
||||
item = QListWidgetItem(f"{field_template.name} ({Translations[field_name_key]})")
|
||||
item.setData(Qt.ItemDataRole.UserRole, field_template)
|
||||
self.list_widget.addItem(item)
|
||||
self.list_widget.setFocus()
|
||||
self.list_widget.setCurrentRow(0)
|
||||
|
||||
super().show()
|
||||
|
||||
@override
|
||||
def keyPressEvent(self, event: QtGui.QKeyEvent) -> None: # noqa N802
|
||||
if event.key() == QtCore.Qt.Key.Key_Escape:
|
||||
self.cancel_button.click()
|
||||
elif event.key() in (QtCore.Qt.Key.Key_Enter, QtCore.Qt.Key.Key_Return):
|
||||
self.save_button.click()
|
||||
else: # Other key presses
|
||||
pass
|
||||
return super().keyPressEvent(event)
|
||||
@@ -24,10 +24,10 @@ from tagstudio.core.library.alchemy.enums import TagColorEnum
|
||||
from tagstudio.core.library.alchemy.library import Library, slugify
|
||||
from tagstudio.core.library.alchemy.models import TagColorGroup
|
||||
from tagstudio.core.utils.types import unwrap
|
||||
from tagstudio.qt.controllers.modal_content import ModalContent
|
||||
from tagstudio.qt.mixed.tag_color_preview import TagColorPreview
|
||||
from tagstudio.qt.models.palette import ColorType, get_tag_color
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.qt.views.panel_modal import PanelWidget
|
||||
from tagstudio.qt.views.stylesheets.stylesheets import (
|
||||
checkbox_style,
|
||||
line_edit_style,
|
||||
@@ -37,7 +37,7 @@ from tagstudio.qt.views.stylesheets.stylesheets import (
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
class BuildColorPanel(PanelWidget):
|
||||
class BuildColorPanel(ModalContent):
|
||||
on_edit = Signal(TagColorGroup)
|
||||
|
||||
def __init__(self, library: Library, color_group: TagColorGroup):
|
||||
@@ -254,8 +254,8 @@ class BuildColorPanel(PanelWidget):
|
||||
self.slug_field.setText(slug)
|
||||
self.update_preview_text()
|
||||
|
||||
if self.panel_save_button is not None:
|
||||
self.panel_save_button.setDisabled(is_name_empty)
|
||||
if self.save_button is not None:
|
||||
self.save_button.setDisabled(is_name_empty)
|
||||
|
||||
def build_color(self) -> tuple[TagColorGroup, TagColorGroup]:
|
||||
name = self.name_field.text()
|
||||
|
||||
@@ -13,14 +13,14 @@ from PySide6.QtWidgets import QLabel, QLineEdit, QVBoxLayout, QWidget
|
||||
from tagstudio.core.constants import RESERVED_NAMESPACE_PREFIX
|
||||
from tagstudio.core.library.alchemy.library import Library, ReservedNamespaceError, slugify
|
||||
from tagstudio.core.library.alchemy.models import Namespace
|
||||
from tagstudio.qt.controllers.modal_content import ModalContent
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.qt.views.panel_modal import PanelWidget
|
||||
from tagstudio.qt.views.stylesheets.stylesheets import line_edit_style
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
class BuildNamespacePanel(PanelWidget):
|
||||
class BuildNamespacePanel(ModalContent):
|
||||
on_edit = Signal(Namespace)
|
||||
|
||||
def __init__(self, library: Library, namespace: Namespace | None = None):
|
||||
@@ -118,8 +118,8 @@ class BuildNamespacePanel(PanelWidget):
|
||||
|
||||
self.slug_field.setText(slug)
|
||||
|
||||
if self.panel_save_button is not None:
|
||||
self.panel_save_button.setDisabled(is_name_empty)
|
||||
if self.save_button is not None:
|
||||
self.save_button.setDisabled(is_name_empty)
|
||||
|
||||
def no_collide(self, slug: str) -> str:
|
||||
"""Return a slug name that's verified not to collide with other known namespace slugs."""
|
||||
|
||||
@@ -28,12 +28,14 @@ from PySide6.QtWidgets import (
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.core.library.alchemy.models import Tag, TagAlias, TagColorGroup
|
||||
from tagstudio.core.utils.types import unwrap
|
||||
from tagstudio.qt.controllers.tag_search_panel_controller import TagSearchModal
|
||||
from tagstudio.qt.controllers.modal import Modal
|
||||
from tagstudio.qt.controllers.modal_content import ModalContent
|
||||
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.panel_modal import PanelModal, PanelWidget
|
||||
from tagstudio.qt.views.search_panel_view import SearchPanelView
|
||||
from tagstudio.qt.views.stylesheets.stylesheets import (
|
||||
checkbox_style,
|
||||
colored_radio_button_style,
|
||||
@@ -73,12 +75,12 @@ class CustomTableItem(QLineEdit):
|
||||
super().keyPressEvent(arg__1)
|
||||
|
||||
|
||||
class BuildTagPanel(PanelWidget):
|
||||
class BuildTagPanel(ModalContent):
|
||||
on_edit = Signal(Tag)
|
||||
|
||||
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
|
||||
@@ -153,7 +155,6 @@ class BuildTagPanel(PanelWidget):
|
||||
|
||||
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)
|
||||
@@ -176,10 +177,11 @@ class BuildTagPanel(PanelWidget):
|
||||
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 ----------------------------------------------------------------
|
||||
@@ -194,14 +196,14 @@ class BuildTagPanel(PanelWidget):
|
||||
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 = PanelModal(
|
||||
self.choose_color_modal = Modal(
|
||||
self.tag_color_selection, chose_tag_color_title, chose_tag_color_title
|
||||
)
|
||||
self.choose_color_modal.done.connect(
|
||||
@@ -325,7 +327,7 @@ class BuildTagPanel(PanelWidget):
|
||||
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
|
||||
@@ -363,7 +365,7 @@ class BuildTagPanel(PanelWidget):
|
||||
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),
|
||||
@@ -371,10 +373,10 @@ class BuildTagPanel(PanelWidget):
|
||||
self.set_parent_tags()
|
||||
|
||||
def on_parent_tag_edit(tag: Tag) -> None:
|
||||
build_tag_panel = BuildTagPanel(self.lib, tag=tag)
|
||||
edit_modal = PanelModal(
|
||||
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,
|
||||
)
|
||||
@@ -382,7 +384,7 @@ class BuildTagPanel(PanelWidget):
|
||||
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))
|
||||
|
||||
@@ -422,7 +424,7 @@ class BuildTagPanel(PanelWidget):
|
||||
while self.aliases_table.rowCount() > 0:
|
||||
self.aliases_table.removeRow(0)
|
||||
|
||||
last: QWidget | None = self.panel_save_button
|
||||
last: QWidget | None = self.save_button
|
||||
aliases = list(self.aliases)
|
||||
alias_names = [a.name for a in aliases]
|
||||
sorted_aliases = sorted(aliases, key=lambda x: alias_names[aliases.index(x)])
|
||||
@@ -493,8 +495,8 @@ class BuildTagPanel(PanelWidget):
|
||||
is_empty = not self.name_field.text().strip()
|
||||
self.name_field.setStyleSheet(line_edit_style() if is_empty else "")
|
||||
|
||||
if self.panel_save_button is not None:
|
||||
self.panel_save_button.setDisabled(is_empty)
|
||||
if self.save_button is not None:
|
||||
self.save_button.setDisabled(is_empty)
|
||||
|
||||
def build_tag(self) -> Tag:
|
||||
tag = self.tag
|
||||
@@ -515,8 +517,8 @@ class BuildTagPanel(PanelWidget):
|
||||
self.setTabOrder(self.shorthand_field, self.aliases_add_button)
|
||||
self.setTabOrder(self.aliases_add_button, self.parent_tags_add_button)
|
||||
self.setTabOrder(self.parent_tags_add_button, self.color_button)
|
||||
self.setTabOrder(self.color_button, unwrap(self.panel_cancel_button))
|
||||
self.setTabOrder(unwrap(self.panel_cancel_button), unwrap(self.panel_save_button))
|
||||
self.setTabOrder(unwrap(self.panel_save_button), self.aliases_table.cellWidget(0, 1))
|
||||
self.setTabOrder(self.color_button, unwrap(self.cancel_button))
|
||||
self.setTabOrder(unwrap(self.cancel_button), unwrap(self.save_button))
|
||||
self.setTabOrder(unwrap(self.save_button), self.aliases_table.cellWidget(0, 1))
|
||||
self.name_field.selectAll()
|
||||
self.name_field.setFocus()
|
||||
|
||||
@@ -12,12 +12,12 @@ from PySide6.QtWidgets import QMessageBox, QPushButton
|
||||
from tagstudio.core.constants import RESERVED_NAMESPACE_PREFIX
|
||||
from tagstudio.core.library.alchemy.models import TagColorGroup
|
||||
from tagstudio.core.utils.types import unwrap
|
||||
from tagstudio.qt.controllers.modal import Modal
|
||||
from tagstudio.qt.mixed.build_color import BuildColorPanel
|
||||
from tagstudio.qt.mixed.field_widget import FieldWidget
|
||||
from tagstudio.qt.mixed.tag_color_label import TagColorLabel
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.qt.views.layouts.flow_layout import FlowLayout
|
||||
from tagstudio.qt.views.panel_modal import PanelModal
|
||||
from tagstudio.qt.views.stylesheets.stylesheets import add_button_style
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
@@ -102,7 +102,7 @@ class ColorBoxWidget(FieldWidget):
|
||||
def edit_color(self, color_group: TagColorGroup):
|
||||
build_color_panel = BuildColorPanel(self.lib, color_group)
|
||||
|
||||
self.edit_modal = PanelModal(
|
||||
self.edit_modal = Modal(
|
||||
build_color_panel,
|
||||
"Edit Color",
|
||||
is_savable=True,
|
||||
|
||||
@@ -9,7 +9,7 @@ from typing import cast, override
|
||||
from PySide6.QtCore import QDateTime
|
||||
from PySide6.QtWidgets import QDateTimeEdit, QLineEdit, QVBoxLayout
|
||||
|
||||
from tagstudio.qt.views.panel_modal import PanelWidget
|
||||
from tagstudio.qt.controllers.modal_content import ModalContent
|
||||
from tagstudio.qt.views.stylesheets.stylesheets import title_line_edit_style
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
@@ -39,7 +39,7 @@ def qdtf2dtf(dtf: str) -> str:
|
||||
return out
|
||||
|
||||
|
||||
class DatetimePicker(PanelWidget):
|
||||
class DatetimePicker(ModalContent):
|
||||
def __init__(self, driver: "QtDriver", name: str, datetime: dt | str):
|
||||
super().__init__()
|
||||
self.setMinimumSize(300, 60)
|
||||
|
||||
@@ -30,12 +30,12 @@ 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.edit_text_controller import EditText
|
||||
from tagstudio.qt.controllers.modal import Modal
|
||||
from tagstudio.qt.controllers.tag_box_controller import TagBoxWidget
|
||||
from tagstudio.qt.mixed.datetime_picker import DatetimePicker
|
||||
from tagstudio.qt.mixed.field_widget import FieldContainer
|
||||
from tagstudio.qt.mixed.text_field import TextContainerWidget
|
||||
from tagstudio.qt.translations import FIELD_TYPE_KEYS, Translations
|
||||
from tagstudio.qt.views.panel_modal import PanelModal
|
||||
from tagstudio.qt.views.stylesheets.stylesheets import inset_container_style
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
@@ -291,7 +291,7 @@ class FieldContainers(QWidget):
|
||||
container.set_inner_widget(inner_widget)
|
||||
|
||||
if not is_mixed:
|
||||
edit_modal = PanelModal(
|
||||
edit_modal = Modal(
|
||||
EditText(field.name, field.value, field.is_multiline),
|
||||
window_title=f"{Translations['field.edit']} ({Translations[field_name_key]})",
|
||||
is_savable=True,
|
||||
@@ -329,7 +329,7 @@ class FieldContainers(QWidget):
|
||||
container.set_inner_widget(inner_widget)
|
||||
|
||||
if not is_mixed:
|
||||
edit_modal = PanelModal(
|
||||
edit_modal = Modal(
|
||||
DatetimePicker(self.driver, field.name, field.value or dt.now()),
|
||||
window_title=f"{Translations['field.edit']} ({Translations[field_name_key]})",
|
||||
is_savable=True,
|
||||
|
||||
@@ -20,6 +20,8 @@ from PySide6.QtWidgets import (
|
||||
)
|
||||
|
||||
from tagstudio.core.enums import ShowFilepathOption, TagClickActionOption
|
||||
from tagstudio.qt.controllers.modal import Modal
|
||||
from tagstudio.qt.controllers.modal_content import ModalContent
|
||||
from tagstudio.qt.global_settings import (
|
||||
DEFAULT_CACHED_THUMB_RES,
|
||||
DEFAULT_THUMB_CACHE_SIZE,
|
||||
@@ -30,7 +32,6 @@ from tagstudio.qt.global_settings import (
|
||||
Theme,
|
||||
)
|
||||
from tagstudio.qt.translations import DEFAULT_TRANSLATION, LANGUAGES, Translations
|
||||
from tagstudio.qt.views.panel_modal import PanelModal, PanelWidget
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from tagstudio.qt.ts_qt import QtDriver
|
||||
@@ -38,7 +39,7 @@ if TYPE_CHECKING:
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
class SettingsPanel(PanelWidget):
|
||||
class SettingsPanel(ModalContent):
|
||||
driver: "QtDriver"
|
||||
|
||||
filepath_option_map: dict[ShowFilepathOption, str] = {
|
||||
@@ -427,15 +428,15 @@ class SettingsPanel(PanelWidget):
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def build_modal(cls, driver: "QtDriver") -> PanelModal:
|
||||
def build_modal(cls, driver: "QtDriver") -> Modal:
|
||||
settings_panel = cls(driver)
|
||||
|
||||
modal = PanelModal(
|
||||
widget=settings_panel,
|
||||
modal = Modal(
|
||||
content_widget=settings_panel,
|
||||
window_title=Translations["settings.title"],
|
||||
is_savable=True,
|
||||
)
|
||||
modal.saved.connect(lambda: settings_panel.update_settings(driver))
|
||||
modal.title_widget.setVisible(False)
|
||||
modal.layout().title_label.setVisible(False)
|
||||
|
||||
return modal
|
||||
|
||||
@@ -23,11 +23,11 @@ from PySide6.QtWidgets import (
|
||||
|
||||
from tagstudio.core.constants import RESERVED_NAMESPACE_PREFIX
|
||||
from tagstudio.core.enums import Theme
|
||||
from tagstudio.qt.controllers.modal import Modal
|
||||
from tagstudio.qt.mixed.build_namespace import BuildNamespacePanel
|
||||
from tagstudio.qt.mixed.color_box import ColorBoxWidget
|
||||
from tagstudio.qt.mixed.field_widget import FieldContainer
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.qt.views.panel_modal import PanelModal
|
||||
from tagstudio.qt.views.stylesheets.stylesheets import header
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
@@ -38,7 +38,7 @@ if TYPE_CHECKING:
|
||||
|
||||
|
||||
class TagColorManager(QWidget):
|
||||
create_namespace_modal: PanelModal | None = None
|
||||
create_namespace_modal: Modal | None = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -174,7 +174,7 @@ class TagColorManager(QWidget):
|
||||
def create_namespace(self):
|
||||
build_namespace_panel = BuildNamespacePanel(self.lib)
|
||||
|
||||
self.create_namespace_modal = PanelModal(
|
||||
self.create_namespace_modal = Modal(
|
||||
build_namespace_panel,
|
||||
Translations["namespace.create.title"],
|
||||
is_savable=True,
|
||||
|
||||
@@ -20,21 +20,22 @@ from PySide6.QtWidgets import (
|
||||
from tagstudio.core.library.alchemy.enums import TagColorEnum
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.core.library.alchemy.models import TagColorGroup
|
||||
from tagstudio.qt.mixed.tag_widget import (
|
||||
get_tag_border_color,
|
||||
get_tag_highlight_color,
|
||||
get_tag_text_color,
|
||||
)
|
||||
from tagstudio.qt.controllers.modal_content import ModalContent
|
||||
from tagstudio.qt.models.palette import ColorType, get_tag_color
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.qt.views.layouts.flow_layout import FlowLayout
|
||||
from tagstudio.qt.views.panel_modal import PanelWidget
|
||||
from tagstudio.qt.views.stylesheets.stylesheets import color_swatch_style, header
|
||||
from tagstudio.qt.views.stylesheets.stylesheets import (
|
||||
color_swatch_style,
|
||||
get_tag_border_color,
|
||||
get_tag_highlight_color,
|
||||
get_tag_text_color,
|
||||
header,
|
||||
)
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
class TagColorSelection(PanelWidget):
|
||||
class TagColorSelection(ModalContent):
|
||||
def __init__(self, library: Library):
|
||||
super().__init__()
|
||||
self.lib = library
|
||||
|
||||
@@ -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")
|
||||
|
||||
+38
-30
@@ -61,7 +61,8 @@ from tagstudio.qt.controllers.field_template_search_panel_controller import Fiel
|
||||
from tagstudio.qt.controllers.fix_ignored_modal_controller import FixIgnoredEntriesModal
|
||||
from tagstudio.qt.controllers.ignore_modal_controller import IgnoreModal
|
||||
from tagstudio.qt.controllers.library_info_window_controller import LibraryInfoWindow
|
||||
from tagstudio.qt.controllers.tag_search_panel_controller import TagSearchModal
|
||||
from tagstudio.qt.controllers.modal import Modal
|
||||
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
|
||||
@@ -82,9 +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.panel_modal import PanelModal
|
||||
from tagstudio.qt.views.search_panel_view import SearchPanelView
|
||||
from tagstudio.qt.views.splash import SplashScreen
|
||||
from tagstudio.qt.views.stylesheets.stylesheets import header
|
||||
|
||||
@@ -167,12 +167,12 @@ class QtDriver(DriverMixin, QObject):
|
||||
favorite_updated = Signal(bool)
|
||||
archived_updated = Signal(bool)
|
||||
|
||||
tag_manager_panel: PanelModal | None = None
|
||||
tag_manager: Modal | None = None
|
||||
color_manager_panel: TagColorManager | None = None
|
||||
field_template_manager_panel: PanelModal | None = None
|
||||
ignore_modal: PanelModal | None = None
|
||||
add_tag_modal: PanelModal | None = None
|
||||
add_field_modal: PanelModal | None = None
|
||||
field_template_manager: Modal | None = None
|
||||
ignore_modal: Modal | None = None
|
||||
add_tag_modal: Modal | None = None
|
||||
add_field_modal: Modal | None = None
|
||||
folders_modal: FoldersToTagsModal
|
||||
about_modal: AboutModal
|
||||
unlinked_modal: FixUnlinkedEntriesModal
|
||||
@@ -364,15 +364,15 @@ class QtDriver(DriverMixin, QObject):
|
||||
if platform.system() != "Windows":
|
||||
self.app.setDesktopFileName("tagstudio")
|
||||
|
||||
# Initialize the Tag Manager panel
|
||||
self.tag_manager_panel = 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_panel.tsp.set_driver(self)
|
||||
|
||||
self.tag_manager_panel.done.connect(
|
||||
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
|
||||
)
|
||||
@@ -382,27 +382,30 @@ class QtDriver(DriverMixin, QObject):
|
||||
self.color_manager_panel = TagColorManager(self)
|
||||
|
||||
# Initialize the Field Template Manager panel
|
||||
self.field_template_manager_panel = PanelModal(
|
||||
widget=FieldTemplateSearchPanel(
|
||||
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,
|
||||
)
|
||||
self.field_template_manager_panel.done.connect(
|
||||
self.field_template_manager.done.connect(
|
||||
lambda checked=False: self.main_window.preview_panel.set_selection(
|
||||
self.selected, update_preview=False
|
||||
)
|
||||
)
|
||||
|
||||
# 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),
|
||||
@@ -486,14 +489,14 @@ class QtDriver(DriverMixin, QObject):
|
||||
lambda f="": self.delete_files_callback(f)
|
||||
)
|
||||
|
||||
self.main_window.menu_bar.tag_manager_action.triggered.connect(self.tag_manager_panel.show)
|
||||
self.main_window.menu_bar.tag_manager_action.triggered.connect(self.tag_manager.show)
|
||||
|
||||
self.main_window.menu_bar.color_manager_action.triggered.connect(
|
||||
self.color_manager_panel.show
|
||||
)
|
||||
|
||||
self.main_window.menu_bar.field_template_manager_action.triggered.connect(
|
||||
self.field_template_manager_panel.show
|
||||
self.field_template_manager.show
|
||||
)
|
||||
|
||||
# endregion
|
||||
@@ -733,7 +736,7 @@ class QtDriver(DriverMixin, QObject):
|
||||
self.ignore_modal = None
|
||||
|
||||
panel = IgnoreModal(self.lib)
|
||||
self.ignore_modal = PanelModal(
|
||||
self.ignore_modal = Modal(
|
||||
panel,
|
||||
Translations["menu.edit.ignore_files"],
|
||||
is_savable=True,
|
||||
@@ -871,7 +874,7 @@ class QtDriver(DriverMixin, QObject):
|
||||
|
||||
def add_tag_action_callback(self):
|
||||
panel = BuildTagPanel(self.lib)
|
||||
self.modal = PanelModal(
|
||||
self.modal = Modal(
|
||||
panel,
|
||||
Translations["tag.new"],
|
||||
Translations["tag.create"],
|
||||
@@ -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()
|
||||
|
||||
@@ -15,14 +15,14 @@ from PySide6.QtWidgets import (
|
||||
)
|
||||
|
||||
from tagstudio.qt.controllers.clickable_label import ClickableLabel
|
||||
from tagstudio.qt.controllers.modal_content import ModalContent
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.qt.views.panel_modal import PanelWidget
|
||||
from tagstudio.qt.views.stylesheets.stylesheets import checkbox_style
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
class EditFieldTemplateModalView(PanelWidget):
|
||||
class EditFieldTemplateModalView(ModalContent):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
|
||||
|
||||
@@ -14,12 +14,12 @@ from PySide6.QtWidgets import (
|
||||
)
|
||||
|
||||
from tagstudio.qt.controllers.clickable_label import ClickableLabel
|
||||
from tagstudio.qt.controllers.modal_content import ModalContent
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.qt.views.panel_modal import PanelWidget
|
||||
from tagstudio.qt.views.stylesheets.stylesheets import checkbox_style, title_line_edit_style
|
||||
|
||||
|
||||
class EditTextView(PanelWidget):
|
||||
class EditTextView(ModalContent):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.setMinimumSize(480, 240)
|
||||
|
||||
@@ -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
|
||||
@@ -14,13 +14,13 @@ from PySide6.QtWidgets import (
|
||||
from tagstudio.core.constants import IGNORE_NAME
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.core.library.alchemy.models import Tag
|
||||
from tagstudio.qt.controllers.modal_content import ModalContent
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.qt.views.panel_modal import PanelWidget
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
class IgnoreModalView(PanelWidget):
|
||||
class IgnoreModalView(ModalContent):
|
||||
on_edit = Signal(Tag)
|
||||
|
||||
def __init__(self, library: Library) -> None:
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
# SPDX-FileCopyrightText: (c) TagStudio Contributors
|
||||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
|
||||
import structlog
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtWidgets import QHBoxLayout, QLabel, QPushButton, QVBoxLayout, QWidget
|
||||
|
||||
from tagstudio.qt.controllers.modal_content import ModalContent
|
||||
from tagstudio.qt.translations import Translations
|
||||
from tagstudio.qt.views.stylesheets.stylesheets import header
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
class ModalView(QVBoxLayout):
|
||||
"""A generic reusable modal panel widget."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
content_widget: ModalContent,
|
||||
title: str = "",
|
||||
is_savable: bool = False,
|
||||
inline_title: bool = True,
|
||||
):
|
||||
super().__init__()
|
||||
self.content_widget = content_widget
|
||||
self.setContentsMargins(6, 6 if inline_title else 12, 6, 6)
|
||||
|
||||
self.button_container = QWidget()
|
||||
self.button_layout = QHBoxLayout(self.button_container)
|
||||
self.button_layout.setContentsMargins(6, 6, 6, 6)
|
||||
self.button_layout.addStretch(1)
|
||||
|
||||
# [Done]
|
||||
# - OR -
|
||||
# [Cancel] [Save]
|
||||
if not is_savable:
|
||||
done_button = QPushButton(Translations["generic.done"])
|
||||
done_button.setAutoDefault(True)
|
||||
self.content_widget.done_button = done_button
|
||||
self.button_layout.addWidget(done_button)
|
||||
else:
|
||||
cancel_button = QPushButton(Translations["generic.cancel"])
|
||||
self.content_widget.cancel_button = cancel_button
|
||||
self.button_layout.addWidget(cancel_button)
|
||||
|
||||
save_button = QPushButton(Translations["generic.save"])
|
||||
save_button.setAutoDefault(True)
|
||||
self.content_widget.save_button = save_button
|
||||
self.button_layout.addWidget(save_button)
|
||||
|
||||
if inline_title:
|
||||
self.title_label = QLabel()
|
||||
self.title_label.setObjectName("fieldTitle")
|
||||
self.title_label.setWordWrap(True)
|
||||
self.title_label.setText(header(title, 3))
|
||||
self.title_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.addWidget(self.title_label)
|
||||
|
||||
self.addWidget(content_widget)
|
||||
self.setStretch(1, 2)
|
||||
self.addWidget(self.button_container)
|
||||
content_widget.parent_post_init()
|
||||
@@ -1,127 +0,0 @@
|
||||
# SPDX-FileCopyrightText: (c) TagStudio Contributors
|
||||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
|
||||
import contextlib
|
||||
from typing import Any, override
|
||||
|
||||
import structlog
|
||||
from PySide6 import QtCore, QtGui
|
||||
from PySide6.QtCore import Qt, Signal
|
||||
from PySide6.QtWidgets import QHBoxLayout, QLabel, QPushButton, QVBoxLayout, QWidget
|
||||
|
||||
from tagstudio.qt.translations import Translations
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
class PanelModal(QWidget):
|
||||
"""A generic reusable modal panel widget."""
|
||||
|
||||
done = Signal()
|
||||
saved = Signal()
|
||||
saved_data = Signal(type(Any))
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
widget: "PanelWidget",
|
||||
title: str = "",
|
||||
window_title: str | None = None,
|
||||
is_savable: bool = False,
|
||||
inline_title: bool = True,
|
||||
):
|
||||
# [Done]
|
||||
# - OR -
|
||||
# [Cancel] [Save]
|
||||
super().__init__()
|
||||
self.widget = widget
|
||||
self.setWindowTitle(title if window_title is None else window_title)
|
||||
self.setWindowModality(Qt.WindowModality.ApplicationModal)
|
||||
self.root_layout = QVBoxLayout(self)
|
||||
self.root_layout.setContentsMargins(6, 0 if inline_title else 12, 6, 6)
|
||||
|
||||
self.button_container = QWidget()
|
||||
self.button_layout = QHBoxLayout(self.button_container)
|
||||
self.button_layout.setContentsMargins(6, 6, 6, 6)
|
||||
self.button_layout.addStretch(1)
|
||||
|
||||
if not is_savable:
|
||||
self.done_button = QPushButton(Translations["generic.done"])
|
||||
self.done_button.setAutoDefault(True)
|
||||
self.done_button.clicked.connect(self.hide)
|
||||
self.done_button.clicked.connect(self.done.emit)
|
||||
self.widget.panel_done_button = self.done_button
|
||||
self.button_layout.addWidget(self.done_button)
|
||||
else:
|
||||
self.cancel_button = QPushButton(Translations["generic.cancel"])
|
||||
self.cancel_button.clicked.connect(self.hide)
|
||||
self.cancel_button.clicked.connect(widget.reset)
|
||||
self.widget.panel_cancel_button = self.cancel_button
|
||||
self.button_layout.addWidget(self.cancel_button)
|
||||
|
||||
self.save_button = QPushButton(Translations["generic.save"])
|
||||
self.save_button.setAutoDefault(True)
|
||||
self.save_button.clicked.connect(self.hide)
|
||||
self.save_button.clicked.connect(self.saved.emit)
|
||||
self.save_button.clicked.connect(lambda: self.saved_data.emit(widget.saved_data()))
|
||||
self.widget.panel_save_button = self.save_button
|
||||
self.button_layout.addWidget(self.save_button)
|
||||
|
||||
if inline_title:
|
||||
self.title_widget = QLabel()
|
||||
self.title_widget.setObjectName("fieldTitle")
|
||||
self.title_widget.setWordWrap(True)
|
||||
self.title_widget.setStyleSheet("font-weight:bold;font-size:14px;padding-top:6px")
|
||||
self.title_widget.setText(title)
|
||||
self.title_widget.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.root_layout.addWidget(self.title_widget)
|
||||
|
||||
self.root_layout.addWidget(widget)
|
||||
widget.parent_modal = self
|
||||
self.root_layout.setStretch(1, 2)
|
||||
self.root_layout.addWidget(self.button_container)
|
||||
widget.parent_post_init()
|
||||
|
||||
@override
|
||||
def closeEvent(self, event: QtGui.QCloseEvent) -> None:
|
||||
with contextlib.suppress(AttributeError):
|
||||
self.cancel_button.click()
|
||||
with contextlib.suppress(AttributeError):
|
||||
self.done_button.click()
|
||||
event.accept()
|
||||
|
||||
|
||||
class PanelWidget(QWidget):
|
||||
"""Used for widgets that go in a modal panel, ex. for editing or searching."""
|
||||
|
||||
parent_modal: PanelModal | None = None
|
||||
panel_save_button: QPushButton | None = None
|
||||
panel_cancel_button: QPushButton | None = None
|
||||
panel_done_button: QPushButton | None = None
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def saved_data(self) -> Any: # pyright: ignore[reportExplicitAny]
|
||||
return None
|
||||
|
||||
def reset(self) -> None:
|
||||
pass
|
||||
|
||||
def parent_post_init(self) -> None:
|
||||
pass
|
||||
|
||||
@override
|
||||
def keyPressEvent(self, event: QtGui.QKeyEvent) -> None:
|
||||
if event.key() == QtCore.Qt.Key.Key_Escape:
|
||||
if self.panel_cancel_button:
|
||||
self.panel_cancel_button.click()
|
||||
elif self.panel_done_button:
|
||||
self.panel_done_button.click()
|
||||
elif event.key() == Qt.Key.Key_Return or event.key() == Qt.Key.Key_Enter:
|
||||
if self.panel_save_button:
|
||||
self.panel_save_button.click()
|
||||
elif self.panel_done_button:
|
||||
self.panel_done_button.click()
|
||||
else: # Other key presses
|
||||
super().keyPressEvent(event)
|
||||
@@ -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.translations import Translations
|
||||
from tagstudio.qt.views.panel_modal import PanelWidget
|
||||
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(PanelWidget):
|
||||
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
|
||||
|
||||
@@ -4,14 +4,7 @@
|
||||
|
||||
import structlog
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtWidgets import (
|
||||
QFrame,
|
||||
QHBoxLayout,
|
||||
QScrollArea,
|
||||
QSizePolicy,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
from PySide6.QtWidgets import QFrame, QHBoxLayout, QScrollArea, QSizePolicy, QVBoxLayout, QWidget
|
||||
|
||||
from tagstudio.qt.controllers.autofill_line_edit import AutofillLineEdit
|
||||
from tagstudio.qt.views.stylesheets.stylesheets import (
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -22,8 +22,9 @@ def test_add_tag_callback(qt_driver: QtDriver):
|
||||
qt_driver.add_tag_action_callback()
|
||||
|
||||
# When
|
||||
assert isinstance(qt_driver.modal.widget, BuildTagPanel)
|
||||
qt_driver.modal.widget.name_field.setText("xxx")
|
||||
build_tag_panel = qt_driver.modal.layout().content_widget
|
||||
assert isinstance(build_tag_panel, BuildTagPanel)
|
||||
build_tag_panel.name_field.setText("xxx")
|
||||
# qt_driver.modal.widget.color_field.setCurrentIndex(1)
|
||||
qt_driver.modal.saved.emit()
|
||||
|
||||
|
||||
@@ -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