Help needed in implementing Drag and Drop with a List Box in C

Goal

I want to create an app that uses Adwaita and Gtk4. I want to have a list of items that can be reordered via drag and drop. If you remember the old 3.X gnome-todo app: I am trying to create something like this for my DND list behavior.

Issue

I have several questions. I already got a lot of advice in https://app.element.io/#/room/#gtk:gnome.org.
To name two:

  1. I have been told that one can get the drop location with Gtk.ListBox.get_row_at_y, after which I’d have to find the drop location index, remove the dragged widget from the list and then re-add it at that index location that I got.
  2. I have also been made aware that gnome-control-center has an example of something similar I’m trying (e.g. keyboard panel).

I still have troubles, though, probably because I’m still a beginner and the gnome-control-center code was to complex for me.

My issue is that I’m not really sure if the things I did are correct and how to proceed from here.

My Questions (see code below)

  1. Do I need to connect to the drag-begin and drag-end signals in my setup_drag_source()?
  2. In on_drag_prepare_cb() I set up the content_provider. The label text is hidden below the cursor icon though. How to fix?
  3. In on_drag_prepare_cb() I return the content_provider and thus cannot g_object_unref() it. Is that an issue?
  4. In setup_drop_target(), should I really use GDK_ACTION_COPY?
  5. In setup_drop_target(), is the GType really suitable for my needs? I don’t plan on moving strings after all, but labels.

Help is much appreciated!

My Code

You can find my questions from above in the comments in the code as well.

#include <adwaita.h>
#include <gtk/gtk.h>




static void
activate_cb(GtkApplication *application,
	    __attribute__((unused)) gpointer user_data);


GtkWidget *
gui_list_box_create(GtkWidget *list_box);


void
setup_drag_source(GtkWidget *label);


static GdkContentProvider *
on_drag_prepare_cb(GtkDragSource *source, double x, double y, GtkWidget *self);


void
setup_drop_target(GtkWidget *label);


static gboolean
on_drop_cb(GtkDropTarget *target,
	   const GValue *value,
	   double x,
	   double y,
	   gpointer data);




int
main(void)
{
	AdwApplication *application;
	int status;

	application =
	    adw_application_new("org.test.me", G_APPLICATION_DEFAULT_FLAGS);

	g_signal_connect(
	    application, "activate", G_CALLBACK(activate_cb), NULL);
	status = g_application_run(G_APPLICATION(application), 0, NULL);

	g_object_unref(application);

	return status;
}


static void
activate_cb(GtkApplication *application,
	    __attribute__((unused)) gpointer user_data)
{
	GtkWidget *window;
	GtkWidget *toolbar_view;
	GtkWidget *header_bar;
	GtkWidget *list_box = NULL;

	window = adw_application_window_new(application);
	toolbar_view = adw_toolbar_view_new();
	header_bar = adw_header_bar_new();
	list_box = gui_list_box_create(list_box);

	adw_toolbar_view_add_top_bar(ADW_TOOLBAR_VIEW(toolbar_view),
				     header_bar);
	adw_toolbar_view_set_content(ADW_TOOLBAR_VIEW(toolbar_view), list_box);
	adw_application_window_set_content(ADW_APPLICATION_WINDOW(window),
					   toolbar_view);

	gtk_window_present(GTK_WINDOW(window));
}


/*
 *  Create a listbox with 3 children: A, B and C.
 *  Also add Drag and Drop functionality here.
 */
GtkWidget *
gui_list_box_create(GtkWidget *list_box)
{
	GtkWidget *label;
	const char *array[] = { "A", "B", "C", NULL };

	list_box = gtk_list_box_new();

	for (int i = 0; array[i] != NULL; i++) {
		label = gtk_label_new(array[i]);

		// I can see that a drag source is added to my label in the
		// inspector (which can be called via Ctrl-Shift-d). Dragging
		// now also changes the cursor.
		setup_drag_source(label);

		// I can see that the drop target has been added to my label in
		// the inspector. Dropping now changes the cursor (to an "I
		// accept drops" icon) and the drop signal successfully prints
		// "DROP STRING".
		setup_drop_target(label);

		gtk_list_box_append(GTK_LIST_BOX(list_box), label);
	}

	return list_box;
}


void
setup_drag_source(GtkWidget *label)
{
	GtkDragSource *drag_source;

	drag_source = gtk_drag_source_new();

	// I want to move label A below label B, so that A->B->C becomes
	// B->A->C. I want to use Drag and Drop for that.
	// Do I need GDK_ACTION_MOVE and thus connect to the drag-begin,
	// drag-end signals? If yes, what do I do in their callbacks?
	// Or do I need GDK_ACTION_COPY? Why?

	g_signal_connect(
	    drag_source, "prepare", G_CALLBACK(on_drag_prepare_cb), label);

	gtk_widget_add_controller(label, GTK_EVENT_CONTROLLER(drag_source));
}


static GdkContentProvider *
on_drag_prepare_cb(GtkDragSource *drag_source,
		   double x,
		   double y,
		   GtkWidget *self)
{
	GdkContentProvider *content_provider;

	// Not sure if this is even correct. As far as a understand this is only
	// used to show text during drag – meaning I do want a string here? In
	// other words: This is unrelated to my wish of moving the widget label
	// "A" below the label "B" via Drag and Drop?
	//
	// Besides: It does have the issue that the text (e.g. "A") is hidden
	// beneath my cursor which isn't ideal. Is that a bug withhin my
	// program? How to solve?
	content_provider = gdk_content_provider_new_typed(
	    G_TYPE_STRING, gtk_label_get_text(GTK_LABEL(self)));

	gtk_drag_source_set_content(drag_source, content_provider);


	// I cannot unref here since I need to return it. Will that cause
	// issues?
	// g_object_unref(content_provider);

	return content_provider;
}


void
setup_drop_target(GtkWidget *label)
{
	GtkDropTarget *drop_target;

	// I want to move my labels.
	// 01. I'm guessing GDK_ACTION_COPY is wrong here and I should use
	//     GDK_ACTION_MOVE instead?
	// 02. I'm having a hard time believing that my GType is correct here.
	//     What should I use as a GType?
	drop_target = gtk_drop_target_new(G_TYPE_STRING, GDK_ACTION_COPY);

	g_signal_connect(drop_target, "drop", G_CALLBACK(on_drop_cb), label);

	gtk_widget_add_controller(label, GTK_EVENT_CONTROLLER(drop_target));
}


static gboolean
on_drop_cb(GtkDropTarget *target,
	   const GValue *value,
	   double x,
	   double y,
	   gpointer data)
{
	GtkWidget *self;

	self = data;

	// Call the appropriate setter depending on the type of data that we
	// received
	if (G_VALUE_HOLDS(value, G_TYPE_STRING)) {
		// Do something like this function which I took from the docs
		// my_widget_set_file(self, g_value_get_object(value));
		puts("DROP STRING");
	} else {
		puts("DROP NOT STRING");
		return FALSE;
	}
	return TRUE;
}

You can build this with

gcc test_me.c -o test_me $(pkg-config --cflags --libs gtk4 libadwaita-1)

PS

This is a continuation from Looking for help to create Drag and Drop with a list view in C. I can not edit that post, however, nor close it. Since it’s apparently really complicated to implement DND with a GtkListView (which is why I decided to use GtkListBox instead), I’m assuming it’s a better idea to create a new topic for this.

I got even more help in the gtk room on element.io and the program now works as it’s supposed to.

For the potential benefit of others, I share the working code:

/*
 *  Goal: Create an application that uses Gtk4 and libadwaita. This application
 *        will have a list (GtkListBox) containing three items: A, B and C.
 *        These items will be reorderable by using drag and drop.
 *
 *  Misc: It can help to know what type the widgets are having, e.g. when
 *        debugging callback functions. The following can be used for that:
 *        `printf("Widget is of type: %s\n", G_OBJECT_TYPE_NAME(widget));`
 *        It returns something like GtkLabel or GtkListBoxRow.
 *
 *  Run:  Save this file as test_me.c, then run `gcc test_me.c -o test_me
 *        $(pkg-config --cflags --libs gtk4 libadwaita-1) -Wall -Wextra`. After
 *        that, run the binary with `./test_me`.
 *
 *  Info: This program was developed on debian trixie. Gtk4 version is 4.18.6,
 *        libadwaita version is 1.7.4 (according to `pkg-config --modversion
 *        gtk4` and `pkg-config --modversion libadwaita-1`).
 */


#include <adwaita.h>
#include <gtk/gtk.h>




static void
activate_cb(GtkApplication *application,
	    __attribute__((unused)) gpointer user_data);


GtkWidget *
list_box_create(GtkWidget *list_box);


void
setup_drag_source(GtkWidget *list_box_row);


static GdkContentProvider *
on_drag_prepare_cb(GtkDragSource *drag_source,
		   double __attribute__((unused)) drag_starting_point_x,
		   double __attribute__((unused)) drag_starting_point_y,
		   GtkWidget __attribute__((unused)) * self);


void
setup_drop_target(GtkWidget *list_box);


static gboolean
on_drop_cb(GtkDropTarget *target,
	   const GValue *value,
	   double __attribute__((unused)) pointer_position_x,
	   double pointer_position_y,
	   gpointer __attribute__((unused)) data);




int
main(void)
{
	AdwApplication *application;
	int status;

	application =
	    adw_application_new("org.test.me", G_APPLICATION_DEFAULT_FLAGS);

	g_signal_connect(
	    application, "activate", G_CALLBACK(activate_cb), NULL);
	status =
	    g_application_run(G_APPLICATION(application), 0, NULL); // NOLINT

	g_object_unref(application);

	return status;
}


static void
activate_cb(GtkApplication *application,
	    __attribute__((unused)) gpointer user_data)
{
	GtkWidget *window;
	GtkWidget *toolbar_view;
	GtkWidget *header_bar;
	GtkWidget *list_box = NULL;

	window = adw_application_window_new(application);
	toolbar_view = adw_toolbar_view_new();
	header_bar = adw_header_bar_new();
	list_box = list_box_create(list_box);

	adw_toolbar_view_add_top_bar(ADW_TOOLBAR_VIEW(toolbar_view),
				     header_bar);
	adw_toolbar_view_set_content(ADW_TOOLBAR_VIEW(toolbar_view), list_box);
	adw_application_window_set_content(ADW_APPLICATION_WINDOW(window),
					   toolbar_view);

	gtk_window_present(GTK_WINDOW(window)); // NOLINT
}


/*
 *  Create a listbox with 3 children: A, B and C.
 *  Also add Drag and Drop functionality here.
 */
GtkWidget *
list_box_create(GtkWidget *list_box)
{
	GtkWidget *list_box_row;
	GtkWidget *label;
	const char *array[] = { "A", "B", "C", NULL };

	list_box = gtk_list_box_new();

	gtk_list_box_set_selection_mode(GTK_LIST_BOX(list_box), // NOLINT
					GTK_SELECTION_NONE);

	for (int i = 0; array[i] != NULL; i++) {
		list_box_row = gtk_list_box_row_new();
		label = gtk_label_new(array[i]);

		// With this function, a drag source is added to the label in
		// the inspector (which can be called via Ctrl-Shift-d).
		// Dragging now also changes the cursor.
		setup_drag_source(list_box_row);

		gtk_list_box_row_set_child(
		    GTK_LIST_BOX_ROW(list_box_row), // NOLINT
		    label);
		gtk_list_box_append(GTK_LIST_BOX(list_box), // NOLINT
				    list_box_row);
	}

	// With this function, a drop target has been added to the list_box. The
	// inspector confirms this. Dropping now changes the cursor to an "I
	// accept drops" icon.
	setup_drop_target(list_box);

	return list_box;
}


void
setup_drag_source(GtkWidget *list_box_row)
{
	GtkDragSource *drag_source;

	drag_source = gtk_drag_source_new();

	g_signal_connect(
	    drag_source, "prepare", G_CALLBACK(on_drag_prepare_cb), NULL);

	gtk_widget_add_controller(list_box_row,
				  GTK_EVENT_CONTROLLER(drag_source)); // NOLINT
}


static GdkContentProvider *
on_drag_prepare_cb(GtkDragSource *drag_source,
		   double __attribute__((unused)) drag_starting_point_x,
		   double __attribute__((unused)) drag_starting_point_y,
		   GtkWidget __attribute__((unused)) * self)
{
	GdkContentProvider *content_provider;
	GtkWidget *list_box_row;

	list_box_row = gtk_event_controller_get_widget(
	    GTK_EVENT_CONTROLLER(drag_source)); // NOLINT
	content_provider =
	    gdk_content_provider_new_typed(G_TYPE_OBJECT, list_box_row);

	gtk_drag_source_set_content(drag_source, content_provider);

	return content_provider;
}


void
setup_drop_target(GtkWidget *list_box)
{
	GtkDropTarget *drop_target;

	// The GType G_TYPE_OBJECT is used, because Widgets are objects (and the
	// goal is to reorder widgets)
	drop_target = gtk_drop_target_new(G_TYPE_OBJECT, GDK_ACTION_COPY);

	g_signal_connect(drop_target, "drop", G_CALLBACK(on_drop_cb), NULL);

	gtk_widget_add_controller(list_box,
				  GTK_EVENT_CONTROLLER(drop_target)); // NOLINT
}


static gboolean
on_drop_cb(GtkDropTarget *target,
	   const GValue *value,
	   double __attribute__((unused)) pointer_position_x,
	   double pointer_position_y,
	   gpointer __attribute__((unused)) data)
{
	// Call the appropriate setter depending on the type of data that we
	// received
	if (G_VALUE_HOLDS(value, G_TYPE_OBJECT)) { // NOLINT
		GtkWidget *label;
		GtkWidget *list_box_row;
		GtkWidget *list_box;
		GtkWidget *target_list_box_row;
		GtkWidget *target_label;

		list_box = gtk_event_controller_get_widget(
		    GTK_EVENT_CONTROLLER(target)); // NOLINT
		list_box_row = g_value_get_object(value);
		label = gtk_list_box_row_get_child(
		    GTK_LIST_BOX_ROW(list_box_row)); // NOLINT
		target_list_box_row =
		    GTK_WIDGET(gtk_list_box_get_row_at_y( // NOLINT
			GTK_LIST_BOX(list_box),
			(int)pointer_position_y));


		// This will happen when you drag a row unto the list_box.
		// target_list_box_row will then be a GtkListBox, causing a SIGV
		// when trying to process further.
		if (!GTK_IS_LIST_BOX_ROW(target_list_box_row)) { // NOLINT
			return FALSE;
		}

		target_label = gtk_list_box_row_get_child(
		    GTK_LIST_BOX_ROW(target_list_box_row)); // NOLINT


		// Used for debugging.
		// TODO: remove this for production code
		printf("Dragging %s on top of %s (%s->%s)\n",
		       gtk_label_get_text(GTK_LABEL(label)),         // NOLINT
		       gtk_label_get_text(GTK_LABEL(target_label)),  // NOLINT
		       gtk_label_get_text(GTK_LABEL(label)),         // NOLINT
		       gtk_label_get_text(GTK_LABEL(target_label))); // NOLINT


		// Keep the widget alive. The below code (gtk_list_box_remove())
		// removes the widget and would reduce its refcount. But the
		// goal is to re-add the widget. For that, refcount must be
		// above 0.
		// This is assured my calling g_object_ref();
		g_object_ref(list_box_row);

		// Remove child at original position, then add child at drop
		// position
		const int index = gtk_list_box_row_get_index(
		    GTK_LIST_BOX_ROW(target_list_box_row)); // NOLINT

		gtk_list_box_remove(GTK_LIST_BOX(list_box), // NOLINT
				    list_box_row);
		gtk_list_box_insert(GTK_LIST_BOX(list_box), // NOLINT
				    list_box_row,
				    index);

		// Unalive widget (undo +1 from code above).
		g_object_unref(list_box_row);
	} else {
		// If something has been dropped that wasn't of type object,
		// then it will be ignored.
		return FALSE;
	}
	return TRUE;
}

Thank you Alice, ebassi and everyone else that helped me with my questions!

1 Like

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