1 /* GIMP - The GNU Image Manipulation Program
2 * Copyright (C) 1995 Spencer Kimball and Peter Mattis
3 *
4 * gimpdial.c
5 * Copyright (C) 2014 Michael Natterer <mitch@gimp.org>
6 *
7 * Based on code from the color-rotate plug-in
8 * Copyright (C) 1997-1999 Sven Anders (anderss@fmi.uni-passau.de)
9 * Based on code from Pavel Grinfeld (pavel@ml.com)
10 *
11 * This program is free software: you can redistribute it and/or modify
12 * it under the terms of the GNU General Public License as published by
13 * the Free Software Foundation; either version 3 of the License, or
14 * (at your option) any later version.
15 *
16 * This program is distributed in the hope that it will be useful,
17 * but WITHOUT ANY WARRANTY; without even the implied warranty of
18 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 * GNU General Public License for more details.
20 *
21 * You should have received a copy of the GNU General Public License
22 * along with this program. If not, see <https://www.gnu.org/licenses/>.
23 */
24
25 #include "config.h"
26
27 #include <gegl.h>
28 #include <gtk/gtk.h>
29
30 #include "libgimpmath/gimpmath.h"
31 #include "libgimpcolor/gimpcolor.h"
32 #include "libgimpwidgets/gimpwidgets.h"
33
34 #include "widgets-types.h"
35
36 #include "core/gimp-cairo.h"
37
38 #include "gimpdial.h"
39
40
41 #define SEGMENT_FRACTION 0.3
42
43
44 enum
45 {
46 PROP_0,
47 PROP_DRAW_BETA,
48 PROP_ALPHA,
49 PROP_BETA,
50 PROP_CLOCKWISE_ANGLES,
51 PROP_CLOCKWISE_DELTA
52 };
53
54 typedef enum
55 {
56 DIAL_TARGET_NONE = 0,
57 DIAL_TARGET_ALPHA = 1 << 0,
58 DIAL_TARGET_BETA = 1 << 1,
59 DIAL_TARGET_BOTH = DIAL_TARGET_ALPHA | DIAL_TARGET_BETA
60 } DialTarget;
61
62
63 struct _GimpDialPrivate
64 {
65 gdouble alpha;
66 gdouble beta;
67 gboolean clockwise_angles;
68 gboolean clockwise_delta;
69 gboolean draw_beta;
70
71 DialTarget target;
72 gdouble last_angle;
73 };
74
75
76 static void gimp_dial_set_property (GObject *object,
77 guint property_id,
78 const GValue *value,
79 GParamSpec *pspec);
80 static void gimp_dial_get_property (GObject *object,
81 guint property_id,
82 GValue *value,
83 GParamSpec *pspec);
84
85 static gboolean gimp_dial_expose_event (GtkWidget *widget,
86 GdkEventExpose *event);
87 static gboolean gimp_dial_button_press_event (GtkWidget *widget,
88 GdkEventButton *bevent);
89 static gboolean gimp_dial_motion_notify_event (GtkWidget *widget,
90 GdkEventMotion *mevent);
91
92 static void gimp_dial_reset_target (GimpCircle *circle);
93
94 static void gimp_dial_set_target (GimpDial *dial,
95 DialTarget target);
96
97 static void gimp_dial_draw_arrows (cairo_t *cr,
98 gint size,
99 gdouble alpha,
100 gdouble beta,
101 gboolean clockwise_delta,
102 DialTarget highlight,
103 gboolean draw_beta);
104
105 static gdouble gimp_dial_normalize_angle (gdouble angle);
106 static gdouble gimp_dial_get_angle_distance (gdouble alpha,
107 gdouble beta);
108
109
G_DEFINE_TYPE_WITH_PRIVATE(GimpDial,gimp_dial,GIMP_TYPE_CIRCLE)110 G_DEFINE_TYPE_WITH_PRIVATE (GimpDial, gimp_dial, GIMP_TYPE_CIRCLE)
111
112 #define parent_class gimp_dial_parent_class
113
114
115 static void
116 gimp_dial_class_init (GimpDialClass *klass)
117 {
118 GObjectClass *object_class = G_OBJECT_CLASS (klass);
119 GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
120 GimpCircleClass *circle_class = GIMP_CIRCLE_CLASS (klass);
121
122 object_class->get_property = gimp_dial_get_property;
123 object_class->set_property = gimp_dial_set_property;
124
125 widget_class->expose_event = gimp_dial_expose_event;
126 widget_class->button_press_event = gimp_dial_button_press_event;
127 widget_class->motion_notify_event = gimp_dial_motion_notify_event;
128
129 circle_class->reset_target = gimp_dial_reset_target;
130
131 g_object_class_install_property (object_class, PROP_ALPHA,
132 g_param_spec_double ("alpha",
133 NULL, NULL,
134 0.0, 2 * G_PI, 0.0,
135 GIMP_PARAM_READWRITE |
136 G_PARAM_CONSTRUCT));
137
138 g_object_class_install_property (object_class, PROP_BETA,
139 g_param_spec_double ("beta",
140 NULL, NULL,
141 0.0, 2 * G_PI, G_PI,
142 GIMP_PARAM_READWRITE |
143 G_PARAM_CONSTRUCT));
144
145 g_object_class_install_property (object_class, PROP_CLOCKWISE_ANGLES,
146 g_param_spec_boolean ("clockwise-angles",
147 NULL, NULL,
148 FALSE,
149 GIMP_PARAM_READWRITE |
150 G_PARAM_CONSTRUCT));
151
152 g_object_class_install_property (object_class, PROP_CLOCKWISE_DELTA,
153 g_param_spec_boolean ("clockwise-delta",
154 NULL, NULL,
155 FALSE,
156 GIMP_PARAM_READWRITE |
157 G_PARAM_CONSTRUCT));
158
159 g_object_class_install_property (object_class, PROP_DRAW_BETA,
160 g_param_spec_boolean ("draw-beta",
161 NULL, NULL,
162 TRUE,
163 GIMP_PARAM_READWRITE |
164 G_PARAM_CONSTRUCT));
165 }
166
167 static void
gimp_dial_init(GimpDial * dial)168 gimp_dial_init (GimpDial *dial)
169 {
170 dial->priv = gimp_dial_get_instance_private (dial);
171 }
172
173 static void
gimp_dial_set_property(GObject * object,guint property_id,const GValue * value,GParamSpec * pspec)174 gimp_dial_set_property (GObject *object,
175 guint property_id,
176 const GValue *value,
177 GParamSpec *pspec)
178 {
179 GimpDial *dial = GIMP_DIAL (object);
180
181 switch (property_id)
182 {
183 case PROP_ALPHA:
184 dial->priv->alpha = g_value_get_double (value);
185 gtk_widget_queue_draw (GTK_WIDGET (dial));
186 break;
187
188 case PROP_BETA:
189 dial->priv->beta = g_value_get_double (value);
190 gtk_widget_queue_draw (GTK_WIDGET (dial));
191 break;
192
193 case PROP_CLOCKWISE_ANGLES:
194 dial->priv->clockwise_angles = g_value_get_boolean (value);
195 gtk_widget_queue_draw (GTK_WIDGET (dial));
196 break;
197
198 case PROP_CLOCKWISE_DELTA:
199 dial->priv->clockwise_delta = g_value_get_boolean (value);
200 gtk_widget_queue_draw (GTK_WIDGET (dial));
201 break;
202
203 case PROP_DRAW_BETA:
204 dial->priv->draw_beta = g_value_get_boolean (value);
205 gtk_widget_queue_draw (GTK_WIDGET (dial));
206 break;
207
208 default:
209 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
210 break;
211 }
212 }
213
214 static void
gimp_dial_get_property(GObject * object,guint property_id,GValue * value,GParamSpec * pspec)215 gimp_dial_get_property (GObject *object,
216 guint property_id,
217 GValue *value,
218 GParamSpec *pspec)
219 {
220 GimpDial *dial = GIMP_DIAL (object);
221
222 switch (property_id)
223 {
224 case PROP_ALPHA:
225 g_value_set_double (value, dial->priv->alpha);
226 break;
227
228 case PROP_BETA:
229 g_value_set_double (value, dial->priv->beta);
230 break;
231
232 case PROP_CLOCKWISE_ANGLES:
233 g_value_set_boolean (value, dial->priv->clockwise_angles);
234 break;
235
236 case PROP_CLOCKWISE_DELTA:
237 g_value_set_boolean (value, dial->priv->clockwise_delta);
238 break;
239
240 case PROP_DRAW_BETA:
241 g_value_set_boolean (value, dial->priv->draw_beta);
242 break;
243
244 default:
245 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
246 break;
247 }
248 }
249
250 static gboolean
gimp_dial_expose_event(GtkWidget * widget,GdkEventExpose * event)251 gimp_dial_expose_event (GtkWidget *widget,
252 GdkEventExpose *event)
253 {
254 GimpDial *dial = GIMP_DIAL (widget);
255
256 GTK_WIDGET_CLASS (parent_class)->expose_event (widget, event);
257
258 if (gtk_widget_is_drawable (widget))
259 {
260 GtkAllocation allocation;
261 gint size;
262 cairo_t *cr;
263 gdouble alpha = dial->priv->alpha;
264 gdouble beta = dial->priv->beta;
265
266 g_object_get (widget,
267 "size", &size,
268 NULL);
269
270 cr = gdk_cairo_create (event->window);
271 gdk_cairo_region (cr, event->region);
272 cairo_clip (cr);
273
274 gtk_widget_get_allocation (widget, &allocation);
275
276 cairo_translate (cr,
277 (gdouble) allocation.x + (allocation.width - size) / 2.0,
278 (gdouble) allocation.y + (allocation.height - size) / 2.0);
279
280 if (dial->priv->clockwise_angles)
281 {
282 alpha = -alpha;
283 beta = -beta;
284 }
285
286 gimp_dial_draw_arrows (cr, size,
287 alpha, beta,
288 dial->priv->clockwise_delta,
289 dial->priv->target,
290 dial->priv->draw_beta);
291
292 cairo_destroy (cr);
293 }
294
295 return FALSE;
296 }
297
298 static gboolean
gimp_dial_button_press_event(GtkWidget * widget,GdkEventButton * bevent)299 gimp_dial_button_press_event (GtkWidget *widget,
300 GdkEventButton *bevent)
301 {
302 GimpDial *dial = GIMP_DIAL (widget);
303
304 if (bevent->type == GDK_BUTTON_PRESS &&
305 bevent->button == 1 &&
306 dial->priv->target != DIAL_TARGET_NONE)
307 {
308 gdouble angle;
309
310 GTK_WIDGET_CLASS (parent_class)->button_press_event (widget, bevent);
311
312 angle = _gimp_circle_get_angle_and_distance (GIMP_CIRCLE (dial),
313 bevent->x, bevent->y,
314 NULL);
315
316 if (dial->priv->clockwise_angles && angle)
317 angle = 2.0 * G_PI - angle;
318
319 dial->priv->last_angle = angle;
320
321 switch (dial->priv->target)
322 {
323 case DIAL_TARGET_ALPHA:
324 g_object_set (dial, "alpha", angle, NULL);
325 break;
326
327 case DIAL_TARGET_BETA:
328 g_object_set (dial, "beta", angle, NULL);
329 break;
330
331 default:
332 break;
333 }
334 }
335
336 return FALSE;
337 }
338
339 static gboolean
gimp_dial_motion_notify_event(GtkWidget * widget,GdkEventMotion * mevent)340 gimp_dial_motion_notify_event (GtkWidget *widget,
341 GdkEventMotion *mevent)
342 {
343 GimpDial *dial = GIMP_DIAL (widget);
344 gdouble angle;
345 gdouble distance;
346
347 angle = _gimp_circle_get_angle_and_distance (GIMP_CIRCLE (dial),
348 mevent->x, mevent->y,
349 &distance);
350
351 if (dial->priv->clockwise_angles && angle)
352 angle = 2.0 * G_PI - angle;
353
354 if (_gimp_circle_has_grab (GIMP_CIRCLE (dial)))
355 {
356 gdouble delta;
357
358 delta = angle - dial->priv->last_angle;
359 dial->priv->last_angle = angle;
360
361 if (delta != 0.0)
362 {
363 switch (dial->priv->target)
364 {
365 case DIAL_TARGET_ALPHA:
366 g_object_set (dial, "alpha", angle, NULL);
367 break;
368
369 case DIAL_TARGET_BETA:
370 g_object_set (dial, "beta", angle, NULL);
371 break;
372
373 case DIAL_TARGET_BOTH:
374 g_object_set (dial,
375 "alpha", gimp_dial_normalize_angle (dial->priv->alpha + delta),
376 "beta", gimp_dial_normalize_angle (dial->priv->beta + delta),
377 NULL);
378 break;
379
380 default:
381 break;
382 }
383 }
384 }
385 else
386 {
387 DialTarget target;
388 gdouble dist_alpha;
389 gdouble dist_beta;
390
391 dist_alpha = gimp_dial_get_angle_distance (dial->priv->alpha, angle);
392 dist_beta = gimp_dial_get_angle_distance (dial->priv->beta, angle);
393
394 if (dial->priv->draw_beta &&
395 distance > SEGMENT_FRACTION &&
396 MIN (dist_alpha, dist_beta) < G_PI / 12)
397 {
398 if (dist_alpha < dist_beta)
399 {
400 target = DIAL_TARGET_ALPHA;
401 }
402 else
403 {
404 target = DIAL_TARGET_BETA;
405 }
406 }
407 else
408 {
409 target = DIAL_TARGET_BOTH;
410 }
411
412 gimp_dial_set_target (dial, target);
413 }
414
415 gdk_event_request_motions (mevent);
416
417 return FALSE;
418 }
419
420 static void
gimp_dial_reset_target(GimpCircle * circle)421 gimp_dial_reset_target (GimpCircle *circle)
422 {
423 gimp_dial_set_target (GIMP_DIAL (circle), DIAL_TARGET_NONE);
424 }
425
426
427 /* public functions */
428
429 GtkWidget *
gimp_dial_new(void)430 gimp_dial_new (void)
431 {
432 return g_object_new (GIMP_TYPE_DIAL, NULL);
433 }
434
435
436 /* private functions */
437
438 static void
gimp_dial_set_target(GimpDial * dial,DialTarget target)439 gimp_dial_set_target (GimpDial *dial,
440 DialTarget target)
441 {
442 if (target != dial->priv->target)
443 {
444 dial->priv->target = target;
445 gtk_widget_queue_draw (GTK_WIDGET (dial));
446 }
447 }
448
449 static void
gimp_dial_draw_arrow(cairo_t * cr,gdouble radius,gdouble angle)450 gimp_dial_draw_arrow (cairo_t *cr,
451 gdouble radius,
452 gdouble angle)
453 {
454 #define REL 0.8
455 #define DEL 0.1
456
457 cairo_move_to (cr, radius, radius);
458 cairo_line_to (cr,
459 radius + radius * cos (angle),
460 radius - radius * sin (angle));
461
462 cairo_move_to (cr,
463 radius + radius * cos (angle),
464 radius - radius * sin (angle));
465 cairo_line_to (cr,
466 radius + radius * REL * cos (angle - DEL),
467 radius - radius * REL * sin (angle - DEL));
468
469 cairo_move_to (cr,
470 radius + radius * cos (angle),
471 radius - radius * sin (angle));
472 cairo_line_to (cr,
473 radius + radius * REL * cos (angle + DEL),
474 radius - radius * REL * sin (angle + DEL));
475 }
476
477 static void
gimp_dial_draw_segment(cairo_t * cr,gdouble radius,gdouble alpha,gdouble beta,gboolean clockwise_delta)478 gimp_dial_draw_segment (cairo_t *cr,
479 gdouble radius,
480 gdouble alpha,
481 gdouble beta,
482 gboolean clockwise_delta)
483 {
484 gint direction = clockwise_delta ? -1 : 1;
485 gint segment_dist;
486 gint tick;
487 gdouble slice;
488
489 segment_dist = radius * SEGMENT_FRACTION;
490 tick = MIN (10, segment_dist);
491
492 cairo_move_to (cr,
493 radius + segment_dist * cos (beta),
494 radius - segment_dist * sin (beta));
495 cairo_line_to (cr,
496 radius + segment_dist * cos (beta) +
497 direction * tick * sin (beta),
498 radius - segment_dist * sin (beta) +
499 direction * tick * cos (beta));
500
501 cairo_new_sub_path (cr);
502
503 if (clockwise_delta)
504 slice = -gimp_dial_normalize_angle (alpha - beta);
505 else
506 slice = gimp_dial_normalize_angle (beta - alpha);
507
508 gimp_cairo_arc (cr, radius, radius, segment_dist,
509 alpha, slice);
510 }
511
512 static void
gimp_dial_draw_arrows(cairo_t * cr,gint size,gdouble alpha,gdouble beta,gboolean clockwise_delta,DialTarget highlight,gboolean draw_beta)513 gimp_dial_draw_arrows (cairo_t *cr,
514 gint size,
515 gdouble alpha,
516 gdouble beta,
517 gboolean clockwise_delta,
518 DialTarget highlight,
519 gboolean draw_beta)
520 {
521 gdouble radius = size / 2.0 - 2.0; /* half the broad line with and half a px */
522
523 cairo_save (cr);
524
525 cairo_translate (cr, 2.0, 2.0); /* half the broad line width and half a px*/
526
527 cairo_set_line_cap (cr, CAIRO_LINE_CAP_ROUND);
528 cairo_set_line_join (cr, CAIRO_LINE_JOIN_ROUND);
529
530 if (highlight != DIAL_TARGET_BOTH)
531 {
532 if (! (highlight & DIAL_TARGET_ALPHA))
533 gimp_dial_draw_arrow (cr, radius, alpha);
534
535 if (draw_beta)
536 {
537 if (! (highlight & DIAL_TARGET_BETA))
538 gimp_dial_draw_arrow (cr, radius, beta);
539
540 if ((highlight & DIAL_TARGET_BOTH) != DIAL_TARGET_BOTH)
541 gimp_dial_draw_segment (cr, radius, alpha, beta, clockwise_delta);
542 }
543
544 cairo_set_line_width (cr, 3.0);
545 cairo_set_source_rgba (cr, 1.0, 1.0, 1.0, 0.6);
546 cairo_stroke_preserve (cr);
547
548 cairo_set_line_width (cr, 1.0);
549 cairo_set_source_rgba (cr, 0.0, 0.0, 0.0, 0.8);
550 cairo_stroke (cr);
551 }
552
553 if (highlight != DIAL_TARGET_NONE)
554 {
555 if (highlight & DIAL_TARGET_ALPHA)
556 gimp_dial_draw_arrow (cr, radius, alpha);
557
558 if (draw_beta)
559 {
560 if (highlight & DIAL_TARGET_BETA)
561 gimp_dial_draw_arrow (cr, radius, beta);
562
563 if ((highlight & DIAL_TARGET_BOTH) == DIAL_TARGET_BOTH)
564 gimp_dial_draw_segment (cr, radius, alpha, beta, clockwise_delta);
565 }
566
567 cairo_set_line_width (cr, 3.0);
568 cairo_set_source_rgba (cr, 0.0, 0.0, 0.0, 0.6);
569 cairo_stroke_preserve (cr);
570
571 cairo_set_line_width (cr, 1.0);
572 cairo_set_source_rgba (cr, 1.0, 1.0, 1.0, 0.8);
573 cairo_stroke (cr);
574 }
575
576 cairo_restore (cr);
577 }
578
579 static gdouble
gimp_dial_normalize_angle(gdouble angle)580 gimp_dial_normalize_angle (gdouble angle)
581 {
582 if (angle < 0)
583 return angle + 2 * G_PI;
584 else if (angle > 2 * G_PI)
585 return angle - 2 * G_PI;
586 else
587 return angle;
588 }
589
590 static gdouble
gimp_dial_get_angle_distance(gdouble alpha,gdouble beta)591 gimp_dial_get_angle_distance (gdouble alpha,
592 gdouble beta)
593 {
594 return ABS (MIN (gimp_dial_normalize_angle (alpha - beta),
595 2 * G_PI - gimp_dial_normalize_angle (alpha - beta)));
596 }
597