OSDN Git Service

colorize: add brushmodifier action & toolbar entry
[mypaint-anime/master.git] / gui / colorsamplerwindow.py
1 import gtk
2 gdk = gtk.gdk
3 from math import pi, sin, cos, sqrt, atan2, ceil
4 import struct
5 import cairo
6 import windowing
7 import stock
8 from elastic import ElasticExpander
9 from lib.helpers import rgb_to_hsv, hsv_to_rgb, clamp
10 from gettext import gettext as _
11 import gobject
12
13 CSTEP=0.007
14 PADDING=4
15
16 class GColorSelector(gtk.DrawingArea):
17     def __init__(self, app):
18         gtk.DrawingArea.__init__(self)
19         self.color = (1,0,0)
20         self.hsv = (0,1,1)
21         self.app = app
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 |
26                         gdk.ENTER_NOTIFY |
27                         gdk.LEAVE_NOTIFY |
28                         gdk.DROP_FINISHED |
29                         gdk.DROP_START |
30                         gdk.DRAG_STATUS |
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
41         self.grabbed = False
42         self.set_size_request(110, 100)
43         self.has_hover_areas = False
44
45     def test_move(self, x, y):
46         """
47         Return true during motion if the selection indicator
48         should move to where the cursor is pointing.
49         """
50         return True
51
52     def test_button_release(self, x, y):
53         """
54         Return true after a button-release-event if the colour under the
55         pointer should be selected.
56         """
57         return True
58
59     def move(self, x, y):
60         pass
61
62     def redraw_on_select(self):
63         self.queue_draw()
64
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:
69             return
70         if self.test_move(event.x, event.y):
71             if not self.grabbed:
72                 self.grabbed = True
73                 self.grab_add()
74             self.move(event.x,event.y)
75
76     def on_configure(self,w, size):
77         self.x_rel = size.x
78         self.y_rel = size.y
79         self.w = size.width
80         self.h = size.height
81         self.configure_calc()
82
83     def configure_calc(self):
84         pass
85
86     def on_select(self,color):
87         pass
88
89     def update_tooltip(self, x, y):
90         """Updates the tooltip during motion, if tooltips are zoned."""
91         pass
92
93     def get_color_at(self, x,y):
94         return self.hsv, True
95
96     def select_color_at(self, x,y):
97         color, is_hsv = self.get_color_at(x,y)
98         if color is None:
99             return
100         if is_hsv:
101             self.hsv = color
102             self.color = hsv_to_rgb(*color)
103         else:
104             self.color = 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)
113
114     def set_color(self, hsv):
115         self.hsv = hsv
116         self.color = hsv_to_rgb(*hsv)
117         self.queue_draw()
118
119     def get_color(self):
120         return self.color
121
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
128         # the select.
129         self.device_pressed = event.device
130
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
135         if self.grabbed:
136             self.grab_remove()
137             self.grabbed = False
138
139     def draw(self,w,event):
140         if not self.window:
141             return
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)
145         cr.fill()
146
147 class RectSlot(GColorSelector):
148     def __init__(self,app,color=(1.0,1.0,1.0),size=32):
149         GColorSelector.__init__(self, app)
150         self.color = color
151         self.set_size_request(size,size)
152
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
158         self.slots = []
159         for i in xrange(N):
160             slot = RectSlot(app)
161             slot.on_select = self.slot_selected
162             self.pack_start(slot, expand=True)
163             self.slots.append(slot)
164         self.app = app
165         app.ch.color_pushed_observers.append(self.refill_slots)
166         self.set_tooltip_text(_("Recently used colors"))
167         self.refill_slots(None)
168         self.show_all()
169
170     def slot_selected(self,color):
171         self.on_select(color)
172
173     def on_select(self,color):
174         pass
175
176     def refill_slots(self, *junk):
177         for hsv,slot in zip(self.app.ch.colors, reversed(self.slots)):
178             slot.set_color(hsv)
179
180     def set_color(self, hsv):
181         pass
182
183 CIRCLE_N = 12.0
184 SLOTS_N = 5
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
187
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
195
196 CIRCLE_TOOLTIPS = \
197   { AREA_SQUARE: _('Change Saturation and Value'),
198     AREA_INSIDE: None,
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'),
203     AREA_OUTSIDE: None,
204     }
205
206 sq32 = sqrt(3)/2
207 sq33 = sqrt(3)/3
208 sq36 = sq33/2
209 sq22 = sqrt(2)/2
210
211 def try_put(list, item):
212     if not item in list:
213         list.append(item)
214
215 class CircleSelector(GColorSelector):
216
217     grab_mask = gdk.BUTTON_RELEASE_MASK | gdk.BUTTON1_MOTION_MASK
218     picking = False
219
220     def __init__(self,app,color=(1,0,0)):
221         GColorSelector.__init__(self,app)
222         self.color = color
223         self.hsv = rgb_to_hsv(*color)
224
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)
228
229         self.has_hover_areas = True
230         self.hover_area = AREA_OUTSIDE
231         self.previous_hover_area = None
232         self.previous_hover_xy = None
233
234         self.picker_icons = []
235         self.connect("map-event", self.init_picker_icons)
236         self.connect("style-set", self.init_picker_icons)
237
238         self.picker_state = gtk.STATE_NORMAL
239         self.button_press = None
240
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
246             self.queue_draw()
247             return True
248         return GColorSelector.on_button_press(self, widget, event)
249
250     def on_button_release(self, widget, event):
251         if self.picking:
252             self.app.pick_color_at_pointer(self)
253             self.picking = False
254             gdk.pointer_ungrab()
255             return True
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
261             self.queue_draw()
262             self.button_press = None
263             return True
264         return GColorSelector.on_button_release(self, widget, event)
265
266     def motion(self, widget, event):
267         if self.picking:
268             if event.state & gdk.BUTTON1_MASK:
269                 self.app.pick_color_at_pointer(self)
270             return True
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
276                     self.queue_draw()
277             else:
278                 if self.picker_state != gtk.STATE_NORMAL:
279                     self.picker_state = gtk.STATE_NORMAL
280                     self.queue_draw()
281             return True
282         return GColorSelector.motion(self, widget, event)
283
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:
288             self.picking = True
289
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
295         pixbufs = []
296         for size in sizes:
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
304
305     def color_pushed_cb(self, pushed_color):
306         self.queue_draw()
307
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)
315
316     def get_previous_color(self):
317         return self.color
318
319     def update_hover_area(self, x, y):
320         area = self.area_at(x, y)
321         old_area = area
322         if self.previous_hover_area is None:
323             old_area = None
324             tooltip = CIRCLE_TOOLTIPS.get(area, None)
325             self.set_tooltip_text(tooltip)
326             self.previous_hover_area = area, x, y
327         else:
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
332         if area != old_area:
333             self.hover_area = area
334             if area == AREA_PICKER:
335                 self.picker_state = gtk.STATE_PRELIGHT
336             else:
337                 self.picker_state = gtk.STATE_NORMAL
338             if AREA_PICKER in (area, old_area):
339                 self.queue_draw()
340
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)
346         return x1,y1,x2,y2
347
348     def configure_calc(self):
349         padding = 5
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
357         else:
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
363
364     def set_color(self, hsv, redraw=True):
365         self.hsv = hsv
366         self.color = hsv_to_rgb(*hsv)
367         if redraw:
368             self.queue_draw()
369
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)
374
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]
380
381     def nearest_move_target(self, x,y):
382         """
383         Returns the nearest (x, y) the selection indicator can move to during a
384         move with the button held down.
385         """
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]:
389             return x,y
390         dx = x-self.x0
391         dy = y-self.y0
392         d = sqrt(dx*dx+dy*dy)
393         x1 = dx/d
394         y1 = dy/d
395         if area_source == AREA_CIRCLE:
396             rx = self.x0 + (self.r2+3.0)*x1
397             ry = self.y0 + (self.r2+3.0)*y1
398         else:
399             m = self.m
400             dx = clamp(dx, -m, m)
401             dy = clamp(dy, -m, m)
402             rx = self.x0 + dx
403             ry = self.y0 + dy
404         return rx,ry
405
406     def move(self,x,y):
407         x_,y_ = self.nearest_move_target(x,y)
408         self.select_color_at(x_,y_)
409
410     def area_at(self, x,y):
411         dx = x-self.x0
412         dy = y-self.y0
413         d = sqrt(dx*dx+dy*dy)
414         sq2 = sqrt(2.0)
415
416         # Two tools, the picker and the prev/current comparator
417         tool_M = self.M/2.0
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
422         cmp_dx = x - cmp_x0
423         pick_dx = x - pick_x0
424         tool_dy = y - tool_y0
425
426         if tool_r > sqrt(cmp_dx*cmp_dx + tool_dy*tool_dy):
427             return AREA_COMPARE
428         if tool_r > sqrt(pick_dx*pick_dx + tool_dy*tool_dy):
429             return AREA_PICKER
430         elif d > self.r3:
431             return AREA_OUTSIDE
432         elif self.r2 < d <= self.r3:
433             return AREA_CIRCLE
434         elif abs(dx)<ceil(self.m) and abs(dy)<ceil(self.m):
435             return AREA_SQUARE
436         elif self.rd < d <= self.r2:
437             return AREA_SAMPLE
438         else:
439             return AREA_INSIDE
440
441     def get_color_at(self, x,y):
442         area = self.area_at(x,y)
443         if area == AREA_CIRCLE:
444             h,s,v = self.hsv
445             h = 0.5 + 0.5*atan2(y-self.y0, self.x0-x)/pi
446             return (h,s,v), True
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]
452                     return clr, False
453             return None, False
454         elif area == AREA_SQUARE:
455             h,s,v = self.hsv
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
459         return None, False
460
461     def draw_square(self,width,height,radius):
462         img = cairo.ImageSurface(cairo.FORMAT_ARGB32, width,height)
463         cr = cairo.Context(img)
464         m = radius/sqrt(2.0)
465
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))
470         cr.fill()
471
472         h,s,v = self.hsv
473         ds = 2*m*CSTEP
474         v = 0.0
475         x1 = self.x0-m
476         x2 = self.x0+m
477         y = self.y0-m
478         while v < 1.0:
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))
482             cr.set_source(g)
483             cr.rectangle(x1,y, 2*m, ds)
484             cr.fill_preserve()
485             cr.stroke()
486             y += ds
487             v += CSTEP
488         h,s,v = self.hsv
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)
493         cr.stroke()
494
495         return img
496
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)
502         cr.fill()
503
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)
511         cr.move_to(x1,y1)
512         cr.line_to(x2,y2)
513         cr.line_to(x3,y3)
514         cr.line_to(x1,y1)
515         cr.fill()
516
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)
524         cr.move_to(x1,y1)
525         cr.line_to(x2,y2)
526         cr.line_to(x3,y3)
527         cr.line_to(x1,y1)
528         cr.fill()
529
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)
538         cr.move_to(x1,y1)
539         cr.line_to(x2,y2)
540         cr.line_to(x3,y3)
541         cr.line_to(x4,y4)
542         cr.fill()
543
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)
552         cr.move_to(x1,y1)
553         cr.line_to(x2,y2)
554         cr.line_to(x3,y3)
555         cr.line_to(x4,y4)
556         cr.fill()
557
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)
566         cr.move_to(x1,y1)
567         cr.line_to(x2,y2)
568         cr.line_to(x3,y3)
569         cr.line_to(x4,y4)
570         cr.fill()
571
572     def inv(self, rgb):
573         r,g,b = rgb
574         return 1-r,1-g,1-b
575
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)
580         cr.fill()
581
582     def draw_harmony_ring(self,width,height):
583         """Draws the harmony ring if any colour harmonies are visible."""
584         if not self.window:
585             return
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)
591             return img
592         h,s,v = self.hsv
593         a = h*2*pi
594         self.angles = []
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
600             if c1 > 1:
601                 c1 -= 1
602             clr = hsv_to_rgb(c1,s,v)
603             hsv = c1,s,v
604             self.simple_colors.append(clr)
605             delta = c1 - h
606             an = -c1*2*pi
607             self.angles.append(-an+pi/CIRCLE_N)
608             a1 = an-pi/CIRCLE_N
609             a2 = an+pi/CIRCLE_N
610             cr.new_path()
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)
615             cr.fill_preserve()
616             cr.set_source_rgb(*NEUTRAL_DARK_GREY) # "lines" between samples
617             cr.stroke()
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)
638         # Fill the centre
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)
643         cr.stroke()
644         return img
645
646     def draw_circle_indicator(self, cr):
647         """Draws the indicator which shows the current hue on the outer ring."""
648         h, s, v = self.hsv
649         a = h*2*pi
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)
657         cr.move_to(x1,y1)
658         cr.line_to(x2,y2)
659         cr.stroke()
660
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)
666         cr.stroke()
667         cr.arc(self.x0,self.y0, self.r3, 0, 2*pi)
668         cr.stroke()
669
670     def draw_circle(self, w, h):
671         if self.circle_img:
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)
676         a1 = 0.0
677         while a1 < 2*pi:
678             clr = hsv_to_rgb(a1/(2*pi), 1.0, 1.0)
679             x1,y1,x2,y2 = self.calc_line(a1)
680             a1 += CSTEP
681             cr.set_source_rgb(*clr)
682             cr.move_to(x1,y1)
683             cr.line_to(x2,y2)
684             cr.stroke()
685         self.circle_img = img
686         return img
687
688     def draw_comparison_semicircles(self, cr):
689         sq2 = sqrt(2.0)
690         M = self.M/2.0
691         r = (sq2-1)*M/(2*sq2)
692         x0 = self.x0+M-r
693         y0 = self.y0+M-r
694         a0 = pi/4
695
696         prev_col = self.get_previous_color()
697         curr_col = self.color
698
699         cr.set_source_rgb(*NEUTRAL_MID_GREY)
700         cr.arc(x0, y0, 0.9*r, a0, a0+(2*pi))
701         cr.fill()
702
703         cr.set_source_rgb(*prev_col)
704         cr.arc(x0, y0, 0.8*r, a0, a0+(2*pi))
705         cr.fill()
706
707         cr.set_source_rgb(*curr_col)
708         cr.arc(x0, y0, 0.8*r, a0+(pi/2), a0+(3*pi/2))
709         cr.fill()
710
711     def draw_picker_icon(self, cr):
712         sq2 = sqrt(2.0)
713         M = self.M/2.0
714         r = (sq2-1)*M/(2*sq2)
715         x0 = self.x0-M+r
716         y0 = self.y0+M-r
717         a0 = pi/4
718
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.
722         pixbuf = None
723         for pr, pw, ph, ppixbuf, psize in self.picker_icons:
724             if pixbuf is not None and pr > r:
725                 break
726             pixbuf = ppixbuf
727             w = pw
728             h = ph
729
730         # Background
731         cr.save()
732         cr.arc(x0, y0, 0.9*r, a0, a0+(2*pi))
733         cr.clip()
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)
737         cr.paint()
738
739         # Picker icon
740         if pixbuf is not None:
741             cr.set_source_pixbuf(pixbuf, int(x0-w/2), int(y0-h/2))
742             cr.paint()
743         cr.restore()
744
745     def draw(self,w,event):
746         if not self.window:
747             return
748         cr = self.window.cairo_create()
749         cr.set_source_surface(self.draw_circle(self.w,self.h))
750         cr.paint()
751         cr.set_source_surface(self.draw_harmony_ring(self.w,self.h))
752         cr.paint()
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))
756         cr.paint()
757         self.draw_comparison_semicircles(cr)
758         self.draw_picker_icon(cr)
759
760 class VSelector(GColorSelector):
761     def __init__(self, app, color=(1,0,0), height=16):
762         GColorSelector.__init__(self,app)
763         self.color = color
764         self.hsv = rgb_to_hsv(*color)
765         self.set_size_request(height*2,height)
766
767     def get_color_at(self, x,y):
768         h,s,v = self.hsv
769         v = clamp(x/self.w, 0.0, 1.0)
770         return (h,s,v), True
771
772     def move(self, x,y):
773         self.select_color_at(x,y)
774
775     def draw_gradient(self,cr, start,end, hsv=True):
776         if hsv:
777             clr1 = hsv_to_rgb(*start)
778             clr2 = hsv_to_rgb(*end)
779         else:
780             clr1 = start
781             clr2 = 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)
785         cr.set_source(g)
786         cr.rectangle(0,0,self.w,self.h)
787         cr.fill()
788
789     def draw_line_at(self, cr, x):
790         cr.set_source_rgb(0,0,0)
791         cr.move_to(x,0)
792         cr.line_to(x, self.h)
793         cr.stroke()
794
795     def draw(self,w, event):
796         if not self.window:
797             return
798         cr = self.window.cairo_create()
799         h,s,v = self.hsv
800         self.draw_gradient(cr, (h,s,0.), (h,s,1.))
801
802         x1 = v*self.w
803         self.draw_line_at(cr, x1)
804
805 class HSelector(VSelector):
806     def get_color_at(self,x,y):
807         h,s,v = self.hsv
808         h = clamp(x/self.w, 0.0, 1.0)
809         return (h,s,v), True
810
811     def draw(self,w, event):
812         if not self.window:
813             return
814         cr = self.window.cairo_create()
815         h,s,v = self.hsv
816         dx = self.w*CSTEP
817         x = 0
818         h1 = 0.
819         while h1 < 1:
820             cr.set_source_rgb(*hsv_to_rgb(h1,s,v))
821             cr.rectangle(x,0,dx,self.h)
822             cr.fill_preserve()
823             cr.stroke()
824             h1 += CSTEP
825             x += dx
826         x1 = h*self.w
827         self.draw_line_at(cr, x1)
828
829 class SSelector(VSelector):
830     def get_color_at(self, x,y):
831         h,s,v = self.hsv
832         s = clamp(x/self.w, 0.0, 1.0)
833         return (h,s,v), True
834
835     def draw(self,w, event):
836         if not self.window:
837             return
838         cr = self.window.cairo_create()
839         h,s,v = self.hsv
840         self.draw_gradient(cr, (h,0.,v), (h,1.,v))
841
842         x1 = s*self.w
843         self.draw_line_at(cr, x1)
844
845 class RSelector(VSelector):
846     def get_color_at(self,x,y):
847         r,g,b = self.color
848         r = clamp(x/self.w, 0.0, 1.0)
849         return (r,g,b), False
850
851     def draw(self,w, event):
852         if not self.window:
853             return
854         cr = self.window.cairo_create()
855         r,g,b = self.color
856         self.draw_gradient(cr, (0.,g,b),(1.,g,b), hsv=False)
857         x1 = r*self.w
858         self.draw_line_at(cr,x1)
859
860 class GSelector(VSelector):
861     def get_color_at(self,x,y):
862         r,g,b = self.color
863         g = clamp(x/self.w, 0.0, 1.0)
864         return (r,g,b), False
865
866     def draw(self,w, event):
867         if not self.window:
868             return
869         cr = self.window.cairo_create()
870         r,g,b = self.color
871         self.draw_gradient(cr, (r,0.,b),(r,1.,b), hsv=False)
872         x1 = g*self.w
873         self.draw_line_at(cr,x1)
874
875 class BSelector(VSelector):
876     def get_color_at(self,x,y):
877         r,g,b = self.color
878         b = clamp(x/self.w, 0.0, 1.0)
879         return (r,g,b), False
880
881     def draw(self,w, event):
882         if not self.window:
883             return
884         cr = self.window.cairo_create()
885         r,g,b = self.color
886         self.draw_gradient(cr, (r,g,0.),(r,g,1.), hsv=False)
887         x1 = b*self.w
888         self.draw_line_at(cr,x1)
889
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)
895     return btn
896
897 class HSVSelector(gtk.VBox):
898     def __init__(self, app, color=(1.,0.,0)):
899         gtk.VBox.__init__(self)
900         self.color = color
901         self.hsv = rgb_to_hsv(*color)
902         self.atomic = False
903
904         hbox = gtk.HBox()
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)
910
911         sbox = gtk.HBox()
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)
917
918         vbox = gtk.HBox()
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)
924
925         self.pack_start(hbox, expand=False)
926         self.pack_start(sbox, expand=False)
927         self.pack_start(vbox, expand=False)
928
929         self.set_tooltip_text(_("Change Hue, Saturation and Value"))
930
931     def user_selected_color(self, hsv):
932         self.set_color(hsv)
933         self.on_select(hsv)
934
935     def set_color(self, hsv):
936         self.atomic = True
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)
944         self.atomic = False
945         self.color = hsv_to_rgb(*hsv)
946
947     def on_select(self, color):
948         pass
949
950     def hue_change(self, spin):
951         if self.atomic:
952             return
953         h,s,v = self.hsv
954         self.set_color((spin.get_value()/359., s,v))
955
956     def sat_change(self, spin):
957         if self.atomic:
958             return
959         h,s,v = self.hsv
960         self.set_color((h, spin.get_value()/100., v))
961
962     def val_change(self, spin):
963         if self.atomic:
964             return
965         h,s,v = self.hsv
966         self.set_color((h,s, spin.get_value()/100.))
967
968 class RGBSelector(gtk.VBox):
969     def __init__(self, app, color=(1.,0.,0)):
970         gtk.VBox.__init__(self)
971         self.color = color
972         self.hsv = rgb_to_hsv(*color)
973         self.atomic = False
974
975         rbox = gtk.HBox()
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)
981
982         gbox = gtk.HBox()
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)
988
989         bbox = gtk.HBox()
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)
995
996         self.pack_start(rbox, expand=False)
997         self.pack_start(gbox, expand=False)
998         self.pack_start(bbox, expand=False)
999
1000         self.set_tooltip_text(_("Change Red, Green and Blue components"))
1001
1002     def user_selected_color(self, hsv):
1003         self.set_color(hsv)
1004         self.on_select(hsv)
1005
1006     def set_color(self, hsv):
1007         self.atomic = True
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)
1015         self.atomic = False
1016         self.hsv = hsv
1017
1018     def on_select(self, hsv):
1019         pass
1020
1021     def r_change(self, spin):
1022         if self.atomic:
1023             return
1024         r,g,b = self.color
1025         self.set_color(rgb_to_hsv(spin.get_value()/255., g,b))
1026
1027     def g_change(self, spin):
1028         if self.atomic:
1029             return
1030         r,g,b = self.color
1031         self.set_color(rgb_to_hsv(r, spin.get_value()/255., b))
1032
1033     def b_change(self, spin):
1034         if self.atomic:
1035             return
1036         r,g,b = self.color
1037         self.set_color(rgb_to_hsv(r,g, spin.get_value()/255.))
1038
1039 class Selector(gtk.VBox):
1040
1041     CIRCLE_MIN_SIZE = (150, 150)
1042
1043     def __init__(self, app):
1044         gtk.VBox.__init__(self)
1045         self.app = app
1046         self.recent = RecentColors(app)
1047         self.circle = CircleSelector(app)
1048         self.circle.set_size_request(*self.CIRCLE_MIN_SIZE)
1049
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
1052         # amiss somewhere.
1053         evbox = gtk.EventBox()
1054         evbox.add(self.circle)
1055         self.pack_start(evbox, expand=True)
1056
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
1061         nb = gtk.Notebook()
1062         nb.append_page(self.rgb_selector, gtk.Label(_('RGB')))
1063         nb.append_page(self.hsv_selector, gtk.Label(_('HSV')))
1064
1065         # Colour history
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)
1071
1072         # Colour details
1073         self.exp_details = expander = ElasticExpander(_('Details'))
1074         expander.set_spacing(6)
1075         expander.add(nb)
1076         expander.connect("notify::expanded", self.expander_expanded_cb, 'details')
1077         self.pack_start(expander, expand=False)
1078
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)
1088
1089         self.exp_config = expander = ElasticExpander(_('Harmonies'))
1090         vbox2 = gtk.VBox()
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."))
1097
1098         vbox3 = gtk.VBox()
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)
1109         expander.add(vbox2)
1110         expander.connect("notify::expanded", self.expander_expanded_cb, 'harmonies')
1111         self.pack_start(expander, expand=False)
1112
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]
1117
1118         self.value_blends = True
1119         self.opposite_blends = False
1120         self.negative_blends = False
1121
1122         self.expander_prefs_loaded = False
1123         self.connect("show", self.show_cb)
1124
1125     def toggle_blend(self, checkbox, name):
1126         attr = name+'_blends'
1127         setattr(self, attr, not getattr(self, attr))
1128
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()
1133         self.queue_draw()
1134
1135     def previous_color(self):
1136         return self.app.ch.last_color
1137
1138     def set_color(self, hsv, exclude=None):
1139         for w in self.widgets:
1140             if w is not exclude:
1141                 w.set_color(hsv)
1142         self.color = hsv_to_rgb(*hsv)
1143         self.hsv = hsv
1144         self.on_select(hsv)
1145
1146     def rgb_selected(self, hsv):
1147         self.set_color(hsv, exclude=self.rgb_selector)
1148
1149     def hsv_selected(self, hsv):
1150         self.set_color(hsv, exclude=self.hsv_selector)
1151
1152     def hue_selected(self, hsv):
1153         self.set_color(hsv, exclude=self.circle)
1154
1155     def recent_selected(self, hsv):
1156         self.set_color(hsv, exclude=self.recent)
1157
1158     def on_select(self, hsv):
1159         pass
1160
1161     def expander_expanded_cb(self, expander, junk, cfg_stem):
1162         # Save the expander state
1163         if not self.expander_prefs_loaded:
1164             return
1165         self.app.preferences['colorsampler.%s_expanded' % cfg_stem] \
1166           = bool(expander.get_expanded())
1167
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
1181
1182
1183 class ToolWidget (Selector):
1184
1185     stock_id = stock.TOOL_COLOR_SAMPLER
1186
1187     def __init__(self, app):
1188         Selector.__init__(self, app)
1189
1190         self.app.brush.observers.append(self.brush_modified_cb)
1191         self.stop_callback = False
1192
1193         # The first callback notification happens before the window is initialized
1194         self.set_color(app.brush.get_color_hsv())
1195
1196     def brush_modified_cb(self, settings):
1197         if not settings.intersection(('color_h', 'color_s', 'color_v')):
1198             return
1199         if self.stop_callback:
1200             return
1201         self.stop_callback = True
1202         hsv = self.app.brush.get_color_hsv()
1203         self.set_color(hsv)
1204         self.stop_callback = False
1205
1206     def on_select(self, hsv):
1207         if self.stop_callback:
1208             return
1209         self.app.brush.set_color_hsv(hsv)
1210
1211
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
1222
1223         self.add(self.selector)
1224         self.app.brush.observers.append(self.brush_modified_cb)
1225         self.stop_callback = False
1226
1227         # The first callback notification happens before the window is initialized
1228         self.selector.set_color(app.brush.get_color_hsv())
1229
1230     def brush_modified_cb(self, settings):
1231         if not settings.intersection(('color_h', 'color_s', 'color_v')):
1232             return
1233         if self.stop_callback:
1234             return
1235         self.stop_callback = True
1236         hsv = self.app.brush.get_color_hsv()
1237         self.selector.set_color(hsv)
1238         self.stop_callback = False
1239
1240     def on_select(self, hsv):
1241         if self.stop_callback:
1242             return
1243         self.app.brush.set_color_hsv(hsv)