1 /* GIMP - The GNU Image Manipulation Program
2  * Copyright (C) 1995 Spencer Kimball and Peter Mattis
3  *
4  * gimpcircle.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 "gimpcircle.h"
37 
38 
39 enum
40 {
41   PROP_0,
42   PROP_SIZE,
43   PROP_BORDER_WIDTH,
44   PROP_BACKGROUND
45 };
46 
47 
48 struct _GimpCirclePrivate
49 {
50   gint                  size;
51   gint                  border_width;
52   GimpCircleBackground  background;
53 
54   GdkWindow            *event_window;
55   cairo_surface_t      *surface;
56   gboolean              has_grab;
57   gboolean              in_widget;
58 };
59 
60 
61 static void        gimp_circle_dispose              (GObject              *object);
62 static void        gimp_circle_set_property         (GObject              *object,
63                                                      guint                 property_id,
64                                                      const GValue         *value,
65                                                      GParamSpec           *pspec);
66 static void        gimp_circle_get_property         (GObject              *object,
67                                                      guint                 property_id,
68                                                      GValue               *value,
69                                                      GParamSpec           *pspec);
70 
71 static void        gimp_circle_realize              (GtkWidget            *widget);
72 static void        gimp_circle_unrealize            (GtkWidget            *widget);
73 static void        gimp_circle_map                  (GtkWidget            *widget);
74 static void        gimp_circle_unmap                (GtkWidget            *widget);
75 static void        gimp_circle_size_request         (GtkWidget            *widget,
76                                                      GtkRequisition       *requisition);
77 static void        gimp_circle_size_allocate        (GtkWidget            *widget,
78                                                      GtkAllocation        *allocation);
79 static gboolean    gimp_circle_expose_event         (GtkWidget            *widget,
80                                                      GdkEventExpose       *event);
81 static gboolean    gimp_circle_button_press_event   (GtkWidget            *widget,
82                                                      GdkEventButton       *bevent);
83 static gboolean    gimp_circle_button_release_event (GtkWidget            *widget,
84                                                      GdkEventButton       *bevent);
85 static gboolean    gimp_circle_enter_notify_event   (GtkWidget            *widget,
86                                                      GdkEventCrossing     *event);
87 static gboolean    gimp_circle_leave_notify_event   (GtkWidget            *widget,
88                                                      GdkEventCrossing     *event);
89 
90 static void        gimp_circle_real_reset_target    (GimpCircle           *circle);
91 
92 static void        gimp_circle_background_hsv       (gdouble               angle,
93                                                      gdouble               distance,
94                                                      guchar               *rgb);
95 
96 static void        gimp_circle_draw_background      (GimpCircle           *circle,
97                                                      cairo_t              *cr,
98                                                      gint                  size,
99                                                      GimpCircleBackground  background);
100 
101 
G_DEFINE_TYPE_WITH_PRIVATE(GimpCircle,gimp_circle,GTK_TYPE_WIDGET)102 G_DEFINE_TYPE_WITH_PRIVATE (GimpCircle, gimp_circle, GTK_TYPE_WIDGET)
103 
104 #define parent_class gimp_circle_parent_class
105 
106 
107 static void
108 gimp_circle_class_init (GimpCircleClass *klass)
109 {
110   GObjectClass   *object_class = G_OBJECT_CLASS (klass);
111   GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
112 
113   object_class->dispose              = gimp_circle_dispose;
114   object_class->get_property         = gimp_circle_get_property;
115   object_class->set_property         = gimp_circle_set_property;
116 
117   widget_class->realize              = gimp_circle_realize;
118   widget_class->unrealize            = gimp_circle_unrealize;
119   widget_class->map                  = gimp_circle_map;
120   widget_class->unmap                = gimp_circle_unmap;
121   widget_class->size_request         = gimp_circle_size_request;
122   widget_class->size_allocate        = gimp_circle_size_allocate;
123   widget_class->expose_event         = gimp_circle_expose_event;
124   widget_class->button_press_event   = gimp_circle_button_press_event;
125   widget_class->button_release_event = gimp_circle_button_release_event;
126   widget_class->enter_notify_event   = gimp_circle_enter_notify_event;
127   widget_class->leave_notify_event   = gimp_circle_leave_notify_event;
128 
129   klass->reset_target                = gimp_circle_real_reset_target;
130 
131   g_object_class_install_property (object_class, PROP_SIZE,
132                                    g_param_spec_int ("size",
133                                                      NULL, NULL,
134                                                      32, 1024, 96,
135                                                      GIMP_PARAM_READWRITE |
136                                                      G_PARAM_CONSTRUCT));
137 
138   g_object_class_install_property (object_class, PROP_BORDER_WIDTH,
139                                    g_param_spec_int ("border-width",
140                                                      NULL, NULL,
141                                                      0, 64, 0,
142                                                      GIMP_PARAM_READWRITE |
143                                                      G_PARAM_CONSTRUCT));
144 
145   g_object_class_install_property (object_class, PROP_BACKGROUND,
146                                    g_param_spec_enum ("background",
147                                                       NULL, NULL,
148                                                       GIMP_TYPE_CIRCLE_BACKGROUND,
149                                                       GIMP_CIRCLE_BACKGROUND_HSV,
150                                                       GIMP_PARAM_READWRITE |
151                                                       G_PARAM_CONSTRUCT));
152 }
153 
154 static void
gimp_circle_init(GimpCircle * circle)155 gimp_circle_init (GimpCircle *circle)
156 {
157   circle->priv = gimp_circle_get_instance_private (circle);
158 
159   gtk_widget_set_has_window (GTK_WIDGET (circle), FALSE);
160   gtk_widget_add_events (GTK_WIDGET (circle),
161                          GDK_POINTER_MOTION_MASK |
162                          GDK_BUTTON_PRESS_MASK   |
163                          GDK_BUTTON_RELEASE_MASK |
164                          GDK_BUTTON1_MOTION_MASK |
165                          GDK_ENTER_NOTIFY_MASK   |
166                          GDK_LEAVE_NOTIFY_MASK);
167 }
168 
169 static void
gimp_circle_dispose(GObject * object)170 gimp_circle_dispose (GObject *object)
171 {
172   GimpCircle *circle = GIMP_CIRCLE (object);
173 
174   g_clear_pointer (&circle->priv->surface, cairo_surface_destroy);
175 
176   G_OBJECT_CLASS (parent_class)->dispose (object);
177 }
178 
179 static void
gimp_circle_set_property(GObject * object,guint property_id,const GValue * value,GParamSpec * pspec)180 gimp_circle_set_property (GObject      *object,
181                           guint         property_id,
182                           const GValue *value,
183                           GParamSpec   *pspec)
184 {
185   GimpCircle *circle = GIMP_CIRCLE (object);
186 
187   switch (property_id)
188     {
189     case PROP_SIZE:
190       circle->priv->size = g_value_get_int (value);
191       gtk_widget_queue_resize (GTK_WIDGET (circle));
192       break;
193 
194     case PROP_BORDER_WIDTH:
195       circle->priv->border_width = g_value_get_int (value);
196       gtk_widget_queue_resize (GTK_WIDGET (circle));
197       break;
198 
199     case PROP_BACKGROUND:
200       circle->priv->background = g_value_get_enum (value);
201       g_clear_pointer (&circle->priv->surface, cairo_surface_destroy);
202       gtk_widget_queue_draw (GTK_WIDGET (circle));
203       break;
204 
205     default:
206       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
207       break;
208     }
209 }
210 
211 static void
gimp_circle_get_property(GObject * object,guint property_id,GValue * value,GParamSpec * pspec)212 gimp_circle_get_property (GObject    *object,
213                           guint       property_id,
214                           GValue     *value,
215                           GParamSpec *pspec)
216 {
217   GimpCircle *circle = GIMP_CIRCLE (object);
218 
219   switch (property_id)
220     {
221     case PROP_SIZE:
222       g_value_set_int (value, circle->priv->size);
223       break;
224 
225     case PROP_BORDER_WIDTH:
226       g_value_set_int (value, circle->priv->border_width);
227       break;
228 
229     case PROP_BACKGROUND:
230       g_value_set_enum (value, circle->priv->background);
231       break;
232 
233     default:
234       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
235       break;
236     }
237 }
238 
239 static void
gimp_circle_realize(GtkWidget * widget)240 gimp_circle_realize (GtkWidget *widget)
241 {
242   GimpCircle    *circle = GIMP_CIRCLE (widget);
243   GtkAllocation  allocation;
244   GdkWindowAttr  attributes;
245   gint           attributes_mask;
246 
247   GTK_WIDGET_CLASS (parent_class)->realize (widget);
248 
249   gtk_widget_get_allocation (widget, &allocation);
250 
251   attributes.window_type = GDK_WINDOW_CHILD;
252   attributes.x           = allocation.x;
253   attributes.y           = allocation.y;
254   attributes.width       = allocation.width;
255   attributes.height      = allocation.height;
256   attributes.wclass      = GDK_INPUT_ONLY;
257   attributes.event_mask  = gtk_widget_get_events (widget);
258 
259   attributes_mask = GDK_WA_X | GDK_WA_Y;
260 
261   circle->priv->event_window = gdk_window_new (gtk_widget_get_window (widget),
262                                                &attributes, attributes_mask);
263   gdk_window_set_user_data (circle->priv->event_window, circle);
264 }
265 
266 static void
gimp_circle_unrealize(GtkWidget * widget)267 gimp_circle_unrealize (GtkWidget *widget)
268 {
269   GimpCircle *circle = GIMP_CIRCLE (widget);
270 
271   if (circle->priv->event_window)
272     {
273       gdk_window_set_user_data (circle->priv->event_window, NULL);
274       gdk_window_destroy (circle->priv->event_window);
275       circle->priv->event_window = NULL;
276     }
277 
278   GTK_WIDGET_CLASS (parent_class)->unrealize (widget);
279 }
280 
281 static void
gimp_circle_map(GtkWidget * widget)282 gimp_circle_map (GtkWidget *widget)
283 {
284   GimpCircle *circle = GIMP_CIRCLE (widget);
285 
286   GTK_WIDGET_CLASS (parent_class)->map (widget);
287 
288   if (circle->priv->event_window)
289     gdk_window_show (circle->priv->event_window);
290 }
291 
292 static void
gimp_circle_unmap(GtkWidget * widget)293 gimp_circle_unmap (GtkWidget *widget)
294 {
295   GimpCircle *circle = GIMP_CIRCLE (widget);
296 
297   if (circle->priv->has_grab)
298     {
299       gtk_grab_remove (widget);
300       circle->priv->has_grab = FALSE;
301     }
302 
303   if (circle->priv->event_window)
304     gdk_window_hide (circle->priv->event_window);
305 
306   GTK_WIDGET_CLASS (parent_class)->unmap (widget);
307 }
308 
309 static void
gimp_circle_size_request(GtkWidget * widget,GtkRequisition * requisition)310 gimp_circle_size_request (GtkWidget      *widget,
311                         GtkRequisition *requisition)
312 {
313   GimpCircle *circle = GIMP_CIRCLE (widget);
314 
315   requisition->width  = 2 * circle->priv->border_width + circle->priv->size;
316   requisition->height = 2 * circle->priv->border_width + circle->priv->size;
317 }
318 
319 static void
gimp_circle_size_allocate(GtkWidget * widget,GtkAllocation * allocation)320 gimp_circle_size_allocate (GtkWidget     *widget,
321                            GtkAllocation *allocation)
322 {
323   GimpCircle *circle = GIMP_CIRCLE (widget);
324 
325   GTK_WIDGET_CLASS (parent_class)->size_allocate (widget, allocation);
326 
327   if (gtk_widget_get_realized (widget))
328     gdk_window_move_resize (circle->priv->event_window,
329                             allocation->x,
330                             allocation->y,
331                             allocation->width,
332                             allocation->height);
333 
334   g_clear_pointer (&circle->priv->surface, cairo_surface_destroy);
335 }
336 
337 static gboolean
gimp_circle_expose_event(GtkWidget * widget,GdkEventExpose * event)338 gimp_circle_expose_event (GtkWidget      *widget,
339                           GdkEventExpose *event)
340 {
341   GimpCircle *circle = GIMP_CIRCLE (widget);
342 
343   if (gtk_widget_is_drawable (widget))
344     {
345       GtkAllocation  allocation;
346       gint           size = circle->priv->size;
347       cairo_t       *cr;
348 
349       cr = gdk_cairo_create (event->window);
350       gdk_cairo_region (cr, event->region);
351       cairo_clip (cr);
352 
353       gtk_widget_get_allocation (widget, &allocation);
354 
355       cairo_translate (cr,
356                        allocation.x + (allocation.width  - size) / 2,
357                        allocation.y + (allocation.height - size) / 2);
358 
359       gimp_circle_draw_background (circle, cr, size, circle->priv->background);
360 
361       cairo_destroy (cr);
362     }
363 
364   return FALSE;
365 }
366 
367 static gboolean
gimp_circle_button_press_event(GtkWidget * widget,GdkEventButton * bevent)368 gimp_circle_button_press_event (GtkWidget      *widget,
369                                 GdkEventButton *bevent)
370 {
371   GimpCircle *circle = GIMP_CIRCLE (widget);
372 
373   if (bevent->type == GDK_BUTTON_PRESS &&
374       bevent->button == 1)
375     {
376       gtk_grab_add (widget);
377       circle->priv->has_grab = TRUE;
378     }
379 
380   return FALSE;
381 }
382 
383 static gboolean
gimp_circle_button_release_event(GtkWidget * widget,GdkEventButton * bevent)384 gimp_circle_button_release_event (GtkWidget      *widget,
385                                   GdkEventButton *bevent)
386 {
387   GimpCircle *circle = GIMP_CIRCLE (widget);
388 
389   if (bevent->button == 1)
390     {
391       gtk_grab_remove (widget);
392       circle->priv->has_grab = FALSE;
393 
394       if (! circle->priv->in_widget)
395         GIMP_CIRCLE_GET_CLASS (circle)->reset_target (circle);
396     }
397 
398   return FALSE;
399 }
400 
401 static gboolean
gimp_circle_enter_notify_event(GtkWidget * widget,GdkEventCrossing * event)402 gimp_circle_enter_notify_event (GtkWidget        *widget,
403                                 GdkEventCrossing *event)
404 {
405   GimpCircle *circle = GIMP_CIRCLE (widget);
406 
407   circle->priv->in_widget = TRUE;
408 
409   return FALSE;
410 }
411 
412 static gboolean
gimp_circle_leave_notify_event(GtkWidget * widget,GdkEventCrossing * event)413 gimp_circle_leave_notify_event (GtkWidget        *widget,
414                                 GdkEventCrossing *event)
415 {
416   GimpCircle *circle = GIMP_CIRCLE (widget);
417 
418   circle->priv->in_widget = FALSE;
419 
420   if (! circle->priv->has_grab)
421     GIMP_CIRCLE_GET_CLASS (circle)->reset_target (circle);
422 
423   return FALSE;
424 }
425 
426 static void
gimp_circle_real_reset_target(GimpCircle * circle)427 gimp_circle_real_reset_target (GimpCircle *circle)
428 {
429 }
430 
431 
432 /*  public functions  */
433 
434 GtkWidget *
gimp_circle_new(void)435 gimp_circle_new (void)
436 {
437   return g_object_new (GIMP_TYPE_CIRCLE, NULL);
438 }
439 
440 
441 /*  protected functions  */
442 
443 static gdouble
get_angle_and_distance(gdouble center_x,gdouble center_y,gdouble radius,gdouble x,gdouble y,gdouble * distance)444 get_angle_and_distance (gdouble  center_x,
445                         gdouble  center_y,
446                         gdouble  radius,
447                         gdouble  x,
448                         gdouble  y,
449                         gdouble *distance)
450 {
451   gdouble angle = atan2 (center_y - y,
452                          x - center_x);
453 
454   if (angle < 0)
455     angle += 2 * G_PI;
456 
457   if (distance)
458     *distance = sqrt ((SQR (x - center_x) +
459                        SQR (y - center_y)) / SQR (radius));
460 
461   return angle;
462 }
463 
464 gboolean
_gimp_circle_has_grab(GimpCircle * circle)465 _gimp_circle_has_grab (GimpCircle *circle)
466 {
467   g_return_val_if_fail (GIMP_IS_CIRCLE (circle), FALSE);
468 
469   return circle->priv->has_grab;
470 }
471 
472 gdouble
_gimp_circle_get_angle_and_distance(GimpCircle * circle,gdouble event_x,gdouble event_y,gdouble * distance)473 _gimp_circle_get_angle_and_distance (GimpCircle *circle,
474                                      gdouble     event_x,
475                                      gdouble     event_y,
476                                      gdouble    *distance)
477 {
478   GtkAllocation allocation;
479   gdouble       center_x;
480   gdouble       center_y;
481 
482   g_return_val_if_fail (GIMP_IS_CIRCLE (circle), 0.0);
483 
484   gtk_widget_get_allocation (GTK_WIDGET (circle), &allocation);
485 
486   center_x = allocation.width  / 2.0;
487   center_y = allocation.height / 2.0;
488 
489   return get_angle_and_distance (center_x, center_y, circle->priv->size / 2.0,
490                                  event_x, event_y,
491                                  distance);
492 }
493 
494 
495 /*  private functions  */
496 
497 static void
gimp_circle_background_hsv(gdouble angle,gdouble distance,guchar * rgb)498 gimp_circle_background_hsv (gdouble  angle,
499                             gdouble  distance,
500                             guchar  *rgb)
501 {
502   GimpHSV hsv;
503   GimpRGB color;
504 
505   gimp_hsv_set (&hsv,
506                 angle / (2.0 * G_PI),
507                 distance,
508                 1 - sqrt (distance) / 4 /* it just looks nicer this way */);
509 
510   gimp_hsv_to_rgb (&hsv, &color);
511 
512   gimp_rgb_get_uchar (&color, rgb, rgb + 1, rgb + 2);
513 }
514 
515 static void
gimp_circle_draw_background(GimpCircle * circle,cairo_t * cr,gint size,GimpCircleBackground background)516 gimp_circle_draw_background (GimpCircle           *circle,
517                              cairo_t              *cr,
518                              gint                  size,
519                              GimpCircleBackground  background)
520 {
521   cairo_save (cr);
522 
523   if (background == GIMP_CIRCLE_BACKGROUND_PLAIN)
524     {
525       cairo_arc (cr, size / 2.0, size / 2.0, size / 2.0 - 1.5, 0.0, 2 * G_PI);
526 
527       cairo_set_line_width (cr, 3.0);
528       cairo_set_source_rgba (cr, 1.0, 1.0, 1.0, 0.6);
529       cairo_stroke_preserve (cr);
530 
531       cairo_set_line_width (cr, 1.0);
532       cairo_set_source_rgba (cr, 0.0, 0.0, 0.0, 0.8);
533       cairo_stroke (cr);
534     }
535   else
536     {
537       if (! circle->priv->surface)
538         {
539           guchar *data;
540           gint    stride;
541           gint    x, y;
542 
543           circle->priv->surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32,
544                                                               size, size);
545 
546           data   = cairo_image_surface_get_data (circle->priv->surface);
547           stride = cairo_image_surface_get_stride (circle->priv->surface);
548 
549           for (y = 0; y < size; y++)
550             {
551               for (x = 0; x < size; x++)
552                 {
553                   gdouble angle;
554                   gdouble distance;
555                   guchar  rgb[3] = { 0, };
556 
557                   angle = get_angle_and_distance (size / 2.0, size / 2.0,
558                                                   size / 2.0,
559                                                   x, y,
560                                                   &distance);
561 
562                   switch (background)
563                     {
564                     case GIMP_CIRCLE_BACKGROUND_HSV:
565                       gimp_circle_background_hsv (angle, distance, rgb);
566                       break;
567 
568                     default:
569                       break;
570                     }
571 
572                   GIMP_CAIRO_ARGB32_SET_PIXEL (data + y * stride + x * 4,
573                                                rgb[0], rgb[1], rgb[2], 255);
574                 }
575             }
576 
577           cairo_surface_mark_dirty (circle->priv->surface);
578         }
579 
580       cairo_set_source_surface (cr, circle->priv->surface, 0.0, 0.0);
581 
582       cairo_arc (cr, size / 2.0, size / 2.0, size / 2.0, 0.0, 2 * G_PI);
583       cairo_clip (cr);
584 
585       cairo_paint (cr);
586     }
587 
588   cairo_restore (cr);
589 }
590