Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion REUSE.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Binary file modified docs/assets/tag_field_bars/field_bar_search.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/assets/tag_field_bars/tag_bar_new.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/assets/tag_field_bars/tag_bar_search_match.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/assets/tag_field_bars/tag_bar_shift_off.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/assets/tag_field_bars/tag_bar_shift_on.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 11 additions & 7 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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"**.

Expand All @@ -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.

Expand All @@ -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">
![Empty File Entry](assets/tag_field_bars/tag_bar_shift_off.png){ 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>
![Empty File Entry](assets/tag_field_bars/tag_bar_shift_on.png){ 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>

---
Expand All @@ -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.
Expand Down
18 changes: 18 additions & 0 deletions src/tagstudio/qt/controllers/field_suggest_box.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down
17 changes: 16 additions & 1 deletion src/tagstudio/qt/controllers/suggest_box.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,18 @@
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
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,
Expand Down Expand Up @@ -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] = []
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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)
Expand Down
18 changes: 18 additions & 0 deletions src/tagstudio/qt/controllers/tag_suggest_box.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 != "":
Expand Down
4 changes: 4 additions & 0 deletions src/tagstudio/qt/resource_manager.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
16 changes: 16 additions & 0 deletions src/tagstudio/qt/resources.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
5 changes: 5 additions & 0 deletions src/tagstudio/qt/views/suggest_box_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)

Expand Down Expand Up @@ -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)

Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading