NSTableView Drag and Drop not working - objective-c

I'm trying to set up very basic drag and drop for my NSTableView. The table view has a single column (with a custom cell). The column is bound to an NSArrayController, and the array controller's content array is bound to an NSArray on my controller object. The data displays fine in the table. I connected the dataSource and delegate outlets of the table view to my controller object, and then implemented these methods:
- (BOOL)tableView:(NSTableView *)aTableView writeRowsWithIndexes:(NSIndexSet *)rowIndexes toPasteboard:(NSPasteboard *)pboard
{
NSLog(#"dragging");
return YES;
}
- (NSDragOperation)tableView:(NSTableView*)tv validateDrop:(id <NSDraggingInfo>)info proposedRow:(NSInteger)row proposedDropOperation:(NSTableViewDropOperation)op
{
return NSDragOperationEvery;
}
- (BOOL)tableView:(NSTableView *)aTableView acceptDrop:(id <NSDraggingInfo>)info
row:(NSInteger)row dropOperation:(NSTableViewDropOperation)operation
{
return YES;
}
I also registered the drag types in -awakeFromNib:
#define MyDragType #"MyDragType"
- (void)awakeFromNib
{
[super awakeFromNib];
[_myTable registerForDraggedTypes:[NSArray arrayWithObjects:MyDragType, nil]];
}
The problem is that the -tableView:writeRowsWithIndexes:toPasteboard: method is never called. I've looked at a bunch of examples and I can't figure out anything I'm doing wrong. Could the problem be that I'm using a custom cell? Is there something I'm supposed to override in the cell subclass to enable this functionality?
EDIT: Confirmed. Switching the custom cell for a regular NSTextFieldCell made dragging work. Now, how do I make drag and drop work with my custom cell?

I was banging my head against the wall in search of a more elegant solution to the same problem, and then I came across this:
http://www.wooji-juice.com/blog/cocoa-10-bindings.html
For instance, if you want to support drag-and-drop from a table, you need to set up a data source for it — even if you’re using bindings to supply the actual data, you can set a data source on it, and Cocoa needs one to handle the tableView:writeRowsWithIndexes:toPasteboard and related messages.
Yep. If you've got everything bound to your array controller, what you can do is to have the array controller implement the necessary drag/drop functions and then set the table view's data source to the array controller with setDataSource:.

I fixed the issue. There seems to be an issue with using bindings with custom NSCells in a table view. Switching to the traditional NSTableViewDataSource methods rather than bindings and an array controller solved it.

What works for me is to call initTextCell rather than init or initImageCell within the initializer of my custom cell (in my case, init). It doesn't seem to matter whether the superclass is NSCell or NSActionCell. Also, I have binding, and dragging still works.

That should be sufficient to allow the drag to start. Are you sure you've connected the delegate methods?

I ran into this problem, your custom cell needs to extend NSActionCell not NSCell if you want drag and drop to work properly. There is probably something you could implement in NSCell that would make it all work too, but I didn't dig any further after switching to NSActionCell. At least, that fixed the issue for me.

I ran into the problem. I have a NSCell subclass, and I did implement the tableView:writeRowsWithIndexes:toPasteboard and the dataSource was set for the NSTableView. Dragging would not work.
If I set the cell type in my init for the custom subclass
self.type = NSTextCellType;
Then I get dragging. If I don't, it defaults to NSNullCellType and dragging doesn't work. I'm guessing the people who got it working by using another subclass NSTextFieldCell works because the cell type is different.

I also observe the similar issue, NStableView drag & drop is not working. I have 4 column in my tableview and two of them are custom cells. Dragging for non-custom cell is working fine even though its working on cell separator as well however its not working with custom cells.
My custom cell was subclassed from NSButtonCell which was causing the
issue. So as suggested, I changed my parent class from NSButtonCell to
NSActionCell. Now, dragging is working perfectly.
It works with both NSCell as well as NSActionCell however I required action on my cell so used NSActionCell.

Related

Receiving notification on a UICollectionViewCell's (or UITableViewCell's) removal from its parent collection or table view

Background:
I have a UICollectionViewController that shows items in one of two modes, which the user can toggle between. Each mode uses a different class of UICollectionViewCell. Let's call these modes "list view" and "grid view".
When I switch modes, I call .reloadData on the UICollectionView, which redraws the collection view using the correct cell classes. Everything works fine here.
Now: Inside the UICollectionViewCell subclass for one type of cells, I want to be notified when the collection view that contains it switches modes. Visually, a cell which was on-screen vanishes; the collection view is drawn fully with the other type of cell. When switching back, the cell is re-displayed.
Question:
How can I be notified when a UICollectionViewCell is "removed" (i.e., no longer shown; I'm not sure what's happening under the hood yet) from its parent collection view?
Notes:
prepareForReuse is not called on the cell when the collection view's updateData causes the cell to no longer be included.
willTransitionFromLayout:toLayout: (an empty layout?) is not called.
Overriding didMoveToSuperview is of no help; it is not called.
Observing .hidden or .alpha on the cell does not work.
The cell's dealloc is not called; it sticks around in the reuse queue.
Something in the cell must be changing that I can observe or hook into, what is it?
Update: UICollectionViewDelegate has this method, which from the documentation seems like it does what I am asking:
collectionView:didEndDisplayingCell:forItemAtIndexPath:
Original answer:
I got this working as desired by having the UICollectionViewController manually notify visible cells of impending doom with this method when I'm about to toggle and call reloadData:
- (void)notifyCellsWillBeHidden {
for (UICollectionViewCell *cell in self.collectionView.visibleCells) {
if ([cell respondsToSelector:#selector(willBeRemovedFromCollectionView)]) {
[cell performSelector:#selector(willBeRemovedFromCollectionView)];
}
}
}
These cells can then do what they need to do if they implement the above method.
Calling prepareForReuse may not a good idea because it will be called again before the cells are re-displayed, if this is a problem.
I'm still curious whether there is a way for a cell to receive notification that it is going to be made non-visible without an explicit call.

Setting Up a UICollectionView in iOS

I've been hunting around for ways to setup a UICollectionView for an iOS app. Google only turns up a few blogs with posts about what it is, not how it works. Then of course, there's the Apple documentation which is helpful, but doesn't provide as much information as I'd like to be able to setup a UICollectionView.
How can one setup a UICollectionView?
The uicollectionview class is almost identical to the uitableview Class. They share many of the same methods and functions. And if the methods / functions are different, most of the time it's just a matter of swapping out "row" for "cell" and vice versa. However there are a few methods that don't exist on UICollectionView that do on UITableView. First though, I'll explain how to setup a UICollectionView:
Begin by adding your UICollectionView to a current ViewController, or creating a new UICollectionViewController. The steps aren't that much different for the view and controller.
If you're using the View and not the ViewController, make sure that the Delegate and DataSource of the CollectionView is the view controller it's on. Also make sure to add the Delegate and DataSource to your header file: <UICollectionViewDataSource, UICollectionViewDelegate>
Next, make sure to include these three methods in your view controller's class:
-(NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView
-(NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
These are the only required methods. The first tells the collection view the number of sections it should have. This should return an integer value. The second method gets the number of cells in each section. Again, this should return an integer value. The last method populates each cell using the data given (usually from an NSArray). This last method should return a CollectionViewCell. If you set breakpoints on this method, you'll notice that it is called once for every cell defined in the numberOfItemsInSection method.
UICollectionViews provide advanced animation methods and allow cells to be deselected and selected (similar to apps like Pages when in 'Edit' mode). However, to my knowledge, UICollectionViews do not provide features such as "swipe to delete" or and kind of disclosure indicator.
UICollectionViews also allow you to create custom cells using xib (AKA nib) files, this allows for some very advanced-looking and unique interfaces without lots of complicated code.
Sadly, UICollectionView is only supported in iOS 6 and up. There are a few projects available such as PSTCollectionView which adds support for CollectionViews in iOS 4.3+, but I haven't figured out how to use them. In my case, when the view loads I just check if the UICollectionView Class is available and if it isn't then I load a Table instead.
Here is a link to Apple's official documentation on Collection Views. You might also want to check out this tutorial.
I created a step-by-step tutorial for setting up UICollectionViews with custom layouts. Hopefully it helps some people to get familiar with the API.

Forwarding drag & drop event to parent view

I have an application where I have one custom view which is derived from NSView.
Within this view, there are several custom subviews, which are also derived from NSView.
I want to implement a drag and drop behavior which allows URLs to be dropped onto the views. Everything is already working for the main view.
So, actually I would have to implement dragging behavior handlers on the child-views and the parent-view class.
The thing is, that I don't want to copy the complete handling code to all the child-views to make them also accept drag events. So I thought that it would be the best way to just let them forward all drag events to the parent view.
Is this possible somehow?? Not sure if I can somehow set this up with the responder-chain maybe?
Any tips are highly appreciated!!
Thanks in advance.
I faced a similar issue where I wanted anything dropped in a view, or any of it's subviews to be processed by the view, but the calls never got there.
After some research I found this answer to be the most helpful: https://stackoverflow.com/a/7389711/327471
Essentially if a view is not registered to receive any drag events it should pass that information up to it's parent view automatically. So in my case I ended up with something like this:
NSArray *subviews = [self.view subviews];
for (NSView *aSubview in subviews) {
[aSubview unregisterDraggedTypes];
}
Of course you can be more precise than that, and make sure to only check subclasses of a certain type or whatever parameters you want. But ultimately the key was unregistering the problem subview from it's dragged types.
I hope this helps.
If the subviews are used for display only and don't require any user interaction, you can override -hitTest: in the parent view like so:
- (NSView *)hitTest:(NSPoint)aPoint
{
NSView* hitView = [super hitTest:aPoint];
if(hitView)
return self;
return nil;
}
This makes the parent view receive all mouse events.
Still works XCode 10.1. Swift 4.2. under 10.14.4 Beta (18E184e).
// MARK: - ViewController lifecycle
override func viewDidLoad() {
super.viewDidLoad()
self.view.subviews.forEach { $0.unregisterDraggedTypes() }
}
There's probably a better way, but you could put your dragging protocol implementation in a category, rather than in the view directly, and include that category in each of the views.

View-based NSOutlineview selection gradient

I'm still struggling with the view-based NSOutlineView in my little Cocoa application. I'm trying model my OutlineView after the finder one. When the Finder OutlineView loses focus (e.g. clicking any folder on the right side), the selected row (e.g. Desktop) stays selected with the bright blue gradient and does not change to the inactive blue-grey gradient.
I'd like to duplicate this behaviour in my application.
In a not view-based OutlineView I was able to subclass NSOutlineView and reimplement (void)highlightSelectionInClipRect:(NSRect)clipRect, so that each highlighted row could be supplied with the bright blue background image.
However, now with my view-based OutlineView (set to SourceList style) this method apparently is not even called. I've even implemented (id)_highlightColorForCell:(NSCell *)cell to return nil, as some sites suggest, but that doesn't help either.
Any hints on how I can set the highlight gradient in the view-based OutlineView?
Are you doing any custom drawing that could be messing with things? As far as I can tell all the selection drawing is handled for you normally, check out the TableViewPlayground example (not Source-list style by default but that's an easy change to the XIB).
But failing that, according to the Mac OS X 10.7 doc entry on highlightSelectionInClipRect:
Note: This method should not be subclassed or overridden for a view-base table view.
Instead, row drawing customization should be done by subclassing NSTableRowView.
So I think (I haven't tried any of this) like you'd want to subclass NSTableRowView, override drawSelectionInRect: (there's an example in TableViewPlayground, and draw your selection. You could check for the app being active with [NSApp active] or maybe use the self.emphasized property like the example does.
You'd then return one of your custom NSTableRowViews in the NSOutlineViewDelegate protocol method (10.7 only!): (NSTableRowView *)outlineView:(NSOutlineView *)outlineView rowViewForItem:(id)item
Hope this works/helps!
I should note that TableViewPlayground example uses the outlineView:viewForTableColumn:item: delegate method by default and does everything with NSTableViewCells, but if you add the rowViewForItem method I mention above it is called. So I'm guessing you could use it to return a view for each row in it's entirety.
Thanks, with your hint I was able to solve the problem quite easily. I subclassed NSTableRowView and overwrote -(BOOL) isEmphasized to always return true.
I then implemented -(NSTableRowView *)outlineView:(NSOutlineView *)outlineView rowViewForItem:(id)item in my OutlineViews delegate to return a item specific instance of my subclass by calling ClbTableRowView *result = [outlineView makeViewWithIdentifier:identifier owner:self];
Edit: Besides that, there also seems to be a pretty hidden way of using the custom NSTableRowView subclass by dropping a new NSView Object into the the OutlineView in Interface Builder. Then set the views class to your subclass and give it a userinterface item identifier of "NSTableViewRowViewKey", according to the Apple documentation.

How to use NSCollectionView and Outlets properly?

I'm desperately trying to connect controls of NSViews which will reside in a NSCollectionView using outlets. The collection view is fed using an NSArrayController.
I created the NSView in a separate NIB file and in the implementation of NSCollectionViewItem I overwrote copyWithZone to load it:
-(id)copyWithZone:(NSZone *)zone
{
id result = [super copyWithZone:zone];
[NSBundle loadNibNamed:#"InputView" owner:result];
return result;
}
I've used this approach according to this instructions.
Unfortunately this is what happening:
The NSView looks like this:
The NSCollectionView resides in a NSScrollView and the scrollbar is set to enable automatically.
But as you can see there's no scrollbar.
I don't really understand what I need to do so the NSCollectionView knows the dimensions of its NSViews.
It has worked before when I didn't have a seperate NIB-file, but then I couldn't make outlet connections from the view to the item :-(
How many item are in the array controller? Your output looks correct for what you've described, assuming there are at least 14 things in the controller (1 view per item). The sizing is just off. It's not clear which problem you're trying to solve.
Perhaps you were looking for a grid, and so need to call setMaximumNumberOfColumns:? Or perhaps your views aren't being resized as you expect (check -maxItemSize and -minItemSize)?