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