3 from math import pi, sin, cos, sqrt, atan2, ceil
8 from layout import ElasticExpander
9 from lib.helpers import rgb_to_hsv, hsv_to_rgb, clamp
10 from gettext import gettext as _
16 class GColorSelector(gtk.DrawingArea):
17 def __init__(self, app):
18 gtk.DrawingArea.__init__(self)
22 self.connect('expose-event', self.draw)
23 self.set_events(gdk.BUTTON_PRESS_MASK |
24 gdk.BUTTON_RELEASE_MASK |
25 gdk.POINTER_MOTION_MASK |
31 gdk.PROXIMITY_IN_MASK |
32 gdk.PROXIMITY_OUT_MASK)
33 # When the colour is chosen set it for the input device that was used
34 # for the input events
35 self.set_extension_events(gdk.EXTENSION_EVENTS_ALL)
36 self.connect('button-press-event', self.on_button_press)
37 self.connect('button-release-event', self.on_button_release)
38 self.connect('configure-event', self.on_configure)
39 self.connect('motion-notify-event', self.motion)
40 self.device_pressed = None
42 self.set_size_request(110, 100)
43 self.has_hover_areas = False
45 def test_move(self, x, y):
47 Return true during motion if the selection indicator
48 should move to where the cursor is pointing.
52 def test_button_release(self, x, y):
54 Return true after a button-release-event if the colour under the
55 pointer should be selected.
62 def redraw_on_select(self):
65 def motion(self,w, event):
66 if self.has_hover_areas:
67 self.update_hover_area(event.x, event.y)
68 if not self.device_pressed:
70 if self.test_move(event.x, event.y):
74 self.move(event.x,event.y)
76 def on_configure(self,w, size):
83 def configure_calc(self):
86 def on_select(self,color):
89 def update_tooltip(self, x, y):
90 """Updates the tooltip during motion, if tooltips are zoned."""
93 def get_color_at(self, x,y):
96 def select_color_at(self, x,y):
97 color, is_hsv = self.get_color_at(x,y)
102 self.color = hsv_to_rgb(*color)
105 self.hsv = rgb_to_hsv(*color)
106 if self.device_pressed:
107 selected_color = self.color
108 # Potential device change, therefore brush & colour change...
109 self.app.doc.tdw.device_used(self.device_pressed)
110 self.color = selected_color #... but *this* is what the user wants
111 self.redraw_on_select()
112 self.on_select(self.hsv)
114 def set_color(self, hsv):
116 self.color = hsv_to_rgb(*hsv)
122 def on_button_press(self, w, event):
123 self.press_x = event.x
124 self.press_y = event.y
125 # Remember the device that was clicked, tapped to the stylus, etc. We
126 # process any potential device changing on the button release to avoid
127 # redrawing with colours associated with the newly picked bush during
129 self.device_pressed = event.device
131 def on_button_release(self,w, event):
132 if self.device_pressed and self.test_button_release(event.x, event.y):
133 self.select_color_at(event.x, event.y)
134 self.device_pressed = None
139 def draw(self,w,event):
142 cr = self.window.cairo_create()
143 cr.set_source_rgb(*self.color)
144 cr.rectangle(PADDING,PADDING, self.w-2*PADDING, self.h-2*PADDING)
147 class RectSlot(GColorSelector):
148 def __init__(self,app,color=(1.0,1.0,1.0),size=32):
149 GColorSelector.__init__(self, app)
151 self.set_size_request(size,size)
153 class RecentColors(gtk.HBox):
154 def __init__(self, app):
155 gtk.HBox.__init__(self)
156 self.set_border_width(4)
157 self.N = N = app.ch.num_colors
161 slot.on_select = self.slot_selected
162 self.pack_start(slot, expand=True)
163 self.slots.append(slot)
165 app.ch.color_pushed_observers.append(self.refill_slots)
166 self.set_tooltip_text(_("Recently used colors"))
167 self.refill_slots(None)
170 def slot_selected(self,color):
171 self.on_select(color)
173 def on_select(self,color):
176 def refill_slots(self, *junk):
177 for hsv,slot in zip(self.app.ch.colors, reversed(self.slots)):
180 def set_color(self, hsv):
185 NEUTRAL_MID_GREY = (0.5, 0.5, 0.5) #: Background neutral mid grey, intended to permit colour comparisons without theme colours distracting and imposing themselves
186 NEUTRAL_DARK_GREY = (0.46, 0.46, 0.46) #: Slightly distinct outlinefor colour pots, intended to reduce "1+1=3 (or more)" effects
188 AREA_SQUARE = 1 #: Central saturation/value square
189 AREA_INSIDE = 2 #: Grey area inside the rings
190 AREA_SAMPLE = 3 #: Inner ring of color sampler boxes (hue)
191 AREA_CIRCLE = 4 #: Outer gradiated ring (hue)
192 AREA_COMPARE = 5 #: Current/previous comparison semicircles
193 AREA_PICKER = 6 #: The picker icon's circle
194 AREA_OUTSIDE = 7 #: Blank outer area, outer space
197 { AREA_SQUARE: _('Change Saturation and Value'),
199 AREA_SAMPLE: _('Harmony color switcher'),
200 AREA_CIRCLE: _('Change Hue'),
201 AREA_COMPARE: _('Current color vs. Last Used'),
202 AREA_PICKER: _('Pick a screen color'),
211 def try_put(list, item):
215 class CircleSelector(GColorSelector):
217 grab_mask = gdk.BUTTON_RELEASE_MASK | gdk.BUTTON1_MOTION_MASK
220 def __init__(self,app,color=(1,0,0)):
221 GColorSelector.__init__(self,app)
223 self.hsv = rgb_to_hsv(*color)
225 self.samples = [] # [(h,s,v)] -- list of `harmonic' colors
226 self.last_line = None #
227 app.ch.color_pushed_observers.append(self.color_pushed_cb)
229 self.has_hover_areas = True
230 self.hover_area = AREA_OUTSIDE
231 self.previous_hover_area = None
232 self.previous_hover_xy = None
234 self.picker_icons = []
235 self.connect("map-event", self.init_picker_icons)
236 self.connect("style-set", self.init_picker_icons)
238 self.picker_state = gtk.STATE_NORMAL
239 self.button_press = None
241 def on_button_press(self, widget, event):
242 area = self.area_at(event.x, event.y)
243 if area == AREA_PICKER:
244 self.button_press = event
245 self.picker_state = gtk.STATE_ACTIVE
248 return GColorSelector.on_button_press(self, widget, event)
250 def on_button_release(self, widget, event):
252 self.app.pick_color_at_pointer(self)
256 elif self.button_press:
257 area = self.area_at(event.x, event.y)
258 if area == AREA_PICKER:
259 gobject.idle_add(self.begin_color_pick)
260 self.picker_state = gtk.STATE_NORMAL
262 self.button_press = None
264 return GColorSelector.on_button_release(self, widget, event)
266 def motion(self, widget, event):
268 if event.state & gdk.BUTTON1_MASK:
269 self.app.pick_color_at_pointer(self)
271 elif self.button_press:
272 area = self.area_at(event.x, event.y)
273 if area == AREA_PICKER:
274 if self.picker_state != gtk.STATE_ACTIVE:
275 self.picker_state = gtk.STATE_ACTIVE
278 if self.picker_state != gtk.STATE_NORMAL:
279 self.picker_state = gtk.STATE_NORMAL
282 return GColorSelector.motion(self, widget, event)
284 def begin_color_pick(self):
285 cursor = self.app.cursor_color_picker
286 result = gdk.pointer_grab(self.window, False, self.grab_mask, None, cursor)
287 if result == gdk.GRAB_SUCCESS:
290 def init_picker_icons(self, *a, **kw):
291 sizes = [gtk.ICON_SIZE_SMALL_TOOLBAR,
292 gtk.ICON_SIZE_LARGE_TOOLBAR,
293 gtk.ICON_SIZE_BUTTON]
294 # No bigger, to help avoid ugly scaling
297 pixbuf = self.render_icon(gtk.STOCK_COLOR_PICKER, size)
298 w = pixbuf.get_width()
299 h = pixbuf.get_height()
300 r = sqrt((w/2)**2 + (h/2)**2)
301 pixbufs.append((r, w, h, pixbuf, size))
302 pixbufs.sort() # in ascending order of size
303 self.picker_icons = pixbufs
305 def color_pushed_cb(self, pushed_color):
308 def has_harmonies_visible(self):
309 return self.app.preferences.get("colorsampler.complementary", False) \
310 or self.app.preferences.get("colorsampler.triadic", False) \
311 or self.app.preferences.get("colorsampler.double_comp", False) \
312 or self.app.preferences.get("colorsampler.split_comp", False) \
313 or self.app.preferences.get("colorsampler.analogous", False) \
314 or self.app.preferences.get("colorsampler.square", False)
316 def get_previous_color(self):
319 def update_hover_area(self, x, y):
320 area = self.area_at(x, y)
322 if self.previous_hover_area is None:
324 tooltip = CIRCLE_TOOLTIPS.get(area, None)
325 self.set_tooltip_text(tooltip)
326 self.previous_hover_area = area, x, y
328 old_area, old_x, old_y = self.previous_hover_area
329 if area != old_area or abs(old_x - x) > 20 or abs(old_y - y) > 20:
330 self.set_tooltip_text(None)
331 self.previous_hover_area = None
333 self.hover_area = area
334 if area == AREA_PICKER:
335 self.picker_state = gtk.STATE_PRELIGHT
337 self.picker_state = gtk.STATE_NORMAL
338 if AREA_PICKER in (area, old_area):
341 def calc_line(self, angle):
342 x1 = self.x0 + self.r2*cos(-angle)
343 y1 = self.y0 + self.r2*sin(-angle)
344 x2 = self.x0 + self.r3*cos(-angle)
345 y2 = self.y0 + self.r3*sin(-angle)
348 def configure_calc(self):
350 self.x0 = self.x_rel + self.w/2.0
351 self.y0 = self.y_rel + self.h/2.0
352 self.M = M = min(self.w,self.h) - (2*padding)
353 self.r3 = 0.5*M # outer radius of hue ring
354 if not self.has_harmonies_visible():
355 self.r2 = 0.42*M # inner radius of hue ring
356 self.rd = 0.42*M # and size of square
358 self.r2 = 0.44*M # line between hue ring & harmony samplers
359 self.rd = 0.34*M # size of square & inner edge of harmony samplers
360 self.m = self.rd/sqrt(2.0)
361 self.circle_img = None
362 self.stroke_width = 0.01*M
364 def set_color(self, hsv, redraw=True):
366 self.color = hsv_to_rgb(*hsv)
370 def test_move(self, x, y):
371 from_area = self.area_at(self.press_x,self.press_y)
372 to_area = self.area_at(x, y)
373 return from_area in (AREA_CIRCLE, AREA_SQUARE)
375 def test_button_release(self, x, y):
376 from_area = self.area_at(self.press_x,self.press_y)
377 to_area = self.area_at(x, y)
378 return from_area == to_area \
379 and from_area in [AREA_SAMPLE, AREA_CIRCLE, AREA_SQUARE]
381 def nearest_move_target(self, x,y):
383 Returns the nearest (x, y) the selection indicator can move to during a
384 move with the button held down.
386 area_source = self.area_at(self.press_x, self.press_y)
387 area = self.area_at(x,y)
388 if area == area_source and area in [AREA_CIRCLE, AREA_SQUARE]:
392 d = sqrt(dx*dx+dy*dy)
395 if area_source == AREA_CIRCLE:
396 rx = self.x0 + (self.r2+3.0)*x1
397 ry = self.y0 + (self.r2+3.0)*y1
400 dx = clamp(dx, -m, m)
401 dy = clamp(dy, -m, m)
407 x_,y_ = self.nearest_move_target(x,y)
408 self.select_color_at(x_,y_)
410 def area_at(self, x,y):
413 d = sqrt(dx*dx+dy*dy)
416 # Two tools, the picker and the prev/current comparator
418 tool_r = (sq2-1)*tool_M/(2*sq2)
419 tool_y0 = self.y0+tool_M-tool_r
420 cmp_x0 = self.x0+tool_M-tool_r
421 pick_x0 = self.x0-tool_M+tool_r
423 pick_dx = x - pick_x0
424 tool_dy = y - tool_y0
426 if tool_r > sqrt(cmp_dx*cmp_dx + tool_dy*tool_dy):
428 if tool_r > sqrt(pick_dx*pick_dx + tool_dy*tool_dy):
432 elif self.r2 < d <= self.r3:
434 elif abs(dx)<ceil(self.m) and abs(dy)<ceil(self.m):
436 elif self.rd < d <= self.r2:
441 def get_color_at(self, x,y):
442 area = self.area_at(x,y)
443 if area == AREA_CIRCLE:
445 h = 0.5 + 0.5*atan2(y-self.y0, self.x0-x)/pi
447 elif area == AREA_SAMPLE:
448 a = pi+atan2(y-self.y0, self.x0-x)
449 for i,a1 in enumerate(self.angles):
450 if a1-2*pi/CIRCLE_N < a < a1:
451 clr = self.simple_colors[i]
454 elif area == AREA_SQUARE:
456 s = (x-self.x0+self.m)/(2*self.m)
457 v = (y-self.y0+self.m)/(2*self.m)
458 return (h,s,1-v), True
461 def draw_square(self,width,height,radius):
462 img = cairo.ImageSurface(cairo.FORMAT_ARGB32, width,height)
463 cr = cairo.Context(img)
466 # Slightly darker border for the colour area
467 sw = max(0.75*self.stroke_width, 3.0)
468 cr.set_source_rgb(*NEUTRAL_DARK_GREY)
469 cr.rectangle(self.x0-m-sw, self.y0-m-sw, 2*(m+sw), 2*(m+sw))
479 g = cairo.LinearGradient(x1,y,x2,y)
480 g.add_color_stop_rgb(0.0, *hsv_to_rgb(h,0.0,1.0-v))
481 g.add_color_stop_rgb(1.0, *hsv_to_rgb(h,1.0,1.0-v))
483 cr.rectangle(x1,y, 2*m, ds)
489 x = self.x0-m + s*2*m
490 y = self.y0-m + (1-v)*2*m
491 cr.set_source_rgb(*hsv_to_rgb(1-h,1-s,1-v))
492 cr.arc(x,y, 3.0, 0.0, 2*pi)
497 def small_circle(self, cr, size, color, angle, rad):
498 x0 = self.x0 + rad*cos(angle)
499 y0 = self.y0 + rad*sin(angle)
500 cr.set_source_rgb(*color)
501 cr.arc(x0,y0,size/2., 0, 2*pi)
504 def small_triangle(self, cr, size, color, angle, rad):
505 x0 = self.x0 + rad*cos(angle)
506 y0 = self.y0 + rad*sin(angle)
507 x1,y1 = x0, y0 - size*sq32
508 x2,y2 = x0 + 0.5*size, y0 + size*sq36
509 x3,y3 = x0 - 0.5*size, y0 + size*sq36
510 cr.set_source_rgb(*color)
517 def small_triangle_down(self, cr, size, color, angle, rad):
518 x0 = self.x0 + rad*cos(angle)
519 y0 = self.y0 + rad*sin(angle)
520 x1,y1 = x0, y0 + size*sq32
521 x2,y2 = x0 + 0.5*size, y0 - size*sq36
522 x3,y3 = x0 - 0.5*size, y0 - size*sq36
523 cr.set_source_rgb(*color)
530 def small_square(self, cr, size, color, angle, rad):
531 x0 = self.x0 + rad*cos(angle)
532 y0 = self.y0 + rad*sin(angle)
533 x1,y1 = x0+size*sq22, y0+size*sq22
534 x2,y2 = x0+size*sq22, y0-size*sq22
535 x3,y3 = x0-size*sq22, y0-size*sq22
536 x4,y4 = x0-size*sq22, y0+size*sq22
537 cr.set_source_rgb(*color)
544 def small_rect(self, cr, size, color, angle, rad):
545 x0 = self.x0 + rad*cos(angle)
546 y0 = self.y0 + rad*sin(angle)
547 x1,y1 = x0+size*sq22, y0+size*0.3
548 x2,y2 = x0+size*sq22, y0-size*0.3
549 x3,y3 = x0-size*sq22, y0-size*0.3
550 x4,y4 = x0-size*sq22, y0+size*0.3
551 cr.set_source_rgb(*color)
558 def small_rect_vert(self, cr, size, color, angle, rad):
559 x0 = self.x0 + rad*cos(angle)
560 y0 = self.y0 + rad*sin(angle)
561 x1,y1 = x0+size*0.3, y0+size*sq22
562 x2,y2 = x0+size*0.3, y0-size*sq22
563 x3,y3 = x0-size*0.3, y0-size*sq22
564 x4,y4 = x0-size*0.3, y0+size*sq22
565 cr.set_source_rgb(*color)
576 def draw_central_fill(self, cr, r):
577 """Draws a neutral-coloured grey circle of the specified radius in the central area."""
578 cr.set_source_rgb(*NEUTRAL_MID_GREY)
579 cr.arc(self.x0, self.y0, r, 0, 2*pi)
582 def draw_harmony_ring(self,width,height):
583 """Draws the harmony ring if any colour harmonies are visible."""
586 points_size = min(width,height)/27.
587 img = cairo.ImageSurface(cairo.FORMAT_ARGB32, width,height)
588 cr = cairo.Context(img)
589 if not self.has_harmonies_visible():
590 self.draw_central_fill(cr, self.r2)
595 self.simple_colors = []
596 cr.set_line_width(0.75*self.stroke_width)
597 self.samples = [self.hsv]
598 for i in range(int(CIRCLE_N)):
599 c1 = c = h + i/CIRCLE_N
602 clr = hsv_to_rgb(c1,s,v)
604 self.simple_colors.append(clr)
607 self.angles.append(-an+pi/CIRCLE_N)
611 cr.set_source_rgb(*clr)
612 cr.move_to(self.x0,self.y0)
613 cr.arc(self.x0, self.y0, self.r2, a1, a2)
614 cr.line_to(self.x0,self.y0)
616 cr.set_source_rgb(*NEUTRAL_DARK_GREY) # "lines" between samples
618 # Indicate harmonic colors
619 if self.app.preferences.get("colorsampler.triadic", False) and i%(CIRCLE_N/3)==0:
620 self.small_triangle(cr, points_size, self.inv(clr), an, (self.r2+self.rd)/2)
621 try_put(self.samples, hsv)
622 if self.app.preferences.get("colorsampler.complementary", False) and i%(CIRCLE_N/2)==0:
623 self.small_circle(cr, points_size, self.inv(clr), an, (self.r2+self.rd)/2)
624 try_put(self.samples, hsv)
625 if self.app.preferences.get("colorsampler.square", False) and i%(CIRCLE_N/4)==0:
626 self.small_square(cr, points_size, self.inv(clr), an, (self.r2+self.rd)/2)
627 try_put(self.samples, hsv)
628 # FIXME: should this harmonies be expressed in terms of CIRCLE_N?
629 if self.app.preferences.get("colorsampler.double_comp", False) and i in [0,2,6,8]:
630 self.small_rect_vert(cr, points_size, self.inv(clr), an, (self.r2+self.rd)/2)
631 try_put(self.samples, hsv)
632 if self.app.preferences.get("colorsampler.split_comp", False) and i in [0,5,7]:
633 self.small_triangle_down(cr, points_size, self.inv(clr), an, (self.r2+self.rd)/2)
634 try_put(self.samples, hsv)
635 if self.app.preferences.get("colorsampler.analogous", False) and i in [0,1,CIRCLE_N-1]:
636 self.small_rect(cr, points_size, self.inv(clr), an, (self.r2+self.rd)/2)
637 try_put(self.samples, hsv)
639 self.draw_central_fill(cr, self.rd)
640 # And an inner thin line
641 cr.set_source_rgb(*NEUTRAL_DARK_GREY)
642 cr.arc(self.x0, self.y0, self.rd, 0, 2*pi)
646 def draw_circle_indicator(self, cr):
647 """Draws the indicator which shows the current hue on the outer ring."""
650 x1 = self.x0 + self.r2*cos(-a)
651 y1 = self.y0 + self.r2*sin(-a)
652 x2 = self.x0 + self.r3*cos(-a)
653 y2 = self.y0 + self.r3*sin(-a)
654 self.last_line = x1,y1, x2,y2, h
655 cr.set_line_width(0.8*self.stroke_width)
656 cr.set_source_rgb(0, 0, 0)
661 def draw_circles(self, cr):
662 """Draws two grey lines just inside and outside the outer hue ring."""
663 cr.set_line_width(0.75*self.stroke_width)
664 cr.set_source_rgb(*NEUTRAL_DARK_GREY)
665 cr.arc(self.x0,self.y0, self.r2, 0, 2*pi)
667 cr.arc(self.x0,self.y0, self.r3, 0, 2*pi)
670 def draw_circle(self, w, h):
672 return self.circle_img
673 img = cairo.ImageSurface(cairo.FORMAT_ARGB32, w,h)
674 cr = cairo.Context(img)
675 cr.set_line_width(0.75*self.stroke_width)
678 clr = hsv_to_rgb(a1/(2*pi), 1.0, 1.0)
679 x1,y1,x2,y2 = self.calc_line(a1)
681 cr.set_source_rgb(*clr)
685 self.circle_img = img
688 def draw_comparison_semicircles(self, cr):
691 r = (sq2-1)*M/(2*sq2)
696 prev_col = self.get_previous_color()
697 curr_col = self.color
699 cr.set_source_rgb(*NEUTRAL_MID_GREY)
700 cr.arc(x0, y0, 0.9*r, a0, a0+(2*pi))
703 cr.set_source_rgb(*prev_col)
704 cr.arc(x0, y0, 0.8*r, a0, a0+(2*pi))
707 cr.set_source_rgb(*curr_col)
708 cr.arc(x0, y0, 0.8*r, a0+(pi/2), a0+(3*pi/2))
711 def draw_picker_icon(self, cr):
714 r = (sq2-1)*M/(2*sq2)
719 # Pick the most suitable themed icon from our sorted cache.
720 # This should be the largest icon that fits in the tool circle,
721 # but we must use the smallest available if the circle is too small.
723 for pr, pw, ph, ppixbuf, psize in self.picker_icons:
724 if pixbuf is not None and pr > r:
732 cr.arc(x0, y0, 0.9*r, a0, a0+(2*pi))
734 bg = self.style.bg[self.picker_state]
735 bg_rgb = [bg.red_float, bg.green_float, bg.blue_float]
736 cr.set_source_rgb(*bg_rgb)
740 if pixbuf is not None:
741 cr.set_source_pixbuf(pixbuf, int(x0-w/2), int(y0-h/2))
745 def draw(self,w,event):
748 cr = self.window.cairo_create()
749 cr.set_source_surface(self.draw_circle(self.w,self.h))
751 cr.set_source_surface(self.draw_harmony_ring(self.w,self.h))
753 self.draw_circle_indicator(cr)
754 self.draw_circles(cr)
755 cr.set_source_surface(self.draw_square(self.w, self.h, self.rd*0.95))
757 self.draw_comparison_semicircles(cr)
758 self.draw_picker_icon(cr)
760 class VSelector(GColorSelector):
761 def __init__(self, app, color=(1,0,0), height=16):
762 GColorSelector.__init__(self,app)
764 self.hsv = rgb_to_hsv(*color)
765 self.set_size_request(height*2,height)
767 def get_color_at(self, x,y):
769 v = clamp(x/self.w, 0.0, 1.0)
773 self.select_color_at(x,y)
775 def draw_gradient(self,cr, start,end, hsv=True):
777 clr1 = hsv_to_rgb(*start)
778 clr2 = hsv_to_rgb(*end)
782 g = cairo.LinearGradient(0,0,self.w,self.h)
783 g.add_color_stop_rgb(0.0, *clr1)
784 g.add_color_stop_rgb(1.0, *clr2)
786 cr.rectangle(0,0,self.w,self.h)
789 def draw_line_at(self, cr, x):
790 cr.set_source_rgb(0,0,0)
792 cr.line_to(x, self.h)
795 def draw(self,w, event):
798 cr = self.window.cairo_create()
800 self.draw_gradient(cr, (h,s,0.), (h,s,1.))
803 self.draw_line_at(cr, x1)
805 class HSelector(VSelector):
806 def get_color_at(self,x,y):
808 h = clamp(x/self.w, 0.0, 1.0)
811 def draw(self,w, event):
814 cr = self.window.cairo_create()
820 cr.set_source_rgb(*hsv_to_rgb(h1,s,v))
821 cr.rectangle(x,0,dx,self.h)
827 self.draw_line_at(cr, x1)
829 class SSelector(VSelector):
830 def get_color_at(self, x,y):
832 s = clamp(x/self.w, 0.0, 1.0)
835 def draw(self,w, event):
838 cr = self.window.cairo_create()
840 self.draw_gradient(cr, (h,0.,v), (h,1.,v))
843 self.draw_line_at(cr, x1)
845 class RSelector(VSelector):
846 def get_color_at(self,x,y):
848 r = clamp(x/self.w, 0.0, 1.0)
849 return (r,g,b), False
851 def draw(self,w, event):
854 cr = self.window.cairo_create()
856 self.draw_gradient(cr, (0.,g,b),(1.,g,b), hsv=False)
858 self.draw_line_at(cr,x1)
860 class GSelector(VSelector):
861 def get_color_at(self,x,y):
863 g = clamp(x/self.w, 0.0, 1.0)
864 return (r,g,b), False
866 def draw(self,w, event):
869 cr = self.window.cairo_create()
871 self.draw_gradient(cr, (r,0.,b),(r,1.,b), hsv=False)
873 self.draw_line_at(cr,x1)
875 class BSelector(VSelector):
876 def get_color_at(self,x,y):
878 b = clamp(x/self.w, 0.0, 1.0)
879 return (r,g,b), False
881 def draw(self,w, event):
884 cr = self.window.cairo_create()
886 self.draw_gradient(cr, (r,g,0.),(r,g,1.), hsv=False)
888 self.draw_line_at(cr,x1)
890 def make_spin(min,max, changed_cb):
891 adj = gtk.Adjustment(0,min,max, 1,10)
892 btn = gtk.SpinButton(adj)
893 btn.connect('value-changed', changed_cb)
894 btn.set_sensitive(False)
897 class HSVSelector(gtk.VBox):
898 def __init__(self, app, color=(1.,0.,0)):
899 gtk.VBox.__init__(self)
901 self.hsv = rgb_to_hsv(*color)
905 self.hsel = hsel = HSelector(app, color)
906 hsel.on_select = self.user_selected_color
907 self.hspin = hspin = make_spin(0,359, self.hue_change)
908 hbox.pack_start(hsel, expand=True)
909 hbox.pack_start(hspin, expand=False)
912 self.ssel = ssel = SSelector(app, color)
913 ssel.on_select = self.user_selected_color
914 self.sspin = sspin = make_spin(0,100, self.sat_change)
915 sbox.pack_start(ssel, expand=True)
916 sbox.pack_start(sspin, expand=False)
919 self.vsel = vsel = VSelector(app, color)
920 vsel.on_select = self.user_selected_color
921 self.vspin = vspin = make_spin(0,100, self.val_change)
922 vbox.pack_start(vsel, expand=True)
923 vbox.pack_start(vspin, expand=False)
925 self.pack_start(hbox, expand=False)
926 self.pack_start(sbox, expand=False)
927 self.pack_start(vbox, expand=False)
929 self.set_tooltip_text(_("Change Hue, Saturation and Value"))
931 def user_selected_color(self, hsv):
935 def set_color(self, hsv):
937 self.hsv = h,s,v = hsv
938 self.hspin.set_value(h*359)
939 self.sspin.set_value(s*100)
940 self.vspin.set_value(v*100)
941 self.hsel.set_color(hsv)
942 self.ssel.set_color(hsv)
943 self.vsel.set_color(hsv)
945 self.color = hsv_to_rgb(*hsv)
947 def on_select(self, color):
950 def hue_change(self, spin):
954 self.set_color((spin.get_value()/359., s,v))
956 def sat_change(self, spin):
960 self.set_color((h, spin.get_value()/100., v))
962 def val_change(self, spin):
966 self.set_color((h,s, spin.get_value()/100.))
968 class RGBSelector(gtk.VBox):
969 def __init__(self, app, color=(1.,0.,0)):
970 gtk.VBox.__init__(self)
972 self.hsv = rgb_to_hsv(*color)
976 self.rsel = rsel = RSelector(app, color)
977 rsel.on_select = self.user_selected_color
978 self.rspin = rspin = make_spin(0,255, self.r_change)
979 rbox.pack_start(rsel, expand=True)
980 rbox.pack_start(rspin, expand=False)
983 self.gsel = gsel = GSelector(app, color)
984 gsel.on_select = self.user_selected_color
985 self.gspin = gspin = make_spin(0,255, self.g_change)
986 gbox.pack_start(gsel, expand=True)
987 gbox.pack_start(gspin, expand=False)
990 self.bsel = bsel = BSelector(app, color)
991 bsel.on_select = self.user_selected_color
992 self.bspin = bspin = make_spin(0,255, self.b_change)
993 bbox.pack_start(bsel, expand=True)
994 bbox.pack_start(bspin, expand=False)
996 self.pack_start(rbox, expand=False)
997 self.pack_start(gbox, expand=False)
998 self.pack_start(bbox, expand=False)
1000 self.set_tooltip_text(_("Change Red, Green and Blue components"))
1002 def user_selected_color(self, hsv):
1006 def set_color(self, hsv):
1008 self.color = r,g,b = hsv_to_rgb(*hsv)
1009 self.rspin.set_value(r*255)
1010 self.gspin.set_value(g*255)
1011 self.bspin.set_value(b*255)
1012 self.rsel.set_color(hsv)
1013 self.gsel.set_color(hsv)
1014 self.bsel.set_color(hsv)
1018 def on_select(self, hsv):
1021 def r_change(self, spin):
1025 self.set_color(rgb_to_hsv(spin.get_value()/255., g,b))
1027 def g_change(self, spin):
1031 self.set_color(rgb_to_hsv(r, spin.get_value()/255., b))
1033 def b_change(self, spin):
1037 self.set_color(rgb_to_hsv(r,g, spin.get_value()/255.))
1039 class Selector(gtk.VBox):
1041 CIRCLE_MIN_SIZE = (150, 150)
1043 def __init__(self, app):
1044 gtk.VBox.__init__(self)
1046 self.recent = RecentColors(app)
1047 self.circle = CircleSelector(app)
1048 self.circle.set_size_request(*self.CIRCLE_MIN_SIZE)
1050 # The colour circle seems to need its own window to draw sanely when
1051 # packed into a Tool wrapper, which probably indicates that something's
1053 evbox = gtk.EventBox()
1054 evbox.add(self.circle)
1055 self.pack_start(evbox, expand=True)
1057 self.rgb_selector = RGBSelector(app)
1058 self.hsv_selector = HSVSelector(app)
1059 self.rgb_selector.on_select = self.rgb_selected
1060 self.hsv_selector.on_select = self.hsv_selected
1062 nb.append_page(self.rgb_selector, gtk.Label(_('RGB')))
1063 nb.append_page(self.hsv_selector, gtk.Label(_('HSV')))
1066 self.exp_history = expander = ElasticExpander(_('Color history'))
1067 expander.set_spacing(6)
1068 expander.add(self.recent)
1069 expander.connect("notify::expanded", self.expander_expanded_cb, 'history')
1070 self.pack_start(expander, expand=False)
1073 self.exp_details = expander = ElasticExpander(_('Details'))
1074 expander.set_spacing(6)
1076 expander.connect("notify::expanded", self.expander_expanded_cb, 'details')
1077 self.pack_start(expander, expand=False)
1079 # Colour scheme harmonies
1080 def harmony_checkbox(attr, label, tooltip=None):
1081 cb = gtk.CheckButton(label)
1082 pref = "colorsampler.%s" % (attr,)
1083 cb.set_active(self.app.preferences.get(pref, False))
1084 cb.connect('toggled', self.harmony_toggled, attr)
1085 if tooltip is not None:
1086 cb.set_tooltip_text(tooltip)
1087 vbox2.pack_start(cb, expand=False)
1089 self.exp_config = expander = ElasticExpander(_('Harmonies'))
1091 harmony_checkbox('analogous', _('Analogous'), _("Three nearby hues on the color wheel.\nOften found in nature, frequently pleasing to the eye."))
1092 harmony_checkbox('complementary', _('Complementary'), _("Two opposite hues on the color wheel.\nVibrant, and maximally contrasting."))
1093 harmony_checkbox('split_comp', _('Split complementary'), _("Two hues next to the current hue's complement.\nContrasting, but adds a possibly pleasing harmony."))
1094 harmony_checkbox('double_comp', _('Double complementary'), _("Four hues in two complementary pairs."))
1095 harmony_checkbox('square', _('Square'), _("Four equally-spaced hues"))
1096 harmony_checkbox('triadic', _('Triadic'), _("Three equally-spaced hues.\nVibrant with equal tension."))
1099 cb_sv = gtk.CheckButton(_('Change value/saturation'))
1100 cb_sv.set_active(True)
1101 cb_sv.connect('toggled', self.toggle_blend, 'value')
1102 cb_opposite = gtk.CheckButton(_('Blend each color to opposite'))
1103 cb_opposite.connect('toggled', self.toggle_blend, 'opposite')
1104 cb_neg = gtk.CheckButton(_('Blend each color to negative'))
1105 cb_neg.connect('toggled', self.toggle_blend, 'negative')
1106 vbox3.pack_start(cb_sv, expand=False)
1107 vbox3.pack_start(cb_opposite, expand=False)
1108 vbox3.pack_start(cb_neg, expand=False)
1110 expander.connect("notify::expanded", self.expander_expanded_cb, 'harmonies')
1111 self.pack_start(expander, expand=False)
1113 self.circle.on_select = self.hue_selected
1114 self.circle.get_previous_color = self.previous_color
1115 self.recent.on_select = self.recent_selected
1116 self.widgets = [self.circle, self.rgb_selector, self.hsv_selector, self.recent]
1118 self.value_blends = True
1119 self.opposite_blends = False
1120 self.negative_blends = False
1122 self.expander_prefs_loaded = False
1123 self.connect("show", self.show_cb)
1125 def toggle_blend(self, checkbox, name):
1126 attr = name+'_blends'
1127 setattr(self, attr, not getattr(self, attr))
1129 def harmony_toggled(self, checkbox, attr):
1130 pref = "colorsampler.%s" % (attr,)
1131 self.app.preferences[pref] = checkbox.get_active()
1132 self.circle.configure_calc()
1135 def previous_color(self):
1136 return self.app.ch.last_color
1138 def set_color(self, hsv, exclude=None):
1139 for w in self.widgets:
1140 if w is not exclude:
1142 self.color = hsv_to_rgb(*hsv)
1146 def rgb_selected(self, hsv):
1147 self.set_color(hsv, exclude=self.rgb_selector)
1149 def hsv_selected(self, hsv):
1150 self.set_color(hsv, exclude=self.hsv_selector)
1152 def hue_selected(self, hsv):
1153 self.set_color(hsv, exclude=self.circle)
1155 def recent_selected(self, hsv):
1156 self.set_color(hsv, exclude=self.recent)
1158 def on_select(self, hsv):
1161 def expander_expanded_cb(self, expander, junk, cfg_stem):
1162 # Save the expander state
1163 if not self.expander_prefs_loaded:
1165 self.app.preferences['colorsampler.%s_expanded' % cfg_stem] \
1166 = bool(expander.get_expanded())
1168 def show_cb(self, widget):
1169 # Restore expander state from prefs.
1170 # Use "show", not "map", so that it fires before we have a window
1171 assert not self.window
1172 assert not self.expander_prefs_loaded
1173 # If we wait until then, sidebar positions will drift as a result.
1174 if self.app.preferences.get("colorsampler.history_expanded", False):
1175 self.exp_history.set_expanded(True)
1176 if self.app.preferences.get("colorsampler.details_expanded", False):
1177 self.exp_details.set_expanded(True)
1178 if self.app.preferences.get("colorsampler.harmonies_expanded", False):
1179 self.exp_config.set_expanded(True)
1180 self.expander_prefs_loaded = True
1183 class ToolWidget (Selector):
1185 stock_id = stock.TOOL_COLOR_SAMPLER
1187 def __init__(self, app):
1188 Selector.__init__(self, app)
1190 self.app.brush.observers.append(self.brush_modified_cb)
1191 self.stop_callback = False
1193 # The first callback notification happens before the window is initialized
1194 self.set_color(app.brush.get_color_hsv())
1196 def brush_modified_cb(self, settings):
1197 if not settings.intersection(('color_h', 'color_s', 'color_v')):
1199 if self.stop_callback:
1201 self.stop_callback = True
1202 hsv = self.app.brush.get_color_hsv()
1204 self.stop_callback = False
1206 def on_select(self, hsv):
1207 if self.stop_callback:
1209 self.app.brush.set_color_hsv(hsv)
1212 class Window(windowing.SubWindow):
1213 def __init__(self,app):
1214 windowing.SubWindow.__init__(self, app)
1215 self.set_title(_('MyPaint color selector'))
1216 self.set_role('Color selector')
1217 self.selector = Selector(app, self)
1218 self.selector.on_select = self.on_select
1219 self.exp_history = self.selector.exp_history
1220 self.exp_details = self.selector.exp_details
1221 self.exp_config = self.selector.exp_config
1223 self.add(self.selector)
1224 self.app.brush.observers.append(self.brush_modified_cb)
1225 self.stop_callback = False
1227 # The first callback notification happens before the window is initialized
1228 self.selector.set_color(app.brush.get_color_hsv())
1230 def brush_modified_cb(self, settings):
1231 if not settings.intersection(('color_h', 'color_s', 'color_v')):
1233 if self.stop_callback:
1235 self.stop_callback = True
1236 hsv = self.app.brush.get_color_hsv()
1237 self.selector.set_color(hsv)
1238 self.stop_callback = False
1240 def on_select(self, hsv):
1241 if self.stop_callback:
1243 self.app.brush.set_color_hsv(hsv)