/* * Window Maker window manager * * Copyright (c) 2014-2026 Window Maker Team - David Maciejak * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #if !defined(_GNU_SOURCE) #define _GNU_SOURCE #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "config.h" #include "xdnd.h" #ifdef HAVE_EXIF #include #endif #ifdef HAVE_PTHREAD #include #endif #ifdef HAVE_LIBARCHIVE #include #include #endif #ifdef USE_RANDR #include #endif #ifdef USE_XPM extern int XpmCreatePixmapFromData(Display *, Drawable, char **, Pixmap *, Pixmap *, void *); /* This is the icon from the eog project git.gnome.org/browse/eog */ #include "wmiv.h" #endif #ifdef DEBUG #define WMIV_DEBUG 1 #else #define WMIV_DEBUG 0 #endif #define FILE_SEPARATOR '/' /* CHUNK size for INCR (bytes). Tune if desired. */ #define CHUNK_SIZE 65536 Display *dpy; Window win; RContext *ctx; RImage *img; Pixmap pix; /* DND support for sending drops */ static DndClass send_dnd; static Bool dnd_initialized = False; static Bool button1_pressed = False; static int drag_start_x, drag_start_y; static int drag_threshold = 5; /* pixels */ const char *APPNAME = "wmiv"; int NEXT = 0; int PREV = 1; float zoom_factor = 0; int max_width = 0; int max_height = 0; /* Archive support */ static char *temp_dir = NULL; static Bool is_archive = False; Bool fullscreen_flag = False; Bool focus = True; Bool back_from_fullscreen = False; Bool ignore_unknown_file_format = False; #ifdef HAVE_PTHREAD Bool slideshow_flag = False; int slideshow_delay = 5; pthread_t tid = 0; #endif XTextProperty title_property; XTextProperty icon_property; unsigned current_index = 1; unsigned max_index = 1; RColor lightGray; RColor darkGray; RColor black; RColor red; /* Structure to hold the frame extents */ typedef struct { unsigned long left; unsigned long right; unsigned long top; unsigned long bottom; } FrameExtents; FrameExtents extents = {0, 0, 0, 0}; typedef struct link link_t; struct link { const void *data; link_t *prev; link_t *next; }; typedef struct linked_list { int count; link_t *first; link_t *last; } linked_list_t; linked_list_t list; link_t *current_link; /* --- Clipboard/PNG + JPEG + INCR globals --- */ static Atom clipboard_atom = None; static Atom targets_atom = None; static Atom png_atom = None; static Atom jpeg_atom = None; static Atom property_atom = None; static Atom incr_atom = None; static unsigned char *clipboard_png_data = NULL; static size_t clipboard_png_size = 0; static unsigned char *clipboard_jpeg_data = NULL; static size_t clipboard_jpeg_size = 0; static Bool we_own_clipboard = False; /* INCR transfer state */ typedef struct { Bool active; Window requestor; Atom property; /* property name the requestor asked for (req->property) */ Atom target; /* requested target (png_atom or jpeg_atom) */ size_t total_size; size_t offset; size_t chunk_size; long saved_event_mask; /* to restore previous event mask on requestor window */ unsigned char *data; /* pointer to either PNG or JPEG data */ } incr_transfer_t; static incr_transfer_t incr = { .active = False }; /* End clipboard globals */ /* Wait for the window manager to set the _NET_FRAME_EXTENTS property by listening for PropertyNotify events Returns 1 on success, 0 on timeout */ int wait_for_frame_extents(Display *disp, Window win, int timeout_ms) { Atom net_frame_extents = XInternAtom(disp, "_NET_FRAME_EXTENTS", False); if (net_frame_extents == None) return 0; XEvent event; int elapsed = 0; const int poll_interval = 10; /* milliseconds */ while (elapsed < timeout_ms) { /* Check if property is already available */ Atom actual_type; int actual_format; unsigned long num_items; unsigned long bytes_after; unsigned char *prop_return = NULL; int status = XGetWindowProperty(disp, win, net_frame_extents, 0L, 1L, False, XA_CARDINAL, &actual_type, &actual_format, &num_items, &bytes_after, &prop_return); if (status == Success && actual_type == XA_CARDINAL && num_items >= 1) { if (prop_return) XFree(prop_return); return 1; } if (prop_return) XFree(prop_return); /* Wait for events or timeout */ if (XPending(disp)) { XNextEvent(disp, &event); if (event.type == PropertyNotify && event.xproperty.window == win && event.xproperty.atom == net_frame_extents && event.xproperty.state == PropertyNewValue) { return 1; } } usleep(poll_interval * 1000); elapsed += poll_interval; XFlush(disp); } return 0; } /* Get the frame extents (decorations) of a window Returns 1 on success, 0 on failure */ int get_window_decor_size(Display *disp, Window win, FrameExtents *extents) { Atom actual_type; int actual_format; unsigned long num_items; unsigned long bytes_after; unsigned char *prop_return = NULL; Atom net_frame_extents; int status; /* Get the atom for the _NET_FRAME_EXTENTS property */ net_frame_extents = XInternAtom(disp, "_NET_FRAME_EXTENTS", False); if (net_frame_extents == None) { fprintf(stderr, "Warning: _NET_FRAME_EXTENTS atom not supported\n"); return 0; } /* Retrieve the property value */ status = XGetWindowProperty(disp, win, net_frame_extents, 0L, 4L, False, XA_CARDINAL, &actual_type, &actual_format, &num_items, &bytes_after, &prop_return); if (status == Success && actual_type == XA_CARDINAL && actual_format == 32 && num_items == 4) { unsigned long *extents_data = (unsigned long *)prop_return; extents->left = extents_data[0]; extents->right = extents_data[1]; extents->top = extents_data[2]; extents->bottom = extents_data[3]; if (WMIV_DEBUG) { fprintf(stderr, "Successfully got frame extents: left=%lu, right=%lu, top=%lu, bottom=%lu\n", extents->left, extents->right, extents->top, extents->bottom); } XFree(prop_return); return 1; } if (prop_return) XFree(prop_return); if (status != Success) { if (WMIV_DEBUG) { fprintf(stderr, "Error: XGetWindowProperty failed with status: %d\n", status); } } else if (actual_type != XA_CARDINAL) { if (WMIV_DEBUG) { fprintf(stderr, "Error: wrong property type: got %lu, expected %lu (XA_CARDINAL)\n", (unsigned long)actual_type, (unsigned long)XA_CARDINAL); } } else if (actual_format != 32) { if (WMIV_DEBUG) { fprintf(stderr, "Error: wrong property format: got %d, expected 32\n", actual_format); } } else if (num_items != 4) { if (WMIV_DEBUG) { fprintf(stderr, "Error: wrong number of items: got %lu, expected 4\n", num_items); } } return 0; } #ifdef HAVE_LIBARCHIVE /* Create directory path recursively Returns 0 on success, -1 on failure */ static int create_directories(const char *path) { char *path_copy = strdup(path); char *p; int result = 0; if (!path_copy) return -1; /* Skip leading slash if present */ p = path_copy; if (*p == '/') p++; /* Create each directory component */ while ((p = strchr(p, '/')) != NULL) { *p = '\0'; if (mkdir(path_copy, 0755) != 0 && errno != EEXIST) { result = -1; break; } *p = '/'; p++; } /* Create final directory component */ if (result == 0 && mkdir(path_copy, 0755) != 0 && errno != EEXIST) result = -1; free(path_copy); return result; } #endif /* Callback function for nftw to remove files and directories */ static int remove_temp_file(const char *fpath, const struct stat *sb, int typeflag, struct FTW *ftwbuf) { (void)sb; (void)typeflag; (void)ftwbuf; return remove(fpath); } /* Remove the temporary directory and its contents */ static void cleanup_temp_dir(void) { if (temp_dir) { if (WMIV_DEBUG) fprintf(stderr, "Cleaning up temporary directory: %s\n", temp_dir); nftw(temp_dir, remove_temp_file, 64, FTW_DEPTH | FTW_PHYS); free(temp_dir); temp_dir = NULL; } is_archive = False; } /* Check if file has archive extension Returns True if it is an archive, False otherwise */ static Bool is_archive_file(const char *filename) { const char *ext = strrchr(filename, '.'); return (ext && (strcasecmp(ext, ".cbz") == 0 || strcasecmp(ext, ".zip") == 0 || strcasecmp(ext, ".cbr") == 0 || strcasecmp(ext, ".rar") == 0 || strcasecmp(ext, ".cbt") == 0 || strcasecmp(ext, ".cb7") == 0 || strcasecmp(ext, ".tar") == 0 || strcasecmp(ext, ".7z") == 0 || strcasecmp(ext, ".tar.gz") == 0 || strcasecmp(ext, ".tar.bz2") == 0)); } #ifdef HAVE_LIBARCHIVE /* Recursively find directory containing image files Returns path to directory with images, or NULL if not found */ static char *find_image_directory(const char *start_dir) { DIR *dir; struct dirent *entry; struct stat st; char full_path[PATH_MAX]; int image_count = 0; char *result = NULL; dir = opendir(start_dir); if (!dir) return NULL; /* First pass: count image files in current directory */ while ((entry = readdir(dir)) != NULL) { if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) continue; snprintf(full_path, sizeof(full_path), "%s/%s", start_dir, entry->d_name); if (stat(full_path, &st) == 0 && S_ISREG(st.st_mode)) image_count++; } closedir(dir); /* If current directory has images, return it */ if (image_count > 0) { return strdup(start_dir); } /* Second pass: recursively check subdirectories */ dir = opendir(start_dir); if (!dir) return NULL; while ((entry = readdir(dir)) != NULL) { if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) continue; snprintf(full_path, sizeof(full_path), "%s/%s", start_dir, entry->d_name); if (stat(full_path, &st) == 0 && S_ISDIR(st.st_mode)) { result = find_image_directory(full_path); if (result) break; } } closedir(dir); return result; } /* Extract archive using libarchive Returns 0 on success, -1 on failure */ static int extract_archive_libarchive(const char *filename, const char *dest_dir) { struct archive *a; struct archive_entry *entry; char dest_path[PATH_MAX]; char *buffer = NULL; const size_t buffer_size = 8192; int r; /* Create archive reader */ a = archive_read_new(); if (!a) { fprintf(stderr, "Error: failed to create archive reader\n"); return -1; } /* Enable support for all formats and filters */ archive_read_support_filter_all(a); archive_read_support_format_all(a); /* Open the archive file */ r = archive_read_open_filename(a, filename, 10240); if (r != ARCHIVE_OK) { fprintf(stderr, "Error: failed to open archive: %s\n", filename); archive_read_free(a); return -1; } buffer = malloc(buffer_size); if (!buffer) { fprintf(stderr, "Error: failed to allocate memory\n"); archive_read_free(a); return -1; } /* Extract each entry */ while ((r = archive_read_next_header(a, &entry)) == ARCHIVE_OK) { const char *entry_name = archive_entry_pathname(entry); la_int64_t entry_size = archive_entry_size(entry); mode_t entry_mode = archive_entry_mode(entry); FILE *output_file; la_ssize_t bytes_read; if (!entry_name) continue; /* Skip directories and special files */ if (!S_ISREG(entry_mode) || entry_size <= 0) continue; /* Create destination path */ snprintf(dest_path, sizeof(dest_path), "%s/%s", dest_dir, entry_name); /* Create directory structure if needed */ char *dir_end = strrchr(dest_path, '/'); if (dir_end) { *dir_end = '\0'; if (create_directories(dest_path) != 0) { if (WMIV_DEBUG) fprintf(stderr, "Warning: failed to create directory: %s\n", dest_path); } *dir_end = '/'; } /* Create output file */ output_file = fopen(dest_path, "wb"); if (!output_file) { if (WMIV_DEBUG) fprintf(stderr, "Warning: failed to create file: %s\n", dest_path); continue; } /* Extract file content */ while ((bytes_read = archive_read_data(a, buffer, buffer_size)) > 0) { fwrite(buffer, 1, bytes_read, output_file); } if (bytes_read < 0) { if (WMIV_DEBUG) fprintf(stderr, "Warning: failed to read data for: %s\n", entry_name); } fclose(output_file); //if (WMIV_DEBUG) // fprintf(stderr, "Extracted %s\n", entry_name); } if (r != ARCHIVE_EOF) fprintf(stderr, "Warning: archive reading ended with error\n"); free(buffer); archive_read_free(a); return 0; } #endif /* Extract archive to temporary directory Returns path to extracted directory on success, NULL on failure */ static char *extract_archive(const char *filename) { #ifdef HAVE_LIBARCHIVE char *temp_template = NULL; int result; /* Create temporary directory template */ if (asprintf(&temp_template, "/tmp/wmiv_XXXXXX") == -1) { fprintf(stderr, "Error: failed to allocate memory\n"); return NULL; } /* Create temporary directory */ temp_dir = mkdtemp(temp_template); if (!temp_dir) { perror("mkdtemp"); free(temp_template); return NULL; } if (WMIV_DEBUG) fprintf(stderr, "Created temporary directory: %s\n", temp_dir); /* Extract the archive */ if (is_archive_file(filename)) { result = extract_archive_libarchive(filename, temp_dir); } else { fprintf(stderr, "Error: unknown archive type\n"); cleanup_temp_dir(); return NULL; } if (result != 0) { cleanup_temp_dir(); return NULL; } /* Check if extraction resulted in nested directories - find the one with actual images */ char *image_dir = find_image_directory(temp_dir); if (image_dir && strcmp(image_dir, temp_dir) != 0) { if (WMIV_DEBUG) fprintf(stderr, "Archive contains nested directories, using image directory: %s\n", image_dir); char *result_dir = strdup(image_dir); free(image_dir); is_archive = True; return result_dir; } if (image_dir) free(image_dir); is_archive = True; return strdup(temp_dir); #else (void) filename; fprintf(stderr, "Error: archive support not available\n"); return NULL; #endif } /* Convert current RImage to specified format using RSaveRawImage Returns 0 on success (buffer & size filled), -1 on failure */ static int convert_image_to_format(RImage *image, const char *format, unsigned char **out_buf, size_t *out_size) { if (!image || !format || !out_buf || !out_size) { if (WMIV_DEBUG) fprintf(stderr, "Error: convert_image_to_format invalid parameters\n"); return -1; } if (!RSaveRawImage(image, format, out_buf, out_size)) { if (WMIV_DEBUG) fprintf(stderr, "Error: RSaveRawImage failed for %s, RErrorCode=%d\n", format, RErrorCode); return -1; } return 0; } /* Free current clipboard data (both PNG and JPEG) */ static void free_clipboard_data(void) { if (clipboard_png_data) { free(clipboard_png_data); clipboard_png_data = NULL; } clipboard_png_size = 0; if (clipboard_jpeg_data) { free(clipboard_jpeg_data); clipboard_jpeg_data = NULL; } clipboard_jpeg_size = 0; } /* Send the next chunk for INCR transfer (called when requestor deletes the property) */ static void send_next_incr_chunk(void) { size_t remaining, to_send; if (!incr.active) return; if (!incr.data || incr.total_size == 0) { /* Nothing to send: finish with zero-length property */ XChangeProperty(dpy, incr.requestor, incr.property, incr.target, 8, PropModeReplace, NULL, 0); /* Clean up: remove PropertyChangeMask from requestor window */ XSelectInput(dpy, incr.requestor, NoEventMask); incr.active = False; we_own_clipboard = False; return; } remaining = incr.total_size - incr.offset; to_send = remaining; if (to_send > incr.chunk_size) to_send = incr.chunk_size; XChangeProperty(dpy, incr.requestor, incr.property, incr.target, 8, PropModeReplace, incr.data + incr.offset, (int)to_send); XFlush(dpy); incr.offset += to_send; if (incr.offset >= incr.total_size) { /* finished: next deletion by client -> we'll send zero-length and finish */ if (WMIV_DEBUG) fprintf(stderr, "INCR: all data sent (%zu bytes), awaiting final property deletion\n", incr.total_size); } else { if (WMIV_DEBUG) fprintf(stderr, "INCR: sent chunk %zu/%zu\n", incr.offset, incr.total_size); } } /* Called when Ctrl+C pressed: encode current image to PNG and JPEG and claim CLIPBOARD */ static void copy_current_image_to_clipboard(void) { if (!dpy || !win || !img) return; /* free previous */ free_clipboard_data(); if (WMIV_DEBUG) fprintf(stderr, "Attempting to copy image to clipboard (size: %dx%d)\n", img->width, img->height); /* Convert image to both formats */ #ifdef USE_PNG if (convert_image_to_format(img, "PNG", &clipboard_png_data, &clipboard_png_size) != 0) { fprintf(stderr, "Error: failed to convert image to PNG\n"); return; } #endif #ifdef USE_JPEG if (convert_image_to_format(img, "JPEG", &clipboard_jpeg_data, &clipboard_jpeg_size) != 0) { fprintf(stderr, "Error: failed to convert image to JPEG\n"); free_clipboard_data(); return; } #endif if (clipboard_png_data == NULL && clipboard_jpeg_data == NULL) { fprintf(stderr, "Error: no image formats available to copy to the clipboard\n"); return; } /* claim CLIPBOARD */ XSetSelectionOwner(dpy, clipboard_atom, win, CurrentTime); if (XGetSelectionOwner(dpy, clipboard_atom) != win) { fprintf(stderr, "Error: failed to own clipboard selection\n"); free_clipboard_data(); we_own_clipboard = False; return; } we_own_clipboard = True; XFlush(dpy); if (WMIV_DEBUG) fprintf(stderr, "Copied image to clipboard (PNG: %zu bytes, JPEG: %zu bytes)\n", clipboard_png_size, clipboard_jpeg_size); } /* Handle SelectionRequest events: serve TARGETS and image formats (with INCR when needed) */ static void handle_selection_request(XSelectionRequestEvent *req) { if (WMIV_DEBUG) { char *target_name = XGetAtomName(req->display, req->target); char *selection_name = XGetAtomName(req->display, req->selection); fprintf(stderr, "SelectionRequest received: target=%s, selection=%s\n", target_name ? target_name : "unknown", selection_name ? selection_name : "unknown"); if (target_name) XFree(target_name); if (selection_name) XFree(selection_name); } XEvent ev; memset(&ev, 0, sizeof(ev)); ev.xselection.type = SelectionNotify; ev.xselection.display = req->display; ev.xselection.requestor = req->requestor; ev.xselection.selection = req->selection; ev.xselection.target = req->target; ev.xselection.time = req->time; ev.xselection.property = None; if (req->target == targets_atom) { /* advertise supported targets (image/png and image/jpeg) */ #if defined(USE_PNG) && defined(USE_JPEG) Atom types[2]; types[0] = png_atom; types[1] = jpeg_atom; #else #if defined(USE_JPEG) Atom types[1]; types[0] = jpeg_atom; #else Atom types[1]; types[0] = png_atom; #endif #endif XChangeProperty(dpy, req->requestor, req->property, XA_ATOM, 32, PropModeReplace, (unsigned char *)types, 2); ev.xselection.property = req->property; } else if (req->target == png_atom) { if (!clipboard_png_data || clipboard_png_size == 0) { ev.xselection.property = None; } else { /* Decide whether to use INCR */ if (clipboard_png_size > CHUNK_SIZE) { /* Start INCR transfer */ if (WMIV_DEBUG) fprintf(stderr, "Starting INCR for PNG %zu bytes\n", clipboard_png_size); /* Prepare incr state */ incr.active = True; incr.requestor = req->requestor; incr.property = req->property; incr.target = req->target; incr.total_size = clipboard_png_size; incr.offset = 0; incr.chunk_size = CHUNK_SIZE; incr.data = clipboard_png_data; /* Announce INCR by writing a 32-bit size to the property of type INCR */ unsigned long size32 = (unsigned long)incr.total_size; XChangeProperty(dpy, req->requestor, req->property, incr_atom, 32, PropModeReplace, (unsigned char *)&size32, 1); ev.xselection.property = req->property; /* Send SelectionNotify now to tell client INCR was used */ XSendEvent(dpy, req->requestor, False, 0, (XEvent *)&ev); XFlush(dpy); /* Now select PropertyChangeMask on the requestor window to see property deletions. The client should delete the property when ready for the first chunk. */ XSelectInput(dpy, req->requestor, PropertyChangeMask); return; /* we already sent SelectionNotify above */ } else { /* Small transfer: send whole PNG in one property set */ XChangeProperty(dpy, req->requestor, req->property, png_atom, 8, PropModeReplace, clipboard_png_data, (int)clipboard_png_size); ev.xselection.property = req->property; } } } else if (req->target == jpeg_atom) { if (!clipboard_jpeg_data || clipboard_jpeg_size == 0) { ev.xselection.property = None; } else { /* Decide whether to use INCR */ if (clipboard_jpeg_size > CHUNK_SIZE) { /* Start INCR transfer */ if (WMIV_DEBUG) fprintf(stderr, "Starting INCR for JPEG %zu bytes\n", clipboard_jpeg_size); /* Prepare incr state */ incr.active = True; incr.requestor = req->requestor; incr.property = req->property; incr.target = req->target; incr.total_size = clipboard_jpeg_size; incr.offset = 0; incr.chunk_size = CHUNK_SIZE; incr.data = clipboard_jpeg_data; /* Announce INCR by writing a 32-bit size to the property of type INCR */ unsigned long size32 = (unsigned long)incr.total_size; XChangeProperty(dpy, req->requestor, req->property, incr_atom, 32, PropModeReplace, (unsigned char *)&size32, 1); ev.xselection.property = req->property; /* Send SelectionNotify now to tell client INCR was used */ XSendEvent(dpy, req->requestor, False, 0, (XEvent *)&ev); XFlush(dpy); /* Now select PropertyChangeMask on the requestor window to see property deletions. The client should delete the property when ready for the first chunk. */ XSelectInput(dpy, req->requestor, PropertyChangeMask); return; /* we already sent SelectionNotify above */ } else { /* Small transfer: send whole JPEG in one property set */ XChangeProperty(dpy, req->requestor, req->property, jpeg_atom, 8, PropModeReplace, clipboard_jpeg_data, (int)clipboard_jpeg_size); ev.xselection.property = req->property; } } } else { ev.xselection.property = None; } XSendEvent(dpy, req->requestor, False, 0, &ev); XFlush(dpy); } /* Called when the owner loses the selection */ static void handle_selection_clear(void) { if (we_own_clipboard) { we_own_clipboard = False; free_clipboard_data(); } if (incr.active) { /* Cancel any active INCR transfer and clean up */ if (incr.requestor != None) { /* Remove PropertyChangeMask from requestor window */ XSelectInput(dpy, incr.requestor, NoEventMask); } incr.active = False; incr.offset = 0; incr.total_size = 0; incr.requestor = None; incr.property = None; incr.target = None; } } /* Load an image and optionally get its orientation if libexif is available Returns the image on success, NULL on failure */ RImage *load_oriented_image(RContext *context, const char *file, int index) { RImage *image; #ifdef HAVE_EXIF int orientation = 0; #endif image = RLoadImage(context, file, index); if (!image) return NULL; #ifdef HAVE_EXIF ExifData *exifData = exif_data_new_from_file(file); if (exifData) { ExifByteOrder byteOrder = exif_data_get_byte_order(exifData); ExifEntry *exifEntry = exif_data_get_entry(exifData, EXIF_TAG_ORIENTATION); if (exifEntry) orientation = exif_get_short(exifEntry->data, byteOrder); exif_data_free(exifData); } /* 0th Row 0th Column 1 top left side 2 top right side 3 bottom right side 4 bottom left side 5 left side top 6 right side top 7 right side bottom 8 left side bottom */ if (image && (orientation > 1)) { RImage *tmp = NULL; switch (orientation) { case 2: tmp = RFlipImage(image, RHorizontalFlip); break; case 3: tmp = RRotateImage(image, 180); break; case 4: tmp = RFlipImage(image, RVerticalFlip); break; case 5: { RImage *tmp2; tmp2 = RFlipImage(image, RVerticalFlip); if (tmp2) { tmp = RRotateImage(tmp2, 90); RReleaseImage(tmp2); } } break; case 6: tmp = RRotateImage(image, 90); break; case 7: { RImage *tmp2; tmp2 = RFlipImage(image, RVerticalFlip); if (tmp2) { tmp = RRotateImage(tmp2, 270); RReleaseImage(tmp2); } } break; case 8: tmp = RRotateImage(image, 270); break; } if (tmp) { RReleaseImage(image); image = tmp; } } #endif return image; } /* Change window title Returns EXIT_SUCCESS on success, EXIT_FAILURE on failure */ int change_title(XTextProperty *prop, char *filename) { char *combined_title = NULL; if (ignore_unknown_file_format) { if (!asprintf(&combined_title, "%s - %s", APPNAME, filename)) return EXIT_FAILURE; } else { if (!asprintf(&combined_title, "%s - %u/%u - %s", APPNAME, current_index, max_index, filename)) if (!asprintf(&combined_title, "%s - %u/%u", APPNAME, current_index, max_index)) return EXIT_FAILURE; } Xutf8TextListToTextProperty(dpy, &combined_title, 1, XUTF8StringStyle, prop); XSetWMName(dpy, win, prop); if (prop->value) XFree(prop->value); free(combined_title); return EXIT_SUCCESS; } /* Rescale the current image based on the screen size Returns EXIT_SUCCESS on success, EXIT_FAILURE on failure */ int rescale_image(void) { long final_width = img->width; long final_height = img->height; int max_width_tmp = max_width; int max_height_tmp = max_height; if (!fullscreen_flag) { max_width_tmp -= extents.left + extents.right; max_height_tmp -= extents.top + extents.bottom; } /* check if there is already a zoom factor applied */ if (fabsf(zoom_factor) <= 0.0f) { final_width = img->width + (int)(img->width * zoom_factor); final_height = img->height + (int)(img->height * zoom_factor); } if ((max_width_tmp < final_width) || (max_height_tmp < final_height)) { double val = 0; if (final_width > final_height) { val = final_height * max_width_tmp / final_width; final_width = ceil(final_width * val / final_height); final_height = ceil(val); if (val > max_height_tmp) { val = final_width * max_height_tmp / final_height; final_height = ceil(final_height * val / final_width); final_width = ceil(val); } } else { val = final_width * max_height_tmp / final_height; final_height = ceil(final_height * val / final_width); final_width = ceil(val); if (val > max_width_tmp) { val = final_height * max_width_tmp / final_width; final_width = ceil(final_width * val / final_height); final_height = ceil(val); } } } if ((final_width != img->width) || (final_height != img->height)) { RImage *old_img = img; img = RScaleImage(img, final_width, final_height); if (!img) { img = old_img; return EXIT_FAILURE; } RReleaseImage(old_img); } if (!RConvertImage(ctx, img, &pix)) { fprintf(stderr, "Error: %s\n", RMessageForError(RErrorCode)); return EXIT_FAILURE; } return EXIT_SUCCESS; } /* Find the best image size for the current display Returns EXIT_SUCCESS on success */ int maximize_image(void) { rescale_image(); 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); return EXIT_SUCCESS; } /* Merge the current image with a checkerboard background Returns EXIT_SUCCESS on success, EXIT_FAILURE on failure */ int merge_with_background(RImage *i) { if (i) { RImage *back; back = RCreateImage(i->width, i->height, True); if (back) { int opaq = 255; int x = 0, y = 0; RFillImage(back, &lightGray); for (x = 0; x <= i->width; x += 8) { if (x/8 % 2) y = 8; else y = 0; for (; y <= i->height; y += 16) ROperateRectangle(back, RAddOperation, x, y, x+8, y+8, &darkGray); } RCombineImagesWithOpaqueness(i, back, opaq); RReleaseImage(back); return EXIT_SUCCESS; } } return EXIT_FAILURE; } /* Rotate the image by the angle passed Returns EXIT_SUCCESS on success, EXIT_FAILURE on failure */ int rotate_image(float angle) { RImage *tmp; if (!img) return EXIT_FAILURE; tmp = RRotateImage(img, angle); if (!tmp) return EXIT_FAILURE; if (!fullscreen_flag) { if (img->width != tmp->width || img->height != tmp->height) XResizeWindow(dpy, win, tmp->width, tmp->height); } RReleaseImage(img); img = tmp; rescale_image(); if (!fullscreen_flag) { XCopyArea(dpy, pix, win, ctx->copy_gc, 0, 0, img->width, img->height, 0, 0); } else { XClearWindow(dpy, win); 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); } return EXIT_SUCCESS; } /* Rotate the image by 90 degrees to the right Returns EXIT_SUCCESS on success, EXIT_FAILURE on failure */ int rotate_image_right(void) { return rotate_image(90.0); } /* Rotate the image by -90 degrees to the left Returns EXIT_SUCCESS on success, EXIT_FAILURE on failure */ int rotate_image_left(void) { return rotate_image(-90.0); } /* Create a red crossed image to indicate an error loading file Returns the image on success, NULL on failure */ RImage *draw_failed_image(void) { RImage *failed_image = NULL; XWindowAttributes attr; if (win && (XGetWindowAttributes(dpy, win, &attr) >= 0)) failed_image = RCreateImage(attr.width, attr.height, False); else failed_image = RCreateImage(50, 50, False); if (!failed_image) return NULL; RFillImage(failed_image, &black); /* Calculate 80% size cross centered in the image */ int cross_width = (int)(failed_image->width * 0.6); int cross_height = (int)(failed_image->height * 0.6); int offset_x = (failed_image->width - cross_width) / 2; int offset_y = (failed_image->height - cross_height) / 2; /* Calculate line thickness based on image size (minimum 3, maximum 8 pixels) */ int thickness = (failed_image->width + failed_image->height) / 100; if (thickness < 3) thickness = 3; if (thickness > 8) thickness = 8; /* Draw thick diagonal lines for the cross using 80% of the image size */ for (int i = -thickness/2; i <= thickness/2; i++) { /* First diagonal (top-left to bottom-right) */ ROperateLine(failed_image, RAddOperation, offset_x + i, offset_y, offset_x + cross_width + i, offset_y + cross_height, &red); ROperateLine(failed_image, RAddOperation, offset_x, offset_y + i, offset_x + cross_width, offset_y + cross_height + i, &red); /* Second diagonal (top-right to bottom-left) */ ROperateLine(failed_image, RAddOperation, offset_x + i, offset_y + cross_height, offset_x + cross_width + i, offset_y, &red); ROperateLine(failed_image, RAddOperation, offset_x, offset_y + cross_height - i, offset_x + cross_width, offset_y - i, &red); } return failed_image; } /* Send event to the window manager to switch from/to full screen mode Returns EXIT_SUCCESS on success, EXIT_FAILURE on failure */ int full_screen(void) { XEvent xev; Atom wm_state = XInternAtom(dpy, "_NET_WM_STATE", True); Atom fullscreen = XInternAtom(dpy, "_NET_WM_STATE_FULLSCREEN", True); long mask = SubstructureNotifyMask; if (fullscreen_flag) { fullscreen_flag = False; zoom_factor = 0; back_from_fullscreen = True; } else { fullscreen_flag = True; zoom_factor = 1000; RReleaseImage(img); img = load_oriented_image(ctx, current_link->data, 0); if (!img) img = draw_failed_image(); else merge_with_background(img); } memset(&xev, 0, sizeof(xev)); xev.type = ClientMessage; xev.xclient.display = dpy; xev.xclient.window = win; xev.xclient.message_type = wm_state; xev.xclient.format = 32; xev.xclient.data.l[0] = fullscreen_flag; xev.xclient.data.l[1] = fullscreen; if (!XSendEvent(dpy, DefaultRootWindow(dpy), False, mask, &xev)) { fprintf(stderr, "Error: sending fullscreen event to xserver\n"); return EXIT_FAILURE; } return EXIT_SUCCESS; } /* Init the linked list */ void linked_list_init(linked_list_t *list) { list->first = list->last = 0; list->count = 0; } /* Add an element to the linked list Returns EXIT_SUCCESS on success, EXIT_FAILURE otherwise */ int linked_list_add(linked_list_t *list, const void *data) { link_t *link; /* calloc sets the "next" field to zero. */ link = calloc(1, sizeof(link_t)); if (!link) { fprintf(stderr, "Error: failed to allocate memory\n"); return EXIT_FAILURE; } link->data = data; if (list->last) { /* Join the two final links together. */ list->last->next = link; link->prev = list->last; list->last = link; } else { list->first = link; list->last = link; } list->count++; return EXIT_SUCCESS; } /* Delete an element from the linked list Returns EXIT_SUCCESS on success, EXIT_FAILURE otherwise */ int linked_list_del(linked_list_t *list, link_t *link) { if (!list || !link) { return EXIT_FAILURE; } /* Update previous link's next pointer */ if (link->prev) { link->prev->next = link->next; } else { /* This was the first link */ list->first = link->next; } /* Update next link's previous pointer */ if (link->next) { link->next->prev = link->prev; } else { /* This was the last link */ list->last = link->prev; } if (link->data) free((char *)link->data); free(link); list->count--; return EXIT_SUCCESS; } /* Deallocate the whole linked list */ void linked_list_free(linked_list_t *list) { link_t *link; link_t *next; for (link = list->first; link; link = next) { /* Store the next value so that we don't access freed memory. */ next = link->next; if (link->data) free((char *)link->data); free(link); } current_link = NULL; } /* Apply a zoom factor on the current image Arg: 1 to zoom in, 0 to zoom out Returns EXIT_SUCCESS on success, EXIT_FAILURE on failure */ int zoom_in_out(int z) { RImage *old_img = img; RImage *tmp = load_oriented_image(ctx, current_link->data, 0); if (!tmp) return EXIT_FAILURE; if (z) { zoom_factor += 0.2f; img = RScaleImage(tmp, tmp->width + (int)(tmp->width * zoom_factor), tmp->height + (int)(tmp->height * zoom_factor)); if (!img) { img = old_img; RReleaseImage(tmp); return EXIT_FAILURE; } } else { zoom_factor -= 0.2f; int new_width = tmp->width + (int) (tmp->width * zoom_factor); int new_height = tmp->height + (int)(tmp->height * zoom_factor); if ((new_width <= 0) || (new_height <= 0)) { zoom_factor += 0.2f; RReleaseImage(tmp); return EXIT_FAILURE; } img = RScaleImage(tmp, new_width, new_height); if (!img) { img = old_img; RReleaseImage(tmp); return EXIT_FAILURE; } } RReleaseImage(old_img); RReleaseImage(tmp); XFreePixmap(dpy, pix); merge_with_background(img); if (!RConvertImage(ctx, img, &pix)) { fprintf(stderr, "Error: %s\n", RMessageForError(RErrorCode)); return EXIT_FAILURE; } XResizeWindow(dpy, win, img->width, img->height); return EXIT_SUCCESS; } /* Transitional function used to call zoom_in_out with zoom in flag Returns EXIT_SUCCESS on success, EXIT_FAILURE on failure */ int zoom_in(void) { return zoom_in_out(1); } /* Transitional function used to call zoom_in_out with zoom out flag Returns EXIT_SUCCESS on success, EXIT_FAILURE on failure */ int zoom_out(void) { return zoom_in_out(0); } /* Load previous or next image Arg: way which could be PREV or NEXT constant Returns EXIT_SUCCESS on success, EXIT_FAILURE on failure */ int change_image(int way) { if (max_index == 0) return EXIT_FAILURE; if (img && current_link) { int old_img_width = img->width; int old_img_height = img->height; RReleaseImage(img); if (way == NEXT) { current_link = current_link->next; current_index++; } else { current_link = current_link->prev; current_index--; } if (current_link == NULL) { if (way == NEXT) { current_link = list.first; current_index = 1; } else { current_link = list.last; current_index = max_index; } } if (WMIV_DEBUG) fprintf(stderr, "Current file is> %s\n", (char *)current_link->data); img = load_oriented_image(ctx, current_link->data, 0); if (!img) { if (strlen((char *)current_link->data) == 0) fprintf(stderr, "Error: %s\n", RMessageForError(RErrorCode)); else fprintf(stderr, "Error: %s %s\n", (char *)current_link->data, RMessageForError(RErrorCode)); img = draw_failed_image(); if (ignore_unknown_file_format) { link_t *tmp_link; if (WMIV_DEBUG) fprintf(stderr, "Skipping file...\n"); if (way == NEXT) if (!current_link->prev) tmp_link = list.last; else tmp_link = current_link->prev; else if (!current_link->next) tmp_link = list.first; else tmp_link = current_link->next; linked_list_del(&list, current_link); current_link = tmp_link; max_index = list.count; return change_image(way); } } else { merge_with_background(img); } rescale_image(); if (win) { if (!fullscreen_flag) { if ((old_img_width != img->width) || (old_img_height != img->height)) XResizeWindow(dpy, win, img->width, img->height); else XCopyArea(dpy, pix, win, ctx->copy_gc, 0, 0, img->width, img->height, 0, 0); change_title(&title_property, (char *)current_link->data); } else { XClearWindow(dpy, win); 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); } } return EXIT_SUCCESS; } return EXIT_FAILURE; } #ifdef HAVE_PTHREAD /* Send an X event to display the next image at every delay set to slideshow_delay */ void *slideshow(void *arg) { (void) arg; XKeyEvent event; event.display = dpy; event.window = win; event.root = DefaultRootWindow(dpy); event.subwindow = None; event.time = CurrentTime; event.x = 1; event.y = 1; event.x_root = 1; event.y_root = 1; event.same_screen = True; event.keycode = XKeysymToKeycode(dpy, XK_Right); event.state = 0; event.type = KeyPress; while (slideshow_flag) { int r; r = XSendEvent(event.display, event.window, True, KeyPressMask, (XEvent *)&event); if (!r) fprintf(stderr, "Error: can't send event\n"); XFlush(dpy); /* default sleep time between moving to next image */ sleep(slideshow_delay); } tid = 0; return arg; } #endif /* List and sort by name all files from a given directory Arg: the directory path that contains images, the linked list where to add the new file refs Returns: the first argument of the list or NULL on failure */ link_t *connect_dir(char *dirpath, linked_list_t *list) { struct dirent **dir; int dv, idx; char path[PATH_MAX] = ""; if (!dirpath) return NULL; dv = scandir(dirpath, &dir, 0, alphasort); if (dv < 0) { /* maybe it's a file */ struct stat stDirInfo; if (lstat(dirpath, &stDirInfo) == 0) linked_list_add(list, strdup(dirpath)); return list->first; } for (idx = 0; idx < dv; idx++) { struct stat stDirInfo; if (dirpath[strlen(dirpath)-1] == FILE_SEPARATOR) snprintf(path, PATH_MAX, "%s%s", dirpath, dir[idx]->d_name); else snprintf(path, PATH_MAX, "%s%c%s", dirpath, FILE_SEPARATOR, dir[idx]->d_name); free(dir[idx]); if ((lstat(path, &stDirInfo) == 0) && !S_ISDIR(stDirInfo.st_mode)) linked_list_add(list, strdup(path)); } free(dir); return list->first; } #ifdef USE_RANDR /* Retrieve the monitor resolution where the window is located */ void get_monitor_dimensions(Display *dpy, int screen_num, Window win, int *width, int *height) { int monitor_count, abs_x, abs_y; Window child; Window root_window = RootWindow(dpy, screen_num); XWindowAttributes attrs; XGetWindowAttributes(dpy, win, &attrs); XTranslateCoordinates(dpy, win, attrs.root, 0, 0, &abs_x, &abs_y, &child); XRRMonitorInfo *monitors = XRRGetMonitors(dpy, root_window, True, &monitor_count); if (monitors && monitor_count > 0) { for (int i = 0; i < monitor_count; i++) { if (abs_x >= monitors[i].x && abs_x < (monitors[i].x + monitors[i].width) && abs_y >= monitors[i].y && abs_y < (monitors[i].y + monitors[i].height)) { *width = monitors[i].width; *height = monitors[i].height; break; } } XRRFreeMonitors(monitors); } } #endif /* Transform the given URI to UTF-8 string */ void decode_uri(char *uri) { char *last = uri + strlen(uri); while (uri < last-2) { if (*uri == '%') { int h; if (sscanf(uri+1, "%2X", &h) != 1) break; *uri = h; memmove(uri+1, uri+3, last - (uri+2)); last -= 2; } uri++; } } /* Provide data for drag operations */ static void widget_get_data_callback(DndClass *dnd, Window window, unsigned char **data, int *length, Atom type) { /* Mark unused parameters to suppress compiler warnings */ (void)dnd; (void)window; (void)type; char *filename, *full_path, *uri_data; int uri_length = 0; static char *last_provided_data = NULL; if (!current_link || !current_link->data) { *data = NULL; *length = 0; return; } /* Get the full absolute path for the current file */ filename = (char *)current_link->data; full_path = realpath(filename, NULL); if (!full_path) { *data = NULL; *length = 0; return; } /* Create file:// URI for the current file */ uri_length = strlen("file://") + strlen(full_path) + 1; uri_data = malloc(uri_length); if (!uri_data) { free(full_path); *data = NULL; *length = 0; return; } snprintf(uri_data, uri_length, "file://%s", full_path); *data = (unsigned char *)uri_data; *length = strlen(uri_data); /* Only log if this is different data than last time */ if (WMIV_DEBUG && (!last_provided_data || strcmp(uri_data, last_provided_data) != 0)) { fprintf(stderr, "Providing drag data: %s\n", uri_data); if (last_provided_data) free(last_provided_data); last_provided_data = strdup(uri_data); } free(full_path); } /* Initialize DND for sending */ static void init_dnd_send(void) { if (!dnd_initialized) { Atom typelist[] = { XInternAtom(dpy, "text/uri-list", False), XInternAtom(dpy, "text/plain", False), None }; xdnd_init(&send_dnd, dpy); send_dnd.widget_get_data = widget_get_data_callback; dnd_initialized = True; xdnd_set_dnd_aware(&send_dnd, win, typelist); } } /* Initiate a file drag operation */ static void initiate_file_drag(void) { Atom action = XInternAtom(dpy, "XdndActionCopy", False); Atom typelist[] = { XInternAtom(dpy, "text/uri-list", False), None }; if (!current_link || !current_link->data) { return; } init_dnd_send(); xdnd_drag(&send_dnd, win, action, typelist); } /* main */ int main(int argc, char **argv) { int option = -1; RContextAttributes attr = {}; XEvent e; KeySym keysym; char *reading_filename = ""; int screen_num, file_i; int quit = 0; XClassHint *class_hints; XSizeHints *size_hints; XWMHints *win_hints; Atom delWindow; Atom xdnd_aware; Atom xdnd_version = XDND_VERSION; #ifdef USE_XPM Pixmap icon_pixmap, icon_shape; #endif class_hints = XAllocClassHint(); if (!class_hints) { fprintf(stderr, "Error: failed to allocate memory\n"); return EXIT_FAILURE; } class_hints->res_name = (char *)APPNAME; class_hints->res_class = "wmiv"; /* Init colors */ lightGray.red = lightGray.green = lightGray.blue = 211; darkGray.red = darkGray.green = darkGray.blue = 169; lightGray.alpha = darkGray.alpha = 1; black.red = black.green = black.blue = 0; red.red = 255; red.green = red.blue = 0; static struct option long_options[] = { {"help", no_argument, 0, 'h'}, {"ignore-unknown", no_argument, 0, 'i'}, {"version", no_argument, 0, 'v'}, {0, 0, 0, 0} }; int option_index = 0; option = getopt_long (argc, argv, "hiv", long_options, &option_index); if (option != -1) { switch (option) { case 'h': printf("Usage: %s [image(s)|directory|archive]\n" "Options:\n" " -h, --help display this help and exit\n" " -i, --ignore-unknown ignore unknown image format\n" " -v, --version print version\n" "\nKeys:\n\n" " [+] zoom in\n" " [-] zoom out\n" " [▸] next image\n" " [◂] previous image\n" " [▴] first image\n" " [▾] last image\n" " [Ctrl+C] copy image to clipboard\n" #ifdef HAVE_PTHREAD " [D] start slideshow\n" #endif " [Esc] actual size\n" " [F] toggle full-screen mode\n" " [L] rotate image on the left\n" " [Q] quit\n" " [R] rotate image on the right\n", argv[0]); return EXIT_SUCCESS; case 'v': printf("%s version %s\n", APPNAME, VERSION); return EXIT_SUCCESS; case 'i': ignore_unknown_file_format = True; break; case '?': return EXIT_FAILURE; } } linked_list_init(&list); #ifdef HAVE_LIBARCHIVE /* Register cleanup function for temporary directory */ atexit(cleanup_temp_dir); #endif dpy = XOpenDisplay(NULL); if (!dpy) { fprintf(stderr, "Error: can't open display\n"); linked_list_free(&list); return EXIT_FAILURE; } /* Initialize clipboard atoms */ clipboard_atom = XInternAtom(dpy, "CLIPBOARD", False); targets_atom = XInternAtom(dpy, "TARGETS", False); png_atom = XInternAtom(dpy, "image/png", False); jpeg_atom = XInternAtom(dpy, "image/jpeg", False); property_atom = XInternAtom(dpy, "WMIV_CLIPBOARD_PROP", False); incr_atom = XInternAtom(dpy, "INCR", False); screen_num = DefaultScreen(dpy); max_width = DisplayWidth(dpy, screen_num); max_height = DisplayHeight(dpy, screen_num); attr.flags = RC_RenderMode | RC_ColorsPerChannel; attr.render_mode = RDitheredRendering; attr.colors_per_channel = 4; ctx = RCreateContext(dpy, screen_num, &attr); if (argc < 2) { argv[1] = "."; argc = 2; } for (file_i = 1; file_i < argc; file_i++) { /* Check if this is an archive file */ if (is_archive_file(argv[file_i])) { char *extracted_dir = extract_archive(argv[file_i]); if (extracted_dir) { current_link = connect_dir(extracted_dir, &list); free(extracted_dir); if (current_link) { reading_filename = (char *)current_link->data; max_index = list.count; } } } else { current_link = connect_dir(argv[file_i], &list); if (current_link) { reading_filename = (char *)current_link->data; max_index = list.count; } } } img = load_oriented_image(ctx, reading_filename, 0); if (!img) { if (strlen(reading_filename) == 0) fprintf(stderr, "Error: %s\n", RMessageForError(RErrorCode)); else fprintf(stderr, "Error: %s %s\n", reading_filename, RMessageForError(RErrorCode)); img = draw_failed_image(); if (!current_link) return EXIT_FAILURE; if (ignore_unknown_file_format) { if (WMIV_DEBUG) fprintf(stderr, "Skipping file...\n"); if (change_image(NEXT) != EXIT_SUCCESS || !img) return EXIT_FAILURE; if (current_link) reading_filename = (char *)current_link->data; } } merge_with_background(img); win = XCreateSimpleWindow(dpy, DefaultRootWindow(dpy), 0, 0, 10, 10, 0, 0, BlackPixel(dpy, screen_num)); XSelectInput(dpy, win, KeyPressMask|StructureNotifyMask|ExposureMask|ButtonPressMask|ButtonReleaseMask|PointerMotionMask|FocusChangeMask|PropertyChangeMask); size_hints = XAllocSizeHints(); if (!size_hints) { fprintf(stderr, "Error: failed to allocate memory\n"); return EXIT_FAILURE; } size_hints->width = img->width; size_hints->height = img->height; delWindow = XInternAtom(dpy, "WM_DELETE_WINDOW", False); XSetWMProtocols(dpy, win, &delWindow, 1); change_title(&title_property, reading_filename); xdnd_aware = XInternAtom(dpy, "XdndAware", False); XChangeProperty(dpy, win, xdnd_aware, XA_ATOM, 32, PropModeReplace, (unsigned char *) &xdnd_version, 1); win_hints = XAllocWMHints(); if (win_hints) { win_hints->flags = StateHint|InputHint|WindowGroupHint; #ifdef USE_XPM if ((XpmCreatePixmapFromData(dpy, win, wmiv_xpm, &icon_pixmap, &icon_shape, NULL)) == 0) { win_hints->flags |= IconPixmapHint|IconMaskHint; win_hints->icon_pixmap = icon_pixmap; win_hints->icon_mask = icon_shape; Atom net_wm_icon = XInternAtom(dpy, "_NET_WM_ICON", False); if (net_wm_icon != None) { int icon_width, icon_height; if (sscanf(wmiv_xpm[0], "%d %d", &icon_width, &icon_height) == 2) { /* Convert XPM pixmap to ARGB32 format */ XImage *icon_image = XGetImage(dpy, icon_pixmap, 0, 0, icon_width, icon_height, AllPlanes, ZPixmap); if (icon_image) { XImage *mask_image = NULL; if (icon_shape != None) { mask_image = XGetImage(dpy, icon_shape, 0, 0, icon_width, icon_height, AllPlanes, ZPixmap); } /* Create ARGB32 data: width, height, followed by ARGB pixel data */ unsigned long *icon_data = malloc(sizeof(unsigned long) * (2 + icon_width * icon_height)); if (icon_data) { icon_data[0] = icon_width; icon_data[1] = icon_height; for (int y = 0; y < icon_height; y++) { for (int x = 0; x < icon_width; x++) { unsigned long pixel = XGetPixel(icon_image, x, y); unsigned long alpha = 0xFF000000; /* default opaque */ /* Check mask for transparency */ if (mask_image) { unsigned long mask_pixel = XGetPixel(mask_image, x, y); if (mask_pixel == 0) { alpha = 0x00000000; /* transparent */ } } icon_data[2 + y * icon_width + x] = alpha | (pixel & 0x00FFFFFF); } } XChangeProperty(dpy, win, net_wm_icon, XA_CARDINAL, 32, PropModeReplace, (unsigned char *)icon_data, 2 + icon_width * icon_height); free(icon_data); } XDestroyImage(icon_image); if (mask_image) { XDestroyImage(mask_image); } } } } } #endif win_hints->initial_state = NormalState; win_hints->input = True; win_hints->window_group = win; XStringListToTextProperty((char **)&APPNAME, 1, &icon_property); XSetWMProperties(dpy, win, NULL, &icon_property, argv, argc, size_hints, win_hints, class_hints); if (icon_property.value) XFree(icon_property.value); XFree(win_hints); XFree(class_hints); XFree(size_hints); } XMapWindow(dpy, win); XFlush(dpy); if (wait_for_frame_extents(dpy, win, 1000)) { /* Wait up to 1 second */ if (!get_window_decor_size(dpy, win, &extents) && WMIV_DEBUG) fprintf(stderr, "Warning: Property found but could not parse frame extents\n"); } else { if (WMIV_DEBUG) fprintf(stderr, "Warning: Window manager did not set _NET_FRAME_EXTENTS property within timeout\n"); } #ifdef USE_RANDR get_monitor_dimensions(dpy, screen_num, win, &max_width, &max_height); #endif if (WMIV_DEBUG) fprintf(stderr, "Display size: %dx%d\n", max_width, max_height); rescale_image(); XCopyArea(dpy, pix, win, ctx->copy_gc, 0, 0, img->width, img->height, 0, 0); XResizeWindow(dpy, win, img->width, img->height); XSync(dpy, True); /* Main event loop */ while (!quit) { XNextEvent(dpy, &e); if (e.type == ClientMessage) { unsigned char *dropBuffer; Atom typelist[] = { XInternAtom(dpy, "text/uri-list", False), XInternAtom(dpy, "text/plain", False), None }; Atom actionlist[] = { XInternAtom(dpy, "XdndActionCopy", False), XInternAtom(dpy, "XdndActionMove", False), None }; int length; Atom type; int x, y; if (xdnd_get_drop(dpy, &e, typelist, actionlist, &dropBuffer, &length, &type, &x, &y)) { char *handled_uri_header = "file://"; char *filename_separator = "\n"; char *ptr = strtok((char *)dropBuffer, filename_separator); if (WMIV_DEBUG) fprintf(stderr, "Dropped data: %s\n", (char *)dropBuffer); linked_list_free(&list); linked_list_init(&list); Bool find_one_file = False; while (ptr != NULL) { if (strstr(ptr, handled_uri_header) == ptr) { char *tmp_file; if (ptr[strlen(ptr) - 1] == '\r') ptr[strlen(ptr) - 1] = '\0'; tmp_file = ptr + strlen(handled_uri_header); /* drag-and-drop file name format as per the spec is encoded as an URI */ decode_uri(tmp_file); if (connect_dir(tmp_file, &list)) find_one_file = True; } ptr = strtok(NULL, filename_separator); } free(dropBuffer); if (find_one_file) { max_index = list.count; current_link = list.last; change_image(NEXT); } else { if (WMIV_DEBUG) fprintf(stderr, "Error: no valid file found in dropped data\n"); XClearArea(dpy, win, 0, 0, 0, 0, True); } } if (e.xclient.data.l[0] == delWindow) quit = 1; continue; } if (e.type == SelectionRequest) { /* Serve clipboard requests */ handle_selection_request(&e.xselectionrequest); continue; } if (e.type == SelectionClear) { /* we lost ownership */ handle_selection_clear(); continue; } if (e.type == PropertyNotify) { XPropertyEvent *pe = &e.xproperty; if (incr.active && (pe->window == incr.requestor) && (pe->atom == incr.property) && (pe->state == PropertyDelete)) { if (WMIV_DEBUG) fprintf(stderr, "INCR: property delete observed, sending next chunk\n"); /* Check if we've already sent all data */ if (incr.offset >= incr.total_size) { /* Send zero-length termination to signal end of transfer */ XChangeProperty(dpy, incr.requestor, incr.property, incr.target, 8, PropModeReplace, NULL, 0); XFlush(dpy); /* Remove PropertyChangeMask from requestor window to clean up */ XSelectInput(dpy, incr.requestor, NoEventMask); incr.active = False; if (WMIV_DEBUG) fprintf(stderr, "INCR: transfer complete\n"); } else { /* Send next data chunk */ send_next_incr_chunk(); } } else if (pe->window == win) { if (WMIV_DEBUG && incr.active) { fprintf(stderr, "Ignoring PropertyNotify on main window during INCR\n"); } } continue; } if (e.type == FocusIn) { focus = True; continue; } if (e.type == FocusOut) { focus = False; continue; } if (!fullscreen_flag && (e.type == Expose)) { XExposeEvent xev = e.xexpose; if (xev.count == 0) XCopyArea(dpy, pix, win, ctx->copy_gc, 0, 0, img->width, img->height, 0, 0); continue; } if (!fullscreen_flag && e.type == ConfigureNotify) { XConfigureEvent xce = e.xconfigure; /* there is no file loaded and the window is resized */ if (!current_link) { XResizeWindow(dpy, win, img->width, img->height); continue; } if (xce.width != img->width || xce.height != img->height) { RImage *old_img = img; img = load_oriented_image(ctx, current_link->data, 0); if (!img) { /* keep the old img and window size */ img = old_img; XResizeWindow(dpy, win, img->width, img->height); } else { RImage *tmp2; if (!back_from_fullscreen) { /* manually resized window */ tmp2 = RScaleImage(img, xce.width, xce.height); } else { /* back from fullscreen mode, maybe img was rotated */ tmp2 = img; back_from_fullscreen = False; XClearWindow(dpy, win); } merge_with_background(tmp2); if (RConvertImage(ctx, tmp2, &pix)) { RReleaseImage(old_img); img = RCloneImage(tmp2); RReleaseImage(tmp2); change_title(&title_property, (char *)current_link->data); XSync(dpy, True); rescale_image(); XResizeWindow(dpy, win, img->width, img->height); XCopyArea(dpy, pix, win, ctx->copy_gc, 0, 0, img->width, img->height, 0, 0); } } } continue; } if (fullscreen_flag && e.type == ConfigureNotify) { int new_width = e.xconfigure.width; int new_height = e.xconfigure.height; if (new_width != img->width || new_height != img->height) maximize_image(); continue; } if (e.type == ButtonPress) { switch (e.xbutton.button) { case Button1: { /* Store button press for potential drag operation */ button1_pressed = True; drag_start_x = e.xbutton.x; drag_start_y = e.xbutton.y; } break; case Button4: zoom_in(); break; case Button5: zoom_out(); break; case 8: change_image(PREV); break; case 9: change_image(NEXT); break; } continue; } if (e.type == ButtonRelease) { if (e.xbutton.button == Button1 && button1_pressed) { /* Button released without dragging - treat as click for image navigation */ button1_pressed = False; if (focus) { if (img && (drag_start_x > img->width/2)) change_image(NEXT); else change_image(PREV); } } continue; } if (e.type == MotionNotify && button1_pressed) { /* Check if we've moved enough to start a drag */ int dx = e.xmotion.x - drag_start_x; int dy = e.xmotion.y - drag_start_y; if (abs(dx) > drag_threshold || abs(dy) > drag_threshold) { /* Start drag operation */ button1_pressed = False; if (current_link && current_link->data) { initiate_file_drag(); } } continue; } if (e.type == KeyPress) { keysym = W_KeycodeToKeysym(dpy, e.xkey.keycode, e.xkey.state & ShiftMask?1:0); #ifdef HAVE_PTHREAD if (keysym != XK_Right) slideshow_flag = False; #endif /* Detect Ctrl+C: state contains ControlMask */ if ((e.xkey.state & ControlMask) && (keysym == XK_c || keysym == XK_C)) { copy_current_image_to_clipboard(); continue; } switch (keysym) { case XK_Right: change_image(NEXT); break; case XK_Left: change_image(PREV); break; case XK_Up: if (current_link) { current_link = list.last; change_image(NEXT); } break; case XK_Down: if (current_link) { current_link = list.first; change_image(PREV); } break; #ifdef HAVE_PTHREAD case XK_F5: case XK_d: if (!tid) { if (current_link && !slideshow_flag) { slideshow_flag = True; pthread_create(&tid, NULL, &slideshow, NULL); } else { fprintf(stderr, "Error: can't start slideshow\n"); } } break; #endif case XK_q: quit = 1; break; case XK_Escape: if (!fullscreen_flag) { zoom_factor = -0.2f; zoom_in(); } else { full_screen(); } break; case XK_plus: zoom_in(); break; case XK_minus: zoom_out(); break; case XK_F11: case XK_f: full_screen(); break; case XK_r: rotate_image_right(); break; case XK_l: rotate_image_left(); break; } } } if (img) RReleaseImage(img); if (pix) XFreePixmap(dpy, pix); #ifdef USE_XPM if (icon_pixmap) XFreePixmap(dpy, icon_pixmap); if (icon_shape) XFreePixmap(dpy, icon_shape); #endif linked_list_free(&list); RDestroyContext(ctx); RShutdown(); free_clipboard_data(); cleanup_temp_dir(); XCloseDisplay(dpy); return EXIT_SUCCESS; }