Posted on November 1, 2008 by rodney
Tags: gnome

I wanted to implement a draggable save button in my GTK+ app. This lets the user save his file by dragging the document’s icon from the application onto the desktop or a file browser window.

Drag and drop (DND) is an intuitive approach to file management and XDS avoids the problem of novice users trying to use the file selector dialog as a file manager. However it’s not suitable for mouseless or limited-mouse users, so applications must also provide the conventional file selector.

The protocol used for drag to save is called Extensible Desktop Save (XDS) and is supported by ROX, Thunar, Konqueror, and Nautilus file managers.

This entry will describe what the programmer needs to do to implement saving with XDS in his own GTK+ app, with a small example. The example is in Python because the language is succinct and good for prototyping.

At some point, I recommend reading or skimming this good introduction to DND in GTK+, and the XDS spec. I referenced these, and the file-roller source to write the example.

How to save with XDS

In your UI you need a widget that the user will drag from (the source widget), and perhaps a text entry for the user to choose a filename.

The following steps correspond to the ones detailed in the spec, except they are translated to GTK+, and details about what the file manager needs to do are omitted.

Step 0 – Declare support for XDS

Create two GDK atoms called “XdndDirectSave0” and “text/plain” and keep them in variables to be used later. The atoms will be used to refer to the XdndDirectSave (type text/plain) property of the source widget. This property is a communication channel between source and destination. It is used by the source widget to provide a filename to the file manager, and by the destination to specify a path to the application.

fixme: charset

XDS_ATOM = gtk.gdk.atom_intern("XdndDirectSave0")
TEXT_ATOM = gtk.gdk.atom_intern("text/plain")

Set up the widget as a drag source, with the single target name XdndDirectSave0. The 0 at the end represents the protocol version you’re implementing.

TARGET_TYPE_XDS = 42  # this can be any integer

def init_xds(self):
    # self.save is a button widget: the drag source
    targets = [("XdndDirectSave0", 0, self.TARGET_TYPE_XDS)]
    self.save.drag_source_set(gtk.gdk.BUTTON1_MASK,
                              targets,
                              gtk.gdk.ACTION_COPY)

Connect the widget’s drag-begin, drag-data-get, drag-end signals up to handlers.

    self.save.connect("drag-begin", self.on_save_drag_begin)
    self.save.connect("drag-data-get", self.on_save_drag_data_get)
    self.save.connect("drag-end", self.on_save_drag_end)

Step 1 – On drag-begin event

Once the user starts dragging the source widget, set the widget’s XdndDirectSave property to the filename (just the filename, no path) that was given by the user.

def on_save_drag_begin(self, widget, context):
    filename = self.filename.get_text()
    context.source_window.property_change(self.XDS_ATOM, self.TEXT_ATOM, 8,
                                          gtk.gdk.PROP_MODE_REPLACE,
                                          filename)

Step 2 – On drag-data-get event

This event is caused by the file manager, after the save widget has been dropped on a folder. The file manager puts the full path and filename into the source widget’s XdndDirectSave property. Get the value of this property and save the file.

def get_xds_filename(self, context):
    if self.XDS_ATOM in context.targets:
        typ, fmt, data = context.source_window.property_get(self.XDS_ATOM,
                                                            self.TEXT_ATOM)
        return data

    return None

def on_save_drag_data_get(self, widget, context, selection, info, time):
    if info == self.TARGET_TYPE_XDS:
        destination = self.get_xds_filename(context)

        if destination is not None:
            error = self.save_file(destination)

Your app must return a status code to the file manager, depending on the result of saving the file. It is put in the selection that is normally used for DND. There are three possible status codes, but only two are important for GTK+ apps: “S” and “E” – Success and Error.

The third, “F” for Failure, is used when the file manager gave a network URI that isn’t supported by the app. In that case the protocol supports sending the file through a selection. But all new GTK+ apps should be using GIO, and with GVFS, this supports saving pretty much anywhere except to Bruce Schneier’s brain. So it’s not necessary to send “F”.

            code = "S" if error is None else "E"
            selection.set(selection.target, 8, code)

If there was a need to present some dialog to the user, for example “Could not save file: Permission denied,” then do it after selection.set(). Otherwise the file manager will be waiting for a response until the user closes the dialog.

            if error is not None:
                self.show_error(error)

Step 3

If the file manager received “S” or “E”, it sends a drag finished event. In the case of Success, it might need to refresh its view so the user can see the new file.

Step 4 – On drag-end event

After the drag operation has finished, your app can delete the source widget’s XdndDirectSave property.

def on_save_drag_end(self, widget, context):
    context.source_window.property_delete(self.XDS_ATOM)

Summary

As you can see, it’s fairly straightforward to implement drag to save in an application. It should be enough to copy and paste this code and translate it to the language you’re using.

An app could also support DND of URIs, depending on what you’re trying to achieve. The example application allows dragging a file onto the window to load it. It also illustrates a simple use of GIO.

Full example source can be downloaded here: