Ajout du GUI
This commit is contained in:
485
kivy/uix/colorpicker.py
Normal file
485
kivy/uix/colorpicker.py
Normal file
@@ -0,0 +1,485 @@
|
||||
'''
|
||||
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 kivy.uix.relativelayout import RelativeLayout
|
||||
from kivy.uix.widget import Widget
|
||||
from kivy.properties import (NumericProperty, BoundedNumericProperty,
|
||||
ListProperty, ObjectProperty,
|
||||
ReferenceListProperty, StringProperty,
|
||||
AliasProperty)
|
||||
from kivy.clock import Clock
|
||||
from kivy.graphics import Mesh, InstructionGroup, Color
|
||||
from kivy.utils import get_color_from_hex, get_hex_from_color
|
||||
from kivy.logger import Logger
|
||||
from math import cos, sin, pi, sqrt, atan
|
||||
from colorsys import rgb_to_hsv, hsv_to_rgb
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
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.
|
||||
'''
|
||||
|
||||
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.
|
||||
'''
|
||||
|
||||
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.
|
||||
'''
|
||||
|
||||
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.
|
||||
'''
|
||||
|
||||
_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
|
||||
|
||||
_hsv = ListProperty([1, 1, 1, 0])
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(ColorWheel, self).__init__(**kwargs)
|
||||
|
||||
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))]
|
||||
|
||||
def on__origin(self, instance, value):
|
||||
self.init_wheel(None)
|
||||
|
||||
def on__radius(self, instance, value):
|
||||
self.init_wheel(None)
|
||||
|
||||
def init_wheel(self, dt):
|
||||
# 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)
|
||||
|
||||
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()
|
||||
|
||||
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
|
||||
goal_sv_idx >= 0 and
|
||||
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()
|
||||
|
||||
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)
|
||||
|
||||
|
||||
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, max(0, float(text)))
|
||||
if mode == 'rgb':
|
||||
self.color[clr_idx] = float(text) / 255.
|
||||
else:
|
||||
hsv = list(self.hsv[:])
|
||||
hsv[clr_idx] = float(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()
|
||||
Reference in New Issue
Block a user