diff --git a/REUSE.toml b/REUSE.toml index 70e2cf7c..1d39078d 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -45,6 +45,12 @@ SPDX-FileCopyrightText = "(c) 2026 Boxicons" SPDX-License-Identifier = "MIT" [[annotations]] -path = ["src/tagstudio/resources/qt/images/dupe_file_stat.png"] +path = [ + "src/tagstudio/resources/qt/images/dupe_file_stat.png", + "src/tagstudio/resources/qt/images/hint_field_add.png", + "src/tagstudio/resources/qt/images/hint_field_create.png", + "src/tagstudio/resources/qt/images/hint_tag_add.png", + "src/tagstudio/resources/qt/images/hint_tag_create.png", +] SPDX-FileCopyrightText = "(c) github:google/material-design-icons Contributors" SPDX-License-Identifier = "Apache-2.0" diff --git a/docs/assets/tag_field_bars/field_bar_search.png b/docs/assets/tag_field_bars/field_bar_search.png index 10f4fca6..c7c8063c 100644 Binary files a/docs/assets/tag_field_bars/field_bar_search.png and b/docs/assets/tag_field_bars/field_bar_search.png differ diff --git a/docs/assets/tag_field_bars/tag_bar_new.png b/docs/assets/tag_field_bars/tag_bar_new.png index 7327d906..3a8b83f1 100644 Binary files a/docs/assets/tag_field_bars/tag_bar_new.png and b/docs/assets/tag_field_bars/tag_bar_new.png differ diff --git a/docs/assets/tag_field_bars/tag_bar_search_match.png b/docs/assets/tag_field_bars/tag_bar_search_match.png index 3e993c4d..24431ccc 100644 Binary files a/docs/assets/tag_field_bars/tag_bar_search_match.png and b/docs/assets/tag_field_bars/tag_bar_search_match.png differ diff --git a/docs/assets/tag_field_bars/tag_bar_shift_off.png b/docs/assets/tag_field_bars/tag_bar_shift_off.png index a52deb6b..32b16034 100644 Binary files a/docs/assets/tag_field_bars/tag_bar_shift_off.png and b/docs/assets/tag_field_bars/tag_bar_shift_off.png differ diff --git a/docs/assets/tag_field_bars/tag_bar_shift_on.png b/docs/assets/tag_field_bars/tag_bar_shift_on.png index be3179a0..f3d72833 100644 Binary files a/docs/assets/tag_field_bars/tag_bar_shift_on.png and b/docs/assets/tag_field_bars/tag_bar_shift_on.png differ diff --git a/docs/usage.md b/docs/usage.md index a983983c..801b765e 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -26,7 +26,7 @@ TagStudio automatically scans for new or updated files when opening a library by With one or more file entries selected, you can **search for** or **create a new tag** by clicking the "Add Tag" button at the bottom of the preview panel or by pressing Ctrl+T. -This will open the the **"Tag Bar"**, a combination search/creation bar that is designed to allow you to quickly create and apply tags that don't exist yet, find and apply existing tags in your library, and verify if certain tags already exist and/or are applied to your selection. +This will open the the **"Tag Bar"**, a combination search/creation bar that is designed to allow you to quickly create and apply tags that don't exist yet, find and apply existing tags from your library, and verify if certain tags already exist and/or are applied to your selection. Every step is keyboard-friendly, allowing for an efficient tagging "flow state" when it comes to creating and applying tags. @@ -43,7 +43,7 @@ Every step is keyboard-friendly, allowing for an efficient tagging "flow state" ### :material-tag-plus: Tag Bar: Creating Tags -Type to search for an existing tag, or use that search term to quickly create and apply a new tag to your selection. Pressing Enter with **no results** will **create** a tag from your search query and apply it to the selected entries. +Type to search for an existing tag, or use that search term to quickly create and apply a new tag to your selection. Pressing Enter with **no results** will **create** :material-tag-plus-outline: a tag from your search query and apply it to the selected entries. After applying a tag, the "Add Tag" button is now focused, allowing you to press Enter or Space to quickly reactivate the bar. Alternatively, you may edit your new tag by right-clicking the tag and selecting **"Edit"**. @@ -62,7 +62,7 @@ Tags can be also viewed, created, edited, or deleted from the **Edit -> Manage T ### :material-tag-search: Tag Bar: Searching for Tags -Pressing Enter with **one or more results** will **apply the first tag** to your selection (assuming it's not already on your selection). +Pressing Enter with **one or more results** will **apply** :material-tag: the underlined tag to your selection (assuming it's not already on your selection). Tags that are already present on your entries will be greyed out and ignored when pressing Enter. This lets you visually confirm that a tag already exists and has been applied to the selection. @@ -75,13 +75,17 @@ Tags that are already present on your entries will be greyed out and ignored whe #### :material-tag-arrow-up: Force Create Tag -Holding Shift while pressing Enter will **force create** a tag from the search bar text, regardless of the results. This is indicated by the first tag in the results becoming greyed out. This is especially useful when you need to create a new tag with a name that partially overlaps with an existing tag's name. +Holding Shift while pressing Enter will **force create** :material-tag-plus-outline: a tag from the search bar text, regardless of the search results. This is especially useful when you need to create a new tag with a name that partially overlaps with an existing tag's name.
![Empty File Entry](assets/tag_field_bars/tag_bar_shift_off.png){ width=80% } -
Pressing Enter will apply the underlined tag, "Cube".
+
Pressing Enter will **apply** :material-tag: the underlined tag, "Cube".
![Empty File Entry](assets/tag_field_bars/tag_bar_shift_on.png){ width=80% } -
With Shift held, pressing Enter will create a new tag called "Cub".
+
+ Shift+Enter will **create** :material-tag-plus-outline: a new tag called "Cub". +
+ Note the the greyed-out tag, showing it will not be applied. +
--- @@ -90,7 +94,7 @@ Holding Shift while pressing Enter will **force create** a [Fields](./fields.md) are extra pieces of information you can add to file entries, such as titles, comments, notes, specific dates or times. Unlike tags, fields are based on [templates](./fields.md#field-templates) that contain pre-filled information such as the field type and title, and that information is _copied_ to fields when adding them to entries. Editing field information on entries **does not** modify the template it was created from. -Creating and adding fields to entries is extremely similar to [how it works for tags](#tagging), with a few notable differences: +Creating and adding fields to entries is extremely similar to how [tagging](#tagging) works, with a few notable differences: - Click "Add Field" or press Ctrl+L to open the field bar. - The same template can by applied to an entry any number of times. diff --git a/src/tagstudio/qt/controllers/field_suggest_box.py b/src/tagstudio/qt/controllers/field_suggest_box.py index 87b2346c..a08563a6 100644 --- a/src/tagstudio/qt/controllers/field_suggest_box.py +++ b/src/tagstudio/qt/controllers/field_suggest_box.py @@ -86,6 +86,24 @@ class FieldSuggestBox(SuggestBox[BaseFieldTemplate]): else: return ([], []) + @override + def _on_shift_held(self, held: bool) -> None: + if held: + self.set_hint_icon(self._rm.hint_field_create) + else: + self._update_hint_icon() + + return super()._on_shift_held(held) + + @override + def _update_hint_icon(self) -> None: + if self.layout().search_field.text() and len(self._search_results) > 0: + self.set_hint_icon(self._rm.hint_field_add) + elif self.layout().search_field.text(): + self.set_hint_icon(self._rm.hint_field_create) + else: + self.set_hint_icon(None) + @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.""" diff --git a/src/tagstudio/qt/controllers/suggest_box.py b/src/tagstudio/qt/controllers/suggest_box.py index a0cdf67a..5dccd023 100644 --- a/src/tagstudio/qt/controllers/suggest_box.py +++ b/src/tagstudio/qt/controllers/suggest_box.py @@ -5,8 +5,9 @@ from functools import partial from typing import Any, override import structlog +from PIL import Image, ImageQt from PySide6.QtCore import Signal -from PySide6.QtGui import QAction, QShowEvent, Qt +from PySide6.QtGui import QAction, QPixmap, QShowEvent, Qt from PySide6.QtWidgets import QGraphicsOpacityEffect, QWidget from tagstudio.core.library.alchemy.library import Library @@ -14,6 +15,8 @@ from tagstudio.qt.controllers.autofill_line_edit import QtCore, QtGui from tagstudio.qt.controllers.modal_content import ModalContent from tagstudio.qt.controllers.underlined_widget import UnderlinedWidget from tagstudio.qt.global_settings import GlobalSettings +from tagstudio.qt.helpers.color_overlay import auto_theme_overlay +from tagstudio.qt.resource_manager import ResourceManager from tagstudio.qt.translations import Translations from tagstudio.qt.views.stylesheets.stylesheets import ( autofill_line_edit_style, @@ -52,6 +55,7 @@ class SuggestBox[T](QWidget): super().__init__() self._lib = library self._settings = settings + self._rm = ResourceManager() self._limit = 25 self._is_shift_held = False self._search_results: list[T] = [] @@ -98,6 +102,13 @@ class SuggestBox[T](QWidget): self.layout().search_field.holding_shift.connect(partial(self._on_shift_held)) self.layout().search_field.index_updated.connect(partial(self._on_index_updated)) + def set_hint_icon(self, icon: Image.Image | None) -> None: + if icon: + pixmap = QPixmap.fromImage(ImageQt.ImageQt(auto_theme_overlay(icon))) + self.layout().hint_icon_action.setIcon(pixmap) + else: + self.layout().hint_icon_action.setIcon(QPixmap()) + def _on_shift_held(self, held: bool) -> None: for i in range(0, self.layout().content_layout.count()): underlined_widget = self.layout().content_layout.itemAt(i).widget() @@ -154,6 +165,7 @@ class SuggestBox[T](QWidget): raise NotImplementedError() def _on_search_query_changed(self, query: str) -> None: + self._update_hint_icon() self._update_items(query.strip()) def _on_search_query_submitted(self, query: str, always_create: bool = False) -> None: @@ -185,6 +197,9 @@ class SuggestBox[T](QWidget): def _is_excluded(self, item: T) -> bool: return _item_id(item) in self.excluded + def _update_hint_icon(self) -> None: + raise NotImplementedError() + 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) diff --git a/src/tagstudio/qt/controllers/tag_suggest_box.py b/src/tagstudio/qt/controllers/tag_suggest_box.py index 2f69f97c..e9538d37 100644 --- a/src/tagstudio/qt/controllers/tag_suggest_box.py +++ b/src/tagstudio/qt/controllers/tag_suggest_box.py @@ -91,6 +91,24 @@ class TagSuggestBox(SuggestBox[Tag]): self._clear_search_query() self.done.emit("*") # The query does not matter + @override + def _on_shift_held(self, held: bool) -> None: + if held: + self.set_hint_icon(self._rm.hint_tag_create) + else: + self._update_hint_icon() + + return super()._on_shift_held(held) + + @override + def _update_hint_icon(self) -> None: + if self.layout().search_field.text() and len(self._search_results) > 0: + self.set_hint_icon(self._rm.hint_tag_add) + elif self.layout().search_field.text(): + self.set_hint_icon(self._rm.hint_tag_create) + else: + self.set_hint_icon(None) + @override def _search_items(self, query: str) -> tuple[list[Tag], list[Tag]]: if query != "": diff --git a/src/tagstudio/qt/resource_manager.pyi b/src/tagstudio/qt/resource_manager.pyi index 986ed884..46e6ca58 100644 --- a/src/tagstudio/qt/resource_manager.pyi +++ b/src/tagstudio/qt/resource_manager.pyi @@ -36,6 +36,10 @@ class ResourceManager: edit: Image.Image file_generic: Image.Image font: Image.Image + hint_field_add: Image.Image + hint_field_create: Image.Image + hint_tag_add: Image.Image + hint_tag_create: Image.Image icon: Image.Image ignored_stat: Image.Image ignored: Image.Image diff --git a/src/tagstudio/qt/resources.json b/src/tagstudio/qt/resources.json index 6533ce38..3fbdefba 100644 --- a/src/tagstudio/qt/resources.json +++ b/src/tagstudio/qt/resources.json @@ -71,6 +71,22 @@ "mode": "pil", "path": "qt/images/file_icons/font.png" }, + "hint_field_add": { + "mode": "pil", + "path": "qt/images/hint_field_add.png" + }, + "hint_field_create": { + "mode": "pil", + "path": "qt/images/hint_field_create.png" + }, + "hint_tag_add": { + "mode": "pil", + "path": "qt/images/hint_tag_add.png" + }, + "hint_tag_create": { + "mode": "pil", + "path": "qt/images/hint_tag_create.png" + }, "icon": { "mode": "pil", "path": "icon.png" diff --git a/src/tagstudio/qt/views/suggest_box_view.py b/src/tagstudio/qt/views/suggest_box_view.py index da5dcd66..7279d6a1 100644 --- a/src/tagstudio/qt/views/suggest_box_view.py +++ b/src/tagstudio/qt/views/suggest_box_view.py @@ -4,6 +4,7 @@ import structlog from PySide6.QtCore import Qt +from PySide6.QtGui import QPixmap from PySide6.QtWidgets import QFrame, QHBoxLayout, QSizePolicy, QVBoxLayout, QWidget from tagstudio.qt.controllers.autofill_line_edit import AutofillLineEdit @@ -19,6 +20,7 @@ logger = structlog.get_logger(__name__) class SuggestBoxView(QVBoxLayout): def __init__(self, placeholder_text: str = "") -> None: super().__init__() + self.setContentsMargins(0, 0, 0, 0) self.setSpacing(0) @@ -66,6 +68,9 @@ class SuggestBoxView(QVBoxLayout): self.search_field.setObjectName("search_field") self.search_field.setMinimumHeight(search_bar_height) self.search_field.setPlaceholderText(placeholder_text) + self.hint_icon_action = self.search_field.addAction( + QPixmap(), AutofillLineEdit.ActionPosition.TrailingPosition + ) self.scroll_area.setFocusProxy(self.search_field) self.search_field.setContextMenuPolicy(Qt.ContextMenuPolicy.ActionsContextMenu) diff --git a/src/tagstudio/resources/qt/images/hint_field_add.png b/src/tagstudio/resources/qt/images/hint_field_add.png new file mode 100644 index 00000000..dabe8d92 Binary files /dev/null and b/src/tagstudio/resources/qt/images/hint_field_add.png differ diff --git a/src/tagstudio/resources/qt/images/hint_field_create.png b/src/tagstudio/resources/qt/images/hint_field_create.png new file mode 100644 index 00000000..f8ee8f19 Binary files /dev/null and b/src/tagstudio/resources/qt/images/hint_field_create.png differ diff --git a/src/tagstudio/resources/qt/images/hint_tag_add.png b/src/tagstudio/resources/qt/images/hint_tag_add.png new file mode 100644 index 00000000..50a1e89a Binary files /dev/null and b/src/tagstudio/resources/qt/images/hint_tag_add.png differ diff --git a/src/tagstudio/resources/qt/images/hint_tag_create.png b/src/tagstudio/resources/qt/images/hint_tag_create.png new file mode 100644 index 00000000..10c94b30 Binary files /dev/null and b/src/tagstudio/resources/qt/images/hint_tag_create.png differ