feat(ui): add autocomplete search for field templates

This commit is contained in:
Travis Abendshien
2026-07-19 03:53:47 -07:00
parent 37f8eeac6f
commit 8e3c0fb092
11 changed files with 365 additions and 140 deletions
@@ -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
@@ -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)
+34 -35
View File
@@ -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
+19 -19
View File
@@ -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)
+1
View File
@@ -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)
+26 -26
View File
@@ -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),
)
)
+10 -1
View File
@@ -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"]
-1
View File
@@ -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)
@@ -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()
+9 -14
View File
@@ -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)
+3 -1
View File
@@ -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",