mirror of
https://github.com/TagStudioDev/TagStudio.git
synced 2026-07-20 12:36:19 +02:00
feat(ui): add autocomplete search for field templates
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user