feat: add per-tag category display options - #1470
Conversation
# Conflicts: # src/tagstudio/core/library/alchemy/library.py
|
No worries, it was easy to resolve. |
CyanVoxel
left a comment
There was a problem hiding this comment.
Thank you so much for your work (and patience) on this! Overall I think this is a very good solution to the category issue, and from putting it through its paces I think the implementation should hold up pretty well.
I've got a few nitpicks and one larger comment on the UI:
- The inner markers of radio buttons for parent tags are incorrectly rendered as being highly translucent, and the outline colors are incorrect. The radio buttons in the categories seem to be correct, though.
- Removing a parent tag that is also a property while another parent tag also providing that category in its hierarchy will incorrectly remove the category from the list, until the edit modal is reopened
- The library database(s) affected after running pytest locally should be committed along with this
- A minor version bump is open to an edge case where if you upgrade library to 301, open it in a TS version for 300, delete a tag that's referenced in the category_exclusions table, then open back up in 301, it'll have a row referencing a deleted tag - so this'll probably need a major version bump to 400 just to be safe
Then for the UI, I feel there needs to be additional visual separation and clarification as to what this category section actually is and how it relates to parent tags. While a larger overhaul of the tag modal is probably due, for now I've put together a patch file with some tweaks to the existing layout here that should hopefully ward off most potential confusion for users: ui-tweaks.patch
Side Note: I'm targeting this PR for v9.6.4, since v9.6.3 will come out soon and act as a critical bugfix patch along with other finished small QoL improvements. Once that's shipped, this'll be the first PR to get pulled for the next update cycle.
I also anticipate #1471 to cause a tiny conflict with the migrations, so I apologize for the inconvenience on that when it comes...
|
I've fixed two more bugs I found and addressed all your feedback except for
since I can't reproduce it. As I've understood it, the hierarchy looks like this flowchart TD
A["Parent (Category)"] --> B[Child 1];
A--> C[Child 2];
B --> D[Grandchild]
C --> D
and removing either of the children should remove the category. I'm also getting Not Found for your UI patch, could you upload it again? |
The issue occurs in a hierarchy like this, where instead of 2 children from the same parent it's when the parent and child are both on a grandchild tag:
If you take away Parent from the Grandchild tag, it also takes away the category even though Child is still providing it via inheriting from Parent. While replicating this I was also able to create a state where category information remained after removing parents, though I'm having difficulty reproducing this state and am unsure of whether or not it's a UI bug or a library bug...
That's odd, I can still download it on my end but I'll try again: ui-tweaks.patch And just in case here it is as a code block:diff --git a/src/tagstudio/qt/mixed/build_tag.py b/src/tagstudio/qt/mixed/build_tag.py
index 631e3fce..e411f016 100644
--- a/src/tagstudio/qt/mixed/build_tag.py
+++ b/src/tagstudio/qt/mixed/build_tag.py
@@ -14,6 +14,7 @@ from PySide6.QtWidgets import (
QButtonGroup,
QCheckBox,
QFrame,
+ QGraphicsOpacityEffect,
QHBoxLayout,
QLabel,
QLineEdit,
@@ -90,7 +91,7 @@ class BuildTagPanel(ModalContent):
self.exclusion_ids: set[int] = set()
self.aliases: list[TagAlias] = []
- self.setMinimumSize(300, 460)
+ self.setMinimumSize(300, 640)
self.root_layout = QVBoxLayout(self)
self.root_layout.setContentsMargins(6, 0, 6, 0)
self.root_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
@@ -98,7 +99,6 @@ class BuildTagPanel(ModalContent):
# Name -----------------------------------------------------------------
self.name_widget = QWidget()
self.name_layout = QVBoxLayout(self.name_widget)
- self.name_layout.setStretch(1, 1)
self.name_layout.setContentsMargins(0, 0, 0, 0)
self.name_layout.setSpacing(0)
self.name_layout.setAlignment(Qt.AlignmentFlag.AlignLeft)
@@ -113,7 +113,6 @@ class BuildTagPanel(ModalContent):
# Shorthand ------------------------------------------------------------
self.shorthand_widget = QWidget()
self.shorthand_layout = QVBoxLayout(self.shorthand_widget)
- self.shorthand_layout.setStretch(1, 1)
self.shorthand_layout.setContentsMargins(0, 0, 0, 0)
self.shorthand_layout.setSpacing(0)
self.shorthand_layout.setAlignment(Qt.AlignmentFlag.AlignLeft)
@@ -125,7 +124,6 @@ class BuildTagPanel(ModalContent):
# Aliases --------------------------------------------------------------
self.aliases_widget = QWidget()
self.aliases_layout = QVBoxLayout(self.aliases_widget)
- self.aliases_layout.setStretch(1, 1)
self.aliases_layout.setContentsMargins(0, 0, 0, 0)
self.aliases_layout.setSpacing(0)
self.aliases_layout.setAlignment(Qt.AlignmentFlag.AlignLeft)
@@ -146,16 +144,14 @@ class BuildTagPanel(ModalContent):
# Parent Tags ----------------------------------------------------------
self.parent_tags_widget = QWidget()
- self.parent_tags_widget.setMinimumHeight(128)
self.parent_tags_layout = QVBoxLayout(self.parent_tags_widget)
- self.parent_tags_layout.setStretch(1, 1)
self.parent_tags_layout.setContentsMargins(0, 0, 0, 0)
self.parent_tags_layout.setSpacing(0)
self.parent_tags_layout.setAlignment(Qt.AlignmentFlag.AlignLeft)
self.disam_button_group = QButtonGroup(self)
self.disam_button_group.setExclusive(False)
- self.parent_tags_title = QLabel(Translations["tag.parent_tags"])
+ self.parent_tags_title = QLabel(header(Translations["tag.parent_tags"], 3))
self.parent_tags_layout.addWidget(self.parent_tags_title)
self.scroll_contents = QWidget()
self.parent_tags_scroll_layout = QVBoxLayout(self.scroll_contents)
@@ -188,22 +184,25 @@ class BuildTagPanel(ModalContent):
# Categories -----------------------------------------------------------
self.category_widget = QWidget()
- self.category_widget.setMinimumHeight(128)
-
self.category_layout = QVBoxLayout(self.category_widget)
- self.category_layout.setStretch(1, 1)
self.category_layout.setContentsMargins(0, 0, 0, 0)
self.category_layout.setSpacing(0)
self.category_layout.setAlignment(Qt.AlignmentFlag.AlignLeft)
- self.category_layout.addWidget(QLabel(Translations["tag.categories"]))
+ self.category_layout.addWidget(QLabel(header(Translations["tag.categories"], 3)))
- self.category_scroll_contents = QWidget()
+ category_subtitle = QLabel(Translations["tag.categories.subtitle"])
+ opacity_effect = QGraphicsOpacityEffect(self)
+ opacity_effect.setOpacity(0.5)
+ category_subtitle.setGraphicsEffect(opacity_effect)
+ self.category_layout.addWidget(category_subtitle)
+ self.category_scroll_contents = QWidget()
self.category_scroll_layout = QVBoxLayout(self.category_scroll_contents)
self.category_scroll_layout.setContentsMargins(6, 6, 6, 0)
self.category_scroll_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
self.category_scroll_area = QScrollArea()
self.category_scroll_area.setFocusPolicy(Qt.FocusPolicy.NoFocus)
self.category_scroll_area.setWidgetResizable(True)
self.category_scroll_area.setFrameShadow(QFrame.Shadow.Plain)
@@ -214,11 +213,10 @@ class BuildTagPanel(ModalContent):
# Color ----------------------------------------------------------------
self.color_widget = QWidget()
self.color_layout = QVBoxLayout(self.color_widget)
- self.color_layout.setStretch(1, 1)
self.color_layout.setContentsMargins(0, 0, 0, 6)
self.color_layout.setSpacing(6)
self.color_layout.setAlignment(Qt.AlignmentFlag.AlignLeft)
- self.color_title = QLabel(Translations["tag.color"])
+ self.color_title = QLabel(header(Translations["tag.color"], 3))
self.color_layout.addWidget(self.color_title)
self.color_button: TagColorPreview
try:
@@ -242,7 +240,6 @@ class BuildTagPanel(ModalContent):
# Category -------------------------------------------------------------
self.cat_widget = QWidget()
self.cat_layout = QHBoxLayout(self.cat_widget)
- self.cat_layout.setStretch(1, 1)
self.cat_layout.setContentsMargins(0, 0, 0, 0)
self.cat_layout.setSpacing(6)
self.cat_layout.setAlignment(Qt.AlignmentFlag.AlignLeft)
@@ -256,7 +253,6 @@ class BuildTagPanel(ModalContent):
# Hidden ---------------------------------------------------------------
self.hidden_widget = QWidget()
self.hidden_layout = QHBoxLayout(self.hidden_widget)
- self.hidden_layout.setStretch(1, 1)
self.hidden_layout.setContentsMargins(0, 0, 0, 0)
self.hidden_layout.setSpacing(6)
self.hidden_layout.setAlignment(Qt.AlignmentFlag.AlignLeft)
@@ -271,17 +267,33 @@ class BuildTagPanel(ModalContent):
self.root_layout.addWidget(self.name_widget)
self.root_layout.addWidget(self.shorthand_widget)
self.root_layout.addWidget(self.aliases_widget)
- self.root_layout.addWidget(self.aliases_table)
+ self.root_layout.addWidget(self.aliases_table, stretch=1)
self.root_layout.addWidget(self.aliases_add_button)
- self.root_layout.addWidget(self.parent_tags_widget)
+ self._add_spaced_separator()
+ self.root_layout.addWidget(self.parent_tags_widget, stretch=1)
+ self._add_spaced_separator()
self.root_layout.addWidget(self.category_widget)
+ self._add_spaced_separator()
self.root_layout.addWidget(self.color_widget)
+ self._add_spaced_separator()
self.root_layout.addWidget(QLabel(header(Translations["tag.properties"], 3)))
self.root_layout.addWidget(self.cat_widget)
self.root_layout.addWidget(self.hidden_widget)
self.set_tag(tag or Tag(name=Translations["tag.new"]))
+ def _add_spaced_separator(self) -> None:
+ sep = QFrame()
+ sep.setFrameShape(QFrame.Shape.HLine)
+ sep.setFrameShadow(QFrame.Shadow.Plain)
+ opacity_effect = QGraphicsOpacityEffect(self)
+ opacity_effect.setOpacity(0.1)
+ sep.setGraphicsEffect(opacity_effect)
+
+ self.root_layout.addSpacing(6)
+ self.root_layout.addWidget(sep)
+ self.root_layout.addSpacing(6)
+
def backspace(self):
focused_widget = QApplication.focusWidget()
row = self.aliases_table.rowCount()
diff --git a/src/tagstudio/resources/translations/en.json b/src/tagstudio/resources/translations/en.json
index 0dad48c7..ad1f29d0 100644
--- a/src/tagstudio/resources/translations/en.json
+++ b/src/tagstudio/resources/translations/en.json
@@ -384,7 +384,8 @@
"tag.add.plural": "Add Tags",
"tag.aliases": "Aliases",
"tag.all_tags": "All Tags",
- "tag.categories": "Categories",
+ "tag.categories": "Category Visibility",
+ "tag.categories.subtitle": "Inherited from Parent Tags",
"tag.categories.tooltip": "Show tag in this category",
"tag.choose_color": "Choose Tag Color",
"tag.color": "Color", |
|
I've applied your patch and fixed the issue with removing categories inherited both directly and indirectly. |
CyanVoxel
left a comment
There was a problem hiding this comment.
Everything looks good, thank you so much for your work on this! It's seriously so nice to have this sorted out, the category system never felt truly complete until now!


Summary
Add per-tag category display options
Resolves #1335
Supersedes #1336
By default, tags are displayed in all inherited categories, leaving the current behavior unchanged.
This extends the
BuildTagPanelwith the options to exclude a tag from being displayed in one, multiple or all possible categories.The exclusions are stored in the new
category_exclusionsjoin table.Also bumps the database version to 301.
Demo
2026-04-02.21-12-53.mp4
Tasks Completed