Haiku API Bindings
Drag and Drop
Not logged in

Documentation | Manual: Drag and Drop

Chapter 9: Drag and Drop

In this chapter, we will learn how to implement drag and drop.

This program for this chapter is called DragDrop.ext.

The portions of this program shown here include only the "meaty" bits. See the program itself for the rest of the code.

Flavors

There are two flavors of drag and drop in Haiku. Simple drag and drop can be used when dealing with a known drop format. This works in three situations:

When dealing with a message format not known in advance, you need to use negotiated drag and drop, which involves two or three messages passed back and forth between the dragger and the dropper.

Handling drops

Handling drops is quite easy in Haiku. The dropped Message automatically targets the View it was dropped on, so you can subclass the View and implement the MessageReceived hook.

(Of course, you can also subclass Window and handle it there, either in DispatchMessage or in the Window's own MessageReceived.)

This example accepts a dropped file from the Tracker.

Perl

sub MessageReceived {
    my ($self, $message) = @_;
    if ($message->what == B_SIMPLE_DATA) {
        my $path = eval { $message->FindRef("refs") };
        $self->SetText($path);
        return;
    }
    $self->SUPER::MessageReceived($message);
}

Python

def MessageReceived(self, message):
    if message.what == B_SIMPLE_DATA:
        path = None
        try:
            path = message.FindRef("refs")
        except:
            pass
        self.SetText(path)
        return
    super(TrackerDropTarget, self).MessageReceived(message)

(Many applications don't even bother checks the message code; they just check Message.WasDropped, and assume that any dropped message will contain a file.)

Initiating drags

There are two parts to iniating drags.

This example passes a color, using a relatively well-known message format. The desktop window will react to this drop by changing the background color.

Perl

sub MouseDown {
    my ($self, $point) = @_;

    # force all mouse events to be sent to us until the button is released
    # (so we don't have to track the mouse manually)
    $self->SetMouseEventMask(B_POINTER_EVENTS, 0);

    my $rect = [$point->[0]-20, $point->[1]-20, @$point];
    my $value = pack('CCCC', @{ $self->HighColor() });

    # Tracker doesn't even look at the message code, but the appearance
    # preflet uses B_PASTE for color messages
    my $message = Haiku::Message->new(B_PASTE);

    # AddColor will send as B_RGB_32_BIT_TYPE, but we need to send as
    # B_RGB_COLOR_TYPE, so we need to use AddData
    $message->AddData(
        name => "RGBColor",
        type => B_RGB_COLOR_TYPE,
        data => $value,
    );

    $self->DragMessage(
        message => $message,
        rect    => $rect,
    );
}

Python

def MouseDown(self, point):

    # force all mouse events to be sent to us until the button is released
    # (so we don't have to track the mouse manually)
    self.SetMouseEventMask(B_POINTER_EVENTS, 0)

    rect = [ point[0]-20, point[1]-20, point[0], point[1] ]
    color = self.HighColor()
    value = struct.pack('BBBB', color.red, color.green, color.blue, color.alpha)

    # Tracker doesn't even look at the message code, but the appearance
    # preflet uses B_PASTE for color messages
    message = Message(B_PASTE)

    # AddColor will send as B_RGB_32_BIT_TYPE, but we need to send as
    # B_RGB_COLOR_TYPE, so we need to use AddData
    message.AddData(
        name = "RGBColor",
        type = B_RGB_COLOR_TYPE,
        data = value,
    )

    self.DragMessage(
        message = message,
        rect    = rect,
    )

Some notes:

Negotiated drag and drop

Negotiated drag and drop is a little more complicated. You can read about it more detail in the Be Book.

Note that in the sections which follow, sender means the sender of the original drag message, and receiver means the drop target of the original drag message.

Drop message

Negotiated drag and drop starts with a drop message with a code of B_SIMPLE_DATA. (Warning: this is the same message code the Tracker uses to send a standard file drop, so in a real application, you would need to do more checking than we do here.)

The message will contain one or more Int32 fields with the name "be:actions". There are four possible actions:

If the sender is willing to send data via a message, there will be one or more string fields with the name "be:types". Each string is a MIME type representing a format in which the sender is willing to send.

If the sender is willing to send data via a file, there will be a similar field with the name "be:filetypes". There will also be a set of strings with the name "be:type_descriptions"; each index under the type desdriptions is a human-readable string describing the MIME type at the same index under the file types.

The message should contain at least one of "be:types" or "be:filetypes", but can contain both.

There is also an optional string field with the name "be:clip_name", containing a suggested name for the data.

The sender may, of course, populate other fields in the message as desired. The Be Book suggests using the names "be:originator" and "be:originator_data" for these fields, but since you will be the only one using this data, you can name it anything you want.

If you do use these fields, you can access them by calling Message.Previous on the reply to get a copy of the message you originally sent.

In this example, we use the contents of the drop message to populate some Views. (We do not show the creation of those Views here; see the program file if you want to see how they were created.)

This chunk of code is from the example View's MessageReceived hook.

Note: the People application sends negotiation messages from its profile picture. If you don't have any contacts with profile pictures, you can open an empty contact and drag any image from the Tracker onto the empty image. You can then drag that image onto the demo application. You do not need to save the contact to do this.

Perl

    if ($message->what == B_SIMPLE_DATA) {
        $self->{datatypes}->MakeEmpty();
        $self->{filetypes}->MakeEmpty();
        $self->{actions}->MakeEmpty();
#
# ListView does not take ownership of its objects, so we need to keep the
# native ListItem objects around, because the underlying C++ objects will be
# deleted when the native wrappers go out of scope; so we make the items part
# of this object
#
        $self->{items} = [];

        my $info = $message->GetInfo("be:actions");
        for my $i (0..$info->{count}-1) {
            my $action = $message->FindInt32("be:actions", $i);
            my $item = {
                item => Haiku::StringItem->new( $actions{$action} ),
                value => $action,
            };

            $self->{actions}->AddItem($item->{item});
            push @{ $self->{items} }, $item;
        }

        my %type_names;
        $info = $message->GetInfo("be:filetypes");
        for my $i (0..$info->{count}-1) {
            my $type = $message->FindString("be:filetypes", $i);
            my $name = $message->FindString("be:type_descriptions", $i);
            my $item = {
                item => Haiku::StringItem->new($name),
                value => $type,
            };
            $self->{filetypes}->AddItem($item->{item});
            push @{ $self->{items} }, $item;
            $type_names{$type} = $name; # so we can use them again for data types
        }

        $info = $message->GetInfo("be:types");
        for my $i (0..$info->{count}-1) {
            my $type = $message->FindString("be:types", $i);
            my $name = $type_names{$type} || $type;
            my $item = {
                item => Haiku::StringItem->new($name),
                value => $type,
            };
            $self->{datatypes}->AddItem($item->{item});
            push @{ $self->{items} }, $item;
        }

        my $name = "";
        if ($message->GetInfo("be:clip_name")) {
            $name = $message->FindString("be:clip_name");
        }
        $self->{clip_name}->SetText($name);

        # We need to respond to this message later, but we can't respond to a
        # copy, and the owning Looper (= our Window) will ordinarily delete
        # it after this method returns. So we have to detach the message and
        # take ownership of it.
        $self->{last_drop_message} = $self->Window()->DetachCurrentMessage();

        return;
    }

Python

    if message.what == B_SIMPLE_DATA:
        self.datatypes.MakeEmpty()
        self.filetypes.MakeEmpty()
        self.actions.MakeEmpty()
#
# ListView does not take ownership of its objects, so we need to keep the
# native ListItem objects around, because the underlying C++ objects will be
# deleted when the native wrappers go out of scope so we make the items part
# of this object
#
        self.items = []

        info = message.GetInfo("be:actions")
        for i in range(0, info['count']):
            action = message.FindInt32("be:actions", i)
            item = {
                'item'  : StringItem( self.action_names[action] ),
                'value' : action,
            }
            self.actions.AddItem(item['item'])
            self.items.append(item)

        type_names = {}
        info = message.GetInfo("be:filetypes")
        for i in range(0, info['count']):
            type = message.FindString("be:filetypes", i)
            name = message.FindString("be:type_descriptions", i)
            item = {
                'item'  : StringItem(name),
                'value' : type,
            }
            self.filetypes.AddItem(item['item'])
            self.items.append(item)
            type_names[type] = name # so we can use them again for data types

        info = message.GetInfo("be:types")
        for i in range(0, info['count']):
            type = message.FindString("be:types", i)
            name = type_names.get(type, type)
            item = {
                'item'  : StringItem(name),
                'value' : type,
            }
            self.datatypes.AddItem(item['item'])
            self.items.append(item)

        name = ""
        if message.GetInfo("be:clip_name"):
            name = message.FindString("be:clip_name")
        self.clip_name.SetText(name)

        # We need to respond to this message later, but we can't respond to a
        # copy, and the owning Looper (= Window) will ordinarily delete
        # it after this method returns. So we have to detach the message and
        # take ownership of it.
        self.last_drop_message = self.Window().DetachCurrentMessage()

        return

Negotiation message

Ordinarily, you woould have a stock negotiation message; when you received a B_SIMPLE_DATA message, you would return your stock negotiation message, and then the sender would find a match between what it is willing to send and what the receiver is willing to accept, and pass the data that way.

In this example, however, we build a reply message on the fly from the user's selections.

The code of your reply should be the action you want to take on the data.

Like the drop message, your message can contain one or both of "be:types" and "be:filetypes". You do not need "be:type_descriptions", but if you have "be:filetypes", then you should have two additional fields: "directory", an entry ref, and "name", a string, indicating where the new file should be created.

It is the receiver's responsibility to make sure the file is creatable. In fact, the Be Book suggests that the sender should create the file to prevent some other application from creating it before the sender can put data in it.

Once again, this code is from the MessageReceived hook.

Perl

    if ($message->what == SEND_REPLY) {
        my ($method, $type_item);
        my $action_index = $self->{actions}->CurrentSelection();
        if ($action_index >= 0) {
            if ($self->{data_radio}->Value()) {
                $method = "be:types";
                my $type_index = $self->{datatypes}->CurrentSelection();
                if ($type_index >= 0) {
                    my $list_item = $self->{datatypes}->ItemAt( $type_index );
                    ($type_item) = grep { $_->{item} == $list_item } @{ $self->{items} };
                }
            }
            elsif ($self->{file_radio}->Value()) {
                $method = "be:filetypes";
                my $type_index = $self->{filetypes}->CurrentSelection();
                if ($type_index >= 0) {
                    my $list_item = $self->{filetypes}->ItemAt( $type_index );
                    ($type_item) = grep { $_->{item} == $list_item } @{ $self->{items} };
                }
            }
        }

        unless ($type_item) {
            Haiku::Alert->new(
                title   => "Action and method needed",
                text    => "You must select an action and a transmission method",
                buttons => ["OK"],
            )->Go(undef);
            return;
        }

        my $list_item = $self->{actions}->ItemAt( $action_index );
        my ($action_item) = grep { $_->{item} == $list_item } @{ $self->{items} };

        my $reply = Haiku::Message->new($action_item->{value});
        unless ($action_item->{value} == B_TRASH_TARGET) {
            $reply->AddString($method, $type_item->{value});
            if ($method eq "be:filetypes") {
                $reply->AddRef("directory", "/boot/home");
                $reply->AddString("name", "dragdrop_result");
                $self->{filepath} = "/boot/home/dragdrop_result";
                # SetPulseRate won't do anything for values < 100000
                $self->Window()->SetPulseRate(100000);
            }

            $self->{type_requested} = $type_item->{value};
            $self->{last_drop_message}->SendReply(
                message => $reply,
                replyTo => $self,
            );
        }

        return;
    }

Python

    if message.what == SEND_REPLY:
        method = None
        type_item = None
        action_index = self.actions.CurrentSelection()
        if action_index >= 0:
            if self.data_radio.Value():
                method = "be:types"
                type_index = self.datatypes.CurrentSelection()
                if type_index >= 0:
                    list_item = self.datatypes.ItemAt(type_index)
                    for item in self.items:
                        if item['item'] == list_item:
                            type_item = item;
            elif self.file_radio.Value():
                method = "be:filetypes"
                type_index = self.filetypes.CurrentSelection()
                if type_index >= 0:
                    list_item = self.filetypes.ItemAt(type_index)
                    for item in self.items:
                        if item['item'] == list_item:
                            type_item = item;

        if not type_item:
            Alert(
                title   = "Action and method needed",
                text    = "You must select an action and a transmission method",
                buttons = ["OK"],
            ).Go(None)
            return

        list_item = self.actions.ItemAt( action_index )
        for item in self.items:
            if item['item'] == list_item:
                action_item = item;

        reply = Message(action_item['value'])
        if action_item['value'] != B_TRASH_TARGET:
            reply.AddString(method, type_item['value'])
            if method == "be:filetypes":
                reply.AddRef("directory", "/boot/home")
                reply.AddString("name", "dragdrop_result")
                self.filepath = "/boot/home/dragdrop_result"
                # SetPulseRate won't do anything for values < 100000
                self.Window().SetPulseRate(100000)

            self.type_requested = type_item['value']
            self.last_drop_message.SendReply(
                message = reply,
                replyTo = self,
            )

        return

Note: SEND_REPLY is not a builtin Haiku message code; it is our own code, sent when the user clicks a button.

Data message

If the sender decides to send data via a Message, then that message will have a code of B_MIME_DATA and a field whose name is the MIME type of the data format and whose type is B_MIME_TYPE.

If we had sent a negotiation message with multiple types, we would have to iterate over the field names in the Message to determine which one the sender had actually sent.

Perl

   if ($message->what == B_MIME_DATA) {
       Haiku::Alert->new(
           title   => "Got data",
           text    => "Got the data as a message",
           buttons => ["OK"],
       )->Go(undef);
       my $data = $message->FindData($self->{type_requested}, B_MIME_TYPE);
       # here we could do something with the data if this were more than a demo
       return;
   }

Python

   if message.what == B_MIME_DATA:
       Alert(
           title   = "Got data",
           text    = "Got the data as a message",
           buttons = ["OK"],
       ).Go(None)
       data = message.FindData(self.type_requested, B_MIME_TYPE);
       # here we could do something with the data if this were more than a demo
       return

Data as a file

If the sender decides to send data in a file, you will need to monitor that file. Since NodeMonitor was not yet ready when this example code was written, we simply watch the directory instead.

For this reason, we do not follow the Be Book's suggestion that the receiver create the file itself. In fact, we remove the file if it is there, and then wait until the sender creates it. We use the Pulse hook for this.

Perl

   sub Pulse {
       my ($self) = @_;
       $self->Window()->SetPulseRate(0);
      return unless exists $self->{filepath};

       my $filepath = $self->{filepath};
       until (-e $filepath) {
           # do something to let other threads grab the interpreter
           snooze(50);
       }
       delete $self->{filepath};

       Haiku::Alert->new(
           title   => "Got data",
           text    => "Got the data as a file",
           buttons => ["OK"],
       )->Go(undef);
       # here we could do something with the file if this were more than a demo
   }

Python

   def Pulse(self):
       self.Window().SetPulseRate(0)
       if not self.filepath:
           return

       filepath = self.filepath
       while not os.path.isfile(filepath):
           # do something to let other threads grab the interpreter
           snooze(50)
       self.filepath = None

       Alert(
           title   = "Got data",
           text    = "Got the data as a file",
           buttons = ["OK"],
       ).Go(None)
       # here we could do something with the file if this were more than a demo

B_TRASH_TARGET

If the receiver chooses B_TRASH_TARGET as the action, then the sender will neither send a Message nor create a file. It will simply do whatever it means by "trashing" the data. (The People application, for example, removes the profile picture.)