Search code examples
python-3.xdrag-and-dropgtkpygtkgtk4

Drag and drop with GTK4: connecting DragSource and DropTarget via ContentProvider for derived classes


I am working on (py)gtk4's drag-and-drop functionality and I hit a wall. I have a flowbox-derived class MediaGallery that contains frames with images and their filenames (class MediaFile), and a listbox-derived class Albums. I want to drag one or more selected images from MediaGallery to Albums, which will eventually add them to the underlying database.

Relevant piece of code:

class Album(gtk.Box):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, orientation=gtk.Orientation.HORIZONTAL, **kwargs)
        self.label = gtk.Label(hexpand=False)
        self.append(self.label)
        self.label.set_visible(False)
        self.entry = gtk.Entry()
        self.append(self.entry)
        self.entry.set_visible(True)
        self.entry.connect('activate', self.on_entry_changed)

        dnd = gtk.DropTarget.new(gdk.FileList, gdk.DragAction.COPY)
        dnd.connect('drop', self.on_dnd_drop)
        dnd.connect('accept', self.on_dnd_accept)
        dnd.connect('enter', self.on_dnd_enter)
        dnd.connect('motion', self.on_dnd_motion)
        dnd.connect('leave', self.on_dnd_leave)
        self.add_controller(dnd)

    def on_entry_changed(self, entry):
        name = entry.get_text()
        self.label.set_text(name)
        self.entry.set_visible(False)
        self.label.set_visible(True)

    def on_dnd_drop(self, value, x, y, user_data):
        print(f'in on_dnd_drop(); value={value}, x={x}, y={y}, user_data={user_data}')

    def on_dnd_accept(self, drop, user_data):
        print(f'in on_dnd_accept(); drop={drop}, user_data={user_data}')
        return True

    def on_dnd_enter(self, drop_target, x, y):
        print(f'in on_dnd_enter(); drop_target={drop_target}, x={x}, y={y}')
        return gdk.DragAction.COPY

    def on_dnd_motion(self, drop_target, x, y):
        print(f'in on_dnd_motion(); drop_target={drop_target}, x={x}, y={y}')
        return gdk.DragAction.COPY

    def on_dnd_leave(self, user_data):
        print(f'in on_dnd_leave(); user_data={user_data}')

class MediaFile(gtk.FlowBoxChild):
    def __init__(self, *args, file, **kwargs):
        super().__init__(*args, **kwargs)

        self.filename = file

        frame = gtk.Frame()
        self.set_child(frame)

        vbox = gtk.Box(orientation=gtk.Orientation.VERTICAL)
        frame.set_child(vbox)

        self.image = gtk.Image.new_from_file(file)
        self.image.set_pixel_size(256)
        vbox.append(self.image)

        label = gtk.Label.new(file[file.rfind('/')+1:])
        vbox.append(label)

    def __repr__(self):
        return f'<MediaFile {self.filename}>'

class MediaGallery(gtk.FlowBox):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self.name = kwargs.get('name', 'default')
        self.connect('child-activated', self.on_media_selected)

        dnd = gtk.DragSource.new()
        dnd.set_actions(gdk.DragAction.COPY)
        dnd.connect('prepare', self.on_dnd_prepare)
        dnd.connect('drag-begin', self.on_dnd_begin)
        dnd.connect('drag-end', self.on_dnd_end)
        self.add_controller(dnd)

    def __repr__(self):
        return f'<MediaGallery {self.name}>'

    def on_media_selected(self, gallery, media_file):
        print(f'on_media_selected(); gallery={gallery}, media_file={media_file}')

    def on_dnd_prepare(self, drag_source, x, y):
        data = self.get_selected_children()
        print(f'in on_dnd_prepare(); drag_source={drag_source}, x={x}, y={y}, data={data}')
        if len(data) == 0:
            return None

        paintable = data[0].image.get_paintable()
        drag_image = gtk.Image.new_from_paintable(paintable)
        drag_image.set_opacity(0.5)  # FIXME: not sure why transparency doesn't work
        drag_source.set_icon(drag_image.get_paintable(), 128, 128)  # FIXME: not sure why hot_x and hot_y don't work
        
        content = gdk.ContentProvider.new_for_value(data)
        return content

    def on_dnd_begin(self, drag_source, data):
        content = data.get_content()
        print(f'in on_dnd_begin(); drag_source={drag_source}, data={data}, content={content}')

    def on_dnd_end(self, drag, drag_data, flag):
        print(f'in on_dnd_end(); drag={drag}, drag_data={drag_data}, flag={flag}')

The entire code is available from here. The output I get:

in on_dnd_prepare(); drag_source=<Gtk.DragSource object at 0x7f755e760940 (GtkDragSource at 0x2d100e0)>, x=176.15234375, y=172.96092224121094, data=[<MediaFile 20210712_190722A.jpg>]
in on_dnd_begin(); drag_source=<Gtk.DragSource object at 0x7f755e760940 (GtkDragSource at 0x2d100e0)>, data=<__gi__.GdkWaylandDrag object at 0x7f75424f2e00 (GdkWaylandDrag at 0x41b6ac0)>, content=<__gi__.GdkContentProviderValue object at 0x7f75424eb040 (GdkContentProviderValue at 0x43a8c10)>
in on_dnd_accept(); drop=<Gtk.DropTarget object at 0x7f755e760940 (GtkDropTarget at 0x3f56aa0)>, user_data=<__gi__.GdkWaylandDrop object at 0x7f7542374440 (GdkWaylandDrop at 0x7f7560157390)>
in on_dnd_enter(); drop_target=<Gtk.DropTarget object at 0x7f755e760940 (GtkDropTarget at 0x3f56aa0)>, x=139.6796875, y=0.0898437574505806
in on_dnd_motion(); drop_target=<Gtk.DropTarget object at 0x7f755e760940 (GtkDropTarget at 0x3f56aa0)>, x=139.6796875, y=0.0898437574505806
in on_dnd_motion(); drop_target=<Gtk.DropTarget object at 0x7f755e760940 (GtkDropTarget at 0x3f56aa0)>, x=139.6796875, y=0.0898437574505806
/usr/lib/python3/dist-packages/gi/overrides/Gio.py:42: Warning: ../../../gobject/gtype.c:4322: type id '0' is invalid
  return Gio.Application.run(self, *args, **kwargs)
/usr/lib/python3/dist-packages/gi/overrides/Gio.py:42: Warning: cant peek value table for type '<invalid>' which is not currently referenced
  return Gio.Application.run(self, *args, **kwargs)
/usr/lib/python3/dist-packages/gi/overrides/Gio.py:42: Warning: ../../../gobject/gvalue.c:185: cannot initialize GValue with type '(null)', this type has no GTypeValueTable implementation
  return Gio.Application.run(self, *args, **kwargs)

(python:196456): Gdk-CRITICAL **: 22:21:40.071: gdk_content_provider_get_value: assertion 'G_IS_VALUE (value)' failed

(python:196456): Gdk-CRITICAL **: 22:21:40.071: gdk_content_provider_get_value: assertion 'G_IS_VALUE (value)' failed

(python:196456): GLib-GIO-CRITICAL **: 22:21:40.071: g_task_return_error: assertion 'error != NULL' failed
in on_dnd_leave(); user_data=<Gtk.DropTarget object at 0x7f7542374480 (GtkDropTarget at 0x3f56aa0)>

I can't find any documentation on how to set gdk type for DropTarget appropriately and how to make DragSource and DropTarget exchange the data via ContentProvider. Can anyone provide any insight, please? Thanks in advance!


Solution

  • You should actually return a GObject.Value in ::prepare

    For example here it is changed to use a gliststore to store multiple items in a single GObject and from that create a GValue:

        def on_dnd_prepare(self, drag_source, x, y):
            data = gio.ListStore()
            data.splice(0, 0, self.get_selected_children())
            print(data.get_n_items())
            print(f'in on_dnd_prepare(); drag_source={drag_source}, x={x}, y={y}, data={data}')
            if len(data) == 0:
                return None
    
            paintable = data[0].image.get_paintable()  # TODO: make this nicer for multiple selections
            drag_image = gtk.Image.new_from_paintable(paintable)
            drag_image.set_opacity(0.5)  # FIXME: not sure why transparency doesn't work
            drag_source.set_icon(drag_image.get_paintable(), 128, 128)  # FIXME: not sure why hot_x and hot_y don't work
            
            content = gdk.ContentProvider.new_for_value(gobject.Value(gio.ListModel, data))
            return content
    

    You should make your drop target accept the correct type (GdkFileList broken in PyGObject https://gitlab.gnome.org/GNOME/pygobject/-/issues/468)

    dnd = gtk.DropTarget.new(gio.ListModel, gdk.DragAction.COPY)
    

    And you should fix the arguments of the drop function

        def on_dnd_drop(self, drop_target, value, x, y):
            print(f'in on_dnd_drop(); value={value}, x={x}, y={y}')
            print(list(value))
    

    You can access what you passed when creating the gobject.Value as the value argument here (in this case a list of MediaFiles)