Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions src/fe-gtk/hex-input-edit.c
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ enum {
SIGNAL_ACTIVATE,
SIGNAL_WORD_CHECK,
SIGNAL_IMAGE_PASTE,
SIGNAL_FILE_PASTE,
LAST_SIGNAL
};

Expand Down Expand Up @@ -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)
{
Expand All @@ -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);
Expand Down Expand Up @@ -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);
}

/* =============================== */
Expand Down
1 change: 1 addition & 0 deletions src/fe-gtk/hex-input-edit.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
157 changes: 120 additions & 37 deletions src/fe-gtk/maingui.c
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
}

Expand Down Expand Up @@ -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));
Expand All @@ -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)
Expand Down
Loading