aboutsummaryrefslogtreecommitdiff
path: root/liblogdiag/ld-canvas.c
diff options
context:
space:
mode:
authorPřemysl Janouch <p.janouch@gmail.com>2011-01-10 16:49:13 +0100
committerPřemysl Janouch <p.janouch@gmail.com>2011-01-10 17:07:02 +0100
commit616c49a5053830a5e0a31c71fd6114926e43235f (patch)
tree8a21f60862a86d5fb2faf5ed7fd70aa7a2ce69d5 /liblogdiag/ld-canvas.c
parent63b36a2b5b8e04f5d96fa9aa8d212a01c73aad49 (diff)
downloadlogdiag-616c49a5053830a5e0a31c71fd6114926e43235f.tar.gz
logdiag-616c49a5053830a5e0a31c71fd6114926e43235f.tar.xz
logdiag-616c49a5053830a5e0a31c71fd6114926e43235f.zip
Make a separate library.
This is required for gtkdoc-scangobj. So far it's much like it's been before, the main differences are that source files are in two directories from now on and the build process has two stages.
Diffstat (limited to 'liblogdiag/ld-canvas.c')
-rw-r--r--liblogdiag/ld-canvas.c1417
1 files changed, 1417 insertions, 0 deletions
diff --git a/liblogdiag/ld-canvas.c b/liblogdiag/ld-canvas.c
new file mode 100644
index 0000000..9523d9d
--- /dev/null
+++ b/liblogdiag/ld-canvas.c
@@ -0,0 +1,1417 @@
+/*
+ * ld-canvas.c
+ *
+ * This file is a part of logdiag.
+ * Copyright Přemysl Janouch 2010 - 2011. All rights reserved.
+ *
+ * See the file LICENSE for licensing information.
+ *
+ */
+
+#include <math.h>
+#include <string.h>
+#include <gdk/gdkkeysyms.h>
+
+#include "liblogdiag.h"
+#include "config.h"
+
+
+/**
+ * SECTION:ld-canvas
+ * @short_description: A canvas.
+ * @see_also: #LdDiagram
+ *
+ * #LdCanvas displays and enables the user to manipulate with an #LdDiagram.
+ */
+
+/* Milimetres per inch. */
+#define MM_PER_INCH 25.4
+/* The default screen resolution in DPI units. */
+#define DEFAULT_SCREEN_RESOLUTION 96
+
+/* The maximal, minimal and default values of zoom. */
+#define ZOOM_MIN 0.01
+#define ZOOM_MAX 100
+#define ZOOM_DEFAULT 1
+/* Multiplication factor for zooming with mouse wheel. */
+#define ZOOM_WHEEL_STEP 1.4
+
+/* When drawing is requested, extend all sides of
+ * the rectangle to be drawn by this number of pixels.
+ */
+#define QUEUE_DRAW_EXTEND 3
+/* Cursor tolerance for object borders. */
+#define OBJECT_BORDER_TOLERANCE 3
+/* Tolerance on all sides of symbols for strokes. */
+#define SYMBOL_CLIP_TOLERANCE 5
+
+/* Size of a highlighted terminal. */
+#define TERMINAL_RADIUS 5
+/* Tolerance around terminal points. */
+#define TERMINAL_HOVER_TOLERANCE 8
+
+/*
+ * OperationEnd:
+ *
+ * Called upon ending an operation.
+ */
+typedef void (*OperationEnd) (LdCanvas *self);
+
+enum
+{
+ OPER_0,
+ OPER_ADD_OBJECT
+};
+
+typedef struct _AddObjectData AddObjectData;
+
+struct _AddObjectData
+{
+ LdDiagramObject *object;
+ gboolean visible;
+};
+
+enum
+{
+ COLOR_BASE,
+ COLOR_GRID,
+ COLOR_OBJECT,
+ COLOR_SELECTION,
+ COLOR_TERMINAL,
+ COLOR_COUNT
+};
+
+typedef struct _LdCanvasColor LdCanvasColor;
+
+struct _LdCanvasColor
+{
+ gdouble r;
+ gdouble g;
+ gdouble b;
+ gdouble a;
+};
+
+/*
+ * LdCanvasPrivate:
+ * @diagram: A diagram object assigned to this canvas as a model.
+ * @library: A library object assigned to this canvas as a model.
+ * @adjustment_h: An adjustment object for the horizontal axis, if any.
+ * @adjustment_v: An adjustment object for the vertical axis, if any.
+ * @x: The X coordinate of the center of view.
+ * @y: The Y coordinate of the center of view.
+ * @zoom: The current zoom of the canvas.
+ * @operation: The current operation.
+ * @operation_data: Data related to the current operation.
+ * @operation_end: A callback to end the operation.
+ * @palette: Colors used by the widget.
+ */
+struct _LdCanvasPrivate
+{
+ LdDiagram *diagram;
+ LdLibrary *library;
+
+ GtkAdjustment *adjustment_h;
+ GtkAdjustment *adjustment_v;
+
+ gdouble x;
+ gdouble y;
+ gdouble zoom;
+
+ LdPoint terminal;
+ gboolean terminal_highlighted;
+
+ gint operation;
+ union
+ {
+ AddObjectData add_object;
+ }
+ operation_data;
+ OperationEnd operation_end;
+
+ LdCanvasColor palette[COLOR_COUNT];
+};
+
+#define OPER_DATA(self, member) ((self)->priv->operation_data.member)
+#define COLOR_GET(self, name) (&(self)->priv->palette[name])
+
+/*
+ * DrawData:
+ * @self: Our #LdCanvas.
+ * @cr: A cairo context to draw on.
+ * @exposed_rect: The area that is to be redrawn.
+ * @scale: Computed size of one diagram unit in pixels.
+ */
+typedef struct _DrawData DrawData;
+
+struct _DrawData
+{
+ LdCanvas *self;
+ cairo_t *cr;
+ LdRectangle exposed_rect;
+ gdouble scale;
+};
+
+enum
+{
+ PROP_0,
+ PROP_DIAGRAM,
+ PROP_LIBRARY,
+ PROP_ZOOM
+};
+
+static void ld_canvas_get_property (GObject *object, guint property_id,
+ GValue *value, GParamSpec *pspec);
+static void ld_canvas_set_property (GObject *object, guint property_id,
+ const GValue *value, GParamSpec *pspec);
+static void ld_canvas_finalize (GObject *gobject);
+
+static void ld_canvas_real_set_scroll_adjustments
+ (LdCanvas *self, GtkAdjustment *horizontal, GtkAdjustment *vertical);
+static void on_adjustment_value_changed
+ (GtkAdjustment *adjustment, LdCanvas *self);
+static void on_size_allocate (GtkWidget *widget, GtkAllocation *allocation,
+ gpointer user_data);
+static void update_adjustments (LdCanvas *self);
+
+static void diagram_connect_signals (LdCanvas *self);
+static void diagram_disconnect_signals (LdCanvas *self);
+
+static gdouble ld_canvas_get_base_unit_in_px (GtkWidget *self);
+static gdouble ld_canvas_get_scale_in_px (LdCanvas *self);
+
+static void simulate_motion (LdCanvas *self);
+static gboolean on_motion_notify (GtkWidget *widget, GdkEventMotion *event,
+ gpointer user_data);
+static gboolean on_leave_notify (GtkWidget *widget, GdkEventCrossing *event,
+ gpointer user_data);
+static gboolean on_button_press (GtkWidget *widget, GdkEventButton *event,
+ gpointer user_data);
+static gboolean on_button_release (GtkWidget *widget, GdkEventButton *event,
+ gpointer user_data);
+static gboolean on_scroll (GtkWidget *widget, GdkEventScroll *event,
+ gpointer user_data);
+
+static void ld_canvas_color_set (LdCanvasColor *color,
+ gdouble r, gdouble g, gdouble b, gdouble a);
+static void ld_canvas_color_apply (LdCanvasColor *color, cairo_t *cr);
+
+static void move_object_to_coords (LdCanvas *self, LdDiagramObject *object,
+ gdouble x, gdouble y);
+static LdDiagramObject *get_object_at_coords (LdCanvas *self,
+ gdouble x, gdouble y);
+static gboolean is_object_selected (LdCanvas *self, LdDiagramObject *object);
+static LdSymbol *resolve_diagram_symbol (LdCanvas *self,
+ LdDiagramSymbol *diagram_symbol);
+static gboolean get_symbol_area (LdCanvas *self, LdDiagramSymbol *symbol,
+ LdRectangle *rect);
+static gboolean get_symbol_clip_area (LdCanvas *self, LdDiagramSymbol *symbol,
+ LdRectangle *rect);
+static gboolean get_object_area (LdCanvas *self, LdDiagramObject *object,
+ LdRectangle *rect);
+static gboolean object_hit_test (LdCanvas *self, LdDiagramObject *object,
+ gdouble x, gdouble y);
+static void check_terminals (LdCanvas *self, gdouble x, gdouble y);
+static void hide_terminals (LdCanvas *self);
+static void queue_draw (LdCanvas *self, LdRectangle *rect);
+static void queue_object_draw (LdCanvas *self, LdDiagramObject *object);
+static void queue_terminal_draw (LdCanvas *self, LdPoint *terminal);
+
+static void ld_canvas_real_cancel_operation (LdCanvas *self);
+static void ld_canvas_add_object_end (LdCanvas *self);
+
+static gboolean on_expose_event (GtkWidget *widget, GdkEventExpose *event,
+ gpointer user_data);
+static void draw_grid (GtkWidget *widget, DrawData *data);
+static void draw_diagram (GtkWidget *widget, DrawData *data);
+static void draw_terminal (GtkWidget *widget, DrawData *data);
+static void draw_object (LdDiagramObject *diagram_object, DrawData *data);
+static void draw_symbol (LdDiagramSymbol *diagram_symbol, DrawData *data);
+
+
+G_DEFINE_TYPE (LdCanvas, ld_canvas, GTK_TYPE_DRAWING_AREA);
+
+static void
+ld_canvas_class_init (LdCanvasClass *klass)
+{
+ GObjectClass *object_class;
+ GtkWidgetClass *widget_class;
+ GtkBindingSet *binding_set;
+ GParamSpec *pspec;
+
+ widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class = G_OBJECT_CLASS (klass);
+ object_class->get_property = ld_canvas_get_property;
+ object_class->set_property = ld_canvas_set_property;
+ object_class->finalize = ld_canvas_finalize;
+
+ klass->set_scroll_adjustments = ld_canvas_real_set_scroll_adjustments;
+ klass->cancel_operation = ld_canvas_real_cancel_operation;
+
+ binding_set = gtk_binding_set_by_class (klass);
+ gtk_binding_entry_add_signal (binding_set, GDK_Escape, 0,
+ "cancel-operation", 0);
+
+/**
+ * LdCanvas:diagram:
+ *
+ * The underlying #LdDiagram object of this canvas.
+ */
+ pspec = g_param_spec_object ("diagram", "Diagram",
+ "The underlying diagram object of this canvas.",
+ LD_TYPE_DIAGRAM, G_PARAM_READWRITE);
+ g_object_class_install_property (object_class, PROP_DIAGRAM, pspec);
+
+/**
+ * LdCanvas:library:
+ *
+ * The #LdLibrary that this canvas retrieves symbols from.
+ */
+ pspec = g_param_spec_object ("library", "Library",
+ "The library that this canvas retrieves symbols from.",
+ LD_TYPE_LIBRARY, G_PARAM_READWRITE);
+ g_object_class_install_property (object_class, PROP_LIBRARY, pspec);
+
+/**
+ * LdCanvas:zoom:
+ *
+ * The zoom of this canvas.
+ */
+ pspec = g_param_spec_double ("zoom", "Zoom",
+ "The zoom of this canvas.",
+ ZOOM_MIN, ZOOM_MAX, ZOOM_DEFAULT, G_PARAM_READWRITE);
+ g_object_class_install_property (object_class, PROP_ZOOM, pspec);
+
+/**
+ * LdCanvas::set-scroll-adjustments:
+ * @horizontal: The horizontal #GtkAdjustment.
+ * @vertical: The vertical #GtkAdjustment.
+ *
+ * Set scroll adjustments for the canvas.
+ */
+ widget_class->set_scroll_adjustments_signal = g_signal_new
+ ("set-scroll-adjustments", G_TYPE_FROM_CLASS (widget_class),
+ G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
+ G_STRUCT_OFFSET (LdCanvasClass, set_scroll_adjustments),
+ NULL, NULL,
+ g_cclosure_user_marshal_VOID__OBJECT_OBJECT,
+ G_TYPE_NONE, 2, GTK_TYPE_ADJUSTMENT, GTK_TYPE_ADJUSTMENT);
+
+/**
+ * LdCanvas::cancel-operation:
+ *
+ * Cancel any current operation.
+ */
+ klass->cancel_operation_signal = g_signal_new
+ ("cancel-operation", G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
+ G_STRUCT_OFFSET (LdCanvasClass, cancel_operation), NULL, NULL,
+ g_cclosure_marshal_VOID__VOID,
+ G_TYPE_NONE, 0);
+
+ g_type_class_add_private (klass, sizeof (LdCanvasPrivate));
+}
+
+static void
+ld_canvas_init (LdCanvas *self)
+{
+ self->priv = G_TYPE_INSTANCE_GET_PRIVATE
+ (self, LD_TYPE_CANVAS, LdCanvasPrivate);
+
+ self->priv->x = 0;
+ self->priv->y = 0;
+ self->priv->zoom = ZOOM_DEFAULT;
+
+ ld_canvas_color_set (COLOR_GET (self, COLOR_BASE), 1, 1, 1, 1);
+ ld_canvas_color_set (COLOR_GET (self, COLOR_GRID), 0.5, 0.5, 0.5, 1);
+ ld_canvas_color_set (COLOR_GET (self, COLOR_OBJECT), 0, 0, 0, 1);
+ ld_canvas_color_set (COLOR_GET (self, COLOR_SELECTION), 0, 0, 1, 1);
+ ld_canvas_color_set (COLOR_GET (self, COLOR_TERMINAL), 1, 0.5, 0.5, 1);
+
+ g_signal_connect (self, "size-allocate",
+ G_CALLBACK (on_size_allocate), NULL);
+ g_signal_connect (self, "expose-event",
+ G_CALLBACK (on_expose_event), NULL);
+
+ g_signal_connect (self, "motion-notify-event",
+ G_CALLBACK (on_motion_notify), NULL);
+ g_signal_connect (self, "leave-notify-event",
+ G_CALLBACK (on_leave_notify), NULL);
+ g_signal_connect (self, "button-press-event",
+ G_CALLBACK (on_button_press), NULL);
+ g_signal_connect (self, "button-release-event",
+ G_CALLBACK (on_button_release), NULL);
+ g_signal_connect (self, "scroll-event",
+ G_CALLBACK (on_scroll), NULL);
+
+ g_object_set (self, "can-focus", TRUE, NULL);
+
+ gtk_widget_add_events (GTK_WIDGET (self),
+ GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK
+ | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK
+ | GDK_LEAVE_NOTIFY_MASK);
+}
+
+static void
+ld_canvas_finalize (GObject *gobject)
+{
+ LdCanvas *self;
+
+ self = LD_CANVAS (gobject);
+
+ ld_canvas_real_set_scroll_adjustments (self, NULL, NULL);
+
+ if (self->priv->diagram)
+ {
+ diagram_disconnect_signals (self);
+ g_object_unref (self->priv->diagram);
+ }
+ if (self->priv->library)
+ g_object_unref (self->priv->library);
+
+ /* Chain up to the parent class. */
+ G_OBJECT_CLASS (ld_canvas_parent_class)->finalize (gobject);
+}
+
+static void
+ld_canvas_get_property (GObject *object, guint property_id,
+ GValue *value, GParamSpec *pspec)
+{
+ LdCanvas *self;
+
+ self = LD_CANVAS (object);
+ switch (property_id)
+ {
+ case PROP_DIAGRAM:
+ g_value_set_object (value, ld_canvas_get_diagram (self));
+ break;
+ case PROP_LIBRARY:
+ g_value_set_object (value, ld_canvas_get_library (self));
+ break;
+ case PROP_ZOOM:
+ g_value_set_double (value, ld_canvas_get_zoom (self));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+ }
+}
+
+static void
+ld_canvas_set_property (GObject *object, guint property_id,
+ const GValue *value, GParamSpec *pspec)
+{
+ LdCanvas *self;
+
+ self = LD_CANVAS (object);
+ switch (property_id)
+ {
+ case PROP_DIAGRAM:
+ ld_canvas_set_diagram (self, LD_DIAGRAM (g_value_get_object (value)));
+ break;
+ case PROP_LIBRARY:
+ ld_canvas_set_library (self, LD_LIBRARY (g_value_get_object (value)));
+ break;
+ case PROP_ZOOM:
+ ld_canvas_set_zoom (self, g_value_get_double (value));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+ }
+}
+
+static void
+ld_canvas_real_set_scroll_adjustments (LdCanvas *self,
+ GtkAdjustment *horizontal, GtkAdjustment *vertical)
+{
+ /* TODO: Infinite canvas. */
+ GtkWidget *widget;
+ gdouble scale;
+
+ widget = GTK_WIDGET (self);
+ scale = ld_canvas_get_scale_in_px (self);
+
+ if (horizontal != self->priv->adjustment_h)
+ {
+ if (self->priv->adjustment_h)
+ {
+ g_signal_handlers_disconnect_by_func (self->priv->adjustment_h,
+ on_adjustment_value_changed, self);
+ g_object_unref (self->priv->adjustment_h);
+
+ self->priv->adjustment_h = NULL;
+ }
+ if (horizontal)
+ {
+ g_object_ref (horizontal);
+ g_signal_connect (horizontal, "value-changed",
+ G_CALLBACK (on_adjustment_value_changed), self);
+
+ horizontal->upper = 100;
+ horizontal->lower = -100;
+ horizontal->step_increment = 0.5;
+ horizontal->page_increment = 5;
+ horizontal->page_size = widget->allocation.width / scale;
+ horizontal->value = -horizontal->page_size / 2;
+
+ self->priv->adjustment_h = horizontal;
+ }
+ }
+
+ if (vertical != self->priv->adjustment_v)
+ {
+ if (self->priv->adjustment_v)
+ {
+ g_signal_handlers_disconnect_by_func (self->priv->adjustment_v,
+ on_adjustment_value_changed, self);
+ g_object_unref (self->priv->adjustment_v);
+
+ self->priv->adjustment_v = NULL;
+ }
+ if (vertical)
+ {
+ g_object_ref (vertical);
+ g_signal_connect (vertical, "value-changed",
+ G_CALLBACK (on_adjustment_value_changed), self);
+
+ vertical->upper = 100;
+ vertical->lower = -100;
+ vertical->step_increment = 0.5;
+ vertical->page_increment = 5;
+ vertical->page_size = widget->allocation.height / scale;
+ vertical->value = -vertical->page_size / 2;
+
+ self->priv->adjustment_v = vertical;
+ }
+ }
+}
+
+static void
+on_adjustment_value_changed (GtkAdjustment *adjustment, LdCanvas *self)
+{
+ GtkWidget *widget;
+ gdouble scale;
+
+ widget = GTK_WIDGET (self);
+ scale = ld_canvas_get_scale_in_px (self);
+
+ if (adjustment == self->priv->adjustment_h)
+ {
+ self->priv->x = adjustment->value
+ + widget->allocation.width / scale / 2;
+ gtk_widget_queue_draw (widget);
+ }
+ else if (adjustment == self->priv->adjustment_v)
+ {
+ self->priv->y = adjustment->value
+ + widget->allocation.height / scale / 2;
+ gtk_widget_queue_draw (widget);
+ }
+}
+
+static void
+on_size_allocate (GtkWidget *widget, GtkAllocation *allocation,
+ gpointer user_data)
+{
+ LdCanvas *self;
+
+ self = LD_CANVAS (widget);
+
+ /* FIXME: If the new allocation is bigger, we may see more than
+ * what we're supposed to be able to see -> adjust X and Y.
+ *
+ * If the visible area is so large that we simply must see more,
+ * let's disable the scrollbars in question.
+ */
+ update_adjustments (self);
+}
+
+static void
+update_adjustments (LdCanvas *self)
+{
+ gdouble scale;
+
+ scale = ld_canvas_get_scale_in_px (self);
+
+ if (self->priv->adjustment_h)
+ {
+ self->priv->adjustment_h->page_size
+ = GTK_WIDGET (self)->allocation.width / scale;
+ self->priv->adjustment_h->value
+ = self->priv->x - self->priv->adjustment_h->page_size / 2;
+ gtk_adjustment_changed (self->priv->adjustment_h);
+ }
+ if (self->priv->adjustment_v)
+ {
+ self->priv->adjustment_v->page_size
+ = GTK_WIDGET (self)->allocation.height / scale;
+ self->priv->adjustment_v->value
+ = self->priv->y - self->priv->adjustment_v->page_size / 2;
+ gtk_adjustment_changed (self->priv->adjustment_v);
+ }
+}
+
+
+/* ===== Generic interface etc. ============================================ */
+
+/**
+ * ld_canvas_new:
+ *
+ * Create an instance.
+ */
+LdCanvas *
+ld_canvas_new (void)
+{
+ return g_object_new (LD_TYPE_CANVAS, NULL);
+}
+
+/**
+ * ld_canvas_set_diagram:
+ * @self: An #LdCanvas object.
+ * @diagram: The #LdDiagram to be assigned to the canvas.
+ *
+ * Assign an #LdDiagram object to the canvas.
+ */
+void
+ld_canvas_set_diagram (LdCanvas *self, LdDiagram *diagram)
+{
+ g_return_if_fail (LD_IS_CANVAS (self));
+ g_return_if_fail (LD_IS_DIAGRAM (diagram));
+
+ if (self->priv->diagram)
+ {
+ diagram_disconnect_signals (self);
+ g_object_unref (self->priv->diagram);
+ }
+
+ self->priv->diagram = diagram;
+ diagram_connect_signals (self);
+ g_object_ref (diagram);
+
+ g_object_notify (G_OBJECT (self), "diagram");
+}
+
+/**
+ * ld_canvas_get_diagram:
+ * @self: An #LdCanvas object.
+ *
+ * Get the #LdDiagram object assigned to this canvas.
+ * The reference count on the diagram is not incremented.
+ */
+LdDiagram *
+ld_canvas_get_diagram (LdCanvas *self)
+{
+ g_return_val_if_fail (LD_IS_CANVAS (self), NULL);
+ return self->priv->diagram;
+}
+
+static void
+diagram_connect_signals (LdCanvas *self)
+{
+ g_return_if_fail (LD_IS_DIAGRAM (self->priv->diagram));
+
+ g_signal_connect_swapped (self->priv->diagram, "changed",
+ G_CALLBACK (gtk_widget_queue_draw), self);
+ g_signal_connect_swapped (self->priv->diagram, "selection-changed",
+ G_CALLBACK (gtk_widget_queue_draw), self);
+}
+
+static void
+diagram_disconnect_signals (LdCanvas *self)
+{
+ g_return_if_fail (LD_IS_DIAGRAM (self->priv->diagram));
+
+ g_signal_handlers_disconnect_matched (self->priv->diagram,
+ G_SIGNAL_MATCH_FUNC | G_SIGNAL_MATCH_DATA, 0, 0, NULL,
+ gtk_widget_queue_draw, self);
+}
+
+/**
+ * ld_canvas_set_library:
+ * @self: An #LdCanvas object.
+ * @library: The #LdLibrary to be assigned to the canvas.
+ *
+ * Assign an #LdLibrary object to the canvas.
+ */
+void
+ld_canvas_set_library (LdCanvas *self, LdLibrary *library)
+{
+ g_return_if_fail (LD_IS_CANVAS (self));
+ g_return_if_fail (LD_IS_LIBRARY (library));
+
+ if (self->priv->library)
+ g_object_unref (self->priv->library);
+
+ self->priv->library = library;
+ g_object_ref (library);
+
+ g_object_notify (G_OBJECT (self), "library");
+}
+
+/**
+ * ld_canvas_get_library:
+ * @self: An #LdCanvas object.
+ *
+ * Get the #LdLibrary object assigned to this canvas.
+ * The reference count on the library is not incremented.
+ */
+LdLibrary *
+ld_canvas_get_library (LdCanvas *self)
+{
+ g_return_val_if_fail (LD_IS_CANVAS (self), NULL);
+ return self->priv->library;
+}
+
+/*
+ * ld_canvas_get_base_unit_in_px:
+ * @self: A #GtkWidget object to retrieve DPI from (indirectly).
+ *
+ * Return value: Length of the base unit in pixels.
+ */
+static gdouble
+ld_canvas_get_base_unit_in_px (GtkWidget *self)
+{
+ gdouble resolution;
+
+ g_return_val_if_fail (GTK_IS_WIDGET (self), 1);
+
+ resolution = gdk_screen_get_resolution (gtk_widget_get_screen (self));
+ if (resolution == -1)
+ resolution = DEFAULT_SCREEN_RESOLUTION;
+
+ /* XXX: It might look better if the unit was rounded to a whole number. */
+ return resolution / MM_PER_INCH * LD_CANVAS_BASE_UNIT_LENGTH;
+}
+
+/*
+ * ld_canvas_get_scale_in_px:
+ * @self: An #LdCanvas object.
+ *
+ * Return value: Displayed length of the base unit in pixels.
+ */
+static gdouble
+ld_canvas_get_scale_in_px (LdCanvas *self)
+{
+ g_return_val_if_fail (LD_IS_CANVAS (self), 1);
+
+ return ld_canvas_get_base_unit_in_px (GTK_WIDGET (self))
+ * self->priv->zoom;
+}
+
+/**
+ * ld_canvas_widget_to_diagram_coords:
+ * @self: An #LdCanvas object.
+ * @wx: The X coordinate to be translated.
+ * @wy: The Y coordinate to be translated.
+ * @dx: (out): The translated X coordinate.
+ * @dy: (out): The translated Y coordinate.
+ *
+ * Translate coordinates located inside the canvas window
+ * into diagram coordinates.
+ */
+void
+ld_canvas_widget_to_diagram_coords (LdCanvas *self,
+ gdouble wx, gdouble wy, gdouble *dx, gdouble *dy)
+{
+ GtkWidget *widget;
+ gdouble scale;
+
+ g_return_if_fail (LD_IS_CANVAS (self));
+ g_return_if_fail (dx != NULL);
+ g_return_if_fail (dy != NULL);
+
+ widget = GTK_WIDGET (self);
+ scale = ld_canvas_get_scale_in_px (self);
+
+ /* We know diagram coordinates of the center of the canvas, so we may
+ * translate the given X and Y coordinates to this center and then scale
+ * them by dividing them by the current scale.
+ */
+ *dx = self->priv->x + (wx - (widget->allocation.width * 0.5)) / scale;
+ *dy = self->priv->y + (wy - (widget->allocation.height * 0.5)) / scale;
+}
+
+/**
+ * ld_canvas_diagram_to_widget_coords:
+ * @self: An #LdCanvas object.
+ * @dx: The X coordinate to be translated.
+ * @dy: The Y coordinate to be translated.
+ * @wx: (out): The translated X coordinate.
+ * @wy: (out): The translated Y coordinate.
+ *
+ * Translate diagram coordinates into canvas coordinates.
+ */
+void
+ld_canvas_diagram_to_widget_coords (LdCanvas *self,
+ gdouble dx, gdouble dy, gdouble *wx, gdouble *wy)
+{
+ GtkWidget *widget;
+ gdouble scale;
+
+ g_return_if_fail (LD_IS_CANVAS (self));
+ g_return_if_fail (wx != NULL);
+ g_return_if_fail (wy != NULL);
+
+ widget = GTK_WIDGET (self);
+ scale = ld_canvas_get_scale_in_px (self);
+
+ /* Just the reversal of ld_canvas_widget_to_diagram_coords(). */
+ *wx = scale * (dx - self->priv->x) + 0.5 * widget->allocation.width;
+ *wy = scale * (dy - self->priv->y) + 0.5 * widget->allocation.height;
+}
+
+/**
+ * ld_canvas_get_zoom:
+ * @self: An #LdCanvas object.
+ *
+ * Return value: Zoom of the canvas.
+ */
+gdouble
+ld_canvas_get_zoom (LdCanvas *self)
+{
+ g_return_val_if_fail (LD_IS_CANVAS (self), -1);
+ return self->priv->zoom;
+}
+
+/**
+ * ld_canvas_set_zoom:
+ * @self: An #LdCanvas object.
+ * @zoom: The zoom.
+ *
+ * Set zoom of the canvas.
+ */
+void
+ld_canvas_set_zoom (LdCanvas *self, gdouble zoom)
+{
+ gdouble clamped_zoom;
+
+ g_return_if_fail (LD_IS_CANVAS (self));
+
+ clamped_zoom = CLAMP (zoom, ZOOM_MIN, ZOOM_MAX);
+ if (self->priv->zoom == clamped_zoom)
+ return;
+
+ self->priv->zoom = clamped_zoom;
+
+ simulate_motion (self);
+ update_adjustments (self);
+ gtk_widget_queue_draw (GTK_WIDGET (self));
+
+ g_object_notify (G_OBJECT (self), "zoom");
+}
+
+
+/* ===== Operations ======================================================== */
+
+static void
+ld_canvas_real_cancel_operation (LdCanvas *self)
+{
+ g_return_if_fail (LD_IS_CANVAS (self));
+
+ if (self->priv->operation)
+ {
+ if (self->priv->operation_end)
+ self->priv->operation_end (self);
+ self->priv->operation = OPER_0;
+ self->priv->operation_end = NULL;
+ }
+}
+
+/**
+ * ld_canvas_add_object_begin:
+ * @self: An #LdCanvas object.
+ * @object: (transfer full): The object to be added to the diagram.
+ *
+ * Begin an operation for adding an object into the diagram.
+ */
+void
+ld_canvas_add_object_begin (LdCanvas *self, LdDiagramObject *object)
+{
+ AddObjectData *data;
+
+ g_return_if_fail (LD_IS_CANVAS (self));
+ g_return_if_fail (LD_IS_DIAGRAM_OBJECT (object));
+
+ ld_canvas_real_cancel_operation (self);
+
+ self->priv->operation = OPER_ADD_OBJECT;
+ self->priv->operation_end = ld_canvas_add_object_end;
+
+ data = &OPER_DATA (self, add_object);
+ data->object = object;
+}
+
+static void
+ld_canvas_add_object_end (LdCanvas *self)
+{
+ AddObjectData *data;
+
+ data = &OPER_DATA (self, add_object);
+ if (data->object)
+ {
+ queue_object_draw (self, data->object);
+ g_object_unref (data->object);
+ data->object = NULL;
+ }
+}
+
+
+/* ===== Events, rendering ================================================= */
+
+static void
+ld_canvas_color_set (LdCanvasColor *color,
+ gdouble r, gdouble g, gdouble b, gdouble a)
+{
+ color->r = r;
+ color->g = g;
+ color->b = b;
+ color->a = a;
+}
+
+static void
+ld_canvas_color_apply (LdCanvasColor *color, cairo_t *cr)
+{
+ cairo_set_source_rgba (cr, color->r, color->g, color->b, color->a);
+}
+
+static void
+move_object_to_coords (LdCanvas *self, LdDiagramObject *object,
+ gdouble x, gdouble y)
+{
+ gdouble dx, dy;
+
+ ld_canvas_widget_to_diagram_coords (self, x, y, &dx, &dy);
+ ld_diagram_object_set_x (object, floor (dx + 0.5));
+ ld_diagram_object_set_y (object, floor (dy + 0.5));
+}
+
+static LdDiagramObject *
+get_object_at_coords (LdCanvas *self, gdouble x, gdouble y)
+{
+ GList *objects, *iter;
+
+ /* Iterate from the top object downwards. */
+ objects = (GList *) ld_diagram_get_objects (self->priv->diagram);
+ for (iter = objects; iter; iter = g_list_next (iter))
+ {
+ LdDiagramObject *object;
+
+ object = LD_DIAGRAM_OBJECT (iter->data);
+ if (object_hit_test (self, object, x, y))
+ return object;
+ }
+ return NULL;
+}
+
+static gboolean
+is_object_selected (LdCanvas *self, LdDiagramObject *object)
+{
+ return g_list_find (ld_diagram_get_selection (self->priv->diagram),
+ object) != NULL;
+}
+
+static LdSymbol *
+resolve_diagram_symbol (LdCanvas *self, LdDiagramSymbol *diagram_symbol)
+{
+ if (!self->priv->library)
+ return NULL;
+
+ return ld_library_find_symbol (self->priv->library,
+ ld_diagram_symbol_get_class (diagram_symbol));
+}
+
+static gboolean
+get_symbol_area (LdCanvas *self, LdDiagramSymbol *symbol, LdRectangle *rect)
+{
+ LdDiagramObject *object;
+ gdouble object_x, object_y;
+ LdSymbol *library_symbol;
+ LdRectangle area;
+ gdouble x1, x2;
+ gdouble y1, y2;
+
+ object = LD_DIAGRAM_OBJECT (symbol);
+ object_x = ld_diagram_object_get_x (object);
+ object_y = ld_diagram_object_get_y (object);
+
+ library_symbol = resolve_diagram_symbol (self, symbol);
+ if (library_symbol)
+ ld_symbol_get_area (library_symbol, &area);
+ else
+ return FALSE;
+
+ /* TODO: Rotate the rectangle for other orientations. */
+ ld_canvas_diagram_to_widget_coords (self,
+ object_x + area.x,
+ object_y + area.y,
+ &x1, &y1);
+ ld_canvas_diagram_to_widget_coords (self,
+ object_x + area.x + area.width,
+ object_y + area.y + area.height,
+ &x2, &y2);
+
+ rect->x = x1;
+ rect->y = y1;
+ rect->width = x2 - x1;
+ rect->height = y2 - y1;
+ return TRUE;
+}
+
+static gboolean
+get_symbol_clip_area (LdCanvas *self, LdDiagramSymbol *symbol,
+ LdRectangle *rect)
+{
+ LdRectangle object_rect;
+
+ if (!get_object_area (self, LD_DIAGRAM_OBJECT (symbol), &object_rect))
+ return FALSE;
+
+ *rect = object_rect;
+ ld_rectangle_extend (rect, SYMBOL_CLIP_TOLERANCE);
+ return TRUE;
+}
+
+static gboolean
+get_object_area (LdCanvas *self, LdDiagramObject *object, LdRectangle *rect)
+{
+ if (LD_IS_DIAGRAM_SYMBOL (object))
+ return get_symbol_area (self, LD_DIAGRAM_SYMBOL (object), rect);
+ return FALSE;
+}
+
+static gboolean
+object_hit_test (LdCanvas *self, LdDiagramObject *object, gdouble x, gdouble y)
+{
+ LdRectangle rect;
+
+ if (!get_object_area (self, object, &rect))
+ return FALSE;
+ ld_rectangle_extend (&rect, OBJECT_BORDER_TOLERANCE);
+ return ld_rectangle_contains (&rect, x, y);
+}
+
+static void
+check_terminals (LdCanvas *self, gdouble x, gdouble y)
+{
+ GList *objects, *iter;
+ LdDiagramSymbol *closest_symbol = NULL;
+ gdouble closest_distance = TERMINAL_HOVER_TOLERANCE;
+ LdPoint closest_terminal;
+
+ objects = (GList *) ld_diagram_get_objects (self->priv->diagram);
+ for (iter = objects; iter; iter = g_list_next (iter))
+ {
+ LdDiagramObject *diagram_object;
+ gdouble object_x, object_y;
+ LdDiagramSymbol *diagram_symbol;
+ LdSymbol *symbol;
+ const LdPointArray *terminals;
+ gint i;
+
+ if (!LD_IS_DIAGRAM_SYMBOL (iter->data))
+ continue;
+
+ diagram_symbol = LD_DIAGRAM_SYMBOL (iter->data);
+ symbol = resolve_diagram_symbol (self, diagram_symbol);
+ if (!symbol)
+ continue;
+
+ diagram_object = LD_DIAGRAM_OBJECT (iter->data);
+ object_x = ld_diagram_object_get_x (diagram_object);
+ object_y = ld_diagram_object_get_y (diagram_object);
+
+ terminals = ld_symbol_get_terminals (symbol);
+
+ for (i = 0; i < terminals->num_points; i++)
+ {
+ LdPoint cur_term;
+ gdouble distance;
+
+ cur_term = terminals->points[i];
+ cur_term.x += object_x;
+ cur_term.y += object_y;
+ ld_canvas_diagram_to_widget_coords (self,
+ cur_term.x, cur_term.y, &cur_term.x, &cur_term.y);
+
+ distance = ld_point_distance (&cur_term, x, y);
+ if (distance <= closest_distance)
+ {
+ closest_symbol = diagram_symbol;
+ closest_distance = distance;
+ closest_terminal = cur_term;
+ }
+ }
+ }
+
+ hide_terminals (self);
+
+ if (closest_symbol)
+ {
+ self->priv->terminal_highlighted = TRUE;
+ self->priv->terminal = closest_terminal;
+ queue_terminal_draw (self, &closest_terminal);
+ }
+}
+
+static void
+hide_terminals (LdCanvas *self)
+{
+ if (self->priv->terminal_highlighted)
+ {
+ self->priv->terminal_highlighted = FALSE;
+ queue_terminal_draw (self, &self->priv->terminal);
+ }
+}
+
+static void
+queue_draw (LdCanvas *self, LdRectangle *rect)
+{
+ LdRectangle area;
+
+ area = *rect;
+ ld_rectangle_extend (&area, QUEUE_DRAW_EXTEND);
+ gtk_widget_queue_draw_area (GTK_WIDGET (self),
+ area.x, area.y, area.width, area.height);
+}
+
+static void
+queue_object_draw (LdCanvas *self, LdDiagramObject *object)
+{
+ if (LD_IS_DIAGRAM_SYMBOL (object))
+ {
+ LdRectangle rect;
+
+ if (!get_symbol_clip_area (self, LD_DIAGRAM_SYMBOL (object), &rect))
+ return;
+ queue_draw (self, &rect);
+ }
+}
+
+static void
+queue_terminal_draw (LdCanvas *self, LdPoint *terminal)
+{
+ LdRectangle rect;
+
+ rect.x = terminal->x - TERMINAL_RADIUS;
+ rect.y = terminal->y - TERMINAL_RADIUS;
+ rect.width = 2 * TERMINAL_RADIUS;
+ rect.height = 2 * TERMINAL_RADIUS;
+ queue_draw (self, &rect);
+}
+
+static void
+simulate_motion (LdCanvas *self)
+{
+ GdkEventMotion event;
+ GtkWidget *widget;
+ gint x, y;
+ GdkModifierType state;
+
+ widget = GTK_WIDGET (self);
+
+ if (gdk_window_get_pointer (widget->window, &x, &y, &state)
+ != widget->window)
+ return;
+
+ memset (&event, 0, sizeof (event));
+ event.type = GDK_MOTION_NOTIFY;
+ event.window = widget->window;
+ event.x = x;
+ event.y = y;
+ event.state = state;
+
+ on_motion_notify (widget, &event, NULL);
+}
+
+static gboolean
+on_motion_notify (GtkWidget *widget, GdkEventMotion *event, gpointer user_data)
+{
+ LdCanvas *self;
+
+ self = LD_CANVAS (widget);
+ switch (self->priv->operation)
+ {
+ AddObjectData *data;
+
+ case OPER_ADD_OBJECT:
+ data = &OPER_DATA (self, add_object);
+ data->visible = TRUE;
+
+ queue_object_draw (self, data->object);
+ move_object_to_coords (self, data->object, event->x, event->y);
+ queue_object_draw (self, data->object);
+ break;
+ case OPER_0:
+ check_terminals (self, event->x, event->y);
+ break;
+ }
+ return FALSE;
+}
+
+static gboolean
+on_leave_notify (GtkWidget *widget, GdkEventCrossing *event, gpointer user_data)
+{
+ LdCanvas *self;
+
+ self = LD_CANVAS (widget);
+ switch (self->priv->operation)
+ {
+ AddObjectData *data;
+
+ case OPER_ADD_OBJECT:
+ data = &OPER_DATA (self, add_object);
+ data->visible = FALSE;
+
+ queue_object_draw (self, data->object);
+ break;
+ }
+ return FALSE;
+}
+
+static gboolean
+on_button_press (GtkWidget *widget, GdkEventButton *event, gpointer user_data)
+{
+ LdCanvas *self;
+
+ if (!gtk_widget_has_focus (widget))
+ gtk_widget_grab_focus (widget);
+
+ self = LD_CANVAS (widget);
+ switch (self->priv->operation)
+ {
+ AddObjectData *data;
+
+ case OPER_ADD_OBJECT:
+ data = &OPER_DATA (self, add_object);
+
+ queue_object_draw (self, data->object);
+ move_object_to_coords (self, data->object, event->x, event->y);
+
+ if (self->priv->diagram)
+ ld_diagram_insert_object (self->priv->diagram, data->object, 0);
+
+ /* XXX: "cancel" causes confusion. */
+ ld_canvas_real_cancel_operation (self);
+ break;
+ case OPER_0:
+ if (self->priv->diagram)
+ {
+ LdDiagramObject *object;
+
+ if (event->state != GDK_SHIFT_MASK)
+ ld_diagram_unselect_all (self->priv->diagram);
+
+ object = get_object_at_coords (self, event->x, event->y);
+ if (object)
+ ld_diagram_selection_add (self->priv->diagram, object, 0);
+ }
+ break;
+ }
+ return FALSE;
+}
+
+static gboolean
+on_button_release (GtkWidget *widget, GdkEventButton *event, gpointer user_data)
+{
+ return FALSE;
+}
+
+static gboolean
+on_scroll (GtkWidget *widget, GdkEventScroll *event, gpointer user_data)
+{
+ gdouble prev_x, prev_y;
+ gdouble new_x, new_y;
+ LdCanvas *self;
+
+ self = LD_CANVAS (widget);
+
+ ld_canvas_widget_to_diagram_coords (self,
+ event->x, event->y, &prev_x, &prev_y);
+
+ switch (event->direction)
+ {
+ case GDK_SCROLL_UP:
+ ld_canvas_set_zoom (self, self->priv->zoom * ZOOM_WHEEL_STEP);
+ break;
+ case GDK_SCROLL_DOWN:
+ ld_canvas_set_zoom (self, self->priv->zoom / ZOOM_WHEEL_STEP);
+ break;
+ default:
+ return FALSE;
+ }
+
+ ld_canvas_widget_to_diagram_coords (self,
+ event->x, event->y, &new_x, &new_y);
+
+ /* Focus on the point under the cursor. */
+ self->priv->x += prev_x - new_x;
+ self->priv->y += prev_y - new_y;
+
+ check_terminals (self, event->x, event->y);
+ return TRUE;
+}
+
+static gboolean
+on_expose_event (GtkWidget *widget, GdkEventExpose *event, gpointer user_data)
+{
+ DrawData data;
+
+ data.cr = gdk_cairo_create (widget->window);
+ data.self = LD_CANVAS (widget);
+ data.scale = ld_canvas_get_scale_in_px (data.self);
+ data.exposed_rect.x = event->area.x;
+ data.exposed_rect.y = event->area.y;
+ data.exposed_rect.width = event->area.width;
+ data.exposed_rect.height = event->area.height;
+
+ gdk_cairo_rectangle (data.cr, &event->area);
+ cairo_clip (data.cr);
+
+ ld_canvas_color_apply (COLOR_GET (data.self, COLOR_BASE), data.cr);
+ cairo_paint (data.cr);
+
+ draw_grid (widget, &data);
+ draw_diagram (widget, &data);
+ draw_terminal (widget, &data);
+
+ cairo_destroy (data.cr);
+ return FALSE;
+}
+
+static void
+draw_grid (GtkWidget *widget, DrawData *data)
+{
+ gdouble grid_step;
+ gdouble x_init, y_init;
+ gdouble x, y;
+
+ grid_step = data->scale;
+ while (grid_step < 5)
+ grid_step *= 5;
+
+ ld_canvas_color_apply (COLOR_GET (data->self, COLOR_GRID), data->cr);
+ cairo_set_line_width (data->cr, 1);
+ cairo_set_line_cap (data->cr, CAIRO_LINE_CAP_ROUND);
+
+ /* Get coordinates of the top-left point. */
+ ld_canvas_widget_to_diagram_coords (data->self,
+ data->exposed_rect.x, data->exposed_rect.y, &x_init, &y_init);
+ ld_canvas_diagram_to_widget_coords (data->self,
+ ceil (x_init), ceil (y_init), &x_init, &y_init);
+
+ /* Iterate over all the points. */
+ for (x = x_init; x <= data->exposed_rect.x + data->exposed_rect.width;
+ x += grid_step)
+ {
+ for (y = y_init; y <= data->exposed_rect.y + data->exposed_rect.height;
+ y += grid_step)
+ {
+ cairo_move_to (data->cr, x, y);
+ cairo_line_to (data->cr, x, y);
+ }
+ }
+ cairo_stroke (data->cr);
+}
+
+static void
+draw_terminal (GtkWidget *widget, DrawData *data)
+{
+ LdCanvasPrivate *priv;
+
+ priv = data->self->priv;
+ if (!priv->terminal_highlighted)
+ return;
+
+ ld_canvas_color_apply (COLOR_GET (data->self, COLOR_TERMINAL), data->cr);
+ cairo_set_line_width (data->cr, 1);
+
+ cairo_new_path (data->cr);
+ cairo_arc (data->cr, priv->terminal.x, priv->terminal.y,
+ TERMINAL_RADIUS, 0, 2 * G_PI);
+ cairo_stroke (data->cr);
+}
+
+static void
+draw_diagram (GtkWidget *widget, DrawData *data)
+{
+ GList *objects, *iter;
+
+ if (!data->self->priv->diagram)
+ return;
+
+ cairo_save (data->cr);
+ cairo_set_line_width (data->cr, 1 / data->scale);
+
+ /* Draw objects from the diagram, from bottom to top. */
+ objects = (GList *) ld_diagram_get_objects (data->self->priv->diagram);
+ for (iter = g_list_last (objects); iter; iter = g_list_previous (iter))
+ draw_object (LD_DIAGRAM_OBJECT (iter->data), data);
+
+ switch (data->self->priv->operation)
+ {
+ AddObjectData *op_data;
+
+ case OPER_ADD_OBJECT:
+ op_data = &OPER_DATA (data->self, add_object);
+ if (op_data->visible)
+ draw_object (op_data->object, data);
+ break;
+ }
+
+ cairo_restore (data->cr);
+}
+
+static void
+draw_object (LdDiagramObject *diagram_object, DrawData *data)
+{
+ g_return_if_fail (LD_IS_DIAGRAM_OBJECT (diagram_object));
+ g_return_if_fail (data != NULL);
+
+ if (is_object_selected (data->self, diagram_object))
+ ld_canvas_color_apply (COLOR_GET (data->self,
+ COLOR_SELECTION), data->cr);
+ else
+ ld_canvas_color_apply (COLOR_GET (data->self,
+ COLOR_OBJECT), data->cr);
+
+ if (LD_IS_DIAGRAM_SYMBOL (diagram_object))
+ draw_symbol (LD_DIAGRAM_SYMBOL (diagram_object), data);
+}
+
+static void
+draw_symbol (LdDiagramSymbol *diagram_symbol, DrawData *data)
+{
+ LdSymbol *symbol;
+ LdRectangle clip_rect;
+ gdouble x, y;
+
+ symbol = resolve_diagram_symbol (data->self, diagram_symbol);
+
+ /* TODO: Resolve this better; draw a cross or whatever. */
+ if (!symbol)
+ {
+ g_warning ("Cannot find symbol %s in the library.",
+ ld_diagram_symbol_get_class (diagram_symbol));
+ return;
+ }
+
+ if (!get_symbol_clip_area (data->self, diagram_symbol, &clip_rect)
+ || !ld_rectangle_intersects (&clip_rect, &data->exposed_rect))
+ return;
+
+ cairo_save (data->cr);
+
+ cairo_rectangle (data->cr, clip_rect.x, clip_rect.y,
+ clip_rect.width, clip_rect.height);
+ cairo_clip (data->cr);
+
+ /* TODO: Rotate the space for other orientations. */
+ ld_canvas_diagram_to_widget_coords (data->self,
+ ld_diagram_object_get_x (LD_DIAGRAM_OBJECT (diagram_symbol)),
+ ld_diagram_object_get_y (LD_DIAGRAM_OBJECT (diagram_symbol)),
+ &x, &y);
+ cairo_translate (data->cr, x, y);
+ cairo_scale (data->cr, data->scale, data->scale);
+ ld_symbol_draw (symbol, data->cr);
+
+ cairo_restore (data->cr);
+}