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:
Within a single Application (dragging from one View to another within your own application). Since you implemented both ends of the drag and drop, the format is known.
Within two tightly copuled Applications. Again, most likely you or your team implemented both ends of the drag and drop.
When dealing with a well-known message format, such as dragging from a Tracker window onto your application.
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 method can only be called from MouseDown, and it ensures that your View receives all the indicated events until the mouse button is released (in this case, pointer events).
-
This method starts the drag process. After this, the base class implementation handles all the rest of the drag.
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:
The desktop window doesn't even check the message code. We use B_PASTE here because that's what the appearance preflet uses for its color messages.
DragMessage can take a Bitmap instead of a Rect. (This requires you to create and draw a Bitmap on the fly.)
More sophisticated implementations might put DragMessage into the MouseMoved hook. This would allow you to make sure the user actually intended to start a drag, by checking that the mouse moved at least two pixels, for example.
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:
- B_COPY_TARGET
- B_MOVE_TARGET
- B_LINK_TARGET
- B_TRASH_TARGET
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.)