Chapter 5. Drag and Drop
Prev | Part I. The Basics | Next

## Chapter 5. Drag and Drop

One of the more powerful features available to FOX applications is drag-and-drop. It's also one of the more complicated to understand. For more background, see the standard FOX documentation on Drag and Drop.

## Drop Sites

We're going to start by presenting a skeleton application consisting of a main window widget (a `DropSite` instance) that parents an `FXCanvas` widget:

    require 'rubygems'
    require_gem 'fxruby'

    include Fox

    class DropSite < FXMainWindow
      def initialize(anApp)
        # Initialize base class
        super(anApp, "Drop Site", nil, nil, DECOR_ALL, 0, 0, 400, 300)

        # Fill main window with canvas
        @canvas = FXCanvas.new(self, nil, 0, LAYOUT_FILL_X|LAYOUT_FILL_Y)
      end

      def create
        # Create the main window and canvas
        super

        # Show the main window
        show(PLACEMENT_SCREEN)
      end
    end

    if __FILE__ == $0
      FXApp.new("DropSite", "FXRuby") do |theApp|
        DropSite.new(theApp)
        theApp.create
        theApp.run
      end
    end

Since the main program (i.e. the part at the end) won't change for the rest of the tutorial, I won't show that code anymore. Since an `FXCanvas` widget relies on some other object (its message target) to draw its contents, we need to handle `SEL_PAINT` messages generated by the canvas. We'll do that by adding a handler that clears the canvas to its current background color:

    require 'rubygems'
    require_gem 'fxruby'

    include Fox

    class DropSite < FXMainWindow
      def initialize(anApp)
        # Initialize base class
        super(anApp, "Drop Site", nil, nil, DECOR_ALL, 0, 0, 400, 300)

        # Fill main window with canvas
        @canvas = FXCanvas.new(self, nil, 0, LAYOUT_FILL_X|LAYOUT_FILL_Y)

        # Handle expose events on the canvas
        @canvas.connect(SEL_PAINT) { |sender, sel, event|
          FXDCWindow.new(@canvas, event) { |dc|
            dc.foreground = @canvas.backColor
            dc.fillRectangle(event.rect.x, event.rect.y, event.rect.w, event.rect.h)
          }
        }
      end

      def create
        # Create the main window and canvas
        super

        # Show the main window
        show(PLACEMENT_SCREEN)
      end
    end

Run this basic version of the program to be sure that it's working properly so far. You should simply see an empty window with a white background.

Now, on to the fun stuff. Our goal is to be able to drag color data from some other window, such as an `FXColorWell` widget, and drop it onto the canvas in order to change the canvas' background color. In order for a FOX widget to be able to accept drops at all, we need to first call its `dropEnable()` method:

    def initialize(anApp)
      # Initialize base class
      super(anApp, "Drop Site", nil, nil, DECOR_ALL, 0, 0, 400, 300)

      # Fill main window with canvas
      @canvas = FXCanvas.new(self, nil, 0, LAYOUT_FILL_X|LAYOUT_FILL_Y)

      # Handle expose events on the canvas
      @canvas.connect(SEL_PAINT) { |sender, sel, event|
        FXDCWindow.new(@canvas, event) { |dc|
          dc.foreground = @canvas.backColor
          dc.fillRectangle(event.rect.x, event.rect.y, event.rect.w, event.rect.h)
        }
      }

      # Enable canvas for drag-and-drop messages
      @canvas.dropEnable
    end

At this point, let's try a little test to see if the program does anything interesting yet. Start by running some other FOX or FXRuby program to use as a drag source for the color data. You should be able to use any program that displays an FXColorWell widget, and this includes the standard color dialog box shown here:

Each of the small colored boxes near the bottom of the color dialog box are color wells, and the large box on the left-hand side of the color dialog box is also a color well. Now start your drag-and-drop test program and try to drag a color from one of these color wells onto this window. At this point, the mouse pointer should turn into a stop sign, indicating that the canvas isn't accepting drops of color data yet.

To correct this problem, we need to use the canvas' `acceptDrop()` method to indicate whether or not we'll accept certain kinds of drops. You can call `acceptDrop()` any time after receiving the initial `SEL_DND_ENTER` message, but it's usually done in response to a `SEL_DND_MOTION` message. Let's add a handler for `SEL_DND_MOTION` messages from the canvas in DropSite's initialize() method. For now, we'll unconditionally accept drops from any drag source, regardless of what kind of data they're offering:

    def initialize(anApp)
      # Initialize base class
      super(anApp, "Drop Site", nil, nil, DECOR_ALL, 0, 0, 400, 300)

      # Fill main window with canvas
      @canvas = FXCanvas.new(self, nil, 0, LAYOUT_FILL_X|LAYOUT_FILL_Y)

      # Handle expose events on the canvas
      @canvas.connect(SEL_PAINT) { |sender, sel, event|
        FXDCWindow.new(@canvas, event) { |dc|
          dc.foreground = @canvas.backColor
          dc.fillRectangle(event.rect.x, event.rect.y, event.rect.w, event.rect.h)
        }
      }

      # Enable canvas for drag-and-drop messages
      @canvas.dropEnable

      # Handle SEL_DND_MOTION messages from the canvas
      @canvas.connect(SEL_DND_MOTION) {
        # Accept drops unconditionally (for now)
        @canvas.acceptDrop
      }
    end

Now try the previous test again. This time, when you try to drag from a color well to the drop-enabled canvas, you should see the mouse pointer turn into a small filled square. This is a visual cue to the user indicating that the canvas will accept a drop of the drag-and-drop data.

Now it's time to get more specific about what kind of data is being dragged between these applications, and how to process that data. So far, our drop-enabled canvas merely knows that you're dragging some kind of data from a drag source, but it doesn't know what kind of data it is. We really need to be more exclusive about what kinds of data are acceptable. FOX uses unique drag types to distinguish between different kinds of "draggable" data. As you'll see later, you have the freedom to define drag types for any kind of application-specific data that you need; but for now, we're going to use FOX's built-in drag type for color data.

Drag types (even the standard ones) must be registered before they can be used, and so we'll start by adding a few lines to `DropSite`'s `create()` method to register the drag type for color data:

    def create
      # Create the main window and canvas
      super

      # Register the drag type for colors
      FXWindow.colorType = getApp().registerDragType(FXWindow.colorTypeName)

      # Show the main window
      show(PLACEMENT_SCREEN)
    end

Note that the first time that `registerDragType()` is called for a particular drag type name (such as `FXWindow.colorTypeName`) it will generate a unique identifier for that drag type. Subsequent calls to `registerDragType()` for the same drag type name will just return the previously-generated drag type. Now, we want to modify our `SEL_DND_MOTION` handler so that it's a little more picky about which kinds of drops it will accept:

    # Handle SEL_DND_MOTION messages from the canvas
    @canvas.connect(SEL_DND_MOTION) {
      if @canvas.offeredDNDType?(FROM_DRAGNDROP, FXWindow.colorType)
        @canvas.acceptDrop
      end
    }

Here, we call the canvas' `offeredDNDType?` method to ask if the drag source can provide its data in the requested format. Only if `offeredDNDType?` returns true will we call `acceptDrop()` as before.

The last step is to actually handle the drop, and for that we add a handler for the `SEL_DND_DROP` message:

    # Handle SEL_DND_DROP message from the canvas
    @canvas.connect(SEL_DND_DROP) {
      # Try to obtain the data as color values first
      data = @canvas.getDNDData(FROM_DRAGNDROP, FXWindow.colorType)
      unless data.nil?
        # Update canvas background color
        @canvas.backColor = Fox.fxdecodeColorData(data)
      end
    }

Assuming that the drag source is able to provide its data in the requested format, the `getDNDData()` method will return a String (which for our purposes is just a byte buffer). If you've defined your own application-specific drag types, this data can of course be anything, and we'll see examples of this in a later tutorial. But the data for standard drag types like `FXWindow.colorType` can be decoded using the appropriate built-in library functions. In this case, we use the `fxdecodeColorData()` method to convert the bytes into a color value that we can use.

Now comes the moment of truth. Try running your test program again (one that displays a color well). Now, when you drag a color from a color well and drop it onto the `DropSite` window, the canvas should change its background color accordingly.

The complete program is listed below, and is included in the `examples` directory under the file name `dropsite.rb`.

    require 'rubygems'
    require_gem 'fxruby'

    include Fox

    class DropSite < FXMainWindow
      def initialize(anApp)
        # Initialize base class
        super(anApp, "Drop Site", nil, nil, DECOR_ALL, 0, 0, 400, 300)

        # Fill main window with canvas
        @canvas = FXCanvas.new(self, nil, 0, LAYOUT_FILL_X|LAYOUT_FILL_Y)

        # Handle expose events on the canvas
        @canvas.connect(SEL_PAINT) { |sender, sel, event|
          FXDCWindow.new(@canvas, event) { |dc|
            dc.foreground = @canvas.backColor
            dc.fillRectangle(event.rect.x, event.rect.y, event.rect.w, event.rect.h)
          }
        }

        # Enable canvas for drag-and-drop messages
        @canvas.dropEnable

        # Handle SEL_DND_MOTION messages from the canvas
        @canvas.connect(SEL_DND_MOTION) {
          if @canvas.offeredDNDType?(FROM_DRAGNDROP, FXWindow.colorType)
            @canvas.acceptDrop
          end
        }

        # Handle SEL_DND_DROP message from the canvas
        @canvas.connect(SEL_DND_DROP) {
          # Try to obtain the data as color values first
          data = @canvas.getDNDData(FROM_DRAGNDROP, FXWindow.colorType)
          unless data.nil?
            # Update canvas background color
            @canvas.backColor = Fox.fxdecodeColorData(data)
          end
        }
      end

      def create
        # Create the main window and canvas
        super

        # Register the drag type for colors
        FXWindow.colorType = getApp().registerDragType(FXWindow.colorTypeName)

        # Show the main window
        show(PLACEMENT_SCREEN)
      end
    end

    if __FILE__ == $0
      FXApp.new("DropSite", "FXRuby") do |theApp|
        DropSite.new(theApp)
        theApp.create
        theApp.run
      end
    end

Prev | Up | Next
Pasting Data from the Clipboard | Home | Drag Sources