diff --git a/tagstudio/resources/translations/en.json b/tagstudio/resources/translations/en.json
index 7aee729c..8076d87a 100644
--- a/tagstudio/resources/translations/en.json
+++ b/tagstudio/resources/translations/en.json
@@ -2,6 +2,7 @@
"app.git": "Git Commit",
"app.pre_release": "Pre-Release",
"app.title": "{base_title} - Library '{library_dir}'",
+ "color.title.no_color": "No Color",
"drop_import.description": "The following files have filenames already exist in the library",
"drop_import.duplicates_choice.plural": "The following {count} files have filenames that already exist in the library.",
"drop_import.duplicates_choice.singular": "The following file has a filename that already exists in the library.",
@@ -88,6 +89,7 @@
"generic.filename": "Filename",
"generic.navigation.back": "Back",
"generic.navigation.next": "Next",
+ "generic.none": "None",
"generic.overwrite_alt": "&Overwrite",
"generic.overwrite": "Overwrite",
"generic.paste": "Paste",
@@ -131,7 +133,7 @@
"json_migration.heading.paths": "Paths:",
"json_migration.heading.shorthands": "Shorthands:",
"json_migration.heading.tags": "Tags:",
- "json_migration.info.description": "Library save files created with TagStudio versions 9.4 and below will need to be migrated to the new v9.5+ format.
What you need to know:
- Your existing library save file will NOT be deleted
- Your personal files will NOT be deleted, moved, or modified
- The new v9.5+ save format can not be opened in earlier versions of TagStudio
",
+ "json_migration.info.description": "Library save files created with TagStudio versions 9.4 and below will need to be migrated to the new v9.5+ format.
What you need to know:
- Your existing library save file will NOT be deleted
- Your personal files will NOT be deleted, moved, or modified
- The new v9.5+ save format can not be opened in earlier versions of TagStudio
What's changed:
- \"Tag Fields\" have been replaced by \"Tags Categories\". Instead of adding tags to fields first, tags now get added directly to file entries. They're then automatically organized into categories based on parent tags marked with the new \"Is Category\" property in the tag editing menu. Any tag can be marked as a category, and child tags will sort themselves underneath parent tags marked as categories. The \"Favorite\" and \"Archived\" tags now inherit from a new \"Meta Tags\" tag which is marked as a category by default.
- Tag colors have been tweaked and expanded upon. Some colors have been renamed or consolidated, however all tag colors will still convert to exact or close matches in v9.5.
",
"json_migration.migrating_files_entries": "Migrating {entries:,d} File Entries...",
"json_migration.migration_complete_with_discrepancies": "Migration Complete, Discrepancies Found",
"json_migration.migration_complete": "Migration Complete!",
@@ -202,6 +204,7 @@
"tag.add.plural": "Add Tags",
"tag.add": "Add Tag",
"tag.aliases": "Aliases",
+ "tag.choose_color": "Choose Tag Color",
"tag.color": "Color",
"tag.confirm_delete": "Are you sure you want to delete the tag \"{tag_name}\"?",
"tag.create": "Create Tag",
diff --git a/tagstudio/src/core/enums.py b/tagstudio/src/core/enums.py
index 88c4e7e9..dee651e5 100644
--- a/tagstudio/src/core/enums.py
+++ b/tagstudio/src/core/enums.py
@@ -71,4 +71,4 @@ class LibraryPrefs(DefaultEnum):
IS_EXCLUDE_LIST = True
EXTENSION_LIST: list[str] = [".json", ".xmp", ".aae"]
PAGE_SIZE: int = 500
- DB_VERSION: int = 3
+ DB_VERSION: int = 4
diff --git a/tagstudio/src/core/library/alchemy/db.py b/tagstudio/src/core/library/alchemy/db.py
index 8cc16828..61269c2c 100644
--- a/tagstudio/src/core/library/alchemy/db.py
+++ b/tagstudio/src/core/library/alchemy/db.py
@@ -49,8 +49,8 @@ def make_tables(engine: Engine) -> None:
if not autoincrement_val or autoincrement_val <= RESERVED_TAG_END:
conn.execute(
text(
- "INSERT INTO tags (id, name, color, is_category) VALUES "
- f"({RESERVED_TAG_END}, 'temp', 1, false)"
+ "INSERT INTO tags (id, name, color_namespace, color_slug, is_category) VALUES "
+ f"({RESERVED_TAG_END}, 'temp', NULL, NULL, false)"
)
)
conn.execute(text(f"DELETE FROM tags WHERE id = {RESERVED_TAG_END}"))
diff --git a/tagstudio/src/core/library/alchemy/default_color_groups.py b/tagstudio/src/core/library/alchemy/default_color_groups.py
new file mode 100644
index 00000000..d489dae1
--- /dev/null
+++ b/tagstudio/src/core/library/alchemy/default_color_groups.py
@@ -0,0 +1,539 @@
+# Copyright (C) 2025
+# Licensed under the GPL-3.0 License.
+# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
+
+
+import structlog
+
+from .models import Namespace, TagColorGroup
+
+logger = structlog.get_logger(__name__)
+
+
+def namespaces() -> list[Namespace]:
+ tagstudio_standard = Namespace("tagstudio-standard", "TagStudio Standard")
+ tagstudio_pastels = Namespace("tagstudio-pastels", "TagStudio Pastels")
+ tagstudio_shades = Namespace("tagstudio-shades", "TagStudio Shades")
+ tagstudio_earth_tones = Namespace("tagstudio-earth-tones", "TagStudio Earth Tones")
+ tagstudio_grayscale = Namespace("tagstudio-grayscale", "TagStudio Grayscale")
+ tagstudio_neon = Namespace("tagstudio-neon", "TagStudio Neon")
+ return [
+ tagstudio_standard,
+ tagstudio_pastels,
+ tagstudio_shades,
+ tagstudio_earth_tones,
+ tagstudio_grayscale,
+ tagstudio_neon,
+ ]
+
+
+def json_to_sql_color(json_color: str) -> tuple[str | None, str | None]:
+ """Convert a color string from a <=9.4 JSON library to a 9.5+ (namespace, slug) tuple."""
+ json_color_ = json_color.lower()
+ match json_color_:
+ case "black":
+ return ("tagstudio-grayscale", "black")
+ case "dark gray":
+ return ("tagstudio-grayscale", "dark-gray")
+ case "gray":
+ return ("tagstudio-grayscale", "gray")
+ case "light gray":
+ return ("tagstudio-grayscale", "light-gray")
+ case "white":
+ return ("tagstudio-grayscale", "white")
+ case "light pink":
+ return ("tagstudio-pastels", "light-pink")
+ case "pink":
+ return ("tagstudio-standard", "pink")
+ case "magenta":
+ return ("tagstudio-standard", "magenta")
+ case "red":
+ return ("tagstudio-standard", "red")
+ case "red orange":
+ return ("tagstudio-standard", "red-orange")
+ case "salmon":
+ return ("tagstudio-pastels", "salmon")
+ case "orange":
+ return ("tagstudio-standard", "orange")
+ case "yellow orange":
+ return ("tagstudio-standard", "amber")
+ case "yellow":
+ return ("tagstudio-standard", "yellow")
+ case "mint":
+ return ("tagstudio-pastels", "mint")
+ case "lime":
+ return ("tagstudio-standard", "lime")
+ case "light green":
+ return ("tagstudio-pastels", "light-green")
+ case "green":
+ return ("tagstudio-standard", "green")
+ case "teal":
+ return ("tagstudio-standard", "teal")
+ case "cyan":
+ return ("tagstudio-standard", "cyan")
+ case "light blue":
+ return ("tagstudio-pastels", "light-blue")
+ case "blue":
+ return ("tagstudio-standard", "blue")
+ case "blue violet":
+ return ("tagstudio-shades", "navy")
+ case "violet":
+ return ("tagstudio-standard", "indigo")
+ case "purple":
+ return ("tagstudio-standard", "purple")
+ case "peach":
+ return ("tagstudio-earth-tones", "peach")
+ case "brown":
+ return ("tagstudio-earth-tones", "brown")
+ case "lavender":
+ return ("tagstudio-pastels", "lavender")
+ case "blonde":
+ return ("tagstudio-earth-tones", "blonde")
+ case "auburn":
+ return ("tagstudio-shades", "auburn")
+ case "light brown":
+ return ("tagstudio-earth-tones", "light-brown")
+ case "dark brown":
+ return ("tagstudio-earth-tones", "dark-brown")
+ case "cool gray":
+ return ("tagstudio-earth-tones", "cool-gray")
+ case "warm gray":
+ return ("tagstudio-earth-tones", "warm-gray")
+ case "olive":
+ return ("tagstudio-shades", "olive")
+ case "berry":
+ return ("tagstudio-shades", "berry")
+ case _:
+ return (None, None)
+
+
+def standard() -> list[TagColorGroup]:
+ red = TagColorGroup(
+ slug="red",
+ namespace="tagstudio-standard",
+ name="Red",
+ primary="#E22C3C",
+ )
+ red_orange = TagColorGroup(
+ slug="red-orange",
+ namespace="tagstudio-standard",
+ name="Red Orange",
+ primary="#E83726",
+ )
+ orange = TagColorGroup(
+ slug="orange",
+ namespace="tagstudio-standard",
+ name="Orange",
+ primary="#ED6022",
+ )
+ amber = TagColorGroup(
+ slug="amber",
+ namespace="tagstudio-standard",
+ name="Amber",
+ primary="#FA9A2C",
+ )
+ yellow = TagColorGroup(
+ slug="yellow",
+ namespace="tagstudio-standard",
+ name="Yellow",
+ primary="#FFD63D",
+ )
+ lime = TagColorGroup(
+ slug="lime",
+ namespace="tagstudio-standard",
+ name="Lime",
+ primary="#92E649",
+ )
+ green = TagColorGroup(
+ slug="green",
+ namespace="tagstudio-standard",
+ name="Green",
+ primary="#45D649",
+ )
+ teal = TagColorGroup(
+ slug="teal",
+ namespace="tagstudio-standard",
+ name="Teal",
+ primary="#22D589",
+ )
+ cyan = TagColorGroup(
+ slug="cyan",
+ namespace="tagstudio-standard",
+ name="Cyan",
+ primary="#3DDBDB",
+ )
+ blue = TagColorGroup(
+ slug="blue",
+ namespace="tagstudio-standard",
+ name="Blue",
+ primary="#3B87F0",
+ )
+ indigo = TagColorGroup(
+ slug="indigo",
+ namespace="tagstudio-standard",
+ name="Indigo",
+ primary="#874FF5",
+ )
+ purple = TagColorGroup(
+ slug="purple",
+ namespace="tagstudio-standard",
+ name="Purple",
+ primary="#BB4FF0",
+ )
+ magenta = TagColorGroup(
+ slug="magenta",
+ namespace="tagstudio-standard",
+ name="Magenta",
+ primary="#F64680",
+ )
+ pink = TagColorGroup(
+ slug="pink",
+ namespace="tagstudio-standard",
+ name="Pink",
+ primary="#FF62AF",
+ )
+ return [
+ red,
+ red_orange,
+ orange,
+ amber,
+ yellow,
+ lime,
+ green,
+ teal,
+ cyan,
+ blue,
+ indigo,
+ purple,
+ pink,
+ magenta,
+ ]
+
+
+def pastels() -> list[TagColorGroup]:
+ coral = TagColorGroup(
+ slug="coral",
+ namespace="tagstudio-pastels",
+ name="Coral",
+ primary="#F2525F",
+ )
+ salmon = TagColorGroup(
+ slug="salmon",
+ namespace="tagstudio-pastels",
+ name="Salmon",
+ primary="#F66348",
+ )
+ light_orange = TagColorGroup(
+ slug="light-orange",
+ namespace="tagstudio-pastels",
+ name="Light Orange",
+ primary="#FF9450",
+ )
+ light_amber = TagColorGroup(
+ slug="light-amber",
+ namespace="tagstudio-pastels",
+ name="Light Amber",
+ primary="#FFBA57",
+ )
+ light_yellow = TagColorGroup(
+ slug="light-yellow",
+ namespace="tagstudio-pastels",
+ name="Light Yellow",
+ primary="#FFE173",
+ )
+ light_lime = TagColorGroup(
+ slug="light-lime",
+ namespace="tagstudio-pastels",
+ name="Light Lime",
+ primary="#C9FF7A",
+ )
+ light_green = TagColorGroup(
+ slug="light-green",
+ namespace="tagstudio-pastels",
+ name="Light Green",
+ primary="#81FF76",
+ )
+ mint = TagColorGroup(
+ slug="mint",
+ namespace="tagstudio-pastels",
+ name="Mint",
+ primary="#68FFB4",
+ )
+ sky_blue = TagColorGroup(
+ slug="sky-blue",
+ namespace="tagstudio-pastels",
+ name="Sky Blue",
+ primary="#8EFFF4",
+ )
+ light_blue = TagColorGroup(
+ slug="light-blue",
+ namespace="tagstudio-pastels",
+ name="Light Blue",
+ primary="#64C6FF",
+ )
+ lavender = TagColorGroup(
+ slug="lavender",
+ namespace="tagstudio-pastels",
+ name="Lavender",
+ primary="#908AF6",
+ )
+ lilac = TagColorGroup(
+ slug="lilac",
+ namespace="tagstudio-pastels",
+ name="Lilac",
+ primary="#DF95FF",
+ )
+ light_pink = TagColorGroup(
+ slug="light-pink",
+ namespace="tagstudio-pastels",
+ name="Light Pink",
+ primary="#FF87BA",
+ )
+ return [
+ coral,
+ salmon,
+ light_orange,
+ light_amber,
+ light_yellow,
+ light_lime,
+ light_green,
+ mint,
+ sky_blue,
+ light_blue,
+ lavender,
+ lilac,
+ light_pink,
+ ]
+
+
+def shades() -> list[TagColorGroup]:
+ auburn = TagColorGroup(
+ slug="auburn",
+ namespace="tagstudio-shades",
+ name="Auburn",
+ primary="#A13220",
+ )
+ olive = TagColorGroup(
+ slug="olive",
+ namespace="tagstudio-shades",
+ name="Olive",
+ primary="#4C652E",
+ )
+ navy = TagColorGroup(
+ slug="navy",
+ namespace="tagstudio-shades",
+ name="Navy",
+ primary="#104B98",
+ )
+ berry = TagColorGroup(
+ slug="berry",
+ namespace="tagstudio-shades",
+ name="Berry",
+ primary="#9F2AA7",
+ )
+ return [auburn, olive, navy, berry]
+
+
+def earth_tones() -> list[TagColorGroup]:
+ dark_brown = TagColorGroup(
+ slug="dark-brown",
+ namespace="tagstudio-earth-tones",
+ name="Dark Brown",
+ primary="#4C2315",
+ )
+ brown = TagColorGroup(
+ slug="brown",
+ namespace="tagstudio-earth-tones",
+ name="Brown",
+ primary="#823216",
+ )
+ light_brown = TagColorGroup(
+ slug="light-brown",
+ namespace="tagstudio-earth-tones",
+ name="Light Brown",
+ primary="#BE5B2D",
+ )
+ blonde = TagColorGroup(
+ slug="blonde",
+ namespace="tagstudio-earth-tones",
+ name="Blonde",
+ primary="#EFC664",
+ )
+ peach = TagColorGroup(
+ slug="peach",
+ namespace="tagstudio-earth-tones",
+ name="Peach",
+ primary="#F1C69C",
+ )
+ warm_gray = TagColorGroup(
+ slug="warm-gray",
+ namespace="tagstudio-earth-tones",
+ name="Warm Gray",
+ primary="#625550",
+ )
+ cool_gray = TagColorGroup(
+ slug="cool-gray",
+ namespace="tagstudio-earth-tones",
+ name="Cool Gray",
+ primary="#515768",
+ )
+ return [dark_brown, brown, light_brown, blonde, peach, warm_gray, cool_gray]
+
+
+def grayscale() -> list[TagColorGroup]:
+ black = TagColorGroup(
+ slug="black",
+ namespace="tagstudio-grayscale",
+ name="Black",
+ primary="#111018",
+ )
+ dark_gray = TagColorGroup(
+ slug="dark-gray",
+ namespace="tagstudio-grayscale",
+ name="Dark Gray",
+ primary="#242424",
+ )
+ gray = TagColorGroup(
+ slug="gray",
+ namespace="tagstudio-grayscale",
+ name="Gray",
+ primary="#53525A",
+ )
+ light_gray = TagColorGroup(
+ slug="light-gray",
+ namespace="tagstudio-grayscale",
+ name="Light Gray",
+ primary="#AAAAAA",
+ )
+ white = TagColorGroup(
+ slug="white",
+ namespace="tagstudio-grayscale",
+ name="White",
+ primary="#F2F1F8",
+ )
+ return [black, dark_gray, gray, light_gray, white]
+
+
+def neon() -> list[TagColorGroup]:
+ neon_red = TagColorGroup(
+ slug="neon-red",
+ namespace="tagstudio-neon",
+ name="Neon Red",
+ primary="#180607",
+ secondary="#E22C3C",
+ )
+ neon_red_orange = TagColorGroup(
+ slug="neon-red-orange",
+ namespace="tagstudio-neon",
+ name="Neon Red Orange",
+ primary="#220905",
+ secondary="#E83726",
+ )
+ neon_orange = TagColorGroup(
+ slug="neon-orange",
+ namespace="tagstudio-neon",
+ name="Neon Orange",
+ primary="#1F0D05",
+ secondary="#ED6022",
+ )
+ neon_amber = TagColorGroup(
+ slug="neon-amber",
+ namespace="tagstudio-neon",
+ name="Neon Amber",
+ primary="#251507",
+ secondary="#FA9A2C",
+ )
+ neon_yellow = TagColorGroup(
+ slug="neon-yellow",
+ namespace="tagstudio-neon",
+ name="Neon Yellow",
+ primary="#2B1C0B",
+ secondary="#FFD63D",
+ )
+ neon_lime = TagColorGroup(
+ slug="neon-lime",
+ namespace="tagstudio-neon",
+ name="Neon Lime",
+ primary="#1B220C",
+ secondary="#92E649",
+ )
+ neon_green = TagColorGroup(
+ slug="neon-green",
+ namespace="tagstudio-neon",
+ name="Neon Green",
+ primary="#091610",
+ secondary="#45D649",
+ )
+ neon_teal = TagColorGroup(
+ slug="neon-teal",
+ namespace="tagstudio-neon",
+ name="Neon Teal",
+ primary="#09191D",
+ secondary="#22D589",
+ )
+ neon_cyan = TagColorGroup(
+ slug="neon-cyan",
+ namespace="tagstudio-neon",
+ name="Neon Cyan",
+ primary="#0B191C",
+ secondary="#3DDBDB",
+ )
+ neon_blue = TagColorGroup(
+ slug="neon-blue",
+ namespace="tagstudio-neon",
+ name="Neon Blue",
+ primary="#09101C",
+ secondary="#3B87F0",
+ )
+ neon_indigo = TagColorGroup(
+ slug="neon-indigo",
+ namespace="tagstudio-neon",
+ name="Neon Indigo",
+ primary="#150B24",
+ secondary="#874FF5",
+ )
+ neon_purple = TagColorGroup(
+ slug="neon-purple",
+ namespace="tagstudio-neon",
+ name="Neon Purple",
+ primary="#1E0B26",
+ secondary="#BB4FF0",
+ )
+ neon_magenta = TagColorGroup(
+ slug="neon-magenta",
+ namespace="tagstudio-neon",
+ name="Neon Magenta",
+ primary="#220A13",
+ secondary="#F64680",
+ )
+ neon_pink = TagColorGroup(
+ slug="neon-pink",
+ namespace="tagstudio-neon",
+ name="Neon Pink",
+ primary="#210E15",
+ secondary="#FF62AF",
+ )
+ neon_white = TagColorGroup(
+ slug="neon-white",
+ namespace="tagstudio-neon",
+ name="Neon White",
+ primary="#131315",
+ secondary="#F2F1F8",
+ )
+ return [
+ neon_red,
+ neon_red_orange,
+ neon_orange,
+ neon_amber,
+ neon_yellow,
+ neon_lime,
+ neon_green,
+ neon_teal,
+ neon_cyan,
+ neon_blue,
+ neon_indigo,
+ neon_purple,
+ neon_pink,
+ neon_magenta,
+ neon_white,
+ ]
diff --git a/tagstudio/src/core/library/alchemy/enums.py b/tagstudio/src/core/library/alchemy/enums.py
index 137f0edb..952b81cd 100644
--- a/tagstudio/src/core/library/alchemy/enums.py
+++ b/tagstudio/src/core/library/alchemy/enums.py
@@ -8,7 +8,7 @@ from src.core.query_lang import Constraint, ConstraintType, Parser
MAX_SQL_VARIABLES = 32766 # 32766 is the max sql bind parameter count as defined here: https://github.com/sqlite/sqlite/blob/master/src/sqliteLimit.h#L140
-class TagColor(enum.IntEnum):
+class TagColorEnum(enum.IntEnum):
DEFAULT = 1
BLACK = 2
DARK_GRAY = 3
@@ -48,11 +48,11 @@ class TagColor(enum.IntEnum):
OLIVE = 37
@staticmethod
- def get_color_from_str(color_name: str) -> "TagColor":
- for color in TagColor:
+ def get_color_from_str(color_name: str) -> "TagColorEnum":
+ for color in TagColorEnum:
if color.name == color_name.upper().replace(" ", "_"):
return color
- return TagColor.DEFAULT
+ return TagColorEnum.DEFAULT
class ItemType(enum.Enum):
diff --git a/tagstudio/src/core/library/alchemy/library.py b/tagstudio/src/core/library/alchemy/library.py
index 5b098623..7a267a1f 100644
--- a/tagstudio/src/core/library/alchemy/library.py
+++ b/tagstudio/src/core/library/alchemy/library.py
@@ -53,8 +53,9 @@ from ...constants import (
TS_FOLDER_NAME,
)
from ...enums import LibraryPrefs
+from . import default_color_groups
from .db import make_tables
-from .enums import MAX_SQL_VARIABLES, FieldTypeEnum, FilterState, SortingModeEnum, TagColor
+from .enums import MAX_SQL_VARIABLES, FieldTypeEnum, FilterState, SortingModeEnum
from .fields import (
BaseField,
DatetimeField,
@@ -62,7 +63,7 @@ from .fields import (
_FieldID,
)
from .joins import TagEntry, TagParent
-from .models import Entry, Folder, Preferences, Tag, TagAlias, ValueType
+from .models import Entry, Folder, Namespace, Preferences, Tag, TagAlias, TagColorGroup, ValueType
from .visitors import SQLBoolExpressionBuilder
logger = structlog.get_logger(__name__)
@@ -93,7 +94,8 @@ def get_default_tags() -> tuple[Tag, ...]:
name="Archived",
aliases={TagAlias(name="Archive")},
parent_tags={meta_tag},
- color=TagColor.RED,
+ color_slug="red",
+ color_namespace="tagstudio-standard",
)
favorite_tag = Tag(
id=TAG_FAVORITE,
@@ -103,7 +105,8 @@ def get_default_tags() -> tuple[Tag, ...]:
TagAlias(name="Favorites"),
},
parent_tags={meta_tag},
- color=TagColor.YELLOW,
+ color_slug="yellow",
+ color_namespace="tagstudio-standard",
)
return archive_tag, favorite_tag, meta_tag
@@ -179,18 +182,23 @@ class Library:
# Tags
for tag in json_lib.tags:
+ color_namespace, color_slug = default_color_groups.json_to_sql_color(tag.color)
self.add_tag(
Tag(
id=tag.id,
name=tag.name,
shorthand=tag.shorthand,
- color=TagColor.get_color_from_str(tag.color),
+ color_namespace=color_namespace,
+ color_slug=color_slug,
)
)
# Apply user edits to built-in JSON tags.
if tag.id in range(RESERVED_TAG_START, RESERVED_TAG_END + 1):
updated_tag = self.get_tag(tag.id)
- updated_tag.color = TagColor.get_color_from_str(tag.color)
+ if not updated_tag:
+ continue
+ updated_tag.color_namespace = color_namespace
+ updated_tag.color_slug = color_slug
self.update_tag(updated_tag) # NOTE: This just calls add_tag?
# Tag Aliases
@@ -292,7 +300,34 @@ class Library:
with Session(self.engine) as session:
make_tables(self.engine)
- # Add default tags to new libraries only.
+ # TODO: Determine a good way of updating built-in data after updates.
+
+ # Add default tag color namespaces.
+ if is_new:
+ namespaces = default_color_groups.namespaces()
+ try:
+ session.add_all(namespaces)
+ session.commit()
+ except IntegrityError as e:
+ logger.error("[Library] Couldn't add default tag color namespaces", error=e)
+ session.rollback()
+
+ # Add default tag colors.
+ if is_new:
+ tag_colors: list[TagColorGroup] = default_color_groups.standard()
+ tag_colors += default_color_groups.pastels()
+ tag_colors += default_color_groups.shades()
+ tag_colors += default_color_groups.grayscale()
+ tag_colors += default_color_groups.earth_tones()
+ tag_colors += default_color_groups.neon()
+ try:
+ session.add_all(tag_colors)
+ session.commit()
+ except IntegrityError as e:
+ logger.error("[Library] Couldn't add default tag colors", error=e)
+ session.rollback()
+
+ # Add default tags.
if is_new:
tags = get_default_tags()
try:
@@ -981,10 +1016,12 @@ class Library:
return target_path
- def get_tag(self, tag_id: int) -> Tag:
+ def get_tag(self, tag_id: int) -> Tag | None:
with Session(self.engine) as session:
tags_query = select(Tag).options(
- selectinload(Tag.parent_tags), selectinload(Tag.aliases)
+ selectinload(Tag.parent_tags),
+ selectinload(Tag.aliases),
+ joinedload(Tag.color),
)
tag = session.scalar(tags_query.where(Tag.id == tag_id))
@@ -1006,12 +1043,19 @@ class Library:
)
return session.scalar(statement)
- def get_alias(self, tag_id: int, alias_id: int) -> TagAlias:
+ def get_alias(self, tag_id: int, alias_id: int) -> TagAlias | None:
with Session(self.engine) as session:
alias_query = select(TagAlias).where(TagAlias.id == alias_id, TagAlias.tag_id == tag_id)
- alias = session.scalar(alias_query.where(TagAlias.id == alias_id))
- return alias
+ return session.scalar(alias_query.where(TagAlias.id == alias_id))
+
+ def get_tag_color(self, slug: str, namespace: str) -> TagColorGroup | None:
+ with Session(self.engine) as session:
+ statement = select(TagColorGroup).where(
+ and_(TagColorGroup.slug == slug, TagColorGroup.namespace == namespace)
+ )
+
+ return session.scalar(statement)
def add_parent_tag(self, parent_id: int, child_id: int) -> bool:
if parent_id == child_id:
@@ -1144,3 +1188,24 @@ class Library:
field_id=field.type_key,
value=field.value,
)
+
+ @property
+ def tag_color_groups(self) -> dict[str, list[TagColorGroup]]:
+ """Return every TagColorGroup in the library."""
+ with Session(self.engine) as session:
+ color_groups: dict[str, list[TagColorGroup]] = {}
+ results = session.scalars(select(TagColorGroup).order_by(asc(TagColorGroup.namespace)))
+ for color in results:
+ if not color_groups.get(color.namespace):
+ color_groups[color.namespace] = []
+ color_groups[color.namespace].append(color)
+ session.expunge(color)
+ return color_groups
+
+ def get_namespace_name(self, namespace: str) -> str:
+ with Session(self.engine) as session:
+ result = session.scalar(select(Namespace).where(Namespace.namespace == namespace))
+ if result:
+ session.expunge(result)
+
+ return "" if not result else result.name
diff --git a/tagstudio/src/core/library/alchemy/models.py b/tagstudio/src/core/library/alchemy/models.py
index b6ffd2e4..9d8f3a1b 100644
--- a/tagstudio/src/core/library/alchemy/models.py
+++ b/tagstudio/src/core/library/alchemy/models.py
@@ -4,12 +4,11 @@
from pathlib import Path
-from sqlalchemy import JSON, ForeignKey, Integer, event
+from sqlalchemy import JSON, ForeignKey, ForeignKeyConstraint, Integer, event
from sqlalchemy.orm import Mapped, mapped_column, relationship
from ...constants import TAG_ARCHIVED, TAG_FAVORITE
from .db import Base, PathType
-from .enums import TagColor
from .fields import (
BaseField,
BooleanField,
@@ -20,6 +19,22 @@ from .fields import (
from .joins import TagParent
+class Namespace(Base):
+ __tablename__ = "namespaces"
+
+ namespace: Mapped[str] = mapped_column(primary_key=True, nullable=False)
+ name: Mapped[str] = mapped_column(nullable=False)
+
+ def __init__(
+ self,
+ namespace: str,
+ name: str,
+ ):
+ self.namespace = namespace
+ self.name = name
+ super().__init__()
+
+
class TagAlias(Base):
__tablename__ = "tag_aliases"
@@ -37,20 +52,47 @@ class TagAlias(Base):
super().__init__()
+class TagColorGroup(Base):
+ __tablename__ = "tag_colors"
+
+ slug: Mapped[str] = mapped_column(primary_key=True, nullable=False)
+ namespace: Mapped[str] = mapped_column(
+ ForeignKey("namespaces.namespace"), primary_key=True, nullable=False
+ )
+ name: Mapped[str] = mapped_column()
+ primary: Mapped[str] = mapped_column(nullable=False)
+ secondary: Mapped[str | None]
+
+ # TODO: Determine if slug and namespace can be optional and generated/added here if needed.
+ def __init__(
+ self,
+ slug: str,
+ namespace: str,
+ name: str,
+ primary: str,
+ secondary: str | None = None,
+ ):
+ self.slug = slug
+ self.namespace = namespace
+ self.name = name
+ self.primary = primary
+ if secondary:
+ self.secondary = secondary
+ super().__init__()
+
+
class Tag(Base):
__tablename__ = "tags"
- __table_args__ = {"sqlite_autoincrement": True}
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
-
name: Mapped[str]
shorthand: Mapped[str | None]
- color: Mapped[TagColor]
+ color_namespace: Mapped[str | None] = mapped_column()
+ color_slug: Mapped[str | None] = mapped_column()
+ color: Mapped[TagColorGroup | None] = relationship(lazy="joined")
is_category: Mapped[bool]
icon: Mapped[str | None]
-
aliases: Mapped[set[TagAlias]] = relationship(back_populates="tag")
-
parent_tags: Mapped[set["Tag"]] = relationship(
secondary=TagParent.__tablename__,
primaryjoin="Tag.id == TagParent.parent_id",
@@ -58,6 +100,13 @@ class Tag(Base):
back_populates="parent_tags",
)
+ __table_args__ = (
+ ForeignKeyConstraint(
+ [color_namespace, color_slug], [TagColorGroup.namespace, TagColorGroup.slug]
+ ),
+ {"sqlite_autoincrement": True},
+ )
+
@property
def parent_ids(self) -> list[int]:
return [tag.id for tag in self.parent_tags]
@@ -78,13 +127,15 @@ class Tag(Base):
aliases: set[TagAlias] | None = None,
parent_tags: set["Tag"] | None = None,
icon: str | None = None,
- color: TagColor = TagColor.DEFAULT,
+ color_namespace: str | None = None,
+ color_slug: str | None = None,
is_category: bool = False,
):
self.name = name
self.aliases = aliases or set()
self.parent_tags = parent_tags or set()
- self.color = color
+ self.color_namespace = color_namespace
+ self.color_slug = color_slug
self.icon = icon
self.shorthand = shorthand
self.is_category = is_category
diff --git a/tagstudio/src/core/palette.py b/tagstudio/src/core/palette.py
index 38b1d890..4cc2bbd7 100644
--- a/tagstudio/src/core/palette.py
+++ b/tagstudio/src/core/palette.py
@@ -6,7 +6,7 @@ from enum import IntEnum
from typing import Any
import structlog
-from src.core.library.alchemy.enums import TagColor
+from src.core.library.alchemy.enums import TagColorEnum
logger = structlog.get_logger(__name__)
@@ -29,266 +29,14 @@ class UiColor(IntEnum):
PURPLE = 6
-TAG_COLORS: dict[TagColor, dict[ColorType, Any]] = {
- TagColor.DEFAULT: {
- ColorType.PRIMARY: "#1e1e1e",
+TAG_COLORS: dict[TagColorEnum, dict[ColorType, Any]] = {
+ TagColorEnum.DEFAULT: {
+ ColorType.PRIMARY: "#111111",
ColorType.TEXT: ColorType.LIGHT_ACCENT,
ColorType.BORDER: "#333333",
ColorType.LIGHT_ACCENT: "#FFFFFF",
ColorType.DARK_ACCENT: "#222222",
- },
- TagColor.BLACK: {
- ColorType.PRIMARY: "#111018",
- ColorType.TEXT: ColorType.LIGHT_ACCENT,
- ColorType.BORDER: "#18171e",
- ColorType.LIGHT_ACCENT: "#b7b6be",
- ColorType.DARK_ACCENT: "#03020a",
- },
- TagColor.DARK_GRAY: {
- ColorType.PRIMARY: "#24232a",
- ColorType.TEXT: ColorType.LIGHT_ACCENT,
- ColorType.BORDER: "#2a2930",
- ColorType.LIGHT_ACCENT: "#bdbcc4",
- ColorType.DARK_ACCENT: "#07060e",
- },
- TagColor.GRAY: {
- ColorType.PRIMARY: "#53525a",
- ColorType.TEXT: ColorType.LIGHT_ACCENT,
- ColorType.BORDER: "#5b5a62",
- ColorType.LIGHT_ACCENT: "#cbcad2",
- ColorType.DARK_ACCENT: "#191820",
- },
- TagColor.LIGHT_GRAY: {
- ColorType.PRIMARY: "#aaa9b0",
- ColorType.TEXT: ColorType.DARK_ACCENT,
- ColorType.BORDER: "#b6b4bc",
- ColorType.LIGHT_ACCENT: "#cbcad2",
- ColorType.DARK_ACCENT: "#191820",
- },
- TagColor.WHITE: {
- ColorType.PRIMARY: "#f2f1f8",
- ColorType.TEXT: ColorType.DARK_ACCENT,
- ColorType.BORDER: "#fefeff",
- ColorType.LIGHT_ACCENT: "#ffffff",
- ColorType.DARK_ACCENT: "#302f36",
- },
- TagColor.LIGHT_PINK: {
- ColorType.PRIMARY: "#ff99c4",
- ColorType.TEXT: ColorType.DARK_ACCENT,
- ColorType.BORDER: "#ffaad0",
- ColorType.LIGHT_ACCENT: "#ffcbe7",
- ColorType.DARK_ACCENT: "#6c2e3b",
- },
- TagColor.PINK: {
- ColorType.PRIMARY: "#F96BB1",
- ColorType.TEXT: ColorType.DARK_ACCENT,
- ColorType.BORDER: "#FA7EBC",
- ColorType.LIGHT_ACCENT: "#FDB6DC",
- ColorType.DARK_ACCENT: "#5B2135",
- },
- TagColor.MAGENTA: {
- ColorType.PRIMARY: "#f6466f",
- ColorType.TEXT: ColorType.DARK_ACCENT,
- ColorType.BORDER: "#f7587f",
- ColorType.LIGHT_ACCENT: "#fba4bf",
- ColorType.DARK_ACCENT: "#61152f",
- },
- TagColor.RED: {
- ColorType.PRIMARY: "#e22c3c",
- ColorType.TEXT: ColorType.DARK_ACCENT,
- ColorType.BORDER: "#e54252",
- ColorType.LIGHT_ACCENT: "#f39caa",
- ColorType.DARK_ACCENT: "#440d12",
- },
- TagColor.RED_ORANGE: {
- ColorType.PRIMARY: "#e83726",
- ColorType.TEXT: ColorType.DARK_ACCENT,
- ColorType.BORDER: "#ea4b3b",
- ColorType.LIGHT_ACCENT: "#f5a59d",
- ColorType.DARK_ACCENT: "#61120b",
- },
- TagColor.SALMON: {
- ColorType.PRIMARY: "#f65848",
- ColorType.TEXT: ColorType.DARK_ACCENT,
- ColorType.BORDER: "#f76c5f",
- ColorType.LIGHT_ACCENT: "#fcadaa",
- ColorType.DARK_ACCENT: "#6f1b16",
- },
- TagColor.ORANGE: {
- ColorType.PRIMARY: "#ed6022",
- ColorType.TEXT: ColorType.DARK_ACCENT,
- ColorType.BORDER: "#ef7038",
- ColorType.LIGHT_ACCENT: "#f7b79b",
- ColorType.DARK_ACCENT: "#551e0a",
- },
- TagColor.YELLOW_ORANGE: {
- ColorType.PRIMARY: "#fa9a2c",
- ColorType.TEXT: ColorType.DARK_ACCENT,
- ColorType.BORDER: "#fba94b",
- ColorType.LIGHT_ACCENT: "#fdd7ab",
- ColorType.DARK_ACCENT: "#66330d",
- },
- TagColor.YELLOW: {
- ColorType.PRIMARY: "#ffd63d",
- ColorType.TEXT: ColorType.DARK_ACCENT,
- ColorType.BORDER: "#ffe071",
- ColorType.LIGHT_ACCENT: "#fff3c4",
- ColorType.DARK_ACCENT: "#754312",
- },
- TagColor.MINT: {
- ColorType.PRIMARY: "#4aed90",
- ColorType.TEXT: ColorType.DARK_ACCENT,
- ColorType.BORDER: "#79f2b1",
- ColorType.LIGHT_ACCENT: "#c8fbe9",
- ColorType.DARK_ACCENT: "#164f3e",
- },
- TagColor.LIME: {
- ColorType.PRIMARY: "#92e649",
- ColorType.TEXT: ColorType.DARK_ACCENT,
- ColorType.BORDER: "#b2ed72",
- ColorType.LIGHT_ACCENT: "#e9f9b7",
- ColorType.DARK_ACCENT: "#405516",
- },
- TagColor.LIGHT_GREEN: {
- ColorType.PRIMARY: "#85ec76",
- ColorType.TEXT: ColorType.DARK_ACCENT,
- ColorType.BORDER: "#a3f198",
- ColorType.LIGHT_ACCENT: "#e7fbe4",
- ColorType.DARK_ACCENT: "#2b5524",
- },
- TagColor.GREEN: {
- ColorType.PRIMARY: "#28bb48",
- ColorType.TEXT: ColorType.DARK_ACCENT,
- ColorType.BORDER: "#43c568",
- ColorType.LIGHT_ACCENT: "#93e2c8",
- ColorType.DARK_ACCENT: "#0d3828",
- },
- TagColor.TEAL: {
- ColorType.PRIMARY: "#1ad9b2",
- ColorType.TEXT: ColorType.DARK_ACCENT,
- ColorType.BORDER: "#4de3c7",
- ColorType.LIGHT_ACCENT: "#a0f3e8",
- ColorType.DARK_ACCENT: "#08424b",
- },
- TagColor.CYAN: {
- ColorType.PRIMARY: "#49e4d5",
- ColorType.TEXT: ColorType.DARK_ACCENT,
- ColorType.BORDER: "#76ebdf",
- ColorType.LIGHT_ACCENT: "#bff5f0",
- ColorType.DARK_ACCENT: "#0f4246",
- },
- TagColor.LIGHT_BLUE: {
- ColorType.PRIMARY: "#55bbf6",
- ColorType.TEXT: ColorType.DARK_ACCENT,
- ColorType.BORDER: "#70c6f7",
- ColorType.LIGHT_ACCENT: "#bbe4fb",
- ColorType.DARK_ACCENT: "#122541",
- },
- TagColor.BLUE: {
- ColorType.PRIMARY: "#3b87f0",
- ColorType.TEXT: ColorType.LIGHT_ACCENT,
- ColorType.BORDER: "#4e95f2",
- ColorType.LIGHT_ACCENT: "#aedbfa",
- ColorType.DARK_ACCENT: "#122948",
- },
- TagColor.BLUE_VIOLET: {
- ColorType.PRIMARY: "#5948f2",
- ColorType.TEXT: ColorType.LIGHT_ACCENT,
- ColorType.BORDER: "#6258f3",
- ColorType.LIGHT_ACCENT: "#9cb8fb",
- ColorType.DARK_ACCENT: "#1b1649",
- },
- TagColor.VIOLET: {
- ColorType.PRIMARY: "#874ff5",
- ColorType.TEXT: ColorType.LIGHT_ACCENT,
- ColorType.BORDER: "#9360f6",
- ColorType.LIGHT_ACCENT: "#c9b0fa",
- ColorType.DARK_ACCENT: "#3a1860",
- },
- TagColor.PURPLE: {
- ColorType.PRIMARY: "#bb4ff0",
- ColorType.TEXT: ColorType.DARK_ACCENT,
- ColorType.BORDER: "#c364f2",
- ColorType.LIGHT_ACCENT: "#dda7f7",
- ColorType.DARK_ACCENT: "#531862",
- },
- TagColor.PEACH: {
- ColorType.PRIMARY: "#f1c69c",
- ColorType.TEXT: ColorType.DARK_ACCENT,
- ColorType.BORDER: "#f4d4b4",
- ColorType.LIGHT_ACCENT: "#fbeee1",
- ColorType.DARK_ACCENT: "#613f2f",
- },
- TagColor.BROWN: {
- ColorType.PRIMARY: "#823216",
- ColorType.TEXT: ColorType.LIGHT_ACCENT,
- ColorType.BORDER: "#8a3e22",
- ColorType.LIGHT_ACCENT: "#cd9d83",
- ColorType.DARK_ACCENT: "#3a1804",
- },
- TagColor.LAVENDER: {
- ColorType.PRIMARY: "#ad8eef",
- ColorType.TEXT: ColorType.DARK_ACCENT,
- ColorType.BORDER: "#b99ef2",
- ColorType.LIGHT_ACCENT: "#d5c7fa",
- ColorType.DARK_ACCENT: "#492b65",
- },
- TagColor.BLONDE: {
- ColorType.PRIMARY: "#efc664",
- ColorType.TEXT: ColorType.DARK_ACCENT,
- ColorType.BORDER: "#f3d387",
- ColorType.LIGHT_ACCENT: "#faebc6",
- ColorType.DARK_ACCENT: "#6d461e",
- },
- TagColor.AUBURN: {
- ColorType.PRIMARY: "#a13220",
- ColorType.TEXT: ColorType.LIGHT_ACCENT,
- ColorType.BORDER: "#aa402f",
- ColorType.LIGHT_ACCENT: "#d98a7f",
- ColorType.DARK_ACCENT: "#3d100a",
- },
- TagColor.LIGHT_BROWN: {
- ColorType.PRIMARY: "#be5b2d",
- ColorType.TEXT: ColorType.DARK_ACCENT,
- ColorType.BORDER: "#c4693d",
- ColorType.LIGHT_ACCENT: "#e5b38c",
- ColorType.DARK_ACCENT: "#4c290e",
- },
- TagColor.DARK_BROWN: {
- ColorType.PRIMARY: "#4c2315",
- ColorType.TEXT: ColorType.LIGHT_ACCENT,
- ColorType.BORDER: "#542a1c",
- ColorType.LIGHT_ACCENT: "#b78171",
- ColorType.DARK_ACCENT: "#211006",
- },
- TagColor.COOL_GRAY: {
- ColorType.PRIMARY: "#515768",
- ColorType.TEXT: ColorType.LIGHT_ACCENT,
- ColorType.BORDER: "#5b6174",
- ColorType.LIGHT_ACCENT: "#9ea1c3",
- ColorType.DARK_ACCENT: "#181a37",
- },
- TagColor.WARM_GRAY: {
- ColorType.PRIMARY: "#625550",
- ColorType.TEXT: ColorType.LIGHT_ACCENT,
- ColorType.BORDER: "#6c5e57",
- ColorType.LIGHT_ACCENT: "#c0a392",
- ColorType.DARK_ACCENT: "#371d18",
- },
- TagColor.OLIVE: {
- ColorType.PRIMARY: "#4c652e",
- ColorType.TEXT: ColorType.LIGHT_ACCENT,
- ColorType.BORDER: "#586f36",
- ColorType.LIGHT_ACCENT: "#b4c17a",
- ColorType.DARK_ACCENT: "#23300e",
- },
- TagColor.BERRY: {
- ColorType.PRIMARY: "#9f2aa7",
- ColorType.TEXT: ColorType.LIGHT_ACCENT,
- ColorType.BORDER: "#aa43b4",
- ColorType.LIGHT_ACCENT: "#cc8fdc",
- ColorType.DARK_ACCENT: "#41114a",
- },
+ }
}
UI_COLORS: dict[UiColor, dict[ColorType, Any]] = {
@@ -337,7 +85,7 @@ UI_COLORS: dict[UiColor, dict[ColorType, Any]] = {
}
-def get_tag_color(color_type: ColorType, color_id: TagColor) -> str:
+def get_tag_color(color_type: ColorType, color_id: TagColorEnum) -> str:
"""Return a hex value given a tag color name and ColorType.
Args:
diff --git a/tagstudio/src/qt/modals/build_tag.py b/tagstudio/src/qt/modals/build_tag.py
index acf2de9a..e3c8897b 100644
--- a/tagstudio/src/qt/modals/build_tag.py
+++ b/tagstudio/src/qt/modals/build_tag.py
@@ -11,7 +11,6 @@ from PySide6.QtCore import Qt, Signal
from PySide6.QtWidgets import (
QApplication,
QCheckBox,
- QComboBox,
QFrame,
QHBoxLayout,
QLabel,
@@ -23,12 +22,14 @@ from PySide6.QtWidgets import (
QWidget,
)
from src.core.library import Library, Tag
-from src.core.library.alchemy.enums import TagColor
-from src.core.palette import ColorType, UiColor, get_tag_color, get_ui_color
+from src.core.library.alchemy.models import TagColorGroup
+from src.core.palette import ColorType, UiColor, get_ui_color
+from src.qt.modals.tag_color_selection import TagColorSelection
from src.qt.modals.tag_search import TagSearchPanel
from src.qt.translations import Translations
from src.qt.widgets.panel import PanelModal, PanelWidget
from src.qt.widgets.tag import TagWidget
+from src.qt.widgets.tag_color_preview import TagColorPreview
logger = structlog.get_logger(__name__)
@@ -59,6 +60,8 @@ class BuildTagPanel(PanelWidget):
super().__init__()
self.lib = library
self.tag: Tag # NOTE: This gets set at the end of the init.
+ self.tag_color_namespace: str | None
+ self.tag_color_slug: str | None
self.setMinimumSize(300, 400)
self.root_layout = QVBoxLayout(self)
@@ -111,14 +114,13 @@ class BuildTagPanel(PanelWidget):
self.aliases_table.horizontalHeader().setVisible(False)
self.aliases_table.verticalHeader().setVisible(False)
self.aliases_table.horizontalHeader().setStretchLastSection(True)
- self.aliases_table.setColumnWidth(0, 35)
+ self.aliases_table.setColumnWidth(0, 32)
self.aliases_table.setTabKeyNavigation(False)
self.aliases_table.setFocusPolicy(Qt.FocusPolicy.NoFocus)
- self.alias_add_button = QPushButton()
- self.alias_add_button.setText("+")
-
- self.alias_add_button.clicked.connect(self.add_alias_callback)
+ self.aliases_add_button = QPushButton()
+ self.aliases_add_button.setText("+")
+ self.aliases_add_button.clicked.connect(self.add_alias_callback)
# Parent Tags ----------------------------------------------------------
self.parent_tags_widget = QWidget()
@@ -134,18 +136,15 @@ class BuildTagPanel(PanelWidget):
self.scroll_contents = QWidget()
self.parent_tags_scroll_layout = QVBoxLayout(self.scroll_contents)
- self.parent_tags_scroll_layout.setContentsMargins(6, 0, 6, 0)
+ self.parent_tags_scroll_layout.setContentsMargins(6, 6, 6, 0)
self.parent_tags_scroll_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
self.scroll_area = QScrollArea()
self.scroll_area.setFocusPolicy(Qt.FocusPolicy.NoFocus)
- # self.scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOn)
self.scroll_area.setWidgetResizable(True)
self.scroll_area.setFrameShadow(QFrame.Shadow.Plain)
self.scroll_area.setFrameShape(QFrame.Shape.NoFrame)
self.scroll_area.setWidget(self.scroll_contents)
- # self.scroll_area.setMinimumHeight(60)
-
self.parent_tags_layout.addWidget(self.scroll_area)
self.parent_tags_add_button = QPushButton()
@@ -168,32 +167,31 @@ class BuildTagPanel(PanelWidget):
self.color_widget = QWidget()
self.color_layout = QVBoxLayout(self.color_widget)
self.color_layout.setStretch(1, 1)
- self.color_layout.setContentsMargins(0, 0, 0, 24)
- self.color_layout.setSpacing(0)
+ 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.translate_qobject(self.color_title, "tag.color")
self.color_layout.addWidget(self.color_title)
- self.color_field = QComboBox()
- self.color_field.setEditable(False)
- self.color_field.setMaxVisibleItems(10)
- self.color_field.setStyleSheet("combobox-popup:0;")
- for color in TagColor:
- self.color_field.addItem(color.name.replace("_", " ").title(), userData=color.value)
- # self.color_field.setProperty("appearance", "flat")
- self.color_field.currentIndexChanged.connect(
- lambda c: (
- self.color_field.setStyleSheet(
- "combobox-popup:0;"
- "font-weight:600;"
- f"color:{get_tag_color(ColorType.TEXT, self.color_field.currentData())};"
- f"background-color:{get_tag_color(
- ColorType.PRIMARY,
- self.color_field.currentData())};"
- )
- )
+ self.color_button: TagColorPreview
+ try:
+ self.color_button = TagColorPreview(tag.color)
+ except Exception as e:
+ # TODO: Investigate why this happens during tests
+ logger.error("[BuildTag] Could not access Tag member attributes", error=e)
+ self.color_button = TagColorPreview(None)
+ self.tag_color_selection = TagColorSelection(self.lib)
+ chose_tag_color_title = Translations.translate_formatted("tag.choose_color")
+ self.choose_color_modal = PanelModal(
+ self.tag_color_selection,
+ chose_tag_color_title,
+ chose_tag_color_title,
+ done_callback=lambda: self.choose_color_callback(
+ self.tag_color_selection.selected_color
+ ),
)
- self.color_layout.addWidget(self.color_field)
+ self.color_button.button.clicked.connect(self.choose_color_modal.show)
+ self.color_layout.addWidget(self.color_button)
# Category -------------------------------------------------------------
self.cat_widget = QWidget()
@@ -229,7 +227,7 @@ class BuildTagPanel(PanelWidget):
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.alias_add_button)
+ self.root_layout.addWidget(self.aliases_add_button)
self.root_layout.addWidget(self.parent_tags_widget)
self.root_layout.addWidget(self.color_widget)
self.root_layout.addWidget(QLabel("Properties
"))
@@ -302,6 +300,16 @@ class BuildTagPanel(PanelWidget):
self.alias_ids.remove(alias_id)
self._set_aliases()
+ def choose_color_callback(self, tag_color_group: TagColorGroup | None):
+ logger.info("choose_color_callback", tag_color_group=tag_color_group)
+ if tag_color_group:
+ self.tag_color_namespace = tag_color_group.namespace
+ self.tag_color_slug = tag_color_group.slug
+ else:
+ self.tag_color_namespace = None
+ self.tag_color_slug = None
+ self.color_button.set_tag_color_group(tag_color_group)
+
def set_parent_tags(self):
while self.parent_tags_scroll_layout.itemAt(0):
self.parent_tags_scroll_layout.takeAt(0).widget().deleteLater()
@@ -326,11 +334,9 @@ class BuildTagPanel(PanelWidget):
names: set[str] = set()
for i in range(0, self.aliases_table.rowCount()):
widget = self.aliases_table.cellWidget(i, 1)
-
names.add(cast(CustomTableItem, widget).text())
remove: set[str] = set(self.alias_names) - names
-
self.alias_names = list(set(self.alias_names) - remove)
for name in names:
@@ -341,8 +347,6 @@ class BuildTagPanel(PanelWidget):
self.alias_names.remove(name)
def _update_new_alias_name_dict(self):
- row = self.aliases_table.rowCount()
- logger.info(row)
for i in range(0, self.aliases_table.rowCount()):
widget = self.aliases_table.cellWidget(i, 1)
self.new_alias_names[widget.id] = widget.text() # type: ignore
@@ -355,7 +359,7 @@ class BuildTagPanel(PanelWidget):
self.alias_names.clear()
- last: QWidget = self.panel_save_button or self.color_field
+ last: QWidget = self.panel_save_button
for alias_id in self.alias_ids:
alias = self.lib.get_alias(self.tag.id, alias_id)
@@ -394,6 +398,7 @@ class BuildTagPanel(PanelWidget):
def set_tag(self, tag: Tag):
logger.info("[BuildTagPanel] Setting Tag", tag=tag)
self.tag = tag
+
self.name_field.setText(tag.name)
self.shorthand_field.setText(tag.shorthand or "")
@@ -405,11 +410,15 @@ class BuildTagPanel(PanelWidget):
self.parent_ids.add(parent_id)
self.set_parent_tags()
- # select item in self.color_field where the userData value matched tag.color
- for i in range(self.color_field.count()):
- if self.color_field.itemData(i) == tag.color:
- self.color_field.setCurrentIndex(i)
- break
+ try:
+ self.tag_color_namespace = tag.color_namespace
+ self.tag_color_slug = tag.color_slug
+ self.color_button.set_tag_color_group(tag.color)
+ self.tag_color_selection.select_radio_button(tag.color)
+ except Exception as e:
+ # TODO: Investigate why this happens during tests
+ logger.error("[BuildTag] Could not access Tag member attributes", error=e)
+ self.color_button.set_tag_color_group(None)
self.cat_checkbox.setChecked(tag.is_category)
@@ -426,24 +435,25 @@ class BuildTagPanel(PanelWidget):
self.panel_save_button.setDisabled(is_empty)
def build_tag(self) -> Tag:
- color = self.color_field.currentData() or TagColor.DEFAULT
tag = self.tag
self.add_aliases()
tag.name = self.name_field.text()
tag.shorthand = self.shorthand_field.text()
- tag.color = color
tag.is_category = self.cat_checkbox.isChecked()
+ tag.color_namespace = self.tag_color_namespace
+ tag.color_slug = self.tag_color_slug
+
logger.info("built tag", tag=tag)
return tag
def parent_post_init(self):
self.setTabOrder(self.name_field, self.shorthand_field)
- self.setTabOrder(self.shorthand_field, self.alias_add_button)
- self.setTabOrder(self.alias_add_button, self.parent_tags_add_button)
- self.setTabOrder(self.parent_tags_add_button, self.color_field)
- self.setTabOrder(self.color_field, self.panel_cancel_button)
+ self.setTabOrder(self.shorthand_field, self.aliases_add_button)
+ self.setTabOrder(self.aliases_add_button, self.parent_tags_add_button)
+ self.setTabOrder(self.parent_tags_add_button, self.color_button)
+ self.setTabOrder(self.color_button, self.panel_cancel_button)
self.setTabOrder(self.panel_cancel_button, self.panel_save_button)
self.setTabOrder(self.panel_save_button, self.aliases_table.cellWidget(0, 1))
self.name_field.selectAll()
diff --git a/tagstudio/src/qt/modals/folders_to_tags.py b/tagstudio/src/qt/modals/folders_to_tags.py
index ff32e5eb..16ba54d2 100644
--- a/tagstudio/src/qt/modals/folders_to_tags.py
+++ b/tagstudio/src/qt/modals/folders_to_tags.py
@@ -20,6 +20,7 @@ from PySide6.QtWidgets import (
)
from src.core.constants import TAG_ARCHIVED, TAG_FAVORITE
from src.core.library import Library, Tag
+from src.core.library.alchemy.enums import TagColorEnum
from src.core.palette import ColorType, get_tag_color
from src.qt.flowlayout import FlowLayout
from src.qt.translations import Translations
@@ -329,10 +330,10 @@ class ModifiedTagWidget(QWidget):
self.bg_button.setStyleSheet(
f"QPushButton{{"
- f"background: {get_tag_color(ColorType.PRIMARY, tag.color)};"
- f"color: {get_tag_color(ColorType.TEXT, tag.color)};"
+ f"background: {get_tag_color(ColorType.PRIMARY, TagColorEnum.DEFAULT)};"
+ f"color: {get_tag_color(ColorType.TEXT, TagColorEnum.DEFAULT)};"
f"font-weight: 600;"
- f"border-color:{get_tag_color(ColorType.BORDER, tag.color)};"
+ f"border-color:{get_tag_color(ColorType.BORDER, TagColorEnum.DEFAULT)};"
f"border-radius: 6px;"
f"border-style:inset;"
f"border-width: {math.ceil(self.devicePixelRatio())}px;"
@@ -342,7 +343,7 @@ class ModifiedTagWidget(QWidget):
f"font-size: 13px"
f"}}"
f"QPushButton::hover{{"
- f"border-color:{get_tag_color(ColorType.LIGHT_ACCENT, tag.color)};"
+ f"border-color:{get_tag_color(ColorType.LIGHT_ACCENT, TagColorEnum.DEFAULT)};"
f"}}"
)
diff --git a/tagstudio/src/qt/modals/tag_color_selection.py b/tagstudio/src/qt/modals/tag_color_selection.py
new file mode 100644
index 00000000..98cd9fab
--- /dev/null
+++ b/tagstudio/src/qt/modals/tag_color_selection.py
@@ -0,0 +1,167 @@
+# Copyright (C) 2025 Travis Abendshien (CyanVoxel).
+# Licensed under the GPL-3.0 License.
+# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
+
+
+import structlog
+from PySide6.QtCore import Qt
+from PySide6.QtGui import QColor
+from PySide6.QtWidgets import (
+ QButtonGroup,
+ QLabel,
+ QRadioButton,
+ QSpacerItem,
+ QVBoxLayout,
+ QWidget,
+)
+from src.core.library import Library
+from src.core.library.alchemy.models import TagColorGroup
+from src.qt.flowlayout import FlowLayout
+from src.qt.translations import Translations
+from src.qt.widgets.panel import PanelWidget
+from src.qt.widgets.tag_color_preview import (
+ get_border_color,
+ get_highlight_color,
+ get_primary_color,
+ get_text_color,
+)
+
+logger = structlog.get_logger(__name__)
+
+
+class TagColorSelection(PanelWidget):
+ def __init__(self, library: Library):
+ super().__init__()
+ self.lib = library
+ self.selected_color: TagColorGroup | None = None
+
+ self.setMinimumSize(308, 540)
+ self.root_layout = QVBoxLayout(self)
+ self.root_layout.setContentsMargins(6, 0, 6, 0)
+ self.root_layout.setSpacing(6)
+ self.root_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
+
+ # Add Widgets to Layout ================================================
+ tag_color_groups = self.lib.tag_color_groups
+ self.button_group = QButtonGroup(self)
+
+ self.add_no_color_widget()
+ self.root_layout.addSpacerItem(QSpacerItem(1, 12))
+ for group, colors in tag_color_groups.items():
+ display_name: str = self.lib.get_namespace_name(group)
+ self.root_layout.addWidget(
+ QLabel(f"{display_name if display_name else group}
")
+ )
+ color_box_widget = QWidget()
+ color_group_layout = FlowLayout()
+ color_group_layout.setSpacing(4)
+ color_group_layout.enable_grid_optimizations(value=False)
+ color_group_layout.setContentsMargins(0, 0, 0, 0)
+ color_box_widget.setLayout(color_group_layout)
+ for color in colors:
+ primary_color = get_primary_color(color)
+ border_color = (
+ get_border_color(primary_color)
+ if not (color and color.secondary)
+ else (QColor(color.secondary))
+ )
+ highlight_color = get_highlight_color(
+ primary_color if not (color and color.secondary) else QColor(color.secondary)
+ )
+ text_color: QColor
+ if color and color.secondary:
+ text_color = QColor(color.secondary)
+ else:
+ text_color = get_text_color(primary_color, highlight_color)
+
+ radio_button = QRadioButton()
+ radio_button.setObjectName(f"{color.namespace}.{color.slug}")
+ radio_button.setToolTip(color.name)
+ radio_button.setFixedSize(24, 24)
+ radio_button.setStyleSheet(
+ f"QRadioButton{{"
+ f"background: rgba{primary_color.toTuple()};"
+ f"color: rgba{text_color.toTuple()};"
+ f"border-color: rgba{border_color.toTuple()};"
+ f"border-radius: 3px;"
+ f"border-style:solid;"
+ f"border-width: 2px;"
+ f"}}"
+ f"QRadioButton::indicator{{"
+ f"width: 12px;"
+ f"height: 12px;"
+ f"border-radius: 1px;"
+ f"margin: 4px;"
+ f"}}"
+ f"QRadioButton::indicator:checked{{"
+ f"background: rgba{text_color.toTuple()};"
+ f"}}"
+ f"QRadioButton::hover{{"
+ f"border-color: rgba{highlight_color.toTuple()};"
+ f"}}"
+ )
+ radio_button.clicked.connect(lambda checked=False, x=color: self.select_color(x))
+ color_group_layout.addWidget(radio_button)
+ self.button_group.addButton(radio_button)
+ self.root_layout.addWidget(color_box_widget)
+ self.root_layout.addSpacerItem(QSpacerItem(1, 12))
+
+ def add_no_color_widget(self):
+ no_color_str: str = Translations.translate_formatted("color.title.no_color")
+ self.root_layout.addWidget(QLabel(f"{no_color_str}
"))
+ color_box_widget = QWidget()
+ color_group_layout = FlowLayout()
+ color_group_layout.setSpacing(4)
+ color_group_layout.enable_grid_optimizations(value=False)
+ color_group_layout.setContentsMargins(0, 0, 0, 0)
+ color_box_widget.setLayout(color_group_layout)
+ color = None
+ primary_color = get_primary_color(color)
+ border_color = get_border_color(primary_color)
+ highlight_color = get_highlight_color(primary_color)
+ text_color: QColor
+ if color and color.secondary:
+ text_color = QColor(color.secondary)
+ else:
+ text_color = get_text_color(primary_color, highlight_color)
+
+ radio_button = QRadioButton()
+ radio_button.setObjectName("None") # NOTE: Internal use, no translation needed.
+ radio_button.setToolTip(no_color_str)
+ radio_button.setFixedSize(24, 24)
+ radio_button.setStyleSheet(
+ f"QRadioButton{{"
+ f"background: rgba{primary_color.toTuple()};"
+ f"color: rgba{text_color.toTuple()};"
+ f"border-color: rgba{border_color.toTuple()};"
+ f"border-radius: 3px;"
+ f"border-style:solid;"
+ f"border-width: 2px;"
+ f"}}"
+ f"QRadioButton::indicator{{"
+ f"width: 12px;"
+ f"height: 12px;"
+ f"border-radius: 1px;"
+ f"margin: 4px;"
+ f"}}"
+ f"QRadioButton::indicator:checked{{"
+ f"background: rgba{text_color.toTuple()};"
+ f"}}"
+ f"QRadioButton::hover{{"
+ f"border-color: rgba{highlight_color.toTuple()};"
+ f"}}"
+ )
+ radio_button.clicked.connect(lambda checked=False, x=color: self.select_color(x))
+ color_group_layout.addWidget(radio_button)
+ self.button_group.addButton(radio_button)
+ self.root_layout.addWidget(color_box_widget)
+
+ def select_color(self, color: TagColorGroup):
+ self.selected_color = color
+
+ def select_radio_button(self, color: TagColorGroup | None):
+ object_name: str = "None" if not color else f"{color.namespace}.{color.slug}"
+ for button in self.button_group.buttons():
+ if button.objectName() == object_name:
+ button.setChecked(True)
+ break
diff --git a/tagstudio/src/qt/modals/tag_search.py b/tagstudio/src/qt/modals/tag_search.py
index fdd986ab..bf1ebc9f 100644
--- a/tagstudio/src/qt/modals/tag_search.py
+++ b/tagstudio/src/qt/modals/tag_search.py
@@ -3,12 +3,10 @@
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
-import math
-
import src.qt.modals.build_tag as build_tag
import structlog
from PySide6.QtCore import QSize, Qt, Signal
-from PySide6.QtGui import QShowEvent
+from PySide6.QtGui import QColor, QShowEvent
from PySide6.QtWidgets import (
QFrame,
QHBoxLayout,
@@ -20,11 +18,17 @@ from PySide6.QtWidgets import (
)
from src.core.constants import RESERVED_TAG_END, RESERVED_TAG_START
from src.core.library import Library, Tag
-from src.core.library.alchemy.enums import TagColor
+from src.core.library.alchemy.enums import TagColorEnum
from src.core.palette import ColorType, get_tag_color
from src.qt.translations import Translations
from src.qt.widgets.panel import PanelModal, PanelWidget
-from src.qt.widgets.tag import TagWidget
+from src.qt.widgets.tag import (
+ TagWidget,
+ get_border_color,
+ get_highlight_color,
+ get_primary_color,
+ get_text_color,
+)
logger = structlog.get_logger(__name__)
@@ -90,28 +94,45 @@ class TagSearchPanel(PanelWidget):
tag_widget.on_remove.connect(lambda t=tag: self.remove_tag(t))
row.addWidget(tag_widget)
+ primary_color = get_primary_color(tag)
+ border_color = (
+ get_border_color(primary_color)
+ if not (tag.color and tag.color.secondary)
+ else (QColor(tag.color.secondary))
+ )
+ highlight_color = get_highlight_color(
+ primary_color
+ if not (tag.color and tag.color.secondary)
+ else QColor(tag.color.secondary)
+ )
+ text_color: QColor
+ if tag.color and tag.color.secondary:
+ text_color = QColor(tag.color.secondary)
+ else:
+ text_color = get_text_color(primary_color, highlight_color)
+
if self.is_tag_chooser:
add_button = QPushButton()
- add_button.setMinimumSize(23, 23)
- add_button.setMaximumSize(23, 23)
+ add_button.setMinimumSize(22, 22)
+ add_button.setMaximumSize(22, 22)
add_button.setText("+")
add_button.setStyleSheet(
f"QPushButton{{"
- f"background: {get_tag_color(ColorType.PRIMARY, tag.color)};"
- f"color: {get_tag_color(ColorType.TEXT, tag.color)};"
+ f"background: rgba{primary_color.toTuple()};"
+ f"color: rgba{text_color.toTuple()};"
f"font-weight: 600;"
- f"border-color:{get_tag_color(ColorType.BORDER, tag.color)};"
+ f"border-color: rgba{border_color.toTuple()};"
f"border-radius: 6px;"
f"border-style:solid;"
- f"border-width: {math.ceil(self.devicePixelRatio())}px;"
- f"padding-bottom: 5px;"
+ f"border-width: 2px;"
+ f"padding-bottom: 4px;"
f"font-size: 20px;"
f"}}"
f"QPushButton::hover"
f"{{"
- f"border-color:{get_tag_color(ColorType.LIGHT_ACCENT, tag.color)};"
- f"color: {get_tag_color(ColorType.DARK_ACCENT, tag.color)};"
- f"background: {get_tag_color(ColorType.LIGHT_ACCENT, tag.color)};"
+ f"border-color: rgba{highlight_color.toTuple()};"
+ f"color: rgba{primary_color.toTuple()};"
+ f"background: rgba{highlight_color.toTuple()};"
f"}}"
)
tag_id = tag.id
@@ -134,24 +155,24 @@ class TagSearchPanel(PanelWidget):
inner_layout.setObjectName("innerLayout")
inner_layout.setContentsMargins(2, 2, 2, 2)
create_button.setLayout(inner_layout)
- create_button.setMinimumSize(math.ceil(22 * 1.5), 22)
+ create_button.setMinimumSize(22, 22)
create_button.setStyleSheet(
f"QPushButton{{"
- f"background: {get_tag_color(ColorType.PRIMARY, TagColor.DEFAULT)};"
- f"color: {get_tag_color(ColorType.TEXT, TagColor.DEFAULT)};"
+ f"background: {get_tag_color(ColorType.PRIMARY, TagColorEnum.DEFAULT)};"
+ f"color: {get_tag_color(ColorType.TEXT, TagColorEnum.DEFAULT)};"
f"font-weight: 600;"
- f"border-color:{get_tag_color(ColorType.BORDER, TagColor.DEFAULT)};"
+ f"border-color:{get_tag_color(ColorType.BORDER, TagColorEnum.DEFAULT)};"
f"border-radius: 6px;"
f"border-style:solid;"
- f"border-width: {math.ceil(self.devicePixelRatio())}px;"
+ f"border-width: 2px;"
f"padding-right: 4px;"
f"padding-bottom: 1px;"
f"padding-left: 4px;"
f"font-size: 13px"
f"}}"
f"QPushButton::hover{{"
- f"border-color:{get_tag_color(ColorType.LIGHT_ACCENT, TagColor.DEFAULT)};"
+ f"border-color:{get_tag_color(ColorType.LIGHT_ACCENT, TagColorEnum.DEFAULT)};"
f"}}"
)
diff --git a/tagstudio/src/qt/widgets/migration_modal.py b/tagstudio/src/qt/widgets/migration_modal.py
index 954ad472..cc16ef74 100644
--- a/tagstudio/src/qt/widgets/migration_modal.py
+++ b/tagstudio/src/qt/widgets/migration_modal.py
@@ -2,6 +2,7 @@
# Licensed under the GPL-3.0 License.
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
+import traceback
from pathlib import Path
import structlog
@@ -21,7 +22,7 @@ from sqlalchemy import select
from sqlalchemy.orm import Session
from src.core.constants import LEGACY_TAG_FIELD_IDS, TS_FOLDER_NAME
from src.core.enums import LibraryPrefs
-from src.core.library.alchemy.enums import TagColor
+from src.core.library.alchemy import default_color_groups
from src.core.library.alchemy.joins import TagParent
from src.core.library.alchemy.library import TAG_ARCHIVED, TAG_FAVORITE, TAG_META
from src.core.library.alchemy.library import Library as SqliteLibrary
@@ -413,6 +414,8 @@ class JsonMigrationModal(QObject):
self.done = True
except Exception as e:
+ traceback.print_stack()
+ logger.error("[MigrationModal] Error:", error=e)
yield f"Error: {type(e).__name__}"
QApplication.beep()
QApplication.alert(self.paged_panel)
@@ -719,17 +722,13 @@ class JsonMigrationModal(QObject):
def check_color_parity(self) -> bool:
"""Check if all JSON tag colors match the new SQL tag colors."""
- sql_color: str = None
- json_color: str = None
+ sql_color: tuple[str | None, str | None] = (None, None)
+ json_color: tuple[str | None, str | None] = (None, None)
for tag in self.sql_lib.tags:
tag_id = tag.id # Tag IDs start at 0
- sql_color = tag.color.name
- json_color = (
- TagColor.get_color_from_str(self.json_lib.get_tag(tag_id).color).name
- if (self.json_lib.get_tag(tag_id).color) != ""
- else TagColor.DEFAULT.name
- )
+ sql_color = (tag.color_namespace, tag.color_slug)
+ json_color = default_color_groups.json_to_sql_color(self.json_lib.get_tag(tag_id).color)
logger.info(
"[Color Parity]",
@@ -738,7 +737,7 @@ class JsonMigrationModal(QObject):
sql_color=sql_color,
)
- if not (sql_color is not None and json_color is not None and (sql_color == json_color)):
+ if sql_color != json_color:
self.discrepancies.append(
f"[Color Parity][Tag ID: {tag_id}]:"
f"\nOLD (JSON):{json_color}\nNEW (SQL):{sql_color}"
diff --git a/tagstudio/src/qt/widgets/preview/preview_thumb.py b/tagstudio/src/qt/widgets/preview/preview_thumb.py
index f9f93614..6b9fa33d 100644
--- a/tagstudio/src/qt/widgets/preview/preview_thumb.py
+++ b/tagstudio/src/qt/widgets/preview/preview_thumb.py
@@ -207,7 +207,7 @@ class PreviewThumb(QWidget):
image = Image.open(str(filepath))
stats["width"] = image.width
stats["height"] = image.height
- except UnidentifiedImageError as e:
+ except (UnidentifiedImageError, FileNotFoundError) as e:
logger.error("[PreviewThumb] Could not get image stats", filepath=filepath, error=e)
elif MediaCategories.is_ext_in_category(
ext, MediaCategories.IMAGE_VECTOR_TYPES, mime_fallback=True
diff --git a/tagstudio/src/qt/widgets/tag.py b/tagstudio/src/qt/widgets/tag.py
index 1af377e3..decf97bd 100644
--- a/tagstudio/src/qt/widgets/tag.py
+++ b/tagstudio/src/qt/widgets/tag.py
@@ -3,11 +3,11 @@
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
-import math
from types import FunctionType
+import structlog
from PySide6.QtCore import QEvent, Qt, Signal
-from PySide6.QtGui import QAction, QEnterEvent, QFontMetrics
+from PySide6.QtGui import QAction, QColor, QEnterEvent, QFontMetrics
from PySide6.QtWidgets import (
QHBoxLayout,
QLineEdit,
@@ -16,10 +16,12 @@ from PySide6.QtWidgets import (
QWidget,
)
from src.core.library import Tag
-from src.core.library.alchemy.enums import TagColor
+from src.core.library.alchemy.enums import TagColorEnum
from src.core.palette import ColorType, get_tag_color
from src.qt.translations import Translations
+logger = structlog.get_logger(__name__)
+
class TagAliasWidget(QWidget):
on_remove = Signal()
@@ -57,8 +59,8 @@ class TagAliasWidget(QWidget):
self.remove_button.setText("–")
self.remove_button.setHidden(False)
self.remove_button.setStyleSheet(
- f"color: {get_tag_color(ColorType.PRIMARY, TagColor.DEFAULT)};"
- f"background: {get_tag_color(ColorType.TEXT, TagColor.DEFAULT)};"
+ f"color: {get_tag_color(ColorType.PRIMARY, TagColorEnum.DEFAULT)};"
+ f"background: {get_tag_color(ColorType.TEXT, TagColorEnum.DEFAULT)};"
f"font-weight: 800;"
f"border-radius: 4px;"
f"border-width:0;"
@@ -142,26 +144,45 @@ class TagWidget(QWidget):
self.inner_layout.setContentsMargins(2, 2, 2, 2)
self.bg_button.setLayout(self.inner_layout)
- self.bg_button.setMinimumSize(math.ceil(22 * 2), 22)
+ self.bg_button.setMinimumSize(22, 22)
+
+ primary_color = get_primary_color(tag)
+ border_color = (
+ get_border_color(primary_color)
+ if not (tag.color and tag.color.secondary)
+ else (QColor(tag.color.secondary))
+ )
+ highlight_color = get_highlight_color(
+ primary_color
+ if not (tag.color and tag.color.secondary)
+ else QColor(tag.color.secondary)
+ )
+ text_color: QColor
+ if tag.color and tag.color.secondary:
+ text_color = QColor(tag.color.secondary)
+ else:
+ text_color = get_text_color(primary_color, highlight_color)
self.bg_button.setStyleSheet(
f"QPushButton{{"
- f"background: {get_tag_color(ColorType.PRIMARY, tag.color)};"
- f"color: {get_tag_color(ColorType.TEXT, tag.color)};"
+ f"background: rgba{primary_color.toTuple()};"
+ f"color: rgba{text_color.toTuple()};"
f"font-weight: 600;"
- f"border-color:{get_tag_color(ColorType.BORDER, tag.color)};"
+ f"border-color: rgba{border_color.toTuple()};"
f"border-radius: 6px;"
f"border-style:solid;"
- f"border-width: {math.ceil(self.devicePixelRatio())}px;"
+ f"border-width: 2px;"
f"padding-right: 4px;"
f"padding-bottom: 1px;"
f"padding-left: 4px;"
f"font-size: 13px"
f"}}"
f"QPushButton::hover{{"
- f"border-color:{get_tag_color(ColorType.LIGHT_ACCENT, tag.color)};"
+ f"border-color: rgba{highlight_color.toTuple()};"
f"}}"
)
+ self.bg_button.setMinimumHeight(22)
+ self.bg_button.setMaximumHeight(22)
self.base_layout.addWidget(self.bg_button)
@@ -171,16 +192,16 @@ class TagWidget(QWidget):
self.remove_button.setText("–")
self.remove_button.setHidden(True)
self.remove_button.setStyleSheet(
- f"color: {get_tag_color(ColorType.PRIMARY, tag.color)};"
- f"background: {get_tag_color(ColorType.TEXT, tag.color)};"
+ f"color: rgba{primary_color.toTuple()};"
+ f"background: rgba{text_color.toTuple()};"
f"font-weight: 800;"
- f"border-radius: 4px;"
+ f"border-radius: 3px;"
f"border-width:0;"
f"padding-bottom: 4px;"
f"font-size: 14px"
)
- self.remove_button.setMinimumSize(19, 19)
- self.remove_button.setMaximumSize(19, 19)
+ self.remove_button.setMinimumSize(18, 18)
+ self.remove_button.setMaximumSize(18, 18)
self.remove_button.clicked.connect(self.on_remove.emit)
if has_remove:
@@ -203,3 +224,41 @@ class TagWidget(QWidget):
self.remove_button.setHidden(True)
self.update()
return super().leaveEvent(event)
+
+
+def get_primary_color(tag: Tag) -> QColor:
+ primary_color = QColor(
+ get_tag_color(ColorType.PRIMARY, TagColorEnum.DEFAULT)
+ if not tag.color
+ else tag.color.primary
+ )
+
+ return primary_color
+
+
+def get_border_color(primary_color: QColor) -> QColor:
+ border_color: QColor = QColor(primary_color)
+ border_color.setRed(min(border_color.red() + 20, 255))
+ border_color.setGreen(min(border_color.green() + 20, 255))
+ border_color.setBlue(min(border_color.blue() + 20, 255))
+
+ return border_color
+
+
+def get_highlight_color(primary_color: QColor) -> QColor:
+ highlight_color: QColor = QColor(primary_color)
+ highlight_color = highlight_color.toHsl()
+ highlight_color.setHsl(highlight_color.hue(), min(highlight_color.saturation(), 200), 225, 255)
+ highlight_color = highlight_color.toRgb()
+
+ return highlight_color
+
+
+def get_text_color(primary_color: QColor, highlight_color: QColor) -> QColor:
+ if primary_color.lightness() > 120:
+ text_color = QColor(primary_color)
+ text_color = text_color.toHsl()
+ text_color.setHsl(text_color.hue(), text_color.saturation(), 50, 255)
+ return text_color.toRgb()
+ else:
+ return highlight_color
diff --git a/tagstudio/src/qt/widgets/tag_color_preview.py b/tagstudio/src/qt/widgets/tag_color_preview.py
new file mode 100644
index 00000000..fcdb052e
--- /dev/null
+++ b/tagstudio/src/qt/widgets/tag_color_preview.py
@@ -0,0 +1,128 @@
+# Copyright (C) 2025 Travis Abendshien (CyanVoxel).
+# Licensed under the GPL-3.0 License.
+# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
+
+
+import structlog
+from PySide6.QtCore import Qt, Signal
+from PySide6.QtGui import QColor
+from PySide6.QtWidgets import (
+ QPushButton,
+ QVBoxLayout,
+ QWidget,
+)
+from src.core.library.alchemy.enums import TagColorEnum
+from src.core.library.alchemy.models import TagColorGroup
+from src.core.palette import ColorType, get_tag_color
+from src.qt.translations import Translations
+
+logger = structlog.get_logger(__name__)
+
+
+class TagColorPreview(QWidget):
+ on_click = Signal()
+
+ def __init__(
+ self,
+ tag_color_group: TagColorGroup | None,
+ ) -> None:
+ super().__init__()
+ self.tag_color_group = tag_color_group
+
+ self.setCursor(Qt.CursorShape.PointingHandCursor)
+ self.base_layout = QVBoxLayout(self)
+ self.base_layout.setObjectName("baseLayout")
+ self.base_layout.setContentsMargins(0, 0, 0, 0)
+
+ self.button = QPushButton(self)
+ self.button.setFlat(True)
+ self.button.setMinimumSize(56, 28)
+ self.button.setMaximumHeight(28)
+ self.button.clicked.connect(self.on_click.emit)
+
+ self.base_layout.addWidget(self.button)
+
+ self.set_tag_color_group(tag_color_group)
+
+ def set_tag_color_group(self, tag_color_group: TagColorGroup | None):
+ self.tag_color_group = tag_color_group
+
+ if tag_color_group:
+ self.button.setText(tag_color_group.name)
+ else:
+ Translations.translate_qobject(self.button, "generic.none")
+
+ primary_color = get_primary_color(tag_color_group)
+ border_color = (
+ get_border_color(primary_color)
+ if not (tag_color_group and tag_color_group.secondary)
+ else (QColor(tag_color_group.secondary))
+ )
+ highlight_color = get_highlight_color(
+ primary_color
+ if not (tag_color_group and tag_color_group.secondary)
+ else QColor(tag_color_group.secondary)
+ )
+ text_color: QColor
+ if tag_color_group and tag_color_group.secondary:
+ text_color = QColor(tag_color_group.secondary)
+ else:
+ text_color = get_text_color(primary_color, highlight_color)
+
+ self.button.setStyleSheet(
+ f"QPushButton{{"
+ f"background: rgba{primary_color.toTuple()};"
+ f"color: rgba{text_color.toTuple()};"
+ f"font-weight: 600;"
+ f"border-color: rgba{border_color.toTuple()};"
+ f"border-radius: 6px;"
+ f"border-style:solid;"
+ f"border-width: 2px;"
+ f"padding-right: 8px;"
+ f"padding-bottom: 1px;"
+ f"padding-left: 8px;"
+ f"font-size: 14px"
+ f"}}"
+ f"QPushButton::hover{{"
+ f"border-color: rgba{highlight_color.toTuple()};"
+ f"}}"
+ )
+ self.button.setMaximumWidth(self.button.sizeHint().width())
+
+
+def get_primary_color(tag_color_group: TagColorGroup | None) -> QColor:
+ primary_color = QColor(
+ get_tag_color(ColorType.PRIMARY, TagColorEnum.DEFAULT)
+ if not tag_color_group
+ else tag_color_group.primary
+ )
+
+ return primary_color
+
+
+def get_border_color(primary_color: QColor) -> QColor:
+ border_color: QColor = QColor(primary_color)
+ border_color.setRed(min(border_color.red() + 20, 255))
+ border_color.setGreen(min(border_color.green() + 20, 255))
+ border_color.setBlue(min(border_color.blue() + 20, 255))
+
+ return border_color
+
+
+def get_highlight_color(primary_color: QColor) -> QColor:
+ highlight_color: QColor = QColor(primary_color)
+ highlight_color = highlight_color.toHsl()
+ highlight_color.setHsl(highlight_color.hue(), min(highlight_color.saturation(), 200), 225, 255)
+ highlight_color = highlight_color.toRgb()
+
+ return highlight_color
+
+
+def get_text_color(primary_color: QColor, highlight_color: QColor) -> QColor:
+ if primary_color.lightness() > 120:
+ text_color = QColor(primary_color)
+ text_color = text_color.toHsl()
+ text_color.setHsl(text_color.hue(), text_color.saturation(), 50, 255)
+ return text_color.toRgb()
+ else:
+ return highlight_color
diff --git a/tagstudio/tests/conftest.py b/tagstudio/tests/conftest.py
index ae386f80..99cc3de9 100644
--- a/tagstudio/tests/conftest.py
+++ b/tagstudio/tests/conftest.py
@@ -11,7 +11,6 @@ sys.path.insert(0, str(CWD.parent))
from src.core.library import Entry, Library, Tag
from src.core.library import alchemy as backend
-from src.core.library.alchemy.enums import TagColor
from src.qt.ts_qt import QtDriver
@@ -67,21 +66,24 @@ def library(request):
tag = Tag(
name="foo",
- color=TagColor.RED,
+ color_namespace="tagstudio-standard",
+ color_slug="red",
)
assert lib.add_tag(tag)
parent_tag = Tag(
id=1500,
name="subbar",
- color=TagColor.YELLOW,
+ color_namespace="tagstudio-standard",
+ color_slug="yellow",
)
assert lib.add_tag(parent_tag)
tag2 = Tag(
id=2000,
name="bar",
- color=TagColor.BLUE,
+ color_namespace="tagstudio-standard",
+ color_slug="blue",
parent_tags={parent_tag},
)
assert lib.add_tag(tag2)
@@ -154,7 +156,7 @@ def qt_driver(qtbot, library):
@pytest.fixture
def generate_tag():
def inner(name, **kwargs):
- params = dict(name=name, color=TagColor.RED) | kwargs
+ params = dict(name=name, color_namespace="tagstudio-standard", color_slug="red") | kwargs
return Tag(**params)
yield inner
diff --git a/tagstudio/tests/fixtures/search_library/.TagStudio/ts_library.json b/tagstudio/tests/fixtures/search_library/.TagStudio/ts_library.json
index ff3fb742..d93928b4 100644
--- a/tagstudio/tests/fixtures/search_library/.TagStudio/ts_library.json
+++ b/tagstudio/tests/fixtures/search_library/.TagStudio/ts_library.json
@@ -1 +1,431 @@
-{"ts-version":"9.4.2","ext_list":[".json",".xmp",".aae",".txt"],"is_exclude_list":true,"tags":[{"id":0,"name":"Archived","aliases":["Archive"],"color":"Red"},{"id":1,"name":"Favorite","aliases":["Favorited","Favorites"],"color":"Yellow"},{"id":1000,"name":"Parent","aliases":[""],"subtag_ids":[1000]},{"id":1001,"name":"Default","aliases":[""]},{"id":1002,"name":"Black","aliases":[""],"subtag_ids":[1040],"color":"black"},{"id":1003,"name":"Dark Gray","aliases":["Dark Grey"],"subtag_ids":[1040,1002,1004],"color":"dark gray"},{"id":1004,"name":"Gray","aliases":["Grey"],"subtag_ids":[1040,1002,1006],"color":"gray"},{"id":1005,"name":"Light Gray","aliases":["Light Grey"],"subtag_ids":[1040,1006,1004],"color":"light gray"},{"id":1006,"name":"White","aliases":[""],"subtag_ids":[1040],"color":"white"},{"id":1007,"name":"Light Pink","aliases":[""],"subtag_ids":[1040,1009,1006],"color":"light pink"},{"id":1008,"name":"Pink","aliases":[""],"subtag_ids":[1040,1006,1009],"color":"pink"},{"id":1009,"name":"Red","aliases":[""],"subtag_ids":[1040],"color":"red"},{"id":1010,"name":"Red Orange","aliases":[""],"subtag_ids":[1040,1009,1011],"color":"red orange"},{"id":1011,"name":"Orange","aliases":[""],"subtag_ids":[1040,1009,1013],"color":"orange"},{"id":1012,"name":"Yellow Orange","aliases":[""],"subtag_ids":[1040,1011],"color":"yellow orange"},{"id":1013,"name":"Yellow","aliases":[""],"subtag_ids":[1040],"color":"yellow"},{"id":1014,"name":"Lime","aliases":[""],"subtag_ids":[1040,1017,1006],"color":"lime"},{"id":1015,"name":"Light Green","aliases":[""],"color":"light green"},{"id":1016,"name":"Mint","aliases":[""],"subtag_ids":[1040,1017,1019],"color":"mint"},{"id":1017,"name":"Green","aliases":[""],"subtag_ids":[1040,1021,1013],"color":"green"},{"id":1018,"name":"Teal","aliases":[""],"subtag_ids":[1040,1017,1021],"color":"teal"},{"id":1019,"name":"Cyan","aliases":[""],"subtag_ids":[1040,1017,1021],"color":"cyan"},{"id":1020,"name":"Light Blue","aliases":[""],"subtag_ids":[1040,1021,1006],"color":"light blue"},{"id":1021,"name":"Blue","aliases":[""],"subtag_ids":[1040],"color":"blue"},{"id":1022,"name":"Blue Violet","aliases":[""],"subtag_ids":[1040,1021,1023],"color":"blue violet"},{"id":1023,"name":"Violet","aliases":[""],"subtag_ids":[1040,1009,1021],"color":"violet"},{"id":1024,"name":"Purple","aliases":[""],"subtag_ids":[1040,1009,1021],"color":"purple"},{"id":1025,"name":"Lavender","aliases":[""],"subtag_ids":[1040,1024,1006],"color":"lavender"},{"id":1026,"name":"Berry","aliases":[""],"color":"berry"},{"id":1027,"name":"Magenta","aliases":[""],"color":"magenta"},{"id":1028,"name":"Salmon","aliases":[""],"color":"salmon"},{"id":1029,"name":"Auburn","aliases":[""],"color":"auburn"},{"id":1030,"name":"Dark Brown","aliases":[""],"color":"dark brown"},{"id":1031,"name":"Brown","aliases":[""],"color":"brown"},{"id":1032,"name":"Light Brown","aliases":[""],"color":"light brown"},{"id":1033,"name":"Blonde","aliases":[""],"color":"blonde"},{"id":1034,"name":"Peach","aliases":[""],"color":"peach"},{"id":1035,"name":"Warm Gray","aliases":["Warm Grey"],"subtag_ids":[1040,1004,1011],"color":"warm gray"},{"id":1036,"name":"Cool Gray","aliases":["Cool Grey"],"subtag_ids":[1040,1004,1021],"color":"cool gray"},{"id":1037,"name":"Olive","aliases":[""],"subtag_ids":[1040,1017,1004],"color":"olive"},{"id":1038,"name":"Square","aliases":[""],"subtag_ids":[1039]},{"id":1039,"name":"Shape","aliases":[""]},{"id":1040,"name":"Color","aliases":[""]},{"id":1041,"name":"Circle","aliases":[""],"subtag_ids":[1039,1042]},{"id":1042,"name":"Ellipse","aliases":[""],"subtag_ids":[1039,1043]},{"id":1043,"name":"Round","aliases":[""]}],"collations":[],"fields":[],"macros":[],"entries":[{"id":0,"filename":"red.jpg","path":"inherit colors shapes"},{"id":1,"filename":"red_square.jpg","path":"inherit colors shapes","fields":[{"6":[1009,1038]}]},{"id":2,"filename":"red_circle.jpg","path":"inherit colors shapes","fields":[{"6":[1041,1009]}]},{"id":3,"filename":"blue_circle.jpg","path":"inherit colors shapes","fields":[{"6":[1021,1041]}]},{"id":4,"filename":"blue_square.jpg","path":"inherit colors shapes","fields":[{"6":[1021,1038]}]},{"id":5,"filename":"blue.jpg","path":"inherit colors shapes","fields":[{"6":[1021]}]},{"id":10,"filename":"green_circle.png","path":"inherit colors shapes","fields":[{"6":[1041,1017]}]},{"id":11,"filename":"green.png","path":"inherit colors shapes","fields":[{"6":[1017]}]},{"id":12,"filename":"green_square.png","path":"inherit colors shapes","fields":[{"6":[1017,1038]}]},{"id":13,"filename":"yellow_circle.png","path":"inherit colors shapes","fields":[{"6":[1041,1013]}]},{"id":14,"filename":"yellow_square.png","path":"inherit colors shapes","fields":[{"6":[1038,1013]}]},{"id":15,"filename":"yellow.png","path":"inherit colors shapes","fields":[{"6":[1013]}]},{"id":16,"filename":"square.png","path":"inherit colors shapes","fields":[{"6":[1038]}]},{"id":17,"filename":"circle.png","path":"inherit colors shapes","fields":[{"6":[1041]}]},{"id":18,"filename":"shape.png","path":"inherit colors shapes","fields":[{"6":[1039]}]},{"id":19,"filename":"orange_circle.png","path":"inherit colors shapes","fields":[{"6":[1041,1011]}]},{"id":20,"filename":"orange_square.png","path":"inherit colors shapes","fields":[{"6":[1011,1038]}]},{"id":21,"filename":"orange.png","path":"inherit colors shapes","fields":[{"6":[1011]}]},{"id":22,"filename":"yellow_ellipse.png","path":"inherit colors shapes","fields":[{"6":[1042,1013]}]},{"id":23,"filename":"ellipse.png","path":"inherit colors shapes","fields":[{"6":[1042]}]},{"id":24,"filename":"red_ellipse.png","path":"inherit colors shapes","fields":[{"6":[1042,1009]}]},{"id":25,"filename":"blue_ellipse.png","path":"inherit colors shapes","fields":[{"6":[1021,1042]}]},{"id":26,"filename":"green_ellipse.png","path":"inherit colors shapes","fields":[{"6":[1042,1017]}]},{"id":27,"filename":"orange_ellipse.png","path":"inherit colors shapes","fields":[{"6":[1042,1011]}]},{"id":30,"filename":"r_circle_b_square.png","path":"comp colors shapes","fields":[{"6":[1021,1041,1009,1038]}]},{"id":31,"filename":"r_circle_g_square.png","path":"comp colors shapes","fields":[{"6":[1041,1017,1009,1038]}]},{"id":32,"filename":"r_circle_y_square.png","path":"comp colors shapes","fields":[{"6":[1041,1009,1038,1013]}]},{"id":33,"filename":"r_circle_o_square.png","path":"comp colors shapes","fields":[{"6":[1041,1011,1009,1038]}]},{"id":34,"filename":"r_circle_r_square.png","path":"comp colors shapes","fields":[{"6":[1041,1009,1038]}]}]}
\ No newline at end of file
+{
+ "ts-version": "9.4.2",
+ "ext_list": [".json", ".xmp", ".aae", ".txt"],
+ "is_exclude_list": true,
+ "tags": [
+ { "id": 0, "name": "Archived", "aliases": ["Archive"], "color": "Red" },
+ {
+ "id": 1,
+ "name": "Favorite",
+ "aliases": ["Favorited", "Favorites"],
+ "color": "Yellow"
+ },
+ { "id": 1000, "name": "Parent", "aliases": [""], "subtag_ids": [1000] },
+ { "id": 1001, "name": "Default", "aliases": [""] },
+ {
+ "id": 1002,
+ "name": "Black",
+ "aliases": [""],
+ "subtag_ids": [1040],
+ "color": "black"
+ },
+ {
+ "id": 1003,
+ "name": "Dark Gray",
+ "aliases": ["Dark Grey"],
+ "subtag_ids": [1040, 1002, 1004],
+ "color": "dark gray"
+ },
+ {
+ "id": 1004,
+ "name": "Gray",
+ "aliases": ["Grey"],
+ "subtag_ids": [1040, 1002, 1006],
+ "color": "gray"
+ },
+ {
+ "id": 1005,
+ "name": "Light Gray",
+ "aliases": ["Light Grey"],
+ "subtag_ids": [1040, 1006, 1004],
+ "color": "light gray"
+ },
+ {
+ "id": 1006,
+ "name": "White",
+ "aliases": [""],
+ "subtag_ids": [1040],
+ "color": "white"
+ },
+ {
+ "id": 1007,
+ "name": "Light Pink",
+ "aliases": [""],
+ "subtag_ids": [1040, 1009, 1006],
+ "color": "light pink"
+ },
+ {
+ "id": 1008,
+ "name": "Pink",
+ "aliases": [""],
+ "subtag_ids": [1040, 1006, 1009],
+ "color": "pink"
+ },
+ {
+ "id": 1009,
+ "name": "Red",
+ "aliases": [""],
+ "subtag_ids": [1040],
+ "color": "red"
+ },
+ {
+ "id": 1010,
+ "name": "Red Orange",
+ "aliases": [""],
+ "subtag_ids": [1040, 1009, 1011],
+ "color": "red orange"
+ },
+ {
+ "id": 1011,
+ "name": "Orange",
+ "aliases": [""],
+ "subtag_ids": [1040, 1009, 1013],
+ "color": "orange"
+ },
+ {
+ "id": 1012,
+ "name": "Yellow Orange",
+ "aliases": [""],
+ "subtag_ids": [1040, 1011],
+ "color": "yellow orange"
+ },
+ {
+ "id": 1013,
+ "name": "Yellow",
+ "aliases": [""],
+ "subtag_ids": [1040],
+ "color": "yellow"
+ },
+ {
+ "id": 1014,
+ "name": "Lime",
+ "aliases": [""],
+ "subtag_ids": [1040, 1017, 1006],
+ "color": "lime"
+ },
+ {
+ "id": 1015,
+ "name": "Light Green",
+ "aliases": [""],
+ "color": "light green"
+ },
+ {
+ "id": 1016,
+ "name": "Mint",
+ "aliases": [""],
+ "subtag_ids": [1040, 1017, 1019],
+ "color": "mint"
+ },
+ {
+ "id": 1017,
+ "name": "Green",
+ "aliases": [""],
+ "subtag_ids": [1040, 1021, 1013],
+ "color": "green"
+ },
+ {
+ "id": 1018,
+ "name": "Teal",
+ "aliases": [""],
+ "subtag_ids": [1040, 1017, 1021],
+ "color": "teal"
+ },
+ {
+ "id": 1019,
+ "name": "Cyan",
+ "aliases": [""],
+ "subtag_ids": [1040, 1017, 1021],
+ "color": "cyan"
+ },
+ {
+ "id": 1020,
+ "name": "Light Blue",
+ "aliases": [""],
+ "subtag_ids": [1040, 1021, 1006],
+ "color": "light blue"
+ },
+ {
+ "id": 1021,
+ "name": "Blue",
+ "aliases": [""],
+ "subtag_ids": [1040],
+ "color": "blue"
+ },
+ {
+ "id": 1022,
+ "name": "Blue Violet",
+ "aliases": [""],
+ "subtag_ids": [1040, 1021, 1023],
+ "color": "blue violet"
+ },
+ {
+ "id": 1023,
+ "name": "Violet",
+ "aliases": [""],
+ "subtag_ids": [1040, 1009, 1021],
+ "color": "violet"
+ },
+ {
+ "id": 1024,
+ "name": "Purple",
+ "aliases": [""],
+ "subtag_ids": [1040, 1009, 1021],
+ "color": "purple"
+ },
+ {
+ "id": 1025,
+ "name": "Lavender",
+ "aliases": [""],
+ "subtag_ids": [1040, 1024, 1006],
+ "color": "lavender"
+ },
+ { "id": 1026, "name": "Berry", "aliases": [""], "color": "berry" },
+ { "id": 1027, "name": "Magenta", "aliases": [""], "color": "magenta" },
+ { "id": 1028, "name": "Salmon", "aliases": [""], "color": "salmon" },
+ { "id": 1029, "name": "Auburn", "aliases": [""], "color": "auburn" },
+ {
+ "id": 1030,
+ "name": "Dark Brown",
+ "aliases": [""],
+ "color": "dark brown"
+ },
+ { "id": 1031, "name": "Brown", "aliases": [""], "color": "brown" },
+ {
+ "id": 1032,
+ "name": "Light Brown",
+ "aliases": [""],
+ "color": "light brown"
+ },
+ { "id": 1033, "name": "Blonde", "aliases": [""], "color": "blonde" },
+ { "id": 1034, "name": "Peach", "aliases": [""], "color": "peach" },
+ {
+ "id": 1035,
+ "name": "Warm Gray",
+ "aliases": ["Warm Grey"],
+ "subtag_ids": [1040, 1004, 1011],
+ "color": "warm gray"
+ },
+ {
+ "id": 1036,
+ "name": "Cool Gray",
+ "aliases": ["Cool Grey"],
+ "subtag_ids": [1040, 1004, 1021],
+ "color": "cool gray"
+ },
+ {
+ "id": 1037,
+ "name": "Olive",
+ "aliases": [""],
+ "subtag_ids": [1040, 1017, 1004],
+ "color": "olive"
+ },
+ { "id": 1038, "name": "Square", "aliases": [""], "subtag_ids": [1039] },
+ { "id": 1039, "name": "Shape", "aliases": [""] },
+ { "id": 1040, "name": "Color", "aliases": [""] },
+ {
+ "id": 1041,
+ "name": "Circle",
+ "aliases": [""],
+ "subtag_ids": [1039, 1042]
+ },
+ {
+ "id": 1042,
+ "name": "Ellipse",
+ "aliases": [""],
+ "subtag_ids": [1039, 1043]
+ },
+ { "id": 1043, "name": "Round", "aliases": [""] }
+ ],
+ "collations": [],
+ "fields": [],
+ "macros": [],
+ "entries": [
+ {
+ "id": 0,
+ "filename": "red.jpg",
+ "path": "inherit colors shapes",
+ "fields": [{ "6": [1009] }]
+ },
+ {
+ "id": 1,
+ "filename": "red_square.jpg",
+ "path": "inherit colors shapes",
+ "fields": [{ "6": [1009, 1038] }]
+ },
+ {
+ "id": 2,
+ "filename": "red_circle.jpg",
+ "path": "inherit colors shapes",
+ "fields": [{ "6": [1041, 1009] }]
+ },
+ {
+ "id": 3,
+ "filename": "blue_circle.jpg",
+ "path": "inherit colors shapes",
+ "fields": [{ "6": [1021, 1041] }]
+ },
+ {
+ "id": 4,
+ "filename": "blue_square.jpg",
+ "path": "inherit colors shapes",
+ "fields": [{ "6": [1021, 1038] }]
+ },
+ {
+ "id": 5,
+ "filename": "blue.jpg",
+ "path": "inherit colors shapes",
+ "fields": [{ "6": [1021] }]
+ },
+ {
+ "id": 10,
+ "filename": "green_circle.png",
+ "path": "inherit colors shapes",
+ "fields": [{ "6": [1041, 1017] }]
+ },
+ {
+ "id": 11,
+ "filename": "green.png",
+ "path": "inherit colors shapes",
+ "fields": [{ "6": [1017] }]
+ },
+ {
+ "id": 12,
+ "filename": "green_square.png",
+ "path": "inherit colors shapes",
+ "fields": [{ "6": [1017, 1038] }]
+ },
+ {
+ "id": 13,
+ "filename": "yellow_circle.png",
+ "path": "inherit colors shapes",
+ "fields": [{ "6": [1041, 1013] }]
+ },
+ {
+ "id": 14,
+ "filename": "yellow_square.png",
+ "path": "inherit colors shapes",
+ "fields": [{ "6": [1038, 1013] }]
+ },
+ {
+ "id": 15,
+ "filename": "yellow.png",
+ "path": "inherit colors shapes",
+ "fields": [{ "6": [1013] }]
+ },
+ {
+ "id": 16,
+ "filename": "square.png",
+ "path": "inherit colors shapes",
+ "fields": [{ "6": [1038] }]
+ },
+ {
+ "id": 17,
+ "filename": "circle.png",
+ "path": "inherit colors shapes",
+ "fields": [{ "6": [1041] }]
+ },
+ {
+ "id": 18,
+ "filename": "shape.png",
+ "path": "inherit colors shapes",
+ "fields": [{ "6": [1039] }]
+ },
+ {
+ "id": 19,
+ "filename": "orange_circle.png",
+ "path": "inherit colors shapes",
+ "fields": [{ "6": [1041, 1011] }]
+ },
+ {
+ "id": 20,
+ "filename": "orange_square.png",
+ "path": "inherit colors shapes",
+ "fields": [{ "6": [1011, 1038] }]
+ },
+ {
+ "id": 21,
+ "filename": "orange.png",
+ "path": "inherit colors shapes",
+ "fields": [{ "6": [1011] }]
+ },
+ {
+ "id": 22,
+ "filename": "yellow_ellipse.png",
+ "path": "inherit colors shapes",
+ "fields": [{ "6": [1042, 1013] }]
+ },
+ {
+ "id": 23,
+ "filename": "ellipse.png",
+ "path": "inherit colors shapes",
+ "fields": [{ "6": [1042] }]
+ },
+ {
+ "id": 24,
+ "filename": "red_ellipse.png",
+ "path": "inherit colors shapes",
+ "fields": [{ "6": [1042, 1009] }]
+ },
+ {
+ "id": 25,
+ "filename": "blue_ellipse.png",
+ "path": "inherit colors shapes",
+ "fields": [{ "6": [1021, 1042] }]
+ },
+ {
+ "id": 26,
+ "filename": "green_ellipse.png",
+ "path": "inherit colors shapes",
+ "fields": [{ "6": [1042, 1017] }]
+ },
+ {
+ "id": 27,
+ "filename": "orange_ellipse.png",
+ "path": "inherit colors shapes",
+ "fields": [{ "6": [1042, 1011] }]
+ },
+ {
+ "id": 30,
+ "filename": "r_circle_b_square.png",
+ "path": "comp colors shapes",
+ "fields": [{ "6": [1021, 1041, 1009, 1038] }]
+ },
+ {
+ "id": 31,
+ "filename": "r_circle_g_square.png",
+ "path": "comp colors shapes",
+ "fields": [{ "6": [1041, 1017, 1009, 1038] }]
+ },
+ {
+ "id": 32,
+ "filename": "r_circle_y_square.png",
+ "path": "comp colors shapes",
+ "fields": [{ "6": [1041, 1009, 1038, 1013] }]
+ },
+ {
+ "id": 33,
+ "filename": "r_circle_o_square.png",
+ "path": "comp colors shapes",
+ "fields": [{ "6": [1041, 1011, 1009, 1038] }]
+ },
+ {
+ "id": 34,
+ "filename": "r_circle_r_square.png",
+ "path": "comp colors shapes",
+ "fields": [{ "6": [1041, 1009, 1038] }]
+ },
+ {
+ "id": 35,
+ "filename": "untagged.txt",
+ "path": ".",
+ "fields": [{ "0": "" }]
+ },
+ {
+ "id": 36,
+ "filename": "untagged.png",
+ "path": ".",
+ "fields": [{ "0": "I have fields, but no tags. I am not empty." }]
+ },
+ { "id": 37, "filename": "empty.png", "path": "." }
+ ]
+}
diff --git a/tagstudio/tests/fixtures/search_library/.TagStudio/ts_library.sqlite b/tagstudio/tests/fixtures/search_library/.TagStudio/ts_library.sqlite
index 2540a468..e4626219 100644
Binary files a/tagstudio/tests/fixtures/search_library/.TagStudio/ts_library.sqlite and b/tagstudio/tests/fixtures/search_library/.TagStudio/ts_library.sqlite differ
diff --git a/tagstudio/tests/qt/test_tag_panel.py b/tagstudio/tests/qt/test_tag_panel.py
index 77f21b2e..c0c31fda 100644
--- a/tagstudio/tests/qt/test_tag_panel.py
+++ b/tagstudio/tests/qt/test_tag_panel.py
@@ -15,7 +15,7 @@ def test_add_tag_callback(qt_driver):
# When
qt_driver.modal.widget.name_field.setText("xxx")
- qt_driver.modal.widget.color_field.setCurrentIndex(1)
+ # qt_driver.modal.widget.color_field.setCurrentIndex(1)
qt_driver.modal.saved.emit()
# Then