1/* $Id$ $Revision$ */
2/* vim:set shiftwidth=4 ts=8: */
3
4/*************************************************************************
5 * Copyright (c) 2011 AT&T Intellectual Property
6 * All rights reserved. This program and the accompanying materials
7 * are made available under the terms of the Eclipse Public License v1.0
8 * which accompanies this distribution, and is available at
9 * http://www.eclipse.org/legal/epl-v10.html
10 *
11 * Contributors: See CVS logs. Details at http://www.graphviz.org/
12 *************************************************************************/
13
14#include "config.h"
15
16#include <string.h>
17#include <stdlib.h>
18#include <math.h>
19
20#include "gvplugin_layout.h"
21#include "gvcint.h"
22#include "gvcproc.h"
23
24extern char *strdup_and_subst_obj(char *str, void * n);
25extern void emit_graph(GVJ_t * job, graph_t * g);
26extern boolean overlap_edge(edge_t *e, boxf b);
27extern boolean overlap_node(node_t *n, boxf b);
28extern int gvLayout(GVC_t *gvc, graph_t *g, const char *engine);
29extern int gvRenderFilename(GVC_t *gvc, graph_t *g, const char *format, const char *filename);
30extern void graph_cleanup(graph_t *g);
31
32#define PANFACTOR 10
33#define ZOOMFACTOR 1.1
34#define EPSILON .0001
35
36static char *s_digraph = "digraph";
37static char *s_graph = "graph";
38static char *s_subgraph = "subgraph";
39static char *s_node = "node";
40static char *s_edge = "edge";
41static char *s_tooltip = "tooltip";
42static char *s_href = "href";
43static char *s_URL = "URL";
44static char *s_tailport = "tailport";
45static char *s_headport = "headport";
46static char *s_key = "key";
47
48static void gv_graph_state(GVJ_t *job, graph_t *g)
49{
50 int j;
51 Agsym_t *a;
52 gv_argvlist_t *list;
53
54 list = &(job->selected_obj_type_name);
55 j = 0;
56 if (g == agroot(g)) {
57 if (agisdirected(g))
58 gv_argvlist_set_item(list, j++, s_digraph);
59 else
60 gv_argvlist_set_item(list, j++, s_graph);
61 }
62 else {
63 gv_argvlist_set_item(list, j++, s_subgraph);
64 }
65 gv_argvlist_set_item(list, j++, agnameof(g));
66 list->argc = j;
67
68 list = &(job->selected_obj_attributes);
69 a = NULL;
70 while ((a = agnxtattr(g, AGRAPH, a))) {
71 gv_argvlist_set_item(list, j++, a->name);
72 gv_argvlist_set_item(list, j++, agxget(g, a));
73 gv_argvlist_set_item(list, j++, (char*)GVATTR_STRING);
74 }
75 list->argc = j;
76
77 a = agfindgraphattr(g, s_href);
78 if (!a)
79 a = agfindgraphattr(g, s_URL);
80 if (a)
81 job->selected_href = strdup_and_subst_obj(agxget(g, a), (void*)g);
82}
83
84static void gv_node_state(GVJ_t *job, node_t *n)
85{
86 int j;
87 Agsym_t *a;
88 Agraph_t *g;
89 gv_argvlist_t *list;
90
91 list = &(job->selected_obj_type_name);
92 j = 0;
93 gv_argvlist_set_item(list, j++, s_node);
94 gv_argvlist_set_item(list, j++, agnameof(n));
95 list->argc = j;
96
97 list = &(job->selected_obj_attributes);
98 g = agroot(agraphof(n));
99 a = NULL;
100 while ((a = agnxtattr(g, AGNODE, a))) {
101 gv_argvlist_set_item(list, j++, a->name);
102 gv_argvlist_set_item(list, j++, agxget(n, a));
103 }
104 list->argc = j;
105
106 a = agfindnodeattr(agraphof(n), s_href);
107 if (!a)
108 a = agfindnodeattr(agraphof(n), s_URL);
109 if (a)
110 job->selected_href = strdup_and_subst_obj(agxget(n, a), (void*)n);
111}
112
113static void gv_edge_state(GVJ_t *job, edge_t *e)
114{
115 int j;
116 Agsym_t *a;
117 Agraph_t *g;
118 gv_argvlist_t *nlist, *alist;
119
120 nlist = &(job->selected_obj_type_name);
121
122 /* only tail, head, and key are strictly identifying properties,
123 * but we commonly also use edge kind (e.g. "->") and tailport,headport
124 * in edge names */
125 j = 0;
126 gv_argvlist_set_item(nlist, j++, s_edge);
127 gv_argvlist_set_item(nlist, j++, agnameof(agtail(e)));
128 j++; /* skip tailport slot for now */
129 gv_argvlist_set_item(nlist, j++, agisdirected(agraphof(agtail(e)))?"->":"--");
130 gv_argvlist_set_item(nlist, j++, agnameof(aghead(e)));
131 j++; /* skip headport slot for now */
132 j++; /* skip key slot for now */
133 nlist->argc = j;
134
135 alist = &(job->selected_obj_attributes);
136 g = agroot(agraphof(aghead(e)));
137 a = NULL;
138 while ((a = agnxtattr(g, AGEDGE, a))) {
139
140 /* tailport and headport can be shown as part of the name, but they
141 * are not identifying properties of the edge so we
142 * also list them as modifyable attributes. */
143 if (strcmp(a->name,s_tailport) == 0)
144 gv_argvlist_set_item(nlist, 2, agxget(e, a));
145 else if (strcmp(a->name,s_headport) == 0)
146 gv_argvlist_set_item(nlist, 5, agxget(e, a));
147
148 /* key is strictly an identifying property to distinguish multiple
149 * edges between the same node pair. Its non-writable, so
150 * no need to list it as an attribute as well. */
151 else if (strcmp(a->name,s_key) == 0) {
152 gv_argvlist_set_item(nlist, 6, agxget(e, a));
153 continue;
154 }
155
156 gv_argvlist_set_item(alist, j++, a->name);
157 gv_argvlist_set_item(alist, j++, agxget(e, a));
158 }
159 alist->argc = j;
160
161 a = agfindedgeattr(agraphof(aghead(e)), s_href);
162 if (!a)
163 a = agfindedgeattr(agraphof(aghead(e)), s_URL);
164 if (a)
165 job->selected_href = strdup_and_subst_obj(agxget(e, a), (void*)e);
166}
167
168static void gvevent_refresh(GVJ_t * job)
169{
170 graph_t *g = job->gvc->g;
171
172 if (!job->selected_obj) {
173 job->selected_obj = g;
174 GD_gui_state(g) |= GUI_STATE_SELECTED;
175 gv_graph_state(job, g);
176 }
177 emit_graph(job, g);
178 job->has_been_rendered = TRUE;
179}
180
181/* recursively find innermost cluster containing the point */
182static graph_t *gvevent_find_cluster(graph_t *g, boxf b)
183{
184 int i;
185 graph_t *sg;
186 boxf bb;
187
188 for (i = 1; i <= GD_n_cluster(g); i++) {
189 sg = gvevent_find_cluster(GD_clust(g)[i], b);
190 if (sg)
191 return(sg);
192 }
193 B2BF(GD_bb(g), bb);
194 if (OVERLAP(b, bb))
195 return g;
196 return NULL;
197}
198
199static void * gvevent_find_obj(graph_t *g, boxf b)
200{
201 graph_t *sg;
202 node_t *n;
203 edge_t *e;
204
205 /* edges might overlap nodes, so search them first */
206 for (n = agfstnode(g); n; n = agnxtnode(g, n))
207 for (e = agfstout(g, n); e; e = agnxtout(g, e))
208 if (overlap_edge(e, b))
209 return (void *)e;
210 /* search graph backwards to get topmost node, in case of overlap */
211 for (n = aglstnode(g); n; n = agprvnode(g, n))
212 if (overlap_node(n, b))
213 return (void *)n;
214 /* search for innermost cluster */
215 sg = gvevent_find_cluster(g, b);
216 if (sg)
217 return (void *)sg;
218
219 /* otherwise - we're always in the graph */
220 return (void *)g;
221}
222
223static void gvevent_leave_obj(GVJ_t * job)
224{
225 void *obj = job->current_obj;
226
227 if (obj) {
228 switch (agobjkind(obj)) {
229 case AGRAPH:
230 GD_gui_state((graph_t*)obj) &= ~GUI_STATE_ACTIVE;
231 break;
232 case AGNODE:
233 ND_gui_state((node_t*)obj) &= ~GUI_STATE_ACTIVE;
234 break;
235 case AGEDGE:
236 ED_gui_state((edge_t*)obj) &= ~GUI_STATE_ACTIVE;
237 break;
238 }
239 }
240 job->active_tooltip = NULL;
241}
242
243static void gvevent_enter_obj(GVJ_t * job)
244{
245 void *obj;
246 graph_t *g;
247 edge_t *e;
248 node_t *n;
249 Agsym_t *a;
250
251 if (job->active_tooltip) {
252 free(job->active_tooltip);
253 job->active_tooltip = NULL;
254 }
255 obj = job->current_obj;
256 if (obj) {
257 switch (agobjkind(obj)) {
258 case AGRAPH:
259 g = (graph_t*)obj;
260 GD_gui_state(g) |= GUI_STATE_ACTIVE;
261 a = agfindgraphattr(g, s_tooltip);
262 if (a)
263 job->active_tooltip = strdup_and_subst_obj(agxget(g, a), obj);
264 break;
265 case AGNODE:
266 n = (node_t*)obj;
267 ND_gui_state(n) |= GUI_STATE_ACTIVE;
268 a = agfindnodeattr(agraphof(n), s_tooltip);
269 if (a)
270 job->active_tooltip = strdup_and_subst_obj(agxget(n, a), obj);
271 break;
272 case AGEDGE:
273 e = (edge_t*)obj;
274 ED_gui_state(e) |= GUI_STATE_ACTIVE;
275 a = agfindedgeattr(agraphof(aghead(e)), s_tooltip);
276 if (a)
277 job->active_tooltip = strdup_and_subst_obj(agxget(e, a), obj);
278 break;
279 }
280 }
281}
282
283static pointf pointer2graph (GVJ_t *job, pointf pointer)
284{
285 pointf p;
286
287 /* transform position in device units to position in graph units */
288 if (job->rotation) {
289 p.x = pointer.y / (job->zoom * job->devscale.y) - job->translation.x;
290 p.y = -pointer.x / (job->zoom * job->devscale.x) - job->translation.y;
291 }
292 else {
293 p.x = pointer.x / (job->zoom * job->devscale.x) - job->translation.x;
294 p.y = pointer.y / (job->zoom * job->devscale.y) - job->translation.y;
295 }
296 return p;
297}
298
299/* CLOSEENOUGH is in 1/72 - probably should be a feature... */
300#define CLOSEENOUGH 1
301
302static void gvevent_find_current_obj(GVJ_t * job, pointf pointer)
303{
304 void *obj;
305 boxf b;
306 double closeenough;
307 pointf p;
308
309 p = pointer2graph (job, pointer);
310
311 /* convert window point to graph coordinates */
312 closeenough = CLOSEENOUGH / job->zoom;
313
314 b.UR.x = p.x + closeenough;
315 b.UR.y = p.y + closeenough;
316 b.LL.x = p.x - closeenough;
317 b.LL.y = p.y - closeenough;
318
319 obj = gvevent_find_obj(job->gvc->g, b);
320 if (obj != job->current_obj) {
321 gvevent_leave_obj(job);
322 job->current_obj = obj;
323 gvevent_enter_obj(job);
324 job->needs_refresh = 1;
325 }
326}
327
328static void gvevent_select_current_obj(GVJ_t * job)
329{
330 void *obj;
331
332 obj = job->selected_obj;
333 if (obj) {
334 switch (agobjkind(obj)) {
335 case AGRAPH:
336 GD_gui_state((graph_t*)obj) |= GUI_STATE_VISITED;
337 GD_gui_state((graph_t*)obj) &= ~GUI_STATE_SELECTED;
338 break;
339 case AGNODE:
340 ND_gui_state((node_t*)obj) |= GUI_STATE_VISITED;
341 ND_gui_state((node_t*)obj) &= ~GUI_STATE_SELECTED;
342 break;
343 case AGEDGE:
344 ED_gui_state((edge_t*)obj) |= GUI_STATE_VISITED;
345 ED_gui_state((edge_t*)obj) &= ~GUI_STATE_SELECTED;
346 break;
347 }
348 }
349
350 if (job->selected_href) {
351 free(job->selected_href);
352 job->selected_href = NULL;
353 }
354
355 obj = job->selected_obj = job->current_obj;
356 if (obj) {
357 switch (agobjkind(obj)) {
358 case AGRAPH:
359 GD_gui_state((graph_t*)obj) |= GUI_STATE_SELECTED;
360 gv_graph_state(job, (graph_t*)obj);
361 break;
362 case AGNODE:
363 ND_gui_state((node_t*)obj) |= GUI_STATE_SELECTED;
364 gv_node_state(job, (node_t*)obj);
365 break;
366 case AGEDGE:
367 ED_gui_state((edge_t*)obj) |= GUI_STATE_SELECTED;
368 gv_edge_state(job, (edge_t*)obj);
369 break;
370 }
371 }
372
373#if 0
374for (i = 0; i < job->selected_obj_type_name.argc; i++)
375 fprintf(stderr,"%s%s", job->selected_obj_type_name.argv[i],
376 (i==(job->selected_obj_type_name.argc - 1))?"\n":" ");
377for (i = 0; i < job->selected_obj_attributes.argc; i++)
378 fprintf(stderr,"%s%s", job->selected_obj_attributes.argv[i], (i%2)?"\n":" = ");
379fprintf(stderr,"\n");
380#endif
381}
382
383static void gvevent_button_press(GVJ_t * job, int button, pointf pointer)
384{
385 switch (button) {
386 case 1: /* select / create in edit mode */
387 gvevent_find_current_obj(job, pointer);
388 gvevent_select_current_obj(job);
389 job->click = 1;
390 job->button = button;
391 job->needs_refresh = 1;
392 break;
393 case 2: /* pan */
394 job->click = 1;
395 job->button = button;
396 job->needs_refresh = 1;
397 break;
398 case 3: /* insert node or edge */
399 gvevent_find_current_obj(job, pointer);
400 job->click = 1;
401 job->button = button;
402 job->needs_refresh = 1;
403 break;
404 case 4:
405 /* scrollwheel zoom in at current mouse x,y */
406/* FIXME - should code window 0,0 point as feature with Y_GOES_DOWN */
407 job->fit_mode = 0;
408 if (job->rotation) {
409 job->focus.x -= (pointer.y - job->height / 2.)
410 * (ZOOMFACTOR - 1.) / (job->zoom * job->devscale.y);
411 job->focus.y += (pointer.x - job->width / 2.)
412 * (ZOOMFACTOR - 1.) / (job->zoom * job->devscale.x);
413 }
414 else {
415 job->focus.x += (pointer.x - job->width / 2.)
416 * (ZOOMFACTOR - 1.) / (job->zoom * job->devscale.x);
417 job->focus.y += (pointer.y - job->height / 2.)
418 * (ZOOMFACTOR - 1.) / (job->zoom * job->devscale.y);
419 }
420 job->zoom *= ZOOMFACTOR;
421 job->needs_refresh = 1;
422 break;
423 case 5: /* scrollwheel zoom out at current mouse x,y */
424 job->fit_mode = 0;
425 job->zoom /= ZOOMFACTOR;
426 if (job->rotation) {
427 job->focus.x += (pointer.y - job->height / 2.)
428 * (ZOOMFACTOR - 1.) / (job->zoom * job->devscale.y);
429 job->focus.y -= (pointer.x - job->width / 2.)
430 * (ZOOMFACTOR - 1.) / (job->zoom * job->devscale.x);
431 }
432 else {
433 job->focus.x -= (pointer.x - job->width / 2.)
434 * (ZOOMFACTOR - 1.) / (job->zoom * job->devscale.x);
435 job->focus.y -= (pointer.y - job->height / 2.)
436 * (ZOOMFACTOR - 1.) / (job->zoom * job->devscale.y);
437 }
438 job->needs_refresh = 1;
439 break;
440 }
441 job->oldpointer = pointer;
442}
443
444static void gvevent_button_release(GVJ_t *job, int button, pointf pointer)
445{
446 job->click = 0;
447 job->button = 0;
448}
449
450static void gvevent_motion(GVJ_t * job, pointf pointer)
451{
452 /* dx,dy change in position, in device independent points */
453 double dx = (pointer.x - job->oldpointer.x) / job->devscale.x;
454 double dy = (pointer.y - job->oldpointer.y) / job->devscale.y;
455
456 if (fabs(dx) < EPSILON && fabs(dy) < EPSILON) /* ignore motion events with no motion */
457 return;
458
459 switch (job->button) {
460 case 0: /* drag with no button - */
461 gvevent_find_current_obj(job, pointer);
462 break;
463 case 1: /* drag with button 1 - drag object */
464 /* FIXME - to be implemented */
465 break;
466 case 2: /* drag with button 2 - pan graph */
467 if (job->rotation) {
468 job->focus.x -= dy / job->zoom;
469 job->focus.y += dx / job->zoom;
470 }
471 else {
472 job->focus.x -= dx / job->zoom;
473 job->focus.y -= dy / job->zoom;
474 }
475 job->needs_refresh = 1;
476 break;
477 case 3: /* drag with button 3 - drag inserted node or uncompleted edge */
478 break;
479 }
480 job->oldpointer = pointer;
481}
482
483static int quit_cb(GVJ_t * job)
484{
485 return 1;
486}
487
488static int left_cb(GVJ_t * job)
489{
490 job->fit_mode = 0;
491 job->focus.x += PANFACTOR / job->zoom;
492 job->needs_refresh = 1;
493 return 0;
494}
495
496static int right_cb(GVJ_t * job)
497{
498 job->fit_mode = 0;
499 job->focus.x -= PANFACTOR / job->zoom;
500 job->needs_refresh = 1;
501 return 0;
502}
503
504static int up_cb(GVJ_t * job)
505{
506 job->fit_mode = 0;
507 job->focus.y += -(PANFACTOR / job->zoom);
508 job->needs_refresh = 1;
509 return 0;
510}
511
512static int down_cb(GVJ_t * job)
513{
514 job->fit_mode = 0;
515 job->focus.y -= -(PANFACTOR / job->zoom);
516 job->needs_refresh = 1;
517 return 0;
518}
519
520static int zoom_in_cb(GVJ_t * job)
521{
522 job->fit_mode = 0;
523 job->zoom *= ZOOMFACTOR;
524 job->needs_refresh = 1;
525 return 0;
526}
527
528static int zoom_out_cb(GVJ_t * job)
529{
530 job->fit_mode = 0;
531 job->zoom /= ZOOMFACTOR;
532 job->needs_refresh = 1;
533 return 0;
534}
535
536static int toggle_fit_cb(GVJ_t * job)
537{
538/*FIXME - should allow for margins */
539/* - similar zoom_to_fit code exists in: */
540/* plugin/gtk/callbacks.c */
541/* plugin/xlib/gvdevice_xlib.c */
542/* lib/gvc/gvevent.c */
543
544 job->fit_mode = !job->fit_mode;
545 if (job->fit_mode) {
546 /* FIXME - this code looks wrong */
547 int dflt_width, dflt_height;
548 dflt_width = job->width;
549 dflt_height = job->height;
550 job->zoom =
551 MIN((double) job->width / (double) dflt_width,
552 (double) job->height / (double) dflt_height);
553 job->focus.x = 0.0;
554 job->focus.y = 0.0;
555 job->needs_refresh = 1;
556 }
557 return 0;
558}
559
560static void gvevent_modify (GVJ_t * job, const char *name, const char *value)
561{
562 /* FIXME */
563}
564
565static void gvevent_delete (GVJ_t * job)
566{
567 /* FIXME */
568}
569
570static void gvevent_read (GVJ_t * job, const char *filename, const char *layout)
571{
572 FILE *f;
573 GVC_t *gvc;
574 Agraph_t *g = NULL;
575 gvlayout_engine_t *gvle;
576
577 gvc = job->gvc;
578 if (!filename) {
579 g = agread(stdin,NIL(Agdisc_t *)); // continue processing stdin
580 }
581 else {
582 f = fopen(filename, "r");
583 if (!f)
584 return; /* FIXME - need some error handling */
585 g = agread(f,NIL(Agdisc_t *));
586 fclose(f);
587 }
588
589 if (!g)
590 return; /* FIXME - need some error handling */
591
592 if (gvc->g) {
593 gvle = gvc->layout.engine;
594 if (gvle && gvle->cleanup)
595 gvle->cleanup(gvc->g);
596 graph_cleanup(gvc->g);
597 agclose(gvc->g);
598 }
599
600 aginit (g, AGRAPH, "Agraphinfo_t", sizeof(Agraphinfo_t), TRUE);
601 aginit (g, AGNODE, "Agnodeinfo_t", sizeof(Agnodeinfo_t), TRUE);
602 aginit (g, AGEDGE, "Agedgeinfo_t", sizeof(Agedgeinfo_t), TRUE);
603 gvc->g = g;
604 GD_gvc(g) = gvc;
605 if (gvLayout(gvc, g, layout) == -1)
606 return; /* FIXME - need some error handling */
607 job->selected_obj = NULL;
608 job->current_obj = NULL;
609 job->needs_refresh = 1;
610}
611
612static void gvevent_layout (GVJ_t * job, const char *layout)
613{
614 gvLayout(job->gvc, job->gvc->g, layout);
615}
616
617static void gvevent_render (GVJ_t * job, const char *format, const char *filename)
618{
619/* If gvc->jobs is set, a new job for doing the rendering won't be created.
620 * If gvc->active_jobs is set, this will be used in a call to gv_end_job.
621 * If we assume this function is called by an interactive front-end which
622 * actually wants to write a file, the above possibilities can cause problems,
623 * with either gvc->job being NULL or the creation of a new window. To avoid
624 * this, we null out these values for rendering the file, and restore them
625 * afterwards. John may have a better way around this.
626 */
627 GVJ_t* save_jobs;
628 GVJ_t* save_active;
629 if (job->gvc->jobs && (job->gvc->job == NULL)) {
630 save_jobs = job->gvc->jobs;
631 save_active = job->gvc->active_jobs;
632 job->gvc->active_jobs = job->gvc->jobs = NULL;
633 }
634 else
635 save_jobs = NULL;
636 gvRenderFilename(job->gvc, job->gvc->g, format, filename);
637 if (save_jobs) {
638 job->gvc->jobs = save_jobs;
639 job->gvc->active_jobs = save_active;
640 }
641}
642
643
644gvevent_key_binding_t gvevent_key_binding[] = {
645 {"Q", quit_cb},
646 {"Left", left_cb},
647 {"KP_Left", left_cb},
648 {"Right", right_cb},
649 {"KP_Right", right_cb},
650 {"Up", up_cb},
651 {"KP_Up", up_cb},
652 {"Down", down_cb},
653 {"KP_Down", down_cb},
654 {"plus", zoom_in_cb},
655 {"KP_Add", zoom_in_cb},
656 {"minus", zoom_out_cb},
657 {"KP_Subtract", zoom_out_cb},
658 {"F", toggle_fit_cb},
659};
660
661int gvevent_key_binding_size = ARRAY_SIZE(gvevent_key_binding);
662
663gvdevice_callbacks_t gvdevice_callbacks = {
664 gvevent_refresh,
665 gvevent_button_press,
666 gvevent_button_release,
667 gvevent_motion,
668 gvevent_modify,
669 gvevent_delete,
670 gvevent_read,
671 gvevent_layout,
672 gvevent_render,
673};
674