'''
Color Picker
============
.. versionadded:: 1.7.0
.. warning::
This widget is experimental. Its use and API can change at any time until
this warning is removed.
.. image:: images/colorpicker.png
:align: right
The ColorPicker widget allows a user to select a color from a chromatic
wheel where pinch and zoom can be used to change the wheel's saturation.
Sliders and TextInputs are also provided for entering the RGBA/HSV/HEX values
directly.
Usage::
clr_picker = ColorPicker()
parent.add_widget(clr_picker)
# To monitor changes, we can bind to color property changes
def on_color(instance, value):
print("RGBA = ", str(value)) # or instance.color
print("HSV = ", str(instance.hsv))
print("HEX = ", str(instance.hex_color))
clr_picker.bind(color=on_color)
'''
__all__ = ('ColorPicker', 'ColorWheel')
from math import cos, sin, pi, sqrt, atan
from colorsys import rgb_to_hsv, hsv_to_rgb
from kivy.clock import Clock
from kivy.graphics import Mesh, InstructionGroup, Color
from kivy.logger import Logger
from kivy.properties import (NumericProperty, BoundedNumericProperty,
ListProperty, ObjectProperty,
ReferenceListProperty, StringProperty,
AliasProperty)
from kivy.uix.relativelayout import RelativeLayout
from kivy.uix.widget import Widget
from kivy.utils import get_color_from_hex, get_hex_from_color
def distance(pt1, pt2):
return sqrt((pt1[0] - pt2[0]) ** 2. + (pt1[1] - pt2[1]) ** 2.)
def polar_to_rect(origin, r, theta):
return origin[0] + r * cos(theta), origin[1] + r * sin(theta)
def rect_to_polar(origin, x, y):
if x == origin[0]:
if y == origin[1]:
return 0, 0
elif y > origin[1]:
return y - origin[1], pi / 2
else:
return origin[1] - y, 3 * pi / 2
t = atan(float((y - origin[1])) / (x - origin[0]))
if x - origin[0] < 0:
t += pi
if t < 0:
t += 2 * pi
return distance((x, y), origin), t
[docs]class ColorWheel(Widget):
'''Chromatic wheel for the ColorPicker.
.. versionchanged:: 1.7.1
`font_size`, `font_name` and `foreground_color` have been removed. The
sizing is now the same as others widget, based on 'sp'. Orientation is
also automatically determined according to the width/height ratio.
'''
r = BoundedNumericProperty(0, min=0, max=1)
'''The Red value of the color currently selected.
:attr:`r` is a :class:`~kivy.properties.BoundedNumericProperty` and
can be a value from 0 to 1. It defaults to 0.
'''
g = BoundedNumericProperty(0, min=0, max=1)
'''The Green value of the color currently selected.
:attr:`g` is a :class:`~kivy.properties.BoundedNumericProperty`
and can be a value from 0 to 1. It defaults to 0.
'''
b = BoundedNumericProperty(0, min=0, max=1)
'''The Blue value of the color currently selected.
:attr:`b` is a :class:`~kivy.properties.BoundedNumericProperty` and
can be a value from 0 to 1. It defaults to 0.
'''
a = BoundedNumericProperty(0, min=0, max=1)
'''The Alpha value of the color currently selected.
:attr:`a` is a :class:`~kivy.properties.BoundedNumericProperty` and
can be a value from 0 to 1. It defaults to 0.
'''
color = ReferenceListProperty(r, g, b, a)
'''The holds the color currently selected.
:attr:`color` is a :class:`~kivy.properties.ReferenceListProperty` and
contains a list of `r`, `g`, `b`, `a` values. It defaults to `[0, 0, 0, 0]`.
'''
_origin = ListProperty((100, 100))
_radius = NumericProperty(100)
_piece_divisions = NumericProperty(10)
_pieces_of_pie = NumericProperty(16)
_inertia_slowdown = 1.25
_inertia_cutoff = .25
_num_touches = 0
_pinch_flag = False
def __init__(self, **kwargs):
self.arcs = []
self.sv_idx = 0
pdv = self._piece_divisions
self.sv_s = [(float(x) / pdv, 1) for x in range(pdv)] + [
(1, float(y) / pdv) for y in reversed(range(pdv))]
super(ColorWheel, self).__init__(**kwargs)
def on__origin(self, _instance, _value):
self._reset_canvas()
def on__radius(self, _instance, _value):
self._reset_canvas()
def _reset_canvas(self):
# initialize list to hold all meshes
self.canvas.clear()
self.arcs = []
self.sv_idx = 0
pdv = self._piece_divisions
ppie = self._pieces_of_pie
for r in range(pdv):
for t in range(ppie):
self.arcs.append(
_ColorArc(
self._radius * (float(r) / float(pdv)),
self._radius * (float(r + 1) / float(pdv)),
2 * pi * (float(t) / float(ppie)),
2 * pi * (float(t + 1) / float(ppie)),
origin=self._origin,
color=(float(t) / ppie,
self.sv_s[self.sv_idx + r][0],
self.sv_s[self.sv_idx + r][1],
1)))
self.canvas.add(self.arcs[-1])
def recolor_wheel(self):
ppie = self._pieces_of_pie
for idx, segment in enumerate(self.arcs):
segment.change_color(
sv=self.sv_s[int(self.sv_idx + idx / ppie)])
def change_alpha(self, val):
for idx, segment in enumerate(self.arcs):
segment.change_color(a=val)
def inertial_incr_sv_idx(self, dt):
# if its already zoomed all the way out, cancel the inertial zoom
if self.sv_idx == len(self.sv_s) - self._piece_divisions:
return False
self.sv_idx += 1
self.recolor_wheel()
if dt * self._inertia_slowdown > self._inertia_cutoff:
return False
else:
Clock.schedule_once(self.inertial_incr_sv_idx,
dt * self._inertia_slowdown)
def inertial_decr_sv_idx(self, dt):
# if its already zoomed all the way in, cancel the inertial zoom
if self.sv_idx == 0:
return False
self.sv_idx -= 1
self.recolor_wheel()
if dt * self._inertia_slowdown > self._inertia_cutoff:
return False
else:
Clock.schedule_once(self.inertial_decr_sv_idx,
dt * self._inertia_slowdown)
[docs] def on_touch_down(self, touch):
r = self._get_touch_r(touch.pos)
if r > self._radius:
return False
# code is still set up to allow pinch to zoom, but this is
# disabled for now since it was fiddly with small wheels.
# Comment out these lines and adjust on_touch_move to reenable
# this.
if self._num_touches != 0:
return False
touch.grab(self)
self._num_touches += 1
touch.ud['anchor_r'] = r
touch.ud['orig_sv_idx'] = self.sv_idx
touch.ud['orig_time'] = Clock.get_time()
[docs] def on_touch_move(self, touch):
if touch.grab_current is not self:
return
r = self._get_touch_r(touch.pos)
goal_sv_idx = (touch.ud['orig_sv_idx'] -
int((r - touch.ud['anchor_r']) /
(float(self._radius) / self._piece_divisions)))
if (
goal_sv_idx != self.sv_idx and
0 <= goal_sv_idx <= len(self.sv_s) - self._piece_divisions
):
# this is a pinch to zoom
self._pinch_flag = True
self.sv_idx = goal_sv_idx
self.recolor_wheel()
[docs] def on_touch_up(self, touch):
if touch.grab_current is not self:
return
touch.ungrab(self)
self._num_touches -= 1
if self._pinch_flag:
if self._num_touches == 0:
# user was pinching, and now both fingers are up. Return
# to normal
if self.sv_idx > touch.ud['orig_sv_idx']:
Clock.schedule_once(
self.inertial_incr_sv_idx,
(Clock.get_time() - touch.ud['orig_time']) /
(self.sv_idx - touch.ud['orig_sv_idx']))
if self.sv_idx < touch.ud['orig_sv_idx']:
Clock.schedule_once(
self.inertial_decr_sv_idx,
(Clock.get_time() - touch.ud['orig_time']) /
(self.sv_idx - touch.ud['orig_sv_idx']))
self._pinch_flag = False
return
else:
# user was pinching, and at least one finger remains. We
# don't want to treat the remaining fingers as touches
return
else:
r, theta = rect_to_polar(self._origin, *touch.pos)
# if touch up is outside the wheel, ignore
if r >= self._radius:
return
# compute which ColorArc is being touched (they aren't
# widgets so we don't get collide_point) and set
# _hsv based on the selected ColorArc
piece = int((theta / (2 * pi)) * self._pieces_of_pie)
division = int((r / self._radius) * self._piece_divisions)
hsva = list(
self.arcs[self._pieces_of_pie * division + piece].color)
self.color = list(hsv_to_rgb(*hsva[:3])) + hsva[-1:]
def _get_touch_r(self, pos):
return distance(pos, self._origin)
class _ColorArc(InstructionGroup):
def __init__(self, r_min, r_max, theta_min, theta_max,
color=(0, 0, 1, 1), origin=(0, 0), **kwargs):
super(_ColorArc, self).__init__(**kwargs)
self.origin = origin
self.r_min = r_min
self.r_max = r_max
self.theta_min = theta_min
self.theta_max = theta_max
self.color = color
self.color_instr = Color(*color, mode='hsv')
self.add(self.color_instr)
self.mesh = self.get_mesh()
self.add(self.mesh)
def __str__(self):
return "r_min: %s r_max: %s theta_min: %s theta_max: %s color: %s" % (
self.r_min, self.r_max, self.theta_min, self.theta_max, self.color
)
def get_mesh(self):
v = []
# first calculate the distance between endpoints of the outer
# arc, so we know how many steps to use when calculating
# vertices
theta_step_outer = 0.1
theta = self.theta_max - self.theta_min
d_outer = int(theta / theta_step_outer)
theta_step_outer = theta / d_outer
if self.r_min == 0:
for x in range(0, d_outer, 2):
v += (polar_to_rect(self.origin, self.r_max,
self.theta_min + x * theta_step_outer
) * 2)
v += polar_to_rect(self.origin, 0, 0) * 2
v += (polar_to_rect(self.origin, self.r_max,
self.theta_min + (x + 1) * theta_step_outer
) * 2)
if not d_outer & 1: # add a last point if d_outer is even
v += (polar_to_rect(self.origin, self.r_max,
self.theta_min + d_outer * theta_step_outer
) * 2)
else:
for x in range(d_outer + 1):
v += (polar_to_rect(self.origin, self.r_min,
self.theta_min + x * theta_step_outer
) * 2)
v += (polar_to_rect(self.origin, self.r_max,
self.theta_min + x * theta_step_outer
) * 2)
return Mesh(vertices=v, indices=range(int(len(v) / 4)),
mode='triangle_strip')
def change_color(self, color=None, color_delta=None, sv=None, a=None):
self.remove(self.color_instr)
if color is not None:
self.color = color
elif color_delta is not None:
self.color = [self.color[i] + color_delta[i] for i in range(4)]
elif sv is not None:
self.color = (self.color[0], sv[0], sv[1], self.color[3])
elif a is not None:
self.color = (self.color[0], self.color[1], self.color[2], a)
self.color_instr = Color(*self.color, mode='hsv')
self.insert(0, self.color_instr)
[docs]class ColorPicker(RelativeLayout):
'''
See module documentation.
'''
font_name = StringProperty('data/fonts/RobotoMono-Regular.ttf')
'''Specifies the font used on the ColorPicker.
:attr:`font_name` is a :class:`~kivy.properties.StringProperty` and
defaults to 'data/fonts/RobotoMono-Regular.ttf'.
'''
color = ListProperty((1, 1, 1, 1))
'''The :attr:`color` holds the color currently selected in rgba format.
:attr:`color` is a :class:`~kivy.properties.ListProperty` and defaults to
(1, 1, 1, 1).
'''
def _get_hsv(self):
return rgb_to_hsv(*self.color[:3])
def _set_hsv(self, value):
if self._updating_clr:
return
self.set_color(value)
hsv = AliasProperty(_get_hsv, _set_hsv, bind=('color', ))
'''The :attr:`hsv` holds the color currently selected in hsv format.
:attr:`hsv` is a :class:`~kivy.properties.ListProperty` and defaults to
(1, 1, 1).
'''
def _get_hex(self):
return get_hex_from_color(self.color)
def _set_hex(self, value):
if self._updating_clr:
return
self.set_color(get_color_from_hex(value)[:4])
hex_color = AliasProperty(_get_hex, _set_hex, bind=('color',), cache=True)
'''The :attr:`hex_color` holds the currently selected color in hex.
:attr:`hex_color` is an :class:`~kivy.properties.AliasProperty` and
defaults to `#ffffffff`.
'''
wheel = ObjectProperty(None)
'''The :attr:`wheel` holds the color wheel.
:attr:`wheel` is an :class:`~kivy.properties.ObjectProperty` and
defaults to None.
'''
_update_clr_ev = _update_hex_ev = None
# now used only internally.
foreground_color = ListProperty((1, 1, 1, 1))
def _trigger_update_clr(self, mode, clr_idx, text):
if self._updating_clr:
return
self._updating_clr = True
self._upd_clr_list = mode, clr_idx, text
ev = self._update_clr_ev
if ev is None:
ev = self._update_clr_ev = Clock.create_trigger(self._update_clr)
ev()
def _update_clr(self, dt):
# to prevent interaction between hsv/rgba, we work internally using rgba
mode, clr_idx, text = self._upd_clr_list
try:
text = min(255.0, max(0.0, float(text)))
if mode == 'rgb':
self.color[clr_idx] = text / 255
else:
hsv = list(self.hsv[:])
hsv[clr_idx] = text / 255
self.color[:3] = hsv_to_rgb(*hsv)
except ValueError:
Logger.warning('ColorPicker: invalid value : {}'.format(text))
finally:
self._updating_clr = False
def _update_hex(self, dt):
try:
if len(self._upd_hex_list) != 9:
return
self._updating_clr = False
self.hex_color = self._upd_hex_list
finally:
self._updating_clr = False
def _trigger_update_hex(self, text):
if self._updating_clr:
return
self._updating_clr = True
self._upd_hex_list = text
ev = self._update_hex_ev
if ev is None:
ev = self._update_hex_ev = Clock.create_trigger(self._update_hex)
ev()
def set_color(self, color):
self._updating_clr = True
if len(color) == 3:
self.color[:3] = color
else:
self.color = color
self._updating_clr = False
def __init__(self, **kwargs):
self._updating_clr = False
super(ColorPicker, self).__init__(**kwargs)
if __name__ in ('__android__', '__main__'):
from kivy.app import App
class ColorPickerApp(App):
def build(self):
cp = ColorPicker(pos_hint={'center_x': .5, 'center_y': .5},
size_hint=(1, 1))
return cp
ColorPickerApp().run()