diff --git a/src/fe-gtk/hex-input-edit.c b/src/fe-gtk/hex-input-edit.c index 7cb6f6fa..3dcaa0d0 100644 --- a/src/fe-gtk/hex-input-edit.c +++ b/src/fe-gtk/hex-input-edit.c @@ -162,6 +162,7 @@ enum { SIGNAL_ACTIVATE, SIGNAL_WORD_CHECK, SIGNAL_IMAGE_PASTE, + SIGNAL_FILE_PASTE, LAST_SIGNAL }; @@ -1101,6 +1102,36 @@ paste_texture_ready_cb (GObject *source, GAsyncResult *result, gpointer data) g_object_unref (edit); } +/* Async file-list paste callback: offer the copied files to the embedder via + * "file-paste" (which uploads any images). If the embedder doesn't handle + * them (no images among them), fall back to pasting the clipboard as text so + * e.g. a copied file's path still pastes where the source app offers one. */ +static void +paste_file_list_ready_cb (GObject *source, GAsyncResult *result, gpointer data) +{ + HexInputEdit *edit = HEX_INPUT_EDIT (data); + GdkClipboard *clip = GDK_CLIPBOARD (source); + const GValue *value = gdk_clipboard_read_value_finish (clip, result, NULL); + gboolean handled = FALSE; + + if (value && G_VALUE_HOLDS (value, GDK_TYPE_FILE_LIST)) + { + GdkFileList *files = g_value_get_boxed (value); + if (files) + g_signal_emit (edit, signals[SIGNAL_FILE_PASTE], 0, files, + &handled); + } + + if (!handled) + { + /* transfers our ref on `edit` to the text callback */ + gdk_clipboard_read_text_async (clip, NULL, + paste_text_ready_cb, edit); + return; + } + g_object_unref (edit); +} + static void do_paste (HexInputEdit *edit, GdkClipboard *clip) { @@ -1117,6 +1148,20 @@ do_paste (HexInputEdit *edit, GdkClipboard *clip) return; } + /* Copied files (Explorer Ctrl+C arrives as CFSTR_SHELLIDLIST, which GDK + * advertises as text/uri-list and deserializes to a GdkFileList; Linux + * file managers offer text/uri-list directly). Offer them to the + * embedder; it falls back to text if none are images. */ + if (prefs.hex_url_image_upload_enable && formats && + gdk_content_formats_contain_gtype (formats, GDK_TYPE_FILE_LIST)) + { + g_object_ref (edit); + gdk_clipboard_read_value_async (clip, GDK_TYPE_FILE_LIST, + G_PRIORITY_DEFAULT, NULL, + paste_file_list_ready_cb, edit); + return; + } + g_object_ref (edit); gdk_clipboard_read_text_async (clip, NULL, paste_text_ready_cb, edit); @@ -2770,6 +2815,18 @@ hex_input_edit_class_init (HexInputEditClass *klass) NULL, NULL, g_cclosure_marshal_generic, G_TYPE_NONE, 1, G_TYPE_BYTES); + + /* Emitted when the user pastes copied files; the argument is a + * GdkFileList. Return TRUE if handled (an upload started) — otherwise + * the widget falls back to pasting the clipboard as text. */ + signals[SIGNAL_FILE_PASTE] = g_signal_new ( + "file-paste", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (HexInputEditClass, file_paste), + g_signal_accumulator_true_handled, NULL, + g_cclosure_marshal_generic, + G_TYPE_BOOLEAN, 1, GDK_TYPE_FILE_LIST); } /* =============================== */ diff --git a/src/fe-gtk/hex-input-edit.h b/src/fe-gtk/hex-input-edit.h index 3e875b27..b871c3f4 100644 --- a/src/fe-gtk/hex-input-edit.h +++ b/src/fe-gtk/hex-input-edit.h @@ -55,6 +55,7 @@ struct _HexInputEditClass void (*activate) (HexInputEdit *edit); gboolean (*word_check) (HexInputEdit *edit, const char *word); void (*image_paste) (HexInputEdit *edit, GBytes *png_bytes); + gboolean (*file_paste) (HexInputEdit *edit, GdkFileList *files); }; GType hex_input_edit_get_type (void) G_GNUC_CONST; diff --git a/src/fe-gtk/maingui.c b/src/fe-gtk/maingui.c index 3bfdc2a2..1dce3a29 100644 --- a/src/fe-gtk/maingui.c +++ b/src/fe-gtk/maingui.c @@ -4732,6 +4732,108 @@ mg_input_image_paste (HexInputEdit *entry, GBytes *png_bytes, gpointer user_data mg_upload_image (current_sess, png_bytes, "pasted.png"); } +/* Is `file` an image? Checks the GIO content type's MIME form, then falls + * back to the extension. The MIME conversion matters on Windows: Win32 GIO + * content types are registry extension strings (".png", not "image/png"), + * so a bare "image/" prefix test on the raw type never matches there, and + * the registry frequently has no "Content Type" for newer formats (.webp), + * hence the extension fallback. */ +static gboolean +mg_file_is_image (GFile *file) +{ + static const char *const image_exts[] = { + ".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp", ".svg", + ".avif", ".heic", ".heif", ".tif", ".tiff", ".ico" + }; + GFileInfo *info; + gboolean is_image = FALSE; + char *basename; + gsize i; + + info = g_file_query_info (file, G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE, + G_FILE_QUERY_INFO_NONE, NULL, NULL); + if (info) + { + const char *ct = g_file_info_get_content_type (info); + if (ct) + { + char *mime = g_content_type_get_mime_type (ct); + if ((mime && g_str_has_prefix (mime, "image/")) || + g_str_has_prefix (ct, "image/")) + is_image = TRUE; + g_free (mime); + } + g_object_unref (info); + } + if (is_image) + return TRUE; + + basename = g_file_get_basename (file); + if (basename) + { + const char *dot = strrchr (basename, '.'); + if (dot) + for (i = 0; i < G_N_ELEMENTS (image_exts); i++) + if (g_ascii_strcasecmp (dot, image_exts[i]) == 0) + { + is_image = TRUE; + break; + } + g_free (basename); + } + return is_image; +} + +/* Upload one dropped file if it's an image. Returns FALSE (without side + * effects) for non-images so the caller can leave the drop unhandled. */ +static gboolean +mg_input_upload_image_file (GFile *file) +{ + char *contents = NULL, *basename; + gsize len = 0; + GBytes *bytes; + + if (!file || !mg_file_is_image (file)) + return FALSE; + + if (!g_file_load_contents (file, NULL, &contents, &len, NULL, NULL)) + return FALSE; + if (len == 0 || len > IMAGE_UPLOAD_MAX_SIZE) + { + g_free (contents); + return FALSE; + } + + basename = g_file_get_basename (file); + bytes = g_bytes_new_take (contents, len); /* takes ownership */ + mg_upload_image (current_sess, bytes, basename); + g_bytes_unref (bytes); + g_free (basename); + return TRUE; +} + +/* "file-paste" from the input widget: files were pasted (e.g. Explorer / + * file-manager Ctrl+C on image files). Upload any images; returning FALSE + * when none are lets the widget fall back to a text paste. */ +static gboolean +mg_input_file_paste (HexInputEdit *entry, GdkFileList *files, gpointer user_data) +{ + GSList *list, *l; + gboolean any = FALSE; + + (void) entry; (void) user_data; + + if (!prefs.hex_url_image_upload_enable) + return FALSE; + + list = gdk_file_list_get_files (files); + for (l = list; l; l = l->next) + if (mg_input_upload_image_file (l->data)) + any = TRUE; + g_slist_free (list); + return any; +} + /* Drop target on the input box: upload dropped image textures and image files; * let everything else fall through to default handling. */ static gboolean @@ -4758,46 +4860,25 @@ mg_input_image_drop_cb (GtkDropTarget *target, const GValue *value, return TRUE; } - if (G_VALUE_HOLDS (value, G_TYPE_FILE)) + /* Windows shell drags (CF_HDROP) arrive as a GdkFileList — a target + * typed only G_TYPE_FILE never matches the offer, so Explorer drags + * saw no drop target at all (same gap as the DCC targets had). */ + if (G_VALUE_HOLDS (value, GDK_TYPE_FILE_LIST)) { - GFile *file = g_value_get_object (value); - GFileInfo *info; - char *contents = NULL, *basename; - gsize len = 0; - gboolean is_image = FALSE; - GBytes *bytes; - - if (!file) - return FALSE; - - info = g_file_query_info (file, G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE, - G_FILE_QUERY_INFO_NONE, NULL, NULL); - if (info) - { - const char *ct = g_file_info_get_content_type (info); - if (ct && g_str_has_prefix (ct, "image/")) - is_image = TRUE; - g_object_unref (info); - } - if (!is_image) - return FALSE; /* not an image — leave to existing behavior */ - - if (!g_file_load_contents (file, NULL, &contents, &len, NULL, NULL)) - return FALSE; - if (len == 0 || len > IMAGE_UPLOAD_MAX_SIZE) - { - g_free (contents); - return FALSE; - } + GSList *files = gdk_file_list_get_files (g_value_get_boxed (value)); + GSList *l; + gboolean any = FALSE; - basename = g_file_get_basename (file); - bytes = g_bytes_new_take (contents, len); /* takes ownership */ - mg_upload_image (current_sess, bytes, basename); - g_bytes_unref (bytes); - g_free (basename); - return TRUE; + for (l = files; l; l = l->next) + if (mg_input_upload_image_file (l->data)) + any = TRUE; + g_slist_free (files); + return any; } + if (G_VALUE_HOLDS (value, G_TYPE_FILE)) + return mg_input_upload_image_file (g_value_get_object (value)); + return FALSE; } @@ -4892,7 +4973,7 @@ mg_create_entry (session *sess, GtkWidget *box) * "image-paste" signal below. */ { GtkDropTarget *drop; - GType drop_types[] = { GDK_TYPE_TEXTURE, G_TYPE_FILE }; + GType drop_types[] = { GDK_TYPE_TEXTURE, GDK_TYPE_FILE_LIST, G_TYPE_FILE }; drop = gtk_drop_target_new (G_TYPE_INVALID, GDK_ACTION_COPY); gtk_drop_target_set_gtypes (drop, drop_types, G_N_ELEMENTS (drop_types)); @@ -4901,6 +4982,8 @@ mg_create_entry (session *sess, GtkWidget *box) } g_signal_connect (entry, "image-paste", G_CALLBACK (mg_input_image_paste), NULL); + g_signal_connect (entry, "file-paste", + G_CALLBACK (mg_input_file_paste), NULL); /* Share xtext's emoji cache and palette with the input box */ if (gui->xtext && GTK_XTEXT (gui->xtext)->emoji_cache)