# Copyright (C) 2006-2007 Red Hat, Inc.
# Copyright (C) 2025 MostlyK
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
#
# SPDX-License-Identifier: LGPL-2.1-or-later
"""
XoColor - GTK4 Port
===================
This class represents all of the colors that the XO can take on.
Each pair of colors represents the fill color and the stroke color.
Ported to GTK4 from sugar3.graphics.xocolor
"""
import random
import logging
import gi
gi.require_version("Gio", "2.0")
from gi.repository import Gio
# Standard Sugar XO Colors palette
colors = [
["#B20008", "#FF2B34"],
["#FF2B34", "#B20008"],
["#E6000A", "#FF2B34"],
["#FF2B34", "#E6000A"],
["#FFADCE", "#FF2B34"],
["#9A5200", "#FF2B34"],
["#FF2B34", "#9A5200"],
["#FF8F00", "#FF2B34"],
["#FF2B34", "#FF8F00"],
["#FFC169", "#FF2B34"],
["#807500", "#FF2B34"],
["#FF2B34", "#807500"],
["#BE9E00", "#FF2B34"],
["#FF2B34", "#BE9E00"],
["#F8E800", "#FF2B34"],
["#008009", "#FF2B34"],
["#FF2B34", "#008009"],
["#00B20D", "#FF2B34"],
["#FF2B34", "#00B20D"],
["#8BFF7A", "#FF2B34"],
["#00588C", "#FF2B34"],
["#FF2B34", "#00588C"],
["#005FE4", "#FF2B34"],
["#FF2B34", "#005FE4"],
["#BCCDFF", "#FF2B34"],
["#5E008C", "#FF2B34"],
["#FF2B34", "#5E008C"],
["#7F00BF", "#FF2B34"],
["#FF2B34", "#7F00BF"],
["#D1A3FF", "#FF2B34"],
["#9A5200", "#FF8F00"],
["#FF8F00", "#9A5200"],
["#C97E00", "#FF8F00"],
["#FF8F00", "#C97E00"],
["#FFC169", "#FF8F00"],
["#807500", "#FF8F00"],
["#FF8F00", "#807500"],
["#BE9E00", "#FF8F00"],
["#FF8F00", "#BE9E00"],
["#F8E800", "#FF8F00"],
["#008009", "#FF8F00"],
["#FF8F00", "#008009"],
["#00B20D", "#FF8F00"],
["#FF8F00", "#00B20D"],
["#8BFF7A", "#FF8F00"],
["#00588C", "#FF8F00"],
["#FF8F00", "#00588C"],
["#005FE4", "#FF8F00"],
["#FF8F00", "#005FE4"],
["#BCCDFF", "#FF8F00"],
["#5E008C", "#FF8F00"],
["#FF8F00", "#5E008C"],
["#A700FF", "#FF8F00"],
["#FF8F00", "#A700FF"],
["#D1A3FF", "#FF8F00"],
["#B20008", "#FF8F00"],
["#FF8F00", "#B20008"],
["#FF2B34", "#FF8F00"],
["#FF8F00", "#FF2B34"],
["#FFADCE", "#FF8F00"],
["#807500", "#F8E800"],
["#F8E800", "#807500"],
["#BE9E00", "#F8E800"],
["#F8E800", "#BE9E00"],
["#FFFA00", "#EDDE00"],
["#008009", "#F8E800"],
["#F8E800", "#008009"],
["#00EA11", "#F8E800"],
["#F8E800", "#00EA11"],
["#8BFF7A", "#F8E800"],
["#00588C", "#F8E800"],
["#F8E800", "#00588C"],
["#00A0FF", "#F8E800"],
["#F8E800", "#00A0FF"],
["#BCCEFF", "#F8E800"],
["#5E008C", "#F8E800"],
["#F8E800", "#5E008C"],
["#AC32FF", "#F8E800"],
["#F8E800", "#AC32FF"],
["#D1A3FF", "#F8E800"],
["#B20008", "#F8E800"],
["#F8E800", "#B20008"],
["#FF2B34", "#F8E800"],
["#F8E800", "#FF2B34"],
["#FFADCE", "#F8E800"],
["#9A5200", "#F8E800"],
["#F8E800", "#9A5200"],
["#FF8F00", "#F8E800"],
["#F8E800", "#FF8F00"],
["#FFC169", "#F8E800"],
["#008009", "#00EA11"],
["#00EA11", "#008009"],
["#00B20D", "#00EA11"],
["#00EA11", "#00B20D"],
["#8BFF7A", "#00EA11"],
["#00588C", "#00EA11"],
["#00EA11", "#00588C"],
["#005FE4", "#00EA11"],
["#00EA11", "#005FE4"],
["#BCCDFF", "#00EA11"],
["#5E008C", "#00EA11"],
["#00EA11", "#5E008C"],
["#7F00BF", "#00EA11"],
["#00EA11", "#7F00BF"],
["#D1A3FF", "#00EA11"],
["#B20008", "#00EA11"],
["#00EA11", "#B20008"],
["#FF2B34", "#00EA11"],
["#00EA11", "#FF2B34"],
["#FFADCE", "#00EA11"],
["#9A5200", "#00EA11"],
["#00EA11", "#9A5200"],
["#FF8F00", "#00EA11"],
["#00EA11", "#FF8F00"],
["#FFC169", "#00EA11"],
["#807500", "#00EA11"],
["#00EA11", "#807500"],
["#BE9E00", "#00EA11"],
["#00EA11", "#BE9E00"],
["#F8E800", "#00EA11"],
["#00588C", "#00A0FF"],
["#00A0FF", "#00588C"],
["#005FE4", "#00A0FF"],
["#00A0FF", "#005FE4"],
["#BCCDFF", "#00A0FF"],
["#5E008C", "#00A0FF"],
["#00A0FF", "#5E008C"],
["#9900E6", "#00A0FF"],
["#00A0FF", "#9900E6"],
["#D1A3FF", "#00A0FF"],
["#B20008", "#00A0FF"],
["#00A0FF", "#B20008"],
["#FF2B34", "#00A0FF"],
["#00A0FF", "#FF2B34"],
["#FFADCE", "#00A0FF"],
["#9A5200", "#00A0FF"],
["#00A0FF", "#9A5200"],
["#FF8F00", "#00A0FF"],
["#00A0FF", "#FF8F00"],
["#FFC169", "#00A0FF"],
["#807500", "#00A0FF"],
["#00A0FF", "#807500"],
["#BE9E00", "#00A0FF"],
["#00A0FF", "#BE9E00"],
["#F8E800", "#00A0FF"],
["#008009", "#00A0FF"],
["#00A0FF", "#008009"],
["#00B20D", "#00A0FF"],
["#00A0FF", "#00B20D"],
["#8BFF7A", "#00A0FF"],
["#5E008C", "#AC32FF"],
["#AC32FF", "#5E008C"],
["#7F00BF", "#AC32FF"],
["#AC32FF", "#7F00BF"],
["#D1A3FF", "#AC32FF"],
["#B20008", "#AC32FF"],
["#AC32FF", "#B20008"],
["#FF2B34", "#AC32FF"],
["#AC32FF", "#FF2B34"],
["#FFADCE", "#AC32FF"],
["#9A5200", "#AC32FF"],
["#AC32FF", "#9A5200"],
["#FF8F00", "#AC32FF"],
["#AC32FF", "#FF8F00"],
["#FFC169", "#AC32FF"],
["#807500", "#AC32FF"],
["#AC32FF", "#807500"],
["#BE9E00", "#AC32FF"],
["#AC32FF", "#BE9E00"],
["#F8E800", "#AC32FF"],
["#008009", "#AC32FF"],
["#AC32FF", "#008009"],
["#00B20D", "#AC32FF"],
["#AC32FF", "#00B20D"],
["#8BFF7A", "#AC32FF"],
["#00588C", "#AC32FF"],
["#AC32FF", "#00588C"],
["#005FE4", "#AC32FF"],
["#AC32FF", "#005FE4"],
["#BCCDFF", "#AC32FF"],
]
def _parse_string(color_string):
"""
Parse a color string into stroke and fill colors.
Args:
color_string (str): two html format strings separated by a comma,
"white", or "insensitive"
Returns:
list or None: [stroke_color, fill_color] or None if parsing fails
"""
if not isinstance(color_string, str):
logging.error("Invalid color string: %r", color_string)
return None
if color_string == "white":
return ["#ffffff", "#414141"]
elif color_string == "insensitive":
return ["#ffffff", "#e2e2e2"]
splitted = color_string.split(",")
if len(splitted) == 2:
return [splitted[0], splitted[1]]
else:
return None
[docs]
class XoColor:
"""
Defines color for XO
This class represents a pair of colors (stroke and fill) that can be
used throughout Sugar activities. Colors can be parsed from strings,
loaded from user settings, or chosen randomly.
Args:
color_string (str, optional): Color specification in one of these formats:
- "stroke_hex,fill_hex" (e.g., "#FF0000,#00FF00")
- "white" for white theme
- "insensitive" for disabled/grayed theme
- None to use user's color from settings or random if not available
Examples:
>>> #from string
>>> color = XoColor("#FF0000,#00FF00")
>>> print(color.get_stroke_color()) # "#FF0000"
>>> print(color.get_fill_color()) # "#00FF00"
>>> # create user's color (or random if not set)
>>> color = XoColor()
>>> # themed colors
>>> white_color = XoColor("white")
>>> disabled_color = XoColor("insensitive")
"""
[docs]
def __init__(self, color_string=None):
parsed_color = None
if color_string is None:
if "org.sugarlabs.user" in Gio.Settings.list_schemas():
try:
settings = Gio.Settings("org.sugarlabs.user")
color_string = settings.get_string("color")
except Exception as e:
logging.debug("Could not load user color from settings: %s", e)
color_string = None
if color_string is not None:
parsed_color = _parse_string(color_string)
if parsed_color is None:
n = int(random.random() * len(colors))
self.stroke, self.fill = colors[n]
else:
self.stroke, self.fill = parsed_color
[docs]
def __eq__(self, other):
"""
Check if two XoColor objects are equal.
Args:
other (object): Another XoColor object to compare
Returns:
bool: True if both stroke and fill colors match
"""
if isinstance(other, XoColor):
return self.stroke == other.stroke and self.fill == other.fill
return False
[docs]
def __ne__(self, other):
"""Check if two XoColor objects are not equal."""
return not self.__eq__(other)
[docs]
def __hash__(self):
"""Make XoColor hashable for use in sets and as dict keys."""
return hash((self.stroke, self.fill))
[docs]
def __str__(self):
"""String representation of XoColor."""
return f"XoColor(stroke={self.stroke}, fill={self.fill})"
[docs]
def __repr__(self):
"""Detailed string representation of XoColor."""
return f'XoColor("{self.to_string()}")'
[docs]
def get_stroke_color(self):
"""
Returns:
str: stroke color in HTML hex format (#RRGGBB)
"""
return self.stroke
[docs]
def get_fill_color(self):
"""
Returns:
str: fill color in HTML hex format (#RRGGBB)
"""
return self.fill
[docs]
def to_string(self):
"""
Returns:
str: formatted string in the format "#STROKEHEX,#FILLHEX"
"""
return f"{self.stroke},{self.fill}"
[docs]
@classmethod
def from_string(cls, color_string):
"""
Create XoColor from string representation.
Args:
color_string (str): Color string to parse
Returns:
XoColor: New XoColor instance
Raises:
ValueError: If color_string cannot be parsed
"""
parsed = _parse_string(color_string)
if parsed is None:
raise ValueError(f"Cannot parse color string: {color_string}")
color = cls.__new__(cls)
color.stroke, color.fill = parsed
return color
[docs]
@classmethod
def get_random_color(cls):
"""
Get a random XO color.
Returns:
XoColor: Random XoColor instance from the standard palette
"""
n = int(random.random() * len(colors))
color = cls.__new__(cls)
color.stroke, color.fill = colors[n]
return color
[docs]
def to_rgba_tuple(self, alpha=1.0):
"""
Convert colors to RGBA tuples for use with Cairo/GTK4.
Args:
alpha (float): Alpha value (0.0 - 1.0)
Returns:
tuple: ((r, g, b, a), (r, g, b, a)) for stroke and fill colors
"""
def hex_to_rgba(hex_color):
hex_color = hex_color.lstrip("#")
r = int(hex_color[0:2], 16) / 255.0
g = int(hex_color[2:4], 16) / 255.0
b = int(hex_color[4:6], 16) / 255.0
return (r, g, b, alpha)
return (hex_to_rgba(self.stroke), hex_to_rgba(self.fill))
if __name__ == "__main__":
import sys
if len(sys.argv) > 1:
# color file generator (for development)
import re
with open(sys.argv[1], "r") as f:
print("colors = [")
for line in f.readlines():
match = re.match(r"fill: ([A-Z0-9]*) stroke: ([A-Z0-9]*)", line)
if match:
print(f"['{match.group(2)}', '{match.group(1)}'],")
print("]")
else:
print("Testing XoColor...")
# random color
color1 = XoColor()
print(f"Random color: {color1}")
# string parsing
color2 = XoColor("#FF0000,#00FF00")
print(f"Parsed color: {color2}")
# equality
color3 = XoColor("#FF0000,#00FF00")
print(f"Colors equal: {color2 == color3}")
# RGBA conversion
rgba = color2.to_rgba_tuple()
print(f"RGBA tuples: {rgba}")
print("XoColor tests completed!")