Skip to content

Instantly share code, notes, and snippets.

@raphaeltraviss
Last active May 21, 2018 20:31
Show Gist options
  • Save raphaeltraviss/6b1e01f5b7069c8e31128766630ebdf7 to your computer and use it in GitHub Desktop.
Save raphaeltraviss/6b1e01f5b7069c8e31128766630ebdf7 to your computer and use it in GitHub Desktop.
Generic DataSource class for NSOutlineView with drag n drop support in Swift. For use in reactive programming models, where you destroy and re-create the datasource on each relevant state change.
import AppKit
// This class will manufacture a datasource object for use with NSOutlineView,
// given the following:
// - A type, specifying what type of object the datasource will provide.
// - A root object, which contains the heirarchy of its children
// - A function that can be called on the object type, to return its children.
// - A function that can be called on the object type, to return its identity.
// - A pasteboard type that can be used for drag and drop.
// - Side effects for insert and move actions.
// The idea with this datasource is that it does not persist: it is built off
// of some other data structure, that has a one-dimensional index. In other
// words, it is a generic outline datasource for a stack.
final class OutlineDataSource<ObjectType> : NSObject, NSOutlineViewDataSource {
typealias ChildFinder = (ObjectType) -> [ObjectType]
typealias ItemIdentifier = (ObjectType) -> Int? // @TODO: When would the found index nil?
typealias MoveAction = (Int, Int) -> Void
typealias InsertAction = (ObjectType, Int?) -> Void // @TODO: When would our target index be nil?
typealias ExtractAction = (Data) -> ObjectType?
private let rootObject: ObjectType
private let getChildren: ChildFinder
private let pasteboardTypeOutgoing: NSPasteboard.PasteboardType
private let pasteboardTypeIncoming: NSPasteboard.PasteboardType
private let extractModel: ExtractAction
private let getIdentifier: ItemIdentifier
private let dispatchMove: MoveAction
private let dispatchInsert: InsertAction
init(root_object: ObjectType,
child_finder: @escaping ChildFinder,
pb_type_outgoing: NSPasteboard.PasteboardType,
pb_type_incoming: NSPasteboard.PasteboardType,
extract_incoming_pb_data: @escaping ExtractAction,
item_identifier: @escaping ItemIdentifier,
move_action: @escaping MoveAction,
insert_action: @escaping InsertAction) {
self.rootObject = root_object
self.pasteboardTypeOutgoing = pb_type_outgoing
self.pasteboardTypeIncoming = pb_type_incoming
self.getChildren = child_finder
self.getIdentifier = item_identifier
self.dispatchMove = move_action
self.dispatchInsert = insert_action
self.extractModel = extract_incoming_pb_data
}
// Returns the children of an item at a given index.
func outlineView(_ outlineView: NSOutlineView, child index: Int, ofItem item: Any?) -> Any {
// nil represents the outline initialization: the root node is the child.
guard let the_item = item as? ObjectType else { return rootObject }
return getChildren(the_item)[index]
}
func outlineView(_ outlineView: NSOutlineView, isItemExpandable item: Any) -> Bool {
guard let the_item = item as? ObjectType else { return false }
// All nodes are expandable if they contain any children.
return getChildren(the_item).count > 0
}
func outlineView(_ outlineView: NSOutlineView, numberOfChildrenOfItem item: Any?) -> Int {
// The number of children of the NSOutline (nil) is 1; just the root object.
guard let the_item = item as? ObjectType else { return 1 }
return getChildren(the_item).count
}
func outlineView(_ outlineView: NSOutlineView, pasteboardWriterForItem item: Any) -> NSPasteboardWriting? {
// @TODO: we should serialize the full node here (in case we want to drag it to another
// view), instead of just its stack index.
guard let the_item = item as? ObjectType else { return nil }
var stack_index = getIdentifier(the_item)
let data = Data(bytes: &stack_index, count: MemoryLayout<Int>.size)
let pb_item = NSPasteboardItem()
pb_item.setData(data, forType: pasteboardTypeOutgoing)
// Set the data for the passed-in type as an integer: this will cause a move
// action when it is dragged over this object.
return pb_item
}
func outlineView(_ outlineView: NSOutlineView, validateDrop info: NSDraggingInfo, proposedItem item: Any?, proposedChildIndex index: Int) -> NSDragOperation {
if let types = info.draggingPasteboard().types {
if types.contains(OpPasteboardType) {
return NSDragOperation.copy
}
if types.contains(StackIndexPasteboardType) {
return NSDragOperation.move
}
}
return []
}
func outlineView(_ outlineView: NSOutlineView, acceptDrop info: NSDraggingInfo, item: Any?, childIndex index: Int) -> Bool {
// switch, based on the pasteboard type, pull out the correct data for each case,
// and execute either a move or an insert.
let pasteboard = info.draggingPasteboard()
// Extract all data, for all type.
guard let pb_types = pasteboard.types else { return false }
// If we are accepting a drag that originated from this same outline, then just
// get the source index, and prepare for a MOVE operation.
if pb_types.contains(pasteboardTypeOutgoing) {
guard let data = pasteboard.data(forType: pasteboardTypeOutgoing) else { return false }
var source_index: UInt8 = 0
data.copyBytes(to: &source_index, count: MemoryLayout<Int>.size)
let destination_index = item is ObjectType ? getIdentifier(item as! ObjectType) : 0
dispatchMove(Int(source_index), destination_index!)
return true
}
// If we are accepting a drag that originated from an external source, then pull
// in the data object using the passed-in closure, and prepare for an INSERT operation.
if pb_types.contains(pasteboardTypeIncoming) {
guard let data = pasteboard.data(forType: pasteboardTypeIncoming) else { return false }
guard let inserted_node = extractModel(data) else { return false }
let target_index = item is ObjectType ? getIdentifier(item as! ObjectType) : 0
dispatchInsert(inserted_node, target_index)
return true
}
return false
}
}
/* code preceeds */
func new_state(state) {
outlineDataSource = OutlineDataSource<MyNodeClass>(
root_object: state.root_node_instance,
child_finder: { return $0.children },
pb_type_outgoing: NodeIndexPasteboardType,
pb_type_incoming: NodePasteboardType,
extract_incoming_pb_data: { data in
// Here shown using Protobuf, but you could use JSON/NSDictionary.
guard let collection = try? OpStackD(serializedData: data) else { return nil }
let elements = collection.map( {ElementWrapperClass(fromDescriptor: $0) })
guard let the_element = elements.first else { return nil }
return MyNodeClass(the_op, 0)
},
item_identifier: { $0.stored_collection_index },
move_action: { GLOBAL.uiActionStore.dispatch(StateMutation.MoveNode($0, $1)) },
insert_action: { GLOBAL.uiActionStore.dispatch(StateMutation.InsertNode($0.state!.node, $1)) }
)
}
/* code follows */
@raphaeltraviss
Copy link
Author

This example data source is meant to apply to a 1-dimensional array of symbols, some of which indicate objects, and some which represent relations. For example, your array might contain the ops of OBJ1, OBJ2, COMBINE, where OBJ2 is considered the "parent", OBJ1 is considered the "child", and COMBINE is the relationship that combines them.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment