How to implement drag and drop in an NSOutlineView using bindings? - cocoa

I'm trying to implement drag and drop in my NSOutlineView but none of the example code, tutorials or other SO questions that I've found seem to work for my situation. I have an NSOutlineView with its content bound to an NSTreeController. The tree controller's Content Array is bound to an NSMutableArray of custom objects that have childern objects of the same type. In the outline view I can add and remove objects at any level in the heirarchy. So far so good.
To implement drag and drop I created and NSObject sublass that will serve as the outline view's dataSource. I have implemented a few methods, based on sample code and posts I found on Stack Overflow. I can initiate a drag, but when I do the drop, outlineView: acceptDrop: item: childIndex: is called but all of the values except for childIndex: are nil. The value for childIndex tells me the index of the drop location within the array but not which node I am at within the heirarchy.
I assume that all the other values passed in outlineView: acceptDrop: ... are nil because I haven't fully implemented dataSource, I'm only using it to control the drag and drop operation. Do I need to set up more pasteboard information when I start the drag? How do I find out what node I'm at when the drop occurs? Why are all the values in outlineView: acceptDrop: ... nil?
Here is the implementation of the outline views dataSource:
\ #implementation TNLDragController
- (void)awakeFromNib {
[self.titlesOutlineView registerForDraggedTypes:[NSArray arrayWithObject:#"Event"]];
}
- (BOOL)outlineView:(NSOutlineView *)outlineView writeItems:(NSArray *)items toPasteboard:(NSPasteboard *)pboard {
NSLog(#"starting a drag");
NSString *pasteBoardType = #"Event";
[pboard declareTypes:[NSArray arrayWithObject:pasteBoardType] owner:self];
return YES;
}
- (NSDragOperation)outlineView:(NSOutlineView *)outlineView
validateDrop:(id < NSDraggingInfo >)info
proposedItem:(id)item
proposedChildIndex:(NSInteger)index {
NSLog(#"validating a drag operation");
return NSDragOperationGeneric;
}
- (BOOL)outlineView:(NSOutlineView *)outlineView acceptDrop:(id < NSDraggingInfo >)info item:(id)item childIndex:(NSInteger)index {
NSLog(#"accepting drag operation");
//todo: move the object in the data model;
NSIndexPath *path = [self.treeController selectionIndexPath]; // these three values are nil too.
NSArray *objects = [self.treeController selectedObjects];
NSArray *nodes = [self.treeController selectedNodes];
return YES;
}
// This method gets called by the framework but the values from bindings are used instead
- (id)outlineView:(NSOutlineView *)outlineView objectValueForTableColumn:(NSTableColumn *)tableColumn byItem:(id)item {
return NULL;
}
/*
The following are implemented as stubs because they are required when
implementing an NSOutlineViewDataSource. Because we use bindings on the
table column these methods are never called. The NSLog statements have been
included to prove that these methods are not called.
*/
- (NSInteger)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(id)item {
NSLog(#"numberOfChildrenOfItem");
return 1;
}
- (BOOL)outlineView:(NSOutlineView *)outlineView isItemExpandable:(id)item {
NSLog(#"isItemExpandable");
return YES;
}
- (id)outlineView:(NSOutlineView *)outlineView child:(NSInteger)index ofItem:(id)item {
NSLog(#"child of Item");
return NULL;
}
#end

the implementation I described in this question was, in fact working just fine, but I made a rookie mistake when trying to determine if it was working. I set a breakpoint in - (BOOL)outlineView:(NSOutlineView *)outlineView acceptDrop:(id < NSDraggingInfo >)info item:(id)item childIndex:(NSInteger)index in order to examine the values that were being passed into the method. I'm using ARC and the values were never referenced within the method so ARC never retained them, making them unavailable to the debugger!

Related

Why doesn't my KVO dependency not work in NSArrayController

I would like to use an NSArrayController with an NSTableView to allow multiple selection but only provided a selected object when a single object is selected (and nil when none or multiple are selected).
I've attempted to implement this with a category on NSArrayController, as shown here:
#implementation NSArrayController (SelectedObject)
+ (NSSet *)keyPathsForValuesAffectingSelectedObject {
return [NSSet setWithObject:#"selection"];
}
- (id)selectedObject {
// Get the actual selected object (or nil) instead of a proxy.
if (self.selectionIndexes.count == 1) {
return [self arrangedObjects][self.selectionIndex];
}
return nil;
}
#end
For some reason, the selectedObject method is not called when the selection of the array controller changes (and something else is observing selectedObject). Why is this?
The selection property of NSArrayController is strange voodoo. I don't know if key-value observing it (and not a path that goes through it) produces change notifications when the selection changes. After all, it returns a proxy and there's no reason to believe that the identity of that proxy changes over time.
In any case, your actual selectedObject method doesn't actually use selection (and it shouldn't). It uses arrangedObjects and selectionIndexes. So, you should return a set containing those keys from +keyPathsForValuesAffectingSelectedObject.
Of course, if you're using a view-based table, you need to make sure the table view's selectionIndexes binding is bound to the array controller's selectionIndexes property, or the array controller just won't know anything about the selection in the table view. (For cell-based table views, you'd typically bind the columns to the array controller and the table view would automatically bind its own bindings based on the columns' bindings.)
Finally, I think you should choose a different name for selectedObject. It's too likely that Apple has a private method of that name or will add one in the future.
I managed to get this working by creating a subclass of NSArrayController and manually observing the selectionIndexes key. I'd prefer to do it using a category but this does appear to work.
static NSString *const kObservingSelectionIndexesContext = #"ObservingSelectionIndexesContext";
#implementation BetterArrayController
- (void)awakeFromNib {
[super awakeFromNib];
[self addObserver:self forKeyPath:#"selectionIndexes" options:NSKeyValueObservingOptionInitial|NSKeyValueObservingOptionNew context:(void *)&kObservingSelectionIndexesContext];
}
- (void)dealloc {
[self removeObserver:self forKeyPath:#"selectionIndexes" context:(void *)&kObservingSelectionIndexesContext];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if (context == (void *)&kObservingSelectionIndexesContext) {
[self willChangeValueForKey:#"selectedObject"];
[self didChangeValueForKey:#"selectedObject"];
} else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
- (id)selectedObject {
// Get the actual selected object (or nil) instead of a proxy.
if (self.selectionIndexes.count == 1) {
return [self arrangedObjects][self.selectionIndex];
}
return nil;
}
#end
I used a context (as per this article) to avoid removing any observers the superclass may have in dealloc (as cautioned against here).

NSCollectionView Drag Drop example

I am trying to implement drag drop in NSCollectionView which will allow to re arrange cells in view. I have set the delegate and implemented below methods :
-(BOOL)collectionView:(NSCollectionView *)collectionView writeItemsAtIndexes:(NSIndexSet *)indexes toPasteboard:(NSPasteboard *)pasteboard {
NSLog(#"Write Items at indexes : %#", indexes);
return YES;
}
- (BOOL)collectionView:(NSCollectionView *)collectionView canDragItemsAtIndexes:(NSIndexSet *)indexes withEvent:(NSEvent *)event {
NSLog(#"Can Drag");
return YES;
}
- (BOOL)collectionView:(NSCollectionView *)collectionView acceptDrop:(id<NSDraggingInfo>)draggingInfo index:(NSInteger)index dropOperation:(NSCollectionViewDropOperation)dropOperation {
NSLog(#"Accept Drop");
return YES;
}
-(NSDragOperation)collectionView:(NSCollectionView *)collectionView validateDrop:(id<NSDraggingInfo>)draggingInfo proposedIndex:(NSInteger *)proposedDropIndex dropOperation:(NSCollectionViewDropOperation *)proposedDropOperation {
NSLog(#"Validate Drop");
return NSDragOperationMove;
}
I am not sure how to take this further. With this I can see that now I can drag around the individual Collection Item but how can I make the Drop ?
You have only implemented the delegate methods but there s no logic in some of the methods. For example to drag around a Collection Item I would add below logic :
-(BOOL)collectionView:(NSCollectionView *)collectionView writeItemsAtIndexes:(NSIndexSet *)indexes toPasteboard:(NSPasteboard *)pasteboard {
NSData *indexData = [NSKeyedArchiver archivedDataWithRootObject:indexes];
[pasteboard setDraggedTypes:#[#"my_drag_type_id"]];
[pasteboard setData:indexData forType:#"my_drag_type_id"];
// Here we temporarily store the index of the Cell,
// being dragged to pasteboard.
return YES;
}
- (BOOL)collectionView:(NSCollectionView *)collectionView acceptDrop:(id<NSDraggingInfo>)draggingInfo index:(NSInteger)index dropOperation:(NSCollectionViewDropOperation)dropOperation {
NSPasteboard *pBoard = [draggingInfo draggingPasteboard];
NSData *indexData = [pBoard dataForType:#"my_drag_type_id"];
NSIndexSet *indexes = [NSKeyedUnarchiver unarchiveObjectWithData:indexData];
NSInteger draggedCell = [indexes firstIndex];
// Now we know the Original Index (draggedCell) and the
// index of destination (index). Simply swap them in the collection view array.
return YES;
}
You also need to register the collection view to drag type in awakefromnib as
[_myCollectionView registerForDraggedTypes:#[#"my_drag_type_id"]];
And make sure that you have set the collection view as selectable.
In addition to what GoodSp33d mentions above, you're also missing the validate delegate function which is required to accept drops. In Swift this is:
func collectionView(_ collectionView: NSCollectionView, validateDrop draggingInfo: NSDraggingInfo, proposedIndexPath proposedDropIndexPath: AutoreleasingUnsafeMutablePointer<NSIndexPath>, dropOperation proposedDropOperation: UnsafeMutablePointer<NSCollectionViewDropOperation>) -> NSDragOperation
Note the return value, NSDragOperation. This method should contain code that determines precisely what kind of drag operation is being attempted and returns this value. Returning the wrong thing can lead to some pretty annoying bugs.
Further note that in order to support this kind of operation, the collection view layout class you are using must also support drag and drop. Flow layout should do this out-of-the-box, but if you're using a custom layout you may need to adapt it to support drag-and-drop so that the collection view can detect valid drop targets and determine a suitable index path for them.

NSTreeController: custom behavior for "canInsert" binding

I have a Cocoa app with an NSOutlineView managed by an NSTreeController.
In addition there's a button for adding new elements to the outline view. I bound the button's enabled flag to the tree controller's canInsert property.
I only want to allow adding up to 5 elements to the outline view. After that, canInsert should return NO.
I created my own sub-class of NSTreeController and overwrote canInsert, but the enabled status of the button does not change, because it doesn't realize that the tree controller has changed when adding elements.
I also implemented: keyPathsForValuesAffectingCanInsert and tried returning various properties such as content, arrangedObjects, but no luck here.
#implementation ILCustomTreeController
- (BOOL)canInsert
{
return [[self arrangedObjects] count] < 5;
}
+ (NSSet *)keyPathsForValuesAffectingCanInsert
{
return [NSSet setWithObject:#"content"]; // I also tried 'arrangedObjects'
}
#end
Here's a workaround that does work (although I still think this should be solved by using keyPathForValuesAffectingCanInsert). Suggestions are welcome.
#implementation ILCustomTreeController
- (BOOL)canInsert
{
return [[self arrangedObjects] count] <= 4;
}
- (void)addObject:(id)object
{
[self willChangeValueForKey:#"canInsert"];
[super addObject:object];
[self didChangeValueForKey:#"canInsert"];
}
- (void)insertObject:(id)object atArrangedObjectIndexPath:(NSIndexPath *)indexPath
{
[self willChangeValueForKey:#"canInsert"];
[super insertObject:object atArrangedObjectIndexPath:indexPath];
[self didChangeValueForKey:#"canInsert"];
}
- (void)remove:(id)sender
{
[self willChangeValueForKey:#"canInsert"];
[super remove:sender];
[self didChangeValueForKey:#"canInsert"];
}
#end

NSButtonCell of Check type within NSTableView does not allow to have value changed

I have window with 3 table views (10.7.2, Xcode 4.2).
They are all created in IB and NSButtonCells are connected with outlets.
I created controller class and I filled all three views with some sample data:
- (NSInteger)numberOfRowsInTableView:(NSTableView *)aTableView {
return 10;
}
- (id)tableView:(NSTableView *)aTableView objectValueForTableColumn:(NSTableColumn *)aTableColumn row:(NSInteger)rowIndex {
NSButtonCell *buttonCell;
if(aTableView == dimensionTable) {
[dimensionButtonCell setTitle:#"Dimension"];
buttonCell = dimensionButtonCell;
}
else if(aTableView == shopTable) {
[shopButtonCell setTitle:#"Shop"];
buttonCell = shopButtonCell;
}
else if(aTableView == countryTable) {
[countryButtonCell setTitle:#"Country"];
buttonCell = countryButtonCell;
}
return buttonCell;
}
I have 2 questions:
I cannot change checkbox state through GUI. I can change it programatically, though. It blinks a bit, when you hold down mouse button, but doesn't allow change...
I tried to fill data as with views, without outlets to cells. It didn't work. Are NSButtonCell cels within cell views somehow different as view based Table Views or "normal" cel based Table Views?
After long struggle I manage to find the solution for the problem. One part of the problem was simple bug at the data model side, but it wasn't crucial, something much more difficult was to be done with NSTableView delegate and datasource.
THere were mainly 3 difficulties that prevented good understanding and managing this problem:
apple's documentation lacks of any reasonable explanation about differences and typical usage of - (id)tableView:(NSTableView *)aTableView objectValueForTableColumn:(NSTableColumn *)aTableColumn row:(NSInteger)rowIndexin table view's data source and - (NSCell *)tableView:(NSTableView *)tableView dataCellForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row of its delegate. While it may seem that you would need latter method, because NSButtonCells are custom NSCells it turns out it is not necessary, but I left it at the end anyway.
internal conversions in NSTableView methods
problem is not documented almost anywhere on the net
Here are steps you should do:
- (id)tableView:(NSTableView *)aTableView objectValueForTableColumn:(NSTableColumn *)aTableColumn row:(NSInteger)rowIndex {
buttonCell = [aTableColumn dataCell];
NSString *columnKey = [aTableColumn identifier];
return buttonCell;
}
You can see this method has to be implemented whether you use it or not.
- (NSCell *)tableView:(NSTableView *)tableView dataCellForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row {
buttonCell = [tableColumn dataCell];
NSString *columnKey = [tableColumn identifier];
if(tableView == dimensionTable) {
// returnObject = #"Dimension";
// [dimensionButtonCell setTitle:#"Dimension"];
// buttonCell = dimensionButtonCell;
}
else if(tableView == shopTable) {
[buttonCell setState:[[mySelectedShops objectAtIndex:row] integerValue]];
[buttonCell setTitle:[myShops objectAtIndex:row]];
}
else if(tableView == countryTable) {
[buttonCell setState:[[mySelectedCountries objectAtIndex:row] integerValue]];
[buttonCell setTitle:[myCountries objectAtIndex:row]];
}
return buttonCell;
}
you can see I used second method, however objectValueForTableColumn could be used solely.
You can also see, I have NSMutableArray mySelectedShops and mySelectedCountries to hold NSInteger (1 or 0) wrapped in NSNumber for each row in Table View.
If you set the state or integerValue of NSCell makes no difference. Both will check and uncheck NSButtonCell with values 1 or 0 of NSInteger type.
- (void)tableView:(NSTableView *)aTableView setObjectValue:(id)anObject forTableColumn:(NSTableColumn *)aTableColumn row:(NSInteger)rowIndex {
NSString *columnKey = [aTableColumn identifier];
if(aTableView == dimensionTable) {
// [dimensionButtonCell setTitle:#"Dimension"];
// buttonCell = dimensionButtonCell;
}
else if(aTableView == shopTable) {
[mySelectedShops replaceObjectAtIndex:rowIndex withObject:[NSNumber numberWithInteger:[(NSCell*)anObject integerValue]]];
}
else if(aTableView == countryTable) {
[mySelectedCountries replaceObjectAtIndex:rowIndex withObject:[NSNumber numberWithInteger:[(NSCell*)anObject integerValue]]];
}
}
Although I passed NSInteger value to NSCell object, anObject here is of __NSCFBoolean type, which means something doesn't work as expected. To be able to replace object value to arrays I have casted it to NSCell only to get integerValues. It actually works without cast as well, so it is another mystery to me, but I like it more that way.
It is clear Apple is moving to view based cells like in UITableView. Still, I hope this will help to somebody.

outlineView:dataCellForTableColumn:item: has strange side effect

I have an outline view delegate and am overriding outlineView:dataCellForTableColumn:item: to make the cells in my outline view into buttons (see this question). Here is the code form my delegate:
- (NSCell *)outlineView:(NSOutlineView *)outlineView dataCellForTableColumn:(NSTableColumn *)tableColumn item:(id)item;
{
MyCell * myCell = [[MyCell alloc] init];
// return nil;
return myCell;
}
Doing this has a strange side effect. In my outline view's data source, the method outlineView:objectValueForTableColumn:byItem: always gets a null value for tableColumn.
The code is:
- (id)outlineView:(NSOutlineView *)outlineView objectValueForTableColumn:(NSTableColumn *)tableColumn byItem:(id)item
{
printf("tableColumn:%s\ttable identifier: %s\n", [[tableColumn className] cString], [[tableColumn identifier] cString]);
return [item valueForKey:[tableColumn identifier]];
}
And the output is:
tableColumn:(null) table identifier: (null)
What's strange is that this only happens when I implement the outlineView:dataCellForTableColumn:item: method. What am I missing here?
EDIT:
Modifying the delegate function like this seems to fix the problem:
- (NSCell *)outlineView:(NSOutlineView *)outlineView dataCellForTableColumn:(NSTableColumn *)tableColumn item:(id)item;
{
printf("delegate column identifier: %s\n", [[tableColumn identifier] cStringUsingEncoding:NSASCIIStringEncoding]);
if (tableColumn == nil)
{
return nil;
}
MyCell * aCustomCell = [[MyCell alloc] init];
return aCustomCell;
}
However, I don't really understand what's going on here. If anyone can explain, that would be helpful. Thanks!
NSOutlineView has the ability for you to use a single cell to draw an entire row, rather than a separate cell for each column. It first asks for a cell passing in a nil table column. If you return a cell for that call, it uses it to draw the whole row, otherwise it continues and asks for a separate cell for each column. So, as you discovered, the solution is to return a nil cell when passed a nil table column, so things will draw normally.

Resources