util/wmiv: an image viewer using wrlib
[wmaker-crm.git] / util / wmiv.c
blob8c382c9b92e4433e463322367135ce4bac4a30fb
1 /*
2 * Window Maker window manager
4 * Copyright (c) 2014 Window Maker Team - David Maciejak
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 2 of the License, or
9 * (at your option) any later version.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
16 * You should have received a copy of the GNU General Public License along
17 * with this program; if not, write to the Free Software Foundation, Inc.,
18 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
21 #define _GNU_SOURCE
22 #include <X11/keysym.h>
23 #include <X11/XKBlib.h>
24 #include <X11/Xatom.h>
25 #include <X11/Xlib.h>
26 #include "wraster.h"
27 #include <stdlib.h>
28 #include <stdio.h>
29 #include <string.h>
30 #include <dirent.h>
31 #include <limits.h>
32 #include <unistd.h>
33 #include <sys/stat.h>
34 #include "config.h"
35 #ifdef HAVE_PTHREAD
36 #include <pthread.h>
37 #endif
39 #ifdef USE_XPM
40 extern int XpmCreatePixmapFromData(Display *, Drawable, char **, Pixmap *, Pixmap *, void *);
41 /* this is the icon from eog project
42 git.gnome.org/browse/eog
44 #include "wmiv.h"
45 #endif
47 #define DEBUG 0
48 #define FILE_SEPARATOR '/'
50 Display *dpy;
51 Window win;
52 RContext *ctx;
53 RImage *img;
54 Pixmap pix;
56 const char *APPNAME = "wmiv";
57 int APPVERSION_MAJOR = 0;
58 int APPVERSION_MINOR = 6;
59 int NEXT = 0;
60 int PREV = 1;
61 float zoom_factor = 0;
62 int max_width = 0;
63 int max_height = 0;
65 Bool fullscreen_flag = False;
66 Bool focus = False;
68 #ifdef HAVE_PTHREAD
69 Bool diaporama_flag = False;
70 int diaporama_delay = 5;
71 pthread_t tid = 0;
72 #endif
73 XTextProperty title_property;
74 XTextProperty icon_property;
75 unsigned current_index = 1;
76 unsigned max_index = 1;
78 RColor lightGray;
79 RColor darkGray;
80 RColor black;
81 RColor red;
83 typedef struct link link_t;
84 struct link {
85 const void * data;
86 link_t *prev;
87 link_t *next;
90 typedef struct linked_list {
91 int count;
92 link_t *first;
93 link_t *last;
94 } linked_list_t;
96 linked_list_t list;
97 link_t *current_link;
100 change_title: used to change window title
101 return EXIT_SUCCESS on success, 1 on failure
103 int change_title(XTextProperty *prop, char *filename) {
104 char *combined_title = NULL;
105 if (!asprintf(&combined_title, "%s - %u/%u - %s", APPNAME, current_index, max_index, filename))
106 if (!asprintf(&combined_title, "%s - %u/%u", APPNAME, current_index, max_index))
107 return EXIT_FAILURE;
108 XStringListToTextProperty(&combined_title, 1, prop);
109 XSetWMName(dpy, win, prop);
110 if (prop->value)
111 XFree(prop->value);
112 free(combined_title);
113 return EXIT_SUCCESS;
117 rescale_image: used to rescale the current image based on the screen size
118 return EXIT_SUCCESS on success
120 int rescale_image() {
121 long final_width = img->width;
122 long final_height = img->height;
124 /* check if there is already a zoom factor applied */
125 if (zoom_factor != 0) {
126 final_width = img->width + (int)(img->width * zoom_factor);
127 final_height = img->height + (int)(img->height * zoom_factor);
129 if ((max_width < final_width) || (max_height < final_height)) {
130 long val = 0;
131 if (final_width > final_height) {
132 val = final_height * max_width / final_width;
133 final_width = final_width * val / final_height;
134 final_height = val;
135 if (val > max_height) {
136 val = final_width * max_height / final_height;
137 final_height = final_height * val / final_width;
138 final_width = val;
140 } else {
141 val = final_width * max_height / final_height;
142 final_height = final_height * val / final_width;
143 final_width = val;
144 if (val > max_width) {
145 val = final_height * max_width / final_width;
146 final_width = final_width * val / final_height;
147 final_height = val;
151 if ((final_width != img->width) || (final_height != img->height)) {
152 RImage *old_img = img;
153 img = RScaleImage(img, final_width, final_height);
154 if (!img) {
155 img = old_img;
156 return EXIT_FAILURE;
158 RReleaseImage(old_img);
160 if (!RConvertImage(ctx, img, &pix)) {
161 fprintf(stderr, "%s\n", RMessageForError(RErrorCode));
162 return EXIT_FAILURE;
164 return EXIT_SUCCESS;
168 maximize_image: find the best image size for the current display
169 return EXIT_SUCCESS on success
171 int maximize_image() {
172 rescale_image();
173 XCopyArea(dpy, pix, win, ctx->copy_gc, 0, 0, img->width, img->height, max_width/2-img->width/2, max_height/2-img->height/2);
174 return EXIT_SUCCESS;
178 merge_with_background: merge the current image with with a checkboard background
179 return EXIT_SUCCESS on success, 1 on failure
181 int merge_with_background(RImage *i) {
182 if (i) {
183 RImage *back;
184 back = RCreateImage(i->width, i->height, True);
185 if (back) {
186 int opaq = 255;
187 int x=0, y=0;
189 RFillImage(back, &lightGray);
190 for (x=0; x <= i->width; x+=8) {
191 if (x/8 % 2)
192 y = 8;
193 else
194 y = 0;
195 for (; y <= i->height; y+=16)
196 ROperateRectangle(back, RAddOperation, x,y,x+8,y+8, &darkGray);
199 RCombineImagesWithOpaqueness(i, back, opaq);
200 RReleaseImage(back);
201 return EXIT_SUCCESS;
204 return EXIT_FAILURE;
208 draw_failed_image: create a red crossed image to indicate an error loading file
209 return the image on success, NULL on failure
212 RImage* draw_failed_image() {
213 RImage *failed_image = NULL;
214 XWindowAttributes attr;
216 if (win && (XGetWindowAttributes(dpy, win, &attr) >= 0)) {
217 failed_image = RCreateImage(attr.width, attr.height, False);
218 } else {
219 failed_image = RCreateImage(50, 50, False);
221 if (!failed_image)
222 return NULL;
224 RFillImage(failed_image, &black);
225 ROperateLine(failed_image, RAddOperation, 0, 0, failed_image->width, failed_image->height, &red);
226 ROperateLine(failed_image, RAddOperation, 0, failed_image->height, failed_image->width, 0, &red);
228 return failed_image;
232 full_screen: sending event to the window manager to switch from/to full screen mode
233 return EXIT_SUCCESS on success, 1 on failure
235 int full_screen() {
236 XEvent xev;
238 Atom wm_state = XInternAtom(dpy, "_NET_WM_STATE", True);
239 Atom fullscreen = XInternAtom(dpy, "_NET_WM_STATE_FULLSCREEN", True);
240 long mask = SubstructureNotifyMask;
242 if (fullscreen_flag) {
243 fullscreen_flag = False;
244 zoom_factor = 0;
245 } else {
246 fullscreen_flag = True;
247 zoom_factor = 1000;
250 memset(&xev, 0, sizeof(xev));
251 xev.type = ClientMessage;
252 xev.xclient.display = dpy;
253 xev.xclient.window = win;
254 xev.xclient.message_type = wm_state;
255 xev.xclient.format = 32;
256 xev.xclient.data.l[0] = fullscreen_flag;
257 xev.xclient.data.l[1] = fullscreen;
259 if (!XSendEvent(dpy, DefaultRootWindow(dpy), False, mask, &xev)) {
260 fprintf(stderr, "Error: sending fullscreen event to xserver\n");
261 return EXIT_FAILURE;
263 return EXIT_SUCCESS;
267 zoom_in_out: apply a zoom factor on the current image
268 arg: 1 to zoom in, 0 to zoom out
269 return EXIT_SUCCESS on success, 1 on failure
271 int zoom_in_out(int z) {
272 RImage *old_img = img;
273 RImage *tmp = RLoadImage(ctx, current_link->data, 0);
274 if (!tmp)
275 return EXIT_FAILURE;
277 if (z) {
278 zoom_factor += 0.2;
279 img = RScaleImage(tmp, tmp->width + (int)(tmp->width * zoom_factor), tmp->height + (int)(tmp->height * zoom_factor));
280 if (!img) {
281 img = old_img;
282 return EXIT_FAILURE;
284 } else {
285 zoom_factor -= 0.2;
286 int new_width = tmp->width + (int) (tmp->width * zoom_factor);
287 int new_height = tmp->height + (int)(tmp->height * zoom_factor);
288 if ((new_width <= 0) || (new_height <= 0)) {
289 zoom_factor += 0.2;
290 RReleaseImage(tmp);
291 return EXIT_FAILURE;
293 img = RScaleImage(tmp, new_width, new_height);
294 if (!img) {
295 img = old_img;
296 return EXIT_FAILURE;
299 RReleaseImage(old_img);
300 RReleaseImage(tmp);
301 XFreePixmap(dpy, pix);
303 merge_with_background(img);
304 if (!RConvertImage(ctx, img, &pix)) {
305 fprintf(stderr, "%s\n", RMessageForError(RErrorCode));
306 return EXIT_FAILURE;
308 XResizeWindow(dpy, win, img->width, img->height);
309 return EXIT_SUCCESS;
313 zoom_in: transitional fct used to call zoom_in_out with zoom in flag
314 return EXIT_SUCCESS on success, 1 on failure
316 int zoom_in() {
317 return zoom_in_out(1);
321 zoom_out: transitional fct used to call zoom_in_out with zoom out flag
322 return EXIT_SUCCESS on success, 1 on failure
324 int zoom_out() {
325 return zoom_in_out(0);
329 change_image: load previous or next image
330 arg: way which could be PREV or NEXT constant
331 return EXIT_SUCCESS on success, 1 on failure
333 int change_image(int way) {
334 if (img && current_link) {
335 int old_img_width = img->width;
336 int old_img_height = img->height;
338 RReleaseImage(img);
340 if (way == NEXT) {
341 current_link = current_link->next;
342 current_index++;
343 } else {
344 current_link = current_link->prev;
345 current_index--;
347 if (current_link == NULL) {
348 if (way == NEXT) {
349 current_link = list.first;
350 current_index = 1;
351 } else {
352 current_link = list.last;
353 current_index = max_index;
356 if (DEBUG)
357 fprintf(stderr, "current file is> %s\n", (char *)current_link->data);
358 img = RLoadImage(ctx, current_link->data, 0);
360 if (!img) {
361 fprintf(stderr, "Error: %s %s\n", (char *)current_link->data, RMessageForError(RErrorCode));
362 img = draw_failed_image();
363 } else {
364 merge_with_background(img);
366 rescale_image();
367 if (!fullscreen_flag) {
368 if ((old_img_width != img->width) || (old_img_height != img->height)) {
369 XResizeWindow(dpy, win, img->width, img->height);
370 } else {
371 XCopyArea(dpy, pix, win, ctx->copy_gc, 0, 0, img->width, img->height, 0, 0);
373 change_title(&title_property, (char *)current_link->data);
374 } else {
375 XClearWindow(dpy, win);
376 XCopyArea(dpy, pix, win, ctx->copy_gc, 0, 0, img->width, img->height, max_width/2-img->width/2, max_height/2-img->height/2);
378 return EXIT_SUCCESS;
380 return EXIT_FAILURE;
383 #ifdef HAVE_PTHREAD
385 diaporama: send a xevent to display the next image at every delay set to diaporama_delay
386 arg: not used
387 return void
389 void* diaporama(void *arg) {
390 (void) arg;
392 XKeyEvent event;
393 event.display = dpy;
394 event.window = win;
395 event.root = DefaultRootWindow(dpy);
396 event.subwindow = None;
397 event.time = CurrentTime;
398 event.x = 1;
399 event.y = 1;
400 event.x_root = 1;
401 event.y_root = 1;
402 event.same_screen = True;
403 event.keycode = XKeysymToKeycode(dpy, XK_Right);
404 event.state = 0;
405 event.type = KeyPress;
407 while(diaporama_flag) {
408 int r;
409 r = XSendEvent(event.display, event.window, True, KeyPressMask, (XEvent *)&event);
410 if (!r)
411 fprintf(stderr, "Error sending event\n");
412 XFlush(dpy);
413 /* default sleep time between moving to next image */
414 sleep(diaporama_delay);
416 tid = 0;
417 return arg;
419 #endif
422 linked_list_init: init the linked list
424 void linked_list_init (linked_list_t *list) {
425 list->first = list->last = 0;
426 list->count = 0;
430 linked_list_add: add an element to the linked list
431 return EXIT_SUCCESS on success, 1 otherwise
433 int linked_list_add (linked_list_t *list, const void *data) {
434 link_t *link;
436 /* calloc sets the "next" field to zero. */
437 link = calloc (1, sizeof (link_t));
438 if (! link) {
439 fprintf (stderr, "calloc failed.\n");
440 return EXIT_FAILURE;
442 link->data = data;
443 if (list->last) {
444 /* Join the two final links together. */
445 list->last->next = link;
446 link->prev = list->last;
447 list->last = link;
448 } else {
449 list->first = link;
450 list->last = link;
452 list->count++;
453 return EXIT_SUCCESS;
457 linked_list_free: deallocate the whole linked list
459 void linked_list_free (linked_list_t *list) {
460 link_t *link;
461 link_t *next;
462 for (link = list->first; link; link = next) {
463 /* Store the next value so that we don't access freed memory. */
464 next = link->next;
465 if (link->data)
466 free((char *)link->data);
467 free (link);
472 connect_dir: list and sort by name all files from a given directory
473 arg: the directory path that contains images, the linked list where to add the new file refs
474 return: the first argument of the list or NULL on failure
476 link_t* connect_dir(char *dirpath, linked_list_t *li) {
477 struct dirent **dir;
478 int dv, idx;
479 char path[PATH_MAX] = "";
481 if (!dirpath)
482 return NULL;
484 dv = scandir(dirpath, &dir, 0, alphasort);
485 if (dv < 0) {
486 /* maybe it's a file */
487 struct stat stDirInfo;
488 if (lstat(dirpath, &stDirInfo) == 0) {
489 linked_list_add (li, strdup(dirpath));
490 return li->first;
491 } else {
492 return NULL;
495 for (idx = 0; idx < dv; idx++) {
496 struct stat stDirInfo;
497 if (dirpath[strlen(dirpath)-1] == FILE_SEPARATOR)
498 snprintf(path, PATH_MAX, "%s%s", dirpath, dir[idx]->d_name);
499 else
500 snprintf(path, PATH_MAX, "%s%c%s", dirpath, FILE_SEPARATOR, dir[idx]->d_name);
502 free(dir[idx]);
503 if ((lstat(path, &stDirInfo) == 0) && !S_ISDIR(stDirInfo.st_mode)) {
504 linked_list_add (li, strdup(path));
507 free(dir);
508 return li->first;
512 main
514 int main(int argc, char **argv) {
515 int option = -1;
516 RContextAttributes attr;
517 XEvent e;
518 KeySym keysym;
519 char *reading_filename = "";
520 int screen, file_i;
521 int quit = 0;
522 XClassHint *class_hints;
523 XSizeHints *size_hints;
524 XWMHints *win_hints;
525 #ifdef USE_XPM
526 Pixmap icon_pixmap, icon_shape;
527 #endif
529 if (!(class_hints = XAllocClassHint())) {
530 fprintf(stderr, "Error: failure allocating memory\n");
531 return EXIT_FAILURE;
533 class_hints->res_name = (char *)APPNAME;
534 class_hints->res_class = "default";
536 /* init colors */
537 lightGray.red = lightGray.green = lightGray.blue = 211;
538 darkGray.red = darkGray.green = darkGray.blue = 169;
539 lightGray.alpha = darkGray.alpha = 1;
540 black.red = black.green = black.blue = 0;
541 red.red = 255;
542 red.green = red.blue = 0;
544 if ((option = getopt(argc, argv, "hv")) != -1) {
545 switch (option) {
546 case 'h':
547 fprintf(stderr, "Usage: %s [image(s)|directory]\n"
548 "Keys:\n"
549 "+: zoom in\n"
550 "-: zoom out\n"
551 "esc: actual size\n"
552 #ifdef HAVE_PTHREAD
553 "d: launch diaporama mode\n"
554 #endif
555 "q: quit\n"
556 "right: next image\n"
557 "left: previous image\n"
558 "up: first image\n"
559 "down: last image\n",
560 argv[0]);
561 return EXIT_SUCCESS;
562 case 'v':
563 fprintf(stderr, "%s version %d.%d\n", APPNAME, APPVERSION_MAJOR, APPVERSION_MINOR);
564 return EXIT_SUCCESS;
565 case '?':
566 return EXIT_FAILURE;
570 linked_list_init (&list);
572 dpy = XOpenDisplay(NULL);
573 if (!dpy) {
574 fprintf(stderr, "Error: can't open display");
575 linked_list_free (&list);
576 return EXIT_FAILURE;
579 screen = DefaultScreen(dpy);
580 max_width = DisplayWidth(dpy, screen);
581 max_height = DisplayHeight(dpy, screen);
583 attr.flags = RC_RenderMode | RC_ColorsPerChannel;
584 attr.render_mode = RDitheredRendering;
585 attr.colors_per_channel = 4;
586 ctx = RCreateContext(dpy, DefaultScreen(dpy), &attr);
588 if (argc < 2) {
589 argv[1] = ".";
590 argc = 2;
593 for (file_i = 1; file_i < argc; file_i++) {
594 current_link = connect_dir(argv[file_i], &list);
595 if (current_link) {
596 reading_filename = (char *)current_link->data;
597 max_index = list.count;
601 img = RLoadImage(ctx, reading_filename, 0);
603 if (!img) {
604 fprintf(stderr, "Error: %s %s\n", reading_filename, RMessageForError(RErrorCode));
605 img = draw_failed_image();
606 if (!current_link)
607 return EXIT_FAILURE;
610 merge_with_background(img);
611 rescale_image();
613 if (DEBUG)
614 fprintf(stderr, "display size: %dx%d\n", max_width, max_height);
616 win = XCreateSimpleWindow(dpy, DefaultRootWindow(dpy), 0, 0, img->width, img->height, 0, 0, BlackPixel(dpy, screen));
617 XSelectInput(dpy, win, KeyPressMask|StructureNotifyMask|ExposureMask|ButtonPressMask|FocusChangeMask);
619 if (!(size_hints = XAllocSizeHints())) {
620 fprintf(stderr, "Error: failure allocating memory\n");
621 return EXIT_FAILURE;
623 size_hints->width = img->width;
624 size_hints->height = img->height;
626 Atom delWindow = XInternAtom(dpy, "WM_DELETE_WINDOW", 0);
627 XSetWMProtocols(dpy, win, &delWindow, 1);
628 change_title(&title_property, reading_filename);
630 win_hints = XAllocWMHints();
631 if (win_hints) {
632 win_hints->flags = StateHint|InputHint|WindowGroupHint;
634 #ifdef USE_XPM
635 if ((XpmCreatePixmapFromData(dpy, win, wmiv_xpm, &icon_pixmap, &icon_shape, NULL)) == 0) {
636 win_hints->flags |= IconPixmapHint|IconMaskHint|IconPositionHint;
637 win_hints->icon_pixmap = icon_pixmap;
638 win_hints->icon_mask = icon_shape;
639 win_hints->icon_x = 0;
640 win_hints->icon_y = 0;
642 #endif
643 win_hints->initial_state = NormalState;
644 win_hints->input = True;
645 win_hints->window_group = win;
646 XStringListToTextProperty((char **)&APPNAME, 1, &icon_property);
647 XSetWMProperties(dpy, win, NULL, &icon_property, argv, argc, size_hints, win_hints, class_hints);
648 if (icon_property.value)
649 XFree(icon_property.value);
650 XFree(win_hints);
651 XFree(class_hints);
652 XFree(size_hints);
655 XMapWindow(dpy, win);
656 XFlush(dpy);
657 XCopyArea(dpy, pix, win, ctx->copy_gc, 0, 0, img->width, img->height, 0, 0);
659 while (!quit) {
660 XNextEvent(dpy, &e);
661 if (e.type == ClientMessage) {
662 if (e.xclient.data.l[0] == delWindow)
663 quit = 1;
664 break;
666 if (e.type == FocusIn) {
667 focus = True;
668 continue;
670 if (e.type == FocusOut) {
671 focus = False;
672 continue;
674 if (!fullscreen_flag && (e.type == Expose)) {
675 XExposeEvent xev = e.xexpose;
676 if (xev.count == 0)
677 XCopyArea(dpy, pix, win, ctx->copy_gc, 0, 0, img->width, img->height, 0, 0);
678 continue;
680 if (!fullscreen_flag && e.type == ConfigureNotify) {
681 XConfigureEvent xce = e.xconfigure;
682 if (xce.width != img->width || xce.height != img->height) {
683 RImage *old_img = img;
684 img = RLoadImage(ctx, current_link->data, 0);
685 if (!img) {
686 /* keep the old img and window size */
687 img = old_img;
688 XResizeWindow(dpy, win, img->width, img->height);
689 } else {
690 img = RScaleImage(img, xce.width, xce.height);
691 if (!img) {
692 img = old_img;
693 XResizeWindow(dpy, win, img->width, img->height);
694 } else {
695 merge_with_background(img);
696 if (RConvertImage(ctx, img, &pix))
697 RReleaseImage(old_img);
698 XResizeWindow(dpy, win, img->width, img->height);
702 continue;
704 if (fullscreen_flag && e.type == ConfigureNotify) {
705 maximize_image();
706 continue;
708 if (e.type == ButtonPress) {
709 switch(e.xbutton.button) {
710 case Button1: {
711 if (focus) {
712 if (img && (e.xbutton.x > img->width/2))
713 change_image(NEXT);
714 else
715 change_image(PREV);
718 break;
719 case Button4:
720 zoom_in();
721 break;
722 case Button5:
723 zoom_out();
724 break;
725 case 8:
726 change_image(PREV);
727 break;
728 case 9:
729 change_image(NEXT);
730 break;
732 continue;
734 if (e.type == KeyPress) {
735 keysym = XkbKeycodeToKeysym(dpy, e.xkey.keycode, 0, e.xkey.state & ShiftMask?1:0);
736 #ifdef HAVE_PTHREAD
737 if (keysym != XK_Right)
738 diaporama_flag = False;
739 #endif
740 switch (keysym) {
741 case XK_Right:
742 change_image(NEXT);
743 break;
744 case XK_Left:
745 change_image(PREV);
746 break;
747 case XK_Up:
748 if (current_link) {
749 current_link = list.last;
750 change_image(NEXT);
752 break;
753 case XK_Down:
754 if (current_link) {
755 current_link = list.first;
756 change_image(PREV);
758 break;
759 #ifdef HAVE_PTHREAD
760 case XK_F5:
761 case XK_d:
762 if (!tid) {
763 if (current_link && !diaporama_flag) {
764 diaporama_flag = True;
765 pthread_create(&tid, NULL, &diaporama, NULL);
766 } else {
767 fprintf(stderr, "Can't use diaporama mode, need a picture directory\n");
770 break;
771 #endif
772 case XK_q:
773 quit = 1;
774 break;
775 case XK_Escape:
776 if (!fullscreen_flag) {
777 zoom_factor = -0.2;
778 /* zoom_in will increase the zoom factor by 0.2 */
779 zoom_in();
780 } else {
781 /* we are in fullscreen mode already, want to return to normal size */
782 full_screen();
784 break;
785 case XK_plus:
786 zoom_in();
787 break;
788 case XK_minus:
789 zoom_out();
790 break;
791 case XK_F11:
792 case XK_f:
793 full_screen();
794 break;
800 if (img)
801 RReleaseImage(img);
802 if (pix)
803 XFreePixmap(dpy, pix);
804 #ifdef USE_XPM
805 if (icon_pixmap)
806 XFreePixmap(dpy, icon_pixmap);
807 if (icon_shape)
808 XFreePixmap(dpy, icon_shape);
809 #endif
810 linked_list_free(&list);
811 RDestroyContext(ctx);
812 RShutdown();
813 XCloseDisplay(dpy);
814 return EXIT_SUCCESS;