Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 14 additions & 5 deletions pyface/color.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@
Bool, HasStrictTraits, Property, Range, Tuple, cached_property
)

from pyface.color_tuple import ColorTuple
from pyface.util.color_helpers import channels_to_ints, is_dark
from pyface.util.color_helpers import ints_to_channels # noqa: F401
from pyface.util.color_parser import parse_text


Expand Down Expand Up @@ -55,8 +55,7 @@ class Color(HasStrictTraits):

Colors implement equality testing, but are not hashable as they are
mutable, and so are not suitable for use as dictionary keys. If you
need a dictionary key, use an appropriate channel tuple from the
object.
need a dictionary key, use a ColorTuple.
"""

#: A tuple holding the red, green, blue, and alpha channels.
Expand Down Expand Up @@ -93,7 +92,7 @@ class Color(HasStrictTraits):
is_dark = Property(Bool, observe='rgba')

@classmethod
def from_str(cls, text, **traits):
def from_str(cls, text: str, **traits):
""" Create a new Color object from a string.

Parameters
Expand Down Expand Up @@ -154,7 +153,17 @@ def to_toolkit(self):
rgba_to_toolkit_color = toolkit_object('color:rgba_to_toolkit_color')
return rgba_to_toolkit_color(self.rgba)

def hex(self):
def to_color_tuple(self) -> ColorTuple:
""" Create a new Color object from a ColorTuple.

Returns
-------
color_tuple : ColorTuple
A ColorTuple instance.
"""
return ColorTuple(*self.rgba)

def hex(self) -> str:
""" Provide a hex representation of the Color object.

Note that because the hex value is restricted to 0-255 integer values
Expand Down
168 changes: 168 additions & 0 deletions pyface/color_tuple.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
# (C) Copyright 2005-2023 Enthought, Inc., Austin, TX
# All rights reserved.
#
# This software is provided without warranty under the terms of the BSD
# license included in LICENSE.txt and may be redistributed only under
# the conditions described in the aforementioned license. The license
# is also available online at http://www.enthought.com/licenses/BSD.txt
#
# Thanks for using Enthought open source!

""" Color classes and corresponding trait types for Pyface.

The base Color class holds red, green, blue and alpha channel values as
a tuple of normalized values from 0.0 to 1.0. Various property traits
pull out the individual channel values and supply values for the HSV
and HSL colour spaces (with and without alpha).

The ``from_toolkit`` and ``to_toolkit`` methods allow conversion to and
from native toolkit color objects.
"""

import colorsys
from typing import NamedTuple, Tuple, TYPE_CHECKING

from pyface.util.color_helpers import (
channel, channels_to_ints, is_dark, RGBTuple
)
from pyface.util.color_parser import parse_text

if TYPE_CHECKING:
from .color import Color


class ColorTuple(NamedTuple):
"""An immutable specification of a color with alpha.

This is a namedtuple designed to be used by user interface elements which
need to color some or all of the interface element. Each color has a
number of different representations as channel tuples, each channel
holding a value between 0.0 and 1.0, inclusive. The standard red,
green, blue and alpha channels are also provided as convenience
properties.

Methods are provided to convert to and from toolkit-specific color
objects.

ColorTuples can be tested for equality and are hashable, just as with all
namedtuples, and so are suitable for use as dictionary keys.
"""

red: channel = channel(1.0)

green: channel = channel(1.0)

blue: channel = channel(1.0)

alpha: channel = channel(1.0)

@classmethod
def from_str(cls, text: str):
""" Create a new ColorTuple from a string.

Parameters
----------
text : str
A string holding the representation of the color. This can be:

- a color name, including all CSS color names, plus any additional
names found in pyface.color.color_table. The names are
normalized to lower case and stripped of whitespace, hyphens and
underscores.

- a hex representation of the color in the form '#RGB', '#RGBA',
'#RRGGBB', '#RRGGBBAA', '#RRRRGGGGBBBB', or '#RRRRGGGGBBBBAAAA'.

Raises
------
ColorParseError
If the string cannot be converted to a valid color.
"""
space, channels = parse_text(text)
return cls(*channels)

@classmethod
def from_color(cls, color: "Color") -> "ColorTuple":
""" Create a new ColorTuple from a Color object.

Parameters
----------
color : Color
A Color object.
"""
return cls(*color.rgba)

@classmethod
def from_toolkit(cls, toolkit_color) -> "ColorTuple":
""" Create a new RGBAColorTuple from a toolkit color object.

Parameters
----------
toolkit_color : toolkit object
A toolkit color object, such as a Qt QColor or a Wx wx.Colour.
**traits
Any additional trait values to be passed as keyword arguments.
"""
from pyface.toolkit import toolkit_object
toolkit_color_to_rgba = toolkit_object('color:toolkit_color_to_rgba')
return cls(*toolkit_color_to_rgba(toolkit_color))

def to_toolkit(self):
""" Create a new toolkit color object from a Color object.

Returns
-------
toolkit_color : toolkit object
A toolkit color object, such as a Qt QColor or a Wx wx.Colour.
"""
from pyface.toolkit import toolkit_object
rgba_to_toolkit_color = toolkit_object('color:rgba_to_toolkit_color')
return rgba_to_toolkit_color(self)

def hex(self) -> str:
""" Provide a hex representation of the Color object.

Note that because the hex value is restricted to 0-255 integer values
for each channel, the representation is not exact.

Returns
-------
hex : str
A hex string in standard ``#RRGGBBAA`` format that represents
the color.
"""
values = channels_to_ints(self)
return "#{:02X}{:02X}{:02X}{:02X}".format(*values)

def __str__(self) -> str:
return "({:0.5}, {:0.5}, {:0.5}, {:0.5})".format(*self)

@property
def rgb(self) -> RGBTuple:
return self[:3]

@property
def hsva(self) -> Tuple[channel, channel, channel, channel]:
r, g, b, a = self
h, s, v = colorsys.rgb_to_hsv(r, g, b)
return (h, s, v, a)

@property
def hsv(self) -> Tuple[channel, channel, channel]:
r, g, b, a = self
return colorsys.rgb_to_hsv(r, g, b)

@property
def hlsa(self) -> Tuple[channel, channel, channel, channel]:
r, g, b, a = self
h, l, s = colorsys.rgb_to_hls(r, g, b)
return (h, l, s, a)

@property
def hls(self) -> Tuple[channel, channel, channel]:
r, g, b, a = self
return colorsys.rgb_to_hls(r, g, b)

@property
def is_dark(self) -> bool:
return is_dark(self)
6 changes: 6 additions & 0 deletions pyface/tests/test_color.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from traits.testing.api import UnittestTools

from pyface.color import Color
from pyface.color_tuple import ColorTuple


class TestColor(UnittestTools, TestCase):
Expand Down Expand Up @@ -81,6 +82,11 @@ def test_toolkit_round_trip(self):
result = Color.from_toolkit(toolkit_color)
self.assertEqual(result.rgba, (0.4, 0.2, 0.6, 0.8))

def test_to_color_tuple(self):
color = Color(rgba=(0.4, 0.2, 0.6, 0.8))
color_tuple = color.to_color_tuple()
self.assertEqual(color_tuple, ColorTuple(0.4, 0.2, 0.6, 0.8))

def test_hex(self):
color = Color(rgba=(0.4, 0.2, 0.6, 0.8))
hex_value = color.hex()
Expand Down
101 changes: 101 additions & 0 deletions pyface/tests/test_color_tuple.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# (C) Copyright 2005-2023 Enthought, Inc., Austin, TX
# All rights reserved.
#
# This software is provided without warranty under the terms of the BSD
# license included in LICENSE.txt and may be redistributed only under
# the conditions described in the aforementioned license. The license
# is also available online at http://www.enthought.com/licenses/BSD.txt
#
# Thanks for using Enthought open source!

from unittest import TestCase

from pyface.color import Color
from pyface.color_tuple import ColorTuple


class TestColorTuple(TestCase):

def assert_tuple_almost_equal(self, tuple_1, tuple_2):
self.assertEqual(len(tuple_1), len(tuple_2))

for x, y in zip(tuple_1, tuple_2):
self.assertAlmostEqual(x, y)

def test_init(self):
color = ColorTuple()
self.assertEqual(color, (1.0, 1.0, 1.0, 1.0))

def test_init_rgba(self):
color = ColorTuple(0.4, 0.2, 0.6, 0.8)
self.assertEqual(color, (0.4, 0.2, 0.6, 0.8))

def test_init_rgb(self):
color = ColorTuple(0.4, 0.2, 0.6)
self.assertEqual(color, (0.4, 0.2, 0.6, 1.0))

def test_init_r_g_b_a(self):
color = ColorTuple(red=0.4, green=0.2, blue=0.6, alpha=0.8)
self.assertEqual(color, (0.4, 0.2, 0.6, 0.8))

def test_init_r_g_b(self):
color = ColorTuple(red=0.4, green=0.2, blue=0.6)
self.assertEqual(color, (0.4, 0.2, 0.6, 1.0))

def test_from_str_name(self):
color = ColorTuple.from_str('rebeccapurple')
self.assertEqual(color, (0.4, 0.2, 0.6, 1.0))

def test_from_str_hex(self):
color = ColorTuple.from_str('#663399ff')
self.assertEqual(color, (0.4, 0.2, 0.6, 1.0))

def test_toolkit_round_trip(self):
color = ColorTuple(0.4, 0.2, 0.6, 0.8)
toolkit_color = color.to_toolkit()
result = ColorTuple.from_toolkit(toolkit_color)
self.assertEqual(result, color)

def test_from_color(self):
color = Color(rgba=(0.4, 0.2, 0.6, 0.8))
color_tuple = ColorTuple.from_color(color)
self.assertEqual(color_tuple, (0.4, 0.2, 0.6, 0.8))

def test_hex(self):
color = ColorTuple(0.4, 0.2, 0.6, 0.8)
hex_value = color.hex()
self.assertEqual(hex_value, "#663399CC")

def test_hex_black(self):
color = ColorTuple(0.0, 0.0, 0.0, 1.0)
hex_value = color.hex()
self.assertEqual(hex_value, "#000000FF")

def test_str(self):
color = ColorTuple(0.4, 0.2, 0.6, 0.8)
result = str(color)
self.assertEqual(result, "(0.4, 0.2, 0.6, 0.8)")

def test_get_rgb(self):
color = ColorTuple(0.4, 0.2, 0.6, 0.8)
self.assertEqual(color.rgb, (0.4, 0.2, 0.6))

def test_get_hsv(self):
color = ColorTuple(0.48, 0.6, 0.528, 0.8)
self.assert_tuple_almost_equal(color.hsv, (0.4, 0.2, 0.6))

def test_get_hsva(self):
color = ColorTuple(0.48, 0.6, 0.528, 0.8)
self.assert_tuple_almost_equal(color.hsva, (0.4, 0.2, 0.6, 0.8))

def test_get_hls(self):
color = ColorTuple(0.08, 0.32, 0.176, 0.8)
self.assert_tuple_almost_equal(color.hls, (0.4, 0.2, 0.6))

def test_get_hlsa(self):
color = ColorTuple(0.08, 0.32, 0.176, 0.8)
self.assert_tuple_almost_equal(color.hlsa, (0.4, 0.2, 0.6, 0.8))

def test_get_is_dark(self):
color = ColorTuple(0.08, 0.32, 0.176, 0.8)
self.assertTrue(color.is_dark)
13 changes: 8 additions & 5 deletions pyface/ui/qt/color.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,15 @@
pyface.color.Color class to_toolkit and from_toolkit methods.
"""

from pyface.qt.QtGui import QColor
from typing import cast

from pyface.color import channels_to_ints, ints_to_channels
from pyface.qt.QtGui import QColor
from pyface.util.color_helpers import (
channels_to_ints, ints_to_channels, RGBATuple,
)


def toolkit_color_to_rgba(qcolor):
def toolkit_color_to_rgba(qcolor: QColor) -> RGBATuple:
""" Convert a QColor to an RGBA tuple.

Parameters
Expand All @@ -37,10 +40,10 @@ def toolkit_color_to_rgba(qcolor):
qcolor.blue(),
qcolor.alpha(),
)
return ints_to_channels(values)
return cast(RGBATuple, ints_to_channels(values))


def rgba_to_toolkit_color(rgba):
def rgba_to_toolkit_color(rgba: RGBATuple) -> QColor:
""" Convert an RGBA tuple to a QColor.

Parameters
Expand Down
8 changes: 5 additions & 3 deletions pyface/ui/wx/color.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@

import wx

from pyface.color import channels_to_ints, ints_to_channels
from pyface.util.color_helpers import (
channels_to_ints, ints_to_channels, RGBATuple
)


def toolkit_color_to_rgba(wx_colour):
def toolkit_color_to_rgba(wx_colour: wx.Colour) -> RGBATuple:
""" Convert a wx.Colour to an RGBA tuple.

Parameters
Expand All @@ -41,7 +43,7 @@ def toolkit_color_to_rgba(wx_colour):
return ints_to_channels(values)


def rgba_to_toolkit_color(rgba):
def rgba_to_toolkit_color(rgba: RGBATuple) -> wx.Colour:
""" Convert an RGBA tuple to a wx.Colour.

Parameters
Expand Down
Loading