I wanted to use Drag & Drop from within my own application for my own custom types. I already have implemented drag & drop for sources outside my application as I have blogged about
in this post. In
another post I have documented the use of the pasteboard from within Swift.
So adding drag & drop when the source and destination are my own app should be easy right?
Well, no. I did not find this easy at all. But I did get it to work :)
First things first: The apple documentation (that I could find) is of little to no help at all. It is either outdated or it laks guidance. Besides, I must be the only one having trouble with this because there is almost nothing to find on the inet ... well, there is, but it is also outdated...
Ok, here is the deal:
First decide which type to drag & drop. For that type the NSPasteboardWriting and NSPasteboardReading protocols have to be adopted. Oh, and that type (class) must inherit from NSObject as well.
Like this:
class MyData: NSObject, NSPasteboardWriting, NSPasteboardReading {
var eData: String!
var oData: String!
init(e: String, o: String) {
super.init() // Necessary for NSObject
oData = o
}
private func jsonString() -> String {
let top = SwifterJSON.createJSONHierarchy()
top["eData"].stringValue = eData
top["oData"].stringValue = oData
return top.description
}
// MARK: - Pasteboard Writing Protocol
@objc func writableTypesForPasteboard(pasteboard: NSPasteboard!) -> [AnyObject]! {
return ["com.mycompany.MyApp.MyData"]
}
@objc func pasteboardPropertyListForType(type: String!) -> AnyObject! {
return jsonString()
}
// MARK: - Pasteboard Reading Protocol
init!(pasteboardPropertyList propertyList: AnyObject!, ofType type: String!) {
super.init()
if let jsonString = propertyList as? String {
let (topOrNil, errorOrNil) = SwifterJSON.createJsonHierarchyFromString(jsonString)
if let top = topOrNil {
oData = top["oData"].stringValue ?? "Error"
eData = top["eData"].stringValue ?? "Error"
} else {
return nil
}
} else {
return nil
}
}
static func readableTypesForPasteboard(pasteboard: NSPasteboard!) -> [AnyObject]! {
return ["com.mycompany.MyApp.MyData"]
}
static func readingOptionsForType(type: String!, pasteboard: NSPasteboard!) -> NSPasteboardReadingOptions {
return NSPasteboardReadingOptions.AsString
}
}
In the above code I used my own SwifterJSON framework to encode and decode the data of MyData. If you would like to take a look at that, find the link on the left hand side of this blog.
Normally the
propertyList in the pasteboard reading protocol will deliver NSData objects, but since I know that there will be a JSON string in there, I have also supplied the optional
readingOptionsForType method so that the
propertyList will be readable as a String when the
init is called.
PS I have ignored error handling in
init for brevity.
With the above code in place, we can now create MyData objects from the pasteboard like this:
func performDragOperation(sender: NSDraggingInfo) -> Bool {
// Get the pasteboard
if let pboard = sender.draggingPasteboard() {
// Get the Y coordinate for the drop
let dropCoordinate = sender.draggingLocation()
// Check if the drop position is within any of our subviews.
if !pointIsInADocumentView(dropCoordinate) { return false }
// Offset the Y coordinate
let dropPoint = theView.convertPoint(dropCoordinate, toView: aSubView)
// Get the index for the drop
let dropIndex = lineIndexFromYCoordinate(dropPoint.y)
// Make sure that there are filenames in the pasteboard
if pboard.availableTypeFromArray([NSFilenamesPboardType]) == NSFilenamesPboardType {
// Get the filenames from the pasteboard
let files = filesFromPboard(pboard)
// Add the filenames to the datasource
insertLines(files, atIndex: dropIndex)
// Update the view
theView.needsDisplay = true
// The drop was accepted
return true
} else if pboard.availableTypeFromArray(["com.mycompany.MyApp.MyData"]) == "com.mycompany.MyApp.MyData" {
if var specs = pboard.readObjectsForClasses([MyData.self], options: nil) as? Array<MyData> {
dataModel.insertLines(fileSpecs, atIndex: dropIndex)
} else {
log.atLevelError(id: 0, source: "performDragOperation", message: "Could not read MyData from pasteboard")
}
// The drop was accepted
return true
}
}
// Still here, then something went wrong. The drop is not accepted.
return false
}
The yellowish underlaid code is the important bit. I have left some of the older code in so that you see how this could all work together. Refer to the blogs I mentioned at the top to see more on that. You can find a link to the logging framework I use to the left of this blog. (Note: I always accept the drop, this is my own data type, so I better make sure all errors are fixed before shipping!)
Oh, lest I forget, we also need to register ourselves to receive the new type of drop: in awakeFromNb I added the following:
// Add drag and drop support
registerForDraggedTypes([NSFilenamesPboardType, "com.mycompany.MyApp.MyData"])
With this code in place we can accept drops for our own custom data on a pasteboard. All that is left is to provide for a source that generates the pasteboard information. I had to do this in a view, but it can also be done in a window (so I believe...). In my custom NSView child I added the dragging source protocol:
class MyView: NSView, NSDraggingSource {...
The only method that I needed was:
func draggingSession(session: NSDraggingSession, sourceOperationMaskForDraggingContext context: NSDraggingContext) -> NSDragOperation {
switch context {
case .OutsideApplication: return .None
case .WithinApplication: return .Move
}
}
In this method I make clear that the drag & drop for the session that this view will begin is only available for internal drag & drop's. The data cannot be exported (dropped) outside my app.
To begin the drag & drop I added the following code to the mouseDown method:
override func mouseDown(theEvent: NSEvent) {
// Get the mouse position from the event position
let mp = convertPoint(theEvent.locationInWindow, fromView: nil)
if (mp.x >= 0.0) && (mp.y >= 0.0) && (mp.x <= frame.width) && (mp.y <= frame.height) {
let mousePosition = CGPointMake(mp.x - MARGIN_BEFORE_LINE, mp.y)
// If the option key is down as well, this will be a drag & drop
if theEvent.modifierFlags.isSet(.AlternateKeyMask) {
// Find out if the mouse is down in a selection
let lineIndex = lineIndexFromYCoordinate(mousePosition.y)
if lineIndex >= dataSource.nofLines { return } // Ignore if the mouse down position is outside any of the lines
let line = dataSource.lineAtIndex(lineIndex)
let charIndex = line.indexForOffset(mousePosition.x)
let lines = dataSource.linesInBlokSelect(lineIndex, charIndex: charIndex, lineSource: dataSource.lineAtIndex)
var dragItems = Array<NSDraggingItem>()
for l in lines { dragItems.append(NSDraggingItem(pasteboardWriter: l)) }
beginDraggingSessionWithItems(dragItems, event: theEvent, source: self)
// Remove the original data from the datamodel
for l in lines { dataSource.removeLine(l) }
} else {
// Normal mouse down
mouseDownPosition = mousePosition
}
} else {
return
}
}
The salient point is the code with the yellowish background. All the other code is only necessary for my application.
beginDraggingSessionWithItems is the code that starts the drag operation. But it needs an array of
NSDraggingItem(s). Thus the MyData objects are wrapped in a
NSDraggingItem The
NSDraggingItem can only wrap objects that implement the
NSPasteboardWriting protocol.
While the above should get you up and running, it is not complete. You will probably want to add visual indicators of your own making to the drag & drop. This can be done (so I understand) through the
NSDraggingItem
Happy coding...
Did this help?, then please help out a small independent.If you decide that you want to make a small donation, you can do so by clicking thislink: a cup of coffee ($2) or use the popup on the right hand side for different amounts.Payments will be processed by PayPal, receiver will be sales at balancingrock dot nlBitcoins will be gladly accepted at: 1GacSREBxPy1yskLMc9de2nofNv2SNdwqHWe don't get the world we wish for... we get the world we pay for.