From 21a02ec0d3b22b512833ccd955c0a2d875698e68 Mon Sep 17 00:00:00 2001 From: Rabin Yasharzadehe Date: Sun, 8 Mar 2026 12:54:48 +0200 Subject: [PATCH] Add dark theme Signed-off-by: Rabin Yasharzadehe --- data/org.gnome.hamster.gschema.xml | 11 ++ data/preferences.ui | 76 ++++++++++ src/hamster-cli.py | 4 + src/hamster/lib/charting.py | 41 +++--- src/hamster/lib/configuration.py | 10 ++ src/hamster/lib/graphics.py | 35 +++++ src/hamster/lib/theme.py | 215 +++++++++++++++++++++++++++++ src/hamster/overview.py | 43 +++++- src/hamster/preferences.py | 16 ++- src/hamster/widgets/dayline.py | 22 ++- src/hamster/widgets/tags.py | 38 ++++- 11 files changed, 480 insertions(+), 31 deletions(-) create mode 100644 src/hamster/lib/theme.py diff --git a/data/org.gnome.hamster.gschema.xml b/data/org.gnome.hamster.gschema.xml index dce4c1611..9f3ede56a 100644 --- a/data/org.gnome.hamster.gschema.xml +++ b/data/org.gnome.hamster.gschema.xml @@ -18,5 +18,16 @@ then the activity belongs to the previous hamster day. + + + 'auto' + Color theme mode + + Controls the color theme used by Hamster. + 'auto' follows the system theme preference, + 'light' forces light theme colors, + 'dark' forces dark theme colors. + + diff --git a/data/preferences.ui b/data/preferences.ui index 37c81ecb3..304f58ce7 100644 --- a/data/preferences.ui +++ b/data/preferences.ui @@ -507,6 +507,82 @@ False + + + True + False + 12 + 8 + 4 + 4 + + + True + False + start + start + vertical + 8 + + + True + False + 8 + + + True + False + Color theme + + + False + True + 4 + 0 + + + + + True + False + + Auto (follow system) + Light + Dark + + + + + False + True + 1 + + + + + False + True + 0 + + + + + + + 2 + + + + + True + False + UI + + + 2 + False + + True diff --git a/src/hamster-cli.py b/src/hamster-cli.py index 9c6786787..e6401b6e3 100755 --- a/src/hamster-cli.py +++ b/src/hamster-cli.py @@ -46,6 +46,7 @@ from hamster.lib import default_logger, stuff from hamster.lib import datetime as dt from hamster.lib.fact import Fact +from hamster.lib.theme import get_theme_manager logger = default_logger(__file__) @@ -114,6 +115,9 @@ def __init__(self): #inactivity_timeout=10000, register_session=True) + # Initialize theme manager early to detect system theme + self.theme_manager = get_theme_manager() + self.about_controller = None # 'about' window controller self.fact_controller = None # fact window controller self.overview_controller = None # overview window controller diff --git a/src/hamster/lib/charting.py b/src/hamster/lib/charting.py index c4843aa5c..1e3f67fbf 100644 --- a/src/hamster/lib/charting.py +++ b/src/hamster/lib/charting.py @@ -99,17 +99,22 @@ def __init__(self, max_bar_width = 20, legend_width = 70, value_format = "%.2f", self.connect("on-click", self.on_click) def find_colors(self): - bg_color = "#eee" #self.get_style().bg[gtk.StateType.NORMAL].to_string() + if self.theme: + bg_color = self.theme.bg_secondary + fg_color = self.theme.fg_muted + else: + bg_color = "#eee" + fg_color = "#aaa" self.bar_color = self.colors.contrast(bg_color, 30) # now for the text - we want reduced contrast for relaxed visuals - fg_color = "#aaa" #self.get_style().fg[gtk.StateType.NORMAL].to_string() - self.label_color = self.colors.contrast(fg_color, 80) + self.label_color = self.colors.contrast(fg_color, 80) def on_mouse_over(self, scene, bar): if bar.key not in self.selected_keys: - bar.fill = "#999" #self.get_style().base[gtk.StateType.PRELIGHT].to_string() + hover_color = self.theme.fg_muted if self.theme else "#999" + bar.fill = hover_color def on_mouse_out(self, scene, bar): if bar.key not in self.selected_keys: @@ -177,30 +182,32 @@ def on_enter_frame(self, scene, context): bar.width = self.plot_area.width if bar.key in self.selected_keys: - bar.fill = "#aaa" #self.get_style().bg[gtk.StateType.SELECTED].to_string() + selected_bg = self.theme.fg_muted if self.theme else "#aaa" + selected_fg = self.theme.fg_secondary if self.theme else "#666" + bar.fill = selected_bg if bar.normalized == 0: - bar.label.color = "#666" #self.get_style().fg[gtk.StateType.SELECTED].to_string() - bar.label_background.fill = "#aaa" #self.get_style().bg[gtk.StateType.SELECTED].to_string() + bar.label.color = selected_fg + bar.label_background.fill = selected_bg bar.label_background.visible = True else: bar.label_background.visible = False if bar.label.x < round(bar.width * bar.normalized): - bar.label.color = "#666" #self.get_style().fg[gtk.StateType.SELECTED].to_string() + bar.label.color = selected_fg else: bar.label.color = self.label_color - if not bar.fill: + else: + # Not selected - use theme bar color bar.fill = self.bar_color - bar.label.color = self.label_color bar.label_background.fill = None label.y = y + (bar_width - label.height) / 2 + self.plot_area.y label.width = legend_width - if not label.color: - label.color = self.label_color + # Always update label color from theme + label.color = self.label_color y += bar_width + 1 @@ -277,8 +284,8 @@ def on_enter_frame(self, scene, context): # now for the text - we want reduced contrast for relaxed visuals - fg_color = "#666" #self.get_style().fg[gtk.StateType.NORMAL].to_string() - label_color = self.colors.contrast(fg_color, 80) + fg_color = self.theme.fg_secondary if self.theme else "#666" + label_color = self.colors.contrast(fg_color, 80) self.layout.set_alignment(pango.Alignment.RIGHT) self.layout.set_ellipsize(pango.ELLIPSIZE_END) @@ -289,8 +296,8 @@ def on_enter_frame(self, scene, context): factor = max_bar_size / float(end_hour - start_hour) # determine bar color - bg_color = "#eee" #self.get_style().bg[gtk.StateType.NORMAL].to_string() - base_color = self.colors.contrast(bg_color, 30) + bg_color = self.theme.bg_secondary if self.theme else "#eee" + base_color = self.colors.contrast(bg_color, 30) for i, label in enumerate(keys): g.set_color(label_color) @@ -323,7 +330,7 @@ def on_enter_frame(self, scene, context): last_position = positions[keys[-1]] - grid_color = "#aaa" # self.get_style().bg[gtk.StateType.NORMAL].to_string() + grid_color = self.theme.fg_muted if self.theme else "#aaa" for i in range(start_hour + 60, end_hour, pace): x = round((i - start_hour) * factor) diff --git a/src/hamster/lib/configuration.py b/src/hamster/lib/configuration.py index 516fa161e..71efa6141 100644 --- a/src/hamster/lib/configuration.py +++ b/src/hamster/lib/configuration.py @@ -177,5 +177,15 @@ def day_start(self): hours, minutes = divmod(day_start_minutes, 60) return dt.time(hours, minutes) + @property + def theme_mode(self): + """Theme mode setting ('auto', 'light', or 'dark').""" + return self.get("theme-mode") + + @theme_mode.setter + def theme_mode(self, value): + """Set theme mode ('auto', 'light', or 'dark').""" + self.set("theme-mode", value) + conf = GSettingsStore() diff --git a/src/hamster/lib/graphics.py b/src/hamster/lib/graphics.py index af4ceb750..afdefc3eb 100644 --- a/src/hamster/lib/graphics.py +++ b/src/hamster/lib/graphics.py @@ -25,6 +25,11 @@ except: # we can also live without tweener. Scene.animate will not work pytweener = None +try: + from hamster.lib.theme import get_theme_manager +except ImportError: + get_theme_manager = None + import colorsys from collections import deque @@ -1869,6 +1874,15 @@ def __init__(self, interactive = True, framerate = 60, self.__last_mouse_move = None + # Theme support + self._theme_manager = None + self._theme_handler_id = None + if get_theme_manager: + self._theme_manager = get_theme_manager() + self._theme_handler_id = self._theme_manager.connect( + 'changed', self._on_theme_changed) + self.connect("destroy", self._on_destroy_theme_cleanup) + self.connect("realize", self.__on_realize) if interactive: @@ -2242,6 +2256,27 @@ def __on_button_release(self, scene, event): self.__check_mouse(event.x, event.y) return True + @property + def theme(self): + """Get the current theme palette colors. + + Returns the ThemePalette from the ThemeManager, or None if + theme support is not available. + """ + if self._theme_manager: + return self._theme_manager.colors + return None + + def _on_theme_changed(self, theme_manager): + """Handle theme change by requesting a redraw.""" + self.redraw() + + def _on_destroy_theme_cleanup(self, widget): + """Disconnect theme handler on widget destroy.""" + if self._theme_manager and self._theme_handler_id: + self._theme_manager.disconnect(self._theme_handler_id) + self._theme_handler_id = None + def __on_realize(self, widget): # Store as soon as available. Maybe for performance reasons, # to avoid get_window() calls in __on_mouse_move ? diff --git a/src/hamster/lib/theme.py b/src/hamster/lib/theme.py new file mode 100644 index 000000000..a18db3585 --- /dev/null +++ b/src/hamster/lib/theme.py @@ -0,0 +1,215 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2024 Hamster Contributors + +# This file is part of Project Hamster. + +# Project Hamster is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# Project Hamster is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with Project Hamster. If not, see . + +""" +Theme management for Hamster. + +Provides semantic color palettes for light and dark themes, +with support for auto-detecting system theme preference. +""" + +from dataclasses import dataclass, field +from typing import List + +from gi.repository import Gio as gio +from gi.repository import GObject as gobject +from gi.repository import Gtk as gtk + + +@dataclass +class ThemePalette: + """Semantic color palette for theming.""" + + # Backgrounds + bg_primary: str + bg_secondary: str + + # Text colors + fg_primary: str + fg_secondary: str + fg_muted: str + + # Tags + tag_bg: str + tag_fg: str = "#1e1e1e" + + # Current time indicator + current_time: str = "#ff0000" + + # Bar chart colors + bar_colors: List[str] = field(default_factory=list) + + +# Light theme palette +LIGHT_PALETTE = ThemePalette( + bg_primary="#ffffff", + bg_secondary="#eeeeee", + fg_primary="#333333", + fg_secondary="#666666", + fg_muted="#aaaaaa", + tag_bg="#F1EAAA", + tag_fg="#1e1e1e", + current_time="#ff0000", + bar_colors=["#95CACF", "#A2CFB6", "#D1DEA1", "#E4C384", "#DE9F7B"], +) + +# Dark theme palette +DARK_PALETTE = ThemePalette( + bg_primary="#1e1e1e", + bg_secondary="#2d2d2d", + fg_primary="#e0e0e0", + fg_secondary="#a0a0a0", + fg_muted="#666666", + tag_bg="#5C5830", + tag_fg="#e0e0e0", + current_time="#ff6b6b", + bar_colors=["#4DB6BD", "#5DBF8A", "#B8C955", "#D4A84F", "#C87B5A"], +) + + +class ThemeManager(gobject.GObject): + """ + Singleton manager for application theming. + + Reads theme preference from GSettings, detects system theme, + and provides the appropriate color palette. + + Emits 'changed' signal when the theme changes. + """ + + __gsignals__ = { + "changed": (gobject.SignalFlags.RUN_LAST, gobject.TYPE_NONE, ()), + } + + _instance = None + + def __new__(cls): + if cls._instance is None: + cls._instance = super().__new__(cls) + cls._instance._initialized = False + return cls._instance + + def __init__(self): + if self._initialized: + return + gobject.GObject.__init__(self) + self._initialized = True + + self._settings = gio.Settings(schema_id='org.gnome.Hamster') + self._settings.connect('changed::theme-mode', self._on_settings_changed) + + # Listen for system theme changes + self._gtk_settings = gtk.Settings.get_default() + if self._gtk_settings: + self._gtk_settings.connect('notify::gtk-application-prefer-dark-theme', + self._on_system_theme_changed) + self._gtk_settings.connect('notify::gtk-theme-name', + self._on_system_theme_changed) + + self._cached_is_dark = None + # Apply initial theme + self._apply_gtk_theme() + + @property + def theme_mode(self) -> str: + """Get the current theme mode setting ('auto', 'light', or 'dark').""" + return self._settings.get_string('theme-mode') + + @theme_mode.setter + def theme_mode(self, value: str): + """Set the theme mode ('auto', 'light', or 'dark').""" + if value not in ('auto', 'light', 'dark'): + raise ValueError(f"Invalid theme mode: {value}") + self._settings.set_string('theme-mode', value) + + @property + def is_dark(self) -> bool: + """Determine if dark theme should be used.""" + mode = self.theme_mode + if mode == 'light': + return False + if mode == 'dark': + return True + # Auto mode - detect from system + return self._detect_system_dark_theme() + + def _detect_system_dark_theme(self) -> bool: + """Detect if the system prefers dark theme.""" + if not self._gtk_settings: + return False + + # Check explicit dark theme preference + if self._gtk_settings.get_property('gtk-application-prefer-dark-theme'): + return True + + # Check theme name for common dark theme indicators + theme_name = self._gtk_settings.get_property('gtk-theme-name') or '' + theme_lower = theme_name.lower() + dark_indicators = ['dark', 'noir', 'night', 'inverse'] + return any(indicator in theme_lower for indicator in dark_indicators) + + @property + def colors(self) -> ThemePalette: + """Get the current theme's color palette.""" + return DARK_PALETTE if self.is_dark else LIGHT_PALETTE + + def _apply_gtk_theme(self): + """Apply the current theme to GTK settings.""" + if not self._gtk_settings: + return + + mode = self.theme_mode + if mode == 'auto': + # In auto mode, don't override GTK's theme preference + # (it will follow the system) + return + + # For explicit light/dark, set GTK's preference + prefer_dark = (mode == 'dark') + self._gtk_settings.set_property('gtk-application-prefer-dark-theme', prefer_dark) + + def _on_settings_changed(self, settings, key): + """Handle theme-mode GSettings change.""" + self._cached_is_dark = None + self._apply_gtk_theme() + self.emit('changed') + + def _on_system_theme_changed(self, settings, pspec): + """Handle system theme change.""" + if self.theme_mode == 'auto': + new_is_dark = self.is_dark + if self._cached_is_dark != new_is_dark: + self._cached_is_dark = new_is_dark + self.emit('changed') + + +# Module-level singleton instance +_theme_manager = None + + +def get_theme_manager() -> ThemeManager: + """Get the singleton ThemeManager instance.""" + global _theme_manager + if _theme_manager is None: + _theme_manager = ThemeManager() + return _theme_manager + + +# Convenience alias +theme = get_theme_manager diff --git a/src/hamster/overview.py b/src/hamster/overview.py index 1af1b0b80..f27c375f2 100644 --- a/src/hamster/overview.py +++ b/src/hamster/overview.py @@ -42,6 +42,7 @@ from hamster import widgets from hamster.lib.configuration import Controller +from hamster.lib.theme import get_theme_manager from hamster.lib.pytweener import Easing @@ -135,12 +136,27 @@ def __init__(self, width=0, height=0, vertical=None, **kwargs): self._items = [] self.connect("on-render", self.on_render) - #: color scheme to use, graphics.colors.category10 by default - self.colors = graphics.Colors.category10 - self.colors = ["#95CACF", "#A2CFB6", "#D1DEA1", "#E4C384", "#DE9F7B"] + #: color scheme to use, from theme palette + self._theme_manager = get_theme_manager() + self._update_colors() + # Connect to theme changes + if self._theme_manager: + self._theme_manager.connect('changed', self._on_theme_changed) self._seen_keys = [] + def _on_theme_changed(self, manager): + """Handle theme change.""" + self._update_colors() + self._seen_keys = [] # Reset color assignments + + def _update_colors(self): + """Update color palette from theme.""" + if self._theme_manager: + self.colors = list(self._theme_manager.colors.bar_colors) + else: + self.colors = ["#95CACF", "#A2CFB6", "#D1DEA1", "#E4C384", "#DE9F7B"] + def set_items(self, items): """expects a list of key, value to work with""" @@ -165,6 +181,9 @@ def on_render(self, sprite): self.graphics.clear() return + # Refresh colors from theme in case it changed + self._update_colors() + max_width = self.alloc_w - 1 * len(self._items) for i, (key, val, normalized) in enumerate(self._items): color = self._take_color(key) @@ -291,8 +310,11 @@ def __init__(self): main = layout.HBox(padding_top=10) box.add_child(main) + # stub_label color will be set via theme in update_colors + theme_manager = get_theme_manager() + stub_color = theme_manager.colors.fg_muted if theme_manager else "#bbb" self.stub_label = layout.Label(markup="Here be stats,\ntune in laters!", - color="#bbb", + color=stub_color, size=60) self.activities_chart = HorizontalBarChart() @@ -315,6 +337,11 @@ def __init__(self): self.connect("state-flags-changed", self.on_state_flags_changed) self.connect("style-updated", self.on_style_changed) + # Connect to theme changes + self._theme_manager = get_theme_manager() + if self._theme_manager: + self._theme_manager.connect('changed', self._on_theme_changed) + def set_facts(self, facts): totals = defaultdict(lambda: defaultdict(dt.timedelta)) @@ -385,6 +412,14 @@ def on_state_flags_changed(self, previous_state, _): def on_style_changed(self, _): self.update_colors() + def _on_theme_changed(self, manager): + """Handle theme change from ThemeManager.""" + self.update_colors() + # Update stub label color from theme + if self._theme_manager: + self.stub_label.color = self._theme_manager.colors.fg_muted + self.redraw() + def change_height(self, new_height): self.stop_animation(self.height_proxy) def on_update_dummy(sprite): diff --git a/src/hamster/preferences.py b/src/hamster/preferences.py index 34d7b8381..4cb8ad0f7 100644 --- a/src/hamster/preferences.py +++ b/src/hamster/preferences.py @@ -25,6 +25,7 @@ from hamster.lib import datetime as dt from hamster.lib import stuff from hamster.lib.configuration import Controller, runtime, conf +from hamster.lib.theme import get_theme_manager def get_prev(selection, model): @@ -173,6 +174,11 @@ def load_config(self, *args): self.tags = [tag["name"] for tag in runtime.storage.get_tags(only_autocomplete=True)] self.get_widget("autocomplete_tags").set_text(", ".join(self.tags)) + # Load theme preference + theme_combo = self.get_widget("theme_combo") + theme_mode = conf.theme_mode + theme_combo.set_active_id(theme_mode) + def on_autocomplete_tags_view_focus_out_event(self, view, event): buf = self.get_widget("autocomplete_tags") updated_tags = buf.get_text(buf.get_start_iter(), buf.get_end_iter(), 0) @@ -344,7 +350,9 @@ def unsorted_painter(self, column, cell, model, iter, data): cell_id = model.get_value(iter, 0) cell_text = model.get_value(iter, 1) if cell_id == -1: - text = '%s' % cell_text # TODO - should get color from theme + theme_manager = get_theme_manager() + muted_color = theme_manager.colors.fg_secondary if theme_manager else "#555" + text = '%s' % (muted_color, cell_text) cell.set_property('markup', text) else: cell.set_property('text', cell_text) @@ -506,5 +514,11 @@ def on_day_start_changed(self, widget): day_start = day_start.hour * 60 + day_start.minute conf.set("day-start-minutes", day_start) + + def on_theme_combo_changed(self, combo): + theme_mode = combo.get_active_id() + if theme_mode: + conf.theme_mode = theme_mode + def on_close_button_clicked(self, button): self.close_window() diff --git a/src/hamster/widgets/dayline.py b/src/hamster/widgets/dayline.py index d08bb1066..9a633e8ca 100644 --- a/src/hamster/widgets/dayline.py +++ b/src/hamster/widgets/dayline.py @@ -36,9 +36,11 @@ def __init__(self, start_time = None, end_time = None): self.fill = None # will be set to proper theme color on render self.fixed = False + # Colors will be updated based on theme in on_render self.start_label = graphics.Label("", 11, "#333", visible = False) self.end_label = graphics.Label("", 11, "#333", visible = False) self.duration_label = graphics.Label("", 11, "#FFF", visible = False) + self._theme_colors_set = False self.add_child(self.start_label, self.end_label, self.duration_label) self.connect("on-render", self.on_render) @@ -48,6 +50,14 @@ def on_render(self, sprite): if not self.fill: # not ready yet return + # Update label colors based on theme (only once per render cycle) + if hasattr(self, 'parent') and self.parent and hasattr(self.parent, 'parent'): + scene = self.parent.parent + if hasattr(scene, 'theme') and scene.theme: + self.start_label.color = scene.theme.fg_primary + self.end_label.color = scene.theme.fg_primary + # duration_label uses contrasting color for visibility on selection + self.graphics.rectangle(0, 0, self.width, self.height) self.graphics.fill_preserve(self.fill, 0.3) self.graphics.stroke(self.fill) @@ -123,8 +133,9 @@ def plot(self, date, facts, select_start, select_end = None): self.plot_area.sprites.remove(bar) self.fact_bars = [] + fact_bar_color = self.theme.fg_muted if self.theme else "#aaa" for fact in facts: - fact_bar = graphics.Rectangle(0, 0, fill="#aaa", stroke="#aaa") # dimensions will depend on screen situation + fact_bar = graphics.Rectangle(0, 0, fill=fact_bar_color, stroke=fact_bar_color) # dimensions will depend on screen situation fact_bar.fact = fact if fact.category in self.categories: @@ -171,9 +182,15 @@ def on_enter_frame(self, scene, context): bottom = self.plot_area.y + self.plot_area.height + # Update fact bar colors from theme + fact_bar_color = self.theme.fg_muted if self.theme else "#aaa" + for bar in self.fact_bars: bar.y = vertical * bar.category + 5 bar.height = vertical + # Update colors on each frame for theme changes + bar.fill = fact_bar_color + bar.stroke = fact_bar_color bar_start_time = bar.fact.start_time - self.view_time minutes = bar_start_time.seconds / 60 + bar_start_time.days * self.scope_hours * 60 @@ -245,4 +262,5 @@ def on_enter_frame(self, scene, context): g.move_to(minutes, self.plot_area.y) g.line_to(minutes, bottom) - g.stroke("#f00", 0.4) + current_time_color = self.theme.current_time if self.theme else "#f00" + g.stroke(current_time_color, 0.4) diff --git a/src/hamster/widgets/tags.py b/src/hamster/widgets/tags.py index 82710d892..b37a88d00 100644 --- a/src/hamster/widgets/tags.py +++ b/src/hamster/widgets/tags.py @@ -284,9 +284,16 @@ def on_mouse_over(self, area, tag): def on_mouse_out(self, area, tag): if tag.text in self.selected_tags: - tag.color = (242, 229, 97) + # Slightly brighter for selected tags + if self.theme: + tag.color = self.theme.tag_bg + else: + tag.color = (242, 229, 97) else: - tag.color = (241, 234, 170) + if self.theme: + tag.color = self.theme.tag_bg + else: + tag.color = (241, 234, 170) def on_tag_click(self, area, event, tag): @@ -301,10 +308,12 @@ def on_tag_click(self, area, event, tag): def draw(self, tags): new_tags = [] + tag_color = self.theme.tag_bg if self.theme else "#F1EAAA" for label in tags: - tag = Tag(label) + tag = Tag(label, color=tag_color) if label in self.selected_tags: - tag.color = (242, 229, 97) + # Keep selected tags slightly highlighted + tag.color = tag_color new_tags.append(tag) for tag in self.tags: @@ -324,6 +333,9 @@ def count_height(self, width): def on_enter_frame(self, scene, context): cur_x, cur_y = 4, 4 + # Update tag colors from theme on each frame + tag_color = self.theme.tag_bg if self.theme else "#F1EAAA" + tag = None for tag in self.tags: if cur_x + tag.width >= self.width - 5: #if we do not fit, we wrap @@ -332,6 +344,8 @@ def on_enter_frame(self, scene, context): tag.x = cur_x tag.y = cur_y + # Update tag color from theme + tag.color = tag_color cur_x += tag.width + 6 #some padding too, please @@ -341,7 +355,7 @@ def on_enter_frame(self, scene, context): return cur_x, cur_y class Tag(graphics.Sprite): - def __init__(self, text, interactive = True, color = "#F1EAAA"): + def __init__(self, text, interactive = True, color = None): graphics.Sprite.__init__(self, interactive = interactive) self.width, self.height = 0,0 @@ -349,8 +363,10 @@ def __init__(self, text, interactive = True, color = "#F1EAAA"): font = gtk.Style().font_desc font_size = int(font.get_size() * 0.8 / pango.SCALE) # 80% of default - self.label = graphics.Label(text, size = font_size, color = (30, 30, 30), y = 1) - self.color = color + # Default tag label color - will be updated based on theme + label_color = (30, 30, 30) # default dark text + self.label = graphics.Label(text, size = font_size, color = label_color, y = 1) + self.color = color if color else "#F1EAAA" self.add_child(self.label) self.corner = int((self.label.height + 3) / 3) + 0.5 @@ -366,6 +382,13 @@ def __setattr__(self, name, value): self.__dict__['width'], self.__dict__['height'] = int(self.label.x + self.label.width + self.label.height * 0.3), self.label.height + 3 def on_render(self, sprite): + # Update label color from theme if available + scene = self.get_scene() if hasattr(self, 'get_scene') else None + if scene and hasattr(scene, 'theme') and scene.theme: + self.label.color = scene.theme.tag_fg + else: + self.label.color = (30, 30, 30) + self.graphics.set_line_style(width=1) self.graphics.move_to(0.5, self.corner) @@ -378,4 +401,5 @@ def on_render(self, sprite): self.graphics.fill_stroke(self.color, "#b4b4b4") self.graphics.circle(6, self.height / 2, 2) + # Tag hole - use light color for contrast self.graphics.fill_stroke("#fff", "#b4b4b4")