Graphviz 14.1.5~dev.20260409.1203
Loading...
Searching...
No Matches
gvdevice_xlib.c
Go to the documentation of this file.
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 v2.0
5 * which accompanies this distribution, and is available at
6 * https://www.eclipse.org/org/documents/epl-2.0/EPL-2.0.html
7 *
8 * Contributors: Details at https://graphviz.org
9 *************************************************************************/
10
11#include "config.h"
12
13#include <assert.h>
14#include <fcntl.h>
15#include <inttypes.h>
16#include <limits.h>
17#include <math.h>
18#include <stdbool.h>
19#include <stdio.h>
20#include <stdlib.h>
21#include <string.h>
22#include <unistd.h>
23#ifdef HAVE_SYS_TIME_H
24#include <sys/time.h>
25#endif
26#ifdef HAVE_SYS_IOCTL_H
27#include <sys/ioctl.h>
28#endif
29#include <sys/types.h>
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#ifdef __sun
36#include <sys/filio.h>
37#endif
38#endif
39#include <errno.h>
40
41#include <gvc/gvplugin_device.h>
42#include <util/agxbuf.h>
43#include <util/exit.h>
44#include <util/gv_math.h>
45#include <util/prisize_t.h>
46
47#include <cairo.h>
48#ifdef CAIRO_HAS_XLIB_SURFACE
49#include <X11/Xutil.h>
50#include <X11/extensions/Xrender.h>
51#include <cairo-xlib.h>
52
53typedef 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
64static void handle_configure_notify(GVJ_t *job, XConfigureEvent *cev) {
65 /*FIXME - should allow for margins */
66 /* - similar zoom_to_fit code exists in: */
67 /* plugin/gtk/callbacks.c */
68 /* plugin/xlib/gvdevice_xlib.c */
69 /* lib/gvc/gvevent.c */
70
71 assert(cev->width >= 0 && "Xlib returned an event with negative width");
72 assert(cev->height >= 0 && "Xlib returned an event with negative height");
73
74 job->zoom *= 1 + fmin(((double)cev->width - job->width) / job->width,
75 ((double)cev->height - job->height) / job->height);
76 if ((unsigned)cev->width > job->width || (unsigned)cev->height > job->height)
77 job->has_grown = true;
78 job->width = (unsigned)cev->width;
79 job->height = (unsigned)cev->height;
80 job->needs_refresh = true;
81}
82
83static void handle_expose(GVJ_t *job, XExposeEvent *eev) {
84 window_t *window;
85
86 window = job->window;
87 assert(eev->width >= 0 &&
88 "Xlib returned an expose event with negative width");
89 assert(eev->height >= 0 &&
90 "Xlib returned an expose event with negative height");
91 XCopyArea(eev->display, window->pix, eev->window, window->gc, eev->x, eev->y,
92 (unsigned)eev->width, (unsigned)eev->height, eev->x, eev->y);
93}
94
95static void handle_client_message(GVJ_t *job, XClientMessageEvent *cmev) {
96 window_t *window;
97
98 window = job->window;
99 if (cmev->format == 32 &&
100 (Atom)cmev->data.l[0] == window->wm_delete_window_atom)
101 graphviz_exit(0);
102}
103
104static bool handle_keypress(GVJ_t *job, XKeyEvent *kev) {
105 const KeyCode *const keycodes = job->keycodes;
106 for (size_t i = 0; i < job->numkeys; i++) {
107 if (kev->keycode == keycodes[i])
108 return job->keybindings[i].callback(job) != 0;
109 }
110 return false;
111}
112
113static void browser_show(GVJ_t *job) {
114 char *exec_argv[3] = {BROWSER, NULL, NULL};
115 pid_t pid;
116
117 exec_argv[1] = job->selected_href;
118
119 pid = fork();
120 if (pid == -1) {
121 fprintf(stderr, "fork failed: %s\n", strerror(errno));
122 } else if (pid == 0) {
123 execvp(exec_argv[0], exec_argv);
124 fprintf(stderr, "error starting %s: %s\n", exec_argv[0], strerror(errno));
125 }
126}
127
128static int handle_xlib_events(GVJ_t *firstjob, Display *dpy) {
129 GVJ_t *job;
130 window_t *window;
131 XEvent xev;
132 pointf pointer;
133 int rc = 0;
134
135 while (XPending(dpy)) {
136 XNextEvent(dpy, &xev);
137
138 for (job = firstjob; job; job = job->next_active) {
139 window = job->window;
140 if (xev.xany.window == window->win) {
141 switch (xev.xany.type) {
142 case ButtonPress:
143 pointer.x = xev.xbutton.x;
144 pointer.y = xev.xbutton.y;
145 assert(xev.xbutton.button <= INT_MAX &&
146 "Xlib returned invalid button event");
147 job->callbacks->button_press(job, (int)xev.xbutton.button, pointer);
148 rc++;
149 break;
150 case MotionNotify:
151 if (job->button) { /* only interested while a button is pressed */
152 pointer.x = xev.xbutton.x;
153 pointer.y = xev.xbutton.y;
154 job->callbacks->motion(job, pointer);
155 rc++;
156 }
157 break;
158 case ButtonRelease:
159 pointer.x = xev.xbutton.x;
160 pointer.y = xev.xbutton.y;
161 assert(xev.xbutton.button <= INT_MAX &&
162 "Xlib returned invalid button event");
163 job->callbacks->button_release(job, (int)xev.xbutton.button, pointer);
164 if (job->selected_href && job->selected_href[0] &&
165 xev.xbutton.button == 1)
166 browser_show(job);
167 rc++;
168 break;
169 case KeyPress:
170 if (handle_keypress(job, &xev.xkey))
171 return -1; /* exit code */
172 rc++;
173 break;
174 case ConfigureNotify:
175 handle_configure_notify(job, &xev.xconfigure);
176 rc++;
177 break;
178 case Expose:
179 handle_expose(job, &xev.xexpose);
180 rc++;
181 break;
182 case ClientMessage:
183 handle_client_message(job, &xev.xclient);
184 rc++;
185 break;
186 default:
187 break;
188 }
189 break;
190 }
191 }
192 }
193 return rc;
194}
195
196static void update_display(GVJ_t *job, Display *dpy) {
197 window_t *window;
198 cairo_surface_t *surface;
199
200 window = job->window;
201
202 // window geometry is set to fixed values
203 assert(job->width <= INT_MAX && "out of range width");
204 assert(job->height <= INT_MAX && "out of range height");
205
206 if (job->has_grown) {
207 XFreePixmap(dpy, window->pix);
208 assert(window->depth >= 0 && "Xlib returned invalid window depth");
209 window->pix = XCreatePixmap(dpy, window->win, job->width, job->height,
210 (unsigned)window->depth);
211 job->has_grown = false;
212 job->needs_refresh = true;
213 }
214 if (job->needs_refresh) {
215 XFillRectangle(dpy, window->pix, window->gc, 0, 0, job->width, job->height);
216 surface = cairo_xlib_surface_create(dpy, window->pix, window->visual,
217 (int)job->width, (int)job->height);
218 job->context = cairo_create(surface);
219 job->external_context = true;
220 job->callbacks->refresh(job);
221 cairo_surface_destroy(surface);
222 XCopyArea(dpy, window->pix, window->win, window->gc, 0, 0, job->width,
223 job->height, 0, 0);
224 job->needs_refresh = false;
225 }
226}
227
228static void init_window(GVJ_t *job, Display *dpy, int scr) {
229 const char *base = "";
230 XGCValues gcv;
231 XSetWindowAttributes attributes;
232 XWMHints *wmhints;
233 XSizeHints *normalhints;
234 XClassHint *classhint;
235 uint64_t attributemask = 0;
236 window_t *window;
237 double zoom_to_fit;
238
239 window = malloc(sizeof(window_t));
240 if (window == NULL) {
241 fprintf(stderr, "Failed to malloc window_t\n");
242 return;
243 }
244
245 unsigned w =
246 480; /* FIXME - w,h should be set by a --geometry commandline option */
247 unsigned h = 325;
248
249 zoom_to_fit = fmin((double)w / job->width, (double)h / job->height);
250 if (zoom_to_fit < 1.0) /* don't make bigger */
251 job->zoom *= zoom_to_fit;
252
253 job->width = w; /* use window geometry */
254 job->height = h;
255
256 job->window = window;
257 job->fit_mode = false;
258 job->needs_refresh = true;
259
260 window->cmap = DefaultColormap(dpy, scr);
261 window->visual = DefaultVisual(dpy, scr);
262 attributes.background_pixel = WhitePixel(dpy, scr);
263 attributes.border_pixel = BlackPixel(dpy, scr);
264 attributemask = (CWBackPixel | CWBorderPixel);
265 window->depth = DefaultDepth(dpy, scr);
266
267 window->win = XCreateWindow(dpy, RootWindow(dpy, scr), 0, 0, job->width,
268 job->height, 0, window->depth, InputOutput,
269 window->visual, attributemask, &attributes);
270
271 agxbuf name = {0};
272 agxbprint(&name, "graphviz: %s", base);
273
274 normalhints = XAllocSizeHints();
275 normalhints->flags = 0;
276 normalhints->x = 0;
277 normalhints->y = 0;
278 assert(job->width <= INT_MAX && "out of range width");
279 normalhints->width = (int)job->width;
280 assert(job->height <= INT_MAX && "out of range height");
281 normalhints->height = (int)job->height;
282
283 classhint = XAllocClassHint();
284 classhint->res_name = "graphviz";
285 classhint->res_class = "Graphviz";
286
287 wmhints = XAllocWMHints();
288 wmhints->flags = InputHint;
289 wmhints->input = True;
290
291 Xutf8SetWMProperties(dpy, window->win, agxbuse(&name), base, 0, 0,
292 normalhints, wmhints, classhint);
293 XFree(wmhints);
294 XFree(classhint);
295 XFree(normalhints);
296 agxbfree(&name);
297
298 assert(window->depth >= 0 && "Xlib returned invalid window depth");
299 window->pix = XCreatePixmap(dpy, window->win, job->width, job->height,
300 (unsigned)window->depth);
301 gcv.foreground = WhitePixel(dpy, scr);
302 window->gc = XCreateGC(dpy, window->pix, GCForeground, &gcv);
303 update_display(job, dpy);
304
305 window->event_mask =
306 (ButtonPressMask | ButtonReleaseMask | PointerMotionMask | KeyPressMask |
307 StructureNotifyMask | ExposureMask);
308 XSelectInput(dpy, window->win, (long)window->event_mask);
309 window->wm_delete_window_atom = XInternAtom(dpy, "WM_DELETE_WINDOW", False);
310 XSetWMProtocols(dpy, window->win, &window->wm_delete_window_atom, 1);
311 XMapWindow(dpy, window->win);
312}
313
314static int handle_stdin_events(GVJ_t *job) {
315
316 if (feof(stdin))
317 return -1;
318 job->callbacks->read(job, job->input_filename, job->layout_type);
319
320 return 1;
321}
322
323#ifdef HAVE_SYS_INOTIFY_H
324static int handle_file_events(GVJ_t *job, int inotify_fd) {
325 int avail, ret, len, rc = 0;
326 char *bf, *p;
327 struct inotify_event *event;
328
329 ret = ioctl(inotify_fd, FIONREAD, &avail);
330 if (ret < 0) {
331 fprintf(stderr, "ioctl() failed\n");
332 return -1;
333 }
334
335 if (avail) {
336 assert(avail > 0 && "invalid value from FIONREAD");
337 void *buf = malloc((size_t)avail);
338 if (!buf) {
339 fprintf(stderr, "out of memory (could not allocate %d bytes)\n", avail);
340 return -1;
341 }
342 len = (int)read(inotify_fd, buf, (size_t)avail);
343 if (len != avail) {
344 fprintf(stderr, "avail = %d, len = %d\n", avail, len);
345 free(buf);
346 return -1;
347 }
348 bf = buf;
349 while (len > 0) {
350 event = (struct inotify_event *)bf;
351 if (event->mask == IN_MODIFY) {
352 p = strrchr(job->input_filename, '/');
353 if (p)
354 p++;
355 else
356 p = job->input_filename;
357 if (strcmp(event->name, p) == 0) {
358 job->callbacks->read(job, job->input_filename, job->layout_type);
359 rc++;
360 }
361 }
362 size_t ln = event->len + sizeof(struct inotify_event);
363 assert(ln <= (size_t)len);
364 bf += ln;
365 len -= (int)ln;
366 }
367 free(buf);
368 if (len != 0) {
369 fprintf(stderr, "length miscalculation, len = %d\n", len);
370 return -1;
371 }
372 }
373 return rc;
374}
375#endif
376
377static void xlib_initialize(GVJ_t *firstjob) {
378 KeySym keysym;
379 KeyCode *keycodes;
380 const char *display_name = NULL;
381 int scr;
382
383 Display *dpy = XOpenDisplay(display_name);
384 if (dpy == NULL) {
385 fprintf(stderr, "Failed to open XLIB display: %s\n", XDisplayName(NULL));
386 return;
387 }
388 scr = DefaultScreen(dpy);
389
390 firstjob->screen = scr;
391
392 keycodes = malloc(firstjob->numkeys * sizeof(KeyCode));
393 if (firstjob->numkeys > 0 && keycodes == NULL) {
394 fprintf(stderr, "Failed to malloc %" PRISIZE_T "*KeyCode\n",
395 firstjob->numkeys);
396 XCloseDisplay(dpy);
397 return;
398 }
399 for (size_t i = 0; i < firstjob->numkeys; i++) {
400 keysym = XStringToKeysym(firstjob->keybindings[i].keystring);
401 if (keysym == NoSymbol)
402 fprintf(stderr, "ERROR: No keysym for \"%s\"\n",
403 firstjob->keybindings[i].keystring);
404 else
405 keycodes[i] = XKeysymToKeycode(dpy, keysym);
406 }
407 firstjob->keycodes = keycodes;
408
409 firstjob->device_dpi.x =
410 DisplayWidth(dpy, scr) * 25.4 / DisplayWidthMM(dpy, scr);
411 firstjob->device_dpi.y =
412 DisplayHeight(dpy, scr) * 25.4 / DisplayHeightMM(dpy, scr);
413 firstjob->device_sets_dpi = true;
414
415 firstjob->display = dpy;
416}
417
418static void xlib_finalize(GVJ_t *firstjob) {
419 GVJ_t *job;
420 Display *dpy = firstjob->display;
421 int scr = firstjob->screen;
422 KeyCode *keycodes = firstjob->keycodes;
423 int numfds, stdin_fd = 0, xlib_fd, ret, events;
424 fd_set rfds;
425 bool watching_stdin_p = false;
426#ifdef HAVE_SYS_INOTIFY_H
427 int wd = 0;
428 int inotify_fd = 0;
429 bool watching_file_p = false;
430 char *p, *cwd = NULL;
431
432#ifdef HAVE_INOTIFY_INIT1
433#ifdef IN_CLOSEXEC
434 inotify_fd = inotify_init1(IN_CLOSEXEC);
435#else
436 inotify_fd = inotify_init1(IN_CLOEXEC);
437#endif
438#else
439 inotify_fd = inotify_init();
440 if (inotify_fd >= 0) {
441 const int flags = fcntl(inotify_fd, F_GETFD);
442 if (fcntl(inotify_fd, F_SETFD, flags | FD_CLOEXEC) < 0) {
443 fprintf(stderr, "setting FD_CLOEXEC failed\n");
444 return;
445 }
446 }
447#endif
448 if (inotify_fd < 0) {
449 fprintf(stderr, "inotify_init() failed\n");
450 return;
451 }
452#endif
453
454 /* skip if initialization previously failed */
455 if (dpy == NULL) {
456 return;
457 }
458
459 numfds = xlib_fd = XConnectionNumber(dpy);
460
461 if (firstjob->input_filename) {
462 if (firstjob->graph_index == 0) {
463#ifdef HAVE_SYS_INOTIFY_H
464 watching_file_p = true;
465
466 agxbuf dir = {0};
467 if (firstjob->input_filename[0] != '/') {
468 cwd = getcwd(NULL, 0);
469 agxbprint(&dir, "%s/%s", cwd, firstjob->input_filename);
470 free(cwd);
471 } else {
472 agxbput(&dir, firstjob->input_filename);
473 }
474 char *dirstr = agxbuse(&dir);
475 p = strrchr(dirstr, '/');
476 *p = '\0';
477
478 wd = inotify_add_watch(inotify_fd, dirstr, IN_MODIFY);
479 agxbfree(&dir);
480
481 numfds = imax(inotify_fd, numfds);
482#endif
483 }
484 } else {
485 watching_stdin_p = true;
486#ifdef F_DUPFD_CLOEXEC
487 stdin_fd = fcntl(STDIN_FILENO, F_DUPFD_CLOEXEC, 0);
488#else
489 stdin_fd = fcntl(STDIN_FILENO, F_DUPFD, 0);
490 (void)fcntl(stdin_fd, F_SETFD, fcntl(stdin_fd, F_GETFD) | FD_CLOEXEC);
491#endif
492 numfds = imax(stdin_fd, numfds);
493 }
494
495 for (job = firstjob; job; job = job->next_active)
496 init_window(job, dpy, scr);
497
498 /* This is the event loop */
499 FD_ZERO(&rfds);
500 while (1) {
501 events = 0;
502
503#ifdef HAVE_SYS_INOTIFY_H
504 if (watching_file_p) {
505 if (FD_ISSET(inotify_fd, &rfds)) {
506 ret = handle_file_events(firstjob, inotify_fd);
507 if (ret < 0)
508 break;
509 events += ret;
510 }
511 FD_SET(inotify_fd, &rfds);
512 }
513#endif
514
515 if (watching_stdin_p) {
516 if (FD_ISSET(stdin_fd, &rfds)) {
517 ret = handle_stdin_events(firstjob);
518 if (ret < 0) {
519 watching_stdin_p = false;
520 FD_CLR(stdin_fd, &rfds);
521 }
522 events += ret;
523 }
524 if (watching_stdin_p)
525 FD_SET(stdin_fd, &rfds);
526 }
527
528 ret = handle_xlib_events(firstjob, dpy);
529 if (ret < 0)
530 break;
531 events += ret;
532 FD_SET(xlib_fd, &rfds);
533
534 if (events) {
535 for (job = firstjob; job; job = job->next_active)
536 update_display(job, dpy);
537 XFlush(dpy);
538 }
539
540 ret = select(numfds + 1, &rfds, NULL, NULL, NULL);
541 if (ret < 0) {
542 fprintf(stderr, "select() failed\n");
543 break;
544 }
545 }
546
547#ifdef HAVE_SYS_INOTIFY_H
548 if (watching_file_p)
549 (void)inotify_rm_watch(inotify_fd, wd);
550#endif
551
552 XCloseDisplay(dpy);
553 firstjob->display = NULL;
554 free(keycodes);
555 firstjob->keycodes = NULL;
556}
557
558static gvdevice_features_t device_features_xlib = {
560 {0., 0.}, /* default margin - points */
561 {0., 0.}, /* default page width, height - points */
562 {96., 96.}, /* dpi */
563};
564
565static gvdevice_engine_t device_engine_xlib = {
566 xlib_initialize,
567 NULL, /* xlib_format */
568 xlib_finalize,
569};
570#endif
571
573#ifdef CAIRO_HAS_XLIB_SURFACE
574 {0, "xlib:cairo", 0, &device_engine_xlib, &device_features_xlib},
575 {0, "x11:cairo", 0, &device_engine_xlib, &device_features_xlib},
576#endif
577 {0, NULL, 0, NULL, NULL}};
Dynamically expanding string buffers.
static void agxbfree(agxbuf *xb)
free any malloced resources
Definition agxbuf.h:97
static int agxbprint(agxbuf *xb, const char *fmt,...)
Printf-style output to an agxbuf.
Definition agxbuf.h:252
static WUR char * agxbuse(agxbuf *xb)
Definition agxbuf.h:325
static NORETURN void graphviz_exit(int status)
Definition exit.h:23
static int flags
Definition gc.c:63
static double len(glCompPoint p)
Definition glutils.c:138
void * malloc(YYSIZE_T)
void free(void *)
node NULL
Definition grammar.y:181
Agraph_t * read(FILE *f)
Definition gv.cpp:64
Arithmetic helper functions.
static int imax(int a, int b)
maximum of two integers
Definition gv_math.h:26
#define GVDEVICE_DOES_TRUECOLOR
Definition gvcjob.h:90
#define GVDEVICE_EVENTS
Definition gvcjob.h:89
agxbput(xb, staging)
gvplugin_installed_t gvdevice_types_xlib[]
#define PRISIZE_T
Definition prisize_t.h:25
int screen
Definition gvcjob.h:293
bool fit_mode
Definition gvcjob.h:336
unsigned char button
Definition gvcjob.h:342
gvdevice_callbacks_t * callbacks
Definition gvcjob.h:288
char * selected_href
Definition gvcjob.h:351
bool needs_refresh
Definition gvcjob.h:337
gvevent_key_binding_t * keybindings
Definition gvcjob.h:356
void * context
Definition gvcjob.h:295
void * keycodes
Definition gvcjob.h:358
bool external_context
Definition gvcjob.h:296
void * display
Definition gvcjob.h:292
pointf device_dpi
Definition gvcjob.h:289
double zoom
Definition gvcjob.h:318
const char * layout_type
Definition gvcjob.h:274
unsigned int width
Definition gvcjob.h:327
void * window
Definition gvcjob.h:353
bool has_grown
Definition gvcjob.h:339
bool device_sets_dpi
Definition gvcjob.h:290
char * input_filename
Definition gvcjob.h:271
int graph_index
Definition gvcjob.h:272
size_t numkeys
Definition gvcjob.h:357
GVJ_t * next_active
Definition gvcjob.h:265
unsigned int height
Definition gvcjob.h:328
void(* button_release)(GVJ_t *job, int button, pointf pointer)
Definition gvcjob.h:150
void(* refresh)(GVJ_t *job)
Definition gvcjob.h:148
void(* button_press)(GVJ_t *job, int button, pointf pointer)
Definition gvcjob.h:149
void(* motion)(GVJ_t *job, pointf pointer)
Definition gvcjob.h:151
void(* read)(GVJ_t *job, const char *filename, const char *layout)
Definition gvcjob.h:154
gvevent_key_callback_t callback
Definition gvcjob.h:163
ingroup plugin_api
Definition gvplugin.h:35
double x
Definition geom.h:29
double y
Definition geom.h:29