From 6ff478e0306a38d8847141e17d65ced917f0e31c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jiao=20Sun=20=28=E5=AD=99=E5=A8=87=29?=
Date: Wed, 23 Sep 2026 22:11:22 +0100
Subject: [PATCH 1/2] Fix linux GTK drop failure
---
packages/desktop_drop/lib/src/channel.dart | 2 +-
.../desktop_drop/linux/desktop_drop_plugin.cc | 261 +++++++++++++++---
2 files changed, 216 insertions(+), 47 deletions(-)
diff --git a/packages/desktop_drop/lib/src/channel.dart b/packages/desktop_drop/lib/src/channel.dart
index efe331b0..968d4a7b 100644
--- a/packages/desktop_drop/lib/src/channel.dart
+++ b/packages/desktop_drop/lib/src/channel.dart
@@ -157,7 +157,7 @@ class DesktopDrop {
_notifyEvent(DropDoneEvent(
location: Offset(offset[0], offset[1]),
files: paths.map((e) => DropItemFile(e)).toList(),
- rawText: text,
+ rawText: call.arguments.length > 2 ? call.arguments[2] as String : text,
));
break;
case "performOperation_portal":
diff --git a/packages/desktop_drop/linux/desktop_drop_plugin.cc b/packages/desktop_drop/linux/desktop_drop_plugin.cc
index d706bd6a..f4ad753b 100644
--- a/packages/desktop_drop/linux/desktop_drop_plugin.cc
+++ b/packages/desktop_drop/linux/desktop_drop_plugin.cc
@@ -3,14 +3,14 @@
#include
#include
#include
-#include
+#include
#define DESKTOP_DROP_PLUGIN(obj) \
(G_TYPE_CHECK_INSTANCE_CAST((obj), desktop_drop_plugin_get_type(), \
DesktopDropPlugin))
struct _DesktopDropPlugin {
- GObject parent_instance;
+ GObject parent_instance;
};
static gboolean isKDE = FALSE;
@@ -18,41 +18,215 @@ static gboolean ignoreNext = FALSE;
G_DEFINE_TYPE(DesktopDropPlugin, desktop_drop_plugin, g_object_get_type())
-void on_drag_data_received(GtkWidget *widget, GdkDragContext *drag_context,
- gint x, gint y, GtkSelectionData *sdata, guint info,
- guint time, gpointer user_data) {
- auto *channel = static_cast(user_data);
- const gchar *method_name = "performOperation_linux";
-
- // The portal target carries a one-time transfer key instead of URIs.
- // Send it through its own method so Dart knows what it received.
- g_autofree gchar *target_name = gdk_atom_name(gtk_selection_data_get_target(sdata));
- if (target_name != nullptr &&
- strcmp(target_name, "application/vnd.portal.filetransfer") == 0) {
- method_name = "performOperation_portal";
- }
+namespace {
+ constexpr char kPortalTarget[] = "application/vnd.portal.filetransfer";
+ constexpr char kUriTarget[] = "text/uri-list";
+ constexpr char kDropPosition[] = "renamer-desktop-drop-position";
+
+ struct DropPosition {
+ gint x;
+ gint y;
+ guint time;
+ };
+
+// Own everything used by the asynchronous D-Bus callbacks. Selection data and
+// the signal arguments themselves are only borrowed for the signal's duration.
+ struct PendingDrop {
+ GtkWidget *widget;
+ GdkDragContext *context;
+ FlMethodChannel *channel;
+ DropPosition position;
+ std::string key;
+
+ PendingDrop(GtkWidget *widget, GdkDragContext *context,
+ FlMethodChannel *channel, DropPosition position)
+ : widget(static_cast(g_object_ref(widget))),
+ context(static_cast(g_object_ref(context))),
+ channel(static_cast(g_object_ref(channel))),
+ position(position) {}
+
+ ~PendingDrop() {
+ g_object_unref(channel);
+ g_object_unref(context);
+ g_object_unref(widget);
+ }
+ };
+
+ void finish_drop(PendingDrop *drop, gboolean success) {
+ g_object_set_data(G_OBJECT(drop->context), kDropPosition, nullptr);
+ // The plugin advertises COPY, never a request to delete the source files.
+ gtk_drag_finish(drop->context, success, FALSE, drop->position.time);
+ delete drop;
+ }
+
+ void deliver_uris(PendingDrop *drop, const gchar *text) {
+ // Do not let comments, empty selections or a portal key become file paths.
+ g_auto(GStrv) uris = g_uri_list_extract_uris(text);
+ g_autoptr(GString) valid_uris = g_string_new(nullptr);
+ for (gchar **uri = uris; uri && *uri; ++uri) {
+ g_autofree gchar *scheme = g_uri_parse_scheme(*uri);
+ if (scheme) {
+ g_string_append(valid_uris, *uri);
+ g_string_append(valid_uris, "\r\n");
+ }
+ }
+ if (valid_uris->len == 0) {
+ finish_drop(drop, FALSE);
+ return;
+ }
+
+ double point[] = {double(drop->position.x), double(drop->position.y)};
+ g_autoptr(FlValue) args = fl_value_new_list();
+ fl_value_append_take(args, fl_value_new_string(valid_uris->str));
+ fl_value_append_take(args, fl_value_new_float_list(point, 2));
+ // Preserve rawText's public contract even though paths are now resolved here.
+ fl_value_append_take(
+ args, fl_value_new_string(drop->key.empty() ? text : drop->key.c_str()));
+ // Reuse desktop_drop's URI decoding and DropDoneEvent, including coordinates.
+ // Dart must not retrieve the one-time portal key a second time.
+ fl_method_channel_invoke_method(drop->channel, "performOperation_linux", args,
+ nullptr, nullptr, nullptr);
+ finish_drop(drop, TRUE);
+ }
+
+ void fallback_to_uris(PendingDrop *drop) {
+ GdkAtom target = gdk_atom_intern_static_string(kUriTarget);
+ if (g_list_find(gdk_drag_context_list_targets(drop->context), target)) {
+ // Keep the drag alive until the fallback selection has been received.
+ gtk_drag_get_data(drop->widget, drop->context, target, drop->position.time);
+ delete drop;
+ } else {
+ finish_drop(drop, FALSE);
+ }
+ }
+
+ void portal_files_received(GObject *source, GAsyncResult *result,
+ gpointer user_data) {
+ auto *drop = static_cast(user_data);
+ g_autoptr(GError) error = nullptr;
+ g_autoptr(GVariant) reply =
+ g_dbus_connection_call_finish(G_DBUS_CONNECTION(source), result, &error);
+ if (!reply) {
+ g_message("desktop_drop: portal transfer failed, trying URI list: %s",
+ error->message);
+ fallback_to_uris(drop);
+ return;
+ }
+
+ g_auto(GStrv) paths = nullptr;
+ g_variant_get(reply, "(^as)", &paths);
+ g_autoptr(GString) uris = g_string_new(nullptr);
+ for (gchar **path = paths; path && *path; ++path) {
+ g_autofree gchar *uri = g_filename_to_uri(*path, nullptr, nullptr);
+ if (uri) {
+ g_string_append(uris, uri);
+ g_string_append(uris, "\r\n");
+ }
+ }
+ if (uris->len == 0) {
+ fallback_to_uris(drop);
+ return;
+ }
+ deliver_uris(drop, uris->str);
+ }
+
+ void portal_bus_ready(GObject *source, GAsyncResult *result,
+ gpointer user_data) {
+ auto *drop = static_cast(user_data);
+ g_autoptr(GError) error = nullptr;
+ g_autoptr(GDBusConnection) bus = g_bus_get_finish(result, &error);
+ if (!bus) {
+ fallback_to_uris(drop);
+ return;
+ }
+ g_dbus_connection_call(
+ bus, "org.freedesktop.portal.Documents",
+ "/org/freedesktop/portal/documents",
+ "org.freedesktop.portal.FileTransfer", "RetrieveFiles",
+ g_variant_new("(s@a{sv})", drop->key.c_str(),
+ g_variant_new_array(G_VARIANT_TYPE("{sv}"), nullptr, 0)),
+ G_VARIANT_TYPE("(as)"), G_DBUS_CALL_FLAGS_NONE, 5000, nullptr,
+ portal_files_received, drop);
+ }
+
+ void receive_drop(PendingDrop *drop, GdkAtom target, const guchar *data,
+ gint length) {
+ const bool portal = target == gdk_atom_intern_static_string(kPortalTarget);
+ if (length <= 0 || !data) {
+ if (portal) {
+ fallback_to_uris(drop);
+ } else {
+ finish_drop(drop, FALSE);
+ }
+ return;
+ }
+ // GTK does not promise a trailing NUL. Copy only the supplied byte length.
+ g_autofree gchar *payload =
+ g_strndup(reinterpret_cast(data), length);
+ if (!g_utf8_validate(payload, -1, nullptr) || payload[0] == '\0') {
+ if (portal) {
+ fallback_to_uris(drop);
+ } else {
+ finish_drop(drop, FALSE);
+ }
+ return;
+ }
+ if (portal) {
+ drop->key = payload;
+ g_bus_get(G_BUS_TYPE_SESSION, nullptr, portal_bus_ready, drop);
+ } else {
+ deliver_uris(drop, payload);
+ }
+ }
+
+ void configure_drag_destination(GtkWidget *widget) {
+ static GtkTargetEntry entries[] = {
+ {const_cast(kPortalTarget), GTK_TARGET_OTHER_APP, 0},
+ {const_cast(kUriTarget), GTK_TARGET_OTHER_APP, 0},
+ {const_cast("STRING"), GTK_TARGET_OTHER_APP, 0},
+ };
+ // GTK_DEST_DEFAULT_DROP would finish the drag as soon as the selection
+ // callback returns, invalidating portal keys before RetrieveFiles completes.
+ gtk_drag_dest_set(widget,
+ static_cast(GTK_DEST_DEFAULT_MOTION |
+ GTK_DEST_DEFAULT_HIGHLIGHT),
+ entries, G_N_ELEMENTS(entries), GDK_ACTION_COPY);
+ }
+
+} // namespace
- // Selection data is not guaranteed to be NUL-terminated.
- gint length = gtk_selection_data_get_length(sdata);
- if (length < 0) {
+void on_drag_data_received(GtkWidget *widget, GdkDragContext *context,
+ gint x, gint y, GtkSelectionData *data, guint info,
+ guint time, gpointer user_data) {
+ auto *position = static_cast(
+ g_object_get_data(G_OBJECT(context), kDropPosition));
+ // Ignore data requested for hover inspection by another handler.
+ if (!position)
return;
- }
- g_autofree gchar *payload = g_strndup(
- reinterpret_cast(gtk_selection_data_get_data(sdata)), length);
+ auto *channel = static_cast(user_data);
+ receive_drop(new PendingDrop(widget, context, channel, *position),
+ gtk_selection_data_get_target(data),
+ gtk_selection_data_get_data(data),
+ gtk_selection_data_get_length(data));
+}
- double point[] = {double(x), double(y)};
- g_autoptr(FlValue) args = fl_value_new_list();
- fl_value_append(args, fl_value_new_string(payload));
- fl_value_append(args, fl_value_new_float_list(point, 2));
- fl_method_channel_invoke_method(channel, method_name, args,
- nullptr, nullptr, nullptr);
+gboolean on_drag_drop(GtkWidget *widget, GdkDragContext *context, gint x,
+ gint y, guint time, gpointer user_data) {
+ GdkAtom target = gtk_drag_dest_find_target(widget, context, nullptr);
+ if (target == GDK_NONE)
+ return FALSE;
+ g_object_set_data_full(
+ G_OBJECT(context), kDropPosition, new DropPosition{x, y, time},
+ [](gpointer data) { delete static_cast(data); });
+ gtk_drag_get_data(widget, context, target, time);
+ return TRUE;
}
-void on_drag_motion(GtkWidget *widget, GdkDragContext *drag_context,
- gint x, gint y, guint time, gpointer user_data) {
+gboolean on_drag_motion(GtkWidget *widget, GdkDragContext *drag_context,
+ gint x, gint y, guint time, gpointer user_data) {
if (ignoreNext) {
ignoreNext = FALSE;
- return;
+ return FALSE;
}
auto *channel = static_cast(user_data);
@@ -60,6 +234,7 @@ void on_drag_motion(GtkWidget *widget, GdkDragContext *drag_context,
g_autoptr(FlValue) args = fl_value_new_float_list(point, 2);
fl_method_channel_invoke_method(channel, "updated", args,
nullptr, nullptr, nullptr);
+ return FALSE;
}
void on_drag_leave(GtkWidget *widget, GdkDragContext *drag_context, guint time, gpointer user_data) {
@@ -70,8 +245,8 @@ void on_drag_leave(GtkWidget *widget, GdkDragContext *drag_context, guint time,
// Called when a method call is received from Flutter.
static void desktop_drop_plugin_handle_method_call(
- DesktopDropPlugin *self,
- FlMethodCall *method_call) {
+ DesktopDropPlugin *self,
+ FlMethodCall *method_call) {
fl_method_call_respond_not_implemented(method_call, nullptr);
}
@@ -86,10 +261,10 @@ static void desktop_drop_plugin_class_init(DesktopDropPluginClass *klass) {
static void desktop_drop_plugin_init(DesktopDropPlugin *self) {
const char * desktopEnv = getenv("XDG_CURRENT_DESKTOP");
if (desktopEnv) {
- const char * lowercaseDesktopEnv = g_ascii_strdown(desktopEnv, -1);
+ g_autofree gchar * lowercaseDesktopEnv = g_ascii_strdown(desktopEnv, -1);
if (strcmp(lowercaseDesktopEnv, "kde") == 0 || strcmp(lowercaseDesktopEnv, "plasma") == 0) {
- isKDE = TRUE;
+ isKDE = TRUE;
}
}
}
@@ -109,27 +284,21 @@ static void method_call_cb(FlMethodChannel *channel, FlMethodCall *method_call,
void desktop_drop_plugin_register_with_registrar(FlPluginRegistrar *registrar) {
DesktopDropPlugin *plugin = DESKTOP_DROP_PLUGIN(
- g_object_new(desktop_drop_plugin_get_type(), nullptr));
+ g_object_new(desktop_drop_plugin_get_type(), nullptr));
auto *fl_view = fl_plugin_registrar_get_view(registrar);
- // Register portal file transfer target FIRST (highest priority)
- // then STRING, then URI targets
- static GtkTargetEntry entries[] = {
- {strdup("application/vnd.portal.filetransfer"), GTK_TARGET_OTHER_APP, 0},
- {strdup("STRING"), GTK_TARGET_OTHER_APP, 0}
- };
- gtk_drag_dest_set(GTK_WIDGET(fl_view), GTK_DEST_DEFAULT_ALL, entries, 2, GDK_ACTION_COPY);
- gtk_drag_dest_add_uri_targets(GTK_WIDGET(fl_view));
+ configure_drag_destination(GTK_WIDGET(fl_view));
g_autoptr(FlStandardMethodCodec) codec = fl_standard_method_codec_new();
FlMethodChannel *channel =
- fl_method_channel_new(fl_plugin_registrar_get_messenger(registrar),
- "desktop_drop",
- FL_METHOD_CODEC(codec));
+ fl_method_channel_new(fl_plugin_registrar_get_messenger(registrar),
+ "desktop_drop",
+ FL_METHOD_CODEC(codec));
fl_method_channel_set_method_call_handler(channel, method_call_cb,
g_object_ref(plugin),
g_object_unref);
+ g_signal_connect(fl_view, "drag-drop", G_CALLBACK(on_drag_drop), channel);
g_signal_connect(fl_view, "drag-motion",
G_CALLBACK(on_drag_motion), channel);
g_signal_connect(GTK_WIDGET(fl_view), "drag-data-received",
From 3cd2cdf8cb50b564ebe27ce35931d003e3bc9a49 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jiao=20Sun=20=28=E5=AD=99=E5=A8=87=29?=
Date: Wed, 23 Sep 2026 22:45:41 +0100
Subject: [PATCH 2/2] File formating
---
packages/desktop_drop/lib/src/channel.dart | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/packages/desktop_drop/lib/src/channel.dart b/packages/desktop_drop/lib/src/channel.dart
index 968d4a7b..3157a1a2 100644
--- a/packages/desktop_drop/lib/src/channel.dart
+++ b/packages/desktop_drop/lib/src/channel.dart
@@ -157,7 +157,8 @@ class DesktopDrop {
_notifyEvent(DropDoneEvent(
location: Offset(offset[0], offset[1]),
files: paths.map((e) => DropItemFile(e)).toList(),
- rawText: call.arguments.length > 2 ? call.arguments[2] as String : text,
+ rawText:
+ call.arguments.length > 2 ? call.arguments[2] as String : text,
));
break;
case "performOperation_portal":