Chapter 21. Drag and Drop

Objective

Drag and drop are typical actions users have been spoiled with. In my use of Gtk+, this was one of the last things I've learned to utilize in programs.

This lesson will demonstrate how to enable drag and drop in a program.

21.1. A few rules to play by.

There are basic steps which should always be present for drag and drop to work correct. Before we start, lets get the lingo right to remove any confusion.

21.1.1. A Checklist for functional Drag and Drop

  • Each item which will be dragged needs to declare itself as a drag source.

    This is accomplished with the 'drag_source_set' method of the Gtk2::Widget class.

  • Each item which will receive dragged items needs to declare itself as a drag destination.

    This is accomplished with the 'drag_dest_set' method of the Gtk2::Widget class.

    An item can be a drag source and destination at the same time.

  • If you want to change the drag icon, connect a callback to the 'drag-begin' signal of the drag source. This callback is then used to set up the drag icon. You use the 'drag_source_set_icon_pixbuf' method of the Gtk2::Widget class for this.

  • When a drag source is dropped onto a valid drag destination, data must flow between them. Two signals and their callbacks are used to accomplish this. First the 'drag-data-get' signal will trigger on the drag source.

    After this signal, the 'drag-data-received' signal will trigger on the drag destination.

    Callbacks need to be connected to both these signals in order for data to flow between the source and destination widgets. The one callback will set up the data, and the other will extract the data.

The above four (or five if you like cosmetics) checks is all you ever need to remember for the typical drag and drop operations.

21.1.2. Two data types explained

There are two data types used in drag and drop operations which can be strange and obscure on first encounter. We discuss them here so you may recognize them in the sample program later on.

21.1.2.1. Gtk2::TargetEntry

The 'drag_source_set' and 'drag_dest_set' methods are used to enable a widget to be a drag source and a drag destination respectively.

$widget->drag_source_set ($start_button_mask, $actions, ...) 
    * $start_button_mask (Gtk2::Gdk::ModifierType)
    * $actions (Gtk2::Gdk::DragAction)
    * ... (list) of Gtk2::TargetEntry's
Gtk2::Gdk::ModifierType and Gtk2::Gdk::DragAction are explained on the POD of Gtk2::Widget and we do not want to duplicate documentation here.

The Gtk2::TargetEntry data structure represents a single type of data which can be supplied or received during drag-and-drop.

# as a HASH
$target_entry = {
    target => 'text/plain', # some string representing the drag type
    flags => [], # Gtk2::TargetFlags
    info => 42,  # some app-defined integer identifier
};               

It is used as an argument to the 'drag_source_set' and 'drag_dest_set' methods.

The 'drag_source_set' usually contains only one Gtk2::TargetEntry, but the 'drag_dest_set' can contain an array of them, each representing a source it is willing to accept drag data from.

#Create a target table to receive drops
my @target_table = (
 
    {'target' => 'STRING',       'flags' => [], 'info' => ID_ICONVIEW   },
    {'target' => "text/uri-list",'flags' => [], 'info' => ID_URI        },
    {'target' => "text/plain",  'flags' => [], 'info' => ID_LABEL       },
);
 
#make this the drag destination (drop) for various drag sources
$slist->drag_dest_set('all', ['copy'], @target_table);

If you want to receive dropped data from the file browser (eg Nautilus), you need to specify a target as "text/uri-list". When you drop data from Nautilus onto a drag destination, it receives a list of newline separated URIs each representing a dragged item.

Note

Not going into to much detail, the usual values will be either 'text/plain' or 'STRING' to transfer text between a drag source and destination. The end results of both are the same. The one is a MIME type, the other is a Gtk2::Gdk::Atom. There are pre-defined Gtk2::Gdk::Atoms which can be used. 'STRING' is such one.

If you want to transfer data other than plain text during drag and drop, you can specify another type of Gtk2::Gdk::Atom.

To find out more about the pre-defined Gtk2::Gdk::Atoms, you can visit this URL.

The flags value is empty most of the time.

The 'info' is an integer value that will represent the source of the data which is send or received. This value will be passed on to the 'drag-data-get' and 'drag-data-received' callbacks. This will enable you to differentiate between the sources of the drag and drop action.

Tip

It makes your program more readable if you assign constants to the integer values.

21.1.2.2. Gtk2::SelectionData

When a drag source is dropped onto a drag destination you need to first set up the data of the drag source, and then extract that data on the drag destination.

A typical signal handler for the drag source's 'drag-data-get' signal will look like this:

sub source_drag_data_get {
#---------------------------------------------------
#This sets up the data of the drag source. It is ---
#required before the 'drag-data-received' event ----
#fires which can be used to extract this data ------
#---------------------------------------------------
    
    my ($widget, $context, $data, $info, $time,$string) = @_;
    
    $data->set_text($string,-1);
}
You may want to pass an extra string to define $data's value. $info is used to determine which source called this callback. It is the 'info' value of the Gtk2::TargetEntry data type. This allows you to utilize the same callback by various drag sources. $data is what this section is discussing, and will follow after a snippet of the typical 'drag-data-received' signal callback.
sub target_drag_data_received {
    #---------------------------------------------------
    #Extract the data which was set up during the  -----
    #'drag-data-get' event which fired just before -----
    #the 'drag-data-received' event. Also checks which--
    #source supplied the data, and handle accordingly----
    #---------------------------------------------------
    
    my ($widget, $context, $x, $y, $data, $info, $time) = @_;
    
    if  ($info eq ID_URI){
    
        print "Mmmm feels like Nautilus...\n";
        foreach ($data->get_uris){
            print "Got file ".$_."\n";
        }
    }
}
$info and $data are used most often. The rest can be used during more advanced implementations.

The Gtk2::SelectionData object is used to store a chunk of data along with the data type and other associated information. It has various convenient methods to insert and extract data to and from it. You can check out its POD for more info.

In the sample program we will use its 'get_uris', 'data' and 'set_text' methods. They are sufficient to transfer text between the drag source and destination.

Tip

Although Gtk2::SelectionData can be used to carry all sorts of data types Perl removes complex type castings :)!

21.1.3. Quit dragging and start dropping!

This is the fun part. Enough theory, lets look at a sample. In the sample we make use of the two Gtk2::Paned classes, Gtk2::VPaned and Gtk2::HPaned. We have a Gtk2::IconView widget that contains a list of all the Gtk2::Stock icons. These can be dragged onto a Gtk2::SimpleList. This Gtk2::SimpleList can also accept dragged items from the Nautilus file browser and the forbidden 'Now Stanley, DO NOT drag this item' event box. (come on, try it out :)). As a last bonus, we introduce Gtk2::ToolTips to the reader.

Table 21-1. Gtk2 object classes used

Gtk2 Class Name
Gtk2::Window
Gtk2::ToolTips
Gtk2::VBox
Gtk2::Paned
Gtk2::VPaned
Gtk2::HPaned
Gtk2::EventBox
Gtk2::Label
Gtk2::IconView
Gtk2::SimpleList
Gtk2::ScrolledWindow
Gtk2::ListStore
Gtk2::IconFactory
Gtk2::IconSet
Gtk2::Stock
Gtk2::Style
Gtk2::TargetEntry
Gtk2::SelectionData

21.1.4. A Screenshot

Figure 21-1. Screenshot of the Drag and Drop sample program

21.1.5. The Code.

The program can be found here: 'gtk2_iconview.pl'

    
  1 #! /usr/bin/perl -w
  2 
  3 use strict;
  4 use Gtk2 '-init';
  5 use Glib qw/TRUE FALSE/;
  6 use Gtk2::SimpleList;
  7 
  8 #Declare our columns
  9 use constant C_MARKUP               => 0;
 10 use constant C_PIXBUF               => 1;
 11 
 12 #Declare our IDENDTIFIER ID's
 13 use constant ID_ICONVIEW            => 48;
 14 use constant ID_LABEL               => 49;
 15 use constant ID_URI                 => 50;
 16 
 17 #Add a tooltip object
 18 my $tooltips = Gtk2::Tooltips->new();
 19 
 20 #standard window creation, placement, and signal connecting
 21 my $window = Gtk2::Window->new('toplevel');
 22 $window->signal_connect('delete_event' => sub { Gtk2->main_quit; });
 23 $window->set_border_width(5);
 24 $window->set_position('center_always');
 25 
 26 #add and show the vbox
 27 $window->add(&ret_vbox);
 28 $window->show();
 29 
 30 #our main event-loop
 31 Gtk2->main();
 32 
 33 sub ret_vbox {
 34 
 35     my $vbox = Gtk2::VBox->new(FALSE,5);
 36     my $h_paned = Gtk2::HPaned->new();
 37 
 38         my $v_paned = Gtk2::VPaned->new();
 39         #get the iconview
 40         $v_paned->pack1(create_iconview(),TRUE,FALSE);
 41         #get the simple list where everything will be dropped into
 42         $v_paned->pack2(create_simple_list(),TRUE,FALSE);
 43 
 44     $h_paned->pack1($v_paned,TRUE,FALSE);
 45 
 46         #create an eventbox to accept drag actions
 47         my $event_box = Gtk2::EventBox->new();
 48             my $label_drag_me = Gtk2::Label->new("Now Stanley,\nDO NOT drag this item");
 49         $event_box->add($label_drag_me);
 50 
 51         #Try and convince the user otherwise
 52         $tooltips->set_tip ($event_box,
 53             'No way! You have to drag it at least once!'.
 54             'Drag it, and drop it into the bottom list'
 55             );
 56 
 57         #Setting up the Gtk2::Event Box instead of the Gtk2::Label, enabling it as 
 58         #a drag source
 59         $event_box->drag_source_set (
 60                                     ['button1_mask', 'button3_mask'],
 61                                     ['copy'],
 62                                     {
 63                                     'target' => 'text/plain',
 64                                     'flags' => [],
 65                                     'info' => ID_LABEL,
 66                                     },
 67         );
 68 
 69             my $text_to_drag = '<span foreground="red" size="x-large">"Well there\'s another nice mess you\'ve gotten me into."</span>';
 70 
 71         #set up the data which needs to be fed to the drag destination (drop)
 72         $event_box->signal_connect ('drag-data-get' => \&source_drag_data_get,$text_to_drag );
 73 
 74     $h_paned->pack2($event_box,TRUE,FALSE);
 75 
 76 $vbox->add($h_paned);
 77 $vbox->show_all();
 78 return $vbox;
 79 }
 80 
 81 sub create_iconview {
 82 #---------------------------------------------------
 83 #Creates an Iconview in a ScrolledWindow. This -----
 84 #Iconview has the ability to drag items off it  -----
 85 #---------------------------------------------------
 86 
 87     my $icon_string= undef;
 88     my $tree_model = create_iconview_model();
 89 
 90     my $icon_view = Gtk2::IconView->new_with_model($tree_model);
 91     $icon_view->set_markup_column(C_MARKUP);
 92     $icon_view->set_pixbuf_column(C_PIXBUF);
 93 
 94     #Enable the Gtk2::IconView as a drag source
 95     $icon_view->drag_source_set (
 96                                 ['button1_mask', 'button3_mask'],
 97                                 ['copy'],
 98                                 {
 99                                     'target' => 'STRING',
100                                     'flags' => [],
101                                     'info' => ID_ICONVIEW,
102                                 },
103                         );
104 
105     #This is a nice to have. It changes the drag icon to that of the
106     #icon which are now selected and dragged (single selection mode)
107     $icon_view->signal_connect('drag-begin' => sub { 
108         $icon_view->selected_foreach ( sub{
109 
110                 my $iter =$tree_model->get_iter($_[1]);
111                 #set the text and pixbuf
112                 my $icon_pixbuf = $tree_model->get_value($iter,C_PIXBUF);
113                 $icon_string = $tree_model->get_value($iter,C_MARKUP);
114                 $icon_view->drag_source_set_icon_pixbuf ($icon_pixbuf);
115         } );
116     });
117 
118     #set up the data which needs to be fed to the drag destination (drop)
119     $icon_view->signal_connect ('drag-data-get' => sub { source_drag_data_get(@_,$icon_string) } );
120     
121     #Standard scrolledwindow to allow growth
122     my $sw = Gtk2::ScrolledWindow->new(undef,undef);
123     $sw->set_policy('never','automatic');
124     $sw->add($icon_view);
125     $sw->set_border_width(6);
126     $sw->set_size_request(600,150);
127     return $sw;
128 }
129 
130 sub create_iconview_model {
131 #----------------------------------------------------
132 #The Iconview needs a Gtk2::Treemodel implementation-
133 #containing at least a Glib::String and -------------
134 #Gtk2::Gdk::Pixbuf type. The first is used for the --
135 #text of the icon, and the last for the icon self----
136 #Gtk2::ListStore is ideal for this ------------------
137 #----------------------------------------------------
138 
139     my $list_store = Gtk2::ListStore->new(qw/Glib::String Gtk2::Gdk::Pixbuf/);
140 
141     #******************************************************
142     #we populate the Gtk2::ListStore with Gtk2::Stock icons
143     #******************************************************
144 
145     my $icon_factory = Gtk2::IconFactory->new();
146 
147     foreach my $val(sort Gtk2::Stock->list_ids){
148         #get the iconset from the icon_factory
149         my $iconset = $icon_factory->lookup_default($val);
150         #try and extract the icon from it
151         my $pixbuf = $iconset->render_icon(Gtk2::Style->new(),'none','normal','dnd',undef);
152 
153         #if there was a valid icon in the iconset, add it
154         if( defined $pixbuf ){
155 
156             my $iter = $list_store->append;
157             $list_store->set (
158                         $iter,
159                         C_MARKUP, "<b>$val</b>",
160                         C_PIXBUF, $pixbuf,
161              );
162 
163         }
164     }
165 
166     return $list_store;
167 }
168 
169 sub create_simple_list {
170 #---------------------------------------------------
171 #Creates a simple list with pixbuf and markup  -----
172 #columns make this list the drop target for various-
173 #drag sources --------------------------------------
174 #---------------------------------------------------
175 
176     my $slist = Gtk2::SimpleList->new ('' => 'pixbuf', ''  => 'markup');
177     $slist->set_rules_hint(TRUE);
178     $slist->set_headers_visible(FALSE);
179 
180      #Also suggest to try Nautilus
181      $tooltips->set_tip ($slist,
182             'Drag a few files from Nautilus '.
183             'and drop it here'
184             );
185 
186     #Create a target table to receive drops
187     my @target_table = (
188 
189         {'target' => 'STRING',       'flags' => [], 'info' => ID_ICONVIEW   },
190         {'target' => "text/uri-list",'flags' => [], 'info' => ID_URI        },
191         {'target' => "text/plain",  'flags' => [], 'info' => ID_LABEL       },
192     );
193 
194     #make this the drag destination (drop) for various drag sources
195     $slist->drag_dest_set('all', ['copy'], @target_table);
196 
197     #do a callback as soon as drag data is received
198     $slist->signal_connect ('drag-data-received' => \&target_drag_data_received,$slist );
199 
200     #Standard scrolledwindow to allow growth
201     my $sw = Gtk2::ScrolledWindow->new(undef,undef);
202     $sw->set_policy('never','automatic');
203     $sw->add($slist);
204     $sw->set_border_width(6);
205     $sw->set_size_request(600,150);
206     return $sw;
207 }
208 
209 sub target_drag_data_received {
210 #---------------------------------------------------
211 #Extract the data which was set up during the  -----
212 #'drag-data-get' event which fired just before -----
213 #the 'drag-data-received' event. Also checks which--
214 #source supplied the data, and handle accordingly----
215 #---------------------------------------------------
216 
217     my ($widget, $context, $x, $y, $data, $info, $time,$slist) = @_;
218 
219     my $icon_factory = Gtk2::IconFactory->new();
220 
221     if ($info eq ID_LABEL){
222         my $iconset = $icon_factory->lookup_default('gtk-no');
223         my $pixbuf = $iconset->render_icon(Gtk2::Style->new(),'none','normal','menu',undef);
224         push @{$slist->{data}}, [ $pixbuf, $data->data ];
225     }
226 
227     if ($info eq ID_URI){
228         my $iconset = $icon_factory->lookup_default('gtk-yes');
229         my $pixbuf = $iconset->render_icon(Gtk2::Style->new(),'none','normal','menu',undef);
230 
231         foreach ($data->get_uris){
232             push @{$slist->{data}}, 
233                     [ $pixbuf, '<span foreground="forest green" size="x-small">'.$_.'</span>' ];
234         }
235     }
236 
237     if ($info eq ID_ICONVIEW){
238 
239         my $no_markup = $data->data;
240         $no_markup =~ s/<[^>]*>//g;
241         my $iconset = $icon_factory->lookup_default($no_markup);
242         my $pixbuf = $iconset->render_icon(Gtk2::Style->new(),'none','normal','menu',undef);
243         push @{$slist->{data}}, [ $pixbuf, $data->data ];
244     }
245 
246     $context->finish (0, 0, $time);
247 }
248 
249 sub source_drag_data_get {
250 #---------------------------------------------------
251 #This sets up the data of the drag source. It is ---
252 #required before the 'drag-data-received' event ----
253 #fires which can be used to extract this data ------
254 #---------------------------------------------------
255 
256     my ($widget, $context, $data, $info, $time,$string) = @_;
257 
258     $data->set_text($string,-1);
259 }

21.1.6. Code that needs further explanation.

21.1.6.1. Fields and Factories

In this sample we make use of the Gtk2::IconFactory class. This class is used to store Gtk2::IconSets. It also contains all the Gtk2::Stock items which contains icons (as Gtk2::IconSets). To get the iconset for a certain Gtk2::Stock item, we call the 'lookup_default' method on an instance of Gtk2::IconFactory.

#get the iconset from the icon_factory
my $iconset = $icon_factory->lookup_default('gtk-ok');
A Gtk2::IconSet bring forth various sizes of icons. This makes it easy to use when you want the same icon in various sizes. A list of possible values for the size of the icon from the Gtk2::IconSet can be extracted with the 'get_sizes' method.

To extract the Gtk2::Gdk::Pixbuf from the Gtk2::IconSet we use its render_icon method. We then specify a required value for Gtk2::IconSize as an argument to the method in order to manipulate the size.

#try and extract the icon from it
my $pixbuf = $iconset->render_icon(Gtk2::Style->new(),'none','normal','dnd',undef);

21.1.6.2. The Gtk2::IconView.

This is a viewer for the typical MVC (Model-View-Controller) design pattern. It makes use of the Gtk2::ListStore as the implementation of the Gtk2::TreeModel interface. This model must contain at least a Gtk2::Gdk::Pixbuf column containing pixbufs of the icons to display.

We connect the Gtk2::Gdk::Pixbuf column in the Gtk2::ListStore to the Gtk2::IconView using its set_pixbuf_column method.

If you need to display text below the icons, you may specify a Glib::String type on the Gtk2::ListStore model. This gets connected to the Gtk2::IconView using its set_text_column. This text can be Gtk2::Pango markup, then you simply connect to the model using the Gtk2::IconView's set_markup_column method.

Tip

Again you can make life easier for yourself by using constants instead of integers to keep track of the columns.

21.1.6.3. Gtk2::Paned

A paned widget draws a separator between the two child widgets and a small handle that the user can drag to adjust the division. It does not draw any relief around the children or around the separator.

You can read more about Gtk2::Paned widgets on its POD.

Tip

To force a minimum size on a widget, you can use the set_size_request method from the Gtk2::Widget class. This helps in setting up the initial shape of widgets packed inside a Gtk2::Paned widget;

21.1.6.4. Drag and Drop

The following section takes the points mentioned at the start of this lesson, and show how each one is implemented in the program.

  • We declare the Gtk2::IconView as a drag source

    #Enable the Gtk2::IconView as a drag source
    $icon_view->drag_source_set (
        ['button1_mask', 'button3_mask'],
        ['copy'],
        {
        'target' => 'STRING',
        'flags' => [],
        'info' => ID_ICONVIEW,
        },
    );

  • We declare the Gtk2::SimpleList as a drag destination. Here we specify a few sources it will accept data from.

    #Create a target table to receive drops
    my @target_table = (
    
        {'target' => 'STRING',       'flags' => [], 'info' => ID_ICONVIEW   },
        {'target' => "text/uri-list",'flags' => [], 'info' => ID_URI        },
        {'target' => "text/plain",  'flags' => [], 'info' => ID_LABEL       },
    );
    
    #make this the drag destination (drop) for various drag sources
    $slist->drag_dest_set('all', ['copy'], @target_table);

  • When a user starts to drag an icon, we want to show which one was chosen.

    #This is a nice to have. It changes the drag icon to that of the
    #icon which are now selected and dragged (single selection mode)
    $icon_view->signal_connect('drag-begin' => sub { 
            $icon_view->selected_foreach ( sub{
        
                my $iter =$tree_model->get_iter($_[1]);
                #set the text and pixbuf
                my $icon_pixbuf = $tree_model->get_value($iter,C_PIXBUF);
                $icon_string = $tree_model->get_value($iter,C_MARKUP);
                $icon_view->drag_source_set_icon_pixbuf ($icon_pixbuf);
            } );
    });
    The second argument passed the the sub called by selected_foreach is a Gtk2::TreePath. We use this to get a Gtk2::TreeIter pointing to the selected icon. Since we are in single selection mode, the foreach will only run once.

  • To set up the data on the drag source source:

    #set up the data which needs to be fed to the drag destination (drop)
    $icon_view->signal_connect ('drag-data-get' => sub { source_drag_data_get(@_,$icon_string) });

  • To extract the data on the drag destination:

    #do a callback as soon as drag data is received
    $slist->signal_connect ('drag-data-received' => \&target_drag_data_received,$slist );

    Note

    If you want to change the appearance of the drag destination as soon as a drag source enters it, connect a callback to the drag-motion signal of the drag destination.

21.1.6.5. Gtk2::ToolTips.

Tooltips are a wonderful way to guide a user into what is expected of him/her. It is also nice to give a bit more detail on certain widgets and their functions.

To create tooltips, first declare a Gtk2::ToolTips object. Then add the widgets with the the tooltip text to it. To do this, make use of the set_tip method.

#Try and convince the user otherwise
$tooltips->set_tip (
    $event_box,
    'No way! You have to drag it at least once!'.
    'Drag it, and drop it into the bottom list'
);    

21.1.7. Conclusion.

This brings us to the end of the drag and drop section. I trust this may get you out of a nice mess some day.