diff options
Diffstat (limited to 'liblogdiag/ld-canvas.c')
-rw-r--r-- | liblogdiag/ld-canvas.c | 1417 |
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); +} |