Version

Quick search

Table Of Contents

Source code for kivy.uix.colorpicker

'''
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()