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