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
+
+
+
+ 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")