diff --git a/tagstudio/resources/translations/en.json b/tagstudio/resources/translations/en.json
index 921cb6713..bbdbc391c 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!",
@@ -199,6 +201,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 74cdcbe4b..407d53996 100644
--- a/tagstudio/src/core/enums.py
+++ b/tagstudio/src/core/enums.py
@@ -70,4 +70,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 8cc168288..61269c2c8 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 000000000..d489dae15
--- /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 137f0edbe..952b81cde 100644
--- a/tagstudio/src/core/library/alchemy/enums.py
+++ b/tagstudio/src/core/library/alchemy/enums.py
@@ -8,7 +8,7 @@
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 24053a2f6..18e907414 100644
--- a/tagstudio/src/core/library/alchemy/library.py
+++ b/tagstudio/src/core/library/alchemy/library.py
@@ -53,8 +53,9 @@
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 @@
_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 @@ def migrate_json_to_sqlite(self, json_lib: JsonLibrary):
# 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 @@ def open_sqlite_library(self, library_dir: Path, is_new: bool) -> LibraryStatus:
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 @@ def save_library_backup_to_disk(self) -> Path:
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 @@ def get_tag_by_name(self, tag_name: str) -> Tag | None:
)
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 @@ def mirror_entry_fields(self, *entries: Entry) -> None:
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 b6ffd2e49..9d8f3a1b1 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 .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 @@ def __init__(self, name: str, tag_id: int | None = None):
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 @@ def __init__(
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 38b1d890d..4cc2bbd70 100644
--- a/tagstudio/src/core/palette.py
+++ b/tagstudio/src/core/palette.py
@@ -6,7 +6,7 @@
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 @@ class UiColor(IntEnum):
}
-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 acf2de9a7..e3c8897b3 100644
--- a/tagstudio/src/qt/modals/build_tag.py
+++ b/tagstudio/src/qt/modals/build_tag.py
@@ -11,7 +11,6 @@
from PySide6.QtWidgets import (
QApplication,
QCheckBox,
- QComboBox,
QFrame,
QHBoxLayout,
QLabel,
@@ -23,12 +22,14 @@
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 @@ def __init__(self, library: Library, tag: Tag | None = None):
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 @@ def __init__(self, library: Library, tag: Tag | None = None):
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 @@ def __init__(self, library: Library, tag: Tag | None = None):
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 @@ def __init__(self, library: Library, tag: Tag | None = None):
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 @@ def __init__(self, library: Library, tag: Tag | None = None):
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 @@ def remove_alias_callback(self, alias_name: str, alias_id: int | None = None):
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 @@ def add_aliases(self):
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 @@ def add_aliases(self):
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 @@ def _set_aliases(self):
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 @@ def _alias_name_change(self, item: CustomTableItem):
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 @@ def set_tag(self, tag: Tag):
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 @@ def on_name_changed(self):
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 ff32e5ebb..16ba54d2b 100644
--- a/tagstudio/src/qt/modals/folders_to_tags.py
+++ b/tagstudio/src/qt/modals/folders_to_tags.py
@@ -20,6 +20,7 @@
)
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 @@ def __init__(self, tag: Tag, parent_tag: Tag) -> None:
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 @@ def __init__(self, tag: Tag, parent_tag: Tag) -> None:
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 000000000..98cd9faba
--- /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 fdd986ab5..bf1ebc9f3 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 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 @@ def __build_row_item_widget(self, tag: Tag):
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 @@ def build_create_tag_button(self, query: str | None):
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 954ad4722..cc16ef74f 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.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 @@ def migration_iterator(self):
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 @@ def sanitize(value):
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 @@ def check_color_parity(self) -> bool:
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 de24bb44f..d75c97185 100644
--- a/tagstudio/src/qt/widgets/preview/preview_thumb.py
+++ b/tagstudio/src/qt/widgets/preview/preview_thumb.py
@@ -207,7 +207,7 @@ def _update_image(self, filepath: Path, ext: str) -> dict:
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 1af377e3c..decf97bd3 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 @@
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 @@ def __init__(
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 @@ def __init__(
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 @@ def __init__(
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 @@ def leaveEvent(self, event: QEvent) -> None: # noqa: N802
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 000000000..fcdb052ed
--- /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 ae386f80c..99cc3de9f 100644
--- a/tagstudio/tests/conftest.py
+++ b/tagstudio/tests/conftest.py
@@ -11,7 +11,6 @@
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 @@ class Args:
@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 ff3fb742b..d93928b4f 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 2540a4682..e46262193 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 77f21b2ef..c0c31fdaf 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