logdiag/liblogdiag/ld-canvas.c

1476 lines
37 KiB
C

/*
* 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 ld_canvas_real_move (LdCanvas *self, gdouble dx, gdouble dy);
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;
klass->move = ld_canvas_real_move;
binding_set = gtk_binding_set_by_class (klass);
gtk_binding_entry_add_signal (binding_set, GDK_Escape, 0,
"cancel-operation", 0);
gtk_binding_entry_add_signal (binding_set, GDK_Left, 0,
"move", 2, G_TYPE_DOUBLE, (gdouble) -1, G_TYPE_DOUBLE, (gdouble) 0);
gtk_binding_entry_add_signal (binding_set, GDK_Right, 0,
"move", 2, G_TYPE_DOUBLE, (gdouble) 1, G_TYPE_DOUBLE, (gdouble) 0);
gtk_binding_entry_add_signal (binding_set, GDK_Up, 0,
"move", 2, G_TYPE_DOUBLE, (gdouble) 0, G_TYPE_DOUBLE, (gdouble) -1);
gtk_binding_entry_add_signal (binding_set, GDK_Down, 0,
"move", 2, G_TYPE_DOUBLE, (gdouble) 0, G_TYPE_DOUBLE, (gdouble) 1);
/**
* 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:
* @self: an #LdCanvas object.
* @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,
ld_marshal_VOID__OBJECT_OBJECT,
G_TYPE_NONE, 2, GTK_TYPE_ADJUSTMENT, GTK_TYPE_ADJUSTMENT);
/**
* LdCanvas::cancel-operation:
* @self: an #LdCanvas object.
*
* 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);
/**
* LdCanvas::move:
* @self: an #LdCanvas object.
* @dx: The difference by which to move on the horizontal axis.
* @dy: The difference by which to move on the vertical axis.
*
* Move the selection, if any, or the document.
*/
klass->move_signal = g_signal_new
("move", G_TYPE_FROM_CLASS (klass),
G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
G_STRUCT_OFFSET (LdCanvasClass, move), NULL, NULL,
ld_marshal_VOID__DOUBLE_DOUBLE,
G_TYPE_NONE, 2, G_TYPE_DOUBLE, G_TYPE_DOUBLE);
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);
}
}
static void
ld_canvas_real_move (LdCanvas *self, gdouble dx, gdouble dy)
{
LdDiagram *diagram;
GList *selection, *iter;
/* TODO: Check/move boundaries, also implement normal
* getters and setters for priv->x and priv->y.
*/
diagram = self->priv->diagram;
selection = ld_diagram_get_selection (diagram);
if (selection)
{
ld_diagram_begin_user_action (diagram);
for (iter = selection; iter; iter = g_list_next (iter))
{
gdouble x, y;
g_object_get (iter->data, "x", &x, "y", &y, NULL);
x += dx;
y += dy;
g_object_set (iter->data, "x", x, "y", y, NULL);
}
ld_diagram_end_user_action (diagram);
}
else
{
self->priv->x += dx;
self->priv->y += dy;
simulate_motion (self);
update_adjustments (self);
}
gtk_widget_queue_draw (GTK_WIDGET (self));
}
/* ===== Generic interface etc. ============================================ */
/**
* ld_canvas_new:
*
* Create an instance.
*/
GtkWidget *
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);
g_object_set (object, "x", floor (dx + 0.5), "y", floor (dy + 0.5), NULL);
}
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 = g_list_last (objects); iter; iter = g_list_previous (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)
{
gdouble object_x, object_y;
LdSymbol *library_symbol;
LdRectangle area;
gdouble x1, x2;
gdouble y1, y2;
g_object_get (symbol, "x", &object_x, "y", &object_y, NULL);
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);
g_object_get (diagram_object, "x", &object_x, "y", &object_y, NULL);
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, -1);
/* 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_select (self->priv->diagram, object);
}
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 = objects; iter; iter = g_list_next (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);
}