/************************************************************************* * 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 #include #include #include #include #include #include #include #include #include #ifdef HAVE_SYS_TIME_H #include #endif #ifdef HAVE_SYS_IOCTL_H #include #endif #ifdef HAVE_SYS_TYPES_H #include #endif #ifdef HAVE_SYS_SELECT_H #include #endif #ifdef HAVE_SYS_INOTIFY_H #include #ifdef __sun #include #endif #endif #include #include #include #include #include #include #include #ifdef CAIRO_HAS_XLIB_SURFACE #include #include #include typedef struct window_xlib_s { Window win; uint64_t event_mask; Pixmap pix; GC gc; Visual *visual; Colormap cmap; int depth; Atom wm_delete_window_atom; } window_t; static void handle_configure_notify(GVJ_t * job, XConfigureEvent * cev) { /*FIXME - should allow for margins */ /* - similar zoom_to_fit code exists in: */ /* plugin/gtk/callbacks.c */ /* plugin/xlib/gvdevice_xlib.c */ /* lib/gvc/gvevent.c */ assert(cev->width >= 0 && "Xlib returned an event with negative width"); assert(cev->height >= 0 && "Xlib returned an event with negative height"); job->zoom *= 1 + fmin( ((double)cev->width - job->width) / job->width, ((double)cev->height - job->height) / job->height); if ((unsigned)cev->width > job->width || (unsigned)cev->height > job->height) job->has_grown = true; job->width = (unsigned)cev->width; job->height = (unsigned)cev->height; job->needs_refresh = true; } static void handle_expose(GVJ_t * job, XExposeEvent * eev) { window_t *window; window = job->window; assert(eev->width >= 0 && "Xlib returned an expose event with negative width"); assert(eev->height >= 0 && "Xlib returned an expose event with negative height"); XCopyArea(eev->display, window->pix, eev->window, window->gc, eev->x, eev->y, (unsigned)eev->width, (unsigned)eev->height, eev->x, eev->y); } static void handle_client_message(GVJ_t * job, XClientMessageEvent * cmev) { window_t *window; window = job->window; if (cmev->format == 32 && (Atom) cmev->data.l[0] == window->wm_delete_window_atom) graphviz_exit(0); } static bool handle_keypress(GVJ_t *job, XKeyEvent *kev) { KeyCode *keycodes; keycodes = job->keycodes; for (size_t i = 0; i < job->numkeys; i++) { if (kev->keycode == keycodes[i]) return job->keybindings[i].callback(job) != 0; } return false; } static Visual *find_argb_visual(Display * dpy, int scr) { XVisualInfo *xvi; XVisualInfo template; int nvi; int i; XRenderPictFormat *format; Visual *visual; template.screen = scr; template.depth = 32; template.class = TrueColor; xvi = XGetVisualInfo(dpy, VisualScreenMask | VisualDepthMask | VisualClassMask, &template, &nvi); if (!xvi) return 0; visual = 0; for (i = 0; i < nvi; i++) { format = XRenderFindVisualFormat(dpy, xvi[i].visual); if (format->type == PictTypeDirect && format->direct.alphaMask) { visual = xvi[i].visual; break; } } XFree(xvi); return visual; } static void browser_show(GVJ_t *job) { #ifdef HAVE_SYS_TYPES_H char *exec_argv[3] = {BROWSER, NULL, NULL}; pid_t pid; exec_argv[1] = job->selected_href; pid = fork(); if (pid == -1) { fprintf(stderr,"fork failed: %s\n", strerror(errno)); } else if (pid == 0) { execvp(exec_argv[0], exec_argv); fprintf(stderr,"error starting %s: %s\n", exec_argv[0], strerror(errno)); } #else fprintf(stdout,"browser_show: %s\n", job->selected_href); #endif } static int handle_xlib_events (GVJ_t *firstjob, Display *dpy) { GVJ_t *job; window_t *window; XEvent xev; pointf pointer; int rc = 0; while (XPending(dpy)) { XNextEvent(dpy, &xev); for (job = firstjob; job; job = job->next_active) { window = job->window; if (xev.xany.window == window->win) { switch (xev.xany.type) { case ButtonPress: pointer.x = (double)xev.xbutton.x; pointer.y = (double)xev.xbutton.y; assert(xev.xbutton.button <= (unsigned)INT_MAX && "Xlib returned invalid button event"); job->callbacks->button_press(job, (int)xev.xbutton.button, pointer); rc++; break; case MotionNotify: if (job->button) { /* only interested while a button is pressed */ pointer.x = (double)xev.xbutton.x; pointer.y = (double)xev.xbutton.y; job->callbacks->motion(job, pointer); rc++; } break; case ButtonRelease: pointer.x = (double)xev.xbutton.x; pointer.y = (double)xev.xbutton.y; assert(xev.xbutton.button <= (unsigned)INT_MAX && "Xlib returned invalid button event"); job->callbacks->button_release(job, (int)xev.xbutton.button, pointer); if (job->selected_href && job->selected_href[0] && xev.xbutton.button == 1) browser_show(job); rc++; break; case KeyPress: if (handle_keypress(job, &xev.xkey)) return -1; /* exit code */ rc++; break; case ConfigureNotify: handle_configure_notify(job, &xev.xconfigure); rc++; break; case Expose: handle_expose(job, &xev.xexpose); rc++; break; case ClientMessage: handle_client_message(job, &xev.xclient); rc++; break; default: break; } break; } } } return rc; } static void update_display(GVJ_t *job, Display *dpy) { window_t *window; cairo_surface_t *surface; window = job->window; // window geometry is set to fixed values assert(job->width <= (unsigned)INT_MAX && "out of range width"); assert(job->height <= (unsigned)INT_MAX && "out of range height"); if (job->has_grown) { XFreePixmap(dpy, window->pix); assert(window->depth >= 0 && "Xlib returned invalid window depth"); window->pix = XCreatePixmap(dpy, window->win, job->width, job->height, (unsigned)window->depth); job->has_grown = false; job->needs_refresh = true; } if (job->needs_refresh) { XFillRectangle(dpy, window->pix, window->gc, 0, 0, job->width, job->height); surface = cairo_xlib_surface_create(dpy, window->pix, window->visual, (int)job->width, (int)job->height); job->context = cairo_create(surface); job->external_context = true; job->callbacks->refresh(job); cairo_surface_destroy(surface); XCopyArea(dpy, window->pix, window->win, window->gc, 0, 0, job->width, job->height, 0, 0); job->needs_refresh = false; } } static void init_window(GVJ_t *job, Display *dpy, int scr) { int argb = 0; const char *base = ""; XGCValues gcv; XSetWindowAttributes attributes; XWMHints *wmhints; XSizeHints *normalhints; XClassHint *classhint; uint64_t attributemask = 0; window_t *window; double zoom_to_fit; window = malloc(sizeof(window_t)); if (window == NULL) { fprintf(stderr, "Failed to malloc window_t\n"); return; } unsigned w = 480; /* FIXME - w,h should be set by a --geometry commandline option */ unsigned h = 325; zoom_to_fit = fmin((double)w / job->width, (double)h / job->height); if (zoom_to_fit < 1.0) /* don't make bigger */ job->zoom *= zoom_to_fit; job->width = w; /* use window geometry */ job->height = h; job->window = window; job->fit_mode = false; job->needs_refresh = true; if (argb && (window->visual = find_argb_visual(dpy, scr))) { window->cmap = XCreateColormap(dpy, RootWindow(dpy, scr), window->visual, AllocNone); attributes.override_redirect = False; attributes.background_pixel = 0; attributes.border_pixel = 0; attributes.colormap = window->cmap; attributemask = ( CWBackPixel | CWBorderPixel | CWOverrideRedirect | CWColormap ); window->depth = 32; } else { window->cmap = DefaultColormap(dpy, scr); window->visual = DefaultVisual(dpy, scr); attributes.background_pixel = WhitePixel(dpy, scr); attributes.border_pixel = BlackPixel(dpy, scr); attributemask = (CWBackPixel | CWBorderPixel); window->depth = DefaultDepth(dpy, scr); } window->win = XCreateWindow(dpy, RootWindow(dpy, scr), 0, 0, job->width, job->height, 0, window->depth, InputOutput, window->visual, attributemask, &attributes); agxbuf name = {0}; agxbprint(&name, "graphviz: %s", base); normalhints = XAllocSizeHints(); normalhints->flags = 0; normalhints->x = 0; normalhints->y = 0; assert(job->width <= (unsigned)INT_MAX && "out of range width"); normalhints->width = (int)job->width; assert(job->height <= (unsigned)INT_MAX && "out of range height"); normalhints->height = (int)job->height; classhint = XAllocClassHint(); classhint->res_name = "graphviz"; classhint->res_class = "Graphviz"; wmhints = XAllocWMHints(); wmhints->flags = InputHint; wmhints->input = True; Xutf8SetWMProperties(dpy, window->win, agxbuse(&name), base, 0, 0, normalhints, wmhints, classhint); XFree(wmhints); XFree(classhint); XFree(normalhints); agxbfree(&name); assert(window->depth >= 0 && "Xlib returned invalid window depth"); window->pix = XCreatePixmap(dpy, window->win, job->width, job->height, (unsigned)window->depth); if (argb) gcv.foreground = 0; else gcv.foreground = WhitePixel(dpy, scr); window->gc = XCreateGC(dpy, window->pix, GCForeground, &gcv); update_display(job, dpy); window->event_mask = ( ButtonPressMask | ButtonReleaseMask | PointerMotionMask | KeyPressMask | StructureNotifyMask | ExposureMask); XSelectInput(dpy, window->win, (long)window->event_mask); window->wm_delete_window_atom = XInternAtom(dpy, "WM_DELETE_WINDOW", False); XSetWMProtocols(dpy, window->win, &window->wm_delete_window_atom, 1); XMapWindow(dpy, window->win); } static int handle_stdin_events(GVJ_t *job) { int rc=0; if (feof(stdin)) return -1; job->callbacks->read(job, job->input_filename, job->layout_type); rc++; return rc; } #ifdef HAVE_SYS_INOTIFY_H static int handle_file_events(GVJ_t *job, int inotify_fd) { int avail, ret, len, rc = 0; char *bf, *p; struct inotify_event *event; ret = ioctl(inotify_fd, FIONREAD, &avail); if (ret < 0) { fprintf(stderr,"ioctl() failed\n"); return -1; } if (avail) { assert(avail > 0 && "invalid value from FIONREAD"); void *buf = malloc((size_t)avail); if (!buf) { fprintf(stderr, "out of memory (could not allocate %d bytes)\n", avail); return -1; } len = (int)read(inotify_fd, buf, (size_t)avail); if (len != avail) { fprintf(stderr,"avail = %d, len = %d\n", avail, len); free(buf); return -1; } bf = buf; while (len > 0) { event = (struct inotify_event *)bf; if (event->mask == IN_MODIFY) { p = strrchr(job->input_filename, '/'); if (p) p++; else p = job->input_filename; if (strcmp(event->name, p) == 0) { job->callbacks->read(job, job->input_filename, job->layout_type); rc++; } } size_t ln = event->len + sizeof(struct inotify_event); assert(ln <= (size_t)len); bf += ln; len -= (int)ln; } free(buf); if (len != 0) { fprintf(stderr,"length miscalculation, len = %d\n", len); return -1; } } return rc; } #endif static bool initialized; static void xlib_initialize(GVJ_t *firstjob) { Display *dpy; KeySym keysym; KeyCode *keycodes; const char *display_name = NULL; int scr; dpy = XOpenDisplay(display_name); if (dpy == NULL) { fprintf(stderr, "Failed to open XLIB display: %s\n", XDisplayName(NULL)); return; } scr = DefaultScreen(dpy); firstjob->display = dpy; firstjob->screen = scr; keycodes = malloc(firstjob->numkeys * sizeof(KeyCode)); if (firstjob->numkeys > 0 && keycodes == NULL) { fprintf(stderr, "Failed to malloc %" PRISIZE_T "*KeyCode\n", firstjob->numkeys); return; } for (size_t i = 0; i < firstjob->numkeys; i++) { keysym = XStringToKeysym(firstjob->keybindings[i].keystring); if (keysym == NoSymbol) fprintf(stderr, "ERROR: No keysym for \"%s\"\n", firstjob->keybindings[i].keystring); else keycodes[i] = XKeysymToKeycode(dpy, keysym); } firstjob->keycodes = keycodes; firstjob->device_dpi.x = DisplayWidth(dpy, scr) * 25.4 / DisplayWidthMM(dpy, scr); firstjob->device_dpi.y = DisplayHeight(dpy, scr) * 25.4 / DisplayHeightMM(dpy, scr); firstjob->device_sets_dpi = true; initialized = true; } static void xlib_finalize(GVJ_t *firstjob) { GVJ_t *job; Display *dpy = firstjob->display; int scr = firstjob->screen; KeyCode *keycodes= firstjob->keycodes; int numfds, stdin_fd=0, xlib_fd, ret, events; fd_set rfds; bool watching_stdin_p = false; #ifdef HAVE_SYS_INOTIFY_H int wd=0; int inotify_fd=0; bool watching_file_p = false; char *p, *cwd = NULL; #ifdef HAVE_INOTIFY_INIT1 #ifdef IN_CLOSEXEC inotify_fd = inotify_init1(IN_CLOSEXEC); #else inotify_fd = inotify_init1(IN_CLOEXEC); #endif #else inotify_fd = inotify_init(); if (inotify_fd >= 0) { const int flags = fcntl(inotify_fd, F_GETFD); if (fcntl(inotify_fd, F_SETFD, flags | FD_CLOEXEC) < 0) { fprintf(stderr, "setting FD_CLOEXEC failed\n"); return; } } #endif if (inotify_fd < 0) { fprintf(stderr,"inotify_init() failed\n"); return; } #endif /* skip if initialization previously failed */ if (!initialized) { return; } numfds = xlib_fd = XConnectionNumber(dpy); if (firstjob->input_filename) { if (firstjob->graph_index == 0) { #ifdef HAVE_SYS_INOTIFY_H watching_file_p = true; agxbuf dir = {0}; if (firstjob->input_filename[0] != '/') { cwd = getcwd(NULL, 0); agxbprint(&dir, "%s/%s", cwd, firstjob->input_filename); free(cwd); } else { agxbput(&dir, firstjob->input_filename); } char *dirstr = agxbuse(&dir); p = strrchr(dirstr,'/'); *p = '\0'; wd = inotify_add_watch(inotify_fd, dirstr, IN_MODIFY); agxbfree(&dir); numfds = imax(inotify_fd, numfds); #endif } } else { watching_stdin_p = true; #ifdef F_DUPFD_CLOEXEC stdin_fd = fcntl(STDIN_FILENO, F_DUPFD_CLOEXEC, 0); #else stdin_fd = fcntl(STDIN_FILENO, F_DUPFD, 0); (void)fcntl(stdin_fd, F_SETFD, fcntl(stdin_fd, F_GETFD) | FD_CLOEXEC); #endif numfds = imax(stdin_fd, numfds); } for (job = firstjob; job; job = job->next_active) init_window(job, dpy, scr); /* This is the event loop */ FD_ZERO(&rfds); while (1) { events = 0; #ifdef HAVE_SYS_INOTIFY_H if (watching_file_p) { if (FD_ISSET(inotify_fd, &rfds)) { ret = handle_file_events(firstjob, inotify_fd); if (ret < 0) break; events += ret; } FD_SET(inotify_fd, &rfds); } #endif if (watching_stdin_p) { if (FD_ISSET(stdin_fd, &rfds)) { ret = handle_stdin_events(firstjob); if (ret < 0) { watching_stdin_p = false; FD_CLR(stdin_fd, &rfds); } events += ret; } if (watching_stdin_p) FD_SET(stdin_fd, &rfds); } ret = handle_xlib_events(firstjob, dpy); if (ret < 0) break; events += ret; FD_SET(xlib_fd, &rfds); if (events) { for (job = firstjob; job; job = job->next_active) update_display(job, dpy); XFlush(dpy); } ret = select(numfds+1, &rfds, NULL, NULL, NULL); if (ret < 0) { fprintf(stderr,"select() failed\n"); break; } } #ifdef HAVE_SYS_INOTIFY_H if (watching_file_p) ret = inotify_rm_watch(inotify_fd, wd); #endif XCloseDisplay(dpy); free(keycodes); firstjob->keycodes = NULL; } static gvdevice_features_t device_features_xlib = { GVDEVICE_DOES_TRUECOLOR | GVDEVICE_EVENTS, /* flags */ {0.,0.}, /* default margin - points */ {0.,0.}, /* default page width, height - points */ {96.,96.}, /* dpi */ }; static gvdevice_engine_t device_engine_xlib = { xlib_initialize, NULL, /* xlib_format */ xlib_finalize, }; #endif gvplugin_installed_t gvdevice_types_xlib[] = { #ifdef CAIRO_HAS_XLIB_SURFACE {0, "xlib:cairo", 0, &device_engine_xlib, &device_features_xlib}, {0, "x11:cairo", 0, &device_engine_xlib, &device_features_xlib}, #endif {0, NULL, 0, NULL, NULL} };