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