FLTK logo

Article #770: Receiving Drag and Drop By Example

FLTK matrix user chat room
(using Element browser app)   FLTK gitter user chat room   GitHub FLTK Project   FLTK News RSS Feed  
  FLTK Apps      FLTK Library      Forums      Links     Login 
 Home  |  Articles & FAQs  |  Bugs & Features  |  Documentation  |  Download  |  Screenshots  ]
 

Return to Articles | Show Comments | Submit Comment ]

Article #770: Receiving Drag and Drop By Example

Created at 08:07 Jan 09, 2008 by alvin

Last modified at 05:38 Jan 15, 2008

Introduction

This short tutorial discusses how to take advantage of FLTK's Drag 'n Drop (DND) infrastructure in your FLTK applications. It is the goal of this tutorial to be a companion to the Drag and Drop Events section of the FLTK documentation. It is assumed that the reader has a working knowledge of FLTK's event model. Furthermore, the reader is encouraged to read the Subclassing section in the FLTK manual.

FLTK provides both receiving and sending DND operations. This tutorial will focus solely on receiving DND events. Sending DND events will be covered in another tutorial.

Background


    FLTK provides Drag 'n Drop facilities by send a series of DND events to the application's widgets. These events are:


        
  • FL_DND_ENTER<br/>Indicates that DND is beginning. If a widget is interested in receiving any further DND events, its handle() function must return 1. If 0 is returned, no further DND events will be received by that widget.

  •     
  • FL_DND_LEAVE<br/>The user has left the widget without completing the DND action. Typically the widget's handle() function returns 1.

  •     
  • FL_DND_DRAG<br/>The user is moving (a.k.a. dragging) something over the widget. The widget's handle() function must return 1. Typically, the widget would indicate, to the user, where the drop will occur.

  •     
  • FL_DND_RELEASE<br/>The user has released (a.k.a. dropped) something onto the widget. Upon receiving this event, the widget can expect to receive a FL_PASTE event.

  •     
  • FL_PASTE<br/>Indicates that buffer returned by Fl::event_text() is ready to be read by the widget. The widget's handle() function must return 1 to indicate that it has accepted the event.

Accessing the DND Payload

The DND payload is a null-terminated block of text. Depending on the nature of the DND operation, the block of text will either be a string containing one or more URLs or a string of plain text. For example, a DND operation originating from a file browser (e.g. Konqueror) would likely be a list of file URLs. Whereas DND received from a text editor would most likely be a plain string of characters (e.g. a sentence fragment, paragraph, etc.).

Upon receiving a FL_PASTE event, the buffer returned by Fl::event_text() will contain the text that was dropped onto the widget. Use Fl::event_length() to determine the length of that buffer. The text contained in the buffer will either be one or more URLs or simply just a block of plain text. It is the responsibility of the widget to distinguish between the two. If the buffer contains one or more URLs, it will have the following characteristics:


        
  • Protocol prefix such as: "file://", "http://", "https://", "ftp://", etc.

  •     

  •         May contain URL escape codes. An abbreviated list is provided below:<br/> <br/> SPACE  %20<br/> <  %3C<br/> >  %3E<br/> #  %23<br/> &  %26<br/> @  %40<br/> <br/>
        

  •     
  • Suffix applied to each URL: \r\n
Here are some examples of what Fl::event_text() may return:<br/>

        
  1. file:///usr/share/wallpapers/blue_bend.jpg\r\n

  2.     
  3. file:///usr/share/wallpapers/blue%20bend.jpg\r\n

  4.     
  5. http://www.fltk.org/index.html\r\n

  6.     
  7. file:///usr/share/doc/fltk/index.html\r\nfile:///usr/share/doc/fltk/Fl_Box.html\r\nfile:///usr/share/doc/fltk/events.html\r\n
FLTK does not provide any facility to decode the URLs or handle the protocols. It is the developer's responsibility to deal with each URL appropriately.<br/> <br/> In the example code below, dnd_open() passes the URL contained in Fl::event_text() to a Fl_Shared_Image class. However, before doing so, the file path must first be extracted from the URL. This is necessary since the Fl_Shared_Image class does not provide any functionality for handling URLs. It is important to note that the implementation used in the example is greatly flawed since URL encoded characters are not handled.

FL_PASTE Caveat

One final note concern the FL_PASTE event. When handling the FL_PASTE event, do not create any windows (e.g. calling fl_choice() for user confirmation). Doing so will interrupt FLTK's DND system and likely cause the application that originated the DND operation to freeze or perhaps crash. That said, the workaround is to defer creating the window to after the widget's handle() has returned 1. An example of this workaround has been included in the example code below and is discussed in the following section.

The Example

The following example code illustrates using a Drag 'n Drop operation to load an image. The image loading capability is provided by FLTK's Fl_Shared_Image class.

The example below creates a Fl_DND_Box widget to handle all the DND processing. The Fl_DND_Box's callback is executed upon receiving the final FL_PASTE event. The benefit of using such a widget is that DND receiving capability can be added to existing FLTK applications by simply adding the Fl_DND_Box widget to the appplication such that is on top and completely covers the target widget. This allows any widget to effectively become a DND target.

By default, the Fl_DND_Box is constructed with FL_NO_LABEL, FL_NO_BOX and with keyboard focus disabled. This effectively makes the Fl_DND_Box widget invisible but, at the same time, be able to receive FLTK events.

Spawning a Window

When a drop action occurs, it is often appropriate for the application to request confirmation of the action before proceeding. This is easily accomplished by the caller executing fl_choice(). However, doing so would require a new window to be displayed thus interrupting FLTK's DND operation. The solution to is to employ the workaround to the FL_PASTE caveat, mentioned in the previous section. This workaround has been included in the Fl_DND_Box widget. The workaround is accomplished by using a FLTK timeout callback of 0 seconds. The timeout callback is executed by the FLTK main-loop once Fl_DND_Box's handle() function has returned. This allows FLTK's DND system to properly complete the DND operation as well as allow the caller to spawn a FLTK window (e.g. calling fl_choice()). It is possible to replace the use of a timeout with a FLTK idle callback. However, the idle callback would need to remove itself which results in an extra step that is not required for the FLTK timeout (i.e. a FLTK timeout automatically removes itself upon completion).

A side effect of this workaround is that the caller cannot rely on Fl::event(), Fl::event_text() or Fl::event_length() to determine why the callback is being executed or to access the DND payload. This doesn't necessarily matter in this example, however if Fl_DND_Box where to be extended, say to support initiating Drag 'n Drop (as will be done in the next tutorial), then this does matter. One solution is for Fl_DND_Box to contain additional local members, int evt, char* evt_txt and int evt_len that retains the FLTK event and DND payload. The caller then uses int Fl_DND_Box::event(), const char* Fl_DND_Box::event_text() and int Fl_DND_Box::event_length() to determine why the callback has been executed and, if appropriate, to access the DND payload.

The remainder of the example code deals with acting upon the DND payload. The specifics of which have been discussed previously in this tutorial. To reiterate, FLTK does not provide any means of handling the various protocols. There are many solutions for this ranging from parsing the buffer to utilising third party libraries, such as libcurl, and as such are beyond the scope of this tutorial.

The example code has been commented, where appropriate, and as such should be fairly readable.

Suggestions, Comments, or Questions?

If there are any comments, suggestions or questions concerning this tutorial, please post them in the comments section below. If there are any questions concerning FLTK itself, please direct them to the fltk.general newsgroup.

Enjoy and happy coding,<br/><br/> Alvin

Example Code

To compile the example, use

fltk-config --use-images --compile dndtest0.cxx
Once compiled, execute
dndtest0
and simply drag an image from your favourite file browser (e.g. Konqueror) onto the engraved area in the application. A dialog box requesting confirmation will appear. If accepted, the image displayed.

// A trival example illustration how to load an image
// using Drag and Drop events.
//
// * Compile using: flt-config --use-images --compile dndtest0.cxx
//
// * Drag an image from your favourite file browser onto the
//         marked box.

#include <FL/Fl.H>
#include <FL/Fl_Double_Window.H>
#include <FL/Fl_Scroll.H>
#include <FL/Fl_Box.H>
#include <FL/Fl_Shared_Image.H> // Image I/O
#include <FL/fl_ask.H>          // fl_alert, fl_choice
#include <FL/filename.H>        // fl_filename_name
#include <string.h>             // strncmp, strlen, etc.


class Fl_DND_Box : public Fl_Box
{
    public:

        static void callback_deferred(void *v)
        {
            Fl_DND_Box *w = (Fl_DND_Box*)v;

            w->do_callback();
        }

        Fl_DND_Box(int X, int Y, int W, int H, const char *L = 0)
                : Fl_Box(X,Y,W,H,L), evt(FL_NO_EVENT), evt_txt(0), evt_len(0)
        {
            labeltype(FL_NO_LABEL);
            box(FL_NO_BOX);
            clear_visible_focus();
        }

        virtual ~Fl_DND_Box()
        {
            delete [] evt_txt;
        }

        int event()
        {
            return evt;
        }

        const char* event_text()
        {
            return evt_txt;
        }

        int event_length()
        {
            return evt_len;
        }

        int handle(int e)
        {
            switch(e)
            {
                case FL_DND_ENTER:
                case FL_DND_RELEASE:
                case FL_DND_LEAVE:
                case FL_DND_DRAG:
                    evt = e;
                    return 1;


                case FL_PASTE:
                    evt = e;

                    // make a copy of the DND payload
                    evt_len = Fl::event_length();

                    delete [] evt_txt;

                    evt_txt = new char[evt_len];
                    strcpy(evt_txt, Fl::event_text());

                    // If there is a callback registered, call it.
                    // The callback must access Fl::event_text() to
                    // get the string or file path that was dropped.
                    // Note that do_callback() is not called directly.
                    // Instead it will be executed by the FLTK main-loop
                    // once we have finished handling the DND event.
                    // This allows caller to popup a window or change widget focus.
                    if(callback() && ((when() & FL_WHEN_RELEASE) || (when() & FL_WHEN_CHANGED)))
                        Fl::add_timeout(0.0, Fl_DND_Box::callback_deferred, (void*)this);
                    return 1;
            }

            return Fl_Box::handle(e);
        }

    protected:
        // The event which caused Fl_DND_Box to execute its callback
        int evt;

        char *evt_txt;
        int evt_len;
};

// Widget that displays the image
Fl_Box *box = (Fl_Box*)0;

void file_close()
{
    Fl_Shared_Image *img = (Fl_Shared_Image*)box->image();

    if(!img)
        return; // no image displayed

    box->image(0);
    
    // The image is shared, release until no more references
    while(img->refcount())
        img->release();
}

void file_open(const char *fpath)
{
    file_close();

    Fl_Shared_Image *img = Fl_Shared_Image::get(fpath);
    
    if(!img)
    {
        fl_alert("Failed to load image: %s", fpath);
        return;
    }

    box->size(img->w(), img->h());
    box->image(img);

    Fl::redraw();
}


void dnd_open(const char *urls)
{
    // count number of files dropped
    int cnt = 0;

    for(const char *c = urls; *c != '\0'; ++c)
    {
        if(*c == '\n')
            ++cnt;
    }

    if(cnt != 1)
    {
        fl_alert("Invalid number of files being dropped: %d.\nDrop 1 file only.", cnt);
        return;
    }
    
    if(strncmp(urls, "file://", 7) != 0)
    {
        fl_alert("Unsupported URL: %s\nOnly local files are supported in this demo.", urls);
        return;
    }

    // Fl::event_text() gives URLs in the format: file:///path/to/image\r\n
    // which is not supported by Fl_Shared_Image. Therefore, we extract
    // the /path/to/image

    // -7 == file:// -2 == \r\n
    int fsz = strlen(urls) - 7 - 2;

    // +1 == '\0'
    char *fpath = new char[fsz+1];

    // copy only what we need
    strncpy(fpath, urls+7, fsz);
    fpath[fsz] = '\0';

    // NOTE: fpath may contain URL escape codes which we really should decode.
    //       For this example, they are ignored.
    if(fl_choice("Do you really want to open image: %s?", "&No", "&Yes", 0, fl_filename_name(fpath)) == 1)
        file_open(fpath);

    delete [] fpath;
}

void dnd_cb(Fl_Widget *o, void *v)
{
    Fl_DND_Box *dnd = (Fl_DND_Box*)o;

    if(dnd->event() == FL_PASTE)
        dnd_open(dnd->event_text());
}

int main(int argc, char *argv[])
{
    Fl::visual(FL_DOUBLE | FL_INDEX);
    Fl::get_system_colors();
    fl_register_images();
    Fl::scheme("gtk+");

    Fl_Double_Window *wnd = new Fl_Double_Window(10, 10, 500, 400, "DND Example");
    {
        {
            Fl_Box *o = new Fl_Box(15, 15, 470, 45, "Drag an image from your favourite file browser onto the area below.");
            o->box(FL_ROUNDED_BOX);
            o->align(FL_ALIGN_INSIDE | FL_ALIGN_WRAP| FL_ALIGN_CENTER);
            o->color((Fl_Color)215);
            o->labelfont(FL_HELVETICA_BOLD);
        }

        { // Fl_Group is necessary so that the Fl_DND_Box will resize along with Fl_Scroll
            Fl_Group *o = new Fl_Group(15, 70, 470, 320, 0);
            o->box(FL_NO_BOX);
            {
                {
                    Fl_Scroll *o = new Fl_Scroll(15, 70, 470, 320, 0);
                    o->box(FL_ENGRAVED_BOX);
                        box = new Fl_Box(17, 72, 466, 316, 0);
                        box->box(FL_FLAT_BOX);
                        box->align(FL_ALIGN_INSIDE | FL_ALIGN_CENTER);
                    o->end();
                }

                { // Fl_DND_Box is constructed with the same dimensions and at the same position as Fl_Scroll
                    Fl_DND_Box *o = new Fl_DND_Box(17, 72, 466, 316, 0);

                    o->callback(dnd_cb);
                }
            }
            o->end();
            Fl_Group::current()->resizable(o);
        }
    }
    wnd->end();
    wnd->show(argc, argv);

    return Fl::run();
}
<br/>

Listing ]


Comments

Submit Comment ]

From alvin, 09:59 Feb 07, 2008 (score=4)

I finally had the time/need to decode URL encoded paths in the DND payload. Here is the routine I'm using. It's doesn't handle every URL escape code, but hopefully provides a good start.

To add new escape codes, at the appropriate entry to the lookup_table.

Comments, suggestions, bugs fixes, escape code additions are all welcomed!

// url_decode.h

#ifndef URL_DECODE_H
#define URL_DECODE_H

/*****************************************************************************/

// Decodes the URL encoded string s. Note that the URL scheme is not removed.
// Caller must 'delete' the return value once it is no longer needed.
//
// s must be null-terminated and must be a valid URL escaped string. If not
// expect undefined behavious as in segfaults and the like.
char* decode(const char *s);

/*****************************************************************************/

#endif
<br/><br/>
// url_decode.cpp
#include "url_decode.h"
#include <FL/filename.H>  // FL_PATH_MAX
#include <string.h>
#include <stdio.h>        // fprintf

/*****************************************************************************/

// decode lookup table: hex code, character
// A very short Google search revealed these ones. Add to the list as needed.
// List must be terminated with a empty entry

static const char *lookup_table[][2] = {
   {"20", " "}, {"24", "$"}, {"23", "#"}, {"25", "%"},  {"26", "&"}, {"27", "'"},
   {"2F", "/"}, {"3A", ":"}, {"3B", ";"}, {"3C", "<"},  {"3D", "="}, {"3E", ">"},
   {"3F", "?"}, {"40", "@"}, {"5B", "["}, {"5C", "\\"}, {"5D", "]"}, {"5E", "^"},
   {"60", "`"}, {"7B", "{"}, {"7C", "|"}, {"7D", "}"}, {"7E", "~"},
   {0, 0}
};

/*****************************************************************************/

const char *decode_lookup(const char *s)
{
   const char *val    = NULL;
   const char **entry = NULL;

   for(int i = 0; lookup_table[i][0]; i++)
   {
      entry = lookup_table[i];

      if(!strcasecmp(s, entry[0]))
      {
         // found match
         val = entry[1];
         break;
      }
   }

   return val;
}

/*****************************************************************************/

char* decode(const char *s)
{
   char s2[FL_PATH_MAX];   // Buffer to hold the decoded string
   char hexcode[3];        // Buffer to hold the 2-digit hex code.

   int i = 0;   // index into s2

   const char *symbol = NULL;   // The decoded character

   // init the buffers
   memset(s2, '\0', FL_PATH_MAX*sizeof(char));
   memset(hexcode, '\0', 3*sizeof(char));

   // Decode s but looking at each character.
   // If a '%' found, then decode the 2-digit
   // hex value that follows it. If the hex value
   // is missing or not in our limited table,
   for(const char *c = s; *c != '\0' && i < FL_PATH_MAX; ++c)
   {
      // Only dive into the encoding if the '%' occurrs within 2+1 positions
      // from the end.
      if(*c == '%' && (c - s > 3))   // start of hex code
      {
         strncpy(hexcode, c+1, 2); // copy the 2 digits

         symbol = decode_lookup(hexcode);

         // Was hexcode known?
         if(symbol)
         {
            // Special cases.
            switch(*symbol)
            {
               // If s has a %25%25 it should be resolved to
               // a singled '%'. Atleast this is what I noticed
               // should happen with Konqueror encoded strings
               case '%':
                  if(i == 0 || s2[i-1] != '%')
                     s2[i++] = *symbol;
               break;

               default:
                  s2[i++] = *symbol;
            }
         }
         else
            fprintf(stderr, "decode: Unknown hex code: %%%s\n", hexcode);

         c += 2; // skip over the hex digits
   }
   else
      s2[i++] = *c;
}

   // Make a stand-alone copy for the caller
   int len      = strlen(s2);
   char *retval = new char[len + 1]; // +1='\0'

   // Should be safe to use strcpy here since s2 is init'd with all '\0'
   strcpy(retval, s2);

   return retval;
}

Reply ]

From Anonymous, 18:32 Dec 06, 2008 (score=3)

It should be noted that on Windows local file paths may not be prepended with "file://" -- it may be a good idea to check the second character of the dragged string against ':' when determining if it is a local file.

The code I discovered this in is cross-compiled for msw from linux using mingw.
Reply ]

From greg.ercolano, 19:02 Jan 10, 2008 (score=3)

I made some mods to dnd_open() that make it work for Firefox on linux, which seems to send 16 bit chars for the URLs, where every other byte is a NUL.

So I'm using a simple hack here to detect ascii in wide char format. The more correct thing would be to use iconv(3), but I didn't want to put a dependency on that, as you have to install iconv(3) separately for windows.

So basically I replaced dnd_open() from the example with the following, and modified the call to dnd_open() to include the new second argument:

BEFORE:   dnd_open(Fl::event_text());
 AFTER:   dnd_open(Fl::event_text(), Fl::event_length());
Here's the code to replace dnd_open():

// Assume pairs of bytes: [char] [nul] [char] [nul] .. [nul] [nul]
void multibyte_convert(const char *in, char *out)
{
     for ( *out = *in; *in; )
        {  out++; in += 2; *out = *in; }
}

void dnd_open(const char *urls, size_t length)
{
    // Check for zero (OSX sometimes does this)
    if ( urls == 0 || length == 0 ) return;

    char *fpath = new char [length+1];

    // Check for multibyte chars from Firefox/linux.
    //    Length will be >1 and second byte will be zero.
    //
    if ( strlen(urls) != length && length > 1 && urls[1] == 0 )
        { multibyte_convert(urls, fpath); }
    else
        { strcpy(fpath, urls); }

    // Count number of files, truncate at first CRLF
    int cnt = 0;
    for ( char *c = fpath; *c; c++ )
    {
        if ( *c == '\n' ) { *c = 0; cnt++; }
        if ( *c == '\r' ) { *c = 0; }
    }

    if (cnt != 1)
    {
        fl_alert("Invalid number of files being dropped: %d.\nDrop 1 file only.", cnt);
        delete [] fpath;
        return;
    }

    // Handle file: prefix
    if ( strncmp(fpath, "file:", 5) == 0 )
        { strcpy(fpath, fpath+5); }

    // NOTE: fpath may contain URL escape codes which we
    // really should decode. For this example, they are ignored.
    //
    if (fl_choice("Do you really want to open image: %s?", "&No", "&Yes", 0, fl_filename_name(fpath)) == 1)
        { file_open(fpath); }

    delete [] fpath;
}

Reply ]

From alvin, 05:25 Jan 10, 2008 (score=3)

I have updated the article to include a "Spawning a Window" section. I have also fixed the automagic anchoring the the website applies to strings such as http://

I also explained the reason for Fl_DND_Box::evt and updated Fl_DND_Box to use an accessor function, Fl_DND_Box::event(), rather than declaring evt public.
Reply ]

 
 

Comments are owned by the poster. All other content is copyright 1998-2024 by Bill Spitzak and others. This project is hosted by The FLTK Team. Please report site problems to 'erco@seriss.com'.