1 | /* GTK - The GIMP Toolkit |
2 | * Copyright (C) 2012 Red Hat, Inc. |
3 | * |
4 | * This library is free software; you can redistribute it and/or |
5 | * modify it under the terms of the GNU Lesser General Public |
6 | * License as published by the Free Software Foundation; either |
7 | * version 2 of the License, or (at your option) any later version. |
8 | * |
9 | * This library is distributed in the hope that it will be useful, |
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
12 | * Lesser General Public License for more details. |
13 | * |
14 | * You should have received a copy of the GNU Lesser General Public |
15 | * License along with this library. If not, see <http://www.gnu.org/licenses/>. |
16 | */ |
17 | |
18 | #include "config.h" |
19 | |
20 | #include "gtkcolorplaneprivate.h" |
21 | |
22 | #include "gtkadjustment.h" |
23 | #include "gtkcolorutils.h" |
24 | #include "gtkgesturedrag.h" |
25 | #include "gtkgesturelongpress.h" |
26 | #include "gtkintl.h" |
27 | #include "gtksnapshot.h" |
28 | #include "gtkprivate.h" |
29 | #include "gtkeventcontrollerkey.h" |
30 | #include "gtkshortcutcontroller.h" |
31 | #include "gtkshortcuttrigger.h" |
32 | #include "gtkshortcutaction.h" |
33 | #include "gtkshortcut.h" |
34 | |
35 | struct _GtkColorPlane |
36 | { |
37 | GtkWidget parent_instance; |
38 | |
39 | GtkAdjustment *h_adj; |
40 | GtkAdjustment *s_adj; |
41 | GtkAdjustment *v_adj; |
42 | |
43 | GdkTexture *texture; |
44 | }; |
45 | |
46 | typedef struct |
47 | { |
48 | GtkWidgetClass parent_class; |
49 | } GtkColorPlaneClass; |
50 | |
51 | enum { |
52 | PROP_0, |
53 | PROP_H_ADJUSTMENT, |
54 | PROP_S_ADJUSTMENT, |
55 | PROP_V_ADJUSTMENT |
56 | }; |
57 | |
58 | G_DEFINE_TYPE (GtkColorPlane, gtk_color_plane, GTK_TYPE_WIDGET) |
59 | |
60 | static void |
61 | sv_to_xy (GtkColorPlane *plane, |
62 | int *x, |
63 | int *y) |
64 | { |
65 | double s, v; |
66 | int width, height; |
67 | |
68 | width = gtk_widget_get_width (GTK_WIDGET (plane)); |
69 | height = gtk_widget_get_height (GTK_WIDGET (plane)); |
70 | |
71 | s = gtk_adjustment_get_value (adjustment: plane->s_adj); |
72 | v = gtk_adjustment_get_value (adjustment: plane->v_adj); |
73 | |
74 | *x = CLAMP (width * v, 0, width - 1); |
75 | *y = CLAMP (height * (1 - s), 0, height - 1); |
76 | } |
77 | |
78 | static void |
79 | plane_snapshot (GtkWidget *widget, |
80 | GtkSnapshot *snapshot) |
81 | { |
82 | GtkColorPlane *plane = GTK_COLOR_PLANE (widget); |
83 | int x, y; |
84 | int width, height; |
85 | |
86 | sv_to_xy (plane, x: &x, y: &y); |
87 | width = gtk_widget_get_width (widget); |
88 | height = gtk_widget_get_height (widget); |
89 | |
90 | gtk_snapshot_append_texture (snapshot, |
91 | texture: plane->texture, |
92 | bounds: &GRAPHENE_RECT_INIT (0, 0, width, height)); |
93 | if (gtk_widget_has_visible_focus (widget)) |
94 | { |
95 | const GdkRGBA c1 = { 1.0, 1.0, 1.0, 0.6 }; |
96 | const GdkRGBA c2 = { 0.0, 0.0, 0.0, 0.8 }; |
97 | |
98 | /* Crosshair border */ |
99 | gtk_snapshot_append_color (snapshot, color: &c1, |
100 | bounds: &GRAPHENE_RECT_INIT (0, y - 1.5, width, 3)); |
101 | gtk_snapshot_append_color (snapshot, color: &c1, |
102 | bounds: &GRAPHENE_RECT_INIT (x - 1.5, 0, 3, height)); |
103 | |
104 | /* Actual crosshair */ |
105 | gtk_snapshot_append_color (snapshot, color: &c2, |
106 | bounds: &GRAPHENE_RECT_INIT (0, y - 0.5, width, 1)); |
107 | gtk_snapshot_append_color (snapshot, color: &c2, |
108 | bounds: &GRAPHENE_RECT_INIT (x - 0.5, 0, 1, height)); |
109 | } |
110 | else |
111 | { |
112 | const GdkRGBA c = { 0.8, 0.8, 0.8, 0.8 }; |
113 | |
114 | /* Horizontal */ |
115 | gtk_snapshot_append_color (snapshot, color: &c, |
116 | bounds: &GRAPHENE_RECT_INIT (0, y - 0.5, width, 1)); |
117 | /* Vertical */ |
118 | gtk_snapshot_append_color (snapshot, color: &c, |
119 | bounds: &GRAPHENE_RECT_INIT (x - 0.5, 0, 1, height)); |
120 | } |
121 | } |
122 | |
123 | static void |
124 | create_texture (GtkWidget *widget) |
125 | { |
126 | const int width = gtk_widget_get_width (widget); |
127 | const int height = gtk_widget_get_height (widget); |
128 | const int stride = width * 3; |
129 | GtkColorPlane *plane = GTK_COLOR_PLANE (widget); |
130 | GBytes *bytes; |
131 | guint8 *data; |
132 | |
133 | if (!gtk_widget_get_mapped (widget)) |
134 | return; |
135 | |
136 | if (width == 0 || height == 0) |
137 | return; |
138 | |
139 | g_clear_object (&plane->texture); |
140 | |
141 | data = g_malloc (n_bytes: height * stride); |
142 | if (width > 1 && height > 1) |
143 | { |
144 | const float h = gtk_adjustment_get_value (adjustment: plane->h_adj); |
145 | int x, y; |
146 | |
147 | for (y = 0; y < height; y++) |
148 | { |
149 | const float s = 1.0 - (float)y / (height - 1); |
150 | |
151 | for (x = 0; x < width; x++) |
152 | { |
153 | const float v = (float)x / (width - 1); |
154 | float r, g, b; |
155 | |
156 | gtk_hsv_to_rgb (h, s, v, r: &r, g: &g, b: &b); |
157 | |
158 | data[(y * stride) + (x * 3) + 0] = r * 255; |
159 | data[(y * stride) + (x * 3) + 1] = g * 255; |
160 | data[(y * stride) + (x * 3) + 2] = b * 255; |
161 | } |
162 | } |
163 | } |
164 | else |
165 | { |
166 | memset (s: data, c: 0, n: height * stride); |
167 | } |
168 | |
169 | bytes = g_bytes_new_take (data, size: height * stride); |
170 | plane->texture = gdk_memory_texture_new (width, height, |
171 | format: GDK_MEMORY_R8G8B8, |
172 | bytes, |
173 | stride); |
174 | g_bytes_unref (bytes); |
175 | } |
176 | |
177 | static void |
178 | plane_size_allocate (GtkWidget *widget, |
179 | int width, |
180 | int height, |
181 | int baseline) |
182 | { |
183 | create_texture (widget); |
184 | } |
185 | |
186 | static void |
187 | set_cross_cursor (GtkWidget *widget, |
188 | gboolean enabled) |
189 | { |
190 | if (enabled) |
191 | gtk_widget_set_cursor_from_name (widget, name: "crosshair" ); |
192 | else |
193 | gtk_widget_set_cursor (widget, NULL); |
194 | } |
195 | |
196 | static void |
197 | h_changed (GtkWidget *plane) |
198 | { |
199 | create_texture (widget: plane); |
200 | gtk_widget_queue_draw (widget: plane); |
201 | } |
202 | |
203 | static void |
204 | sv_changed (GtkColorPlane *plane) |
205 | { |
206 | gtk_widget_queue_draw (GTK_WIDGET (plane)); |
207 | } |
208 | |
209 | static void |
210 | update_color (GtkColorPlane *plane, |
211 | int x, |
212 | int y) |
213 | { |
214 | GtkWidget *widget = GTK_WIDGET (plane); |
215 | double s, v; |
216 | |
217 | s = CLAMP (1 - y * (1.0 / gtk_widget_get_height (widget)), 0, 1); |
218 | v = CLAMP (x * (1.0 / gtk_widget_get_width (widget)), 0, 1); |
219 | gtk_adjustment_set_value (adjustment: plane->s_adj, value: s); |
220 | gtk_adjustment_set_value (adjustment: plane->v_adj, value: v); |
221 | |
222 | gtk_widget_queue_draw (widget); |
223 | } |
224 | |
225 | static void |
226 | hold_action (GtkGestureLongPress *gesture, |
227 | double x, |
228 | double y, |
229 | GtkWidget *plane) |
230 | { |
231 | gtk_widget_activate_action (widget: plane, |
232 | name: "color.edit" , |
233 | format_string: "s" , gtk_widget_get_name (widget: plane)); |
234 | } |
235 | |
236 | static void |
237 | sv_move (GtkColorPlane *plane, |
238 | double ds, |
239 | double dv) |
240 | { |
241 | double s, v; |
242 | |
243 | s = gtk_adjustment_get_value (adjustment: plane->s_adj); |
244 | v = gtk_adjustment_get_value (adjustment: plane->v_adj); |
245 | |
246 | if (s + ds > 1) |
247 | { |
248 | if (s < 1) |
249 | s = 1; |
250 | else |
251 | goto error; |
252 | } |
253 | else if (s + ds < 0) |
254 | { |
255 | if (s > 0) |
256 | s = 0; |
257 | else |
258 | goto error; |
259 | } |
260 | else |
261 | { |
262 | s += ds; |
263 | } |
264 | |
265 | if (v + dv > 1) |
266 | { |
267 | if (v < 1) |
268 | v = 1; |
269 | else |
270 | goto error; |
271 | } |
272 | else if (v + dv < 0) |
273 | { |
274 | if (v > 0) |
275 | v = 0; |
276 | else |
277 | goto error; |
278 | } |
279 | else |
280 | { |
281 | v += dv; |
282 | } |
283 | |
284 | gtk_adjustment_set_value (adjustment: plane->s_adj, value: s); |
285 | gtk_adjustment_set_value (adjustment: plane->v_adj, value: v); |
286 | return; |
287 | |
288 | error: |
289 | gtk_widget_error_bell (GTK_WIDGET (plane)); |
290 | } |
291 | |
292 | static gboolean |
293 | key_controller_key_pressed (GtkEventControllerKey *controller, |
294 | guint keyval, |
295 | guint keycode, |
296 | GdkModifierType state, |
297 | GtkWidget *widget) |
298 | { |
299 | GtkColorPlane *plane = GTK_COLOR_PLANE (widget); |
300 | double step; |
301 | |
302 | if ((state & GDK_ALT_MASK) != 0) |
303 | step = 0.1; |
304 | else |
305 | step = 0.01; |
306 | |
307 | if (keyval == GDK_KEY_Up || |
308 | keyval == GDK_KEY_KP_Up) |
309 | sv_move (plane, ds: step, dv: 0); |
310 | else if (keyval == GDK_KEY_Down || |
311 | keyval == GDK_KEY_KP_Down) |
312 | sv_move (plane, ds: -step, dv: 0); |
313 | else if (keyval == GDK_KEY_Left || |
314 | keyval == GDK_KEY_KP_Left) |
315 | sv_move (plane, ds: 0, dv: -step); |
316 | else if (keyval == GDK_KEY_Right || |
317 | keyval == GDK_KEY_KP_Right) |
318 | sv_move (plane, ds: 0, dv: step); |
319 | else |
320 | return FALSE; |
321 | |
322 | return TRUE; |
323 | } |
324 | |
325 | static void |
326 | plane_drag_gesture_begin (GtkGestureDrag *gesture, |
327 | double start_x, |
328 | double start_y, |
329 | GtkWidget *plane) |
330 | { |
331 | guint button; |
332 | |
333 | button = gtk_gesture_single_get_current_button (GTK_GESTURE_SINGLE (gesture)); |
334 | |
335 | if (button == GDK_BUTTON_SECONDARY) |
336 | { |
337 | gtk_widget_activate_action (widget: plane, |
338 | name: "color.edit" , |
339 | format_string: "s" , gtk_widget_get_name (widget: plane)); |
340 | } |
341 | |
342 | if (button != GDK_BUTTON_PRIMARY) |
343 | { |
344 | gtk_gesture_set_state (GTK_GESTURE (gesture), state: GTK_EVENT_SEQUENCE_DENIED); |
345 | return; |
346 | } |
347 | |
348 | set_cross_cursor (widget: plane, TRUE); |
349 | update_color (GTK_COLOR_PLANE (plane), x: start_x, y: start_y); |
350 | gtk_widget_grab_focus (widget: plane); |
351 | gtk_gesture_set_state (GTK_GESTURE (gesture), state: GTK_EVENT_SEQUENCE_CLAIMED); |
352 | } |
353 | |
354 | static void |
355 | plane_drag_gesture_update (GtkGestureDrag *gesture, |
356 | double offset_x, |
357 | double offset_y, |
358 | GtkColorPlane *plane) |
359 | { |
360 | double start_x, start_y; |
361 | |
362 | gtk_gesture_drag_get_start_point (GTK_GESTURE_DRAG (gesture), |
363 | x: &start_x, y: &start_y); |
364 | update_color (plane, x: start_x + offset_x, y: start_y + offset_y); |
365 | } |
366 | |
367 | static void |
368 | plane_drag_gesture_end (GtkGestureDrag *gesture, |
369 | double offset_x, |
370 | double offset_y, |
371 | GtkColorPlane *plane) |
372 | { |
373 | set_cross_cursor (GTK_WIDGET (plane), FALSE); |
374 | } |
375 | |
376 | static void |
377 | gtk_color_plane_init (GtkColorPlane *plane) |
378 | { |
379 | GtkEventController *controller; |
380 | GtkGesture *gesture; |
381 | GtkShortcutTrigger *trigger; |
382 | GtkShortcutAction *action; |
383 | GtkShortcut *shortcut; |
384 | |
385 | gtk_widget_set_focusable (GTK_WIDGET (plane), TRUE); |
386 | |
387 | gesture = gtk_gesture_drag_new (); |
388 | g_signal_connect (gesture, "drag-begin" , |
389 | G_CALLBACK (plane_drag_gesture_begin), plane); |
390 | g_signal_connect (gesture, "drag-update" , |
391 | G_CALLBACK (plane_drag_gesture_update), plane); |
392 | g_signal_connect (gesture, "drag-end" , |
393 | G_CALLBACK (plane_drag_gesture_end), plane); |
394 | gtk_gesture_single_set_button (GTK_GESTURE_SINGLE (gesture), button: 0); |
395 | gtk_widget_add_controller (GTK_WIDGET (plane), GTK_EVENT_CONTROLLER (gesture)); |
396 | |
397 | gesture = gtk_gesture_long_press_new (); |
398 | g_signal_connect (gesture, "pressed" , |
399 | G_CALLBACK (hold_action), plane); |
400 | gtk_gesture_single_set_touch_only (GTK_GESTURE_SINGLE (gesture), |
401 | TRUE); |
402 | gtk_widget_add_controller (GTK_WIDGET (plane), GTK_EVENT_CONTROLLER (gesture)); |
403 | |
404 | controller = gtk_event_controller_key_new (); |
405 | g_signal_connect (controller, "key-pressed" , |
406 | G_CALLBACK (key_controller_key_pressed), plane); |
407 | gtk_widget_add_controller (GTK_WIDGET (plane), controller); |
408 | |
409 | controller = gtk_shortcut_controller_new (); |
410 | trigger = gtk_alternative_trigger_new (first: gtk_keyval_trigger_new (GDK_KEY_F10, modifiers: GDK_SHIFT_MASK), |
411 | second: gtk_keyval_trigger_new (GDK_KEY_Menu, modifiers: 0)); |
412 | action = gtk_named_action_new (name: "color.edit" ); |
413 | shortcut = gtk_shortcut_new_with_arguments (trigger, action, format_string: "s" , "sv" ); |
414 | gtk_shortcut_controller_add_shortcut (GTK_SHORTCUT_CONTROLLER (controller), shortcut); |
415 | gtk_widget_add_controller (GTK_WIDGET (plane), controller); |
416 | } |
417 | |
418 | static void |
419 | plane_finalize (GObject *object) |
420 | { |
421 | GtkColorPlane *plane = GTK_COLOR_PLANE (object); |
422 | |
423 | g_clear_object (&plane->texture); |
424 | |
425 | g_clear_object (&plane->h_adj); |
426 | g_clear_object (&plane->s_adj); |
427 | g_clear_object (&plane->v_adj); |
428 | |
429 | G_OBJECT_CLASS (gtk_color_plane_parent_class)->finalize (object); |
430 | } |
431 | |
432 | static void |
433 | plane_set_property (GObject *object, |
434 | guint prop_id, |
435 | const GValue *value, |
436 | GParamSpec *pspec) |
437 | { |
438 | GtkColorPlane *plane = GTK_COLOR_PLANE (object); |
439 | GtkAdjustment *adjustment; |
440 | |
441 | /* Construct only properties can only be set once, these are created |
442 | * only in order to be properly buildable from gtkcoloreditor.ui |
443 | */ |
444 | switch (prop_id) |
445 | { |
446 | case PROP_H_ADJUSTMENT: |
447 | adjustment = g_value_get_object (value); |
448 | if (adjustment) |
449 | { |
450 | plane->h_adj = g_object_ref_sink (adjustment); |
451 | g_signal_connect_swapped (adjustment, "value-changed" , G_CALLBACK (h_changed), plane); |
452 | } |
453 | break; |
454 | case PROP_S_ADJUSTMENT: |
455 | adjustment = g_value_get_object (value); |
456 | if (adjustment) |
457 | { |
458 | plane->s_adj = g_object_ref_sink (adjustment); |
459 | g_signal_connect_swapped (adjustment, "value-changed" , G_CALLBACK (sv_changed), plane); |
460 | } |
461 | break; |
462 | case PROP_V_ADJUSTMENT: |
463 | adjustment = g_value_get_object (value); |
464 | if (adjustment) |
465 | { |
466 | plane->v_adj = g_object_ref_sink (adjustment); |
467 | g_signal_connect_swapped (adjustment, "value-changed" , G_CALLBACK (sv_changed), plane); |
468 | } |
469 | break; |
470 | default: |
471 | G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); |
472 | break; |
473 | } |
474 | } |
475 | |
476 | static void |
477 | gtk_color_plane_class_init (GtkColorPlaneClass *class) |
478 | { |
479 | GObjectClass *object_class = G_OBJECT_CLASS (class); |
480 | GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (class); |
481 | |
482 | object_class->finalize = plane_finalize; |
483 | object_class->set_property = plane_set_property; |
484 | |
485 | widget_class->snapshot = plane_snapshot; |
486 | widget_class->size_allocate = plane_size_allocate; |
487 | |
488 | g_object_class_install_property (oclass: object_class, |
489 | property_id: PROP_H_ADJUSTMENT, |
490 | pspec: g_param_spec_object (name: "h-adjustment" , |
491 | nick: "Hue Adjustment" , |
492 | blurb: "Hue Adjustment" , |
493 | GTK_TYPE_ADJUSTMENT, |
494 | GTK_PARAM_WRITABLE | |
495 | G_PARAM_CONSTRUCT_ONLY)); |
496 | |
497 | g_object_class_install_property (oclass: object_class, |
498 | property_id: PROP_S_ADJUSTMENT, |
499 | pspec: g_param_spec_object (name: "s-adjustment" , |
500 | nick: "Saturation Adjustment" , |
501 | blurb: "Saturation Adjustment" , |
502 | GTK_TYPE_ADJUSTMENT, |
503 | GTK_PARAM_WRITABLE | |
504 | G_PARAM_CONSTRUCT_ONLY)); |
505 | |
506 | g_object_class_install_property (oclass: object_class, |
507 | property_id: PROP_V_ADJUSTMENT, |
508 | pspec: g_param_spec_object (name: "v-adjustment" , |
509 | nick: "Value Adjustment" , |
510 | blurb: "Value Adjustment" , |
511 | GTK_TYPE_ADJUSTMENT, |
512 | GTK_PARAM_WRITABLE | |
513 | G_PARAM_CONSTRUCT_ONLY)); |
514 | |
515 | gtk_widget_class_set_css_name (widget_class, name: "plane" ); |
516 | } |
517 | |
518 | GtkWidget * |
519 | gtk_color_plane_new (GtkAdjustment *h_adj, |
520 | GtkAdjustment *s_adj, |
521 | GtkAdjustment *v_adj) |
522 | { |
523 | return g_object_new (GTK_TYPE_COLOR_PLANE, |
524 | first_property_name: "h-adjustment" , h_adj, |
525 | "s-adjustment" , s_adj, |
526 | "v-adjustment" , v_adj, |
527 | NULL); |
528 | } |
529 | |