1 /* This file is part of Ganv.
2  * Copyright 2007-2016 David Robillard <http://drobilla.net>
3  *
4  * Ganv is free software: you can redistribute it and/or modify it under the
5  * terms of the GNU General Public License as published by the Free Software
6  * Foundation, either version 3 of the License, or any later version.
7  *
8  * Ganv is distributed in the hope that it will be useful, but WITHOUT ANY
9  * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
10  * FOR A PARTICULAR PURPOSE.  See the GNU General Public License for details.
11  *
12  * You should have received a copy of the GNU General Public License along
13  * with Ganv.  If not, see <http://www.gnu.org/licenses/>.
14  */
15 
16 #include "boilerplate.h"
17 #include "color.h"
18 #include "ganv-private.h"
19 #include "gettext.h"
20 
21 #include "ganv/canvas.h"
22 #include "ganv/edge.h"
23 #include "ganv/item.h"
24 #include "ganv/node.h"
25 #include "ganv/types.h"
26 
27 #include <cairo.h>
28 #include <glib-object.h>
29 #include <glib.h>
30 #include <gtk/gtk.h>
31 #include <stdarg.h>
32 
33 #include <math.h>
34 #include <string.h>
35 
36 #define ARROW_DEPTH   32
37 #define ARROW_BREADTH 32
38 
39 // Uncomment to see control point path as straight lines
40 //#define GANV_DEBUG_CURVES 1
41 
42 // Uncomment along with GANV_DEBUG_CURVES to see bounding box (buggy)
43 //#define GANV_DEBUG_BOUNDS 1
44 
45 enum {
46 	PROP_0,
47 	PROP_TAIL,
48 	PROP_HEAD,
49 	PROP_WIDTH,
50 	PROP_HANDLE_RADIUS,
51 	PROP_DASH_LENGTH,
52 	PROP_DASH_OFFSET,
53 	PROP_COLOR,
54 	PROP_CONSTRAINING,
55 	PROP_CURVED,
56 	PROP_ARROWHEAD,
57 	PROP_SELECTED,
58 	PROP_HIGHLIGHTED,
59 	PROP_GHOST
60 };
61 
62 G_DEFINE_TYPE_WITH_CODE(GanvEdge, ganv_edge, GANV_TYPE_ITEM,
63                         G_ADD_PRIVATE(GanvEdge))
64 
65 static GanvItemClass* parent_class;
66 
67 static void
ganv_edge_init(GanvEdge * edge)68 ganv_edge_init(GanvEdge* edge)
69 {
70 	GanvEdgePrivate* impl =
71 	    (GanvEdgePrivate*)ganv_edge_get_instance_private(edge);
72 
73 	edge->impl = impl;
74 
75 	impl->tail = NULL;
76 	impl->head = NULL;
77 
78 	memset(&impl->coords, '\0', sizeof(GanvEdgeCoords));
79 	impl->coords.width         = 2.0;
80 	impl->coords.handle_radius = 4.0;
81 	impl->coords.constraining  = TRUE;
82 	impl->coords.curved        = FALSE;
83 	impl->coords.arrowhead     = FALSE;
84 
85 	impl->old_coords  = impl->coords;
86 	impl->dash_length = 0.0;
87 	impl->dash_offset = 0.0;
88 	impl->color       = 0;
89 }
90 
91 static void
ganv_edge_destroy(GtkObject * object)92 ganv_edge_destroy(GtkObject* object)
93 {
94 	g_return_if_fail(object != NULL);
95 	g_return_if_fail(GANV_IS_EDGE(object));
96 
97 	GanvEdge*   edge   = GANV_EDGE(object);
98 	GanvCanvas* canvas = GANV_CANVAS(edge->item.impl->canvas);
99 	if (canvas && !edge->impl->ghost) {
100 		edge->item.impl->canvas = NULL;
101 	}
102 	edge->item.impl->parent = NULL;
103 
104 	if (GTK_OBJECT_CLASS(parent_class)->destroy) {
105 		(*GTK_OBJECT_CLASS(parent_class)->destroy)(object);
106 	}
107 }
108 
109 static void
ganv_edge_set_property(GObject * object,guint prop_id,const GValue * value,GParamSpec * pspec)110 ganv_edge_set_property(GObject*      object,
111                        guint         prop_id,
112                        const GValue* value,
113                        GParamSpec*   pspec)
114 {
115 	g_return_if_fail(object != NULL);
116 	g_return_if_fail(GANV_IS_EDGE(object));
117 
118 	GanvItem*        item   = GANV_ITEM(object);
119 	GanvEdge*        edge   = GANV_EDGE(object);
120 	GanvEdgePrivate* impl   = edge->impl;
121 	GanvEdgeCoords*  coords = &impl->coords;
122 
123 	switch (prop_id) {
124 		SET_CASE(WIDTH, double, coords->width)
125 		SET_CASE(HANDLE_RADIUS, double, coords->handle_radius)
126 		SET_CASE(DASH_LENGTH, double, impl->dash_length)
127 		SET_CASE(DASH_OFFSET, double, impl->dash_offset)
128 		SET_CASE(COLOR, uint, impl->color)
129 		SET_CASE(CONSTRAINING, boolean, impl->coords.constraining)
130 		SET_CASE(CURVED, boolean, impl->coords.curved)
131 		SET_CASE(ARROWHEAD, boolean, impl->coords.arrowhead)
132 		SET_CASE(SELECTED, boolean, impl->selected)
133 		SET_CASE(HIGHLIGHTED, boolean, impl->highlighted)
134 		SET_CASE(GHOST, boolean, impl->ghost)
135 	case PROP_TAIL: {
136 		const gobject tmp = g_value_get_object(value);
137 		if (impl->tail != tmp) {
138 			impl->tail = GANV_NODE(tmp);
139 			ganv_item_request_update(item);
140 		}
141 		break;
142 	}
143 	case PROP_HEAD: {
144 		const gobject tmp = g_value_get_object(value);
145 		if (impl->head != tmp) {
146 			impl->head = GANV_NODE(tmp);
147 			ganv_item_request_update(item);
148 		}
149 		break;
150 	}
151 	default:
152 		G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
153 		break;
154 	}
155 }
156 
157 static void
ganv_edge_get_property(GObject * object,guint prop_id,GValue * value,GParamSpec * pspec)158 ganv_edge_get_property(GObject*    object,
159                        guint       prop_id,
160                        GValue*     value,
161                        GParamSpec* pspec)
162 {
163 	g_return_if_fail(object != NULL);
164 	g_return_if_fail(GANV_IS_EDGE(object));
165 
166 	GanvEdge*        edge = GANV_EDGE(object);
167 	GanvEdgePrivate* impl = edge->impl;
168 
169 	switch (prop_id) {
170 		GET_CASE(TAIL, object, impl->tail)
171 		GET_CASE(HEAD, object, impl->head)
172 		GET_CASE(WIDTH, double, impl->coords.width)
173 		SET_CASE(HANDLE_RADIUS, double, impl->coords.handle_radius)
174 		GET_CASE(DASH_LENGTH, double, impl->dash_length)
175 		GET_CASE(DASH_OFFSET, double, impl->dash_offset)
176 		GET_CASE(COLOR, uint, impl->color)
177 		GET_CASE(CONSTRAINING, boolean, impl->coords.constraining)
178 		GET_CASE(CURVED, boolean, impl->coords.curved)
179 		GET_CASE(ARROWHEAD, boolean, impl->coords.arrowhead)
180 		GET_CASE(SELECTED, boolean, impl->selected)
181 		GET_CASE(HIGHLIGHTED, boolean, impl->highlighted)
182 		SET_CASE(GHOST, boolean, impl->ghost)
183 	default:
184 		G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
185 		break;
186 	}
187 }
188 
189 void
ganv_edge_request_redraw(GanvItem * item,const GanvEdgeCoords * coords)190 ganv_edge_request_redraw(GanvItem*             item,
191                          const GanvEdgeCoords* coords)
192 {
193 	GanvCanvas* canvas = item->impl->canvas;
194 	const double w = coords->width;
195 	if (coords->curved) {
196 		const double src_x  = coords->x1;
197 		const double src_y  = coords->y1;
198 		const double dst_x  = coords->x2;
199 		const double dst_y  = coords->y2;
200 		const double join_x = (src_x + dst_x) / 2.0;
201 		const double join_y = (src_y + dst_y) / 2.0;
202 		const double src_x1 = coords->cx1;
203 		const double src_y1 = coords->cy1;
204 		const double dst_x1 = coords->cx2;
205 		const double dst_y1 = coords->cy2;
206 
207 		const double r1x1 = MIN(MIN(src_x, join_x), src_x1);
208 		const double r1y1 = MIN(MIN(src_y, join_y), src_y1);
209 		const double r1x2 = MAX(MAX(src_x, join_x), src_x1);
210 		const double r1y2 = MAX(MAX(src_y, join_y), src_y1);
211 		ganv_canvas_request_redraw_w(canvas,
212 		                             r1x1 - w, r1y1 - w,
213 		                             r1x2 + w, r1y2 + w);
214 
215 		const double r2x1 = MIN(MIN(dst_x, join_x), dst_x1);
216 		const double r2y1 = MIN(MIN(dst_y, join_y), dst_y1);
217 		const double r2x2 = MAX(MAX(dst_x, join_x), dst_x1);
218 		const double r2y2 = MAX(MAX(dst_y, join_y), dst_y1);
219 		ganv_canvas_request_redraw_w(canvas,
220 		                             r2x1 - w, r2y1 - w,
221 		                             r2x2 + w, r2y2 + w);
222 
223 	} else {
224 		const double x1 = MIN(coords->x1, coords->x2);
225 		const double y1 = MIN(coords->y1, coords->y2);
226 		const double x2 = MAX(coords->x1, coords->x2);
227 		const double y2 = MAX(coords->y1, coords->y2);
228 
229 		ganv_canvas_request_redraw_w(canvas,
230 		                             x1 - w, y1 - w,
231 		                             x2 + w, y2 + w);
232 	}
233 
234 	if (coords->handle_radius > 0.0) {
235 		ganv_canvas_request_redraw_w(
236 			canvas,
237 			coords->handle_x - coords->handle_radius - w,
238 			coords->handle_y - coords->handle_radius - w,
239 			coords->handle_x + coords->handle_radius + w,
240 			coords->handle_y + coords->handle_radius + w);
241 	}
242 
243 	if (coords->arrowhead) {
244 		ganv_canvas_request_redraw_w(
245 			canvas,
246 			coords->x2 - ARROW_DEPTH,
247 			coords->y2 - ARROW_BREADTH,
248 			coords->x2 + ARROW_DEPTH,
249 			coords->y2 + ARROW_BREADTH);
250 	}
251 }
252 
253 static void
ganv_edge_bounds(GanvItem * item,double * x1,double * y1,double * x2,double * y2)254 ganv_edge_bounds(GanvItem* item,
255                  double* x1, double* y1,
256                  double* x2, double* y2)
257 {
258 	GanvEdge*        edge   = GANV_EDGE(item);
259 	GanvEdgePrivate* impl   = edge->impl;
260 	GanvEdgeCoords*  coords = &impl->coords;
261 	const double     w      = coords->width;
262 
263 	if (coords->curved) {
264 		*x1 = MIN(coords->x1, MIN(coords->cx1, MIN(coords->x2, coords->cx2))) - w;
265 		*y1 = MIN(coords->y1, MIN(coords->cy1, MIN(coords->y2, coords->cy2))) - w;
266 		*x2 = MAX(coords->x1, MAX(coords->cx1, MAX(coords->x2, coords->cx2))) + w;
267 		*y2 = MAX(coords->y1, MAX(coords->cy1, MAX(coords->y2, coords->cy2))) + w;
268 	} else {
269 		*x1 = MIN(impl->coords.x1, impl->coords.x2) - w;
270 		*y1 = MIN(impl->coords.y1, impl->coords.y2) - w;
271 		*x2 = MAX(impl->coords.x1, impl->coords.x2) + w;
272 		*y2 = MAX(impl->coords.y1, impl->coords.y2) + w;
273 	}
274 }
275 
276 void
ganv_edge_get_coords(const GanvEdge * edge,GanvEdgeCoords * coords)277 ganv_edge_get_coords(const GanvEdge* edge, GanvEdgeCoords* coords)
278 {
279 	GanvEdgePrivate* impl = edge->impl;
280 
281 	GANV_NODE_GET_CLASS(impl->tail)->tail_vector(
282 		impl->tail, impl->head,
283 		&coords->x1, &coords->y1, &coords->cx1, &coords->cy1);
284 	GANV_NODE_GET_CLASS(impl->head)->head_vector(
285 		impl->head, impl->tail,
286 		&coords->x2, &coords->y2, &coords->cx2, &coords->cy2);
287 
288 	const double dx = coords->x2 - coords->x1;
289 	const double dy = coords->y2 - coords->y1;
290 
291 	coords->handle_x = coords->x1 + (dx / 2.0);
292 	coords->handle_y = coords->y1 + (dy / 2.0);
293 
294 	const double abs_dx = fabs(dx);
295 	const double abs_dy = fabs(dy);
296 
297 	coords->cx1 = coords->x1 + (coords->cx1 * (abs_dx / 4.0));
298 	coords->cy1 = coords->y1 + (coords->cy1 * (abs_dy / 4.0));
299 	coords->cx2 = coords->x2 + (coords->cx2 * (abs_dx / 4.0));
300 	coords->cy2 = coords->y2 + (coords->cy2 * (abs_dy / 4.0));
301 }
302 
303 static void
ganv_edge_update(GanvItem * item,int flags)304 ganv_edge_update(GanvItem* item, int flags)
305 {
306 	GanvEdge*        edge = GANV_EDGE(item);
307 	GanvEdgePrivate* impl = edge->impl;
308 
309 	// Request redraw of old location
310 	ganv_edge_request_redraw(item, &impl->old_coords);
311 
312 	// Calculate new coordinates from tail and head
313 	ganv_edge_get_coords(edge, &impl->coords);
314 
315 	// Update old coordinates
316 	impl->old_coords = impl->coords;
317 
318 	// Get bounding box
319 	double x1 = 0.0;
320 	double x2 = 0.0;
321 	double y1 = 0.0;
322 	double y2 = 0.0;
323 	ganv_edge_bounds(item, &x1, &y1, &x2, &y2);
324 
325 	// Ensure bounding box has non-zero area
326 	if (x1 == x2) {
327 		x2 += 1.0;
328 	}
329 	if (y1 == y2) {
330 		y2 += 1.0;
331 	}
332 
333 	// Update world-relative bounding box
334 	item->impl->x1 = x1;
335 	item->impl->y1 = y1;
336 	item->impl->x2 = x2;
337 	item->impl->y2 = y2;
338 	ganv_item_i2w_pair(item, &item->impl->x1, &item->impl->y1, &item->impl->x2, &item->impl->y2);
339 
340 	// Request redraw of new location
341 	ganv_edge_request_redraw(item, &impl->coords);
342 
343 	parent_class->update(item, flags);
344 }
345 
346 static void
ganv_edge_draw(GanvItem * item,cairo_t * cr,double cx,double cy,double cw,double ch)347 ganv_edge_draw(GanvItem* item,
348                cairo_t* cr, double cx, double cy, double cw, double ch)
349 {
350 	(void)cx;
351 	(void)cy;
352 	(void)cw;
353 	(void)ch;
354 
355 	GanvEdge*        edge = GANV_EDGE(item);
356 	GanvEdgePrivate* impl = edge->impl;
357 
358 	double src_x = impl->coords.x1;
359 	double src_y = impl->coords.y1;
360 	double dst_x = impl->coords.x2;
361 	double dst_y = impl->coords.y2;
362 	double dx    = src_x - dst_x;
363 	double dy    = src_y - dst_y;
364 
365 	double r = 0.0;
366 	double g = 0.0;
367 	double b = 0.0;
368 	double a = 0.0;
369 	if (impl->highlighted) {
370 		color_to_rgba(highlight_color(impl->color, 0x40), &r, &g, &b, &a);
371 	} else {
372 		color_to_rgba(impl->color, &r, &g, &b, &a);
373 	}
374 	cairo_set_source_rgba(cr, r, g, b, a);
375 
376 	cairo_set_line_width(cr, impl->coords.width);
377 	cairo_move_to(cr, src_x, src_y);
378 
379 	const double dash_length = (impl->selected ? 4.0 : impl->dash_length);
380 	if (dash_length > 0.0) {
381 		double dashed[2] = { dash_length, dash_length };
382 		cairo_set_dash(cr, dashed, 2, impl->dash_offset);
383 	} else {
384 		cairo_set_dash(cr, &dash_length, 0, 0);
385 	}
386 
387 	const double join_x = (src_x + dst_x) / 2.0;
388 	const double join_y = (src_y + dst_y) / 2.0;
389 
390 	if (impl->coords.curved) {
391 		// Curved line as 2 paths which join at the middle point
392 
393 		// Path 1 (src_x, src_y) -> (join_x, join_y)
394 		// Control point 1
395 		const double src_x1 = impl->coords.cx1;
396 		const double src_y1 = impl->coords.cy1;
397 		// Control point 2
398 		const double src_x2 = (join_x + src_x1) / 2.0;
399 		const double src_y2 = (join_y + src_y1) / 2.0;
400 
401 		// Path 2, (join_x, join_y) -> (dst_x, dst_y)
402 		// Control point 1
403 		const double dst_x1 = impl->coords.cx2;
404 		const double dst_y1 = impl->coords.cy2;
405 		// Control point 2
406 		const double dst_x2 = (join_x + dst_x1) / 2.0;
407 		const double dst_y2 = (join_y + dst_y1) / 2.0;
408 
409 		cairo_move_to(cr, src_x, src_y);
410 		cairo_curve_to(cr, src_x1, src_y1, src_x2, src_y2, join_x, join_y);
411 		cairo_curve_to(cr, dst_x2, dst_y2, dst_x1, dst_y1, dst_x, dst_y);
412 
413 #ifdef GANV_DEBUG_CURVES
414 		cairo_stroke(cr);
415 		cairo_save(cr);
416 		cairo_set_source_rgba(cr, 1.0, 0, 0, 0.5);
417 
418 		cairo_move_to(cr, src_x, src_y);
419 		cairo_line_to(cr, src_x1, src_y1);
420 		cairo_stroke(cr);
421 
422 		cairo_move_to(cr, join_x, join_y);
423 		cairo_line_to(cr, src_x2, src_y2);
424 		cairo_stroke(cr);
425 
426 		cairo_move_to(cr, join_x, join_y);
427 		cairo_line_to(cr, dst_x2, dst_y2);
428 		cairo_stroke(cr);
429 
430 		cairo_move_to(cr, dst_x, dst_y);
431 		cairo_line_to(cr, dst_x1, dst_y1);
432 		cairo_stroke(cr);
433 
434 #ifdef GANV_DEBUG_BOUNDS
435 		double bounds_x1, bounds_y1, bounds_x2, bounds_y2;
436 		ganv_edge_bounds(item, &bounds_x1, &bounds_y1, &bounds_x2, &bounds_y2);
437 		cairo_rectangle(cr,
438 		                bounds_x1, bounds_y1,
439 		                bounds_x2 - bounds_x1, bounds_y2 - bounds_y1);
440 #endif
441 
442 		cairo_restore(cr);
443 #endif
444 
445 		cairo_stroke(cr);
446 		if (impl->coords.arrowhead) {
447 			cairo_move_to(cr, dst_x - 12, dst_y - 4);
448 			cairo_line_to(cr, dst_x, dst_y);
449 			cairo_line_to(cr, dst_x - 12, dst_y + 4);
450 			cairo_close_path(cr);
451 			cairo_stroke_preserve(cr);
452 			cairo_fill(cr);
453 		}
454 
455 	} else {
456 		// Straight line from (x1, y1) to (x2, y2)
457 		cairo_move_to(cr, src_x, src_y);
458 		cairo_line_to(cr, dst_x, dst_y);
459 		cairo_stroke(cr);
460 
461 		if (impl->coords.arrowhead) {
462 			const double ah  = sqrt(dx * dx + dy * dy);
463 			const double adx = dx / ah * 8.0;
464 			const double ady = dy / ah * 8.0;
465 
466 			cairo_move_to(cr,
467 			              dst_x + adx - ady/1.5,
468 			              dst_y + ady + adx/1.5);
469 			cairo_set_line_join(cr, CAIRO_LINE_JOIN_BEVEL);
470 			cairo_line_to(cr, dst_x, dst_y);
471 			cairo_set_line_join(cr, CAIRO_LINE_JOIN_MITER);
472 			cairo_line_to(cr,
473 			              dst_x + adx + ady/1.5,
474 			              dst_y + ady - adx/1.5);
475 			cairo_close_path(cr);
476 			cairo_stroke_preserve(cr);
477 			cairo_fill(cr);
478 		}
479 	}
480 
481 	if (!ganv_canvas_exporting(item->impl->canvas) &&
482 	    impl->coords.handle_radius > 0.0) {
483 		cairo_move_to(cr, join_x, join_y);
484 		cairo_arc(cr, join_x, join_y, impl->coords.handle_radius, 0, 2 * G_PI);
485 		cairo_fill(cr);
486 	}
487 }
488 
489 static double
ganv_edge_point(GanvItem * item,double x,double y,GanvItem ** actual_item)490 ganv_edge_point(GanvItem* item, double x, double y, GanvItem** actual_item)
491 {
492 	const GanvEdge*       edge = GANV_EDGE(item);
493 	const GanvEdgeCoords* coords = &edge->impl->coords;
494 
495 	const double dx = fabs(x - coords->handle_x);
496 	const double dy = fabs(y - coords->handle_y);
497 	const double d  = sqrt((dx * dx) + (dy * dy));
498 
499 	*actual_item = item;
500 
501 	if (d <= coords->handle_radius) {
502 		// Point is inside the handle
503 		return 0.0;
504 	} else {
505 		// Distance from the edge of the handle
506 		return d - (coords->handle_radius + coords->width);
507 	}
508 }
509 
510 gboolean
ganv_edge_is_within(const GanvEdge * edge,double x1,double y1,double x2,double y2)511 ganv_edge_is_within(const GanvEdge* edge,
512                     double          x1,
513                     double          y1,
514                     double          x2,
515                     double          y2)
516 {
517 	const double handle_x = edge->impl->coords.handle_x;
518 	const double handle_y = edge->impl->coords.handle_y;
519 
520 	return handle_x >= x1
521 		&& handle_x <= x2
522 		&& handle_y >= y1
523 		&& handle_y <= y2;
524 }
525 
526 static void
ganv_edge_class_init(GanvEdgeClass * klass)527 ganv_edge_class_init(GanvEdgeClass* klass)
528 {
529 	GObjectClass*   gobject_class = (GObjectClass*)klass;
530 	GtkObjectClass* object_class  = (GtkObjectClass*)klass;
531 	GanvItemClass*  item_class    = (GanvItemClass*)klass;
532 
533 	parent_class = GANV_ITEM_CLASS(g_type_class_peek_parent(klass));
534 
535 	gobject_class->set_property = ganv_edge_set_property;
536 	gobject_class->get_property = ganv_edge_get_property;
537 
538 	g_object_class_install_property(
539 		gobject_class, PROP_TAIL, g_param_spec_object(
540 			"tail",
541 			_("Tail"),
542 			_("Node this edge starts from."),
543 			GANV_TYPE_NODE,
544 			G_PARAM_READWRITE));
545 
546 	g_object_class_install_property(
547 		gobject_class, PROP_HEAD, g_param_spec_object(
548 			"head",
549 			_("Head"),
550 			_("Node this edge ends at."),
551 			GANV_TYPE_NODE,
552 			G_PARAM_READWRITE));
553 
554 	g_object_class_install_property(
555 		gobject_class, PROP_WIDTH, g_param_spec_double(
556 			"width",
557 			_("Line width"),
558 			_("Width of edge line."),
559 			0.0, G_MAXDOUBLE,
560 			2.0,
561 			G_PARAM_READWRITE));
562 
563 	g_object_class_install_property(
564 		gobject_class, PROP_HANDLE_RADIUS, g_param_spec_double(
565 			"handle-radius",
566 			_("Gandle radius"),
567 			_("Radius of handle in canvas units."),
568 			0.0, G_MAXDOUBLE,
569 			4.0,
570 			G_PARAM_READWRITE));
571 
572 	g_object_class_install_property(
573 		gobject_class, PROP_DASH_LENGTH, g_param_spec_double(
574 			"dash-length",
575 			_("Line dash length"),
576 			_("Length of line dashes, or zero for no dashing."),
577 			0.0, G_MAXDOUBLE,
578 			0.0,
579 			G_PARAM_READWRITE));
580 
581 	g_object_class_install_property(
582 		gobject_class, PROP_DASH_OFFSET, g_param_spec_double(
583 			"dash-offset",
584 			_("Line dash offset"),
585 			_("Start offset for line dashes, used for selected animation."),
586 			0.0, G_MAXDOUBLE,
587 			0.0,
588 			G_PARAM_READWRITE));
589 
590 	g_object_class_install_property(
591 		gobject_class, PROP_COLOR, g_param_spec_uint(
592 			"color",
593 			_("Color"),
594 			_("Line color as an RGBA integer."),
595 			0, G_MAXUINT,
596 			0,
597 			G_PARAM_READWRITE));
598 
599 	g_object_class_install_property(
600 		gobject_class, PROP_CONSTRAINING, g_param_spec_boolean(
601 			"constraining",
602 			_("Constraining"),
603 			_("Whether edge should constrain the layout."),
604 			1,
605 			G_PARAM_READWRITE));
606 
607 	g_object_class_install_property(
608 		gobject_class, PROP_CURVED, g_param_spec_boolean(
609 			"curved",
610 			_("Curved"),
611 			_("Whether line should be curved rather than straight."),
612 			0,
613 			G_PARAM_READWRITE));
614 
615 	g_object_class_install_property(
616 		gobject_class, PROP_ARROWHEAD, g_param_spec_boolean(
617 			"arrowhead",
618 			_("Arrowhead"),
619 			_("Whether to show an arrowhead at the head of this edge."),
620 			0,
621 			G_PARAM_READWRITE));
622 
623 	g_object_class_install_property(
624 		gobject_class, PROP_SELECTED, g_param_spec_boolean(
625 			"selected",
626 			_("Selected"),
627 			_("Whether this edge is selected."),
628 			0,
629 			G_PARAM_READWRITE));
630 
631 	g_object_class_install_property(
632 		gobject_class, PROP_HIGHLIGHTED, g_param_spec_boolean(
633 			"highlighted",
634 			_("Highlighted"),
635 			_("Whether to highlight the edge."),
636 			0,
637 			G_PARAM_READWRITE));
638 
639 	g_object_class_install_property(
640 		gobject_class, PROP_GHOST, g_param_spec_boolean(
641 			"ghost",
642 			_("Ghost"),
643 			_("Whether this edge is a `ghost', which is an edge that is not "
644 			  "added to the canvas data structures.  Ghost edges are used for "
645 			  "temporary edges that are not considered `real', e.g. the edge "
646 			  "made while dragging to make a connection."),
647 			0,
648 			G_PARAM_READWRITE));
649 
650 	object_class->destroy = ganv_edge_destroy;
651 
652 	item_class->update = ganv_edge_update;
653 	item_class->bounds = ganv_edge_bounds;
654 	item_class->point  = ganv_edge_point;
655 	item_class->draw   = ganv_edge_draw;
656 }
657 
658 GanvEdge*
ganv_edge_new(GanvCanvas * canvas,GanvNode * tail,GanvNode * head,const char * first_prop_name,...)659 ganv_edge_new(GanvCanvas* canvas,
660               GanvNode*   tail,
661               GanvNode*   head,
662               const char* first_prop_name, ...)
663 {
664 	GanvEdge* edge = GANV_EDGE(
665 		g_object_new(ganv_edge_get_type(), NULL));
666 
667 	va_list args;
668 	va_start(args, first_prop_name);
669 	ganv_item_construct(&edge->item,
670 	                    GANV_ITEM(ganv_canvas_root(canvas)),
671 	                    first_prop_name, args);
672 	va_end(args);
673 
674 	edge->impl->tail = tail;
675 	edge->impl->head = head;
676 
677 	if (!edge->impl->color) {
678 		const guint tail_color = GANV_NODE(tail)->impl->fill_color;
679 		g_object_set(G_OBJECT(edge),
680 		             "color", EDGE_COLOR(tail_color),
681 		             NULL);
682 	}
683 
684 	if (!edge->impl->ghost) {
685 		ganv_canvas_add_edge(canvas, edge);
686 	}
687 	return edge;
688 }
689 
690 void
ganv_edge_update_location(GanvEdge * edge)691 ganv_edge_update_location(GanvEdge* edge)
692 {
693 	ganv_item_request_update(GANV_ITEM(edge));
694 }
695 
696 gboolean
ganv_edge_get_constraining(const GanvEdge * edge)697 ganv_edge_get_constraining(const GanvEdge* edge)
698 {
699 	return edge->impl->coords.constraining;
700 }
701 
702 void
ganv_edge_set_constraining(GanvEdge * edge,gboolean constraining)703 ganv_edge_set_constraining(GanvEdge* edge, gboolean constraining)
704 {
705 	edge->impl->coords.constraining = constraining;
706 	ganv_edge_request_redraw(GANV_ITEM(edge), &edge->impl->coords);
707 }
708 
709 gboolean
ganv_edge_get_curved(const GanvEdge * edge)710 ganv_edge_get_curved(const GanvEdge* edge)
711 {
712 	return edge->impl->coords.curved;
713 }
714 
715 void
ganv_edge_set_curved(GanvEdge * edge,gboolean curved)716 ganv_edge_set_curved(GanvEdge* edge, gboolean curved)
717 {
718 	edge->impl->coords.curved = curved;
719 	ganv_edge_request_redraw(GANV_ITEM(edge), &edge->impl->coords);
720 }
721 
722 void
ganv_edge_set_selected(GanvEdge * edge,gboolean selected)723 ganv_edge_set_selected(GanvEdge* edge, gboolean selected)
724 {
725 	GanvCanvas* canvas = GANV_CANVAS(edge->item.impl->canvas);
726 	if (selected) {
727 		ganv_canvas_select_edge(canvas, edge);
728 	} else {
729 		ganv_canvas_unselect_edge(canvas, edge);
730 	}
731 }
732 
733 void
ganv_edge_select(GanvEdge * edge)734 ganv_edge_select(GanvEdge* edge)
735 {
736 	ganv_edge_set_selected(edge, TRUE);
737 }
738 
739 void
ganv_edge_unselect(GanvEdge * edge)740 ganv_edge_unselect(GanvEdge* edge)
741 {
742 	ganv_edge_set_selected(edge, FALSE);
743 }
744 
745 void
ganv_edge_highlight(GanvEdge * edge)746 ganv_edge_highlight(GanvEdge* edge)
747 {
748 	ganv_edge_set_highlighted(edge, TRUE);
749 }
750 
751 void
ganv_edge_unhighlight(GanvEdge * edge)752 ganv_edge_unhighlight(GanvEdge* edge)
753 {
754 	ganv_edge_set_highlighted(edge, FALSE);
755 }
756 
757 void
ganv_edge_set_highlighted(GanvEdge * edge,gboolean highlighted)758 ganv_edge_set_highlighted(GanvEdge* edge, gboolean highlighted)
759 {
760 	edge->impl->highlighted = highlighted;
761 	ganv_edge_request_redraw(GANV_ITEM(edge), &edge->impl->coords);
762 }
763 
764 void
ganv_edge_tick(GanvEdge * edge,double seconds)765 ganv_edge_tick(GanvEdge* edge, double seconds)
766 {
767 	ganv_item_set(GANV_ITEM(edge),
768 	              "dash-offset", seconds * 8.0,
769 	              NULL);
770 }
771 
772 void
ganv_edge_disconnect(GanvEdge * edge)773 ganv_edge_disconnect(GanvEdge* edge)
774 {
775 	if (!edge->impl->ghost) {
776 		ganv_canvas_disconnect_edge(
777 			GANV_CANVAS(edge->item.impl->canvas),
778 			edge);
779 	}
780 }
781 
782 void
ganv_edge_remove(GanvEdge * edge)783 ganv_edge_remove(GanvEdge* edge)
784 {
785 	if (!edge->impl->ghost) {
786 		ganv_canvas_remove_edge(
787 			GANV_CANVAS(edge->item.impl->canvas),
788 			edge);
789 	}
790 }
791 
792 GanvNode*
ganv_edge_get_tail(const GanvEdge * edge)793 ganv_edge_get_tail(const GanvEdge* edge)
794 {
795 	return edge->impl->tail;
796 }
797 
798 GanvNode*
ganv_edge_get_head(const GanvEdge * edge)799 ganv_edge_get_head(const GanvEdge* edge)
800 {
801 	return edge->impl->head;
802 }
803