feat(ui): add hint icons to suggest boxes (#1475)
* feat(ui): add hint icons to suggest boxes * docs: directly mention new icons
@@ -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"
|
||||
|
||||
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 8.3 KiB After Width: | Height: | Size: 9.0 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 9.9 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 12 KiB |
@@ -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 <kbd>Ctrl</kbd>+<kbd>T</kbd>.
|
||||
|
||||
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 <kbd>Enter</kbd> 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 <kbd>Enter</kbd> 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 <kbd>Enter</kbd> or <kbd>Space</kbd> 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 <kbd>Enter</kbd> with **one or more results** will **apply the first tag** to your selection (assuming it's not already on your selection).
|
||||
Pressing <kbd>Enter</kbd> 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 <kbd>Enter</kbd>. 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 <kbd>Shift</kbd> while pressing <kbd>Enter</kbd> 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 <kbd>Shift</kbd> while pressing <kbd>Enter</kbd> 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.
|
||||
|
||||
<figure markdown="span">
|
||||
{ width=80% }
|
||||
<figcaption>Pressing <kbd>Enter</kbd> will apply the underlined tag, "Cube".</figcaption>
|
||||
<figcaption markdown="span">Pressing <kbd>Enter</kbd> will **apply** :material-tag: the underlined tag, "Cube".</figcaption>
|
||||
{ width=80% }
|
||||
<figcaption>With <kbd>Shift</kbd> held, pressing <kbd>Enter</kbd> will <b>create a new tag</b> called "Cub".</figcaption>
|
||||
<figcaption markdown="span">
|
||||
<kbd>Shift</kbd>+<kbd>Enter</kbd> will **create** :material-tag-plus-outline: a new tag called "Cub".
|
||||
<br>
|
||||
Note the the greyed-out tag, showing it will not be applied.
|
||||
</figcaption>
|
||||
</figure>
|
||||
|
||||
---
|
||||
@@ -90,7 +94,7 @@ Holding <kbd>Shift</kbd> while pressing <kbd>Enter</kbd> 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 <kbd>Ctrl</kbd>+<kbd>L</kbd> to open the field bar.
|
||||
- The same template can by applied to an entry any number of times.
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 != "":
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 2.4 KiB |