diff --git a/configure.ac b/configure.ac index ad67fc91..86ac2a00 100644 --- a/configure.ac +++ b/configure.ac @@ -857,6 +857,22 @@ AM_CONDITIONAL([ICON_EXT_XPM], [test "x$ICONEXT" = "xxpm"]) AM_CONDITIONAL([ICON_EXT_TIFF], [test "x$ICONEXT" = "xtiff"]) +dnl Archive Support for wmiv +dnl ================================= +dnl Check for libarchive (comprehensive archive support) +AC_CHECK_LIB([archive], [archive_read_new], [ + AC_CHECK_HEADER([archive.h], [ + AC_DEFINE([HAVE_LIBARCHIVE], [1], [Define if libarchive is available]) + LIBARCHIVE_LIBS="-larchive" + ], [ + AC_MSG_WARN([libarchive header not found, archive support disabled]) + ]) +], [ + AC_MSG_WARN([libarchive not found, archive support disabled]) +]) +AC_SUBST([LIBARCHIVE_LIBS]) + + dnl ============================================== dnl End of Graphic Format Libraries dnl ============================================== diff --git a/doc/wmiv.1 b/doc/wmiv.1 index 924c317f..0c66e90e 100644 --- a/doc/wmiv.1 +++ b/doc/wmiv.1 @@ -3,14 +3,17 @@ wmiv \- quick image viewer using wrlib .SH SYNOPSIS .B wmiv -.RI [ \,image(s)\/ | \,directory\/ ] +.RI [ \,image(s)\/ | \,directory\/ | \,archive\/] .SH OPTIONS .TP -.B \-\-help +.B \-h, \-\-help print help text .TP -.B \-\-version +.B \-v, \-\-version print version +.TP +.B \-i, \-\-ignore-unknown +ignore unknown image format .SH KEYS .TP [+] @@ -19,21 +22,6 @@ zoom in [\-] zoom out .TP -[Esc] -actual size -.TP -[D] -launch diaporama mode -.TP -[L] -rotate image on the left -.TP -[Q] -quit -.TP -[R] -rotate image on the right -.TP [▸] next image .TP @@ -45,6 +33,27 @@ first image .TP [▾] last image +.TP +[Ctrl+C] +copy image to clipboard +.TP +[D] +start slideshow +.TP +[Esc] +actual size +.TP +[F] +toggle full-screen mode +.TP +[L] +rotate image on the left +.TP +[Q] +quit +.TP +[R] +rotate image on the right .SH AUTHOR .B wmiv is a part of Window Maker. It was written by David Maciejak. diff --git a/util/Makefile.am b/util/Makefile.am index 0c148702..ef9e586e 100644 --- a/util/Makefile.am +++ b/util/Makefile.am @@ -76,9 +76,10 @@ wmiv_LDADD = \ $(top_builddir)/wrlib/libwraster.la \ $(top_builddir)/WINGs/libWINGs.la \ @XLFLAGS@ @XLIBS@ @GFXLIBS@ \ - @PANGO_LIBS@ @PTHREAD_LIBS@ @LIBEXIF@ + @PANGO_LIBS@ @PTHREAD_LIBS@ @LIBEXIF@ \ + @LIBARCHIVE_LIBS@ @LIBM@ @LIBXRANDR@ -wmiv_SOURCES = wmiv.c wmiv.h +wmiv_SOURCES = wmiv.c wmiv.hi xdnd.c xdnd.h CLEANFILES = wmaker.inst diff --git a/util/wmiv.c b/util/wmiv.c index 0d33a535..47290501 100755 --- a/util/wmiv.c +++ b/util/wmiv.c @@ -1,7 +1,7 @@ /* * Window Maker window manager * - * Copyright (c) 2014-2023 Window Maker Team - David Maciejak + * 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 @@ -33,9 +33,13 @@ #include #include #include +#include #include #include +#include +#include #include "config.h" +#include "xdnd.h" #ifdef HAVE_EXIF #include @@ -45,23 +49,45 @@ #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 eog project - git.gnome.org/browse/eog -*/ +/* 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; @@ -69,13 +95,18 @@ 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 = False; +Bool focus = True; Bool back_from_fullscreen = False; +Bool ignore_unknown_file_format = False; #ifdef HAVE_PTHREAD -Bool diaporama_flag = False; -int diaporama_delay = 5; +Bool slideshow_flag = False; +int slideshow_delay = 5; pthread_t tid = 0; #endif XTextProperty title_property; @@ -88,6 +119,16 @@ 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; @@ -104,11 +145,755 @@ typedef struct linked_list { 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 */ /* - load_oriented_image: used to load an image and optionally - get its orientation if libexif is available - return the image on success, NULL on failure + 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) { @@ -189,16 +974,22 @@ RImage *load_oriented_image(RContext *context, const char *file, int index) } /* - change_title: used to change window title - return EXIT_SUCCESS on success, 1 on failure + Change window title + Returns EXIT_SUCCESS on success, EXIT_FAILURE on failure */ int change_title(XTextProperty *prop, char *filename) { char *combined_title = NULL; - 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)) + if (ignore_unknown_file_format) { + if (!asprintf(&combined_title, "%s - %s", APPNAME, filename)) return EXIT_FAILURE; - XStringListToTextProperty(&combined_title, 1, prop); + } + 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); @@ -207,38 +998,45 @@ int change_title(XTextProperty *prop, char *filename) } /* - rescale_image: used to rescale the current image based on the screen size - return EXIT_SUCCESS on 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 < final_width) || (max_height < final_height)) { - long val = 0; + if ((max_width_tmp < final_width) || (max_height_tmp < final_height)) { + double val = 0; if (final_width > final_height) { - val = final_height * max_width / final_width; - final_width = final_width * val / final_height; - final_height = val; - if (val > max_height) { - val = final_width * max_height / final_height; - final_height = final_height * val / final_width; - final_width = val; + 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 / final_height; - final_height = final_height * val / final_width; - final_width = val; - if (val > max_width) { - val = final_height * max_width / final_width; - final_width = final_width * val / final_height; - final_height = val; + 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); } } } @@ -252,15 +1050,15 @@ int rescale_image(void) RReleaseImage(old_img); } if (!RConvertImage(ctx, img, &pix)) { - fprintf(stderr, "%s\n", RMessageForError(RErrorCode)); + fprintf(stderr, "Error: %s\n", RMessageForError(RErrorCode)); return EXIT_FAILURE; } return EXIT_SUCCESS; } /* - maximize_image: find the best image size for the current display - return EXIT_SUCCESS on success + Find the best image size for the current display + Returns EXIT_SUCCESS on success */ int maximize_image(void) { @@ -271,8 +1069,8 @@ int maximize_image(void) } /* - merge_with_background: merge the current image with with a checkboard background - return EXIT_SUCCESS on success, 1 on failure + Merge the current image with a checkerboard background + Returns EXIT_SUCCESS on success, EXIT_FAILURE on failure */ int merge_with_background(RImage *i) { @@ -302,10 +1100,10 @@ int merge_with_background(RImage *i) } /* - turn_image: rotate the image by the angle passed - return EXIT_SUCCESS on success, EXIT_FAILURE on failure + Rotate the image by the angle passed + Returns EXIT_SUCCESS on success, EXIT_FAILURE on failure */ -int turn_image(float angle) +int rotate_image(float angle) { RImage *tmp; @@ -337,27 +1135,26 @@ int turn_image(float angle) } /* - turn_image_right: rotate the image by 90 degree - return EXIT_SUCCESS on success, EXIT_FAILURE on failure + Rotate the image by 90 degrees to the right + Returns EXIT_SUCCESS on success, EXIT_FAILURE on failure */ -int turn_image_right(void) +int rotate_image_right(void) { - return turn_image(90.0); + return rotate_image(90.0); } /* - turn_image_left: rotate the image by -90 degree - return EXIT_SUCCESS on success, 1 on failure + Rotate the image by -90 degrees to the left + Returns EXIT_SUCCESS on success, EXIT_FAILURE on failure */ -int turn_image_left(void) +int rotate_image_left(void) { - return turn_image(-90.0); + return rotate_image(-90.0); } /* - draw_failed_image: create a red crossed image to indicate an error loading file - return the image on success, NULL on failure - + Create a red crossed image to indicate an error loading file + Returns the image on success, NULL on failure */ RImage *draw_failed_image(void) { @@ -372,15 +1169,43 @@ RImage *draw_failed_image(void) return NULL; RFillImage(failed_image, &black); - ROperateLine(failed_image, RAddOperation, 0, 0, failed_image->width, failed_image->height, &red); - ROperateLine(failed_image, RAddOperation, 0, failed_image->height, failed_image->width, 0, &red); + + /* 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; } /* - full_screen: sending event to the window manager to switch from/to full screen mode - return EXIT_SUCCESS on success, 1 on failure + 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) { @@ -397,6 +1222,12 @@ int full_screen(void) } 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)); @@ -416,9 +1247,98 @@ int full_screen(void) } /* - zoom_in_out: apply a zoom factor on the current image - arg: 1 to zoom in, 0 to zoom out - return EXIT_SUCCESS on success, 1 on failure + 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) { @@ -458,7 +1378,7 @@ int zoom_in_out(int z) merge_with_background(img); if (!RConvertImage(ctx, img, &pix)) { - fprintf(stderr, "%s\n", RMessageForError(RErrorCode)); + fprintf(stderr, "Error: %s\n", RMessageForError(RErrorCode)); return EXIT_FAILURE; } XResizeWindow(dpy, win, img->width, img->height); @@ -466,8 +1386,8 @@ int zoom_in_out(int z) } /* - zoom_in: transitional fct used to call zoom_in_out with zoom in flag - return EXIT_SUCCESS on success, 1 on failure + 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) { @@ -475,8 +1395,8 @@ int zoom_in(void) } /* - zoom_out: transitional fct used to call zoom_in_out with zoom out flag - return EXIT_SUCCESS on success, 1 on failure + 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) { @@ -484,12 +1404,15 @@ int zoom_out(void) } /* - change_image: load previous or next image - arg: way which could be PREV or NEXT constant - return EXIT_SUCCESS on success, 1 on failure + 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; @@ -513,27 +1436,49 @@ int change_image(int way) } } if (WMIV_DEBUG) - fprintf(stderr, "current file is> %s\n", (char *)current_link->data); + fprintf(stderr, "Current file is> %s\n", (char *)current_link->data); img = load_oriented_image(ctx, current_link->data, 0); - if (!img) { - fprintf(stderr, "Error: %s %s\n", (char *)current_link->data, - RMessageForError(RErrorCode)); + 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 (!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); + 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; } @@ -542,11 +1487,9 @@ int change_image(int way) #ifdef HAVE_PTHREAD /* - diaporama: send a xevent to display the next image at every delay set to diaporama_delay - arg: not used - return void + Send an X event to display the next image at every delay set to slideshow_delay */ -void *diaporama(void *arg) +void *slideshow(void *arg) { (void) arg; @@ -565,14 +1508,14 @@ void *diaporama(void *arg) event.state = 0; event.type = KeyPress; - while (diaporama_flag) { + while (slideshow_flag) { int r; r = XSendEvent(event.display, event.window, True, KeyPressMask, (XEvent *)&event); if (!r) - fprintf(stderr, "Error sending event\n"); + fprintf(stderr, "Error: can't send event\n"); XFlush(dpy); /* default sleep time between moving to next image */ - sleep(diaporama_delay); + sleep(slideshow_delay); } tid = 0; return arg; @@ -580,64 +1523,11 @@ void *diaporama(void *arg) #endif /* - linked_list_init: init the linked list + 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 */ -void linked_list_init(linked_list_t *list) -{ - list->first = list->last = 0; - list->count = 0; -} - -/* - linked_list_add: add an element to the linked list - return EXIT_SUCCESS on success, 1 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: memory allocation failed\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; -} - -/* - linked_list_free: 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); - } -} - -/* - connect_dir: 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 - return: the first argument of the list or NULL on failure -*/ -link_t *connect_dir(char *dirpath, linked_list_t *li) +link_t *connect_dir(char *dirpath, linked_list_t *list) { struct dirent **dir; int dv, idx; @@ -650,12 +1540,9 @@ link_t *connect_dir(char *dirpath, linked_list_t *li) if (dv < 0) { /* maybe it's a file */ struct stat stDirInfo; - if (lstat(dirpath, &stDirInfo) == 0) { - linked_list_add(li, strdup(dirpath)); - return li->first; - } else { - return NULL; - } + if (lstat(dirpath, &stDirInfo) == 0) + linked_list_add(list, strdup(dirpath)); + return list->first; } for (idx = 0; idx < dv; idx++) { struct stat stDirInfo; @@ -666,10 +1553,147 @@ link_t *connect_dir(char *dirpath, linked_list_t *li) free(dir[idx]); if ((lstat(path, &stDirInfo) == 0) && !S_ISDIR(stDirInfo.st_mode)) - linked_list_add(li, strdup(path)); + linked_list_add(list, strdup(path)); } free(dir); - return li->first; + 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); } /* @@ -682,23 +1706,28 @@ int main(int argc, char **argv) XEvent e; KeySym keysym; char *reading_filename = ""; - int screen, file_i; + 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: failure allocating memory\n"); + fprintf(stderr, "Error: failed to allocate memory\n"); return EXIT_FAILURE; } class_hints->res_name = (char *)APPNAME; - class_hints->res_class = "default"; + class_hints->res_class = "wmiv"; - /* init colors */ + /* Init colors */ lightGray.red = lightGray.green = lightGray.blue = 211; darkGray.red = darkGray.green = darkGray.blue = 169; lightGray.alpha = darkGray.alpha = 1; @@ -707,39 +1736,46 @@ int main(int argc, char **argv) red.green = red.blue = 0; static struct option long_options[] = { - {"version", no_argument, 0, 'v'}, - {"help", no_argument, 0, 'h'}, - {0, 0, 0, 0} - }; + {"version", no_argument, 0, 'v'}, + {"help", no_argument, 0, 'h'}, + {"ignore-unknown", no_argument, 0, 'i'}, + {0, 0, 0, 0} + }; int option_index = 0; - option = getopt_long (argc, argv, "hv", long_options, &option_index); + option = getopt_long (argc, argv, "hiv", long_options, &option_index); if (option != -1) { switch (option) { case 'h': - printf("Usage: %s [image(s)|directory]\n" + printf("Usage: %s [image(s)|directory|archive]\n" "Options:\n" - " -h, --help print this help text\n" - " -v, --version print version\n" - "Keys:\n" + " -h, --help print this help text\n" + " -v, --version print version\n" + " -i, --ignore-unknown ignore unknown image format\n" + "\nKeys:\n\n" " [+] zoom in\n" " [-] zoom out\n" - " [Esc] actual size\n" -#ifdef HAVE_PTHREAD - " [D] launch diaporama mode\n" -#endif - " [L] rotate image on the left\n" - " [Q] quit\n" - " [R] rotate image on the right\n" " [▸] next image\n" " [◂] previous image\n" " [▴] first image\n" - " [▾] last 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; } @@ -747,6 +1783,10 @@ int main(int argc, char **argv) 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"); @@ -754,14 +1794,22 @@ int main(int argc, char **argv) return EXIT_FAILURE; } - screen = DefaultScreen(dpy); - max_width = DisplayWidth(dpy, screen); - max_height = DisplayHeight(dpy, screen); + /* 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, DefaultScreen(dpy), &attr); + ctx = RCreateContext(dpy, screen_num, &attr); if (argc < 2) { argv[1] = "."; @@ -769,55 +1817,122 @@ int main(int argc, char **argv) } for (file_i = 1; file_i < argc; file_i++) { - current_link = connect_dir(argv[file_i], &list); - if (current_link) { - reading_filename = (char *)current_link->data; - max_index = list.count; + /* 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) { - fprintf(stderr, "Error: %s %s\n", reading_filename, RMessageForError(RErrorCode)); + 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); - rescale_image(); - if (WMIV_DEBUG) - fprintf(stderr, "display size: %dx%d\n", max_width, max_height); - - win = XCreateSimpleWindow(dpy, DefaultRootWindow(dpy), 0, 0, - img->width, img->height, 0, 0, BlackPixel(dpy, screen)); - XSelectInput(dpy, win, KeyPressMask|StructureNotifyMask|ExposureMask|ButtonPressMask|FocusChangeMask); + win = XCreateSimpleWindow(dpy, DefaultRootWindow(dpy), 0, 0, 1, 1, 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: failure allocating memory\n"); + fprintf(stderr, "Error: failed to allocate memory\n"); return EXIT_FAILURE; } size_hints->width = img->width; size_hints->height = img->height; - Atom delWindow = XInternAtom(dpy, "WM_DELETE_WINDOW", 0); + 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|IconPositionHint; + win_hints->flags |= IconPixmapHint|IconMaskHint; win_hints->icon_pixmap = icon_pixmap; win_hints->icon_mask = icon_shape; - win_hints->icon_x = 0; - win_hints->icon_y = 0; + + 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; @@ -830,25 +1945,131 @@ int main(int argc, char **argv) XFree(win_hints); XFree(class_hints); XFree(size_hints); - } XMapWindow(dpy, win); XFlush(dpy); - XCopyArea(dpy, pix, win, ctx->copy_gc, 0, 0, img->width, img->height, 0, 0); + 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)) { - /* - * This break could be related to all ClientMessages or - * related to delWindow. Before the patch about this comment - * the break was indented with one tab more (at the same level - * than "quit = 1;" in the previous line. - */ - break; + 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; @@ -866,6 +2087,13 @@ int main(int argc, char **argv) } 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); @@ -875,9 +2103,10 @@ int main(int argc, char **argv) XResizeWindow(dpy, win, img->width, img->height); } else { RImage *tmp2; - if (!back_from_fullscreen) + 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; @@ -891,9 +2120,10 @@ int main(int argc, char **argv) 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); + img->width, img->height, 0, 0); } } @@ -901,20 +2131,21 @@ int main(int argc, char **argv) continue; } if (fullscreen_flag && e.type == ConfigureNotify) { - maximize_image(); + 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: { - if (focus) { - if (img && (e.xbutton.x > img->width/2)) - change_image(NEXT); - else - change_image(PREV); - } - } - break; + /* 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; @@ -930,12 +2161,44 @@ int main(int argc, char **argv) } 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) - diaporama_flag = False; + 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); @@ -945,25 +2208,25 @@ int main(int argc, char **argv) break; case XK_Up: if (current_link) { - current_link = list.last; - change_image(NEXT); + current_link = list.last; + change_image(NEXT); } break; case XK_Down: if (current_link) { - current_link = list.first; - change_image(PREV); + current_link = list.first; + change_image(PREV); } break; #ifdef HAVE_PTHREAD case XK_F5: case XK_d: if (!tid) { - if (current_link && !diaporama_flag) { - diaporama_flag = True; - pthread_create(&tid, NULL, &diaporama, NULL); + if (current_link && !slideshow_flag) { + slideshow_flag = True; + pthread_create(&tid, NULL, &slideshow, NULL); } else { - fprintf(stderr, "Can't use diaporama mode\n"); + fprintf(stderr, "Error: can't start slideshow\n"); } } break; @@ -974,10 +2237,8 @@ int main(int argc, char **argv) case XK_Escape: if (!fullscreen_flag) { zoom_factor = -0.2f; - /* zoom_in will increase the zoom factor by 0.2 */ zoom_in(); } else { - /* we are in fullscreen mode already, want to return to normal size */ full_screen(); } break; @@ -992,10 +2253,10 @@ int main(int argc, char **argv) full_screen(); break; case XK_r: - turn_image_right(); + rotate_image_right(); break; case XK_l: - turn_image_left(); + rotate_image_left(); break; } @@ -1015,6 +2276,10 @@ int main(int argc, char **argv) linked_list_free(&list); RDestroyContext(ctx); RShutdown(); + + free_clipboard_data(); + cleanup_temp_dir(); + XCloseDisplay(dpy); return EXIT_SUCCESS; } diff --git a/util/xdnd.c b/util/xdnd.c new file mode 100644 index 00000000..330db063 --- /dev/null +++ b/util/xdnd.c @@ -0,0 +1,1596 @@ +/* xdnd.c, xdnd.h - C program library for handling the Xdnd protocol + Copyright (C) 1996-2000 Paul Sheer + + 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., 59 Temple Place, Suite 330, Boston, MA + 02111-1307, USA. + */ + + +/* + Released 1998-08-07 + Changes: + + 2000-08-08: INCR protocol implemented. + +*/ + +/* + DONE: + - INCR protocol now implemented + + TODO: + - action_choose_dialog not yet supported (never called) + - widget_delete_selection not yet supported and DELETE requests are ignored + - not yet tested with applications that only supported XDND 0 or 1 +*/ + +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef HAVE_SYS_SELECT_H +# include +#endif + +#include "xdnd.h" + +static void xdnd_send_enter (DndClass * dnd, Window window, Window from, Atom * typelist); +static void xdnd_send_position (DndClass * dnd, Window window, Window from, Atom action, int x, int y, + unsigned long etime); +static void xdnd_send_status (DndClass * dnd, Window window, Window from, int will_accept, int want_position, + int x, int y, int w, int h, Atom action); +static void xdnd_send_leave (DndClass * dnd, Window window, Window from); +static void xdnd_send_drop (DndClass * dnd, Window window, Window from, unsigned long etime); +static void xdnd_send_finished (DndClass * dnd, Window window, Window from, int error); +static int xdnd_convert_selection (DndClass * dnd, Window window, Window requester, Atom type); +static void xdnd_selection_send (DndClass * dnd, XSelectionRequestEvent * request, unsigned char *data, + int length); +static int xdnd_get_selection (DndClass * dnd, Window from, Atom property, Window insert); + + +/* just to remind us : */ + +#if 0 +typedef struct { + int type; + unsigned long serial; + Bool send_event; + Display *display; + Window window; + Atom message_type; + int format; + union { + char b[20]; + short s[10]; + long l[5]; + } data; +} XClientMessageEvent; +XClientMessageEvent xclient; +#endif + +/* #define DND_DEBUG */ + +#define xdnd_xfree(x) {if (x) { free (x); x = 0; }} + +#ifdef DND_DEBUG + +#include +#include + +char *xdnd_debug_milliseconds (void) +{ + struct timeval tv; + static char r[22]; + gettimeofday (&tv, 0); + sprintf (r, "%.2ld.%.3ld", tv.tv_sec % 100L, tv.tv_usec / 1000L); + return r; +} + +#define dnd_debug1(a) printf("%s: %d: %s: " a "\n", __FILE__, __LINE__, xdnd_debug_milliseconds ()) +#define dnd_debug2(a,b) printf("%s: %d: %s: " a "\n", __FILE__, __LINE__, xdnd_debug_milliseconds (), b) +#define dnd_debug3(a,b,c) printf("%s: %d: %s: " a "\n", __FILE__, __LINE__, xdnd_debug_milliseconds (), b, c) +#define dnd_debug4(a,b,c,d) printf("%s: %d: %s: " a "\n", __FILE__, __LINE__, xdnd_debug_milliseconds (), b, c, d) +#else +#define dnd_debug1(a) do {} while (0) +#define dnd_debug2(a,b) do {} while (0) +#define dnd_debug3(a,b,c) do {} while (0) +#define dnd_debug4(a,b,c,d) do {} while (0) +#endif + +#define dnd_warning(a) fprintf (stderr, a) + +#define dnd_version_at_least(a,b) ((a) >= (b)) + +static unsigned char dnd_copy_cursor_bits[] = +{ + 0x00, 0x00, 0x00, 0x00, 0xfe, 0xff, 0x0f, 0x00, 0x02, 0x00, 0x08, 0x01, + 0x02, 0x00, 0x08, 0x01, 0x02, 0x00, 0x08, 0x01, 0x02, 0x00, 0xe8, 0x0f, + 0x02, 0x00, 0x08, 0x01, 0x02, 0x00, 0x08, 0x01, 0x02, 0x00, 0x08, 0x01, + 0x02, 0x00, 0x08, 0x00, 0x02, 0x04, 0x08, 0x00, 0x02, 0x0c, 0x08, 0x00, + 0x02, 0x1c, 0x08, 0x00, 0x02, 0x3c, 0x08, 0x00, 0x02, 0x7c, 0x08, 0x00, + 0x02, 0xfc, 0x08, 0x00, 0x02, 0xfc, 0x09, 0x00, 0x02, 0xfc, 0x0b, 0x00, + 0x02, 0x7c, 0x08, 0x00, 0xfe, 0x6d, 0x0f, 0x00, 0x00, 0xc4, 0x00, 0x00, + 0x00, 0xc0, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, + 0x00, 0x00, 0x00, 0x00}; + +static unsigned char dnd_copy_mask_bits[] = +{ + 0xff, 0xff, 0x1f, 0x00, 0xff, 0xff, 0xff, 0x1f, 0xff, 0xff, 0xff, 0x1f, + 0x07, 0x00, 0xfc, 0x1f, 0x07, 0x00, 0xfc, 0x1f, 0x07, 0x00, 0xfc, 0x1f, + 0x07, 0x00, 0xfc, 0x1f, 0x07, 0x00, 0xfc, 0x1f, 0x07, 0x00, 0xfc, 0x1f, + 0x07, 0x06, 0xfc, 0x1f, 0x07, 0x0e, 0xfc, 0x1f, 0x07, 0x1e, 0x1c, 0x00, + 0x07, 0x3e, 0x1c, 0x00, 0x07, 0x7e, 0x1c, 0x00, 0x07, 0xfe, 0x1c, 0x00, + 0x07, 0xfe, 0x1d, 0x00, 0x07, 0xfe, 0x1f, 0x00, 0x07, 0xfe, 0x1f, 0x00, + 0xff, 0xff, 0x1f, 0x00, 0xff, 0xff, 0x1e, 0x00, 0xff, 0xef, 0x1f, 0x00, + 0x00, 0xe6, 0x01, 0x00, 0x00, 0xc0, 0x03, 0x00, 0x00, 0xc0, 0x03, 0x00, + 0x00, 0x80, 0x01, 0x00}; + +static unsigned char dnd_move_cursor_bits[] = +{ + 0x00, 0x00, 0x00, 0xfe, 0xff, 0x0f, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, + 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, + 0x02, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x04, 0x08, 0x02, 0x0c, 0x08, + 0x02, 0x1c, 0x08, 0x02, 0x3c, 0x08, 0x02, 0x7c, 0x08, 0x02, 0xfc, 0x08, + 0x02, 0xfc, 0x09, 0x02, 0xfc, 0x0b, 0x02, 0x7c, 0x08, 0xfe, 0x6d, 0x0f, + 0x00, 0xc4, 0x00, 0x00, 0xc0, 0x00, 0x00, 0x80, 0x01, 0x00, 0x80, 0x01, + 0x00, 0x00, 0x00}; + +static unsigned char dnd_move_mask_bits[] = +{ + 0xff, 0xff, 0x1f, 0xff, 0xff, 0x1f, 0xff, 0xff, 0x1f, 0x07, 0x00, 0x1c, + 0x07, 0x00, 0x1c, 0x07, 0x00, 0x1c, 0x07, 0x00, 0x1c, 0x07, 0x00, 0x1c, + 0x07, 0x00, 0x1c, 0x07, 0x06, 0x1c, 0x07, 0x0e, 0x1c, 0x07, 0x1e, 0x1c, + 0x07, 0x3e, 0x1c, 0x07, 0x7e, 0x1c, 0x07, 0xfe, 0x1c, 0x07, 0xfe, 0x1d, + 0x07, 0xfe, 0x1f, 0x07, 0xfe, 0x1f, 0xff, 0xff, 0x1f, 0xff, 0xff, 0x1e, + 0xff, 0xef, 0x1f, 0x00, 0xe6, 0x01, 0x00, 0xc0, 0x03, 0x00, 0xc0, 0x03, + 0x00, 0x80, 0x01}; + +static unsigned char dnd_link_cursor_bits[] = +{ + 0x00, 0x00, 0x00, 0x00, 0xfe, 0xff, 0x0f, 0x00, 0x02, 0x00, 0x08, 0x01, + 0x02, 0x00, 0x88, 0x00, 0x02, 0x00, 0x48, 0x00, 0x02, 0x00, 0xe8, 0x0f, + 0x02, 0x00, 0x48, 0x00, 0x02, 0x00, 0x88, 0x00, 0x02, 0x00, 0x08, 0x01, + 0x02, 0x00, 0x08, 0x00, 0x02, 0x04, 0x08, 0x00, 0x02, 0x0c, 0x08, 0x00, + 0x02, 0x1c, 0x08, 0x00, 0x02, 0x3c, 0x08, 0x00, 0x02, 0x7c, 0x08, 0x00, + 0x02, 0xfc, 0x08, 0x00, 0x02, 0xfc, 0x09, 0x00, 0x02, 0xfc, 0x0b, 0x00, + 0x02, 0x7c, 0x08, 0x00, 0xfe, 0x6d, 0x0f, 0x00, 0x00, 0xc4, 0x00, 0x00, + 0x00, 0xc0, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, + 0x00, 0x00, 0x00, 0x00}; + +static unsigned char dnd_link_mask_bits[] = +{ + 0xff, 0xff, 0x1f, 0x00, 0xff, 0xff, 0xff, 0x1f, 0xff, 0xff, 0xff, 0x1f, + 0x07, 0x00, 0xfc, 0x1f, 0x07, 0x00, 0xfc, 0x1f, 0x07, 0x00, 0xfc, 0x1f, + 0x07, 0x00, 0xfc, 0x1f, 0x07, 0x00, 0xfc, 0x1f, 0x07, 0x00, 0xfc, 0x1f, + 0x07, 0x06, 0xfc, 0x1f, 0x07, 0x0e, 0xfc, 0x1f, 0x07, 0x1e, 0x1c, 0x00, + 0x07, 0x3e, 0x1c, 0x00, 0x07, 0x7e, 0x1c, 0x00, 0x07, 0xfe, 0x1c, 0x00, + 0x07, 0xfe, 0x1d, 0x00, 0x07, 0xfe, 0x1f, 0x00, 0x07, 0xfe, 0x1f, 0x00, + 0xff, 0xff, 0x1f, 0x00, 0xff, 0xff, 0x1e, 0x00, 0xff, 0xef, 0x1f, 0x00, + 0x00, 0xe6, 0x01, 0x00, 0x00, 0xc0, 0x03, 0x00, 0x00, 0xc0, 0x03, 0x00, + 0x00, 0x80, 0x01, 0x00}; + +static unsigned char dnd_ask_cursor_bits[] = +{ + 0x00, 0x00, 0x00, 0x00, 0xfe, 0xff, 0x0f, 0x00, 0x02, 0x00, 0x88, 0x03, + 0x02, 0x00, 0x48, 0x04, 0x02, 0x00, 0x08, 0x04, 0x02, 0x00, 0x08, 0x02, + 0x02, 0x00, 0x08, 0x01, 0x02, 0x00, 0x08, 0x01, 0x02, 0x00, 0x08, 0x00, + 0x02, 0x00, 0x08, 0x01, 0x02, 0x04, 0x08, 0x00, 0x02, 0x0c, 0x08, 0x00, + 0x02, 0x1c, 0x08, 0x00, 0x02, 0x3c, 0x08, 0x00, 0x02, 0x7c, 0x08, 0x00, + 0x02, 0xfc, 0x08, 0x00, 0x02, 0xfc, 0x09, 0x00, 0x02, 0xfc, 0x0b, 0x00, + 0x02, 0x7c, 0x08, 0x00, 0xfe, 0x6d, 0x0f, 0x00, 0x00, 0xc4, 0x00, 0x00, + 0x00, 0xc0, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, + 0x00, 0x00, 0x00, 0x00}; + +static unsigned char dnd_ask_mask_bits[] = +{ + 0xff, 0xff, 0x1f, 0x00, 0xff, 0xff, 0xff, 0x1f, 0xff, 0xff, 0xff, 0x1f, + 0x07, 0x00, 0xfc, 0x1f, 0x07, 0x00, 0xfc, 0x1f, 0x07, 0x00, 0xfc, 0x1f, + 0x07, 0x00, 0xfc, 0x1f, 0x07, 0x00, 0xfc, 0x1f, 0x07, 0x00, 0xfc, 0x1f, + 0x07, 0x06, 0xfc, 0x1f, 0x07, 0x0e, 0xfc, 0x1f, 0x07, 0x1e, 0x1c, 0x00, + 0x07, 0x3e, 0x1c, 0x00, 0x07, 0x7e, 0x1c, 0x00, 0x07, 0xfe, 0x1c, 0x00, + 0x07, 0xfe, 0x1d, 0x00, 0x07, 0xfe, 0x1f, 0x00, 0x07, 0xfe, 0x1f, 0x00, + 0xff, 0xff, 0x1f, 0x00, 0xff, 0xff, 0x1e, 0x00, 0xff, 0xef, 0x1f, 0x00, + 0x00, 0xe6, 0x01, 0x00, 0x00, 0xc0, 0x03, 0x00, 0x00, 0xc0, 0x03, 0x00, + 0x00, 0x80, 0x01, 0x00}; + +static DndCursor dnd_cursors[] = +{ + {29, 25, 10, 10, dnd_copy_cursor_bits, dnd_copy_mask_bits, "XdndActionCopy", 0, 0, 0, 0}, + {21, 25, 10, 10, dnd_move_cursor_bits, dnd_move_mask_bits, "XdndActionMove", 0, 0, 0, 0}, + {29, 25, 10, 10, dnd_link_cursor_bits, dnd_link_mask_bits, "XdndActionLink", 0, 0, 0, 0}, + {29, 25, 10, 10, dnd_ask_cursor_bits, dnd_ask_mask_bits, "XdndActionAsk", 0, 0, 0, 0}, + {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} +}; + +void xdnd_reset (DndClass * dnd) +{ + dnd->stage = XDND_DROP_STAGE_IDLE; + dnd->dragging_version = 0; + dnd->internal_drag = 0; + dnd->want_position = 0; + dnd->ready_to_drop = 0; + dnd->will_accept = 0; + dnd->rectangle.x = dnd->rectangle.y = 0; + dnd->rectangle.width = dnd->rectangle.height = 0; + dnd->dropper_window = 0; + dnd->dropper_toplevel = 0; + dnd->dragger_window = 0; + dnd->dragger_typelist = 0; + dnd->desired_type = 0; + dnd->time = 0; +} + +void xdnd_init (DndClass * dnd, Display * display) +{ + DndCursor *cursor; + XColor black, white; + memset (dnd, 0, sizeof (*dnd)); + + dnd->display = display; + dnd->root_window = DefaultRootWindow (display); + dnd->version = XDND_VERSION; + + dnd->XdndAware = XInternAtom (dnd->display, "XdndAware", False); + dnd->XdndSelection = XInternAtom (dnd->display, "XdndSelection", False); + dnd->XdndEnter = XInternAtom (dnd->display, "XdndEnter", False); + dnd->XdndLeave = XInternAtom (dnd->display, "XdndLeave", False); + dnd->XdndPosition = XInternAtom (dnd->display, "XdndPosition", False); + dnd->XdndDrop = XInternAtom (dnd->display, "XdndDrop", False); + dnd->XdndFinished = XInternAtom (dnd->display, "XdndFinished", False); + dnd->XdndStatus = XInternAtom (dnd->display, "XdndStatus", False); + dnd->XdndActionCopy = XInternAtom (dnd->display, "XdndActionCopy", False); + dnd->XdndActionMove = XInternAtom (dnd->display, "XdndActionMove", False); + dnd->XdndActionLink = XInternAtom (dnd->display, "XdndActionLink", False); + dnd->XdndActionAsk = XInternAtom (dnd->display, "XdndActionAsk", False); + dnd->XdndActionPrivate = XInternAtom (dnd->display, "XdndActionPrivate", False); + dnd->XdndTypeList = XInternAtom (dnd->display, "XdndTypeList", False); + dnd->XdndActionList = XInternAtom (dnd->display, "XdndActionList", False); + dnd->XdndActionDescription = XInternAtom (dnd->display, "XdndActionDescription", False); + + dnd->Xdnd_NON_PROTOCOL_ATOM = XInternAtom (dnd->display, "JXSelectionWindowProperty", False); + + xdnd_reset (dnd); + + dnd->cursors = dnd_cursors; + + black.pixel = BlackPixel (dnd->display, DefaultScreen (dnd->display)); + white.pixel = WhitePixel (dnd->display, DefaultScreen (dnd->display)); + + XQueryColor (dnd->display, DefaultColormap (dnd->display, DefaultScreen (dnd->display)), &black); + XQueryColor (dnd->display, DefaultColormap (dnd->display, DefaultScreen (dnd->display)), &white); + + for (cursor = &dnd->cursors[0]; cursor->width; cursor++) { + cursor->image_pixmap = XCreateBitmapFromData \ + (dnd->display, dnd->root_window, (char *) cursor->image_data, cursor->width, cursor->height); + cursor->mask_pixmap = XCreateBitmapFromData \ + (dnd->display, dnd->root_window, (char *) cursor->mask_data, cursor->width, cursor->height); + cursor->cursor = XCreatePixmapCursor (dnd->display, cursor->image_pixmap, + cursor->mask_pixmap, &black, &white, cursor->x, cursor->y); + XFreePixmap (dnd->display, cursor->image_pixmap); + XFreePixmap (dnd->display, cursor->mask_pixmap); + cursor->action = XInternAtom (dnd->display, cursor->_action, False); + } +} + +void xdnd_shut (DndClass * dnd) +{ + DndCursor *cursor; + for (cursor = &dnd->cursors[0]; cursor->width; cursor++) + XFreeCursor (dnd->display, cursor->cursor); + memset (dnd, 0, sizeof (*dnd)); + return; +} + + +/* typelist is a null terminated array */ +static int array_length (Atom * a) +{ + int n; + for (n = 0; a[n]; n++); + return n; +} + +void xdnd_set_dnd_aware (DndClass * dnd, Window window, Atom * typelist) +{ + Window root_return, parent; + unsigned int nchildren_return; + Window *children_return = 0; + int r, s; + if(!window) return; + if (dnd->widget_exists) + if (!(*dnd->widget_exists) (dnd, window)) + return; + s = XChangeProperty (dnd->display, window, dnd->XdndAware, XA_ATOM, 32, PropModeReplace, + (unsigned char *) &dnd->version, 1); +#if 1 + dnd_debug4 ("XChangeProperty() = %d, window = %ld, widget = %s", s, window, ""); +#endif + if (s && typelist) { + int n; + n = array_length (typelist); + if (n) + s = XChangeProperty (dnd->display, window, dnd->XdndAware, XA_ATOM, 32, PropModeAppend, + (unsigned char *) typelist, n); + } + r = + XQueryTree (dnd->display, window, &root_return, &parent, &children_return, + &nchildren_return); + if (children_return) + XFree (children_return); + if (r && parent != root_return) + xdnd_set_dnd_aware (dnd, parent, typelist); +} + +int xdnd_is_dnd_aware (DndClass * dnd, Window window, int *version, Atom * typelist) +{ + Atom actual; + int format; + unsigned long count, remaining; + unsigned char *data = 0; + Atom *types, *t; + int result = 1; + + *version = 0; + XGetWindowProperty (dnd->display, window, dnd->XdndAware, + 0, 0x8000000L, False, XA_ATOM, + &actual, &format, + &count, &remaining, &data); + + if (actual != XA_ATOM || format != 32 || count == 0 || !data) { + dnd_debug2 ("XGetWindowProperty failed in xdnd_is_dnd_aware - XdndAware = %ld", dnd->XdndAware); + if (data) + XFree (data); + return 0; + } + types = (Atom *) data; +#if XDND_VERSION >= 3 + if (types[0] < 3) { + if (data) + XFree (data); + return 0; + } +#endif + *version = dnd->version < types[0] ? dnd->version : types[0]; /* minimum */ + dnd_debug2 ("Using XDND version %d", *version); + if (count > 1) { + result = 0; + for (t = typelist; *t; t++) { + int j; + for (j = 1; j < count; j++) { + if (types[j] == *t) { + result = 1; + break; + } + } + if (result) + break; + } + } + XFree (data); + return result; +} + +void xdnd_set_type_list (DndClass * dnd, Window window, Atom * typelist) +{ + int n; + n = array_length (typelist); + XChangeProperty (dnd->display, window, dnd->XdndTypeList, XA_ATOM, 32, + PropModeReplace, (unsigned char *) typelist, n); +} + +/* result must be free'd */ +void xdnd_get_type_list (DndClass * dnd, Window window, Atom ** typelist) +{ + Atom type, *a; + int format, i; + unsigned long count, remaining; + unsigned char *data = NULL; + + *typelist = 0; + + XGetWindowProperty (dnd->display, window, dnd->XdndTypeList, + 0, 0x8000000L, False, XA_ATOM, + &type, &format, &count, &remaining, &data); + + if (type != XA_ATOM || format != 32 || count == 0 || !data) { + if (data) + XFree (data); + dnd_debug2 ("XGetWindowProperty failed in xdnd_get_type_list - dnd->XdndTypeList = %ld", dnd->XdndTypeList); + return; + } + *typelist = malloc ((count + 1) * sizeof (Atom)); + a = (Atom *) data; + for (i = 0; i < count; i++) + (*typelist)[i] = a[i]; + (*typelist)[count] = 0; + + XFree (data); +} + +void xdnd_get_three_types (DndClass * dnd, XEvent * xevent, Atom ** typelist) +{ + int i; + (void) dnd; + + *typelist = malloc ((XDND_THREE + 1) * sizeof (Atom)); + for (i = 0; i < XDND_THREE; i++) + (*typelist)[i] = XDND_ENTER_TYPE (xevent, i); + (*typelist)[XDND_THREE] = 0; /* although (*typelist)[1] or (*typelist)[2] may also be set to nill */ +} + +/* result must be free'd */ +static char *concat_string_list (char **t, int *bytes) +{ + int l, n; + char *s; + for (l = n = 0;; n++) { + if (!t[n]) + break; + if (!t[n][0]) + break; + l += strlen (t[n]) + 1; + } + s = malloc (l + 1); + for (l = n = 0;; n++) { + if (!t[n]) + break; + if (!(t[n][0])) + break; + int t_size = strlen (t[n]) + 1; + memcpy (s + l, t[n], t_size); + l += t_size; + } + *bytes = l; + s[l] = '\0'; + return s; +} + +void xdnd_set_actions (DndClass * dnd, Window window, Atom * actions, char **descriptions) +{ + int n, l; + char *s; + n = array_length (actions); + + XChangeProperty (dnd->display, window, dnd->XdndActionList, XA_ATOM, 32, + PropModeReplace, (unsigned char *) actions, n); + + s = concat_string_list (descriptions, &l); + XChangeProperty (dnd->display, window, dnd->XdndActionList, XA_STRING, 8, + PropModeReplace, (unsigned char *) s, l); + xdnd_xfree (s); +} + +/* returns 1 on error or no actions, otherwise result must be free'd + xdnd_get_actions (window, &actions, &descriptions); + free (actions); free (descriptions); */ +int xdnd_get_actions (DndClass * dnd, Window window, Atom ** actions, char ***descriptions) +{ + Atom type, *a; + int format, i; + unsigned long count, dcount, remaining; + unsigned char *data = 0, *r; + + *actions = 0; + *descriptions = 0; + XGetWindowProperty (dnd->display, window, dnd->XdndActionList, + 0, 0x8000000L, False, XA_ATOM, + &type, &format, &count, &remaining, &data); + + if (type != XA_ATOM || format != 32 || count == 0 || !data) { + if (data) + XFree (data); + return 1; + } + *actions = malloc ((count + 1) * sizeof (Atom)); + a = (Atom *) data; + for (i = 0; i < count; i++) + (*actions)[i] = a[i]; + (*actions)[count] = 0; + + XFree (data); + + data = 0; + XGetWindowProperty (dnd->display, window, dnd->XdndActionDescription, + 0, 0x8000000L, False, XA_STRING, &type, &format, + &dcount, &remaining, &data); + + if (type != XA_STRING || format != 8 || dcount == 0) { + if (data) + XFree (data); + *descriptions = malloc ((count + 1) * sizeof (char *)); + dnd_warning ("XGetWindowProperty no property or wrong format for action descriptions"); + for (i = 0; i < count; i++) + (*descriptions)[i] = ""; + (*descriptions)[count] = 0; + } else { + int l; + l = (count + 1) * sizeof (char *); + *descriptions = malloc (l + dcount); + memcpy (*descriptions + l, data, dcount); + XFree (data); + data = (unsigned char *) *descriptions; + data += l; + l = 0; + for (i = 0, r = data;; r += l + 1, i++) { + l = strlen ((char *) r); + if (!l || i >= count) + break; + (*descriptions)[i] = (char *) r; + } + for (; i < count; i++) { + (*descriptions)[i] = ""; + } + (*descriptions)[count] = 0; + } + return 0; +} + +/* returns non-zero on cancel */ +int xdnd_choose_action_dialog (DndClass * dnd, Atom * actions, char **descriptions, Atom * result) +{ + if (!actions[0]) + return 1; + if (!dnd->action_choose_dialog) { /* default to return the first action if no dialog set */ + *result = actions[0]; + return 0; + } + return (*dnd->action_choose_dialog) (dnd, descriptions, actions, result); +} + +static void xdnd_send_event (DndClass * dnd, Window window, XEvent * xevent) +{ + dnd_debug4 ("xdnd_send_event(), window = %ld, l[0] = %ld, l[4] = %ld", + window, xevent->xclient.data.l[0], xevent->xclient.data.l[4]); + dnd_debug2 ("xdnd_send_event(), from widget widget %s", (char *) ""); + XSendEvent (dnd->display, window, 0, 0, xevent); +} + +static void xdnd_send_enter (DndClass * dnd, Window window, Window from, Atom * typelist) +{ + XEvent xevent; + int n, i; + n = array_length (typelist); + + memset (&xevent, 0, sizeof (xevent)); + + xevent.xany.type = ClientMessage; + xevent.xany.display = dnd->display; + xevent.xclient.window = window; + xevent.xclient.message_type = dnd->XdndEnter; + xevent.xclient.format = 32; + + XDND_ENTER_SOURCE_WIN (&xevent) = from; + XDND_ENTER_THREE_TYPES_SET (&xevent, n > XDND_THREE); + XDND_ENTER_VERSION_SET (&xevent, dnd->version); + for (i = 0; i < n && i < XDND_THREE; i++) + XDND_ENTER_TYPE (&xevent, i) = typelist[i]; + xdnd_send_event (dnd, window, &xevent); +} + +static void xdnd_send_position (DndClass * dnd, Window window, Window from, Atom action, int x, int y, unsigned long time) +{ + XEvent xevent; + + memset (&xevent, 0, sizeof (xevent)); + + xevent.xany.type = ClientMessage; + xevent.xany.display = dnd->display; + xevent.xclient.window = window; + xevent.xclient.message_type = dnd->XdndPosition; + xevent.xclient.format = 32; + + XDND_POSITION_SOURCE_WIN (&xevent) = from; + XDND_POSITION_ROOT_SET (&xevent, x, y); + if (dnd_version_at_least (dnd->dragging_version, 1)) + XDND_POSITION_TIME (&xevent) = time; + if (dnd_version_at_least (dnd->dragging_version, 2)) + XDND_POSITION_ACTION (&xevent) = action; + + xdnd_send_event (dnd, window, &xevent); +} + +static void xdnd_send_status (DndClass * dnd, Window window, Window from, int will_accept, \ + int want_position, int x, int y, int w, int h, Atom action) +{ + XEvent xevent; + + memset (&xevent, 0, sizeof (xevent)); + + xevent.xany.type = ClientMessage; + xevent.xany.display = dnd->display; + xevent.xclient.window = window; + xevent.xclient.message_type = dnd->XdndStatus; + xevent.xclient.format = 32; + + XDND_STATUS_TARGET_WIN (&xevent) = from; + XDND_STATUS_WILL_ACCEPT_SET (&xevent, will_accept); + if (will_accept) + XDND_STATUS_WANT_POSITION_SET (&xevent, want_position); + if (want_position) + XDND_STATUS_RECT_SET (&xevent, x, y, w, h); + if (dnd_version_at_least (dnd->dragging_version, 2)) + if (will_accept) + XDND_STATUS_ACTION (&xevent) = action; + + xdnd_send_event (dnd, window, &xevent); +} + +static void xdnd_send_leave (DndClass * dnd, Window window, Window from) +{ + XEvent xevent; + + memset (&xevent, 0, sizeof (xevent)); + + xevent.xany.type = ClientMessage; + xevent.xany.display = dnd->display; + xevent.xclient.window = window; + xevent.xclient.message_type = dnd->XdndLeave; + xevent.xclient.format = 32; + + XDND_LEAVE_SOURCE_WIN (&xevent) = from; + + xdnd_send_event (dnd, window, &xevent); +} + +static void xdnd_send_drop (DndClass * dnd, Window window, Window from, unsigned long time) +{ + XEvent xevent; + + memset (&xevent, 0, sizeof (xevent)); + + xevent.xany.type = ClientMessage; + xevent.xany.display = dnd->display; + xevent.xclient.window = window; + xevent.xclient.message_type = dnd->XdndDrop; + xevent.xclient.format = 32; + + XDND_DROP_SOURCE_WIN (&xevent) = from; + if (dnd_version_at_least (dnd->dragging_version, 1)) + XDND_DROP_TIME (&xevent) = time; + + xdnd_send_event (dnd, window, &xevent); +} + +/* error is not actually used, i think future versions of the protocol should return an error status + to the calling window with the XdndFinished client message */ +static void xdnd_send_finished (DndClass * dnd, Window window, Window from, int error) +{ + XEvent xevent; + (void) error; + + memset (&xevent, 0, sizeof (xevent)); + + xevent.xany.type = ClientMessage; + xevent.xany.display = dnd->display; + xevent.xclient.window = window; + xevent.xclient.message_type = dnd->XdndFinished; + xevent.xclient.format = 32; + + XDND_FINISHED_TARGET_WIN (&xevent) = from; + + xdnd_send_event (dnd, window, &xevent); +} + +/* returns non-zero on error - i.e. no selection owner set. Type is of course the mime type */ +static int xdnd_convert_selection (DndClass * dnd, Window window, Window requester, Atom type) +{ + if (!(window = XGetSelectionOwner (dnd->display, dnd->XdndSelection))) { + dnd_debug1 ("xdnd_convert_selection(): XGetSelectionOwner failed"); + return 1; + } + XConvertSelection (dnd->display, dnd->XdndSelection, type, + dnd->Xdnd_NON_PROTOCOL_ATOM, requester, CurrentTime); + return 0; +} + +/* returns non-zero on error */ +static int xdnd_set_selection_owner (DndClass * dnd, Window window, Atom type, Time time) +{ + (void) type; + + if (!XSetSelectionOwner (dnd->display, dnd->XdndSelection, window, time)) { + dnd_debug1 ("xdnd_set_selection_owner(): XSetSelectionOwner failed"); + return 1; + } + return 0; +} + +static void xdnd_selection_send (DndClass * dnd, XSelectionRequestEvent * request, unsigned char *data, int length) +{ + XEvent xevent; + dnd_debug2 (" requestor = %ld", request->requestor); + dnd_debug2 (" property = %ld", request->property); + dnd_debug2 (" length = %d", length); + XChangeProperty (dnd->display, request->requestor, request->property, + request->target, 8, PropModeReplace, data, length); + xevent.xselection.type = SelectionNotify; + xevent.xselection.property = request->property; + xevent.xselection.display = request->display; + xevent.xselection.requestor = request->requestor; + xevent.xselection.selection = request->selection; + xevent.xselection.target = request->target; + xevent.xselection.time = request->time; + xdnd_send_event (dnd, request->requestor, &xevent); +} + +#if 0 +/* respond to a notification that a primary selection has been sent */ +int xdnd_get_selection (DndClass * dnd, Window from, Atom property, Window insert) +{ + long read; + int error = 0; + unsigned long remaining; + if (!property) + return 1; + read = 0; + do { + unsigned char *s; + Atom actual; + int format; + unsigned long count; + if (XGetWindowProperty (dnd->display, insert, property, read / 4, 65536, 1, + AnyPropertyType, &actual, &format, + &count, &remaining, + &s) != Success) { + XFree (s); + return 1; + } + read += count; + if (dnd->widget_insert_drop && !error) + error = (*dnd->widget_insert_drop) (dnd, s, count, remaining, insert, from, actual); + XFree (s); + } while (remaining); + return error; +} +#endif + +static int paste_prop_internal (DndClass * dnd, Window from, Window insert, unsigned long prop, int delete_prop) +{ + long nread = 0; + unsigned long nitems; + unsigned long bytes_after; + int error = 0; + do { + Atom actual_type; + int actual_fmt; + unsigned char *s = 0; + if (XGetWindowProperty (dnd->display, insert, prop, + nread / 4, 65536, delete_prop, + AnyPropertyType, &actual_type, &actual_fmt, + &nitems, &bytes_after, &s) != Success) { + XFree (s); + return 1; + } + nread += nitems; + if (dnd->widget_insert_drop && !error) + error = (*dnd->widget_insert_drop) (dnd, s, nitems, bytes_after, insert, from, actual_fmt); + XFree (s); + } while (bytes_after); + if (!nread) + return 1; + return 0; +} + +/* + * Respond to a notification that a primary selection has been sent (supports INCR) + */ +static int xdnd_get_selection (DndClass * dnd, Window from, Atom prop, Window insert) +{ + struct timeval tv, tv_start; + unsigned long bytes_after; + Atom actual_type; + int actual_fmt; + unsigned long nitems; + unsigned char *s = 0; + if (prop == None) + return 1; + if (XGetWindowProperty + (dnd->display, insert, prop, 0, 8, False, AnyPropertyType, &actual_type, &actual_fmt, + &nitems, &bytes_after, &s) != Success) { + XFree (s); + return 1; + } + XFree (s); + if (actual_type != XInternAtom (dnd->display, "INCR", False)) + return paste_prop_internal (dnd, from, insert, prop, True); + XDeleteProperty (dnd->display, insert, prop); + gettimeofday (&tv_start, 0); + for (;;) { + long t; + fd_set r; + XEvent xe; + if (XCheckMaskEvent (dnd->display, PropertyChangeMask, &xe)) { + if (xe.type == PropertyNotify && xe.xproperty.state == PropertyNewValue) { +/* time between arrivals of data */ + gettimeofday (&tv_start, 0); + if (paste_prop_internal (dnd, from, insert, prop, True)) + break; + } + } else { + tv.tv_sec = 0; + tv.tv_usec = 10000; + FD_ZERO (&r); + FD_SET (ConnectionNumber (dnd->display), &r); + select (ConnectionNumber (dnd->display) + 1, &r, 0, 0, &tv); + if (FD_ISSET (ConnectionNumber (dnd->display), &r)) + continue; + } + gettimeofday (&tv, 0); + t = (tv.tv_sec - tv_start.tv_sec) * 1000000L + (tv.tv_usec - tv_start.tv_usec); +/* no data for five seconds, so quit */ + if (t > 5000000L) + return 1; + } + return 0; +} + + +int outside_rectangle (int x, int y, XRectangle * r) +{ + return (x < r->x || y < r->y || x >= r->x + r->width || y >= r->y + r->height); +} + +/* avoids linking with the maths library */ +static float xdnd_sqrt (float x) +{ + float last_ans, ans = 2, a; + if (x <= 0.0f) + return 0.0f; + do { + last_ans = ans; + ans = (ans + x / ans) / 2; + a = (ans - last_ans) / ans; + if (a < 0.0f) + a = (-a); + } while (a > 0.001f); + return ans; +} + +/* returns action on success, 0 otherwise */ +Atom xdnd_drag (DndClass * dnd, Window from, Atom action, Atom * typelist) +{ + XEvent xevent, xevent_temp; + Window over_window = 0, last_window = 0; +#if XDND_VERSION >= 3 + Window last_dropper_toplevel = 0; + int internal_dropable = 1; +#endif + int n; + DndCursor *cursor; + float x_mouse, y_mouse; + int result = 0, dnd_aware; + + if (!typelist) + dnd_warning ("xdnd_drag() called with typelist = 0"); + +/* first wait until the mouse moves more than five pixels */ + do { + XNextEvent (dnd->display, &xevent); + if (xevent.type == ButtonRelease) { + dnd_debug1 ("button release - no motion"); + XSendEvent (dnd->display, xevent.xany.window, 0, ButtonReleaseMask, &xevent); + return 0; + } + } while (xevent.type != MotionNotify); + + x_mouse = (float) xevent.xmotion.x_root; + y_mouse = (float) xevent.xmotion.y_root; + + if (dnd->drag_threshold < 0.001f) + dnd->drag_threshold = 4.0f; + for (;;) { + XNextEvent (dnd->display, &xevent); + if (xevent.type == MotionNotify) + if (xdnd_sqrt ((x_mouse - xevent.xmotion.x_root) * (x_mouse - xevent.xmotion.x_root) + + (y_mouse - xevent.xmotion.y_root) * (y_mouse - xevent.xmotion.y_root)) > dnd->drag_threshold) + break; + if (xevent.type == ButtonRelease) { + XSendEvent (dnd->display, xevent.xany.window, 0, ButtonReleaseMask, &xevent); + return 0; + } + } + + dnd_debug1 ("moved 5 pixels - going to drag"); + + n = array_length (typelist); + if (n > XDND_THREE) + xdnd_set_type_list (dnd, from, typelist); + + xdnd_reset (dnd); + + dnd->stage = XDND_DRAG_STAGE_DRAGGING; + + for (cursor = &dnd->cursors[0]; cursor->width; cursor++) + if (cursor->action == action) + break; + if (!cursor->width) + cursor = &dnd->cursors[0]; + +/* the mouse has been dragged a little, so this is a drag proper */ + if (XGrabPointer (dnd->display, dnd->root_window, False, + ButtonMotionMask | PointerMotionMask | ButtonPressMask | ButtonReleaseMask, + GrabModeAsync, GrabModeAsync, None, + cursor->cursor, CurrentTime) != GrabSuccess) + dnd_debug1 ("Unable to grab pointer"); + + + while (xevent.xany.type != ButtonRelease) { + XAllowEvents (dnd->display, SyncPointer, CurrentTime); + XNextEvent (dnd->display, &xevent); + switch (xevent.type) { + case Expose: + if (dnd->handle_expose_events) + (*dnd->handle_expose_events) (dnd, &xevent); + break; + case EnterNotify: +/* this event is not actually reported, so we find out by ourselves from motion events */ + break; + case LeaveNotify: +/* this event is not actually reported, so we find out by ourselves from motion events */ + break; + case ButtonRelease: +/* done, but must send a leave event */ + dnd_debug1 ("ButtonRelease - exiting event loop"); + break; + case MotionNotify: + dnd_aware = 0; + dnd->dropper_toplevel = 0; + memcpy (&xevent_temp, &xevent, sizeof (xevent)); + xevent.xmotion.subwindow = xevent.xmotion.window; + { + Window root_return, child_return; + int x_temp, y_temp; + unsigned int mask_return; + while (XQueryPointer (dnd->display, xevent.xmotion.subwindow, &root_return, &child_return, + &x_temp, &y_temp, &xevent.xmotion.x, + &xevent.xmotion.y, &mask_return)) { +#if XDND_VERSION >= 3 + if (!dnd_aware) { + if ((dnd_aware = xdnd_is_dnd_aware (dnd, xevent.xmotion.subwindow, &dnd->dragging_version, typelist))) { + dnd->dropper_toplevel = xevent.xmotion.subwindow; + xevent.xmotion.x_root = x_temp; + xevent.xmotion.y_root = y_temp; + } + } +#else + xevent.xmotion.x_root = x_temp; + xevent.xmotion.y_root = y_temp; +#endif + if (!child_return) + goto found_descendent; + xevent.xmotion.subwindow = child_return; + } + break; + } + found_descendent: + +/* last_window is just for debug purposes */ + if (last_window != xevent.xmotion.subwindow) { + dnd_debug2 ("window crossing to %ld", xevent.xmotion.subwindow); + dnd_debug2 (" current window is %ld", over_window); + dnd_debug3 (" last_window = %ld, xmotion.subwindow = %ld", last_window, xevent.xmotion.subwindow); +#if XDND_VERSION >= 3 + dnd_debug3 (" dropper_toplevel = %ld, last_dropper_toplevel.subwindow = %ld", dnd->dropper_toplevel, last_dropper_toplevel); +#endif + dnd_debug3 (" dnd_aware = %d, dnd->options & XDND_OPTION_NO_HYSTERESIS = %ld", dnd_aware, (long) dnd->options & XDND_OPTION_NO_HYSTERESIS); + } + +#if XDND_VERSION < 3 +/* is the new window dnd aware? if not stay in the old window */ + if (over_window != xevent.xmotion.subwindow && + last_window != xevent.xmotion.subwindow && + ( + (dnd_aware = xdnd_is_dnd_aware (dnd, xevent.xmotion.subwindow, &dnd->dragging_version, typelist)) + || + (dnd->options & XDND_OPTION_NO_HYSTERESIS) + )) +#else + internal_dropable = 1; + if (dnd->widget_exists && (*dnd->widget_exists) (dnd, xevent.xmotion.subwindow)) + if (!xdnd_is_dnd_aware (dnd, xevent.xmotion.subwindow, &dnd->dragging_version, typelist)) + internal_dropable = 0; + dnd_debug3 ("dnd->dropper_toplevel = %ld, last_dropper_toplevel = %ld\n", dnd->dropper_toplevel, last_dropper_toplevel); + if ((dnd->dropper_toplevel != last_dropper_toplevel || + last_window != xevent.xmotion.subwindow) && internal_dropable && + ( + (dnd_aware) + || + (dnd->options & XDND_OPTION_NO_HYSTERESIS) + )) +#endif + { +/* leaving window we were over */ + if (over_window) { + if (dnd->stage == XDND_DRAG_STAGE_ENTERED) { + dnd_debug1 ("got leave at right stage"); + dnd->stage = XDND_DRAG_STAGE_DRAGGING; + if (dnd->internal_drag) { + dnd_debug1 (" our own widget"); + if (dnd->widget_apply_leave) + (*dnd->widget_apply_leave) (dnd, over_window); + } else { + dnd_debug1 (" not our widget - sending XdndLeave"); +#if XDND_VERSION < 3 + xdnd_send_leave (dnd, over_window, from); +#else + if (dnd->dropper_toplevel != last_dropper_toplevel) { + xdnd_send_leave (dnd, last_dropper_toplevel, from); + } else { + dnd_debug1 (" not sending leave --> dnd->dropper_toplevel == last_dropper_toplevel"); + } +#endif + } + dnd->internal_drag = 0; + dnd->dropper_window = 0; + dnd->ready_to_drop = 0; + } else { + dnd_debug1 ("got leave at wrong stage - ignoring"); + } + } +/* entering window we are currently over */ + over_window = xevent.xmotion.subwindow; + if (dnd_aware) { + dnd_debug1 (" is dnd aware"); + dnd->stage = XDND_DRAG_STAGE_ENTERED; + if (dnd->widget_exists && (*dnd->widget_exists) (dnd, over_window)) + dnd->internal_drag = 1; + if (dnd->internal_drag) { + dnd_debug1 (" our own widget"); + } else { + dnd_debug2 (" not our widget - sending XdndEnter to %ld", over_window); +#if XDND_VERSION < 3 + xdnd_send_enter (dnd, over_window, from, typelist); +#else + if (dnd->dropper_toplevel != last_dropper_toplevel) + xdnd_send_enter (dnd, dnd->dropper_toplevel, from, typelist); +#endif + } + dnd->want_position = 1; + dnd->ready_to_drop = 0; + dnd->rectangle.width = dnd->rectangle.height = 0; + dnd->dropper_window = over_window; +/* we want an additional motion event in case the pointer enters and then stops */ + XSendEvent (dnd->display, from, 0, ButtonMotionMask, &xevent_temp); + XSync (dnd->display, 0); + } +#if XDND_VERSION >= 3 + last_dropper_toplevel = dnd->dropper_toplevel; +#endif +/* we are now officially in a new window */ + } else { +/* got here, so we are just moving `inside' the same window */ + if (dnd->stage == XDND_DRAG_STAGE_ENTERED) { + dnd->supported_action = dnd->XdndActionCopy; + dnd_debug1 ("got motion at right stage"); + dnd->x = xevent.xmotion.x_root; + dnd->y = xevent.xmotion.y_root; + if (dnd->want_position || outside_rectangle (dnd->x, dnd->y, &dnd->rectangle)) { + dnd_debug1 (" want position and outside rectangle"); + if (dnd->internal_drag) { + dnd_debug1 (" our own widget"); + dnd->ready_to_drop = (*dnd->widget_apply_position) (dnd, over_window, from, + action, dnd->x, dnd->y, xevent.xmotion.time, typelist, + &dnd->want_position, &dnd->supported_action, &dnd->desired_type, &dnd->rectangle); + /* if not ready, keep sending positions, this check is repeated below for XdndStatus from external widgets */ + if (!dnd->ready_to_drop) { + dnd->want_position = 1; + dnd->rectangle.width = dnd->rectangle.height = 0; + } + dnd_debug2 (" return action=%ld", dnd->supported_action); + } else { +#if XDND_VERSION < 3 + dnd_debug3 (" not our own widget - sending XdndPosition to %ld, action %ld", over_window, action); + xdnd_send_position (dnd, over_window, from, action, dnd->x, dnd->y, xevent.xmotion.time); +#else + dnd_debug3 (" not our own widget - sending XdndPosition to %ld, action %ld", dnd->dropper_toplevel, action); + xdnd_send_position (dnd, dnd->dropper_toplevel, from, action, dnd->x, dnd->y, xevent.xmotion.time); +#endif + } + } else if (dnd->want_position) { + dnd_debug1 (" inside rectangle"); + } else { + dnd_debug1 (" doesn't want position"); + } + } + } + last_window = xevent.xmotion.subwindow; + break; + case ClientMessage: + dnd_debug1 ("ClientMessage recieved"); + if (xevent.xclient.message_type == dnd->XdndStatus && !dnd->internal_drag) { + dnd_debug1 (" XdndStatus recieved"); + if (dnd->stage == XDND_DRAG_STAGE_ENTERED +#if XDND_VERSION < 3 + && XDND_STATUS_TARGET_WIN (&xevent) == dnd->dropper_window +#endif + ) { + dnd_debug1 (" XdndStatus stage correct, dropper window correct"); + dnd->want_position = XDND_STATUS_WANT_POSITION (&xevent); + dnd->ready_to_drop = XDND_STATUS_WILL_ACCEPT (&xevent); + dnd->rectangle.x = XDND_STATUS_RECT_X (&xevent); + dnd->rectangle.y = XDND_STATUS_RECT_Y (&xevent); + dnd->rectangle.width = XDND_STATUS_RECT_WIDTH (&xevent); + dnd->rectangle.height = XDND_STATUS_RECT_HEIGHT (&xevent); + dnd->supported_action = dnd->XdndActionCopy; + if (dnd_version_at_least (dnd->dragging_version, 2)) + dnd->supported_action = XDND_STATUS_ACTION (&xevent); + dnd_debug3 (" return action=%ld, ready=%d", dnd->supported_action, dnd->ready_to_drop); + /* if not ready, keep sending positions, this check is repeated above for internal widgets */ + if (!dnd->ready_to_drop) { + dnd->want_position = 1; + dnd->rectangle.width = dnd->rectangle.height = 0; + } + dnd_debug3 (" rectangle = (x=%d, y=%d, ", dnd->rectangle.x, dnd->rectangle.y); + dnd_debug4 ("w=%d, h=%d), want_position=%d\n", dnd->rectangle.width, dnd->rectangle.height, dnd->want_position); + } +#if XDND_VERSION < 3 + else if (XDND_STATUS_TARGET_WIN (&xevent) != dnd->dropper_window) { + dnd_debug3 (" XdndStatus XDND_STATUS_TARGET_WIN (&xevent) = %ld, dnd->dropper_window = %ld", XDND_STATUS_TARGET_WIN (&xevent), dnd->dropper_window); + } +#endif + else { + dnd_debug2 (" XdndStatus stage incorrect dnd->stage = %d", dnd->stage); + } + } + break; + case SelectionRequest:{ +/* the target widget MAY request data, so wait for SelectionRequest */ + int length = 0; + unsigned char *data = 0; + dnd_debug1 ("SelectionRequest - getting widget data"); + + (*dnd->widget_get_data) (dnd, from, &data, &length, xevent.xselectionrequest.target); + if (data) { + dnd_debug1 (" sending selection"); + xdnd_selection_send (dnd, &xevent.xselectionrequest, data, length); + xdnd_xfree (data); + } + } + break; + } + } + + if (dnd->ready_to_drop) { + Time time; + dnd_debug1 ("ready_to_drop - sending XdndDrop"); + time = xevent.xbutton.time; + if (dnd->internal_drag) { +/* we are dealing with our own widget, no need to send drop events, just put the data straight */ + int length = 0; + unsigned char *data = 0; + if (dnd->widget_insert_drop) { + (*dnd->widget_get_data) (dnd, from, &data, &length, dnd->desired_type); + if (data) { + if (!(*dnd->widget_insert_drop) (dnd, data, length, 0, dnd->dropper_window, from, dnd->desired_type)) { + result = dnd->supported_action; /* success - so return action to caller */ + dnd_debug1 (" inserted data into widget - success"); + } else { + dnd_debug1 (" inserted data into widget - failed"); + } + xdnd_xfree (data); + } else { + dnd_debug1 (" got data from widget, but data is null"); + } + } + } else { + xdnd_set_selection_owner (dnd, from, dnd->desired_type, time); +#if XDND_VERSION < 3 + xdnd_send_drop (dnd, dnd->dropper_window, from, time); +#else + xdnd_send_drop (dnd, dnd->dropper_toplevel, from, time); +#endif + } + if (!dnd->internal_drag) + for (;;) { + XAllowEvents (dnd->display, SyncPointer, CurrentTime); + XNextEvent (dnd->display, &xevent); + if (xevent.type == ClientMessage && xevent.xclient.message_type == dnd->XdndFinished) { + dnd_debug1 ("XdndFinished"); +#if XDND_VERSION < 3 + if (XDND_FINISHED_TARGET_WIN (&xevent) == dnd->dropper_window) { +#endif + dnd_debug2 (" source correct - exiting event loop, action=%ld", dnd->supported_action); + result = dnd->supported_action; /* success - so return action to caller */ + break; +#if XDND_VERSION < 3 + } +#endif + } else if (xevent.type == Expose) { + if (dnd->handle_expose_events) + (*dnd->handle_expose_events) (dnd, &xevent); + } else if (xevent.type == MotionNotify) { + if (xevent.xmotion.time > time + (dnd->time_out ? dnd->time_out * 1000 : 10000)) { /* allow a ten second timeout as default */ + dnd_debug1 ("timeout - exiting event loop"); + break; + } + } else if (xevent.type == SelectionRequest && xevent.xselectionrequest.selection == dnd->XdndSelection) { +/* the target widget is going to request data, so check for SelectionRequest events */ + int length = 0; + unsigned char *data = 0; + + dnd_debug1 ("SelectionRequest - getting widget data"); + (*dnd->widget_get_data) (dnd, from, &data, &length, xevent.xselectionrequest.target); + if (data) { + dnd_debug1 (" sending selection"); + xdnd_selection_send (dnd, &xevent.xselectionrequest, data, length); + xdnd_xfree (data); + } +/* don't wait for a XdndFinished event */ + if (!dnd_version_at_least (dnd->dragging_version, 2)) + break; + } + } + } else { + dnd_debug1 ("not ready_to_drop - ungrabbing pointer"); + } + XUngrabPointer (dnd->display, CurrentTime); + xdnd_reset (dnd); + return result; +} + +/* returns non-zero if event is handled */ +int xdnd_handle_drop_events (DndClass * dnd, XEvent * xevent) +{ + int result = 0; + if (xevent->type == SelectionNotify) { + dnd_debug1 ("got SelectionNotify"); + if (xevent->xselection.property == dnd->Xdnd_NON_PROTOCOL_ATOM && dnd->stage == XDND_DROP_STAGE_CONVERTING) { + int error; + dnd_debug1 (" property is Xdnd_NON_PROTOCOL_ATOM - getting selection"); + error = xdnd_get_selection (dnd, dnd->dragger_window, xevent->xselection.property, xevent->xany.window); +/* error is not actually used, i think future versions of the protocol maybe should return + an error status to the calling window with the XdndFinished client message */ + if (dnd_version_at_least (dnd->dragging_version, 2)) { +#if XDND_VERSION >= 3 + xdnd_send_finished (dnd, dnd->dragger_window, dnd->dropper_toplevel, error); +#else + xdnd_send_finished (dnd, dnd->dragger_window, dnd->dropper_window, error); +#endif + dnd_debug1 (" sending finished"); + } + xdnd_xfree (dnd->dragger_typelist); + xdnd_reset (dnd); + dnd->stage = XDND_DROP_STAGE_IDLE; + result = 1; + } else { + dnd_debug1 (" property is not Xdnd_NON_PROTOCOL_ATOM - ignoring"); + } + } else if (xevent->type == ClientMessage) { + dnd_debug2 ("got ClientMessage to xevent->xany.window = %ld", xevent->xany.window); + if (xevent->xclient.message_type == dnd->XdndEnter) { + dnd_debug2 (" message_type is XdndEnter, version = %ld", XDND_ENTER_VERSION (xevent)); +#if XDND_VERSION >= 3 + if (XDND_ENTER_VERSION (xevent) < 3) + return 0; +#endif + xdnd_reset (dnd); + dnd->dragger_window = XDND_ENTER_SOURCE_WIN (xevent); +#if XDND_VERSION >= 3 + dnd->dropper_toplevel = xevent->xany.window; + dnd->dropper_window = 0; /* enter goes to the top level window only, + so we don't really know what the + sub window is yet */ +#else + dnd->dropper_window = xevent->xany.window; +#endif + xdnd_xfree (dnd->dragger_typelist); + if (XDND_ENTER_THREE_TYPES (xevent)) { + dnd_debug1 (" three types only"); + xdnd_get_three_types (dnd, xevent, &dnd->dragger_typelist); + } else { + dnd_debug1 (" more than three types - getting list"); + xdnd_get_type_list (dnd, dnd->dragger_window, &dnd->dragger_typelist); + } + if (dnd->dragger_typelist) + dnd->stage = XDND_DROP_STAGE_ENTERED; + else + dnd_debug1 (" typelist returned as zero!"); + dnd->dragging_version = XDND_ENTER_VERSION (xevent); + result = 1; + } else if (xevent->xclient.message_type == dnd->XdndLeave) { +#if XDND_VERSION >= 3 + if (xevent->xany.window == dnd->dropper_toplevel && dnd->dropper_window) + xevent->xany.window = dnd->dropper_window; +#endif + dnd_debug1 (" message_type is XdndLeave"); + if (dnd->dragger_window == XDND_LEAVE_SOURCE_WIN (xevent) && dnd->stage == XDND_DROP_STAGE_ENTERED) { + dnd_debug1 (" leaving"); + if (dnd->widget_apply_leave) + (*dnd->widget_apply_leave) (dnd, xevent->xany.window); + dnd->stage = XDND_DROP_STAGE_IDLE; + xdnd_xfree (dnd->dragger_typelist); + result = 1; + dnd->dropper_toplevel = dnd->dropper_window = 0; + } else { + dnd_debug1 (" wrong stage or from wrong window"); + } + } else if (xevent->xclient.message_type == dnd->XdndPosition) { + dnd_debug2 (" message_type is XdndPosition to %ld", xevent->xany.window); + if (dnd->dragger_window == XDND_POSITION_SOURCE_WIN (xevent) && dnd->stage == XDND_DROP_STAGE_ENTERED) { + int want_position; + Atom action; + XRectangle rectangle; + Window last_window; + last_window = dnd->dropper_window; +#if XDND_VERSION >= 3 +/* version 3 gives us the top-level window only. WE have to find the child that the pointer is over: */ + if (1 || xevent->xany.window != dnd->dropper_toplevel || !dnd->dropper_window) { + Window parent, child, new_child = 0; + dnd->dropper_toplevel = xevent->xany.window; + parent = dnd->root_window; + child = dnd->dropper_toplevel; + for (;;) { + int xd, yd; + new_child = 0; + if (!XTranslateCoordinates (dnd->display, parent, child, + XDND_POSITION_ROOT_X (xevent), XDND_POSITION_ROOT_Y (xevent), + &xd, &yd, &new_child)) + break; + if (!new_child) + break; + child = new_child; + } + dnd->dropper_window = xevent->xany.window = child; + dnd_debug2 (" child window translates to %ld", dnd->dropper_window); + } else if (xevent->xany.window == dnd->dropper_toplevel && dnd->dropper_window) { + xevent->xany.window = dnd->dropper_window; + dnd_debug2 (" child window previously found: %ld", dnd->dropper_window); + } +#endif + action = dnd->XdndActionCopy; + dnd->supported_action = dnd->XdndActionCopy; + dnd->x = XDND_POSITION_ROOT_X (xevent); + dnd->y = XDND_POSITION_ROOT_Y (xevent); + dnd->time = CurrentTime; + if (dnd_version_at_least (dnd->dragging_version, 1)) + dnd->time = XDND_POSITION_TIME (xevent); + if (dnd_version_at_least (dnd->dragging_version, 1)) + action = XDND_POSITION_ACTION (xevent); +#if XDND_VERSION >= 3 + if (last_window && last_window != xevent->xany.window) + if (dnd->widget_apply_leave) + (*dnd->widget_apply_leave) (dnd, last_window); +#endif + dnd->will_accept = (*dnd->widget_apply_position) (dnd, xevent->xany.window, dnd->dragger_window, + action, dnd->x, dnd->y, dnd->time, dnd->dragger_typelist, + &want_position, &dnd->supported_action, &dnd->desired_type, &rectangle); + dnd_debug2 (" will accept = %d", dnd->will_accept); +#if XDND_VERSION >= 3 + dnd_debug2 (" sending status of %ld", dnd->dropper_toplevel); + xdnd_send_status (dnd, dnd->dragger_window, dnd->dropper_toplevel, dnd->will_accept, + want_position, rectangle.x, rectangle.y, rectangle.width, rectangle.height, dnd->supported_action); +#else + dnd_debug2 (" sending status of %ld", xevent->xany.window); + xdnd_send_status (dnd, dnd->dragger_window, xevent->xany.window, dnd->will_accept, + want_position, rectangle.x, rectangle.y, rectangle.width, rectangle.height, dnd->supported_action); +#endif + result = 1; + } else { + dnd_debug1 (" wrong stage or from wrong window"); + } + } else if (xevent->xclient.message_type == dnd->XdndDrop) { +#if XDND_VERSION >= 3 + if (xevent->xany.window == dnd->dropper_toplevel && dnd->dropper_window) + xevent->xany.window = dnd->dropper_window; +#endif + dnd_debug1 (" message_type is XdndDrop"); + if (dnd->dragger_window == XDND_DROP_SOURCE_WIN (xevent) && dnd->stage == XDND_DROP_STAGE_ENTERED) { + dnd->time = CurrentTime; + if (dnd_version_at_least (dnd->dragging_version, 1)) + dnd->time = XDND_DROP_TIME (xevent); + if (dnd->will_accept) { + dnd_debug1 (" will_accept is true - converting selectiong"); + dnd_debug2 (" my window is %ld", dnd->dropper_window); + dnd_debug2 (" source window is %ld", dnd->dragger_window); + xdnd_convert_selection (dnd, dnd->dragger_window, dnd->dropper_window, dnd->desired_type); + dnd->stage = XDND_DROP_STAGE_CONVERTING; + } else { + dnd_debug1 (" will_accept is false - sending finished"); + if (dnd_version_at_least (dnd->dragging_version, 2)) { +#if XDND_VERSION >= 3 + xdnd_send_finished (dnd, dnd->dragger_window, dnd->dropper_toplevel, 1); +#else + xdnd_send_finished (dnd, dnd->dragger_window, xevent->xany.window, 1); +#endif + } + xdnd_xfree (dnd->dragger_typelist); + xdnd_reset (dnd); + dnd->stage = XDND_DROP_STAGE_IDLE; + } + result = 1; + } else { + dnd_debug1 (" wrong stage or from wrong window"); + } + } + } + return result; +} + +/* + Following here is a sample implementation: Suppose we want a window + to recieve drops, but do not want to be concerned with setting up all + the DndClass methods. All we then do is call xdnd_get_drop() whenever a + ClientMessage is recieved. If the message has nothing to do with XDND, + xdnd_get_drop quickly returns 0. If it is a XdndEnter message, then + xdnd_get_drop enters its own XNextEvent loop and handles all XDND + protocol messages internally, returning the action requested. + + You should pass a desired typelist and actionlist to xdnd_get_type. + These must be null terminated arrays of atoms, or a null pointer + if you would like any action or type to be accepted. If typelist + is null then the first type of the dragging widgets typelist will + be the one used. If actionlist is null, then only XdndActionCopy will + be accepted. + + The result is stored in *data, length, type, x and y. + *data must be free'd. + */ + +struct xdnd_get_drop_info { + unsigned char *drop_data; + int drop_data_length; + int x, y; + Atom return_type; + Atom return_action; + Atom *typelist; + Atom *actionlist; +}; + +static int widget_insert_drop (DndClass * dnd, unsigned char *data, int length, int remaining, Window into, Window from, Atom type) +{ + struct xdnd_get_drop_info *i; + (void) remaining; + (void) into; + (void) from; + (void) type; + + i = (struct xdnd_get_drop_info *) dnd->user_hook1; + if (!i->drop_data) { + i->drop_data = malloc (length); + if (!i->drop_data) + return 1; + memcpy (i->drop_data, data, length); + i->drop_data_length = length; + } else { + unsigned char *t; + t = malloc (i->drop_data_length + length); + if (!t) { + free (i->drop_data); + i->drop_data = 0; + return 1; + } + memcpy (t, i->drop_data, i->drop_data_length); + memcpy (t + i->drop_data_length, data, length); + free (i->drop_data); + i->drop_data = t; + i->drop_data_length += length; + } + return 0; +} + +static int widget_apply_position (DndClass * dnd, Window widgets_window, Window from, + Atom action, int x, int y, Time t, Atom * typelist, + int *want_position, Atom * supported_action_return, Atom * desired_type, + XRectangle * rectangle) +{ + int i, j; + struct xdnd_get_drop_info *info; + Atom *dropper_typelist, supported_type = 0; + Atom *supported_actions, supported_action = 0; + (void) widgets_window; + (void) from; + (void) t; + + info = (struct xdnd_get_drop_info *) dnd->user_hook1; + dropper_typelist = info->typelist; + supported_actions = info->actionlist; + + if (dropper_typelist) { +/* find a correlation: */ + for (j = 0; dropper_typelist[j]; j++) { + for (i = 0; typelist[i]; i++) { + if (typelist[i] == dropper_typelist[j]) { + supported_type = typelist[i]; + break; + } + } + if (supported_type) + break; + } + } else { +/* user did not specify, so return first type */ + supported_type = typelist[0]; + } +/* not supported, so return false */ + if (!supported_type) + return 0; + + if (supported_actions) { + for (j = 0; supported_actions[j]; j++) { + if (action == supported_actions[j]) { + supported_action = action; + break; + } + } + } else { +/* user did not specify */ + if (action == dnd->XdndActionCopy) + supported_action = action; + } + if (!supported_action) + return 0; + + *want_position = 1; + rectangle->x = rectangle->y = 0; + rectangle->width = rectangle->height = 0; + + info->return_action = *supported_action_return = supported_action; + info->return_type = *desired_type = supported_type; + info->x = x; + info->y = y; + + return 1; +} + +Atom xdnd_get_drop (Display * display, XEvent * xevent, Atom * typelist, Atom * actionlist, + unsigned char **data, int *length, Atom * type, int *x, int *y) +{ + Atom action = 0; + static int initialised = 0; + static DndClass dnd; + if (!initialised) { + xdnd_init (&dnd, display); + initialised = 1; + } + if (xevent->type != ClientMessage || xevent->xclient.message_type != dnd.XdndEnter) { + return 0; + } else { + struct xdnd_get_drop_info i; + +/* setup user structure */ + memset (&i, 0, sizeof (i)); + i.actionlist = actionlist; + i.typelist = typelist; + dnd.user_hook1 = &i; + +/* setup methods */ + dnd.widget_insert_drop = widget_insert_drop; + dnd.widget_apply_position = widget_apply_position; + +/* main loop */ + for (;;) { + xdnd_handle_drop_events (&dnd, xevent); + if (dnd.stage == XDND_DROP_STAGE_IDLE) + break; + XNextEvent (dnd.display, xevent); + } + +/* return results */ + if (i.drop_data) { + *length = i.drop_data_length; + *data = i.drop_data; + action = i.return_action; + *type = i.return_type; + *x = i.x; + *y = i.y; + } + } + return action; +} diff --git a/util/xdnd.h b/util/xdnd.h new file mode 100644 index 00000000..53a2a8fb --- /dev/null +++ b/util/xdnd.h @@ -0,0 +1,219 @@ +/* xdnd.c, xdnd.h - C program library for handling the Xdnd protocol + Copyright (C) 1996-2000 Paul Sheer + + 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., 59 Temple Place, Suite 330, Boston, MA + 02111-1307, USA. + */ + +#ifndef _X_DND_H +#define _X_DND_H + +#ifdef __cplusplus +extern "C" { +#endif + +/* you can set this to either 2 (which support 0 and 1 as well) or 3 */ +/* #define XDND_VERSION 2 */ + +#define XDND_VERSION 3 + +/* XdndEnter */ +#define XDND_THREE 3 +#define XDND_ENTER_SOURCE_WIN(e) ((e)->xclient.data.l[0]) +#define XDND_ENTER_THREE_TYPES(e) (((e)->xclient.data.l[1] & 0x1UL) == 0) +#define XDND_ENTER_THREE_TYPES_SET(e,b) (e)->xclient.data.l[1] = ((e)->xclient.data.l[1] & ~0x1UL) | (((b) == 0) ? 0 : 0x1UL) +#define XDND_ENTER_VERSION(e) ((e)->xclient.data.l[1] >> 24) +#define XDND_ENTER_VERSION_SET(e,v) (e)->xclient.data.l[1] = ((e)->xclient.data.l[1] & ~(0xFF << 24)) | ((v) << 24) +#define XDND_ENTER_TYPE(e,i) ((e)->xclient.data.l[2 + i]) /* i => (0, 1, 2) */ + +/* XdndPosition */ +#define XDND_POSITION_SOURCE_WIN(e) ((e)->xclient.data.l[0]) +#define XDND_POSITION_ROOT_X(e) ((e)->xclient.data.l[2] >> 16) +#define XDND_POSITION_ROOT_Y(e) ((e)->xclient.data.l[2] & 0xFFFFUL) +#define XDND_POSITION_ROOT_SET(e,x,y) (e)->xclient.data.l[2] = ((x) << 16) | ((y) & 0xFFFFUL) +#define XDND_POSITION_TIME(e) ((e)->xclient.data.l[3]) +#define XDND_POSITION_ACTION(e) ((e)->xclient.data.l[4]) + +/* XdndStatus */ +#define XDND_STATUS_TARGET_WIN(e) ((e)->xclient.data.l[0]) +#define XDND_STATUS_WILL_ACCEPT(e) ((e)->xclient.data.l[1] & 0x1L) +#define XDND_STATUS_WILL_ACCEPT_SET(e,b) (e)->xclient.data.l[1] = ((e)->xclient.data.l[1] & ~0x1UL) | (((b) == 0) ? 0 : 0x1UL) +#define XDND_STATUS_WANT_POSITION(e) ((e)->xclient.data.l[1] & 0x2UL) +#define XDND_STATUS_WANT_POSITION_SET(e,b) (e)->xclient.data.l[1] = ((e)->xclient.data.l[1] & ~0x2UL) | (((b) == 0) ? 0 : 0x2UL) +#define XDND_STATUS_RECT_X(e) ((e)->xclient.data.l[2] >> 16) +#define XDND_STATUS_RECT_Y(e) ((e)->xclient.data.l[2] & 0xFFFFL) +#define XDND_STATUS_RECT_WIDTH(e) ((e)->xclient.data.l[3] >> 16) +#define XDND_STATUS_RECT_HEIGHT(e) ((e)->xclient.data.l[3] & 0xFFFFL) +#define XDND_STATUS_RECT_SET(e,x,y,w,h) {(e)->xclient.data.l[2] = ((x) << 16) | ((y) & 0xFFFFUL); (e)->xclient.data.l[3] = ((w) << 16) | ((h) & 0xFFFFUL); } +#define XDND_STATUS_ACTION(e) ((e)->xclient.data.l[4]) + +/* XdndLeave */ +#define XDND_LEAVE_SOURCE_WIN(e) ((e)->xclient.data.l[0]) + +/* XdndDrop */ +#define XDND_DROP_SOURCE_WIN(e) ((e)->xclient.data.l[0]) +#define XDND_DROP_TIME(e) ((e)->xclient.data.l[2]) + +/* XdndFinished */ +#define XDND_FINISHED_TARGET_WIN(e) ((e)->xclient.data.l[0]) + +struct _DndCursor { + int width, height; + int x, y; + unsigned char *image_data, *mask_data; + char *_action; + Pixmap image_pixmap, mask_pixmap; + Cursor cursor; + Atom action; +}; + +typedef struct _DndCursor DndCursor; +typedef struct _DndClass DndClass; + +struct _DndClass { +/* insert chars sequentionally into the target widget, type will be the same as `desired_type' + returned from widget_apply_position. This may be called several times in succession + with sequention blocks of data. Must return non-zero on failure */ + int (*widget_insert_drop) (DndClass * dnd, unsigned char *data, int length, int remaining, Window into, Window from, Atom type); + +/* In response to DELETE requests : FIXME - not yet used */ + int (*widget_delete_selection) (DndClass * dnd, Window window, Window from); + +/* returns 1 if widget exists, zero otherwise. If this method is not + set then the code assumes that no widgets have support for recieving drops. + In this case none of the widget methods need be set. */ + int (*widget_exists) (DndClass * dnd, Window window); + +/* must update the widgets border to its default appearance */ + void (*widget_apply_leave) (DndClass * dnd, Window widgets_window); + +/* must update the widgets border to give the appearance of being able to recieve a drop, + plus return all data to pointers. As per the protocol, if the widget cannot + perform the action specified by `action' then it should return either XdndActionPrivate + or XdndActionCopy into supported_action (leaving 0 supported_action unchanged is equivalent + to XdndActionCopy). Returns 1 if ready to ok drop */ + int (*widget_apply_position) (DndClass * dnd, Window widgets_window, Window from, + Atom action, int x, int y, Time t, Atom * typelist, + int *want_position, Atom * supported_action, Atom * desired_type, + XRectangle * rectangle); + +/* returns drag data of the specified type. This will be one of `typelist' given to xdnd_drag */ + void (*widget_get_data) (DndClass * dnd, Window window, unsigned char **data, int *length, Atom type); + +/* this is called from with the main event loop if an expose event is recieved and is optional */ + void (*handle_expose_events) (DndClass * dnd, XEvent * xevent); + +/* creates a chooser dialog if the action is XdndActionAsk. Returns non-zero on cancel */ + int (*action_choose_dialog) (DndClass * dnd, char **descriptions, Atom * actions, Atom * result); + +#if 0 /* implemented internally */ +/* returns a widget that is dnd aware within a parent widget that lies under the point x, y */ + Window (*widget_get_child_widget) (DndClass * dnd, Window parent, int x, int y); +#endif + + void *pad1[8]; + + DndCursor *cursors; + + Display *display; + + Atom XdndAware; + Atom XdndSelection; + Atom XdndEnter; + Atom XdndLeave; + Atom XdndPosition; + Atom XdndDrop; + Atom XdndFinished; + Atom XdndStatus; + Atom XdndActionCopy; + Atom XdndActionMove; + Atom XdndActionLink; + Atom XdndActionAsk; + Atom XdndActionPrivate; + Atom XdndTypeList; + Atom XdndActionList; + Atom XdndActionDescription; + + Atom Xdnd_NON_PROTOCOL_ATOM; + Atom version; + + Atom pad2[16]; + + Window root_window; + +#define XDND_DROP_STAGE_IDLE 0 +#define XDND_DRAG_STAGE_DRAGGING 1 +#define XDND_DRAG_STAGE_ENTERED 2 +#define XDND_DROP_STAGE_CONVERTING 3 +#define XDND_DROP_STAGE_ENTERED 4 + int stage; + int dragging_version; + int internal_drag; + int want_position; + int ready_to_drop; + int will_accept; + XRectangle rectangle; + Window dropper_window, dragger_window; + Atom *dragger_typelist; + Atom desired_type; + Atom supported_action; + Time time; +/* drop position from last XdndPosition */ + int x, y; + int pad3[16]; + +/* move euclidian pixels before considering this to be an actual drag */ + float drag_threshold; + +/* block for only this many seconds on not receiving a XdndFinished from target, default : 10 */ + int time_out; + +#define XDND_OPTION_NO_HYSTERESIS (1<<0) + int options; + +/* user hooks */ + void *user_hook1; + void *user_hook2; + void *user_hook3; + Window dropper_toplevel; + void *pad4[15]; +}; + + +void xdnd_init (DndClass * dnd, Display * display); +void xdnd_shut (DndClass * dnd); +/* for nested widgets where parent and child receive drops of different +types; then always pass typelist as null */ +void xdnd_set_dnd_aware (DndClass * dnd, Window window, Atom * typelist); +int xdnd_is_dnd_aware (DndClass * dnd, Window window, int *version, Atom * typelist); +void xdnd_set_type_list (DndClass * dnd, Window window, Atom * typelist); +void xdnd_set_actions (DndClass * dnd, Window window, Atom * actions, char **descriptions); +int xdnd_get_actions (DndClass * dnd, Window window, Atom ** actions, char ***descriptions); +int xdnd_choose_action_dialog (DndClass * dnd, Atom * actions, char **descriptions, Atom * result); +Atom xdnd_drag (DndClass * dnd, Window from, Atom action, Atom * typelist); + +/* Returns 1 if event is handled, This must be placed in the widget +libraries main event loop and be called if the event type is +ClientMessage or SelectionNotify */ +int xdnd_handle_drop_events (DndClass * dnd, XEvent * xevent); +Atom xdnd_get_drop (Display * display, XEvent * xevent, Atom * typelist, Atom * actionlist, + unsigned char **data, int *length, Atom * type, int *x, int *y); + + +#ifdef __cplusplus +} +#endif + +#endif /* !_X_DND_H */