Drag and Drop with Gtk2 and Perl

For the impatient, scroll down to the complete code example.  You may find this to be all you need.

I’ve struggled with drag and drop using Gtk2-perl for years… now I’m sharing my discoveries so you don’t have to struggle, too!

Here are some resources I used to create this article:

I’m mainly concerned with drag and drop within and between a tree or list (Gtk2::TreeView).  This turns out to be a rather complicated subject.  There are three ways to setup drag and drop in a tree view: set_reorderable, enable_model_drag_dest/enable_model_drag_source, and your own low level implementation using drag_dest_set/drag_source_set.  You can only use one of these methods.  The methods will step on each other and badness ensues when you try to use more than one.  set_reorderable is a convenience function for setting up reordering of elements within a tree using drag and drop.  It does not allow items to be dragged between different trees.  For these reasons we will not cover this function here.  Implementing your own low level drag and drop is very powerful and opens doors to unlimited possibilities, but requires a lot of work and I’ve found the middle road to be just fine for applications I’ve worked on.  The middle road (using enable_model_drag_dest and enable_model_drag_source) gives you lots of expected functionality with little effort.

In this tutorial the dragged item is moved and all code assumes the item is to be moved.  If you need the item to be copied, linked, or something else, you’ll need to modify the code.

set_reorderable – caution!

Gtk2::TreeView->set_reorderable – sets up drag and drop for reordering elements in the tree view.  No drag and drop is enabled between other widgets or applications with this method.  This is great until you start to want both reordering and drag and drop to other widgets.  Enabling this feature interferes with calls to drag_source_set and drag_dest_set, because it makes its own calls to those methods.  (note: if you’re using Glade to build your user interface, the “set_reorderable” can be set using Glade, effectively hiding the call to set_reorderable from you).

Basic Overview

The drag and drop process follows these steps internally:

  1. User drags a row from one view to another.
  2. Gtk calls drag_data_get on the source widget.
  3. The drag_data_get handler (written by you) puts the dragged data into a selection.
  4. Gtk calls drag_data_received on the destination widget.
  5. The drag_data_received handler (written by you) puts the dragged data from the selection into the destination and calls finish on the drag context.
  6. Gtk updates the view.
  7. User is happy.

As the developer all you need to do (not as simple as it sounds until you’ve done it once) is implement step 3, step 5, and connect the signals to the signal handlers in steps 3 and 5.

Nitty Gritty Details

There are so many nitty gritty details to programming in Gtk2 and Gtk2-perl, it drives me nuts!!  Here are some that I discovered while working on the code example below… but by no means is this a complete list.  Please leave comments with details you discovered that may be of use to me and others.

  • get_selected only works if GTK_SELECTION_MULTIPLE is not used.  See my ($model,$iter) = $tv_selection->get_selected; below.
  • enable_model_drag_dest can only be called once on a TreeView (if called multiple times, only the last call is used).  If you need multiple targets or actions you’ll have to list all the targets as arguments and combine all the actions into a Perl array reference, for example: [ 'move', 'link', 'copy' ]

Complete Code Example

The code highlighted in pastel red controls the drag and drop functionality.  All other code sets up the window, views, initialization code, etc..

#!/opt/local/bin/perl

use strict;

use Gtk2 '-init';

my $main = Gtk2::Window->new;

my $src_tree   = Gtk2::TreeView->new;
my $dest_tree  = Gtk2::TreeView->new;
my $src_model  = Gtk2::TreeStore->new("Glib::String");
my $dest_model = Gtk2::TreeStore->new("Glib::String");

# Setup trees
# src tree
$src_tree->set_model($src_model);
my $col = Gtk2::TreeViewColumn->new;
$src_tree->append_column($col);
my $renderer = Gtk2::CellRendererText->new;
$col->pack_start($renderer,1);
$col->add_attribute($renderer,"text",0);
# dest tree
$dest_tree->set_model($dest_model);
$col = Gtk2::TreeViewColumn->new;
$dest_tree->append_column($col);
$renderer = Gtk2::CellRendererText->new;
$col->pack_start($renderer,1);
$col->add_attribute($renderer,"text",0);
# end setup trees
# Drag and drop
$src_tree->enable_model_drag_source(
    'button1-mask',
    'move',
    [ 'text/plain', 'same-app', 1 ]
);
$dest_tree->enable_model_drag_dest(
    'move',
    [ 'text/plain', 'same-app', 1 ]
);
# signals for drag and drop:
$src_tree->signal_connect_after( drag_data_get => \&drag_data_get );
$dest_tree->signal_connect( drag_data_received => \&drag_data_received );
# end drag and drop
# setup ui
my $hbox = Gtk2::HBox->new;
my $vbox = Gtk2::VBox->new;

$hbox->pack_start($src_tree,1,1,5);
$hbox->pack_start($dest_tree,1,1,5);

$vbox->pack_start($hbox,1,1,5);
$main->add($vbox);

$main->set_size_request(640,480);
#end setup ui

# Fill models with test data
$src_model->set($src_model->append(undef),0,"One");
$src_model->set($src_model->append(undef),0,"Two");
$src_model->set($src_model->append(undef),0,"Three");
$dest_model->set($dest_model->append(undef),0,"Four");
$dest_model->set($dest_model->append(undef),0,"Five");
$dest_model->set($dest_model->append(undef),0,"Six");
# end fill models

$main->show_all;

Gtk2->main;
# Drag and drop signal handlers.

sub drag_data_get {
    my ($widget,$context,$selection,$info,$time,$data) = @_;
    my $tv_selection = $widget->get_selection;
    my ($model,$iter) = $tv_selection->get_selected;
    my $value = $model->get_value($iter,0);
    $selection->set($selection->target,8,$value);
    return 0;
}

sub drag_data_received {
    my ($widget,$context,$x,$y,$selection,$info,$time,$data) = @_;
    my $value = $selection->data;
    my $model = $widget->get_model;
    my ($path,$position) = $widget->get_dest_row_at_pos($x,$y);
    my $iter = undef;
    if ( $path ) {
        my $drop_iter = $model->get_iter($path);
        if (
            $position eq 'before' ||
            $position eq 'GTK_TREE_VIEW_DROP_BEFORE' ||
            $position eq 'into-or-before' ||
            $position eq 'GTK_TREE_VIEW_DROP_INTO_OR_BEFORE'
        ) {
            $iter = $model->insert_before(undef,$drop_iter);
        } else {
            $iter = $model->insert_after(undef,$drop_iter);
        }
    } else {
        $iter = $model->append(undef);
    }
    $model->set($iter,0,$value);
    $context->finish(1,1,$time);
    return 0;
}