NSCollectionView Drag Drop example - macos

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.

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).

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

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!

How to add subview in ListView?

I am developing my first MAC application, i downloaded one Example of PxListView
and i have to added one button and background image on cell xib and bind them with controller
and, when on button click i was set height of that cell is much bigger then other. that is done,
and work fine.
but now i want to develop like after is witch cell has open in that cell i want to add some extra contain (Controller) on it, so how it will possible using given example?
pls help me to give some suggest how it will be done.
for Ex like before click on button
after chick on button i want to develop like
You write
i have to added one button and background image on cell xib and bind them with controller
It sounds like you've subclassed PXListViewCell--for convenience, let's call your subclass TemplateListViewCell--and added a xib from which instances of TemplateListViewCell will be loaded in
+[PXListViewCell cellLoadedFromNibNamed:bundle:reusableIdentifier:]
In addition, there is a[t least one] button in TemplateListViewCell.xib.
You write
when on button click i was set height of that cell is much bigger then other. that is done, and work fine
It sounds like this button has as its action a method on TemplateListViewCell such as
- (IBAction)toggleDetail:(id)sender
{
//Code to grow or shrink the height of [self frame].
//...
}
In my approach to implementing -toggleDetail, two modifications to the PXListView files were necessary:
1. Adding a protocol method
- (void)listView:(PXListView *)aListView setHeight:(CGFloat)height ofRow:(NSUInteger)row;
to the PXListViewDelegate protocol.
2. Adding a property
#property (nonatomic, assign) BOOL expanded;
to PXListViewCell.
My implementation of -toggleDetail looks something like this:
- (IBAction)toggleDetail:(id)sender
{
BOOL wasExpanded = [self expanded];
NSRect oldFrame = [self frame];
CGFloat oldHeight = oldFrame.size.height;
CGFloat newHeight = oldHeight;
CGFloat heightIncrement = 0.0f;
if (wasExpanded) {
heightIncrement = -80.0f; //use whatever value is appropriate
} else {
heightIncrement = 80.0f; //use whatever value is appropriate
}
newHeight += heightIncrement;
[[[self listView] delegate] listView:[self listView] setHeight:newHeight ofRow:[self row]];
[[self listView] reloadData];
BOOL isExpanded = !wasExpanded;
[self setExpanded:isExpanded];
}
It might seem better to use [[self listView] reloadRowAtIndex:[self row]]; in place of [[self listView] reloadData], but unfortunately, this doesn't work: if the user hides the detail--shrinks the cell vertically--new cells which should appear on the screen do not.
You write
that is done, and work fine.
It sounds like you were able to implement successfully a method analogous to -[TemplateListViewCell toggleDetail:].
You write
but now i want to develop like after is witch cell has open in that cell i want to add some extra contain (Controller) on it, so how it will possible using given example? pls help me to give some suggest how it will be done.
It sounds like you want instances of TemplateListViewCell to contain extra views if they are expanded.
It might seem tempting to put this code into -[TemplateListViewCell toggleDetail], but this will not work out as we might hope. The trouble is, we need to handle cases where expanded cells have been scrolled out of view and scrolled back into view.
To get this right, we need to have a notion of expanded which persists beyond the usage of a PXListViewCell subclass instance: we either need to keep track of expansion in the PXListView itself or in its delegate.
The better--but less expedient--design seems to be to keep track of this information in the PXListView itself. For the sake of this question, however, I'll demonstrate how to keep track of cell expansion in the delegate. To do this, I'm expanding the PXListViewDelegate protocol and making other changes to the PXListView files:
1. Adding the methods
- (void)listView:(PXListView *)aListView setExpanded:(BOOL)expanded atRow:(NSUInteger)row;
- (BOOL)listView:(PXListView *)aListView expandedAtRow:(NSUInteger)row;
to PXListViewDelegate.
2. Adding the method
- (void)setCell:(PXListViewCell *)cell expandedAtRow:(NSUInteger)row
{
if ([[self delegate] respondsToSelector:#selector(listView:expandedAtRow:)]) {
[cell setExpanded:[[self delegate] listView:self expandedAtRow:row]];
}
}
to PXListView.
3. Calling -[PXListView setCell:expandedAtRow:] from -[PXListView layoutCells]
- (void)layoutCells
{
//Set the frames of the cells
for(id cell in _visibleCells)
{
NSInteger row = [cell row];
[cell setFrame:[self rectOfRow:row]];
[self setCell:cell expandedAtRow:row];
[cell layoutSubviews];
}
NSRect bounds = [self bounds];
CGFloat documentHeight = _totalHeight>NSHeight(bounds)?_totalHeight:(NSHeight(bounds) -2);
//Set the new height of the document view
[[self documentView] setFrame:NSMakeRect(0.0f, 0.0f, NSWidth([self contentViewRect]), documentHeight)];
}
and from -[PXListView layoutCell:atRow:]:
- (void)layoutCell:(PXListViewCell*)cell atRow:(NSUInteger)row
{
[[self documentView] addSubview:cell];
[cell setFrame:[self rectOfRow:row]];
[cell setListView:self];
[cell setRow:row];
[cell setHidden:NO];
[self setCell:cell expandedAtRow:row];
}
4. Setting _expanded to NO in -[PXListViewCell prepareForReuse]:
- (void)prepareForReuse
{
_dropHighlight = PXListViewDropNowhere;
_expanded = NO;
}
Note: In the sample PXListViewCell subclass, MyListViewCell, distributed with PXListView, the implementation of -[MyListViewCell prepareForReuse] fails to call [super prepareForReuse]. Make sure that this call is made in [TemplateListViewCell prepareForReuse]:
- (void)prepareForReuse
{
//...
[super prepareForReuse];
}
One change needs to be made to -[TemplateListViewCell toggleDetail:]. The line
[self setExpanded:isExpanded];
needs to be replaced by
[[[self listView] delegate] listView:[self listView] setExpanded:isExpanded atRow:[self row]];
Once you've set up your PXListView's delegate to properly handle the new delegate methods, you're ready to override [PXListViewCell setExpanded:] in your subclass TemplateListViewCell:
- (void)setExpanded:(BOOL)expanded
{
if (expanded) {
//add detail subviews
} else {
//remove detail subviews
}
[super setExpanded:expanded];
}
Replace //add detail subviews with your own code which programmatically adds the detail subviews that you want and replace //remove detail subviews with code to remove the detail subviews that you want, checking to see that they are present first.
You write
i want to add some extra contain (Controller) on it
It sounds like you want to add view controllers rather than views to your TemplateListViewCell. To do this, use an NSBox and set the box's contentView to your view controller's view. (For details on this, see this answer.)
If you plan on just showing a single view controller's view in an NSBox on the expanded TemplateListViewCell, you can just (1) add a property to TemplateListViewCell referencing your view controller and (2) add an NSBox to TemplateListViewCell xib and set its contentView to the appropriate view controller's view on [cell setExpanded:YES] and set its contentView to nil on [cell setExpanded:NO].

Coloring rows in View based NSTableview

I have a view based nstableview. I want to color entire row based on some condtion for which I have used code below
- (NSTableRowView *)tableView:(NSTableView *)tableView rowViewForRow:(NSInteger)row
{
NSTableRowView *view = [[NSTableRowView alloc] initWithFrame:NSMakeRect(1, 1, 100, 50)];
[view setBackgroundColor:[NSColor redColor]];
return view;;
}
The delegate method is called, but table doesn't seem to be using NSTableRowView returned by delegate method.
Main aim here is coloring entire row based on some condition. Whats wrong in above implementation?
For anyone else who hits this and wants a custom NSTableRowView backgroundColor, there are two approaches.
If you don't need custom drawing, simply set rowView.backgroundColor in - (void)tableView:(NSTableView *)tableView didAddRowView:(NSTableRowView *)rowView forRow:(NSInteger)row in your NSTableViewDelegate.
Example:
- (void)tableView:(NSTableView *)tableView
didAddRowView:(NSTableRowView *)rowView
forRow:(NSInteger)row {
rowView.backgroundColor = [NSColor redColor];
}
If you do need custom drawing, create your own NSTableRowView subclass with desired drawRect. Then, implement the following in NSTableViewDelegate:
Example:
- (NSTableRowView *)tableView:(NSTableView *)tableView
rowViewForRow:(NSInteger)row {
static NSString* const kRowIdentifier = #"RowView";
MyRowViewSubclass* rowView = [tableView makeViewWithIdentifier:kRowIdentifier owner:self];
if (!rowView) {
// Size doesn't matter, the table will set it
rowView = [[[MyRowViewSubclass alloc] initWithFrame:NSZeroRect] autorelease];
// This seemingly magical line enables your view to be found
// next time "makeViewWithIdentifier" is called.
rowView.identifier = kRowIdentifier;
}
// Can customize properties here. Note that customizing
// 'backgroundColor' isn't going to work at this point since the table
// will reset it later. Use 'didAddRow' to customize if desired.
return rowView;
}
Finally it worked as below
- (NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row
{
NSView *cellView = (NSView*) [tableView makeViewWithIdentifier:[tableColumn identifier] owner:[tableView delegate]];
CALayer *viewLayer = [CALayer layer];
[viewLayer setBackgroundColor:[[NSColor redcolor] CGColor]];
[cellView setWantsLayer:YES];
[cellView setLayer:viewLayer];
return cellView;
}
Please note.. u need to convert nscolor to cgcolor which you can find in https://gist.github.com/707921 or http://forrst.com/posts/CGColor_Additions_for_NSColor-1eW
If you watch the presentation on view based tableviews from WWDC 2011, you'll see that the main idea is to create the views in Interface Builder, and then obtain them from there. Something like:
[tableView makeViewWithIdentifier:#"GroupRow" owner:self];
Once you have obtained the view, just set its properties and return it.
Notice in this example that it has its own identifier, so remember to set that, but you can also used automatic identifiers.
I don't know if a direct link to the WWDC will work, but the main page is here: https://developer.apple.com/videos/wwdc/2011/ and if you search for "View Based NSTableView Basic to Advanced", you'll find it. It is well worth watching.
I re-wrote the layer approach.
In Swift 3.2
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
let greenCell = self.tableview.make(withIdentifier: "green", owner: self)
let layer:CALayer = CALayer()
layer.backgroundColor = NSColor.green.cgColor
greenCell?.wantsLayer = true
greenCell?.layer = layer
return greenCell
}
Don't forget to change the Identifier of the cell according to your storyboard, and in the code identifier "green". And surely, the background color if you want.

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

Resources