Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

NSOutlineView jumps up to the top when content updates

I have an NSOutlineView that is displaying a directory hierarchy. It's bound to an NSTreeController, which is bound to my class that manages file system nodes. When a filesystem event occurs, I fire a KVO notification on the children keypath, which causes the outline view to update. But when it updates, it suddenly scrolls up to the very top. I want the scrolling position to stay the same. Any ideas?

Here's the code that runs when an FS event occurs:

- (void)URLWatcher:(CDEvents *)URLWatcher eventOccurred:(CDEvent *)event {
    [self willChangeValueForKey:@"children"];
    children = nil; // this will refreshed next time children is called
    [self didChangeValueForKey:@"children"];
}

This is in the model, so I can't to access the view.

like image 858
tbodt Avatar asked Apr 20 '15 03:04

tbodt


1 Answers

I haven't tested or attempted the following, but thought I'd give it a shot anyway.

First, managing anything complicated with NSTableView or NSOutlineView with an NS*Controller is painful and sacrifices precise control in exchange for simplicity. If you find yourself fighting behavior in this situation, consider implementing the datasource and delegate protocols (NSTableViewDataSource, NSTableViewDelegate or NSOutlineViewDataSource, NSOutlineViewDelegate) in your own custom controller.

Second, Warren Burton's comment regarding firing KVO notifications is pertinent, since you should be telling the responsible controller (your NSTreeController) about the changes, since it's the one controlling (and observing) that collection anyway. More to the point, you should be using NSTreeController's add/insert/remove methods directly. The way you're doing it now (whacking the entire structure each time you nullify then reset it later) will cause the entire tree to reload. Since the controller is observing that collection, it's telling the outline view to refresh itself, possibly allowing it to see an empty outline first, then a further-expanded version of the outline later, which will lose your user's expansion states, etc. Modifying the model via the tree controller will allow for smarter, more efficient view updates.

Third, you could consider taking advantage of my second point above by subclassing NSTreeController and overriding the add/insert/remove methods to do the following:

  1. Ask the outline view for its -visibleRect.
  2. Call super to initiate the change.
  3. Tell the outline view to -scrollRectToVisible:.

You may have to delay the call in step 3 by scheduling it on the main thread (so it happens after the current trip through the run loop). Or, alternately, replace step 3 with storing the visible rect somewhere and implementing the NSOutlineViewDelegate -outlineView:didAdd/RemoveRowView:forRow: methods to check for this flag THEN call -scrollRectToVisible: from there if the rect is non-zero (remember to reset it to NSZeroRect so it doesn't attempt to adjust scroll every time an outline row is added or removed).

Clunky, but a reasonable(?) path that allows you to keep the NSTreeController.

Fourth, alternatively (and the way I'd go), you could drop NSTreeController entirely and implement NSOutlineViewDataSource (and NSOutlineViewDelegate) protocols in your own controller class, and let that controller directly handle adding to or removing from your tree structure. Then it becomes cleaner since you don't have to worry about KVO timings. On any adding of nodes, you can note the visible rect, update the outline view, then adjust scroll all within the same method and trip through the run loop.

I hope this helps and wasn't too rambling. :-)

like image 180
Joshua Nozzi Avatar answered Oct 21 '22 11:10

Joshua Nozzi