Problem
I recently included drag and drop in my GTK4/Libadwaita/GNOME 50 application and it worked. Now, I suddenly get the following error:
Failed to read dropped files: Could not convert data from application/vnd.portal.filetransfer to GdkFileList
My file manager is Nautilus (GNOME 50) version as well and, as far as I know, a native package, i.e., installed via apt. I also run the Python application natively, i.e., I have no flatpak packaging yet.
I’m really not sure how this error came to be, and it seems it has nothing to do with my code because it exists when I read the files from the Gdk.Drop.
One notable difference to my previous tests was that I now opened Nautilus from PyCharm via “Open in…”. The Nautilus window then showed the icon of PyCharm in the app overview as well. Only after closing Nautilus and waiting a minute or so, I could make it work again (when Nautilus showed the usual Nautilus icon again).
Question
Now, I’m worried that the drag and drop mechanism I implemented is not universal and may break on some systems. How can I improve it? Are there reference GNOME applications for how to implement drag and drop well?
Thanks for any help regarding this question.
Example
A minimal working example of my implementation:
#!/usr/bin/env python3
import gi
gi.require_version("Gtk", "4.0")
from gi.repository import Gdk, Gio, GLib, GObject, Gtk
class DropArea(Gtk.Overlay):
__gtype_name__ = "DropArea"
__gsignals__ = {
"files-dropped": (
GObject.SignalFlags.RUN_LAST,
None,
(Gdk.FileList,),
),
}
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.set_hexpand(True)
self.set_vexpand(True)
placeholder = Gtk.Label(
label="Drop files here",
hexpand=True,
vexpand=True,
)
placeholder.set_size_request(400, 250)
self.set_child(placeholder)
drop_label = Gtk.Label(label="Release to drop files")
self._drop_revealer = Gtk.Revealer(
child=drop_label,
transition_type=Gtk.RevealerTransitionType.CROSSFADE,
hexpand=True,
vexpand=True,
)
self._drop_revealer.set_halign(Gtk.Align.FILL)
self._drop_revealer.set_valign(Gtk.Align.FILL)
self.add_overlay(self._drop_revealer)
formats = Gdk.ContentFormats.new_for_gtype(Gdk.FileList)
target = Gtk.DropTargetAsync.new(
formats=formats,
actions=Gdk.DragAction.COPY,
)
target.connect("drag-enter", self._on_drop_enter)
target.connect("drag-leave", self._on_drop_leave)
target.connect("drop", self._on_drop)
self.add_controller(target)
def _on_drop_enter(
self,
_target: Gtk.DropTargetAsync,
drop: Gdk.Drop,
_x: float,
_y: float,
) -> Gdk.DragAction:
print("Offered formats:", drop.get_formats().to_string())
self._drop_revealer.set_reveal_child(True)
return drop.get_actions() & Gdk.DragAction.COPY
def _on_drop_leave(
self,
_target: Gtk.DropTargetAsync,
_drop: Gdk.Drop,
) -> None:
self._drop_revealer.set_reveal_child(False)
def _on_drop(
self,
_target: Gtk.DropTargetAsync,
drop: Gdk.Drop,
_x: float,
_y: float,
) -> bool:
drop.read_value_async(
type=Gdk.FileList,
io_priority=GLib.PRIORITY_DEFAULT,
cancellable=None,
callback=self._on_drop_data_ready,
)
return True
def _on_drop_data_ready(
self,
drop: Gdk.Drop,
result: Gio.AsyncResult,
) -> None:
try:
files = drop.read_value_finish(result)
except GLib.Error as error:
print(f"Failed to read dropped files: {error.message}")
drop.finish(Gdk.DragAction(0))
else:
self.emit("files-dropped", files)
drop.finish(Gdk.DragAction.COPY)
finally:
self._drop_revealer.set_reveal_child(False)
class Application(Gtk.Application):
def __init__(self):
super().__init__(
application_id="org.example.DragAndDrop",
flags=Gio.ApplicationFlags.DEFAULT_FLAGS,
)
def do_activate(self) -> None:
drop_area = DropArea()
drop_area.connect("files-dropped", self._on_files_dropped)
window = Gtk.ApplicationWindow(
application=self,
title="Drag and Drop Example",
default_width=600,
default_height=400,
child=drop_area,
)
window.present()
@staticmethod
def _on_files_dropped(
_drop_area: DropArea,
file_list: Gdk.FileList,
) -> None:
print("Dropped files:")
for file in file_list.get_files():
print(f" path: {file.get_path()}")
print(f" URI: {file.get_uri()}")
if __name__ == "__main__":
Application().run()
Output when dragging a folder onto it after having opened Nautilus through PyCharm:
Offered formats: GdkFileList GFile gchararray text/plain;charset=utf-8 text/uri-list application/vnd.portal.filetransfer application/vnd.portal.files
Failed to read dropped files: Could not convert data from application/vnd.portal.filetransfer to GdkFileList
And when I close Nautilus and wait a minute before re-opening:
Offered formats: GdkFileList GFile gchararray text/plain;charset=utf-8 text/uri-list application/vnd.portal.filetransfer application/vnd.portal.files
Dropped files:
path: /home/user/Pictures
URI: file:///home/user/Pictures
Edit
It looks like these can be read if I add the offered formats to the target’s formats:
g_formats = Gdk.ContentFormats.new_for_gtype(Gdk.FileList)
m_formats = Gdk.ContentFormats.new(["text/plain;charset=utf-8",
"text/uri-list",
"application/vnd.portal.filetransfer",
"application/vnd.portal.files"])
formats = g_formats.union(m_formats)
target = Gtk.DropTargetAsync.new(
formats=formats,
actions=Gdk.DragAction.COPY,
)
and read them as str:
drop.read_value_async(
type=str,
io_priority=GLib.PRIORITY_DEFAULT,
cancellable=None,
callback=self._on_drop_data_ready,
)
However, having the view of someone experienced on this question or some reference codebase would be great.