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 <stdio.h>
17 #include <stdlib.h>
18 #include <string.h>
19 #include <inttypes.h>
20 #include <unistd.h>
21 #ifdef HAVE_SYS_TIME_H
22 #include <sys/time.h>
23 #endif
24 #ifdef HAVE_SYS_IOCTL_H
25 #include <sys/ioctl.h>
26 #endif
27 #ifdef HAVE_SYS_TYPES_H
28 #include <sys/types.h>
29 #endif
30 #ifdef HAVE_SYS_SELECT_H
31 #include <sys/select.h>
32 #endif
33 #ifdef HAVE_SYS_INOTIFY_H
34 #include <sys/inotify.h>
35 #endif
36 #include <errno.h>
37 #ifdef HAVE_FCNTL_H
38 #include <fcntl.h>
39 #endif
40 
41 #if 0
42 #include <poll.h>
43 #endif
44 
45 #include "gvplugin_device.h"
46 
47 #include <cairo.h>
48 #ifdef CAIRO_HAS_XLIB_SURFACE
49 #include <cairo-xlib.h>
50 #include <X11/Xutil.h>
51 #include <X11/extensions/Xrender.h>
52 
53 typedef struct window_xlib_s {
54     Window win;
55     uint64_t event_mask;
56     Pixmap pix;
57     GC gc;
58     Visual *visual;
59     Colormap cmap;
60     int depth;
61     Atom wm_delete_window_atom;
62 } window_t;
63 
handle_configure_notify(GVJ_t * job,XConfigureEvent * cev)64 static void handle_configure_notify(GVJ_t * job, XConfigureEvent * cev)
65 {
66 /*FIXME - should allow for margins */
67 /*	- similar zoom_to_fit code exists in: */
68 /*	plugin/gtk/callbacks.c */
69 /*	plugin/xlib/gvdevice_xlib.c */
70 /*	lib/gvc/gvevent.c */
71 
72     job->zoom *= 1 + MIN(
73 	((double) cev->width - (double) job->width) / (double) job->width,
74 	((double) cev->height - (double) job->height) / (double) job->height);
75     if (cev->width > job->width || cev->height > job->height)
76         job->has_grown = 1;
77     job->width = cev->width;
78     job->height = cev->height;
79     job->needs_refresh = 1;
80 }
81 
handle_expose(GVJ_t * job,XExposeEvent * eev)82 static void handle_expose(GVJ_t * job, XExposeEvent * eev)
83 {
84     window_t *window;
85 
86     window = (window_t *)job->window;
87     XCopyArea(eev->display, window->pix, eev->window, window->gc,
88               eev->x, eev->y, eev->width, eev->height, eev->x, eev->y);
89 }
90 
handle_client_message(GVJ_t * job,XClientMessageEvent * cmev)91 static void handle_client_message(GVJ_t * job, XClientMessageEvent * cmev)
92 {
93     window_t *window;
94 
95     window = (window_t *)job->window;
96     if (cmev->format == 32
97         && (Atom) cmev->data.l[0] == window->wm_delete_window_atom)
98         exit(0);
99 }
100 
handle_keypress(GVJ_t * job,XKeyEvent * kev)101 static boolean handle_keypress(GVJ_t *job, XKeyEvent *kev)
102 {
103 
104     int i;
105     KeyCode *keycodes;
106 
107     keycodes = (KeyCode *)job->keycodes;
108     for (i=0; i < job->numkeys; i++) {
109 	if (kev->keycode == keycodes[i])
110 	    return (job->keybindings[i].callback)(job);
111     }
112     return FALSE;
113 }
114 
find_argb_visual(Display * dpy,int scr)115 static Visual *find_argb_visual(Display * dpy, int scr)
116 {
117     XVisualInfo *xvi;
118     XVisualInfo template;
119     int nvi;
120     int i;
121     XRenderPictFormat *format;
122     Visual *visual;
123 
124     template.screen = scr;
125     template.depth = 32;
126     template.class = TrueColor;
127     xvi = XGetVisualInfo(dpy,
128                          VisualScreenMask |
129                          VisualDepthMask |
130                          VisualClassMask, &template, &nvi);
131     if (!xvi)
132         return 0;
133     visual = 0;
134     for (i = 0; i < nvi; i++) {
135         format = XRenderFindVisualFormat(dpy, xvi[i].visual);
136         if (format->type == PictTypeDirect && format->direct.alphaMask) {
137             visual = xvi[i].visual;
138             break;
139         }
140     }
141 
142     XFree(xvi);
143     return visual;
144 }
145 
browser_show(GVJ_t * job)146 static void browser_show(GVJ_t *job)
147 {
148 #if defined HAVE_SYS_TYPES_H && defined HAVE_UNISTD_H
149    char *exec_argv[3] = {BROWSER, NULL, NULL};
150    pid_t pid;
151    int err;
152 
153    exec_argv[1] = job->selected_href;
154 
155    pid = fork();
156    if (pid == -1) {
157        fprintf(stderr,"fork failed: %s\n", strerror(errno));
158    }
159    else if (pid == 0) {
160        err = execvp(exec_argv[0], exec_argv);
161        fprintf(stderr,"error starting %s: %s\n", exec_argv[0], strerror(errno));
162    }
163 #else
164    fprintf(stdout,"browser_show: %s\n", job->selected_href);
165 #endif
166 }
167 
handle_xlib_events(GVJ_t * firstjob,Display * dpy)168 static int handle_xlib_events (GVJ_t *firstjob, Display *dpy)
169 {
170     GVJ_t *job;
171     window_t *window;
172     XEvent xev;
173     pointf pointer;
174     int rc = 0;
175 
176     while (XPending(dpy)) {
177         XNextEvent(dpy, &xev);
178 
179         for (job = firstjob; job; job = job->next_active) {
180 	    window = (window_t *)job->window;
181 	    if (xev.xany.window == window->win) {
182                 switch (xev.xany.type) {
183                 case ButtonPress:
184 		    pointer.x = (double)xev.xbutton.x;
185 		    pointer.y = (double)xev.xbutton.y;
186                     (job->callbacks->button_press)(job, xev.xbutton.button, pointer);
187 		    rc++;
188                     break;
189                 case MotionNotify:
190 		    if (job->button) { /* only interested while a button is pressed */
191 		        pointer.x = (double)xev.xbutton.x;
192 		        pointer.y = (double)xev.xbutton.y;
193                         (job->callbacks->motion)(job, pointer);
194 		        rc++;
195 		    }
196                     break;
197                 case ButtonRelease:
198 		    pointer.x = (double)xev.xbutton.x;
199 		    pointer.y = (double)xev.xbutton.y;
200                     (job->callbacks->button_release)(job, xev.xbutton.button, pointer);
201 		    if (job->selected_href && job->selected_href[0] && xev.xbutton.button == 1)
202 		        browser_show(job);
203 		    rc++;
204                     break;
205                 case KeyPress:
206 		    if (handle_keypress(job, &xev.xkey))
207 			return -1;  /* exit code */
208 		    rc++;
209                     break;
210                 case ConfigureNotify:
211                     handle_configure_notify(job, &xev.xconfigure);
212 		    rc++;
213                     break;
214                 case Expose:
215                     handle_expose(job, &xev.xexpose);
216 		    rc++;
217                     break;
218                 case ClientMessage:
219                     handle_client_message(job, &xev.xclient);
220 		    rc++;
221                     break;
222                 }
223 	        break;
224 	    }
225 	}
226     }
227     return rc;
228 }
229 
update_display(GVJ_t * job,Display * dpy)230 static void update_display(GVJ_t *job, Display *dpy)
231 {
232     window_t *window;
233     cairo_surface_t *surface;
234 
235     window = (window_t *)job->window;
236 
237     if (job->has_grown) {
238 	XFreePixmap(dpy, window->pix);
239 	window->pix = XCreatePixmap(dpy, window->win,
240 			job->width, job->height, window->depth);
241 	job->has_grown = 0;
242 	job->needs_refresh = 1;
243     }
244     if (job->needs_refresh) {
245 	XFillRectangle(dpy, window->pix, window->gc, 0, 0,
246                 	job->width, job->height);
247 	surface = cairo_xlib_surface_create(dpy,
248 			window->pix, window->visual,
249 			job->width, job->height);
250     	job->context = (void *)cairo_create(surface);
251 	job->external_context = TRUE;
252         (job->callbacks->refresh)(job);
253 	cairo_surface_destroy(surface);
254 	XCopyArea(dpy, window->pix, window->win, window->gc,
255 			0, 0, job->width, job->height, 0, 0);
256         job->needs_refresh = 0;
257     }
258 }
259 
init_window(GVJ_t * job,Display * dpy,int scr)260 static void init_window(GVJ_t *job, Display *dpy, int scr)
261 {
262     int argb = 0;
263     const char *base = "";
264     XGCValues gcv;
265     XSetWindowAttributes attributes;
266     XWMHints *wmhints;
267     XSizeHints *normalhints;
268     XClassHint *classhint;
269     uint64_t attributemask = 0;
270     char *name;
271     window_t *window;
272     int w, h;
273     double zoom_to_fit;
274 
275     window = (window_t *)malloc(sizeof(window_t));
276     if (window == NULL) {
277 	fprintf(stderr, "Failed to malloc window_t\n");
278 	return;
279     }
280 
281     w = 480;    /* FIXME - w,h should be set by a --geometry commandline option */
282     h = 325;
283 
284     zoom_to_fit = MIN((double) w / (double) job->width,
285 			(double) h / (double) job->height);
286     if (zoom_to_fit < 1.0) /* don't make bigger */
287 	job->zoom *= zoom_to_fit;
288 
289     job->width  = w;    /* use window geometry */
290     job->height = h;
291 
292     job->window = (void *)window;
293     job->fit_mode = 0;
294     job->needs_refresh = 1;
295 
296     if (argb && (window->visual = find_argb_visual(dpy, scr))) {
297         window->cmap = XCreateColormap(dpy, RootWindow(dpy, scr),
298                                     window->visual, AllocNone);
299         attributes.override_redirect = False;
300         attributes.background_pixel = 0;
301         attributes.border_pixel = 0;
302         attributes.colormap = window->cmap;
303         attributemask = ( CWBackPixel
304                           | CWBorderPixel
305 			  | CWOverrideRedirect
306 			  | CWColormap );
307         window->depth = 32;
308     } else {
309         window->cmap = DefaultColormap(dpy, scr);
310         window->visual = DefaultVisual(dpy, scr);
311         attributes.background_pixel = WhitePixel(dpy, scr);
312         attributes.border_pixel = BlackPixel(dpy, scr);
313         attributemask = (CWBackPixel | CWBorderPixel);
314         window->depth = DefaultDepth(dpy, scr);
315     }
316 
317     window->win = XCreateWindow(dpy, RootWindow(dpy, scr),
318                              0, 0, job->width, job->height, 0, window->depth,
319                              InputOutput, window->visual,
320                              attributemask, &attributes);
321 
322     name = malloc(strlen("graphviz: ") + strlen(base) + 1);
323     strcpy(name, "graphviz: ");
324     strcat(name, base);
325 
326     normalhints = XAllocSizeHints();
327     normalhints->flags = 0;
328     normalhints->x = 0;
329     normalhints->y = 0;
330     normalhints->width = job->width;
331     normalhints->height = job->height;
332 
333     classhint = XAllocClassHint();
334     classhint->res_name = "graphviz";
335     classhint->res_class = "Graphviz";
336 
337     wmhints = XAllocWMHints();
338     wmhints->flags = InputHint;
339     wmhints->input = True;
340 
341     Xutf8SetWMProperties(dpy, window->win, name, base, 0, 0,
342                          normalhints, wmhints, classhint);
343     XFree(wmhints);
344     XFree(classhint);
345     XFree(normalhints);
346     free(name);
347 
348     window->pix = XCreatePixmap(dpy, window->win, job->width, job->height,
349 		window->depth);
350     if (argb)
351         gcv.foreground = 0;
352     else
353         gcv.foreground = WhitePixel(dpy, scr);
354     window->gc = XCreateGC(dpy, window->pix, GCForeground, &gcv);
355     update_display(job, dpy);
356 
357     window->event_mask = (
358           ButtonPressMask
359         | ButtonReleaseMask
360         | PointerMotionMask
361         | KeyPressMask
362         | StructureNotifyMask
363         | ExposureMask);
364     XSelectInput(dpy, window->win, window->event_mask);
365     window->wm_delete_window_atom =
366         XInternAtom(dpy, "WM_DELETE_WINDOW", False);
367     XSetWMProtocols(dpy, window->win, &window->wm_delete_window_atom, 1);
368     XMapWindow(dpy, window->win);
369 }
370 
handle_stdin_events(GVJ_t * job,int stdin_fd)371 static int handle_stdin_events(GVJ_t *job, int stdin_fd)
372 {
373     int rc=0;
374 
375     if (feof(stdin))
376 	return -1;
377     (job->callbacks->read)(job, job->input_filename, job->layout_type);
378 
379     rc++;
380     return rc;
381 }
382 
383 #ifdef HAVE_SYS_INOTIFY_H
handle_file_events(GVJ_t * job,int inotify_fd)384 static int handle_file_events(GVJ_t *job, int inotify_fd)
385 {
386     int avail, ret, len, ln, rc = 0;
387     static char *buf;
388     char *bf, *p;
389     struct inotify_event *event;
390 
391     ret = ioctl(inotify_fd, FIONREAD, &avail);
392     if (ret < 0) {
393 	fprintf(stderr,"ioctl() failed\n");
394 	return -1;;
395     }
396 
397     if (avail) {
398         buf = realloc(buf, avail);
399         if (!buf) {
400             fprintf(stderr,"problem with realloc(%d)\n", avail);
401             return -1;
402         }
403         len = read(inotify_fd, buf, avail);
404         if (len != avail) {
405             fprintf(stderr,"avail = %u, len = %u\n", avail, len);
406             return -1;
407         }
408         bf = buf;
409         while (len > 0) {
410     	    event = (struct inotify_event *)bf;
411 	    switch (event->mask) {
412 	    case IN_MODIFY:
413 		p = strrchr(job->input_filename, '/');
414 		if (p)
415 		    p++;
416 		else
417 		    p = job->input_filename;
418 		if (strcmp((char*)(&(event->name)), p) == 0) {
419 		    (job->callbacks->read)(job, job->input_filename, job->layout_type);
420 		    rc++;
421 		}
422 		break;
423 
424             case IN_ACCESS:
425             case IN_ATTRIB:
426             case IN_CLOSE_WRITE:
427             case IN_CLOSE_NOWRITE:
428             case IN_OPEN:
429             case IN_MOVED_FROM:
430             case IN_MOVED_TO:
431             case IN_CREATE:
432             case IN_DELETE:
433             case IN_DELETE_SELF:
434             case IN_MOVE_SELF:
435             case IN_UNMOUNT:
436             case IN_Q_OVERFLOW:
437             case IN_IGNORED:
438             case IN_ISDIR:
439             case IN_ONESHOT:
440 		break;
441     	    }
442 	    ln = event->len + sizeof(struct inotify_event);
443             bf += ln;
444             len -= ln;
445         }
446         if (len != 0) {
447             fprintf(stderr,"length miscalculation, len = %d\n", len);
448             return -1;
449         }
450     }
451     return rc;
452 }
453 #endif
454 
xlib_initialize(GVJ_t * firstjob)455 static void xlib_initialize(GVJ_t *firstjob)
456 {
457     Display *dpy;
458     KeySym keysym;
459     KeyCode *keycodes;
460     const char *display_name = NULL;
461     int i, scr;
462 
463     dpy = XOpenDisplay(display_name);
464     if (dpy == NULL) {
465 	fprintf(stderr, "Failed to open XLIB display: %s\n",
466 		XDisplayName(NULL));
467 	return;
468     }
469     scr = DefaultScreen(dpy);
470 
471     firstjob->display = (void*)dpy;
472     firstjob->screen = scr;
473 
474     keycodes = (KeyCode *)malloc(firstjob->numkeys * sizeof(KeyCode));
475     if (keycodes == NULL) {
476         fprintf(stderr, "Failed to malloc %d*KeyCode\n", firstjob->numkeys);
477         return;
478     }
479     for (i = 0; i < firstjob->numkeys; i++) {
480         keysym = XStringToKeysym(firstjob->keybindings[i].keystring);
481         if (keysym == NoSymbol)
482             fprintf(stderr, "ERROR: No keysym for \"%s\"\n",
483 		firstjob->keybindings[i].keystring);
484         else
485             keycodes[i] = XKeysymToKeycode(dpy, keysym);
486     }
487     firstjob->keycodes = (void*)keycodes;
488 
489     firstjob->device_dpi.x = DisplayWidth(dpy, scr) * 25.4 / DisplayWidthMM(dpy, scr);
490     firstjob->device_dpi.y = DisplayHeight(dpy, scr) * 25.4 / DisplayHeightMM(dpy, scr);
491     firstjob->device_sets_dpi = TRUE;
492 }
493 
xlib_finalize(GVJ_t * firstjob)494 static void xlib_finalize(GVJ_t *firstjob)
495 {
496     GVJ_t *job;
497     Display *dpy = (Display *)(firstjob->display);
498     int scr = firstjob->screen;
499     KeyCode *keycodes= firstjob->keycodes;
500     int numfds, stdin_fd=0, xlib_fd, ret, events;
501     fd_set rfds;
502     boolean watching_stdin_p = FALSE;
503 #ifdef HAVE_SYS_INOTIFY_H
504     int wd=0;
505     int inotify_fd=0;
506     boolean watching_file_p = FALSE;
507     static char *dir;
508     char *p, *cwd = NULL;
509 
510     inotify_fd = inotify_init();
511     if (inotify_fd < 0) {
512 	fprintf(stderr,"inotify_init() failed\n");
513 	return;
514     }
515 #endif
516 
517     numfds = xlib_fd = XConnectionNumber(dpy);
518 
519     if (firstjob->input_filename) {
520         if (firstjob->graph_index == 0) {
521 #ifdef HAVE_SYS_INOTIFY_H
522 	    watching_file_p = TRUE;
523 
524 	    if (firstjob->input_filename[0] != '/') {
525     	        cwd = getcwd(NULL, 0);
526 	        dir = realloc(dir, strlen(cwd) + 1 + strlen(firstjob->input_filename) + 1);
527 	        strcpy(dir, cwd);
528 	        strcat(dir, "/");
529 	        strcat(dir, firstjob->input_filename);
530 	        free(cwd);
531 	    }
532 	    else {
533 	        dir = realloc(dir, strlen(firstjob->input_filename) + 1);
534 	        strcpy(dir, firstjob->input_filename);
535 	    }
536 	    p = strrchr(dir,'/');
537 	    *p = '\0';
538 
539     	    wd = inotify_add_watch(inotify_fd, dir, IN_MODIFY );
540 
541             numfds = MAX(inotify_fd, numfds);
542 #endif
543 	}
544     }
545     else {
546 	watching_stdin_p = TRUE;
547 	stdin_fd = fcntl(STDIN_FILENO, F_DUPFD, 0);
548 	numfds = MAX(stdin_fd, numfds);
549     }
550 
551     for (job = firstjob; job; job = job->next_active)
552 	init_window(job, dpy, scr);
553 
554     /* This is the event loop */
555     FD_ZERO(&rfds);
556     while (1) {
557 	events = 0;
558 
559 #ifdef HAVE_SYS_INOTIFY_H
560 	if (watching_file_p) {
561 	    if (FD_ISSET(inotify_fd, &rfds)) {
562                 ret = handle_file_events(firstjob, inotify_fd);
563 	        if (ret < 0)
564 	            break;
565 	        events += ret;
566 	    }
567             FD_SET(inotify_fd, &rfds);
568 	}
569 #endif
570 
571 	if (watching_stdin_p) {
572 	    if (FD_ISSET(stdin_fd, &rfds)) {
573                 ret = handle_stdin_events(firstjob, stdin_fd);
574 	        if (ret < 0) {
575 	            watching_stdin_p = FALSE;
576                     FD_CLR(stdin_fd, &rfds);
577                 }
578 	        events += ret;
579 	    }
580 	    if (watching_stdin_p)
581 	        FD_SET(stdin_fd, &rfds);
582 	}
583 
584 	ret = handle_xlib_events(firstjob, dpy);
585 	if (ret < 0)
586 	    break;
587 	events += ret;
588         FD_SET(xlib_fd, &rfds);
589 
590 	if (events) {
591             for (job = firstjob; job; job = job->next_active)
592 	        update_display(job, dpy);
593 	    XFlush(dpy);
594 	}
595 
596 	ret = select(numfds+1, &rfds, NULL, NULL, NULL);
597 	if (ret < 0) {
598 	    fprintf(stderr,"select() failed\n");
599 	    break;
600 	}
601     }
602 
603 #ifdef HAVE_SYS_INOTIFY_H
604     if (watching_file_p)
605 	ret = inotify_rm_watch(inotify_fd, wd);
606 #endif
607 
608     XCloseDisplay(dpy);
609     free(keycodes);
610     firstjob->keycodes = NULL;
611 }
612 
613 static gvdevice_features_t device_features_xlib = {
614     GVDEVICE_DOES_TRUECOLOR
615 	| GVDEVICE_EVENTS,      /* flags */
616     {0.,0.},                    /* default margin - points */
617     {0.,0.},                    /* default page width, height - points */
618     {96.,96.},                  /* dpi */
619 };
620 
621 static gvdevice_engine_t device_engine_xlib = {
622     xlib_initialize,
623     NULL,		/* xlib_format */
624     xlib_finalize,
625 };
626 #endif
627 
628 gvplugin_installed_t gvdevice_types_xlib[] = {
629 #ifdef CAIRO_HAS_XLIB_SURFACE
630     {0, "xlib:cairo", 0, &device_engine_xlib, &device_features_xlib},
631     {0, "x11:cairo", 0, &device_engine_xlib, &device_features_xlib},
632 #endif
633     {0, NULL, 0, NULL, NULL}
634 };
635