diff --git a/src/tagstudio/qt/controllers/field_suggest_box.py b/src/tagstudio/qt/controllers/field_suggest_box.py new file mode 100644 index 00000000..d1cd7ce7 --- /dev/null +++ b/src/tagstudio/qt/controllers/field_suggest_box.py @@ -0,0 +1,153 @@ +# SPDX-FileCopyrightText: (c) TagStudio Contributors +# SPDX-License-Identifier: GPL-3.0-only + + +import typing +from typing import override +from warnings import catch_warnings + +import structlog +from PySide6.QtGui import QAction, Qt +from PySide6.QtWidgets import 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.suggest_box import SuggestBox +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 + + # Context Menu Actions + edit_field_on_add_action = QAction(Translations["settings.edit_field_on_add"], self) + edit_field_on_add_action.setCheckable(True) + self.setContextMenuPolicy(Qt.ContextMenuPolicy.ActionsContextMenu) + 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.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() + + @override + def _on_item_create(self) -> None: + """Opens panel to create a new field template and optionally add it to an entry. + + Populates name field using current search query. + + Args: + add_to_entry (bool): Should this item be added to currently selected entries? + """ + # NOTE: Unlike tags, creating new field templates will ALWAYS spawn an edit window + # 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( + panel, + Translations["field_template.new"], + Translations["field_template.new"], + is_savable=True, + ) + if query.strip(): + panel.name_field.setText(query) + + modal.saved.connect(lambda: self._create_item_from_modal(panel)) + modal.show() + + @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.saved.connect(lambda: self._edit_item(panel)) + modal.show() + + @override + def _on_item_chosen(self, item: BaseFieldTemplate) -> None: + self.item_chosen.emit(item) + self.done.emit() + + @override + def _search_items(self, query: str) -> tuple[list[BaseFieldTemplate], list[BaseFieldTemplate]]: + if query != "": + return self._lib.search_field_templates(name=query, limit=0), [] + else: + return ([], []) + + @override + def _set_item_widget(self, item: BaseFieldTemplate | None, index: int) -> None: + """Set the field template of a field template widget at a specific index.""" + field_template_widget: FieldTemplateWidget = self._get_item_widget(index, self._lib) + field_template_widget.has_remove = False + field_template_widget.set_field_template(item) + field_template_widget.setHidden(item is None) + + if item is None: + return + + # Disconnect previous callbacks + with catch_warnings(record=True): + field_template_widget.on_edit.disconnect() + field_template_widget.on_remove.disconnect() + field_template_widget.on_click.disconnect() + + # Connect callbacks + field_template_widget.on_edit.connect(lambda item_=item: self._on_item_edit(item_)) + field_template_widget.on_click.connect( + lambda checked=False, item_=item: self._on_item_chosen(item_) + ) + + @override + def _create_item_from_modal(self, edit_item_panel: PanelWidget) -> None: + if isinstance(edit_item_panel, EditFieldTemplateModal): + template: BaseFieldTemplate = edit_item_panel.build_field_template() + self._lib.add_field_template(template) + self._on_item_chosen(template) + self._clear_search_query() + + edit_item_panel.hide() + self._on_search_query_changed(self._layout.search_field.text()) + + @override + def _edit_item(self, edit_item_panel: PanelWidget) -> 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._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.content_layout.count() <= index: + while self._layout.content_layout.count() <= index: + pad_field_template_widget = FieldTemplateWidget() + pad_field_template_widget.setHidden(True) + self._layout.content_layout.addWidget(pad_field_template_widget) + + field_template_widget: QWidget = self._layout.content_layout.itemAt(index).widget() + assert isinstance(field_template_widget, FieldTemplateWidget) + return field_template_widget diff --git a/src/tagstudio/qt/controllers/preview_panel_controller.py b/src/tagstudio/qt/controllers/preview_panel_controller.py index 8ce08d2c..57f9a2f0 100644 --- a/src/tagstudio/qt/controllers/preview_panel_controller.py +++ b/src/tagstudio/qt/controllers/preview_panel_controller.py @@ -3,6 +3,9 @@ import typing +from datetime import datetime as dt +from enum import IntEnum +from functools import partial from pathlib import Path from warnings import catch_warnings @@ -11,12 +14,23 @@ from PySide6 import QtCore from PySide6.QtGui import QShortcut from PySide6.QtWidgets import QWidget -from tagstudio.core.library.alchemy.fields import BaseFieldTemplate +from tagstudio.core.library.alchemy.fields import ( + BaseField, + BaseFieldTemplate, + DatetimeField, + DatetimeFieldTemplate, + TextField, + TextFieldTemplate, +) 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.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: @@ -25,6 +39,11 @@ if typing.TYPE_CHECKING: logger = structlog.get_logger(__name__) +class _ItemMode(IntEnum): + TAG = 1 + FIELD = 2 + + class PreviewPanel(QWidget): def __init__(self, driver: "QtDriver") -> None: super().__init__() @@ -41,36 +60,60 @@ class PreviewPanel(QWidget): self._add_tag_action = QShortcut(key, self) self.setLayout(self._layout) + self._set_item_mode(None) self._connect_callbacks() def _connect_callbacks(self) -> None: - self._layout.add_field_button.clicked.connect(self._add_field_button_callback) - self._layout.add_tag_button.clicked.connect(self._add_tag_button_callback) - self._layout.preview_thumb.stats_updated.connect(self._thumb_stats_updated_callback) + # Tag Search + self._layout.add_tag_button.clicked.connect(lambda: self._set_item_mode(_ItemMode.TAG)) self._add_tag_action.activated.connect(self._layout.add_tag_button.setFocus) self._add_tag_action.activated.connect(self._layout.add_tag_button.click) - self._layout.tag_search_box.done.connect(self.tag_added_callback) - self._layout.tag_search_box.tags_updated.connect(self._update_added_callback) + self._layout.tag_search_box.done.connect(self._tag_added_callback) + self._layout.tag_search_box.items_updated.connect(self._update_added_callback) + + # Field Search + self._layout.add_field_button.clicked.connect(lambda: self._set_item_mode(_ItemMode.FIELD)) + self._layout.field_search_box.done.connect(self._field_added_callback) + + # Previews + self._layout.preview_thumb.stats_updated.connect(self._thumb_stats_updated_callback) self._layout.preview_thumb.check_ffmpeg.connect(self._toggle_ffmpeg_warning) - def _add_field_button_callback(self) -> None: - # self.__add_field_modal.show() - pass + def _set_item_mode(self, mode: _ItemMode | None): + def hide_and_disable_buttons(): + self._layout.add_tag_button.setHidden(True) + self._layout.add_tag_button.setEnabled(False) + self._layout.add_field_button.setHidden(True) + self._layout.add_field_button.setEnabled(False) - def _add_tag_button_callback(self) -> None: - self._layout.tag_search_box.added = self._layout.containers.tags - self._layout.tag_search_box.layout().search_field.setDisabled(False) - self._layout.tag_search_box.setHidden(False) - self._layout.add_tag_button.setHidden(True) - self._layout.add_field_button.setHidden(True) + def restore_buttons(): + self._layout.add_tag_button.setHidden(False) + self._layout.add_tag_button.setEnabled(True) + self._layout.add_field_button.setHidden(False) + self._layout.add_field_button.setEnabled(True) - def tag_added_callback(self): - self._layout.tag_search_box.setHidden(True) - self._layout.add_tag_button.setHidden(False) - self._layout.add_field_button.setHidden(False) + if mode == _ItemMode.TAG: + self._layout.field_search_box.hide_and_reset() + self._layout.tag_search_box.added = self._layout.containers.tags + self._layout.tag_search_box.setHidden(False) + hide_and_disable_buttons() + elif mode == _ItemMode.FIELD: + self._layout.tag_search_box.hide_and_reset() + self._layout.field_search_box.setHidden(False) + hide_and_disable_buttons() + else: + self._layout.tag_search_box.hide_and_reset() + self._layout.field_search_box.hide_and_reset() + restore_buttons() + def _tag_added_callback(self): + self._set_item_mode(None) self._layout.add_tag_button.setFocus() + def _field_added_callback(self): + self._set_item_mode(None) + self._layout.add_field_button.setFocus() + def _update_added_callback(self): self._layout.tag_search_box.added = self._layout.containers.tags @@ -95,17 +138,56 @@ class PreviewPanel(QWidget): def _set_selection_callback(self) -> None: with catch_warnings(record=True): - self._layout.field_search_box.field_template_chosen.disconnect() + self._layout.field_search_box.item_chosen.disconnect() self._layout.tag_search_box.item_chosen.disconnect() - self._layout.field_search_box.field_template_chosen.connect(self._add_field_to_selected) + self._layout.field_search_box.item_chosen.connect(self._add_field_to_selected) self._layout.tag_search_box.item_chosen.connect(self._add_tag_to_selected) def _add_field_to_selected(self, template: BaseFieldTemplate) -> None: self._layout.containers.add_field_to_selected(template) + # TODO: Allow editing of fields across multiple entries at once. if len(self._selected) == 1: + if self._driver.settings.edit_field_on_add: + entry = unwrap(self._lib.get_entry_full(self._selected[0])) + entry_field = None + if isinstance(template, TextFieldTemplate): + entry_field = entry.text_fields[-1] + elif isinstance(template, DatetimeFieldTemplate): + entry_field = entry.datetime_fields[-1] + if entry_field is not None: + self._edit_field(entry.id, entry_field) + self._layout.containers.update_from_entry(self._selected[0]) + def _edit_field(self, entry_id: int, field: BaseField) -> None: + # TODO: A lot of this code is similar to or straight up shared with FieldContainers. + # It's possible to reuse it later, after a FieldContainers refactor. + field_name_key: str = FIELD_TYPE_KEYS.get(field.class_name, "field_type.unknown") + + if type(field) is TextField: + edit_modal = PanelModal( + EditText(field.name, field.value, field.is_multiline), + window_title=f"{Translations['field.edit']} ({Translations[field_name_key]})", + is_savable=True, + inline_title=False, + ) + edit_modal.saved_data.connect( + partial(self._layout.containers.update_text_field_callback, field, entry_id) + ) + edit_modal.show() + elif type(field) is DatetimeField: + edit_modal = PanelModal( + DatetimePicker(self._driver, field.name, field.value or dt.now()), + window_title=f"{Translations['field.edit']} ({Translations[field_name_key]})", + is_savable=True, + inline_title=False, + ) + edit_modal.saved_data.connect( + partial(self._layout.containers.update_datetime_field_callback, field, entry_id) + ) + edit_modal.show() + def _add_tag_to_selected(self, tag_id: int) -> None: self._layout.containers.add_tags_to_selected(tag_id) if len(self._selected) == 1: @@ -127,6 +209,7 @@ class PreviewPanel(QWidget): (Only works with one or more items selected) """ self._selected = selected + self._set_item_mode(None) try: # No Items Selected if len(selected) == 0: @@ -135,12 +218,8 @@ class PreviewPanel(QWidget): self._layout.file_attrs.update_stats() self._layout.file_attrs.update_date_label() self._layout.containers.hide_containers() - self._layout.add_tag_button.setEnabled(False) self._layout.add_field_button.setEnabled(False) - self._layout.add_tag_button.setHidden(False) - self._layout.add_field_button.setHidden(False) - self._layout.tag_search_box.hide_and_reset() # One Item Selected elif len(selected) == 1: @@ -157,15 +236,8 @@ class PreviewPanel(QWidget): self._layout.file_attrs.update_stats(filepath, stats) self._layout.file_attrs.update_date_label(filepath) self._layout.containers.update_from_entry(entry_id) - self._set_selection_callback() - self._layout.add_tag_button.setEnabled(True) - self._layout.add_field_button.setEnabled(True) - self._layout.add_tag_button.setHidden(False) - self._layout.add_field_button.setHidden(False) - self._layout.tag_search_box.hide_and_reset() - # Multiple Selected Items elif len(selected) > 1: # items: list[Entry] = [self.lib.get_entry_full(x) for x in self.driver.selected] @@ -174,15 +246,8 @@ class PreviewPanel(QWidget): self._layout.file_attrs.update_multi_selection(len(selected)) self._layout.file_attrs.update_date_label() self._layout.containers.hide_containers() # TODO: Allow for mixed editing - self._set_selection_callback() - self._layout.add_tag_button.setEnabled(True) - self._layout.add_field_button.setEnabled(True) - self._layout.add_tag_button.setHidden(False) - self._layout.add_field_button.setHidden(False) - self._layout.tag_search_box.hide_and_reset() - except Exception as e: logger.error("[Preview Panel] Error updating selection", error=e) diff --git a/src/tagstudio/qt/controllers/suggest_box.py b/src/tagstudio/qt/controllers/suggest_box.py index 15389ed0..73ca62f8 100644 --- a/src/tagstudio/qt/controllers/suggest_box.py +++ b/src/tagstudio/qt/controllers/suggest_box.py @@ -19,7 +19,6 @@ from tagstudio.qt.views.suggest_box_view import SuggestBoxView logger = structlog.get_logger(__name__) -# Only import for type checking/autocompletion, will not be imported at runtime. if TYPE_CHECKING: from tagstudio.qt.ts_qt import QtDriver @@ -43,9 +42,9 @@ def _item_name(item: object) -> str: class SuggestBox[T](QWidget): - item_chosen = Signal(int) + item_chosen = Signal(object) done = Signal() - tags_updated = Signal() + items_updated = Signal() def __init__(self, driver: "QtDriver", view: SuggestBoxView) -> None: super().__init__() @@ -60,14 +59,19 @@ class SuggestBox[T](QWidget): self.setLayout(self._layout) self._connect_callbacks() + def hide_and_reset(self): + self.hide() + self._layout.search_field.setDisabled(True) + self._on_shift_held(held=False) + def _connect_callbacks(self) -> None: - self._layout.search_field.textChanged.connect(self.on_search_query_changed) - self._layout.search_field.editingFinished.connect(self.test_editing_finished) + self._layout.search_field.textChanged.connect(self._on_search_query_changed) + self._layout.search_field.editingFinished.connect(self._editing_finished_callback) self._layout.search_field.return_pressed.connect( - lambda: self.on_search_query_submitted(self._layout.search_field.text()) + lambda: self._on_search_query_submitted(self._layout.search_field.text()) ) self._layout.search_field.shift_return_pressed.connect( - lambda: self.on_search_query_submitted( + lambda: self._on_search_query_submitted( self._layout.search_field.text(), always_create=True ) ) @@ -86,16 +90,16 @@ class SuggestBox[T](QWidget): if self._layout.content_layout.count() > 0: self._layout.content_layout.itemAt(0).widget().setGraphicsEffect(None) # pyright: ignore[reportArgumentType] - def clear_search_query(self) -> None: + def _clear_search_query(self) -> None: self._layout.search_field.setText("") - def get_item_widget(self, index: int, library: Library) -> Any: # pyright: ignore[reportExplicitAny] - return self.get_item_widget(index, library) + def _get_item_widget(self, index: int, library: Library) -> Any: # pyright: ignore + raise NotImplementedError() - def on_search_query_changed(self, query: str) -> None: - self.update_items(query) + def _on_search_query_changed(self, query: str) -> None: + self._update_items(query) - def on_search_query_submitted(self, query: str, always_create: bool = False) -> None: + def _on_search_query_submitted(self, query: str, always_create: bool = False) -> None: # Focus search field if no query logger.info("Query submitted") if not query: @@ -107,17 +111,17 @@ class SuggestBox[T](QWidget): # Create and add item if no search results if (len(self._search_results) <= 0) or always_create: - self.on_item_create() + self._on_item_create() else: self._on_item_chosen(self._search_results[0]) - self.clear_search_query() - self.update_items() + self._clear_search_query() + self._update_items() - def on_item_create(self) -> None: + def _on_item_create(self) -> None: raise NotImplementedError() - def on_item_edit(self, item: T) -> None: # pyright: ignore[reportUnusedParameter] + def _on_item_edit(self, item: T) -> None: # pyright: ignore[reportUnusedParameter] raise NotImplementedError() def _on_item_chosen(self, item: T) -> None: # pyright: ignore[reportUnusedParameter] @@ -126,13 +130,13 @@ class SuggestBox[T](QWidget): def _is_excluded(self, item: T) -> bool: return _item_id(item) in self.excluded - def update_items(self, query: str | None = None) -> None: + def _update_items(self, query: str | None = None) -> None: """Update the item list given a search query.""" logger.info("[SearchPanel] Updating items", limit=self._limit) # Get results for the search query query_lower = "" if not query else query.lower() - search_results: tuple[list[T], list[T]] = self.search_items(query_lower) + search_results: tuple[list[T], list[T]] = self._search_items(query_lower) # Sort and prioritize the results direct_results = list({item for item in search_results[0] if not self._is_excluded(item)}) @@ -168,7 +172,7 @@ class SuggestBox[T](QWidget): for i in range(0, self._limit): item: T | None = all_results[i] if i < len(all_results) else None - self.set_item_widget(item=item, index=i) + self._set_item_widget(item=item, index=i) if self._layout.content_layout.isEmpty(): self._layout.scroll_area.setHidden(True) @@ -179,35 +183,30 @@ class SuggestBox[T](QWidget): self._layout.content_layout.setContentsMargins(6, 6, 6, 6) self._layout.search_field.setStyleSheet(autofill_line_edit_top_style()) - def search_items(self, query: str) -> tuple[list[T], list[T]]: # pyright: ignore[reportUnusedParameter] + def _search_items(self, query: str) -> tuple[list[T], list[T]]: # pyright: ignore[reportUnusedParameter] raise NotImplementedError() - def set_item_widget(self, item: T | None, index: int) -> None: # pyright: ignore[reportUnusedParameter] + def _set_item_widget(self, item: T | None, index: int) -> None: # pyright: ignore[reportUnusedParameter] raise NotImplementedError() - def test_editing_finished(self): - logger.info("Editing finished") - self.tags_updated.emit() + def _editing_finished_callback(self): + self.items_updated.emit() if self._layout.search_field.text() == "": self.done.emit() self.hide_and_reset() - def hide_and_reset(self): - self.hide() - self._layout.search_field.setDisabled(True) - self._on_shift_held(held=False) - - def create_item_from_modal(self, edit_item_panel: PanelWidget) -> None: # pyright: ignore[reportUnusedParameter] + def _create_item_from_modal(self, edit_item_panel: PanelWidget) -> None: # pyright: ignore[reportUnusedParameter] raise NotImplementedError() - def edit_item(self, edit_item_panel: PanelWidget) -> None: # pyright: ignore[reportUnusedParameter] + def _edit_item(self, edit_item_panel: PanelWidget) -> None: # pyright: ignore[reportUnusedParameter] raise NotImplementedError() @override def showEvent(self, event: QShowEvent) -> None: - self.update_items() + self._update_items() self._on_shift_held(held=False) - self.clear_search_query() + self._layout.search_field.setDisabled(False) + self._clear_search_query() return super().showEvent(event) @override diff --git a/src/tagstudio/qt/controllers/tag_suggest_box.py b/src/tagstudio/qt/controllers/tag_suggest_box.py index d95291e5..87e9fde9 100644 --- a/src/tagstudio/qt/controllers/tag_suggest_box.py +++ b/src/tagstudio/qt/controllers/tag_suggest_box.py @@ -44,19 +44,19 @@ class TagSuggestBox(SuggestBox[Tag]): lambda checked: self.toggle_edit_on_tag_create(checked) ) - def search_for_tag(self, tag_id: int) -> None: + 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..""" + """Toggle the setting for opening the edit window after creating a tag.""" self._driver.settings.edit_tag_on_create = checked self._driver.settings.save() @override - def on_item_create(self) -> None: + def _on_item_create(self) -> None: """Opens panel to create a new tag and optionally add it to an entry. Populates name field using current search query. @@ -74,16 +74,16 @@ class TagSuggestBox(SuggestBox[Tag]): if query.strip(): panel.name_field.setText(query) - modal.saved.connect(lambda: self.create_item_from_modal(panel)) + modal.saved.connect(lambda: self._create_item_from_modal(panel)) modal.show() else: tag = Tag(name=query) self._lib.add_tag(tag) self._on_item_chosen(tag) - self.clear_search_query() + self._clear_search_query() @override - def on_item_edit(self, item: Tag) -> None: + def _on_item_edit(self, item: Tag) -> None: edit_tag_panel: BuildTagPanel = BuildTagPanel(self._lib, tag=item) edit_tag_modal: PanelModal = PanelModal( edit_tag_panel, @@ -91,7 +91,7 @@ class TagSuggestBox(SuggestBox[Tag]): Translations["tag.edit"], is_savable=True, ) - edit_tag_modal.saved.connect(lambda: self.edit_item(edit_tag_panel)) + edit_tag_modal.saved.connect(lambda: self._edit_item(edit_tag_panel)) edit_tag_modal.show() @override @@ -100,16 +100,16 @@ class TagSuggestBox(SuggestBox[Tag]): self.done.emit() @override - def search_items(self, query: str) -> tuple[list[Tag], list[Tag]]: + def _search_items(self, query: str) -> tuple[list[Tag], list[Tag]]: if query != "": return self._lib.search_tags(name=query, limit=0) else: return ([], []) @override - def set_item_widget(self, item: Tag | None, index: int) -> None: + def _set_item_widget(self, item: Tag | None, index: int) -> None: """Set the tag of a tag widget at a specific index.""" - tag_widget: TagWidget = self.get_item_widget(index, self._lib) + tag_widget: TagWidget = self._get_item_widget(index, self._lib) tag_widget.has_remove = False tag_widget.set_tag(item) tag_widget.setHidden(item is None) @@ -130,30 +130,30 @@ class TagSuggestBox(SuggestBox[Tag]): tag_widget.search_for_tag_action.triggered.disconnect() # Connect callbacks - tag_widget.on_edit.connect(lambda edit_tag=item: self.on_item_edit(edit_tag)) + tag_widget.on_edit.connect(lambda edit_tag=item: self._on_item_edit(edit_tag)) tag_widget.bg_button.clicked.connect( lambda checked=False, tag=item: self._on_item_chosen(tag) ) tag_widget.search_for_tag_action.triggered.connect( - lambda checked=False, tag_id=item.id: self.search_for_tag(tag_id) + lambda checked=False, tag_id=item.id: self._search_for_tag_callback(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: PanelWidget) -> None: if isinstance(edit_item_panel, BuildTagPanel): tag: Tag = edit_item_panel.build_tag() self._lib.add_tag( tag, parent_ids=edit_item_panel.parent_ids, aliases=edit_item_panel.aliases ) self._on_item_chosen(tag) - self.clear_search_query() + self._clear_search_query() edit_item_panel.hide() - self.on_search_query_changed(self._layout.search_field.text()) + 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: PanelWidget) -> None: if not isinstance(edit_item_panel, BuildTagPanel): return @@ -162,16 +162,16 @@ class TagSuggestBox(SuggestBox[Tag]): parent_ids=edit_item_panel.parent_ids, aliases=edit_item_panel.aliases, ) - self.update_items(self._layout.search_field.text()) + self._update_items(self._layout.search_field.text()) @override - def get_item_widget(self, index: int, library: Library | None) -> TagWidget: + 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.content_layout.count() <= index: while self._layout.content_layout.count() <= index: tag_widget = TagWidget(tag=None, has_edit=True, has_remove=True, library=library) - tag_widget.on_remove.connect(self.update_items) + tag_widget.on_remove.connect(self._update_items) tag_widget.setHidden(True) self._layout.content_layout.addWidget(tag_widget) diff --git a/src/tagstudio/qt/global_settings.py b/src/tagstudio/qt/global_settings.py index b72df4dc..0e9cda7b 100644 --- a/src/tagstudio/qt/global_settings.py +++ b/src/tagstudio/qt/global_settings.py @@ -75,6 +75,7 @@ class GlobalSettings(BaseModel): show_filepath: ShowFilepathOption = Field(default=ShowFilepathOption.DEFAULT) tag_click_action: TagClickActionOption = Field(default=TagClickActionOption.DEFAULT) edit_tag_on_create: bool = Field(default=False) + edit_field_on_add: bool = Field(default=True) theme: Theme = Field(default=Theme.SYSTEM) splash: Splash = Field(default=Splash.DEFAULT) windows_start_command: bool = Field(default=False) diff --git a/src/tagstudio/qt/mixed/field_containers.py b/src/tagstudio/qt/mixed/field_containers.py index 1983fbc2..731bd560 100644 --- a/src/tagstudio/qt/mixed/field_containers.py +++ b/src/tagstudio/qt/mixed/field_containers.py @@ -242,6 +242,27 @@ class FieldContainers(QWidget): ) self.driver.add_tags_to_selected_callback(tag_ids) + def update_text_field_callback( + self, field: TextField, entry_id: int, content: dict[str, str | bool] + ) -> None: + """Callback called when a text field has updated data.""" + self._update_text_field( + field, str(content["name"]), str(content["value"]), bool(content["is_multiline"]) + ) + self.update_from_entry(entry_id) + + def update_datetime_field_callback( + self, field: DatetimeField, entry_id: int, content: dict[str, str] + ) -> None: + """Callback called when a datetime field has updated data.""" + self.update_datetime_field(field, str(content["name"]), str(content["value"])) + self.update_from_entry(entry_id) + + def remove_field_callback(self, field: BaseField, entry_id: int) -> None: + """Callback called when a field needs to be removed from an entry.""" + self._remove_field(field) + self.update_from_entry(entry_id) + def write_field_container(self, index: int, field: BaseField, is_mixed: bool = False) -> None: """Update/Create data for a field FieldContainer. @@ -252,27 +273,6 @@ class FieldContainers(QWidget): If True, field is not present in all selected items. """ - def update_text_field_callback( - field: TextField, entry_id: int, content: dict[str, str | bool] - ) -> None: - """Callback called when a text field has updated data.""" - self._update_text_field( - field, str(content["name"]), str(content["value"]), bool(content["is_multiline"]) - ) - self.update_from_entry(entry_id) - - def update_datetime_field_callback( - field: DatetimeField, entry_id: int, content: dict[str, str] - ) -> None: - """Callback called when a datetime field has updated data.""" - self.update_datetime_field(field, str(content["name"]), str(content["value"])) - self.update_from_entry(entry_id) - - def remove_field_callback(field: BaseField, entry_id: int) -> None: - """Callback called when a field needs to be removed from an entry.""" - self._remove_field(field) - self.update_from_entry(entry_id) - def write_text_container( container: FieldContainer, field: TextField, title: str, is_mixed: bool ): @@ -296,14 +296,14 @@ class FieldContainers(QWidget): inline_title=False, ) edit_modal.saved_data.connect( - partial(update_text_field_callback, field, self.top_entry_id) + partial(self.update_text_field_callback, field, self.top_entry_id) ) container.set_edit_callback(edit_modal.show) container.set_remove_callback( lambda: self.remove_message_box( prompt=self.remove_field_prompt(title), - callback=partial(remove_field_callback, field, self.top_entry_id), + callback=partial(self.remove_field_callback, field, self.top_entry_id), ) ) @@ -334,14 +334,14 @@ class FieldContainers(QWidget): inline_title=False, ) edit_modal.saved_data.connect( - partial(update_datetime_field_callback, field, self.top_entry_id) + partial(self.update_datetime_field_callback, field, self.top_entry_id) ) container.set_edit_callback(edit_modal.show) container.set_remove_callback( lambda: self.remove_message_box( prompt=self.remove_field_prompt(field.name), - callback=partial(remove_field_callback, field, self.top_entry_id), + callback=partial(self.remove_field_callback, field, self.top_entry_id), ) ) @@ -352,7 +352,7 @@ class FieldContainers(QWidget): container.set_remove_callback( lambda: self.remove_message_box( prompt=self.remove_field_prompt(field.name), - callback=partial(remove_field_callback, field, self.top_entry_id), + callback=partial(self.remove_field_callback, field, self.top_entry_id), ) ) diff --git a/src/tagstudio/qt/mixed/settings_panel.py b/src/tagstudio/qt/mixed/settings_panel.py index 37e630fb..380e848e 100644 --- a/src/tagstudio/qt/mixed/settings_panel.py +++ b/src/tagstudio/qt/mixed/settings_panel.py @@ -216,13 +216,20 @@ class SettingsPanel(PanelWidget): Translations["settings.tag_click_action.label"], self.tag_click_action_combobox ) - # Open Edit Window When Creating Tag + # Open Edit Window When Creating a Tag self.edit_tag_on_create_checkbox = QCheckBox() self.edit_tag_on_create_checkbox.setChecked(self.driver.settings.edit_tag_on_create) form_layout.addRow( Translations["settings.edit_tag_on_create"], self.edit_tag_on_create_checkbox ) + # Open Edit Window When Adding a Field + self.edit_field_on_add_checkbox = QCheckBox() + self.edit_field_on_add_checkbox.setChecked(self.driver.settings.edit_field_on_add) + form_layout.addRow( + Translations["settings.edit_field_on_add"], self.edit_field_on_add_checkbox + ) + # TODO: Implement Library Settings def __build_library_settings(self): # pyright: ignore[reportUnusedFunction] form_layout = QFormLayout(self.library_settings_container) @@ -374,6 +381,7 @@ class SettingsPanel(PanelWidget): "theme": self.theme_combobox.currentData(), "tag_click_action": self.tag_click_action_combobox.currentData(), "edit_tag_on_create": self.edit_tag_on_create_checkbox.isChecked(), + "edit_field_on_add": self.edit_field_on_add_checkbox.isChecked(), "date_format": self.dateformat_combobox.currentData(), "hour_format": self.hourformat_checkbox.isChecked(), "zero_padding": self.zeropadding_checkbox.isChecked(), @@ -397,6 +405,7 @@ class SettingsPanel(PanelWidget): driver.settings.theme = settings["theme"] driver.settings.tag_click_action = settings["tag_click_action"] driver.settings.edit_tag_on_create = settings["edit_tag_on_create"] + driver.settings.edit_field_on_add = settings["edit_field_on_add"] driver.settings.date_format = settings["date_format"] driver.settings.hour_format = settings["hour_format"] driver.settings.zero_padding = settings["zero_padding"] diff --git a/src/tagstudio/qt/mixed/tag_widget.py b/src/tagstudio/qt/mixed/tag_widget.py index 67112cb8..5b397ce0 100644 --- a/src/tagstudio/qt/mixed/tag_widget.py +++ b/src/tagstudio/qt/mixed/tag_widget.py @@ -166,7 +166,6 @@ class TagWidget(QWidget): self.bg_button.setFixedHeight(22) self.setSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Minimum) - self.bg_button.setSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Minimum) self.base_layout.addWidget(self.bg_button) diff --git a/src/tagstudio/qt/views/field_template_widget_view.py b/src/tagstudio/qt/views/field_template_widget_view.py index 78efdb93..7ccd7d4a 100644 --- a/src/tagstudio/qt/views/field_template_widget_view.py +++ b/src/tagstudio/qt/views/field_template_widget_view.py @@ -2,8 +2,8 @@ # SPDX-License-Identifier: GPL-3.0-only from PySide6.QtCore import Signal -from PySide6.QtGui import QColor -from PySide6.QtWidgets import QHBoxLayout, QPushButton, QVBoxLayout, QWidget +from PySide6.QtGui import QColor, Qt +from PySide6.QtWidgets import QHBoxLayout, QPushButton, QSizePolicy, QVBoxLayout, QWidget from tagstudio.core.library.alchemy.enums import TagColorEnum from tagstudio.qt.models.palette import ColorType, get_tag_color @@ -30,7 +30,7 @@ class FieldTemplateWidgetView(QWidget): self.__root_layout = QVBoxLayout(self) self.__root_layout.setObjectName("root_layout") - + self.__root_layout.setAlignment(Qt.AlignmentFlag.AlignLeft) self.__root_layout.setContentsMargins(0, 0, 0, 0) # Background button @@ -47,6 +47,8 @@ class FieldTemplateWidgetView(QWidget): self.__inner_layout.setObjectName("inner_layout") self._bg_button.setLayout(self.__inner_layout) + self.setSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Minimum) + self.__inner_layout.setContentsMargins(0, 0, 0, 0) # Remove button @@ -58,7 +60,7 @@ class FieldTemplateWidgetView(QWidget): self._delete_button.setMaximumSize(22, 22) self.__inner_layout.addWidget(self._delete_button) - self.__inner_layout.addStretch(1) + self.__inner_layout.setAlignment(Qt.AlignmentFlag.AlignLeft) self.__connect_callbacks() diff --git a/src/tagstudio/qt/views/preview_panel_view.py b/src/tagstudio/qt/views/preview_panel_view.py index 10468598..87bf8541 100644 --- a/src/tagstudio/qt/views/preview_panel_view.py +++ b/src/tagstudio/qt/views/preview_panel_view.py @@ -11,7 +11,7 @@ from PySide6.QtGui import QDesktopServices from PySide6.QtWidgets import QHBoxLayout, QLabel, QSplitter, QVBoxLayout, QWidget from tagstudio.core.constants import FFMPEG_HELP_URL -from tagstudio.qt.controllers.field_template_search_panel_controller import FieldTemplateSearchPanel +from tagstudio.qt.controllers.field_suggest_box import FieldSuggestBox from tagstudio.qt.controllers.preview_thumb_controller import PreviewThumb from tagstudio.qt.controllers.return_button import ReturnButton from tagstudio.qt.controllers.tag_suggest_box import TagSuggestBox @@ -19,7 +19,6 @@ from tagstudio.qt.mixed.field_containers import FieldContainers from tagstudio.qt.mixed.file_attributes import FileAttributes from tagstudio.qt.resource_manager import ResourceManager from tagstudio.qt.translations import Translations -from tagstudio.qt.views.field_template_search_panel_view import FieldTemplateSearchPanelView from tagstudio.qt.views.stylesheets.stylesheets import button_style, preview_warning_style from tagstudio.qt.views.suggest_box_view import SuggestBoxView @@ -36,20 +35,16 @@ class PreviewPanelView(QVBoxLayout): self.setSpacing(6) rm = ResourceManager() - # Search/Create Boxes - self.field_search_box: FieldTemplateSearchPanel = FieldTemplateSearchPanel( - driver.lib, - is_field_template_chooser=True, - view=FieldTemplateSearchPanelView(is_field_template_chooser=True), - ) + def ph_text(key: str) -> str: + return " ".join([Translations[key], Translations["home.search.how_to_exit"]]) - tag_placeholder = " ".join( - [Translations["home.search_or_create_tags"], Translations["home.search.how_to_exit"]] + # Search/Create Boxes + self.field_search_box = FieldSuggestBox( + driver, view=SuggestBoxView(placeholder_text=ph_text("home.search_or_create_fields")) ) self.tag_search_box = TagSuggestBox( - driver, view=SuggestBoxView(placeholder_text=tag_placeholder) + driver, view=SuggestBoxView(placeholder_text=ph_text("home.search_or_create_tags")) ) - self.tag_search_box.hide() self.preview_thumb = PreviewThumb(driver.lib, driver) self.file_attrs = FileAttributes(driver.lib, driver) @@ -119,8 +114,9 @@ class PreviewPanelView(QVBoxLayout): add_buttons_layout.addWidget(self.add_tag_button) add_buttons_layout.addWidget(self.add_field_button) add_buttons_layout.addWidget(self.tag_search_box) - # add_buttons_layout.addWidget(self.field_search) + add_buttons_layout.addWidget(self.field_search_box) + # Finalize Layout preview_layout.addWidget(self.preview_thumb) info_layout.addWidget(self.warning_banner) info_layout.addWidget(self.file_attrs) @@ -130,6 +126,5 @@ class PreviewPanelView(QVBoxLayout): splitter.addWidget(info_section) splitter.setStretchFactor(1, 2) - # Finalize Layout self.addWidget(splitter) self.addWidget(add_buttons_container) diff --git a/src/tagstudio/resources/translations/en.json b/src/tagstudio/resources/translations/en.json index 199aef6c..b303bd5b 100644 --- a/src/tagstudio/resources/translations/en.json +++ b/src/tagstudio/resources/translations/en.json @@ -163,6 +163,7 @@ "home.search_entries": "Search Entries", "home.search_field_templates": "Search Field Templates…", "home.search_library": "Search Library", + "home.search_or_create_fields": "Search or Create Fields…", "home.search_or_create_tags": "Search or Create Tags…", "home.search_tags": "Search Tags…", "home.search.how_to_exit": "(Esc to Exit)", @@ -317,7 +318,8 @@ "settings.dateformat.international": "International", "settings.dateformat.label": "Date Format", "settings.dateformat.system": "System", - "settings.edit_tag_on_create": "Edit Tag After Creation", + "settings.edit_field_on_add": "Edit After Adding a Field", + "settings.edit_tag_on_create": "Edit After Creating a New Tag", "settings.filepath.label": "Filepath Visibility", "settings.filepath.option.full": "Show Full Paths", "settings.filepath.option.name": "Show Filenames Only",