[AdgGtkArea] New API: adg_gtk_get_extents()
[adg.git] / src / adg-gtk / adg-gtk-area.c
blob970a62fd375ca2eddd491ac1477bc1c42fdcdf7a
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"
57 #define _ADG_OLD_OBJECT_CLASS ((GObjectClass *) adg_gtk_area_parent_class)
58 #define _ADG_OLD_WIDGET_CLASS ((GtkWidgetClass *) adg_gtk_area_parent_class)
61 G_DEFINE_TYPE(AdgGtkArea, adg_gtk_area, GTK_TYPE_DRAWING_AREA);
63 enum {
64 PROP_0,
65 PROP_CANVAS,
66 PROP_FACTOR
69 enum {
70 CANVAS_CHANGED,
71 LAST_SIGNAL
75 static void _adg_dispose (GObject *object);
76 static void _adg_get_property (GObject *object,
77 guint prop_id,
78 GValue *value,
79 GParamSpec *pspec);
80 static void _adg_set_property (GObject *object,
81 guint prop_id,
82 const GValue *value,
83 GParamSpec *pspec);
84 static void _adg_size_request (GtkWidget *widget,
85 GtkRequisition *requisition);
86 static void _adg_size_allocate (GtkWidget *widget,
87 GtkAllocation *allocation);
88 static gboolean _adg_expose_event (GtkWidget *widget,
89 GdkEventExpose *event);
90 static gboolean _adg_scroll_event (GtkWidget *widget,
91 GdkEventScroll *event);
92 static gboolean _adg_button_press_event (GtkWidget *widget,
93 GdkEventButton *event);
94 static gboolean _adg_motion_notify_event(GtkWidget *widget,
95 GdkEventMotion *event);
96 static gboolean _adg_get_map (GtkWidget *widget,
97 gboolean local_space,
98 AdgMatrix *map,
99 AdgMatrix *inverted);
100 static void _adg_set_map (GtkWidget *widget,
101 gboolean local_space,
102 const AdgMatrix *map);
103 static const CpmlExtents *
104 _adg_get_extents (AdgGtkArea *area);
106 static guint _adg_signals[LAST_SIGNAL] = { 0 };
109 static void
110 adg_gtk_area_class_init(AdgGtkAreaClass *klass)
112 GObjectClass *gobject_class;
113 GtkWidgetClass *widget_class;
114 GParamSpec *param;
116 gobject_class = (GObjectClass *) klass;
117 widget_class = (GtkWidgetClass *) klass;
119 g_type_class_add_private(klass, sizeof(AdgGtkAreaPrivate));
121 gobject_class->dispose = _adg_dispose;
122 gobject_class->get_property = _adg_get_property;
123 gobject_class->set_property = _adg_set_property;
125 widget_class->size_request = _adg_size_request;
126 widget_class->size_allocate = _adg_size_allocate;
127 widget_class->expose_event = _adg_expose_event;
128 widget_class->scroll_event = _adg_scroll_event;
129 widget_class->button_press_event = _adg_button_press_event;
130 widget_class->motion_notify_event = _adg_motion_notify_event;
132 param = g_param_spec_object("canvas",
133 P_("Canvas"),
134 P_("The canvas to be shown"),
135 ADG_TYPE_CANVAS,
136 G_PARAM_READWRITE | G_PARAM_CONSTRUCT);
137 g_object_class_install_property(gobject_class, PROP_CANVAS, param);
139 param = g_param_spec_double("factor",
140 P_("Factor"),
141 P_("The factor used in zooming in and out"),
142 1., G_MAXDOUBLE, 1.05,
143 G_PARAM_READWRITE);
144 g_object_class_install_property(gobject_class, PROP_FACTOR, param);
147 * AdgGtkArea::canvas-changed:
148 * @area: an #AdgGtkArea
150 * Emitted when the #AdgGtkArea has a new canvas.
152 _adg_signals[CANVAS_CHANGED] =
153 g_signal_new("canvas-changed", ADG_GTK_TYPE_AREA,
154 G_SIGNAL_RUN_LAST|G_SIGNAL_NO_RECURSE,
155 G_STRUCT_OFFSET(AdgGtkAreaClass, canvas_changed),
156 NULL, NULL,
157 g_cclosure_marshal_VOID__VOID,
158 G_TYPE_NONE, 0);
161 static void
162 adg_gtk_area_init(AdgGtkArea *area)
164 AdgGtkAreaPrivate *data = G_TYPE_INSTANCE_GET_PRIVATE(area,
165 ADG_GTK_TYPE_AREA,
166 AdgGtkAreaPrivate);
168 data->canvas = NULL;
169 data->factor = 1.05;
171 data->x_event = 0;
172 data->y_event = 0;
173 data->src_factor = 0;
175 area->data = data;
177 /* Enable GDK events to catch wheel rotation and drag */
178 gtk_widget_add_events((GtkWidget *) area,
179 GDK_BUTTON_PRESS_MASK |
180 GDK_BUTTON2_MOTION_MASK |
181 GDK_SCROLL_MASK);
184 static void
185 _adg_dispose(GObject *object)
187 AdgGtkAreaPrivate *data = ((AdgGtkArea *) object)->data;
189 if (data->canvas) {
190 g_object_unref(data->canvas);
191 data->canvas = NULL;
194 if (_ADG_OLD_OBJECT_CLASS->dispose)
195 _ADG_OLD_OBJECT_CLASS->dispose(object);
198 static void
199 _adg_get_property(GObject *object, guint prop_id,
200 GValue *value, GParamSpec *pspec)
202 AdgGtkAreaPrivate *data = ((AdgGtkArea *) object)->data;
204 switch (prop_id) {
205 case PROP_CANVAS:
206 g_value_set_object(value, data->canvas);
207 break;
208 case PROP_FACTOR:
209 g_value_set_double(value, data->factor);
210 break;
211 default:
212 G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
213 break;
217 static void
218 _adg_set_property(GObject *object, guint prop_id,
219 const GValue *value, GParamSpec *pspec)
221 AdgGtkArea *area;
222 AdgGtkAreaPrivate *data;
223 AdgCanvas *canvas;
225 area = (AdgGtkArea *) object;
226 data = area->data;
228 switch (prop_id) {
229 case PROP_CANVAS:
230 canvas = g_value_get_object(value);
231 if (canvas)
232 g_object_ref(canvas);
233 if (data->canvas)
234 g_object_unref(data->canvas);
235 data->canvas = canvas;
236 g_signal_emit(area, _adg_signals[CANVAS_CHANGED], 0);
237 break;
238 case PROP_FACTOR:
239 data->factor = g_value_get_double(value);
240 break;
241 default:
242 G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
243 break;
249 * adg_gtk_area_new:
251 * Creates a new empty #AdgGtkArea. The widget is useful only after
252 * an #AdgCanvas has been added either using the #AdgGtkArea:canvas
253 * property or with adg_gtk_area_set_canvas().
255 * Returns: the newly created widget
257 GtkWidget *
258 adg_gtk_area_new(void)
260 return g_object_new(ADG_GTK_TYPE_AREA, NULL);
264 * adg_gtk_area_new_with_canvas:
265 * @canvas: the #AdgCanvas shown by this widget
267 * Creates a new #AdgGtkArea and sets the #AdgGtkArea:canvas property
268 * to @canvas.
270 * Returns: the newly created widget
272 GtkWidget *
273 adg_gtk_area_new_with_canvas(AdgCanvas *canvas)
275 g_return_val_if_fail(ADG_IS_CANVAS(canvas), NULL);
277 return g_object_new(ADG_GTK_TYPE_AREA, "canvas", canvas, NULL);
281 * adg_gtk_area_set_canvas:
282 * @area: an #AdgGtkArea
283 * @canvas: the new #AdgCanvas
285 * Sets a new canvas on @area. The old canvas, if presents, is
286 * unreferenced.
288 void
289 adg_gtk_area_set_canvas(AdgGtkArea *area, AdgCanvas *canvas)
291 g_return_if_fail(ADG_GTK_IS_AREA(area));
292 g_object_set(area, "canvas", canvas, NULL);
296 * adg_gtk_area_get_canvas:
297 * @area: an #AdgGtkArea
299 * Gets the canvas associated to @area.
301 * Returns: the requested #AdgCanvas object or %NULL on errors
303 AdgCanvas *
304 adg_gtk_area_get_canvas(AdgGtkArea *area)
306 AdgGtkAreaPrivate *data;
308 g_return_val_if_fail(ADG_GTK_IS_AREA(area), NULL);
310 data = area->data;
312 return data->canvas;
316 * adg_gtk_area_get_extents:
317 * @area: an #AdgGtkArea
319 * Gets the extents of the canvas bound to @area. The returned
320 * struct is owned by @area and should not modified or freed.
321 * This is a convenient function that gets the extents with
322 * adg_entity_get_extents() and add to it the margins of the
323 * canvas .
325 * If @area still does not have any canvas associated to it or
326 * the canvas is invalid or empty, %NULL is returned.
328 * The canvas will be updated, meaning the adg_entity_arrange()
329 * will be called before the extents computation.
331 * Returns: the extents of the @area canvas or %NULL on errors
333 const CpmlExtents *
334 adg_gtk_area_get_extents(AdgGtkArea *area)
336 g_return_val_if_fail(ADG_GTK_IS_AREA(area), NULL);
338 return _adg_get_extents(area);
342 * adg_gtk_area_set_factor:
343 * @area: an #AdgGtkArea
344 * @factor: the new zoom factor
346 * Sets a new zoom factor to @area. If the factor is less than
347 * 1, it will be clamped to 1.
349 void
350 adg_gtk_area_set_factor(AdgGtkArea *area, gdouble factor)
352 g_return_if_fail(ADG_GTK_IS_AREA(area));
353 g_object_set(area, "factor", factor, NULL);
357 * adg_gtk_area_get_factor:
358 * @area: an #AdgGtkArea
360 * Gets the zoom factor associated to @area. The zoom factor is
361 * directly used to zoom in (that is, the default zoom factor of
362 * 1.05 will zoom of 5% every iteration) and it is reversed while
363 * zooming out (that is, the default factor will use 1/1.05).
365 * Returns: the requested zoom factor or 0 on error
367 gdouble
368 adg_gtk_area_get_factor(AdgGtkArea *area)
370 AdgGtkAreaPrivate *data;
372 g_return_val_if_fail(ADG_GTK_IS_AREA(area), 0.);
374 data = area->data;
375 return data->factor;
379 static void
380 _adg_size_request(GtkWidget *widget, GtkRequisition *requisition)
382 const CpmlExtents *extents = _adg_get_extents((AdgGtkArea *) widget);
384 if (extents != NULL && extents->is_defined) {
385 requisition->width = extents->size.x;
386 requisition->height = extents->size.y;
391 * _adg_size_allocate:
392 * @widget: an #AdgGtkArea widget
393 * @allocation: the new allocation struct
395 * Scales the drawing according to the new allocation.
397 * The current implementation translates the drawing as a user would
398 * expect: keep the point in the center of the #AdgCanvas at the
399 * center of the new #AdgGtkArea.
401 * The scaling is not that easy, though. The drawing is scaled up to
402 * the edges of the widget. Anyway, if the model matrix changes (that is,
403 * if the model is translated or scaled), the algorithm behaves as if
404 * the model matrix was not changed.
406 static void
407 _adg_size_allocate(GtkWidget *widget, GtkAllocation *allocation)
409 AdgGtkArea *area;
410 AdgGtkAreaPrivate *data;
411 AdgCanvas *canvas;
412 AdgEntity *entity;
413 GtkAllocation *src_allocation;
414 AdgPair ratio;
415 gdouble factor;
416 AdgMatrix map;
418 if (_ADG_OLD_WIDGET_CLASS->size_allocate)
419 _ADG_OLD_WIDGET_CLASS->size_allocate(widget, allocation);
421 area = (AdgGtkArea *) widget;
422 data = area->data;
423 canvas = data->canvas;
425 if (canvas == NULL)
426 return;
428 /* Check if the allocated space is enough:
429 * if not, there is no much we can do... */
430 g_return_if_fail(allocation->width > 0 && allocation->height > 0);
432 entity = (AdgEntity *) canvas;
433 src_allocation = &data->src_allocation;
435 adg_matrix_copy(&map, adg_entity_get_global_map(entity));
437 if (data->src_factor <= 0) {
438 /* First allocation */
439 const CpmlExtents *extents = _adg_get_extents(area);
441 if (extents == NULL || !extents->is_defined)
442 return;
444 if (extents->size.x <= 0 || extents->size.y <= 0)
445 return;
447 ratio.x = (gdouble) allocation->width / extents->size.x;
448 ratio.y = (gdouble) allocation->height / extents->size.y;
449 factor = MIN(ratio.x, ratio.y);
451 /* TODO: check the following formula, especially
452 * the final translation */
453 map.x0 = allocation->width - extents->size.x * factor;
454 map.y0 = allocation->height - extents->size.y * factor;
455 map.x0 /= 2;
456 map.y0 /= 2;
457 map.x0 -= extents->org.x;
458 map.y0 -= extents->org.y;
460 *src_allocation = *allocation;
461 src_allocation->x = map.x0;
462 src_allocation->y = map.y0;
463 data->src_factor = factor;
464 } else {
465 /* Scaling with reference to the first allocation */
466 ratio.x = data->src_factor * allocation->width / src_allocation->width;
467 ratio.y = data->src_factor * allocation->height / src_allocation->height;
469 factor = MIN(ratio.x, ratio.y);
471 map.x0 = src_allocation->x * factor +
472 (allocation->width - src_allocation->width * factor) / 2;
473 map.y0 = src_allocation->y * factor +
474 (allocation->height - src_allocation->height * factor) / 2;
477 map.xx = map.yy = factor;
478 adg_entity_set_global_map(entity, &map);
481 static gboolean
482 _adg_expose_event(GtkWidget *widget, GdkEventExpose *event)
484 AdgGtkAreaPrivate *data;
485 AdgCanvas *canvas;
487 data = ((AdgGtkArea *) widget)->data;
488 canvas = data->canvas;
490 if (canvas && event->window) {
491 cairo_t *cr = gdk_cairo_create(event->window);
492 adg_entity_render((AdgEntity *) canvas, cr);
493 cairo_destroy(cr);
496 if (_ADG_OLD_WIDGET_CLASS->expose_event == NULL)
497 return FALSE;
499 return _ADG_OLD_WIDGET_CLASS->expose_event(widget, event);
502 static gboolean
503 _adg_scroll_event(GtkWidget *widget, GdkEventScroll *event)
505 gboolean zoom_in, zoom_out, local_space, global_space;
506 AdgMatrix map, inverted;
507 AdgGtkAreaPrivate *data;
508 double factor, x, y;
510 zoom_in = event->direction == GDK_SCROLL_UP;
511 zoom_out = event->direction == GDK_SCROLL_DOWN;
512 local_space = (event->state & ADG_GTK_MODIFIERS) == 0;
513 global_space = (event->state & ADG_GTK_MODIFIERS) == GDK_SHIFT_MASK;
515 if ((zoom_in || zoom_out) && (local_space || global_space) &&
516 _adg_get_map(widget, local_space, &map, &inverted)) {
517 data = ((AdgGtkArea *) widget)->data;
518 factor = zoom_in ? data->factor : 1. / data->factor;
519 x = event->x;
520 y = event->y;
522 cairo_matrix_transform_point(&inverted, &x, &y);
523 cairo_matrix_scale(&map, factor, factor);
524 cairo_matrix_translate(&map, x/factor - x, y/factor - y);
526 _adg_set_map(widget, local_space, &map);
528 gtk_widget_queue_draw(widget);
531 if (_ADG_OLD_WIDGET_CLASS->scroll_event == NULL)
532 return FALSE;
534 return _ADG_OLD_WIDGET_CLASS->scroll_event(widget, event);
537 static gboolean
538 _adg_button_press_event(GtkWidget *widget, GdkEventButton *event)
540 AdgGtkAreaPrivate *data = ((AdgGtkArea *) widget)->data;
542 if (event->type == GDK_BUTTON_PRESS && event->button == 2) {
543 /* Remember the starting coordinates of a (probable) translation */
544 data->x_event = event->x;
545 data->y_event = event->y;
548 if (_ADG_OLD_WIDGET_CLASS->button_press_event == NULL)
549 return FALSE;
551 return _ADG_OLD_WIDGET_CLASS->button_press_event(widget, event);
554 static gboolean
555 _adg_motion_notify_event(GtkWidget *widget, GdkEventMotion *event)
557 gboolean translating, local_space, global_space;
558 AdgMatrix map, inverted;
559 AdgGtkAreaPrivate *data;
560 double x, y;
562 translating = (event->state & GDK_BUTTON2_MASK) == GDK_BUTTON2_MASK;
563 local_space = (event->state & ADG_GTK_MODIFIERS) == 0;
564 global_space = (event->state & ADG_GTK_MODIFIERS) == GDK_SHIFT_MASK;
566 if (translating && (local_space || global_space) &&
567 _adg_get_map(widget, local_space, &map, &inverted)) {
568 data = ((AdgGtkArea *) widget)->data;
569 x = event->x - data->x_event;
570 y = event->y - data->y_event;
572 cairo_matrix_transform_distance(&inverted, &x, &y);
573 cairo_matrix_translate(&map, x, y);
574 data->x_event = event->x;
575 data->y_event = event->y;
577 _adg_set_map(widget, local_space, &map);
579 gtk_widget_queue_draw(widget);
582 if (_ADG_OLD_WIDGET_CLASS->motion_notify_event == NULL)
583 return FALSE;
585 return _ADG_OLD_WIDGET_CLASS->motion_notify_event(widget, event);
588 static gboolean
589 _adg_get_map(GtkWidget *widget, gboolean local_space,
590 AdgMatrix *map, AdgMatrix *inverted)
592 AdgGtkAreaPrivate *data;
593 AdgEntity *entity;
595 data = ((AdgGtkArea *) widget)->data;
596 entity = (AdgEntity *) data->canvas;
597 if (entity == NULL)
598 return FALSE;
600 if (local_space) {
601 adg_matrix_copy(map, adg_entity_get_local_map(entity));
603 /* The inverted map is subject to the global matrix */
604 adg_matrix_copy(inverted, adg_entity_get_global_matrix(entity));
605 adg_matrix_transform(inverted, map, ADG_TRANSFORM_BEFORE);
606 } else {
607 adg_matrix_copy(map, adg_entity_get_global_map(entity));
608 adg_matrix_copy(inverted, map);
611 return cairo_matrix_invert(inverted) == CAIRO_STATUS_SUCCESS;
614 static void
615 _adg_set_map(GtkWidget *widget, gboolean local_space, const AdgMatrix *map)
617 AdgGtkAreaPrivate *data;
618 AdgEntity *entity;
620 data = ((AdgGtkArea *) widget)->data;
621 entity = (AdgEntity *) data->canvas;
623 if (entity == NULL)
624 return;
626 if (local_space)
627 adg_entity_set_local_map(entity, map);
628 else
629 adg_entity_set_global_map(entity, map);
632 static const CpmlExtents *
633 _adg_get_extents(AdgGtkArea *area)
635 AdgGtkAreaPrivate *data;
636 AdgCanvas *canvas;
637 AdgEntity *entity;
638 const CpmlExtents *extents;
639 gdouble top, right, bottom, left;
641 data = area->data;
642 data->extents.is_defined = FALSE;
644 canvas = data->canvas;
645 if (!ADG_IS_CANVAS(canvas))
646 return NULL;
648 entity = (AdgEntity *) canvas;
650 adg_entity_arrange(entity);
652 extents = adg_entity_get_extents(entity);
653 if (extents == NULL || !extents->is_defined)
654 return NULL;
656 top = adg_canvas_get_top_margin(canvas);
657 right = adg_canvas_get_right_margin(canvas);
658 bottom = adg_canvas_get_bottom_margin(canvas);
659 left = adg_canvas_get_left_margin(canvas);
661 data->extents = *extents;
662 data->extents.org.x -= left;
663 data->extents.org.y -= top;
664 data->extents.size.x += left + right;
665 data->extents.size.y += top + bottom;
666 data->extents.is_defined = TRUE;
668 return &data->extents;