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.
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
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.
48 * All fields are private and should not be used directly.
49 * Use its public methods instead.
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
);
77 static void _adg_dispose (GObject
*object
);
78 static void _adg_get_property (GObject
*object
,
82 static void _adg_set_property (GObject
*object
,
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
,
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 };
112 adg_gtk_area_class_init(AdgGtkAreaClass
*klass
)
114 GObjectClass
*gobject_class
;
115 GtkWidgetClass
*widget_class
;
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",
136 P_("The canvas to be shown"),
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",
143 P_("The factor used in zooming in and out"),
144 1., G_MAXDOUBLE
, 1.05,
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
),
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
),
179 adg_gtk_marshal_VOID__POINTER
,
180 G_TYPE_NONE
, 1, G_TYPE_POINTER
);
184 adg_gtk_area_init(AdgGtkArea
*area
)
186 AdgGtkAreaPrivate
*data
= G_TYPE_INSTANCE_GET_PRIVATE(area
,
195 data
->src_factor
= 0;
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
|
207 _adg_dispose(GObject
*object
)
209 AdgGtkAreaPrivate
*data
= ((AdgGtkArea
*) object
)->data
;
212 g_object_unref(data
->canvas
);
216 if (_ADG_OLD_OBJECT_CLASS
->dispose
)
217 _ADG_OLD_OBJECT_CLASS
->dispose(object
);
221 _adg_get_property(GObject
*object
, guint prop_id
,
222 GValue
*value
, GParamSpec
*pspec
)
224 AdgGtkAreaPrivate
*data
= ((AdgGtkArea
*) object
)->data
;
228 g_value_set_object(value
, data
->canvas
);
231 g_value_set_double(value
, data
->factor
);
234 G_OBJECT_WARN_INVALID_PROPERTY_ID(object
, prop_id
, pspec
);
240 _adg_set_property(GObject
*object
, guint prop_id
,
241 const GValue
*value
, GParamSpec
*pspec
)
244 AdgGtkAreaPrivate
*data
;
247 area
= (AdgGtkArea
*) object
;
252 canvas
= g_value_get_object(value
);
254 g_object_ref(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);
263 data
->factor
= g_value_get_double(value
);
266 G_OBJECT_WARN_INVALID_PROPERTY_ID(object
, prop_id
, pspec
);
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
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
294 * Returns: the newly created widget
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
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
328 adg_gtk_area_get_canvas(AdgGtkArea
*area
)
330 AdgGtkAreaPrivate
*data
;
332 g_return_val_if_fail(ADG_GTK_IS_AREA(area
), NULL
);
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
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
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.
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
392 adg_gtk_area_get_factor(AdgGtkArea
*area
)
394 AdgGtkAreaPrivate
*data
;
396 g_return_val_if_fail(ADG_GTK_IS_AREA(area
), 0.);
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.
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.
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
);
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.
461 _adg_size_allocate(GtkWidget
*widget
, GtkAllocation
*allocation
)
464 AdgGtkAreaPrivate
*data
;
467 GtkAllocation
*src_allocation
;
472 if (_ADG_OLD_WIDGET_CLASS
->size_allocate
)
473 _ADG_OLD_WIDGET_CLASS
->size_allocate(widget
, allocation
);
475 area
= (AdgGtkArea
*) widget
;
477 canvas
= data
->canvas
;
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
)
498 if (extents
->size
.x
<= 0 || extents
->size
.y
<= 0)
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
;
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
;
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
);
536 _adg_expose_event(GtkWidget
*widget
, GdkEventExpose
*event
)
538 AdgGtkAreaPrivate
*data
;
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
);
550 if (_ADG_OLD_WIDGET_CLASS
->expose_event
== NULL
)
553 return _ADG_OLD_WIDGET_CLASS
->expose_event(widget
, event
);
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
;
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
;
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
)
588 return _ADG_OLD_WIDGET_CLASS
->scroll_event(widget
, event
);
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
)
605 return _ADG_OLD_WIDGET_CLASS
->button_press_event(widget
, event
);
609 _adg_motion_notify_event(GtkWidget
*widget
, GdkEventMotion
*event
)
611 gboolean translating
, local_space
, global_space
;
612 AdgMatrix map
, inverted
;
613 AdgGtkAreaPrivate
*data
;
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
)
639 return _ADG_OLD_WIDGET_CLASS
->motion_notify_event(widget
, event
);
643 _adg_get_map(GtkWidget
*widget
, gboolean local_space
,
644 AdgMatrix
*map
, AdgMatrix
*inverted
)
646 AdgGtkAreaPrivate
*data
;
649 data
= ((AdgGtkArea
*) widget
)->data
;
650 entity
= (AdgEntity
*) data
->canvas
;
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
);
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
;
669 _adg_set_map(GtkWidget
*widget
, gboolean local_space
, const AdgMatrix
*map
)
671 AdgGtkAreaPrivate
*data
;
674 data
= ((AdgGtkArea
*) widget
)->data
;
675 entity
= (AdgEntity
*) data
->canvas
;
681 adg_entity_set_local_map(entity
, map
);
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
;
693 CpmlExtents old_extents
;
694 gdouble top
, right
, bottom
, left
;
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
;
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
;