[AdgGtkArea] Added "extents-changed" signal
[adg.git] / src / adg-gtk / adg-gtk-area.c
blob5b3eb07a05f1779291de678afa35b85e1eecc106
1 /* ADG - Automatic Drawing Generation
2 * Copyright (C) 2007,2008,2009,2010 Nicola Fontana <ntd at entidi.it>
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.
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.
14 * You should have received a copy of the GNU Lesser General Public
15 * License along with this library; if not, write to the
16 * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
17 * Boston, MA 02110-1301, USA.
21 /**
22 * SECTION:adg-gtk-area
23 * @short_description: A #GtkWidget specifically designed to contain
24 * an #AdgCanvas entity
26 * This is a #GtkDrawingArea derived object that provides an easy way
27 * to show an ADG based canvas. The associated canvas can be set
28 * directly with the adg_gtk_area_new_with_canvas() constructor
29 * function or by using adg_gtk_area_set_canvas().
31 * The default minimum size of this widget will depend on the canvas
32 * content. The global matrix of the #AdgCanvas will be adjusted to
33 * expose the drawing in the proper position. The empty space around
34 * the drawing can be changed by setting the margins of the underlying
35 * #AdgCanvas object.
37 * The default implementation reacts to some mouse events: if you drag
38 * the mouse keeping the wheel pressed the canvas will be translated;
39 * if the mouse wheel is rotated the canvas will be scaled up or down
40 * (accordingly to the wheel direction) by the factor specified in the
41 * #AdgGtkArea:factor property.
42 **/
45 /**
46 * AdgGtkArea:
48 * All fields are private and should not be used directly.
49 * Use its public methods instead.
50 **/
53 #include "adg-gtk-internal.h"
54 #include "adg-gtk-area.h"
55 #include "adg-gtk-area-private.h"
56 #include "adg-gtk-marshal.h"
58 #define _ADG_OLD_OBJECT_CLASS ((GObjectClass *) adg_gtk_area_parent_class)
59 #define _ADG_OLD_WIDGET_CLASS ((GtkWidgetClass *) adg_gtk_area_parent_class)
62 G_DEFINE_TYPE(AdgGtkArea, adg_gtk_area, GTK_TYPE_DRAWING_AREA);
64 enum {
65 PROP_0,
66 PROP_CANVAS,
67 PROP_FACTOR
70 enum {
71 CANVAS_CHANGED,
72 EXTENTS_CHANGED,
73 LAST_SIGNAL
77 static void _adg_dispose (GObject *object);
78 static void _adg_get_property (GObject *object,
79 guint prop_id,
80 GValue *value,
81 GParamSpec *pspec);
82 static void _adg_set_property (GObject *object,
83 guint prop_id,
84 const GValue *value,
85 GParamSpec *pspec);
86 static void _adg_size_request (GtkWidget *widget,
87 GtkRequisition *requisition);
88 static void _adg_size_allocate (GtkWidget *widget,
89 GtkAllocation *allocation);
90 static gboolean _adg_expose_event (GtkWidget *widget,
91 GdkEventExpose *event);
92 static gboolean _adg_scroll_event (GtkWidget *widget,
93 GdkEventScroll *event);
94 static gboolean _adg_button_press_event (GtkWidget *widget,
95 GdkEventButton *event);
96 static gboolean _adg_motion_notify_event(GtkWidget *widget,
97 GdkEventMotion *event);
98 static gboolean _adg_get_map (GtkWidget *widget,
99 gboolean local_space,
100 AdgMatrix *map,
101 AdgMatrix *inverted);
102 static void _adg_set_map (GtkWidget *widget,
103 gboolean local_space,
104 const AdgMatrix *map);
105 static const CpmlExtents *
106 _adg_get_extents (AdgGtkArea *area);
108 static guint _adg_signals[LAST_SIGNAL] = { 0 };
111 static void
112 adg_gtk_area_class_init(AdgGtkAreaClass *klass)
114 GObjectClass *gobject_class;
115 GtkWidgetClass *widget_class;
116 GParamSpec *param;
118 gobject_class = (GObjectClass *) klass;
119 widget_class = (GtkWidgetClass *) klass;
121 g_type_class_add_private(klass, sizeof(AdgGtkAreaPrivate));
123 gobject_class->dispose = _adg_dispose;
124 gobject_class->get_property = _adg_get_property;
125 gobject_class->set_property = _adg_set_property;
127 widget_class->size_request = _adg_size_request;
128 widget_class->size_allocate = _adg_size_allocate;
129 widget_class->expose_event = _adg_expose_event;
130 widget_class->scroll_event = _adg_scroll_event;
131 widget_class->button_press_event = _adg_button_press_event;
132 widget_class->motion_notify_event = _adg_motion_notify_event;
134 param = g_param_spec_object("canvas",
135 P_("Canvas"),
136 P_("The canvas to be shown"),
137 ADG_TYPE_CANVAS,
138 G_PARAM_READWRITE | G_PARAM_CONSTRUCT);
139 g_object_class_install_property(gobject_class, PROP_CANVAS, param);
141 param = g_param_spec_double("factor",
142 P_("Factor"),
143 P_("The factor used in zooming in and out"),
144 1., G_MAXDOUBLE, 1.05,
145 G_PARAM_READWRITE);
146 g_object_class_install_property(gobject_class, PROP_FACTOR, param);
149 * AdgGtkArea::canvas-changed:
150 * @area: an #AdgGtkArea
151 * @old_canvas: the old #AdgCanvas object
153 * Emitted when the #AdgGtkArea has a new canvas. If the new canvas
154 * is the same as the old one, the signal is not emitted.
156 _adg_signals[CANVAS_CHANGED] =
157 g_signal_new("canvas-changed", ADG_GTK_TYPE_AREA,
158 G_SIGNAL_RUN_LAST|G_SIGNAL_NO_RECURSE,
159 G_STRUCT_OFFSET(AdgGtkAreaClass, canvas_changed),
160 NULL, NULL,
161 adg_gtk_marshal_VOID__OBJECT,
162 G_TYPE_NONE, 1, ADG_TYPE_CANVAS);
165 * AdgGtkArea::extents-changed:
166 * @area: an #AdgGtkArea
167 * @old_extents: the old #CpmlExtents struct
169 * Emitted when the extents of @area have been changed.
170 * The old extents are always compared to the new ones,
171 * so when the extents are recalculated but the result
172 * is the same the signal is not emitted.
174 _adg_signals[EXTENTS_CHANGED] =
175 g_signal_new("extents-changed", ADG_GTK_TYPE_AREA,
176 G_SIGNAL_RUN_LAST|G_SIGNAL_NO_RECURSE,
177 G_STRUCT_OFFSET(AdgGtkAreaClass, extents_changed),
178 NULL, NULL,
179 adg_gtk_marshal_VOID__POINTER,
180 G_TYPE_NONE, 1, G_TYPE_POINTER);
183 static void
184 adg_gtk_area_init(AdgGtkArea *area)
186 AdgGtkAreaPrivate *data = G_TYPE_INSTANCE_GET_PRIVATE(area,
187 ADG_GTK_TYPE_AREA,
188 AdgGtkAreaPrivate);
190 data->canvas = NULL;
191 data->factor = 1.05;
193 data->x_event = 0;
194 data->y_event = 0;
195 data->src_factor = 0;
197 area->data = data;
199 /* Enable GDK events to catch wheel rotation and drag */
200 gtk_widget_add_events((GtkWidget *) area,
201 GDK_BUTTON_PRESS_MASK |
202 GDK_BUTTON2_MOTION_MASK |
203 GDK_SCROLL_MASK);
206 static void
207 _adg_dispose(GObject *object)
209 AdgGtkAreaPrivate *data = ((AdgGtkArea *) object)->data;
211 if (data->canvas) {
212 g_object_unref(data->canvas);
213 data->canvas = NULL;
216 if (_ADG_OLD_OBJECT_CLASS->dispose)
217 _ADG_OLD_OBJECT_CLASS->dispose(object);
220 static void
221 _adg_get_property(GObject *object, guint prop_id,
222 GValue *value, GParamSpec *pspec)
224 AdgGtkAreaPrivate *data = ((AdgGtkArea *) object)->data;
226 switch (prop_id) {
227 case PROP_CANVAS:
228 g_value_set_object(value, data->canvas);
229 break;
230 case PROP_FACTOR:
231 g_value_set_double(value, data->factor);
232 break;
233 default:
234 G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
235 break;
239 static void
240 _adg_set_property(GObject *object, guint prop_id,
241 const GValue *value, GParamSpec *pspec)
243 AdgGtkArea *area;
244 AdgGtkAreaPrivate *data;
245 AdgCanvas *canvas;
247 area = (AdgGtkArea *) object;
248 data = area->data;
250 switch (prop_id) {
251 case PROP_CANVAS:
252 canvas = g_value_get_object(value);
253 if (canvas)
254 g_object_ref(canvas);
255 if (data->canvas)
256 g_object_unref(data->canvas);
257 if (data->canvas != canvas) {
258 data->canvas = canvas;
259 g_signal_emit(area, _adg_signals[CANVAS_CHANGED], 0);
261 break;
262 case PROP_FACTOR:
263 data->factor = g_value_get_double(value);
264 break;
265 default:
266 G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
267 break;
273 * adg_gtk_area_new:
275 * Creates a new empty #AdgGtkArea. The widget is useful only after
276 * an #AdgCanvas has been added either using the #AdgGtkArea:canvas
277 * property or with adg_gtk_area_set_canvas().
279 * Returns: the newly created widget
281 GtkWidget *
282 adg_gtk_area_new(void)
284 return g_object_new(ADG_GTK_TYPE_AREA, NULL);
288 * adg_gtk_area_new_with_canvas:
289 * @canvas: the #AdgCanvas shown by this widget
291 * Creates a new #AdgGtkArea and sets the #AdgGtkArea:canvas property
292 * to @canvas.
294 * Returns: the newly created widget
296 GtkWidget *
297 adg_gtk_area_new_with_canvas(AdgCanvas *canvas)
299 g_return_val_if_fail(ADG_IS_CANVAS(canvas), NULL);
301 return g_object_new(ADG_GTK_TYPE_AREA, "canvas", canvas, NULL);
305 * adg_gtk_area_set_canvas:
306 * @area: an #AdgGtkArea
307 * @canvas: the new #AdgCanvas
309 * Sets a new canvas on @area. The old canvas, if presents, is
310 * unreferenced.
312 void
313 adg_gtk_area_set_canvas(AdgGtkArea *area, AdgCanvas *canvas)
315 g_return_if_fail(ADG_GTK_IS_AREA(area));
316 g_object_set(area, "canvas", canvas, NULL);
320 * adg_gtk_area_get_canvas:
321 * @area: an #AdgGtkArea
323 * Gets the canvas associated to @area.
325 * Returns: the requested #AdgCanvas object or %NULL on errors
327 AdgCanvas *
328 adg_gtk_area_get_canvas(AdgGtkArea *area)
330 AdgGtkAreaPrivate *data;
332 g_return_val_if_fail(ADG_GTK_IS_AREA(area), NULL);
334 data = area->data;
336 return data->canvas;
340 * adg_gtk_area_get_extents:
341 * @area: an #AdgGtkArea
343 * Gets the extents of the canvas bound to @area. The returned
344 * struct is owned by @area and should not modified or freed.
345 * This is a convenient function that gets the extents with
346 * adg_entity_get_extents() and add to it the margins of the
347 * canvas .
349 * If @area still does not have any canvas associated to it or
350 * the canvas is invalid or empty, %NULL is returned.
352 * The canvas will be updated, meaning the adg_entity_arrange()
353 * will be called before the extents computation.
355 * Returns: the extents of the @area canvas or %NULL on errors
357 const CpmlExtents *
358 adg_gtk_area_get_extents(AdgGtkArea *area)
360 g_return_val_if_fail(ADG_GTK_IS_AREA(area), NULL);
362 return _adg_get_extents(area);
366 * adg_gtk_area_set_factor:
367 * @area: an #AdgGtkArea
368 * @factor: the new zoom factor
370 * Sets a new zoom factor to @area. If the factor is less than
371 * 1, it will be clamped to 1.
373 void
374 adg_gtk_area_set_factor(AdgGtkArea *area, gdouble factor)
376 g_return_if_fail(ADG_GTK_IS_AREA(area));
377 g_object_set(area, "factor", factor, NULL);
381 * adg_gtk_area_get_factor:
382 * @area: an #AdgGtkArea
384 * Gets the zoom factor associated to @area. The zoom factor is
385 * directly used to zoom in (that is, the default zoom factor of
386 * 1.05 will zoom of 5% every iteration) and it is reversed while
387 * zooming out (that is, the default factor will use 1/1.05).
389 * Returns: the requested zoom factor or 0 on error
391 gdouble
392 adg_gtk_area_get_factor(AdgGtkArea *area)
394 AdgGtkAreaPrivate *data;
396 g_return_val_if_fail(ADG_GTK_IS_AREA(area), 0.);
398 data = area->data;
399 return data->factor;
403 * adg_gtk_area_canvas_changed:
404 * @area: an #AdgGtkArea
405 * @canvas: the old canvas bound to @area
407 * Emits the #AdgGtkArea::canvas-changed signal on @area.
409 void
410 adg_gtk_area_canvas_changed(AdgGtkArea *area, AdgCanvas *old_canvas)
412 g_return_if_fail(ADG_GTK_IS_AREA(area));
414 g_signal_emit(area, _adg_signals[CANVAS_CHANGED], 0, old_canvas);
418 * adg_gtk_area_extents_changed:
419 * @area: an #AdgGtkArea
420 * @old_extents: the old extents of @area
422 * Emits the #AdgGtkArea::extents-changed signal on @area.
424 void
425 adg_gtk_area_extents_changed(AdgGtkArea *area, const CpmlExtents *old_extents)
427 g_return_if_fail(ADG_GTK_IS_AREA(area));
429 g_signal_emit(area, _adg_signals[EXTENTS_CHANGED], 0, old_extents);
433 static void
434 _adg_size_request(GtkWidget *widget, GtkRequisition *requisition)
436 const CpmlExtents *extents = _adg_get_extents((AdgGtkArea *) widget);
438 if (extents != NULL && extents->is_defined) {
439 requisition->width = extents->size.x;
440 requisition->height = extents->size.y;
445 * _adg_size_allocate:
446 * @widget: an #AdgGtkArea widget
447 * @allocation: the new allocation struct
449 * Scales the drawing according to the new allocation.
451 * The current implementation translates the drawing as a user would
452 * expect: keep the point in the center of the #AdgCanvas at the
453 * center of the new #AdgGtkArea.
455 * The scaling is not that easy, though. The drawing is scaled up to
456 * the edges of the widget. Anyway, if the model matrix changes (that is,
457 * if the model is translated or scaled), the algorithm behaves as if
458 * the model matrix was not changed.
460 static void
461 _adg_size_allocate(GtkWidget *widget, GtkAllocation *allocation)
463 AdgGtkArea *area;
464 AdgGtkAreaPrivate *data;
465 AdgCanvas *canvas;
466 AdgEntity *entity;
467 GtkAllocation *src_allocation;
468 AdgPair ratio;
469 gdouble factor;
470 AdgMatrix map;
472 if (_ADG_OLD_WIDGET_CLASS->size_allocate)
473 _ADG_OLD_WIDGET_CLASS->size_allocate(widget, allocation);
475 area = (AdgGtkArea *) widget;
476 data = area->data;
477 canvas = data->canvas;
479 if (canvas == NULL)
480 return;
482 /* Check if the allocated space is enough:
483 * if not, there is no much we can do... */
484 g_return_if_fail(allocation->width > 0 && allocation->height > 0);
486 entity = (AdgEntity *) canvas;
487 src_allocation = &data->src_allocation;
489 adg_matrix_copy(&map, adg_entity_get_global_map(entity));
491 if (data->src_factor <= 0) {
492 /* First allocation */
493 const CpmlExtents *extents = _adg_get_extents(area);
495 if (extents == NULL || !extents->is_defined)
496 return;
498 if (extents->size.x <= 0 || extents->size.y <= 0)
499 return;
501 ratio.x = (gdouble) allocation->width / extents->size.x;
502 ratio.y = (gdouble) allocation->height / extents->size.y;
503 factor = MIN(ratio.x, ratio.y);
505 /* TODO: check the following formula, especially
506 * the final translation */
507 map.x0 = allocation->width - extents->size.x * factor;
508 map.y0 = allocation->height - extents->size.y * factor;
509 map.x0 /= 2;
510 map.y0 /= 2;
511 map.x0 -= extents->org.x;
512 map.y0 -= extents->org.y;
514 *src_allocation = *allocation;
515 src_allocation->x = map.x0;
516 src_allocation->y = map.y0;
517 data->src_factor = factor;
518 } else {
519 /* Scaling with reference to the first allocation */
520 ratio.x = data->src_factor * allocation->width / src_allocation->width;
521 ratio.y = data->src_factor * allocation->height / src_allocation->height;
523 factor = MIN(ratio.x, ratio.y);
525 map.x0 = src_allocation->x * factor +
526 (allocation->width - src_allocation->width * factor) / 2;
527 map.y0 = src_allocation->y * factor +
528 (allocation->height - src_allocation->height * factor) / 2;
531 map.xx = map.yy = factor;
532 adg_entity_set_global_map(entity, &map);
535 static gboolean
536 _adg_expose_event(GtkWidget *widget, GdkEventExpose *event)
538 AdgGtkAreaPrivate *data;
539 AdgCanvas *canvas;
541 data = ((AdgGtkArea *) widget)->data;
542 canvas = data->canvas;
544 if (canvas && event->window) {
545 cairo_t *cr = gdk_cairo_create(event->window);
546 adg_entity_render((AdgEntity *) canvas, cr);
547 cairo_destroy(cr);
550 if (_ADG_OLD_WIDGET_CLASS->expose_event == NULL)
551 return FALSE;
553 return _ADG_OLD_WIDGET_CLASS->expose_event(widget, event);
556 static gboolean
557 _adg_scroll_event(GtkWidget *widget, GdkEventScroll *event)
559 gboolean zoom_in, zoom_out, local_space, global_space;
560 AdgMatrix map, inverted;
561 AdgGtkAreaPrivate *data;
562 double factor, x, y;
564 zoom_in = event->direction == GDK_SCROLL_UP;
565 zoom_out = event->direction == GDK_SCROLL_DOWN;
566 local_space = (event->state & ADG_GTK_MODIFIERS) == 0;
567 global_space = (event->state & ADG_GTK_MODIFIERS) == GDK_SHIFT_MASK;
569 if ((zoom_in || zoom_out) && (local_space || global_space) &&
570 _adg_get_map(widget, local_space, &map, &inverted)) {
571 data = ((AdgGtkArea *) widget)->data;
572 factor = zoom_in ? data->factor : 1. / data->factor;
573 x = event->x;
574 y = event->y;
576 cairo_matrix_transform_point(&inverted, &x, &y);
577 cairo_matrix_scale(&map, factor, factor);
578 cairo_matrix_translate(&map, x/factor - x, y/factor - y);
580 _adg_set_map(widget, local_space, &map);
582 gtk_widget_queue_draw(widget);
585 if (_ADG_OLD_WIDGET_CLASS->scroll_event == NULL)
586 return FALSE;
588 return _ADG_OLD_WIDGET_CLASS->scroll_event(widget, event);
591 static gboolean
592 _adg_button_press_event(GtkWidget *widget, GdkEventButton *event)
594 AdgGtkAreaPrivate *data = ((AdgGtkArea *) widget)->data;
596 if (event->type == GDK_BUTTON_PRESS && event->button == 2) {
597 /* Remember the starting coordinates of a (probable) translation */
598 data->x_event = event->x;
599 data->y_event = event->y;
602 if (_ADG_OLD_WIDGET_CLASS->button_press_event == NULL)
603 return FALSE;
605 return _ADG_OLD_WIDGET_CLASS->button_press_event(widget, event);
608 static gboolean
609 _adg_motion_notify_event(GtkWidget *widget, GdkEventMotion *event)
611 gboolean translating, local_space, global_space;
612 AdgMatrix map, inverted;
613 AdgGtkAreaPrivate *data;
614 double x, y;
616 translating = (event->state & GDK_BUTTON2_MASK) == GDK_BUTTON2_MASK;
617 local_space = (event->state & ADG_GTK_MODIFIERS) == 0;
618 global_space = (event->state & ADG_GTK_MODIFIERS) == GDK_SHIFT_MASK;
620 if (translating && (local_space || global_space) &&
621 _adg_get_map(widget, local_space, &map, &inverted)) {
622 data = ((AdgGtkArea *) widget)->data;
623 x = event->x - data->x_event;
624 y = event->y - data->y_event;
626 cairo_matrix_transform_distance(&inverted, &x, &y);
627 cairo_matrix_translate(&map, x, y);
628 data->x_event = event->x;
629 data->y_event = event->y;
631 _adg_set_map(widget, local_space, &map);
633 gtk_widget_queue_draw(widget);
636 if (_ADG_OLD_WIDGET_CLASS->motion_notify_event == NULL)
637 return FALSE;
639 return _ADG_OLD_WIDGET_CLASS->motion_notify_event(widget, event);
642 static gboolean
643 _adg_get_map(GtkWidget *widget, gboolean local_space,
644 AdgMatrix *map, AdgMatrix *inverted)
646 AdgGtkAreaPrivate *data;
647 AdgEntity *entity;
649 data = ((AdgGtkArea *) widget)->data;
650 entity = (AdgEntity *) data->canvas;
651 if (entity == NULL)
652 return FALSE;
654 if (local_space) {
655 adg_matrix_copy(map, adg_entity_get_local_map(entity));
657 /* The inverted map is subject to the global matrix */
658 adg_matrix_copy(inverted, adg_entity_get_global_matrix(entity));
659 adg_matrix_transform(inverted, map, ADG_TRANSFORM_BEFORE);
660 } else {
661 adg_matrix_copy(map, adg_entity_get_global_map(entity));
662 adg_matrix_copy(inverted, map);
665 return cairo_matrix_invert(inverted) == CAIRO_STATUS_SUCCESS;
668 static void
669 _adg_set_map(GtkWidget *widget, gboolean local_space, const AdgMatrix *map)
671 AdgGtkAreaPrivate *data;
672 AdgEntity *entity;
674 data = ((AdgGtkArea *) widget)->data;
675 entity = (AdgEntity *) data->canvas;
677 if (entity == NULL)
678 return;
680 if (local_space)
681 adg_entity_set_local_map(entity, map);
682 else
683 adg_entity_set_global_map(entity, map);
685 _adg_get_extents((AdgGtkArea *) widget);
688 static const CpmlExtents *
689 _adg_get_extents(AdgGtkArea *area)
691 AdgGtkAreaPrivate *data;
692 AdgCanvas *canvas;
693 CpmlExtents old_extents;
694 gdouble top, right, bottom, left;
696 data = area->data;
697 old_extents = data->extents;
698 data->extents.is_defined = FALSE;
700 canvas = data->canvas;
702 if (ADG_IS_CANVAS(canvas)) {
703 const CpmlExtents *extents;
704 AdgEntity *entity;
706 entity = (AdgEntity *) canvas;
708 adg_entity_arrange(entity);
710 extents = adg_entity_get_extents(entity);
711 if (extents != NULL && extents->is_defined) {
712 top = adg_canvas_get_top_margin(canvas);
713 right = adg_canvas_get_right_margin(canvas);
714 bottom = adg_canvas_get_bottom_margin(canvas);
715 left = adg_canvas_get_left_margin(canvas);
717 data->extents = *extents;
718 data->extents.org.x -= left;
719 data->extents.org.y -= top;
720 data->extents.size.x += left + right;
721 data->extents.size.y += top + bottom;
722 data->extents.is_defined = TRUE;
726 if (!cpml_extents_equal(&data->extents, &old_extents))
727 g_signal_emit(area, _adg_signals[EXTENTS_CHANGED], 0, &old_extents);
729 return &data->extents;