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.
Related
I have looked over the google but have not found many examples on this. Most only relate to the NSTableView.
How do I subclass my NSOutlineView to look exactly like XCode's plist editor? I've do not know how to get the border colors shown below. I currently have it working exactly the same, except for the coloring. Any help would be appreciated.
UPDATE
The part I am confused about is what the draw code should look like. Google didnt help me much on this.
Step 1: Create an NSTableViewRow subclass
The borders are drawn on the NSTableRowView instances that make up the table's rows. To get them, you could subclass NSTableRowView and give it a borders property that stores a mask specifying which borders you want drawn on the row. You'd accompany this with a custom implementation of drawRect, which calls super first, and then strokes on the specified borders.
Step 2: Implement the delegate methods that signal when you'll need to update the borders
In the simplest case, I can think of three:
- (void)outlineViewItemWillExpand:(NSNotification *)notification
- (void)outlineViewItemDidCollapse:(NSNotification *)notification
- (NSIndexSet *) outlineView:(NSOutlineView *)outlineView
selectionIndexesForProposedSelection:(NSIndexSet *)proposedSelectionIndexes
This is where you'll find the hard work. I'd start by having a go at the last one first. There will be lots of different ways of doing it, but you could get the node that's about to be selected from the proposedSelectionIndexes argument in conjunction with the NSOutlineView api. Once you've got this node you can work out which rows will need a border by getting the nodes represented in the rows below this soon-to-be-selected row. If a given node is a descendant of the new selection, then it'll need a border of some sort (hint: the indexPath property of NSTreeNode comes in handy here).
Step 3: Make sure the outline view is using your custom row views
Implement the following delegate method so that it returns your custom row views:
- (NSTableRowView *)outlineView:(NSOutlineView *)outlineView rowViewForItem:(id)item
Note that the second item argument is an NSTreeNode object. By keeping a record (a map of some sort) of which nodes are associated with which borders, you can then set the border mask property of your NSTableRowView subclass.
I am trying to create a custom NSView that hosts a CALayer hierarchy to perform efficient display. This NSView is then embedded within a NSTableCellView that is displayed by a View-Based NSOutlineView.
The problem is that whenever I expand or collapse an item, all rows are being moved, but the layer's content remains displayed at the position it was before changing the outline.
Scrolling the NSOutlineView seems to refresh the layers and they resync with their rows at that point.
I have debugged this behavior using Instruments and it seems that the scrolling provokes a layout operation which updates the layers with a setPosition: call that should have occured when expanding or collapsing items.
Here is some sample code for a simple layer hosting NSView subclass.
#interface TestView : NSView
#end
#implementation TestView
- (instancetype)initWithFrame:(NSRect)frameRect
{
self = [super initWithFrame:frameRect];
CAShapeLayer* layer = [CAShapeLayer layer];
layer.bounds = self.bounds;
layer.position = CGPointMake(NSMidX(self.bounds), NSMidY(self.bounds));
layer.path = [NSBezierPath bezierPathWithOvalInRect:self.bounds].CGPath;
layer.fillColor = [NSColor redColor].CGColor;
layer.delegate = self;
self.layer = layer;
self.wantsLayer = YES;
return self;
}
#end
I have tried a lot of potential solutions to this problem but I couldn't find any interesting method that gets called on the NSView instance that could be overriden to call [self.layer setNeedsDisplay] or [self.layer setNeedsLayout]. I also tried various setters on the CALayer itself such as :
layer.autoresizingMask = kCALayerWidthSizable | kCALayerHeightSizable;
layer.needsDisplayOnBoundsChange = YES;
self.layerContentsRedrawPolicy = NSViewLayerContentsRedrawOnSetNeedsDisplay;
Can anyone help me figure out how to make this layer display properly inside a NSOutlineView?
I ended up answering my question. The problem wasn't in the way my TestView was implemented. I simply missed one of the steps for enabling CoreAnimation support within the application. The relevant reference is within the Core Animation Programming Guide.
Basically, in iOS Core Animation and layer-backing is always enabled by default. On OS X, it has to be enabled this way :
Link against the QuartzCore framework
Enable layer support for one or more of your NSView objects by doing one of the following
In your nib files, use the View Effects inspector to enable layer support for your views. The inspector displays checkboxes for the selected view and its subviews. It is recommended that you enable layer support in the content view of your window whenever possible
For views you create programmatically, call the view’s setWantsLayer: method and pass a value of YES to indicate that the view should use layers.
Once I enable layer support on any of the NSOutlineView's parents, the various glitches are solved.
It is difficult to read the NSOutlineView reference documents and find the information about cell reuse that is likely giving you fits here.
You may have looked at outlineViewItemDidCollapse: but it's kind of a useless for our issue, because it doesn't have a pointer to an NSView, and that's because it's older than view-based outline views.
Perhaps the one helpful mention, buried within the NSOutlineViewDelegate protocol, down in the section on view-based NSOutlineView methods, there is a single mention within outlineView:didRemoveRowView:forRow: that:
The removed rowView may be reused by the table, so any additionally inserted views should be removed at this point.
In other words, when you call the outline view's makeViewWithIdentifier:owner:, for a cellView or rowView with a particular ID you often get a recycled view. Especially often because of collapse. Incidentally, that method is from the NSTableView superclass, and in that reference, there's also this comment:
This method may also return a reused view with the same identifier that is no longer available on screen. If a view with the specified identifier can’t be instantiated from the nib file or found in the reuse queue, this method returns nil.
So you have the option of altering the view hierarchy or niling properties in didRemoveRowView:forRow. However, buried within a third cocoa reference, that for NSView, there is within the commentary on prepareForReuse, this comment:
This method offers a way to reset a view to some initial state so that it can be reused. For example, the NSTableView class uses it to prepare views for reuse and thereby avoid the expense of creating new views as they scroll into view. If you implement a view-reuse system in your own code, you can call this method from your own code prior to reusing them.
So, TL;DR, you need to implement prepareForReuse.
The pertinent references are (mostly) the superclasses of both NSOutlineView and NSTableCellView.
And, FWIW, there was a similar question here, where the questioner seems to indicate things are even worse than I think, in that NSOutlineView is more creative behind the scenes than NSTableView.
In my own work with outline views and embedded NSTextViews, I've seen wildly terrible rendering hiccups relating to expand/collapse/scroll that I seem to have managed in just the NSOutlineViewDelegate methods. On iOS they did everyone the favor of renaming makeViewWithIdentifier to the more explicit dequeueReusableCellViewWithIdentifier.
You shouldn't have to enable layer backing for any of the ancestor views (like the outline view).
In my experience, the layer immediately assigned to a view (as opposed to sublayers) doesn't need its bounds, position, or autoresizing mask to be set. It is automatically made to track the bounds of the view. In fact, I would avoid setting those properties, just in case that breaks the automatic synchronization with the view's bounds rect.
So, the question is: how are you arranging for the view to move or resize with its superview? Are you using auto layout? If so, did you turn off its translatesAutoresizingMaskIntoConstraints? If yes to both, what constraints are you setting on the view? If no to either, how did you position the view within its superview? What frame did you set? Also, is the superview configured to autoresize its subviews (probably yes, since that's the default)? What is your view's autoresizingMask?
You could also override -setFrameOrigin: and -setFrameSize: in your custom view class and call through to super. Also, add logging to show when that's happening and what the new frame rect is. Is your view being moved as you expect when you expand or collapse rows?
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.
I am wanting to change the text background color on a tableview's cell when it is hovered upon, similar to how AddressBook "highlights" the label of a contact's element when you mouseover the label names. However I cannot figure out how to accomplish...
detecting a mouseover on a particular NSCell and...
After detecting the cell his hovered upon, highlighting the text in that cell (not highlighting the entire row as if the user selected that row)
As NSCell is not a subclass of NSView this seems to be a very difficult task.
Any example of this or explanation on how this might be done would be greatly appreciated.
Thanks!
I actually got it working using another method. I got it from the example posted here... http://www.cocoadev.com/index.pl?NSTableViewRollover
https://web.archive.org/web/20111013060111/http://cocoadev.com/index.pl?NSTableViewRollover
Instead of using NSCell's tracking mechanism, I am tracking mouseEntered/mouseExited and mouseMoved within my subclassed NSTableView.
When the tableview awakeFromNib method is called, I create a trackingRect from the visible portion of the tableview
I have a BOOL ivar that is set to YES when the mouse is within the tracking area(mouseEntered) and NO when it is not (mouseExited)
Within the mouseMoved method, I determine the current row the mouse cursor is on and set it to an NSInteger ivar and then call the tableview's setNeedsDisplayInRect: passing the rect of the row that the mouse is on.
I also override resetCursorRects to remove the old tracking rect and add a new one...this method is called when the tableview is scrolled upon so that it's tracking the latest visible rect.
Finally in my tableview's delegate, I determine the selected row (by retrieving the row index from the NSInteger ivar of the table view and change the cell's text color (or anything you want) if the currently drawn cell matches the row the mouse cursor is on. All this is done in the delegate method: tableView:(NSTableView *)aTableView willDisplayCell:(id)aCell forTableColumn:(NSTableColumn *)aTableColumn row:(NSInteger)rowIndex
I hope this helps others, as this was a bit tricky. It is also probably important to make sure that tableview is the firstResponder when the view loads, just makes things a bit more streamlined and cleaner.
Btw, is there a way to make a specific control in a view always be the firstResponder with nothing else possible as being the firstResponder? Even a method such as the iPhones... viewWillAppear method will help as I could set the first responder each time the view is visible...but i'm not aware of such a method on the Mac.
Overall, it's not a simple task as you noticed.
To track the mouse in an NSCell, subclass NSCell and override
-[NSCell startTrackingAt:inView:]
and
-[NSCell stopTracking:at:inView:mouseIsUp:]
Once you've detected the mouse is tracking inside a cell, you can find out which cell you are in the table with [tableView rowAtPoint:point] and [tableView columnAtPoint:point], and then find your frame with [tableView frameOfCellAtColumn:column row:row]
Then, you can change the way your cell is drawn by changing some property of the cell or changing the way it's drawn directly by overriding drawInteriorWithFrame:inView:.
Here's documentation on subclassing NSCell:
http://developer.apple.com/mac/library/documentation/cocoa/conceptual/ControlCell/Tasks/SubclassingNSCell.html
I achieved something similar by making use of addGlobalMonitorForEventsMatchingMask: handler: of NSEvent within my NSTableView subclass for the NSMouseMovedMask. using this along with columnAtPoint and rowAtPoint of NSTableView I was able to figure out if the position of the mouse was within a given cell.
Using this information I was able to bring up a PopOver when the mouse was over a particular cell.
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)?