1# This file is part of MyPaint.
2# Copyright (C) 2010-2018 by the MyPaint Development Team
3# Copyright (C) 2009-2013 by Martin Renold <martinxyz@gmx.ch>
4#
5# This program is free software; you can redistribute it and/or modify
6# it under the terms of the GNU General Public License as published by
7# the Free Software Foundation; either version 2 of the License, or
8# (at your option) any later version.
9
10## Imports
11
12from __future__ import division, print_function
13from gettext import gettext as _
14import colorsys
15
16import gui.mode
17from .overlays import Overlay
18from .overlays import rounded_box
19import lib.color
20
21
22## Color picking mode, with a preview rectangle overlay
23
24class ColorPickMode (gui.mode.OneshotDragMode):
25    """Mode for picking colors from the screen, with a preview
26
27    This can be invoked in quite a number of ways:
28
29    * The keyboard hotkey ("R" by default)
30    * Modifier and pointer button: (Ctrl+Button1 by default)
31    * From the toolbar or menu
32
33    The first two methods pick immediately. Moving the mouse with the
34    initial keys or buttons held down keeps picking with a little
35    preview square appearing.
36
37    The third method doesn't pick immediately: you have to click on the
38    canvas to start picking.
39
40    While the preview square is visible, it's possible to pick outside
41    the window. This "hidden" functionality may not work at all with
42    more modern window managers and DEs, and may be removed if it proves
43    slow or faulty.
44
45    """
46    # Class configuration
47    ACTION_NAME = 'ColorPickMode'
48    PICK_SIZE = 6
49
50    # Keyboard activation behaviour (instance defaults)
51    # See keyboard.py and doc.mode_flip_action_activated_cb()
52    keyup_timeout = 0   # don't change behaviour by timeout
53
54    pointer_behavior = gui.mode.Behavior.EDIT_OBJECTS
55    scroll_behavior = gui.mode.Behavior.NONE
56    # XXX ^^^^^^^ grabs ptr, so no CHANGE_VIEW
57    supports_button_switching = False
58
59    @property
60    def inactive_cursor(self):
61        return self.doc.app.cursor_color_picker
62
63    @classmethod
64    def get_name(cls):
65        return _(u"Pick Color")
66
67    def get_usage(self):
68        return _(u"Set the color used for painting")
69
70    def __init__(self, ignore_modifiers=False, pickmode="PickAll", **kwds):
71        super(ColorPickMode, self).__init__(**kwds)
72        self._overlay = None
73        self._started_from_key_press = ignore_modifiers
74        self._start_drag_on_next_motion_event = False
75        self._pickmode = pickmode
76
77    def enter(self, doc, **kwds):
78        """Enters the mode, arranging for necessary grabs ASAP"""
79        super(ColorPickMode, self).enter(doc, **kwds)
80        if self._started_from_key_press:
81            # Pick now using the last recorded event position
82            doc = self.doc
83            tdw = self.doc.tdw
84            t, x, y = doc.get_last_event_info(tdw)
85            if None not in (x, y):
86                self._pick_color_mode(tdw, x, y, self._pickmode)
87            # Start the drag when possible
88            self._start_drag_on_next_motion_event = True
89            self._needs_drag_start = True
90
91    def leave(self, **kwds):
92        self._remove_overlay()
93        super(ColorPickMode, self).leave(**kwds)
94
95    def button_press_cb(self, tdw, event):
96        self._pick_color_mode(tdw, event.x, event.y, self._pickmode)
97        # Supercall will start the drag normally
98        self._start_drag_on_next_motion_event = False
99        return super(ColorPickMode, self).button_press_cb(tdw, event)
100
101    def motion_notify_cb(self, tdw, event):
102        if self._start_drag_on_next_motion_event:
103            self._start_drag(tdw, event)
104            self._start_drag_on_next_motion_event = False
105        return super(ColorPickMode, self).motion_notify_cb(tdw, event)
106
107    def drag_stop_cb(self, tdw):
108        self._remove_overlay()
109        super(ColorPickMode, self).drag_stop_cb(tdw)
110
111    def drag_update_cb(self, tdw, event, dx, dy):
112        self._pick_color_mode(tdw, event.x, event.y, self._pickmode)
113        self._place_overlay(tdw, event.x, event.y)
114        return super(ColorPickMode, self).drag_update_cb(tdw, event, dx, dy)
115
116    def _place_overlay(self, tdw, x, y):
117        if self._overlay is None:
118            self._overlay = ColorPickPreviewOverlay(self.doc, tdw, x, y)
119        else:
120            self._overlay.move(x, y)
121
122    def _remove_overlay(self):
123        if self._overlay is None:
124            return
125        self._overlay.cleanup()
126        self._overlay = None
127
128    def get_options_widget(self):
129        return None
130
131    def _pick_color_mode(self, tdw, x, y, mode):
132        # Init shared variables between normal and HCY modes.
133        pickcolor = tdw.pick_color(x, y)
134        brushcolor = self._get_app_brush_color()
135        brushcolor_rgb = brushcolor.get_rgb()
136        pickcolor_rgb = pickcolor.get_rgb()
137
138        # If brush and pick colors are the same, nothing to do.
139        if brushcolor_rgb != pickcolor_rgb:
140            pickcolor_hsv = pickcolor.get_hsv()
141            brushcolor_hsv = brushcolor.get_hsv()
142            cm = self.doc.app.brush_color_manager
143            c_min = 0.0001
144            y_min = 0.0001
145            y_max = 0.9999
146
147            # Normal pick mode, but preserve hue for achromatic colors.
148            # Easy because we are staying in HSV
149            # and not converting to another color space.
150
151            if mode == "PickAll":
152                if (pickcolor_hsv[1] == 0) or (pickcolor_hsv[2] == 0):
153                    brushcolornew_hsv = lib.color.HSVColor(
154                        brushcolor_hsv[0],
155                        pickcolor_hsv[1],
156                        pickcolor_hsv[2],
157                    )
158                    pickcolor = brushcolornew_hsv
159                cm.set_color(pickcolor)
160            else:
161                # Pick H, C, or Y independently.
162                # Deal with scenarios to avoid achromatic conversions
163                # from HCY to RGB to HSV losing hue information.
164                # Basically prevent C from reaching 0
165                # and Y from reaching 0.0 or 1.0.
166                brushcolor_hcy = lib.color.RGB_to_HCY(brushcolor_rgb)
167                pickcolor_hcy = lib.color.RGB_to_HCY(pickcolor_rgb)
168
169                if mode == "PickHue":
170                    brushcolornew_hcy = (
171                        pickcolor_hcy[0],
172                        brushcolor_hcy[1],
173                        brushcolor_hcy[2],
174                    )
175                    if brushcolor_hcy[1] < c_min:
176                        brushcolornew_hcy = (
177                            brushcolornew_hcy[0],
178                            c_min,
179                            brushcolornew_hcy[2],
180                        )
181                    if pickcolor_hcy[2] < y_min:
182                        brushcolornew_hcy = (
183                            brushcolornew_hcy[0],
184                            brushcolornew_hcy[1],
185                            y_min,
186                        )
187                    if pickcolor_hcy[2] > y_max:
188                        brushcolornew_hcy = (
189                            brushcolornew_hcy[0],
190                            brushcolornew_hcy[1],
191                            y_max,
192                        )
193                    if (pickcolor_hsv[1] == 0) or (pickcolor_hsv[2] == 0):
194                        brushcolornew_hcy = (
195                            brushcolor_hsv[0],
196                            brushcolor_hcy[1],
197                            brushcolor_hcy[2],
198                        )
199                elif mode == "PickLuma":
200                    brushcolornew_hcy = (
201                        brushcolor_hsv[0],
202                        brushcolor_hcy[1],
203                        pickcolor_hcy[2],
204                    )
205                    if brushcolor_hcy[1] < c_min:
206                        brushcolornew_hcy = (
207                            brushcolornew_hcy[0],
208                            c_min,
209                            pickcolor_hcy[2],
210                        )
211                    if pickcolor_hcy[2] < y_min:
212                        brushcolornew_hcy = (
213                            brushcolornew_hcy[0],
214                            brushcolornew_hcy[1],
215                            y_min,
216                        )
217                    if pickcolor_hcy[2] > y_max:
218                        brushcolornew_hcy = (
219                            brushcolornew_hcy[0],
220                            brushcolornew_hcy[1],
221                            y_max,
222                        )
223                elif mode == "PickChroma":
224                    brushcolornew_hcy = (
225                        brushcolor_hsv[0],
226                        pickcolor_hcy[1],
227                        brushcolor_hcy[2],
228                    )
229                    if pickcolor_hcy[1] < c_min:
230                        brushcolornew_hcy = (
231                            brushcolornew_hcy[0],
232                            c_min,
233                            brushcolornew_hcy[2],
234                        )
235                    if brushcolor_hcy[2] < y_min:
236                        brushcolornew_hcy = (
237                            brushcolornew_hcy[0],
238                            brushcolornew_hcy[1],
239                            y_min,
240                        )
241                    if brushcolor_hcy[2] > y_max:
242                        brushcolornew_hcy = (
243                            brushcolornew_hcy[0],
244                            brushcolornew_hcy[1],
245                            y_max,
246                        )
247
248                brushcolornew = lib.color.HCY_to_RGB(brushcolornew_hcy)
249                brushcolornew = colorsys.rgb_to_hsv(*brushcolornew)
250                brushcolornew = lib.color.HSVColor(*brushcolornew)
251                cm.set_color(brushcolornew)
252        return None
253
254    def _get_app_brush_color(self):
255        app = self.doc.app
256        return lib.color.HSVColor(*app.brush.get_color_hsv())
257
258
259class ColorPickModeH (ColorPickMode):
260
261    # Class configuration
262    ACTION_NAME = 'ColorPickModeH'
263
264    @property
265    def inactive_cursor(self):
266        return self.doc.app.cursor_color_picker_h
267
268    @classmethod
269    def get_name(cls):
270        return _(u"Pick Hue")
271
272    def get_usage(self):
273        return _(u"Set the color Hue used for painting")
274
275    def __init__(self, ignore_modifiers=False, pickmode="PickHue", **kwds):
276        super(ColorPickModeH, self).__init__(**kwds)
277        self._overlay = None
278        self._started_from_key_press = ignore_modifiers
279        self._start_drag_on_next_motion_event = False
280        self._pickmode = pickmode
281
282
283class ColorPickModeC (ColorPickMode):
284    # Class configuration
285    ACTION_NAME = 'ColorPickModeC'
286
287    @property
288    def inactive_cursor(self):
289        return self.doc.app.cursor_color_picker_c
290
291    @classmethod
292    def get_name(cls):
293        return _(u"Pick Chroma")
294
295    def get_usage(self):
296        return _(u"Set the color Chroma used for painting")
297
298    def __init__(self, ignore_modifiers=False, pickmode="PickChroma", **kwds):
299        super(ColorPickModeC, self).__init__(**kwds)
300        self._overlay = None
301        self._started_from_key_press = ignore_modifiers
302        self._start_drag_on_next_motion_event = False
303        self._pickmode = pickmode
304
305
306class ColorPickModeY (ColorPickMode):
307    # Class configuration
308    ACTION_NAME = 'ColorPickModeY'
309
310    @property
311    def inactive_cursor(self):
312        return self.doc.app.cursor_color_picker_y
313
314    @classmethod
315    def get_name(cls):
316        return _(u"Pick Luma")
317
318    def get_usage(self):
319        return _(u"Set the color Luma used for painting")
320
321    def __init__(self, ignore_modifiers=False, pickmode="PickLuma", **kwds):
322        super(ColorPickModeY, self).__init__(**kwds)
323        self._overlay = None
324        self._started_from_key_press = ignore_modifiers
325        self._start_drag_on_next_motion_event = False
326        self._pickmode = pickmode
327
328
329class ColorPickPreviewOverlay (Overlay):
330    """Preview overlay during color picker mode.
331
332    This is only shown when dragging the pointer with a button or the
333    hotkey held down, to avoid flashing and distraction.
334
335    """
336
337    PREVIEW_SIZE = 70
338    OUTLINE_WIDTH = 3
339    CORNER_RADIUS = 10
340
341    def __init__(self, doc, tdw, x, y):
342        """Initialize, attaching to the brush and to the tdw.
343
344        Observer callbacks and canvas overlays are registered by this
345        constructor, so cleanup() must be called when the owning mode leave()s.
346
347        """
348        Overlay.__init__(self)
349        self._doc = doc
350        self._tdw = tdw
351        self._x = int(x)+0.5
352        self._y = int(y)+0.5
353        alloc = tdw.get_allocation()
354        self._tdw_w = alloc.width
355        self._tdw_h = alloc.height
356        self._color = self._get_app_brush_color()
357        app = doc.app
358        app.brush.observers.append(self._brush_color_changed_cb)
359        tdw.display_overlays.append(self)
360        self._previous_area = None
361        self._queue_tdw_redraw()
362
363    def cleanup(self):
364        """Cleans up temporary observer stuff, allowing garbage collection.
365        """
366        app = self._doc.app
367        app.brush.observers.remove(self._brush_color_changed_cb)
368        self._tdw.display_overlays.remove(self)
369        assert self._brush_color_changed_cb not in app.brush.observers
370        assert self not in self._tdw.display_overlays
371        self._queue_tdw_redraw()
372
373    def move(self, x, y):
374        """Moves the preview square to a new location, in tdw pointer coords.
375        """
376        self._x = int(x)+0.5
377        self._y = int(y)+0.5
378        self._queue_tdw_redraw()
379
380    def _get_app_brush_color(self):
381        app = self._doc.app
382        return lib.color.HSVColor(*app.brush.get_color_hsv())
383
384    def _brush_color_changed_cb(self, settings):
385        if not settings.intersection(('color_h', 'color_s', 'color_v')):
386            return
387        self._color = self._get_app_brush_color()
388        self._queue_tdw_redraw()
389
390    def _queue_tdw_redraw(self):
391        if self._previous_area is not None:
392            self._tdw.queue_draw_area(*self._previous_area)
393            self._previous_area = None
394        area = self._get_area()
395        if area is not None:
396            self._tdw.queue_draw_area(*area)
397
398    def _get_area(self):
399        # Returns the drawing area for the square
400        size = self.PREVIEW_SIZE
401
402        # Start with the pointer location
403        x = self._x
404        y = self._y
405
406        offset = size // 2
407
408        # Only show if the pointer is inside the tdw
409        alloc = self._tdw.get_allocation()
410        if x < 0 or y < 0 or y > alloc.height or x > alloc.width:
411            return None
412
413        # Convert to preview location
414        # Pick a direction - N,W,E,S - in which to offset the preview
415        if y + size > alloc.height - offset:
416            x -= offset
417            y -= size + offset
418        elif x < offset:
419            x += offset
420            y -= offset
421        elif x > alloc.width - offset:
422            x -= size + offset
423            y -= offset
424        else:
425            x -= offset
426            y += offset
427
428        ## Correct to place within the tdw
429        #   if x < 0:
430        #       x = 0
431        #   if y < 0:
432        #       y = 0
433        #   if x + size > alloc.width:
434        #       x = alloc.width - size
435        #   if y + size > alloc.height:
436        #       y = alloc.height - size
437
438        return (int(x), int(y), size, size)
439
440    def paint(self, cr):
441        area = self._get_area()
442        if area is not None:
443            x, y, w, h = area
444
445            cr.set_source_rgb(*self._color.get_rgb())
446            x += (self.OUTLINE_WIDTH // 2) + 1.5
447            y += (self.OUTLINE_WIDTH // 2) + 1.5
448            w -= self.OUTLINE_WIDTH + 3
449            h -= self.OUTLINE_WIDTH + 3
450            rounded_box(cr, x, y, w, h, self.CORNER_RADIUS)
451            cr.fill_preserve()
452
453            cr.set_source_rgb(0, 0, 0)
454            cr.set_line_width(self.OUTLINE_WIDTH)
455            cr.stroke()
456
457        self._previous_area = area
458