123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551 |
- /*************************************************************************
- * Copyright (c) 2011 AT&T Intellectual Property
- * All rights reserved. This program and the accompanying materials
- * are made available under the terms of the Eclipse Public License v1.0
- * which accompanies this distribution, and is available at
- * https://www.eclipse.org/legal/epl-v10.html
- *
- * Contributors: Details at https://graphviz.org
- *************************************************************************/
- #include "config.h"
- #include <string.h>
- #include <stdbool.h>
- #include <stdlib.h>
- #include <math.h>
- #include <gvc/gvplugin_layout.h>
- #include <gvc/gvcint.h>
- #include <gvc/gvcproc.h>
- #include <common/utils.h>
- #include <util/gv_fopen.h>
- extern void emit_graph(GVJ_t * job, graph_t * g);
- extern int gvLayout(GVC_t *gvc, graph_t *g, const char *engine);
- extern int gvRenderFilename(GVC_t *gvc, graph_t *g, const char *format, const char *filename);
- extern void graph_cleanup(graph_t *g);
- #define PANFACTOR 10
- #define ZOOMFACTOR 1.1
- #define EPSILON .0001
- static char *s_tooltip = "tooltip";
- static char *s_href = "href";
- static char *s_URL = "URL";
- static void gv_graph_state(GVJ_t *job, graph_t *g)
- {
- Agsym_t *a = agfindgraphattr(g, s_href);
- if (!a)
- a = agfindgraphattr(g, s_URL);
- if (a)
- job->selected_href = strdup_and_subst_obj(agxget(g, a), g);
- }
- static void gv_node_state(GVJ_t *job, node_t *n)
- {
- Agsym_t *a = agfindnodeattr(agraphof(n), s_href);
- if (!a)
- a = agfindnodeattr(agraphof(n), s_URL);
- if (a)
- job->selected_href = strdup_and_subst_obj(agxget(n, a), n);
- }
- static void gv_edge_state(GVJ_t *job, edge_t *e)
- {
- Agsym_t *a = agfindedgeattr(agraphof(aghead(e)), s_href);
- if (!a)
- a = agfindedgeattr(agraphof(aghead(e)), s_URL);
- if (a)
- job->selected_href = strdup_and_subst_obj(agxget(e, a), e);
- }
- static void gvevent_refresh(GVJ_t * job)
- {
- graph_t *g = job->gvc->g;
- if (!job->selected_obj) {
- job->selected_obj = g;
- GD_gui_state(g) |= GUI_STATE_SELECTED;
- gv_graph_state(job, g);
- }
- emit_graph(job, g);
- job->has_been_rendered = true;
- }
- /* recursively find innermost cluster containing the point */
- static graph_t *gvevent_find_cluster(graph_t *g, boxf b)
- {
- int i;
- graph_t *sg;
- boxf bb;
- for (i = 1; i <= GD_n_cluster(g); i++) {
- sg = gvevent_find_cluster(GD_clust(g)[i], b);
- if (sg)
- return sg;
- }
- B2BF(GD_bb(g), bb);
- if (OVERLAP(b, bb))
- return g;
- return NULL;
- }
- static void * gvevent_find_obj(graph_t *g, boxf b)
- {
- graph_t *sg;
- node_t *n;
- edge_t *e;
- /* edges might overlap nodes, so search them first */
- for (n = agfstnode(g); n; n = agnxtnode(g, n))
- for (e = agfstout(g, n); e; e = agnxtout(g, e))
- if (overlap_edge(e, b))
- return e;
- /* search graph backwards to get topmost node, in case of overlap */
- for (n = aglstnode(g); n; n = agprvnode(g, n))
- if (overlap_node(n, b))
- return n;
- /* search for innermost cluster */
- sg = gvevent_find_cluster(g, b);
- if (sg)
- return sg;
- /* otherwise - we're always in the graph */
- return g;
- }
- static void gvevent_leave_obj(GVJ_t * job)
- {
- void *obj = job->current_obj;
- if (obj) {
- switch (agobjkind(obj)) {
- case AGRAPH:
- GD_gui_state(obj) &= (unsigned char)~GUI_STATE_ACTIVE;
- break;
- case AGNODE:
- ND_gui_state(obj) &= (unsigned char)~GUI_STATE_ACTIVE;
- break;
- case AGEDGE:
- ED_gui_state(obj) &= (unsigned char)~GUI_STATE_ACTIVE;
- break;
- }
- }
- job->active_tooltip = NULL;
- }
- static void gvevent_enter_obj(GVJ_t * job)
- {
- void *obj;
- graph_t *g;
- edge_t *e;
- node_t *n;
- Agsym_t *a;
- free(job->active_tooltip);
- job->active_tooltip = NULL;
- obj = job->current_obj;
- if (obj) {
- switch (agobjkind(obj)) {
- case AGRAPH:
- g = obj;
- GD_gui_state(g) |= GUI_STATE_ACTIVE;
- a = agfindgraphattr(g, s_tooltip);
- if (a)
- job->active_tooltip = strdup_and_subst_obj(agxget(g, a), obj);
- break;
- case AGNODE:
- n = obj;
- ND_gui_state(n) |= GUI_STATE_ACTIVE;
- a = agfindnodeattr(agraphof(n), s_tooltip);
- if (a)
- job->active_tooltip = strdup_and_subst_obj(agxget(n, a), obj);
- break;
- case AGEDGE:
- e = obj;
- ED_gui_state(e) |= GUI_STATE_ACTIVE;
- a = agfindedgeattr(agraphof(aghead(e)), s_tooltip);
- if (a)
- job->active_tooltip = strdup_and_subst_obj(agxget(e, a), obj);
- break;
- }
- }
- }
- static pointf pointer2graph (GVJ_t *job, pointf pointer)
- {
- pointf p;
- /* transform position in device units to position in graph units */
- if (job->rotation) {
- p.x = pointer.y / (job->zoom * job->devscale.y) - job->translation.x;
- p.y = -pointer.x / (job->zoom * job->devscale.x) - job->translation.y;
- }
- else {
- p.x = pointer.x / (job->zoom * job->devscale.x) - job->translation.x;
- p.y = pointer.y / (job->zoom * job->devscale.y) - job->translation.y;
- }
- return p;
- }
- /* CLOSEENOUGH is in 1/72 - probably should be a feature... */
- #define CLOSEENOUGH 1
- static void gvevent_find_current_obj(GVJ_t * job, pointf pointer)
- {
- void *obj;
- boxf b;
- double closeenough;
- pointf p;
- p = pointer2graph (job, pointer);
- /* convert window point to graph coordinates */
- closeenough = CLOSEENOUGH / job->zoom;
- b.UR.x = p.x + closeenough;
- b.UR.y = p.y + closeenough;
- b.LL.x = p.x - closeenough;
- b.LL.y = p.y - closeenough;
- obj = gvevent_find_obj(job->gvc->g, b);
- if (obj != job->current_obj) {
- gvevent_leave_obj(job);
- job->current_obj = obj;
- gvevent_enter_obj(job);
- job->needs_refresh = true;
- }
- }
- static void gvevent_select_current_obj(GVJ_t * job)
- {
- void *obj;
- obj = job->selected_obj;
- if (obj) {
- switch (agobjkind(obj)) {
- case AGRAPH:
- GD_gui_state(obj) |= GUI_STATE_VISITED;
- GD_gui_state(obj) &= (unsigned char)~GUI_STATE_SELECTED;
- break;
- case AGNODE:
- ND_gui_state(obj) |= GUI_STATE_VISITED;
- ND_gui_state(obj) &= (unsigned char)~GUI_STATE_SELECTED;
- break;
- case AGEDGE:
- ED_gui_state(obj) |= GUI_STATE_VISITED;
- ED_gui_state(obj) &= (unsigned char)~GUI_STATE_SELECTED;
- break;
- }
- }
- free(job->selected_href);
- job->selected_href = NULL;
- obj = job->selected_obj = job->current_obj;
- if (obj) {
- switch (agobjkind(obj)) {
- case AGRAPH:
- GD_gui_state(obj) |= GUI_STATE_SELECTED;
- gv_graph_state(job, obj);
- break;
- case AGNODE:
- ND_gui_state(obj) |= GUI_STATE_SELECTED;
- gv_node_state(job, obj);
- break;
- case AGEDGE:
- ED_gui_state(obj) |= GUI_STATE_SELECTED;
- gv_edge_state(job, obj);
- break;
- }
- }
- }
- static void gvevent_button_press(GVJ_t * job, int button, pointf pointer)
- {
- switch (button) {
- case 1: /* select / create in edit mode */
- gvevent_find_current_obj(job, pointer);
- gvevent_select_current_obj(job);
- job->click = true;
- job->button = (unsigned char)button;
- job->needs_refresh = true;
- break;
- case 2: /* pan */
- job->click = true;
- job->button = (unsigned char)button;
- job->needs_refresh = true;
- break;
- case 3: /* insert node or edge */
- gvevent_find_current_obj(job, pointer);
- job->click = true;
- job->button = (unsigned char)button;
- job->needs_refresh = true;
- break;
- case 4:
- /* scrollwheel zoom in at current mouse x,y */
- /* FIXME - should code window 0,0 point as feature with Y_GOES_DOWN */
- job->fit_mode = false;
- if (job->rotation) {
- job->focus.x -= (pointer.y - job->height / 2.)
- * (ZOOMFACTOR - 1.) / (job->zoom * job->devscale.y);
- job->focus.y += (pointer.x - job->width / 2.)
- * (ZOOMFACTOR - 1.) / (job->zoom * job->devscale.x);
- }
- else {
- job->focus.x += (pointer.x - job->width / 2.)
- * (ZOOMFACTOR - 1.) / (job->zoom * job->devscale.x);
- job->focus.y += (pointer.y - job->height / 2.)
- * (ZOOMFACTOR - 1.) / (job->zoom * job->devscale.y);
- }
- job->zoom *= ZOOMFACTOR;
- job->needs_refresh = true;
- break;
- case 5: /* scrollwheel zoom out at current mouse x,y */
- job->fit_mode = false;
- job->zoom /= ZOOMFACTOR;
- if (job->rotation) {
- job->focus.x += (pointer.y - job->height / 2.)
- * (ZOOMFACTOR - 1.) / (job->zoom * job->devscale.y);
- job->focus.y -= (pointer.x - job->width / 2.)
- * (ZOOMFACTOR - 1.) / (job->zoom * job->devscale.x);
- }
- else {
- job->focus.x -= (pointer.x - job->width / 2.)
- * (ZOOMFACTOR - 1.) / (job->zoom * job->devscale.x);
- job->focus.y -= (pointer.y - job->height / 2.)
- * (ZOOMFACTOR - 1.) / (job->zoom * job->devscale.y);
- }
- job->needs_refresh = true;
- break;
- }
- job->oldpointer = pointer;
- }
- static void gvevent_button_release(GVJ_t *job, int button, pointf pointer)
- {
- (void)button;
- (void)pointer;
- job->click = false;
- job->button = false;
- }
- static void gvevent_motion(GVJ_t * job, pointf pointer)
- {
- /* dx,dy change in position, in device independent points */
- double dx = (pointer.x - job->oldpointer.x) / job->devscale.x;
- double dy = (pointer.y - job->oldpointer.y) / job->devscale.y;
- if (fabs(dx) < EPSILON && fabs(dy) < EPSILON) /* ignore motion events with no motion */
- return;
- switch (job->button) {
- case 0: /* drag with no button - */
- gvevent_find_current_obj(job, pointer);
- break;
- case 1: /* drag with button 1 - drag object */
- /* FIXME - to be implemented */
- break;
- case 2: /* drag with button 2 - pan graph */
- if (job->rotation) {
- job->focus.x -= dy / job->zoom;
- job->focus.y += dx / job->zoom;
- }
- else {
- job->focus.x -= dx / job->zoom;
- job->focus.y -= dy / job->zoom;
- }
- job->needs_refresh = true;
- break;
- case 3: /* drag with button 3 - drag inserted node or uncompleted edge */
- break;
- }
- job->oldpointer = pointer;
- }
- static int quit_cb(GVJ_t * job)
- {
- (void)job;
- return 1;
- }
- static int left_cb(GVJ_t * job)
- {
- job->fit_mode = false;
- job->focus.x += PANFACTOR / job->zoom;
- job->needs_refresh = true;
- return 0;
- }
- static int right_cb(GVJ_t * job)
- {
- job->fit_mode = false;
- job->focus.x -= PANFACTOR / job->zoom;
- job->needs_refresh = true;
- return 0;
- }
- static int up_cb(GVJ_t * job)
- {
- job->fit_mode = false;
- job->focus.y += -(PANFACTOR / job->zoom);
- job->needs_refresh = true;
- return 0;
- }
- static int down_cb(GVJ_t * job)
- {
- job->fit_mode = false;
- job->focus.y -= -(PANFACTOR / job->zoom);
- job->needs_refresh = true;
- return 0;
- }
- static int zoom_in_cb(GVJ_t * job)
- {
- job->fit_mode = false;
- job->zoom *= ZOOMFACTOR;
- job->needs_refresh = true;
- return 0;
- }
- static int zoom_out_cb(GVJ_t * job)
- {
- job->fit_mode = false;
- job->zoom /= ZOOMFACTOR;
- job->needs_refresh = true;
- return 0;
- }
- static int toggle_fit_cb(GVJ_t * job)
- {
- /*FIXME - should allow for margins */
- /* - similar zoom_to_fit code exists in: */
- /* plugin/xlib/gvdevice_xlib.c */
- /* lib/gvc/gvevent.c */
- job->fit_mode = !job->fit_mode;
- if (job->fit_mode) {
- /* FIXME - this code looks wrong */
- int dflt_width, dflt_height;
- dflt_width = job->width;
- dflt_height = job->height;
- job->zoom =
- MIN((double) job->width / (double) dflt_width,
- (double) job->height / (double) dflt_height);
- job->focus.x = 0.0;
- job->focus.y = 0.0;
- job->needs_refresh = true;
- }
- return 0;
- }
- static void gvevent_read (GVJ_t * job, const char *filename, const char *layout)
- {
- FILE *f;
- GVC_t *gvc;
- Agraph_t *g = NULL;
- gvlayout_engine_t *gvle;
- gvc = job->gvc;
- if (!filename) {
- g = agread(stdin,NULL); // continue processing stdin
- }
- else {
- f = gv_fopen(filename, "r");
- if (!f)
- return; /* FIXME - need some error handling */
- g = agread(f,NULL);
- fclose(f);
- }
- if (!g)
- return; /* FIXME - need some error handling */
- if (gvc->g) {
- gvle = gvc->layout.engine;
- if (gvle && gvle->cleanup)
- gvle->cleanup(gvc->g);
- graph_cleanup(gvc->g);
- agclose(gvc->g);
- }
- aginit (g, AGRAPH, "Agraphinfo_t", sizeof(Agraphinfo_t), true);
- aginit (g, AGNODE, "Agnodeinfo_t", sizeof(Agnodeinfo_t), true);
- aginit (g, AGEDGE, "Agedgeinfo_t", sizeof(Agedgeinfo_t), true);
- gvc->g = g;
- GD_gvc(g) = gvc;
- if (gvLayout(gvc, g, layout) == -1)
- return; /* FIXME - need some error handling */
- job->selected_obj = NULL;
- job->current_obj = NULL;
- job->needs_refresh = true;
- }
- static void gvevent_layout (GVJ_t * job, const char *layout)
- {
- gvLayout(job->gvc, job->gvc->g, layout);
- }
- static void gvevent_render (GVJ_t * job, const char *format, const char *filename)
- {
- /* If gvc->jobs is set, a new job for doing the rendering won't be created.
- * If gvc->active_jobs is set, this will be used in a call to gv_end_job.
- * If we assume this function is called by an interactive front-end which
- * actually wants to write a file, the above possibilities can cause problems,
- * with either gvc->job being NULL or the creation of a new window. To avoid
- * this, we null out these values for rendering the file, and restore them
- * afterwards. John may have a better way around this.
- */
- GVJ_t* save_jobs;
- GVJ_t* save_active;
- if (job->gvc->jobs && (job->gvc->job == NULL)) {
- save_jobs = job->gvc->jobs;
- save_active = job->gvc->active_jobs;
- job->gvc->active_jobs = job->gvc->jobs = NULL;
- }
- else
- save_jobs = NULL;
- gvRenderFilename(job->gvc, job->gvc->g, format, filename);
- if (save_jobs) {
- job->gvc->jobs = save_jobs;
- job->gvc->active_jobs = save_active;
- }
- }
- gvevent_key_binding_t gvevent_key_binding[] = {
- {"Q", quit_cb},
- {"Left", left_cb},
- {"KP_Left", left_cb},
- {"Right", right_cb},
- {"KP_Right", right_cb},
- {"Up", up_cb},
- {"KP_Up", up_cb},
- {"Down", down_cb},
- {"KP_Down", down_cb},
- {"plus", zoom_in_cb},
- {"KP_Add", zoom_in_cb},
- {"minus", zoom_out_cb},
- {"KP_Subtract", zoom_out_cb},
- {"F", toggle_fit_cb},
- };
- const size_t gvevent_key_binding_size =
- sizeof(gvevent_key_binding) / sizeof(gvevent_key_binding[0]);
- gvdevice_callbacks_t gvdevice_callbacks = {
- gvevent_refresh,
- gvevent_button_press,
- gvevent_button_release,
- gvevent_motion,
- NULL, // modify
- NULL, // del
- gvevent_read,
- gvevent_layout,
- gvevent_render,
- };
|