New Gio.ListStore Drag & drop interface?

The now deprecated Gtk ListStore implemented drag & drop, but its replacement, Gio ListStore documentation says nothing about such functionality.

So how does it work now, how to make Gtk.ListView items be drag & droppable / re-orderable?

For reference here is the code:


class ModelObject(GObject.GObject):
    __gtype_name__ = 'ModelObject'
    def __init__(self, name):
        super().__init__()
        self._name = os.path.basename(name)
        self._path = name

    @GObject.Property(type=str)
    def name(self):
        return self._name

    @GObject.Property(type=str)
    def path(self):
        return self._path
model = Gio.ListStore.new(ModelObject)
selection = Gtk.SingleSelection.new(model)

def __init__(self, **kwargs):
    super().__init__(**kwargs)

    factory = Gtk.BuilderListItemFactory.new_from_resource(None, '/org/yphil/matinee/resources/listitem.ui')
    self.listView = Gtk.ListView.new(self.selection, factory)
    self.scrolledWindow.set_child(self.listView)

And here is the listitem.ui resource:

<?xml version="1.0" encoding="UTF-8"?>
<interface>
  <template class="GtkListItem">
    <property name="child">
      <object class="GtkLabel">
        <binding name="label">
          <lookup name="name" type="ModelObject">
            <lookup name="item">GtkListItem</lookup>
          </lookup>
        </binding>
      </object>
    </property>
  </template>
</interface>

Yes this is Python, but I only need to understand the logic and I can’t seem to find any information, in any language, specific to this new implementation.

GtkListStore is a storage type, and never implemented drag and drop: that’s something provided by GtkTreeView.

There is no automatic “drag a row and drop it somewhere to re-order the contents of the model” API in GtkListView, because the widget recycles rows, and that makes it very complicated. You can implement dragging from a row in the list view, and dropping into the list view, as rows are widgets. See GtkDragSource and GtkDropTarget. Implementing re-ordering with scroll when reaching the edge of the list view is also possible, but will require a fair chunk of work.

From a usability perspective, “drag and drop to re-order rows” isn’t a great user experience. It’s not accessible, and requires a pointing device at all times. You should look into possible substitutes for this interaction.

1 Like

Thanks for your help! I managed to implement the Drag action, but I’m still struggling with the Drop.

    dropTarget = Gtk.DropTarget.new(Gio.ListStore, Gdk.DragAction.ASK)
    dropTarget.connect('drop', self.on_dnd_drop)

    dragSource = Gtk.DragSource.new()
    dragSource.set_actions(Gdk.DragAction.COPY)
    dragSource.connect('prepare', self.on_dnd_prepare)
    dragSource.connect('prepare', self.on_dnd_prepare)
    dragSource.connect('drag-begin', self.on_dnd_begin)
    dragSource.connect('drag-end', self.on_dnd_end)

    factory = Gtk.BuilderListItemFactory.new_from_resource(None, '/org/yphil/matinee/resources/listitem.ui')
    self.listView = Gtk.ListView.new(self.selection, factory)
    self.scrolledWindow.set_child(self.listView)
    self.listView.add_controller(dropTarget)
    self.listView.add_controller(dragSource)

Intuitively, I though that the selection was the object the DragSource was supposed to be added to, and the ListView, the object the DropTarget was supposed to be added to, but apparently the ListView is the source, and not the target…?

All the Drag callbacks are firing, but not the drop one, and the drop gets rejected.

I’m a bit overwhelmed:

    dragSource = Gtk.DragSource.new()
    dragSource.set_actions(Gdk.DragAction.MOVE)
    dragSource.connect('prepare', self.on_dnd_prepare)
    dragSource.connect('drag-begin', self.on_dnd_begin)
    dragSource.connect('drag-end', self.on_dnd_end)

    dropTarget = Gtk.DropTarget.new(Gio.ListStore, Gdk.DragAction.MOVE)
    dropTarget.connect('drop', self.on_dnd_drop)
    dropTarget.connect('accept', self.on_dnd_accept)
    dropTarget.connect('enter', self.on_dnd_enter)
    dropTarget.connect('motion', self.on_dnd_motion)
    dropTarget.connect('leave', self.on_dnd_leave)

Receiving the “leave” signal at the end of the drop (? and a lot of enter / leave errors) is as far as I can get.

Seriously, I really have to take care of all those complex and error-prone steps, when all I’m doing is re-ordering elements in a list (that is, always the same type into the same type)? I must be missing something, please tell me I’m missing something.

OK, I think I’m really close now : I get the dragged object attributes in the on_drop() callback :slight_smile:

I had to make my ListItem wrapper object iterable, like this:

class ModelObject(GObject.GObject):
    __gtype_name__ = 'ModelObject'
    def __init__(self, name):
        super().__init__()
        self._name = os.path.basename(name)
        self._path = name

    def __iter__(self):
        yield {
            "name": self._name,
            "path": self._path
        }
        
    @GObject.Property(type=str)
    def name(self):
        return self._name

    @GObject.Property(type=str)
    def path(self):
        return self._path

But what I really need is the index of the element in the list in order to remove it from the list model, and it wants a int for that.

To act upon said list element (not for drag & dropping, just for activation) I use

    selection = self.selection.get_selected()
    self.load_video_file(self.model[selection].path)

But in the drag & drop context self.selection.get_selected() doesn’t work, it always returns 0.

Here is the code:

model = Gio.ListStore.new(ModelObject)
selection = Gtk.SingleSelection.new(model)

def __init__(self, **kwargs):
    super().__init__(**kwargs)

    dropTarget = Gtk.DropTarget.new(ModelObject, Gdk.DragAction.MOVE)
    dropTarget.connect('drop', self.on_dnd_drop)
    dropTarget.connect('accept', self.on_dnd_accept)
    dropTarget.connect('enter', self.on_dnd_enter)
    dropTarget.connect('motion', self.on_dnd_motion)
    dropTarget.connect('leave', self.on_dnd_leave)
    
    dragSource = Gtk.DragSource.new()
    dragSource.set_actions(Gdk.DragAction.MOVE)
    dragSource.connect('prepare', self.on_dnd_prepare)
    dragSource.connect('drag-begin', self.on_dnd_begin)
    dragSource.connect('drag-end', self.on_dnd_end)

# (...)
def on_dnd_drop(self, drop_target, value, x, y):
    print("Drop:", list(value))

def on_dnd_accept(self, drop, user_data):
    print("Accept", user_data)
    return True

def on_dnd_enter(self, drop_target, x, y):
    return Gdk.DragAction.MOVE

def on_dnd_motion(self, drop_target, x, y):
    return Gdk.DragAction.MOVE

def on_dnd_leave(self, user_data):
    print("Leave")
    
def on_dnd_begin(self, drag_source, data):
          print("drag_source, data:", drag_source, data)

def on_dnd_end(self, drag, drag_data, flag):
    pass

def on_dnd_prepare(self, drag_source, x, y):
    data = self.selection.get_selected_item()
    content = Gdk.ContentProvider.new_for_value(data)
    return content

How (and when) can I get the index of the dragged list item? I may have to make a method in my ModelObject class to get an iterator, but I don’t know

  • If this makes any sense (does the object even know where it is in the list?) ;
  • how to do it.

EDIT: In fact, I can’t even get the dragged item’s properties on drop, only the dropped one :frowning: it doesn’t help that the selected (not dragged) item stays selected during and after the drag & drop, even though:

selection.props.can_unselect = True
selection.props.autoselect = False

So what I’m effectively dragging is the already selected item, that stays selected even though I clicked elsewhere!

Getting the row via the selection doesn’t seem to be the right way… So what is?

Instead, what I am getting is slightly mad, can somebody help me?

Use ListStore::find to get the index of an item. If that is too slow, you will have to come up with some other way to sort the store and keep track of the indices.

1 Like

Hi Jason, thanks (a lot) for your help ; the thing is, for now I have to figure out how to get the dragged item’s index in the first place, before I can search / find it in the store…!

def on_dnd_prepare(self, drag_source, x, y):
    data = self.selection.get_selected_item() # Selected, *not* dragged row
    content = Gdk.ContentProvider.new_for_value(data)
    return content

Is using the selection (Gtk.SingleSelection) even the right way? It doesn’t seem to be, as no matter what I try the selection stays selected while I drag / move the item ; So how can I access the dragged item at all?

Peek 2022-11-30 22-58

Just a guess: you will probably have to create separate drag sources for each of the individual widgets in the list view. If you want to extend this to multi selection, you could probably try to detect if the object is in the selection at the start of the drag, and then if it is you move the whole selection.

1 Like

Ah, yes. That sounds only logical: Create a dragsource event for each item ever appended to the model, and attach said even(s) to the listView ; I’m going to try that tomorrow ;

If you want to extend this to multi selection

Of course I do, but first things first :slight_smile:

Good night, everybody.

EDIT : Can’t get it to work (that is, the result is the same) the thing is, the DragSource is never really connected to the item:

        for vFile in self.files:
            dragSource = Gtk.DragSource.new()
            dragSource.set_actions(Gdk.DragAction.MOVE)
            dragSource.connect('prepare', self.on_dnd_prepare)
            dragSource.connect('drag-begin', self.on_dnd_begin)
            dragSource.connect('drag-end', self.on_dnd_end)
            self.model.append(ModelObject(vFile.get_path()))
            self.listView.add_controller(dragSource)

That’s still one dragSource. And, the selection is still not “unselecting” as in the image above ; Why? Wow, this is really hard to understand. @ebassi Please, can you provide some guidance here? Should I use the selection, and if yes, then how to get the actually clicked row? Anything, pseudocode, a laconic explanation of how dragSource is / works, I mean, no one seems to know? A search for it returns only one relevant page, and it’s the theorical documentation. No mailing list discussion, no pastebin, nothing!

There is only one question about it on StackOverflow, and it’s not really applicable, as it uses gtk.FlowBox that apparently has nice methods like get_selected_children() that actually does what it says on the box, but I don’t have such luxury. What I’m dragging, is what is already selected before the drag.

Emanuele, please, I implore you, if you know how to get my dragged row, please explain it to me and I promise I’ll get out of your hair :slight_smile:

I did implement it, but I still can’t get it to work the way it should, as the only row I can get info about is the already selected row / widget. Do you know how I can access the actually dragged widget? Do I have to do something to my ModelObject class to make the instance somehow always know its place in the list? Please, at least tell me this :

  • Should I use my existing GtkSingleSelection at all to get access to the dragged widget, or is this a dead end?

This is still not correct. The controller has to be added to the widget generated by the factory. Not the list view.

You mean like this?

            dragSource = Gtk.DragSource.new()
            dragSource.set_actions(Gdk.DragAction.MOVE)
            dragSource.connect('prepare', self.on_dnd_prepare)
            dragSource.connect('drag-begin', self.on_dnd_begin)
            dragSource.connect('drag-end', self.on_dnd_end)
            modelObject = ModelObject(vFile.get_path())
            modelObject.add_controller(dragSource)
            self.model.append(modelObject)

I tried that, but without any error, my list is empty.

A model object is not a widget, you cannot add controllers to it. If using the SignalListItemFactory, you must add the controller to the widget you are setting with ListItem::set_child. Edit: Sorry I forgot you were using BuilderListItemFactory, with that you can add the controller as a child of the ListItem in the builder XML and then connect signal callbacks to it. I cannot remember if that works right from the python bindings.

Ok, that works:

<?xml version="1.0" encoding="UTF-8"?>
<interface>
  <template class="GtkListItem">
    <property name="child">
      <object class="GtkLabel">
        <property name="xalign">0</property>
        <property name="margin-bottom">3</property>
        <property name="margin-end">3</property>
        <property name="margin-start">3</property>
        <property name="margin-top">3</property>
        <binding name="label">
          <lookup name="name" type="ModelObject">
            <lookup name="item">GtkListItem</lookup>
			<object class="GtkDropControllerMotion"/>
          </lookup>
        </binding>
      </object>
    </property>
  </template>
</interface>

But about that:

and then connect signal callbacks to it

I’m not sure how to access it ; I cannot seem to access it the way I access the elements in my window XML template…

I think I have to change my ModelObject class too:

class ModelObject(GObject.GObject):
    __gtype_name__ = 'ModelObject'
    def __init__(self, name):
        super().__init__()
        self._name = os.path.basename(name)
        self._path = name

    def __iter__(self):
        yield {
            "name": self._name,
            "path": self._path
        }
        
    @GObject.Property(type=str)
    def name(self):
        return self._name

    @GObject.Property(type=str)
    def path(self):
        return self._path

But I’m not exactly sure how.

Thank a ton for you help, really. Getting closer every day :slight_smile:

From my last attempts, it would seem that it does not ; Can somebody confirm this? After all, no one has ever seen a Drag & droppable Gtk4 List in Python.

I looked into it a little bit. It can technically be done, but it seems there is a bug in the python bindings preventing callbacks in BuilderListItemFactory from working correctly.

1 Like

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.