gvdevice_xlib.c 17 KB

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