Skip to content

Commit fce9785

Browse files
CyanVoxelpython357-1Computerdores
authored
feat!: tag categories (#655)
* refactor: remove TagBoxField and TagField (NOT WORKING) * refactor: remove tag field types * ci: fix mypy and ruff tests * refactor: split up preview_panel * fix: search now uses `TagEntry` (#656) * fix: move theme check inside class * refactor: reimplement file previews * refactor: modularize `file_attributes.py` * ui: show fields in preview panel known issues: - fields to not visually update after being edited until the entries are reloaded from the thumbnail grid (yes, the thumbnail grid) - add field button currently non-functional - surprise segfaults * search: remove TagEntry join * fix: remove extra `self.filter` assignment * add success return flag to `add_tags_to_entry()` * refactor: use entry IDs instead of objects and indices - fixes preview panel not updating after entry edits - fixes slow selection performance - fixes double render call * feat: add tag categories to preview panel * ui: add "is category" checkbox in tag panel * fix: tags can be compared for name sorting * fix: don't add tags to previous selections * fix: badges now properly update * ui: hide sizeGrip * ui: add blue ui color * ui: display empty selection; better multi-selection * cleanup comments; rename tsp to tag_search_panel * fix(ui): properly unset container callbacks * fix: optimize queries * fix: catch int cast exception * fix: remove unnecessary update calls * fix: restore try/except block in preview_panel * fix: correct type hints for get_tag_categories * fix: tags no longer lazy load subtags and aliases * fix: recursively include parent tag categories * chore: update copyright info * chore: remove unused code * fix: load fields for selected entry * refactor: remove `is_connected` from AddFieldModal * fix: include category tags under their own categories * fix: badges now update when last tag is removed * fix: resolve differences with main * fix: return empty set in place of `None` * ui: add field highlighting, tweak theming * refactor!: eradicate use of the term "subtag" - Removes ambiguity between the use of the term "parent tag" and "subtag" - Fixes inconstancies between the use of the term "subtag" to refer to either parent tags or child tags - Fixes duplicate and ambiguous subtags mapped relationship for the Tag model - Does NOT fix tests * fix: catch and show library load errors * tests: fix and/or remove tests * suppress db preference warnings * tests: add field container tests * tests: add tag category tests * refactor(ui): move recent libraries list to file menu * docs: update roadmap and docs for tag categories * fix: restore json migration functionality * logs: remove/update debug logs * chore: remove unused code * tests: remove tests related to `TagBoxWidget` * ui: optimize selection and badge updates * docs: update usage * fix: change typo of `tag.id` to `tag_id` Co-authored-by: Jann Stute <46534683+Computerdores@users.noreply.github.com> * fix: use term "child tags" instead of "subtags" in docstring Co-authored-by: Jann Stute <46534683+Computerdores@users.noreply.github.com> * fix: reference `child_id` instead of `parent_id` when deleting tags Co-Authored-By: Jann Stute <46534683+Computerdores@users.noreply.github.com> * add TODO comment for `update_thumbs()` optimization * fix: combine and check (most) built-in tag data from JSON Known issue: Tag colors from built-in JSON tags are not updated. This can be seen in the failing test. * refactor: rename `select_item()` to `toggle_item_selection()` * add TODO to optimize `add_tags_to_entry()` * fix: remove unnecessary joins in search * Revert "fix: remove unnecessary joins in search" This reverts commit 4c019ca. * fix: remove unnecessary joins in search * reremove unused method `get_all_child_tag_ids()` * fix: migrate user-edited tag colors for built-in tags * style: update header for contributor-created files * fix: use absolute path in "open file" context menu * chore: change paramater type hint --------- Co-authored-by: python357-1 <jb2101554@gmail.com> Co-authored-by: Jann Stute <46534683+Computerdores@users.noreply.github.com>
1 parent 5860a2c commit fce9785

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+2520
-2468
lines changed

README.md

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -71,12 +71,12 @@ Translation hosting generously provided by [Weblate](https://weblate.org/en/). C
7171
- Create libraries/vaults centered around a system directory. Libraries contain a series of entries: the representations of your files combined with metadata fields. Each entry represents a file in your library’s directory, and is linked to its location.
7272
- Address moved, deleted, or otherwise "unlinked" files by using the "Fix Unlinked Entries" option in the Tools menu.
7373

74-
### Metadata + Tagging
74+
### Tagging + Custom Metadata
7575

76+
- Add custom powerful tags to your library entries
7677
- Add metadata to your library entries, including:
7778
- Name, Author, Artist (Single-Line Text Fields)
7879
- Description, Notes (Multiline Text Fields)
79-
- Tags, Meta Tags, Content Tags (Tag Boxes)
8080
- Create rich tags composed of a name, a list of aliases, and a list of “parent tags” - being tags in which these tags inherit values from.
8181
- Copy and paste tags and fields across file entries
8282
- Generate tags from your existing folder structure with the "Folders to Tags" macro (NOTE: these tags do NOT sync with folders after they are created)
@@ -139,20 +139,22 @@ In order to scan for new files or file changes, you’ll need to manually go to
139139
> [!NOTE]
140140
> In the future, library refreshing will also be automatically done in the background, or additionally on app startup.
141141
142-
### Adding Metadata to Entries
142+
### Adding Tags to File Entries
143143

144-
To add a metadata field to a file entry, start by clicking the “Add Field” button under the file preview in the right-hand preview panel. From the dropdown menu, select the type of metadata field you’d like to add to the entry.
144+
Click the "Add Tag" button at the bottom of the preview panel with one or more tags selected. Search for existing inside the new dialog popup or create a new one from here. Click the “+” button next to whichever tags you want to add. Alternatively, after you search for a tag, press the Enter/Return key to add the first item in the list. Press Enter/Return once more to close the dialog box.
145+
146+
To remove a tag from a file entry, hover over the tag in the preview panel and click on the "-" icon that appears.
147+
148+
### Adding Metadata to File Entries
149+
150+
To add a metadata field to a file entry, start by clicking the “Add Field” button at the bottom of the preview panel. From the dropdown menu, select the type of metadata field you’d like to add to the entry
145151

146152
### Editing Metadata Fields
147153

148154
#### Text Line / Text Box
149155

150156
Hover over the field and click the pencil icon. From there, add or edit text in the dialog box popup.
151157

152-
#### Tag Box
153-
154-
Click the “+” button at the end of the Tags list, and search for tags to add inside the new dialog popup. Click the “+” button next to whichever tags you want to add. Alternatively, after you search for a tag, press the Enter/Return key to add the add the first item in the list. Press Enter/Return once more to close the dialog box
155-
156158
> [!WARNING]
157159
> Keyboard control and navigation is currently _very_ buggy, but will be improved in future versions.
158160

docs/library/tag_categories.md

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
11
---
22
tags:
3-
- Upcoming Feature
43
---
54

6-
# Tag Categories
5+
# Tag Categories (v9.5)
76

8-
Replaces [Tag Fields](field.md#tag_box). Tags are able to be marked as a “category” which then displays as tag fields currently do, with any tags inheriting from that category being displayed underneath.
7+
The "Is Category" property of tags determines if a tag should be treated as a category itself when being organized inside the preview panel. Tags marked as categories will show themselves and all tags inheriting from it (including recursively) underneath a field-like section with the tag's name. This means that duplicates of tags can appear on entries if the tag inherits from multiple parent categories, however this is by design and reflects the nature multiple inheritance. Any tags not inheriting from a category tag will simply show under a default "Tag" section.
8+
9+
### Built-In Tags and Categories
10+
11+
The built-in tags "Favorite" and "Archived" inherit from the built-in "Meta Tags" category which is marked as a category by default. This behavior of default tags can be fully customized by disabling the category option and/or by adding/removing the tags' Parent Tags.
12+
13+
### Migrating from v9.4 Libraries
14+
15+
Due to the nature of how tags and Tag Felids operated prior to v9.5, the organization style of Tag Categories vs Tag Fields is not 1:1. Instead of tags being organized into fields on a per-entry basis, tags themselves determine their organizational layout via the "Is Property" flag. Any tags _(not currently inheriting from either the "Favorite" or "Archived" tags)_ will be shown under the default "Tags" header upon migrating to the v9.5+ library format. Similar organization to Tag Fields can be achieved by using the built-in "Meta Tags" tag or any other marked with "Is Category" and then setting those tags as parents for other tags to inherit from.

docs/updates/roadmap.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ Features are broken up into the following priority levels, with nested prioritie
3434
- [ ] Existing colors are now a set of base colors [HIGH]
3535
- [ ] Editable [MEDIUM]
3636
- [ ] Non-removable [HIGH]
37-
- [ ] [Tag Categories](../library/tag_categories.md) [HIGH]
38-
- [ ] Property available for tags that allow the tag and any inheriting from it to be displayed separately in the preview panel under a title [HIGH]
37+
- [x] [Tag Categories](../library/tag_categories.md) [HIGH]
38+
- [x] Property available for tags that allow the tag and any inheriting from it to be displayed separately in the preview panel under a title [HIGH]
3939
- [ ] Title is tag name [HIGH]
4040
- [ ] Title has tag color [MEDIUM]
4141
- [ ] Tag marked as category does not display as a tag itself [HIGH]
@@ -170,8 +170,8 @@ These version milestones are rough estimations for when the previous core featur
170170
- [ ] Existing colors are now a set of base colors [HIGH]
171171
- [ ] Editable [MEDIUM]
172172
- [ ] Non-removable [HIGH]
173-
- [ ] [Tag Categories](../library/tag_categories.md) [HIGH]
174-
- [ ] Property available for tags that allow the tag and any inheriting from it to be displayed separately in the preview panel under a title [HIGH]
173+
- [x] [Tag Categories](../library/tag_categories.md) [HIGH]
174+
- [x] Property available for tags that allow the tag and any inheriting from it to be displayed separately in the preview panel under a title [HIGH]
175175
- [ ] Search engine [HIGH]
176176
- [x] Boolean operators [HIGH]
177177
- [ ] Tag objects + autocomplete [HIGH]

docs/usage.md

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,20 +11,21 @@ In order to scan for new files or file changes, you’ll need to manually go to
1111
!!! note
1212
In the future, library refreshing will also be automatically done in the background, or additionally on app startup.
1313

14-
## Adding Metadata to Entries
14+
## Adding Tags to File Entries
15+
Click the "Add Tag" button at the bottom of the preview panel with one or more tags selected. Search for existing inside the new dialog popup or create a new one from. Click the “+” button next to whichever tags you want to add. Alternatively, after you search for a tag, press the Enter/Return key to add the first item in the list. Press Enter/Return once more to close the dialog box.
1516

16-
To add a metadata field to a file entry, start by clicking the “Add Field” button under the file preview in the right-hand preview panel. From the dropdown menu, select the type of metadata field you’d like to add to the entry.
17+
To remove a tag from a file entry, hover over the tag in the preview panel and click on the "-" icon that appears.
18+
19+
## Adding Metadata Fields to File Entries
20+
21+
To add a metadata field to a file entry, start by clicking the “Add Field” button at the bottom of the preview panel. From the dropdown menu, select the type of metadata field you’d like to add to the entry.
1722

1823
## Editing Metadata Fields
1924

2025
### Text Line / Text Box
2126

2227
Hover over the field and click the pencil icon. From there, add or edit text in the dialog box popup.
2328

24-
### Tag Box
25-
26-
Click the “+” button at the end of the Tags list, and search for tags to add inside the new dialog popup. Click the “+” button next to whichever tags you want to add. Alternatively, after you search for a tag, press the Enter/Return key to add the add the first item in the list. Press Enter/Return once more to close the dialog box
27-
2829
!!! warning
2930
Keyboard control and navigation is currently _very_ buggy, but will be improved in future versions.
3031

@@ -51,6 +52,10 @@ Inevitably, some of the files inside your library will be renamed, moved, or del
5152
!!! warning
5253
If multiple matches for a moved file are found (matches are currently defined as files with a matching filename as the original), TagStudio will currently ignore the match groups. Adding a GUI for manual selection, as well as smarter automated relinking, are top priorities for future versions.
5354

55+
## Deleting Tags
56+
57+
To delete a tag from your library, go to File -> Tag Manager, hover over the tag you wish to delete, and click the "-" icon that appears. You will be prompted to make sure you wish to delete this tag from your library and across all file entries.
58+
5459
## Saving the Library
5560

5661
Libraries are saved upon exiting the program. To manually save, select File -> Save Library from the menu bar. To save a backup of your library, select File -> Save Library Backup from the menu bar.

tagstudio/resources/translations/en.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,8 @@
158158
"menu.file.close_library": "&Close Library",
159159
"menu.file.new_library": "New Library",
160160
"menu.file.open_create_library": "&Open/Create Library",
161+
"menu.file.open_recent_library": "Open Recent",
162+
"menu.file.clear_recent_libraries": "Clear Recent",
161163
"menu.file.open_library": "Open Library",
162164
"menu.file.refresh_directories": "&Refresh Directories",
163165
"menu.file.save_backup": "&Save Library Backup",

tagstudio/src/core/constants.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
# Copyright (C) 2025
2+
# Licensed under the GPL-3.0 License.
3+
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
4+
15
VERSION: str = "9.5.0" # Major.Minor.Patch
26
VERSION_BRANCH: str = "EXPERIMENTAL" # Usually "" or "Pre-Release"
37

@@ -11,7 +15,12 @@
1115
)
1216
FONT_SAMPLE_SIZES: list[int] = [10, 15, 20]
1317

14-
TAG_FAVORITE = 1
18+
# NOTE: These were the field IDs used for the "Tags", "Content Tags", and "Meta Tags" fields inside
19+
# the legacy JSON database. These are used to help migrate libraries from JSON to SQLite.
20+
LEGACY_TAG_FIELD_IDS: set[int] = {6, 7, 8}
21+
1522
TAG_ARCHIVED = 0
23+
TAG_FAVORITE = 1
24+
TAG_META = 2
1625
RESERVED_TAG_START = 0
1726
RESERVED_TAG_END = 999

tagstudio/src/core/enums.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
# Copyright (C) 2025
2+
# Licensed under the GPL-3.0 License.
3+
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
4+
15
import enum
26
from typing import Any
37
from uuid import uuid4
@@ -20,10 +24,11 @@ class Theme(str, enum.Enum):
2024
COLOR_DARK_LABEL = "#DD000000"
2125
COLOR_BG = "#65000000"
2226

23-
COLOR_HOVER = "#65AAAAAA"
24-
COLOR_PRESSED = "#65EEEEEE"
25-
COLOR_DISABLED = "#65F39CAA"
26-
COLOR_DISABLED_BG = "#65440D12"
27+
COLOR_HOVER = "#65444444"
28+
COLOR_PRESSED = "#65777777"
29+
COLOR_DISABLED_BG = "#30000000"
30+
COLOR_FORBIDDEN = "#65F39CAA"
31+
COLOR_FORBIDDEN_BG = "#65440D12"
2732

2833

2934
class OpenStatus(enum.IntEnum):
@@ -65,4 +70,4 @@ class LibraryPrefs(DefaultEnum):
6570
IS_EXCLUDE_LIST = True
6671
EXTENSION_LIST: list[str] = [".json", ".xmp", ".aae"]
6772
PAGE_SIZE: int = 500
68-
DB_VERSION: int = 2
73+
DB_VERSION: int = 3

tagstudio/src/core/library/alchemy/db.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
# Copyright (C) 2025
2+
# Licensed under the GPL-3.0 License.
3+
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
4+
15
from pathlib import Path
26

37
import structlog
@@ -44,7 +48,10 @@ def make_tables(engine: Engine) -> None:
4448
autoincrement_val = result.scalar()
4549
if not autoincrement_val or autoincrement_val <= RESERVED_TAG_END:
4650
conn.execute(
47-
text(f"INSERT INTO tags (id, name, color) VALUES ({RESERVED_TAG_END}, 'temp', 1)")
51+
text(
52+
"INSERT INTO tags (id, name, color, is_category) VALUES "
53+
f"({RESERVED_TAG_END}, 'temp', 1, false)"
54+
)
4855
)
4956
conn.execute(text(f"DELETE FROM tags WHERE id = {RESERVED_TAG_END}"))
5057
conn.commit()

tagstudio/src/core/library/alchemy/fields.py

Lines changed: 6 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
# Copyright (C) 2025
2+
# Licensed under the GPL-3.0 License.
3+
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
4+
15
from __future__ import annotations
26

37
from dataclasses import dataclass, field
@@ -11,7 +15,7 @@
1115
from .enums import FieldTypeEnum
1216

1317
if TYPE_CHECKING:
14-
from .models import Entry, Tag, ValueType
18+
from .models import Entry, ValueType
1519

1620

1721
class BaseField(Base):
@@ -75,33 +79,11 @@ def __key(self) -> tuple:
7579
def __eq__(self, value) -> bool:
7680
if isinstance(value, TextField):
7781
return self.__key() == value.__key()
78-
elif isinstance(value, (TagBoxField, DatetimeField)):
82+
elif isinstance(value, DatetimeField):
7983
return False
8084
raise NotImplementedError
8185

8286

83-
class TagBoxField(BaseField):
84-
__tablename__ = "tag_box_fields"
85-
86-
tags: Mapped[set[Tag]] = relationship(secondary="tag_fields")
87-
88-
def __key(self):
89-
return (
90-
self.entry_id,
91-
self.type_key,
92-
)
93-
94-
@property
95-
def value(self) -> None:
96-
"""For interface compatibility with other field types."""
97-
return None
98-
99-
def __eq__(self, value) -> bool:
100-
if isinstance(value, TagBoxField):
101-
return self.__key() == value.__key()
102-
raise NotImplementedError
103-
104-
10587
class DatetimeField(BaseField):
10688
__tablename__ = "datetime_fields"
10789

@@ -133,9 +115,6 @@ class _FieldID(Enum):
133115
URL = DefaultField(id=3, name="URL", type=FieldTypeEnum.TEXT_LINE)
134116
DESCRIPTION = DefaultField(id=4, name="Description", type=FieldTypeEnum.TEXT_LINE)
135117
NOTES = DefaultField(id=5, name="Notes", type=FieldTypeEnum.TEXT_BOX)
136-
TAGS = DefaultField(id=6, name="Tags", type=FieldTypeEnum.TAGS)
137-
TAGS_CONTENT = DefaultField(id=7, name="Content Tags", type=FieldTypeEnum.TAGS, is_default=True)
138-
TAGS_META = DefaultField(id=8, name="Meta Tags", type=FieldTypeEnum.TAGS, is_default=True)
139118
COLLATION = DefaultField(id=9, name="Collation", type=FieldTypeEnum.TEXT_LINE)
140119
DATE = DefaultField(id=10, name="Date", type=FieldTypeEnum.DATETIME)
141120
DATE_CREATED = DefaultField(id=11, name="Date Created", type=FieldTypeEnum.DATETIME)
Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,22 @@
1+
# Copyright (C) 2025
2+
# Licensed under the GPL-3.0 License.
3+
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
4+
15
from sqlalchemy import ForeignKey
26
from sqlalchemy.orm import Mapped, mapped_column
37

48
from .db import Base
59

610

7-
class TagSubtag(Base):
8-
__tablename__ = "tag_subtags"
11+
class TagParent(Base):
12+
__tablename__ = "tag_parents"
913

1014
parent_id: Mapped[int] = mapped_column(ForeignKey("tags.id"), primary_key=True)
1115
child_id: Mapped[int] = mapped_column(ForeignKey("tags.id"), primary_key=True)
1216

1317

14-
class TagField(Base):
15-
__tablename__ = "tag_fields"
18+
class TagEntry(Base):
19+
__tablename__ = "tag_entries"
1620

17-
field_id: Mapped[int] = mapped_column(ForeignKey("tag_box_fields.id"), primary_key=True)
1821
tag_id: Mapped[int] = mapped_column(ForeignKey("tags.id"), primary_key=True)
22+
entry_id: Mapped[int] = mapped_column(ForeignKey("entries.id"), primary_key=True)

0 commit comments

Comments
 (0)