feat!: expanded tag color system (#709)

* attempt at adding `TagColor` table

* store both columns of `TagColor` inside `Tag` table

* fix: fix tag color relationships

* refactor: replace TagColor enums with TagColorGroup system

* ui: derive tag accent colors from primary

* refactor: move dynamic tag color logic, apply to "+" button

* feat(ui): add neon tag colors

* remove tag text color field

* ui: use secondary color for border and text

* ui: add TagColorPreview widget

* feat(ui): add new tag color selector

* ui: add color name tooltips

* ui: tweak tag colors and selector

* feat: add `namespaces` table

* translations: add + update translation keys

* tests: update fixtures

* chore: code cleanup

* ui: add spacing between color groups

* fix: expand refactor to create and add button

* chore: format with ruff
This commit is contained in:
Travis Abendshien
2025-01-27 11:39:42 -08:00
committed by GitHub
parent 4c337cb1a3
commit c06f3bb336
20 changed files with 1619 additions and 396 deletions

View File

@@ -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 <b>9.4 and below</b> will need to be migrated to the new <b>v9.5+</b> format.<br><h2>What you need to know:</h2><ul><li>Your existing library save file will <b><i>NOT</i></b> be deleted</li><li>Your personal files will <b><i>NOT</i></b> be deleted, moved, or modified</li><li>The new v9.5+ save format can not be opened in earlier versions of TagStudio</li></ul>",
"json_migration.info.description": "Library save files created with TagStudio versions <b>9.4 and below</b> will need to be migrated to the new <b>v9.5+</b> format.<br><h2>What you need to know:</h2><ul><li>Your existing library save file will <b><i>NOT</i></b> be deleted</li><li>Your personal files will <b><i>NOT</i></b> be deleted, moved, or modified</li><li>The new v9.5+ save format can not be opened in earlier versions of TagStudio</li></ul><h3>What's changed:</h3><ul><li>\"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.</li><li>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.</li></ul><ul>",
"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",

View File

@@ -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

View File

@@ -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}"))

View File

@@ -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,
]

View File

@@ -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):

View File

@@ -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

View File

@@ -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

View File

@@ -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:

View File

@@ -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("<h3>Properties</h3>"))
@@ -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()

View File

@@ -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"}}"
)

View File

@@ -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"<h4>{display_name if display_name else group}</h4>")
)
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"<h4>{no_color_str}</h4>"))
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

View File

@@ -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"}}"
)

View File

@@ -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}"

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

File diff suppressed because one or more lines are too long

View File

@@ -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