diff --git a/REUSE.toml b/REUSE.toml index 70e2cf7c4..1d39078d2 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 10f4fca63..c7c8063c4 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 7327d9063..3a8b83f1b 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 3e993c4dd..24431ccc0 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 a52deb6b2..32b16034a 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 be3179a0c..f3d728333 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 a983983c8..801b765e4 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 87b2346cc..a08563a67 100644 --- a/src/tagstudio/qt/controllers/field_suggest_box.py +++ b/src/tagstudio/qt/controllers/field_suggest_box.py @@ -86,6 +86,24 @@ def _search_items(self, query: str) -> tuple[list[BaseFieldTemplate], list[BaseF 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 a0cdf67a1..5dccd0238 100644 --- a/src/tagstudio/qt/controllers/suggest_box.py +++ b/src/tagstudio/qt/controllers/suggest_box.py @@ -5,8 +5,9 @@ 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.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 @@ def __init__( 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 @@ def _connect_callbacks(self) -> None: 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 @@ def _get_item_widget(self, index: int, library: Library) -> Any: # pyright: ign 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 @@ def _on_item_chosen(self, item: T) -> None: # pyright: ignore[reportUnusedParam 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 2f69f97c6..e9538d374 100644 --- a/src/tagstudio/qt/controllers/tag_suggest_box.py +++ b/src/tagstudio/qt/controllers/tag_suggest_box.py @@ -91,6 +91,24 @@ def _on_item_chosen(self, item: Tag) -> None: 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 986ed8843..46e6ca582 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 6533ce38c..3fbdefba3 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 da5dcd662..7279d6a1e 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 @@ 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 @@ def __init__(self, placeholder_text: str = "") -> None: 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 000000000..dabe8d927 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 000000000..f8ee8f191 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 000000000..50a1e89a8 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 000000000..10c94b306 Binary files /dev/null and b/src/tagstudio/resources/qt/images/hint_tag_create.png differ