gvevent.c 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551
  1. /*************************************************************************
  2. * Copyright (c) 2011 AT&T Intellectual Property
  3. * All rights reserved. This program and the accompanying materials
  4. * are made available under the terms of the Eclipse Public License v1.0
  5. * which accompanies this distribution, and is available at
  6. * https://www.eclipse.org/legal/epl-v10.html
  7. *
  8. * Contributors: Details at https://graphviz.org
  9. *************************************************************************/
  10. #include "config.h"
  11. #include <string.h>
  12. #include <stdbool.h>
  13. #include <stdlib.h>
  14. #include <math.h>
  15. #include <gvc/gvplugin_layout.h>
  16. #include <gvc/gvcint.h>
  17. #include <gvc/gvcproc.h>
  18. #include <common/utils.h>
  19. #include <util/gv_fopen.h>
  20. extern void emit_graph(GVJ_t * job, graph_t * g);
  21. extern int gvLayout(GVC_t *gvc, graph_t *g, const char *engine);
  22. extern int gvRenderFilename(GVC_t *gvc, graph_t *g, const char *format, const char *filename);
  23. extern void graph_cleanup(graph_t *g);
  24. #define PANFACTOR 10
  25. #define ZOOMFACTOR 1.1
  26. #define EPSILON .0001
  27. static char *s_tooltip = "tooltip";
  28. static char *s_href = "href";
  29. static char *s_URL = "URL";
  30. static void gv_graph_state(GVJ_t *job, graph_t *g)
  31. {
  32. Agsym_t *a = agfindgraphattr(g, s_href);
  33. if (!a)
  34. a = agfindgraphattr(g, s_URL);
  35. if (a)
  36. job->selected_href = strdup_and_subst_obj(agxget(g, a), g);
  37. }
  38. static void gv_node_state(GVJ_t *job, node_t *n)
  39. {
  40. Agsym_t *a = agfindnodeattr(agraphof(n), s_href);
  41. if (!a)
  42. a = agfindnodeattr(agraphof(n), s_URL);
  43. if (a)
  44. job->selected_href = strdup_and_subst_obj(agxget(n, a), n);
  45. }
  46. static void gv_edge_state(GVJ_t *job, edge_t *e)
  47. {
  48. Agsym_t *a = agfindedgeattr(agraphof(aghead(e)), s_href);
  49. if (!a)
  50. a = agfindedgeattr(agraphof(aghead(e)), s_URL);
  51. if (a)
  52. job->selected_href = strdup_and_subst_obj(agxget(e, a), e);
  53. }
  54. static void gvevent_refresh(GVJ_t * job)
  55. {
  56. graph_t *g = job->gvc->g;
  57. if (!job->selected_obj) {
  58. job->selected_obj = g;
  59. GD_gui_state(g) |= GUI_STATE_SELECTED;
  60. gv_graph_state(job, g);
  61. }
  62. emit_graph(job, g);
  63. job->has_been_rendered = true;
  64. }
  65. /* recursively find innermost cluster containing the point */
  66. static graph_t *gvevent_find_cluster(graph_t *g, boxf b)
  67. {
  68. int i;
  69. graph_t *sg;
  70. boxf bb;
  71. for (i = 1; i <= GD_n_cluster(g); i++) {
  72. sg = gvevent_find_cluster(GD_clust(g)[i], b);
  73. if (sg)
  74. return sg;
  75. }
  76. B2BF(GD_bb(g), bb);
  77. if (OVERLAP(b, bb))
  78. return g;
  79. return NULL;
  80. }
  81. static void * gvevent_find_obj(graph_t *g, boxf b)
  82. {
  83. graph_t *sg;
  84. node_t *n;
  85. edge_t *e;
  86. /* edges might overlap nodes, so search them first */
  87. for (n = agfstnode(g); n; n = agnxtnode(g, n))
  88. for (e = agfstout(g, n); e; e = agnxtout(g, e))
  89. if (overlap_edge(e, b))
  90. return e;
  91. /* search graph backwards to get topmost node, in case of overlap */
  92. for (n = aglstnode(g); n; n = agprvnode(g, n))
  93. if (overlap_node(n, b))
  94. return n;
  95. /* search for innermost cluster */
  96. sg = gvevent_find_cluster(g, b);
  97. if (sg)
  98. return sg;
  99. /* otherwise - we're always in the graph */
  100. return g;
  101. }
  102. static void gvevent_leave_obj(GVJ_t * job)
  103. {
  104. void *obj = job->current_obj;
  105. if (obj) {
  106. switch (agobjkind(obj)) {
  107. case AGRAPH:
  108. GD_gui_state(obj) &= (unsigned char)~GUI_STATE_ACTIVE;
  109. break;
  110. case AGNODE:
  111. ND_gui_state(obj) &= (unsigned char)~GUI_STATE_ACTIVE;
  112. break;
  113. case AGEDGE:
  114. ED_gui_state(obj) &= (unsigned char)~GUI_STATE_ACTIVE;
  115. break;
  116. }
  117. }
  118. job->active_tooltip = NULL;
  119. }
  120. static void gvevent_enter_obj(GVJ_t * job)
  121. {
  122. void *obj;
  123. graph_t *g;
  124. edge_t *e;
  125. node_t *n;
  126. Agsym_t *a;
  127. free(job->active_tooltip);
  128. job->active_tooltip = NULL;
  129. obj = job->current_obj;
  130. if (obj) {
  131. switch (agobjkind(obj)) {
  132. case AGRAPH:
  133. g = obj;
  134. GD_gui_state(g) |= GUI_STATE_ACTIVE;
  135. a = agfindgraphattr(g, s_tooltip);
  136. if (a)
  137. job->active_tooltip = strdup_and_subst_obj(agxget(g, a), obj);
  138. break;
  139. case AGNODE:
  140. n = obj;
  141. ND_gui_state(n) |= GUI_STATE_ACTIVE;
  142. a = agfindnodeattr(agraphof(n), s_tooltip);
  143. if (a)
  144. job->active_tooltip = strdup_and_subst_obj(agxget(n, a), obj);
  145. break;
  146. case AGEDGE:
  147. e = obj;
  148. ED_gui_state(e) |= GUI_STATE_ACTIVE;
  149. a = agfindedgeattr(agraphof(aghead(e)), s_tooltip);
  150. if (a)
  151. job->active_tooltip = strdup_and_subst_obj(agxget(e, a), obj);
  152. break;
  153. }
  154. }
  155. }
  156. static pointf pointer2graph (GVJ_t *job, pointf pointer)
  157. {
  158. pointf p;
  159. /* transform position in device units to position in graph units */
  160. if (job->rotation) {
  161. p.x = pointer.y / (job->zoom * job->devscale.y) - job->translation.x;
  162. p.y = -pointer.x / (job->zoom * job->devscale.x) - job->translation.y;
  163. }
  164. else {
  165. p.x = pointer.x / (job->zoom * job->devscale.x) - job->translation.x;
  166. p.y = pointer.y / (job->zoom * job->devscale.y) - job->translation.y;
  167. }
  168. return p;
  169. }
  170. /* CLOSEENOUGH is in 1/72 - probably should be a feature... */
  171. #define CLOSEENOUGH 1
  172. static void gvevent_find_current_obj(GVJ_t * job, pointf pointer)
  173. {
  174. void *obj;
  175. boxf b;
  176. double closeenough;
  177. pointf p;
  178. p = pointer2graph (job, pointer);
  179. /* convert window point to graph coordinates */
  180. closeenough = CLOSEENOUGH / job->zoom;
  181. b.UR.x = p.x + closeenough;
  182. b.UR.y = p.y + closeenough;
  183. b.LL.x = p.x - closeenough;
  184. b.LL.y = p.y - closeenough;
  185. obj = gvevent_find_obj(job->gvc->g, b);
  186. if (obj != job->current_obj) {
  187. gvevent_leave_obj(job);
  188. job->current_obj = obj;
  189. gvevent_enter_obj(job);
  190. job->needs_refresh = true;
  191. }
  192. }
  193. static void gvevent_select_current_obj(GVJ_t * job)
  194. {
  195. void *obj;
  196. obj = job->selected_obj;
  197. if (obj) {
  198. switch (agobjkind(obj)) {
  199. case AGRAPH:
  200. GD_gui_state(obj) |= GUI_STATE_VISITED;
  201. GD_gui_state(obj) &= (unsigned char)~GUI_STATE_SELECTED;
  202. break;
  203. case AGNODE:
  204. ND_gui_state(obj) |= GUI_STATE_VISITED;
  205. ND_gui_state(obj) &= (unsigned char)~GUI_STATE_SELECTED;
  206. break;
  207. case AGEDGE:
  208. ED_gui_state(obj) |= GUI_STATE_VISITED;
  209. ED_gui_state(obj) &= (unsigned char)~GUI_STATE_SELECTED;
  210. break;
  211. }
  212. }
  213. free(job->selected_href);
  214. job->selected_href = NULL;
  215. obj = job->selected_obj = job->current_obj;
  216. if (obj) {
  217. switch (agobjkind(obj)) {
  218. case AGRAPH:
  219. GD_gui_state(obj) |= GUI_STATE_SELECTED;
  220. gv_graph_state(job, obj);
  221. break;
  222. case AGNODE:
  223. ND_gui_state(obj) |= GUI_STATE_SELECTED;
  224. gv_node_state(job, obj);
  225. break;
  226. case AGEDGE:
  227. ED_gui_state(obj) |= GUI_STATE_SELECTED;
  228. gv_edge_state(job, obj);
  229. break;
  230. }
  231. }
  232. }
  233. static void gvevent_button_press(GVJ_t * job, int button, pointf pointer)
  234. {
  235. switch (button) {
  236. case 1: /* select / create in edit mode */
  237. gvevent_find_current_obj(job, pointer);
  238. gvevent_select_current_obj(job);
  239. job->click = true;
  240. job->button = (unsigned char)button;
  241. job->needs_refresh = true;
  242. break;
  243. case 2: /* pan */
  244. job->click = true;
  245. job->button = (unsigned char)button;
  246. job->needs_refresh = true;
  247. break;
  248. case 3: /* insert node or edge */
  249. gvevent_find_current_obj(job, pointer);
  250. job->click = true;
  251. job->button = (unsigned char)button;
  252. job->needs_refresh = true;
  253. break;
  254. case 4:
  255. /* scrollwheel zoom in at current mouse x,y */
  256. /* FIXME - should code window 0,0 point as feature with Y_GOES_DOWN */
  257. job->fit_mode = false;
  258. if (job->rotation) {
  259. job->focus.x -= (pointer.y - job->height / 2.)
  260. * (ZOOMFACTOR - 1.) / (job->zoom * job->devscale.y);
  261. job->focus.y += (pointer.x - job->width / 2.)
  262. * (ZOOMFACTOR - 1.) / (job->zoom * job->devscale.x);
  263. }
  264. else {
  265. job->focus.x += (pointer.x - job->width / 2.)
  266. * (ZOOMFACTOR - 1.) / (job->zoom * job->devscale.x);
  267. job->focus.y += (pointer.y - job->height / 2.)
  268. * (ZOOMFACTOR - 1.) / (job->zoom * job->devscale.y);
  269. }
  270. job->zoom *= ZOOMFACTOR;
  271. job->needs_refresh = true;
  272. break;
  273. case 5: /* scrollwheel zoom out at current mouse x,y */
  274. job->fit_mode = false;
  275. job->zoom /= ZOOMFACTOR;
  276. if (job->rotation) {
  277. job->focus.x += (pointer.y - job->height / 2.)
  278. * (ZOOMFACTOR - 1.) / (job->zoom * job->devscale.y);
  279. job->focus.y -= (pointer.x - job->width / 2.)
  280. * (ZOOMFACTOR - 1.) / (job->zoom * job->devscale.x);
  281. }
  282. else {
  283. job->focus.x -= (pointer.x - job->width / 2.)
  284. * (ZOOMFACTOR - 1.) / (job->zoom * job->devscale.x);
  285. job->focus.y -= (pointer.y - job->height / 2.)
  286. * (ZOOMFACTOR - 1.) / (job->zoom * job->devscale.y);
  287. }
  288. job->needs_refresh = true;
  289. break;
  290. }
  291. job->oldpointer = pointer;
  292. }
  293. static void gvevent_button_release(GVJ_t *job, int button, pointf pointer)
  294. {
  295. (void)button;
  296. (void)pointer;
  297. job->click = false;
  298. job->button = false;
  299. }
  300. static void gvevent_motion(GVJ_t * job, pointf pointer)
  301. {
  302. /* dx,dy change in position, in device independent points */
  303. double dx = (pointer.x - job->oldpointer.x) / job->devscale.x;
  304. double dy = (pointer.y - job->oldpointer.y) / job->devscale.y;
  305. if (fabs(dx) < EPSILON && fabs(dy) < EPSILON) /* ignore motion events with no motion */
  306. return;
  307. switch (job->button) {
  308. case 0: /* drag with no button - */
  309. gvevent_find_current_obj(job, pointer);
  310. break;
  311. case 1: /* drag with button 1 - drag object */
  312. /* FIXME - to be implemented */
  313. break;
  314. case 2: /* drag with button 2 - pan graph */
  315. if (job->rotation) {
  316. job->focus.x -= dy / job->zoom;
  317. job->focus.y += dx / job->zoom;
  318. }
  319. else {
  320. job->focus.x -= dx / job->zoom;
  321. job->focus.y -= dy / job->zoom;
  322. }
  323. job->needs_refresh = true;
  324. break;
  325. case 3: /* drag with button 3 - drag inserted node or uncompleted edge */
  326. break;
  327. }
  328. job->oldpointer = pointer;
  329. }
  330. static int quit_cb(GVJ_t * job)
  331. {
  332. (void)job;
  333. return 1;
  334. }
  335. static int left_cb(GVJ_t * job)
  336. {
  337. job->fit_mode = false;
  338. job->focus.x += PANFACTOR / job->zoom;
  339. job->needs_refresh = true;
  340. return 0;
  341. }
  342. static int right_cb(GVJ_t * job)
  343. {
  344. job->fit_mode = false;
  345. job->focus.x -= PANFACTOR / job->zoom;
  346. job->needs_refresh = true;
  347. return 0;
  348. }
  349. static int up_cb(GVJ_t * job)
  350. {
  351. job->fit_mode = false;
  352. job->focus.y += -(PANFACTOR / job->zoom);
  353. job->needs_refresh = true;
  354. return 0;
  355. }
  356. static int down_cb(GVJ_t * job)
  357. {
  358. job->fit_mode = false;
  359. job->focus.y -= -(PANFACTOR / job->zoom);
  360. job->needs_refresh = true;
  361. return 0;
  362. }
  363. static int zoom_in_cb(GVJ_t * job)
  364. {
  365. job->fit_mode = false;
  366. job->zoom *= ZOOMFACTOR;
  367. job->needs_refresh = true;
  368. return 0;
  369. }
  370. static int zoom_out_cb(GVJ_t * job)
  371. {
  372. job->fit_mode = false;
  373. job->zoom /= ZOOMFACTOR;
  374. job->needs_refresh = true;
  375. return 0;
  376. }
  377. static int toggle_fit_cb(GVJ_t * job)
  378. {
  379. /*FIXME - should allow for margins */
  380. /* - similar zoom_to_fit code exists in: */
  381. /* plugin/xlib/gvdevice_xlib.c */
  382. /* lib/gvc/gvevent.c */
  383. job->fit_mode = !job->fit_mode;
  384. if (job->fit_mode) {
  385. /* FIXME - this code looks wrong */
  386. int dflt_width, dflt_height;
  387. dflt_width = job->width;
  388. dflt_height = job->height;
  389. job->zoom =
  390. MIN((double) job->width / (double) dflt_width,
  391. (double) job->height / (double) dflt_height);
  392. job->focus.x = 0.0;
  393. job->focus.y = 0.0;
  394. job->needs_refresh = true;
  395. }
  396. return 0;
  397. }
  398. static void gvevent_read (GVJ_t * job, const char *filename, const char *layout)
  399. {
  400. FILE *f;
  401. GVC_t *gvc;
  402. Agraph_t *g = NULL;
  403. gvlayout_engine_t *gvle;
  404. gvc = job->gvc;
  405. if (!filename) {
  406. g = agread(stdin,NULL); // continue processing stdin
  407. }
  408. else {
  409. f = gv_fopen(filename, "r");
  410. if (!f)
  411. return; /* FIXME - need some error handling */
  412. g = agread(f,NULL);
  413. fclose(f);
  414. }
  415. if (!g)
  416. return; /* FIXME - need some error handling */
  417. if (gvc->g) {
  418. gvle = gvc->layout.engine;
  419. if (gvle && gvle->cleanup)
  420. gvle->cleanup(gvc->g);
  421. graph_cleanup(gvc->g);
  422. agclose(gvc->g);
  423. }
  424. aginit (g, AGRAPH, "Agraphinfo_t", sizeof(Agraphinfo_t), true);
  425. aginit (g, AGNODE, "Agnodeinfo_t", sizeof(Agnodeinfo_t), true);
  426. aginit (g, AGEDGE, "Agedgeinfo_t", sizeof(Agedgeinfo_t), true);
  427. gvc->g = g;
  428. GD_gvc(g) = gvc;
  429. if (gvLayout(gvc, g, layout) == -1)
  430. return; /* FIXME - need some error handling */
  431. job->selected_obj = NULL;
  432. job->current_obj = NULL;
  433. job->needs_refresh = true;
  434. }
  435. static void gvevent_layout (GVJ_t * job, const char *layout)
  436. {
  437. gvLayout(job->gvc, job->gvc->g, layout);
  438. }
  439. static void gvevent_render (GVJ_t * job, const char *format, const char *filename)
  440. {
  441. /* If gvc->jobs is set, a new job for doing the rendering won't be created.
  442. * If gvc->active_jobs is set, this will be used in a call to gv_end_job.
  443. * If we assume this function is called by an interactive front-end which
  444. * actually wants to write a file, the above possibilities can cause problems,
  445. * with either gvc->job being NULL or the creation of a new window. To avoid
  446. * this, we null out these values for rendering the file, and restore them
  447. * afterwards. John may have a better way around this.
  448. */
  449. GVJ_t* save_jobs;
  450. GVJ_t* save_active;
  451. if (job->gvc->jobs && (job->gvc->job == NULL)) {
  452. save_jobs = job->gvc->jobs;
  453. save_active = job->gvc->active_jobs;
  454. job->gvc->active_jobs = job->gvc->jobs = NULL;
  455. }
  456. else
  457. save_jobs = NULL;
  458. gvRenderFilename(job->gvc, job->gvc->g, format, filename);
  459. if (save_jobs) {
  460. job->gvc->jobs = save_jobs;
  461. job->gvc->active_jobs = save_active;
  462. }
  463. }
  464. gvevent_key_binding_t gvevent_key_binding[] = {
  465. {"Q", quit_cb},
  466. {"Left", left_cb},
  467. {"KP_Left", left_cb},
  468. {"Right", right_cb},
  469. {"KP_Right", right_cb},
  470. {"Up", up_cb},
  471. {"KP_Up", up_cb},
  472. {"Down", down_cb},
  473. {"KP_Down", down_cb},
  474. {"plus", zoom_in_cb},
  475. {"KP_Add", zoom_in_cb},
  476. {"minus", zoom_out_cb},
  477. {"KP_Subtract", zoom_out_cb},
  478. {"F", toggle_fit_cb},
  479. };
  480. const size_t gvevent_key_binding_size =
  481. sizeof(gvevent_key_binding) / sizeof(gvevent_key_binding[0]);
  482. gvdevice_callbacks_t gvdevice_callbacks = {
  483. gvevent_refresh,
  484. gvevent_button_press,
  485. gvevent_button_release,
  486. gvevent_motion,
  487. NULL, // modify
  488. NULL, // del
  489. gvevent_read,
  490. gvevent_layout,
  491. gvevent_render,
  492. };