Implement a Finder-like treeview and representing its nodes with lazy loading using NSOutlineview - cocoa

When implement a big file system tree in NSOutlineview, it lacks an event notification when user click the left side down arrow triangle icon of a node. This is very important when lazy-loading a large amount of file nodes into a directory node and represent it into NSOutlineview. Otherwise, developer have to load entire directory into it, in this way, the loading will force end user to wait, this is not acceptable. In short, current version of NSOutlineview can't implement lazy-loading caused by this issue. Does anyone meet this issue or have an alternative solution to implement a Finder-like treeview with Cocoa NSOutlineview, any help will be great appreciate.
In Addition:
NSOutlineview does not emit an outlineViewItemWillExpand event when click the icon, and there still lacks enough info to get which node will expand even that event emitted, [NSOutlineview selectedRow] can't work because the node which will be expanded have not been selected yet.

I load my child nodes in the following call in NSOutlineViewDataSource:
func outlineView(_ outlineView: NSOutlineView, isItemExpandable item: Any) -> Bool {
guard let node = item as? Node else { return false }
return node.children.count > 0
}
The children property of my Node class is lazy, and hence forced to load here.
This method is only called on Nodes that are visible in the outliner. This way you are only loading one layer ahead of the visible tree. The disclosure triangle (expand arrow) will then be visible if children exist.
If you have an efficient way to know the number of children a node has without actually loading them, then it may be better just to return the child count in outlineView(_: isItemExpandable:) and then do the actual load in outlineViewItemWillExpand(_:) as Willeke has suggested in the comments.

Related

How to reorder the way VoiceOver reads NSTextField's on a NSTableCellView?

I have a single-column NSTableView, populated with custom NSTableCellView's, very similar to the standard Mail app of macOS. Each custom NSTableCellView has four non-selectable read-only NSTextField's. VoiceOver for Accessibility reads things left to right, top to bottom. This results in a non-optimal order for VoiceOver users.
How can I change the order in which VoiceOver reads the NSTextField's within each NSTableCellView?
All examples and documentation I saw about re-ordering VoiceOver elements is related to iOS, but unfortunately the API in macOS is still different.
The order in which an element's accessibility children get visited can be controlled via accessibilityChildrenInNavigationOrder(). There are three wrinkles:
The children need to match what the view itself is reporting as its accessibilityChildren. So for controls, this will be the control's cell.
The view for which the children are being reordered must be an accessibilityElement.
The method expects all of the children which you want to visit to be passed to it. If you leave any out, they will not be visited, and will effectively be hidden from VoiceOver.
So, if you have 4 fields which VO is reading in the order labelC, labelB, labelD, labelA but you want in a different order, you can use it like this in Swift 5:
let orderedFields = [labelA, labelB, labelC, labelD]
let orderedChildren = orderedFields.compactMap { $0?.cell }
view.setAccessibilityChildrenInNavigationOrder(orderedChildren)
view.setAccessibilityElement(true)
This quickly becomes unwieldy for views with lots of children, but in your case it should work. If you want to do this dynamically, and all of your children are NSControls with cells, you can use NSCell's tag property to sort the cells:
let children = view.accessibilityChildren() as! [NSCell]
let orderedChildren = children.sorted { $0.tag < $1.tag }
view.setAccessibilityChildrenInNavigationOrder(orderedChildren as [NSAccessibilityElementProtocol])
view.setAccessibilityElement(true)
Then just set the cell tags in Interface Builder.

NSSplitView Collapse Event

I'm trying to implement an NSSplitView similar to Xcode where you can collapse a view by dragging its handle to under half its width and it will collapse automatically. I have that part working, but I need to update the state of a button in the toolbar when this happens. I tried listening to splitViewDidResizeSubviews and checking if the splitView's view is collapsed, but that method fires 16 times with collapsed == true, so I don't think I want to update the button's state 16 times. Is there a cleaner way to do this? I'm new to Cocoa, but from what I've seen, I would expect there to be some way to just say bind this button's state to the isCollapsed property and be done with it. Does such a thing exist?
If you subclass your NSSplitViewController you can add a listener for the SplitViewItem's isCollapsed property:
class MySplitViewController: NSSplitViewController {
var observer: NSKeyValueObservation?
override func viewDidLoad() {
super.viewDidLoad()
// Do view setup here.
let sideViewSplitViewItem = splitViewItems[0]
observer = sideViewSplitViewItem.observe(\.isCollapsed, options: [.initial, .new]) {splitViewItem, _ in
print("Sidebar collapsed state changed to: \(splitViewItem.isCollapsed)")
}
}
}
The best way to see what bindings are available is to check the docs, specifically the Cocoa Bindings Reference (look in the sidebar for the view you're after).
NSSplitView doesn't have the binding you describe, so I think you're on the right track with your current approach. Of course, you don't need to update the button's state sixteen times, just check it's value each time, and update it if needs be. It might seem a bit wasteful, but checking the value of a bool is a very cheap operation, and you won't notice any kind of performance hit.
While NSSplitView has no event or observable property for when one of it's subviews is "collapsed", the immediate subview itself will have its hidden property set to YES.
So you can either observe the subview's hidden property yourself, or if you're creating your own custom subview of NSView, you can override the -(void) setHidden:(BOOL)hidden to catch the event.
I do the latter, and it works correctly every time.
-(void)setHidden:(BOOL)hidden
{
[super setHidden:hidden];
// Insert code to react to the "collapsed" event here.
// (You're likely going to tell your parent view,
// self.window.windowController, or something.)
}
Of course the setHidden method / hidden property can in theory be called for purposes other than from NSSplitView "collapsing" your view, but this is very unlikely to ever happen in practice.

Prevent or revert drag & drop in Primefaces tree

I have a Primefaces 4 tree in which I want enable drag and drop. But I have constraints which do not allow to move some node types to other (i.e. only leafs can be moved and they can be moved only to the same type of parent nodes).
How can I prevent or cancel such drag and drop operation if the constraints fail? I would expect the onDragDrop event handler to return false or so in such cases but this method has void return type. I can check constraints in event handler but the drag and drop operation is already done.
What to do here?

NSOutlineView expand / collapse animation from code

i'm wondering how does one animate the expansion/collapse of an NSOutlineView's tree node from code ?
// this works ok but doesn't animate
NSTreeNode *node = [self.outlineView itemAtRow:self.outlineView.clickedRow];
if([self.outlineView isItemExpanded:node])
{
[self.outlineView.animator collapseItem:node];
}else{
[self.outlineView.animator expandItem:node];
}
an outline view naturally animates if you expand a node via the default-drawn arrow
so there IS a way...
My original code was OK, this just wasn't available under 10.7
Original text from Application Kit Release Notes for OS X v10.8 :
NSOutlineView
The following methods now support being animated via the -animator proxy: -expandItem:, -expandItem:expandChildren:, -collapseItem:, and -collapseItem:collapseChildren:. As an example, to animate the expansion of a particular item: [[outlineView animator] expandItem:item];
The problem is likely the node you are passing to collapseItem:. You need to pass the object your tree controller uses to represent the node rather than the actual node from your data model. If you are using NSTreeController, then you need to traverse the structure returned from -[NSTreeController arrangedObjects] to locate the node that represents your data model object.

NSOutlineView drag and drop: how to prevent parent being dropped on one of its children

I'm setting up drag and drop for my NSOutlineView and I want to prevent parents being dropped on one of their children. The parent and children are all of the Group entity. I understand I need to do this in the outlineView:validatedrop method. I think I need to create an NSFetchRequest with the NSPredictate to get all the children of the parent, but I don't know what the predicate should be.
Edit: it should be recursively, so it should also get the children of children, if any.
Any suggestions?
- (NSDragOperation)outlineView:(NSOutlineView *)ov validateDrop:
(id <NSDraggingInfo>)info proposedItem:(id)item proposedChildIndex:(NSInteger)childIndex;
In this above method, you can manage the drag item can or can't accept. Please, return NSDragOperationNone, means prevent drop.
SOLVED:
Instead of digging into the core data objects, I just look at the NSTreeNodes instead, following Apple's sample code.
Bindings take care of updating the underlying core data model. Once again, if it looks too complicated, it's probably not the correct approach.

Resources