Implementing drag and drop doesn’t have a lot of documentation, so I thought it would be useful to lay down my experiences, in particular with accessing HTML5 drag and drop which is the only way of doing cross-window DnD.
Documentation
The 2 most useful sources of documentation here are the scrapbook tutorial, which gives a good overview of DnD if you’re using a CPCollectionView as your drag source, and this post on the mailing list from Ross Boucher which gives a better idea of doing DnD with other items.
Basic DnD
In my basic app (see below) we’re dragging and dropping an ordinary CPImage. Images are held with a CPImageView, but in order to make a view draggable, you need to add in handlers for various events. Therefore you will need to subclass the CPImageView to make it draggable.
@import
PictureDragType = "PictureDragType";
@implementation MyImageView : CPImageView
{
CPImage _image;
}
To make something draggable, we need a drag type. This is simply a global string variable (PictureDragType). We then subrclass CPImageView, and create a local CPImage object that will hold our image.
- (void)initWithFrame:(CGRect)aFrame
{
CPLog('initing view frame');
self = [super initWithFrame:aFrame];
CPLog('Adding an image');
var myBundle = [CPBundle mainBundle];
var path = [myBundle pathForResource:@"testphoto.jpg"];
_image = [[CPImage alloc] initByReferencingFile:path size:CGSizeMake(150,251)];
[self setFrameSize:[_image size]];
[self setImage:_image];
[_image setDelegate:self];
return self;
}
First up we need to add to the initWithFrame method. We can use the existing method from CPImageView, so we super that, and then (for our example app) we’re loading in our image and setting the frame size. Most importantly (for DnD) we setDelegate to self so that the app knows where to look for the DnD functions.
- (void)mouseDragged:(CPEvent)anEvent
{
CPLog('dragging');
var point = [self convertPoint:[anEvent locationInWindow] fromView:nil],
bounds = [self bounds];
CPLog('initialise pasteboard');
[[CPPasteboard pasteboardWithName:CPDragPboard] declareTypes:[CPArray arrayWithObject:PictureDragType] owner:self];
This is our first DnD function – an event for when the drag is started. We store the coordinates and bounds, and then we create our CPPasteboard object. Here we need to tell it which types we can drag – in our case only PictureDragType, but it is possible to handle multiple types.
CPLog('create drag view');
var dragView = [[CPImageView alloc] initWithFrame:bounds];
[dragView setImage:_image];
CPLog('initialise drag view');
[self dragView: dragView
at: CPPointMake(point.x - bounds.size.width / 2.0, point.y - bounds.size.height / 2.0)
offset: CPPointMake(0.0, 0.0)
event: anEvent
pasteboard: nil
source: self
slideBack: YES];
CPLog('finished initialising drag');
}
We can set what is shown attached to the pointer when we’re dragging. In our case, it makes sense to use the image that we’re dragging, but you can put anything into this view to be displayed. Construct it the way you would construct any CPView.
- (void)pasteboard:(CPPasteboard)aPasteboard provideDataForType:(CPString)aType
{
var mydata = [CPKeyedArchiver archivedDataWithRootObject:_image];
if(aType == PictureDragType)
[aPasteboard setData:mydata forType:aType];
}
@end
Now we need to implement the pasteboard. This tells the app how to store the data that it’s getting. In our case, when the pasteboard is called with the correct type (PictureDragType) we pass in our image, using the CPKeyedArchiver to handle it properly.
That’s the end of the drag source. Next up we will implement the drag destination (ie where we drop). Once again, this can be any CPView (or subclass of CPView), but we need to subclass it to add the appropriate functions/events so that it knows it’s a drag destination.
@import
@import "ImageController.j"
@implementation DropView : CPView
{
}
Note that we need to import our drag source code (ImageController.j) so that we have access to the global PictureDragType variable.
-(void)initWithFrame:(CGRect)aRect
{
CPLog('initing new view');
self = [super initWithFrame:aRect ];
[self registerForDraggedTypes:[CPArray arrayWithObjects:PictureDragType]];
return self;
}
We init the view in the normal way, but we need to register that the view is able to accept drags of the correct type.
- (void)performDragOperation:(id
{
CPLog('drag successful!');
CPLog('getting data');
var data = [CPKeyedUnarchiver unarchiveObjectWithData:[[sender draggingPasteboard] dataForType:PictureDragType]];
CPLog('data = '+data);
CPLog('trying to add the image');
var imageview = [[CPImageView alloc] initWithFrame:CGRectMake(0,0,0,0)];
[imageview setFrameSize:[data size]];
[imageview setImage:data];
CPLog('adding image to view');
[self addSubview:imageview];
CPLog('reloading the view');
[self setNeedsDisplay:YES];
}
Now we implement the performDragOperation event – this is fired when an item is dropped on our destination. Firstly we unarchive the data, then (for the purposes of our application) we handle it by adding the image to the destination view.
There are other events that we can handle (eg draggingEntered and draggingExited), but these are optional.
We now have a function drag source and drag destination created. All we need to do is tie them together simply in our main application, and it should all work. However….
HTML5
….part of the reason for this is to get cross-window DnD working. This functionality is disabled by default because it’s not well supported in all the browsers. However, it _does_ work and it’s useful to see how.
First up we need to create a custom build of cappuccino. All the code for HTML5 is available, but it is disabled by default. We can (initially) enable it for all browsers by editing the AppKit/Platform/DOM/CPPlatform.j file at line 60:
+ (BOOL)supportsDragAndDrop
{
return YES;
//return CPFeatureIsCompatible(CPHTMLDragAndDropFeature) && (CPPlatformEnableHTMLDragAndDrop || ![self isBrowser]);
}
(We’ll look later at some browser sniffing code)
Once this is rebuilt (run jake debug, and then copy the new build files across with capp gen --build -f --force ) HTML5 support will automatically be enabled. Now we can construct our app:
Our basic AppController.j is taken from the sample Hello World that you get when you create a new project from scratch with capp gen. We need to @import "WindowController.j" to import our drag destination code from above.
CPLog('creating button');
var testButton = [[CPButton alloc] initWithFrame:CGRectMake(200,330,65, 35)];
[testButton setTitle:@"Test"];
[testButton setTarget:self];
[testButton setAction:@selector(buttonDispatcher:)];
CPLog('adding button to view');
[contentView addSubview:testButton];
CPLog('Adding an image');
var imageView = [[MyImageView alloc] initWithFrame:CGRectMake(0,0,150,251)];
[contentView addSubview:imageView];
}
Next we add a button that we’ll use to create our new window, and add the image we’re going to drag, making sure to instantiate our revised version of CPImageView.
- (@action)buttonDispatcher:(id)sender
{
CPLog("Button dispatcher");
var platwin = [[CPPlatformWindow alloc] initWithContentRect:CGRectMake(200,200,320,530)];
var viewwindow = [[CPWindow alloc] initWithContentRect:[platwin contentBounds] styleMask:CPClosableWindowMask];
var myview = [[DropView alloc] initWithFrame:[[viewwindow contentView] bounds]];
[[viewwindow contentView] addSubview:myview];
[viewwindow setFullPlatformWindow:YES];
[viewwindow setPlatformWindow:platwin];
[viewwindow orderFront:self];
[platwin orderFront:self];
}
@end
Lastly, we use the buttonDispatcher to create a new CPPlatformWindow. The view inside the window is created with our DropView code, setting it as a drag destination. And we’re done.
If you open up this application, you’ll get a standard window, with the Hello World message still in the centre. In the top left is an image, and ‘Test’ button. The image is draggable, but won’t have anywhere to drop yet. Clicking the button will open a new window (assuming you enable pop-ups), and you can drag the image there and once you’ve dropped it, the image will be displayed in the new window too.
Quirks
This code works perfectly in Safari. In Firefox the CPView attached to the pointer when you drag doesn’t seem to work, but you can successfully drag the image from the source window to the destination. Chrome seems to break heavily here. I haven’t tested in IE, but I don’t hold out much hope….
In order to allow for this, I have a revised copy of CPPlatform.j which implements the quirksmode.org browser detection script in order to only enable HTML5 DnD on Safari and Firefox.
Installing the demo
I’ve attached a zipfile which contains the 4 files documented in this post, and the test image. To install the application, first create a custom cappuccino build using the AppKit/Platform/DOM/CPPlatform.j provided (be careful not to replace the wrong CPPlatform.j file!). Then create a new app with capp gen –build, and copy in the other three files (overwrite AppController.j and add in WindowController.j, ImageController.j and Resources/testphoto.jpg) and you should be ready to go.
Download DnDApp.zip from here