NSOutlineView jumps up to the top when content updates
Asked Answered
F

2

15

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.

Frighten answered 20/4, 2015 at 3:22 Comment(4)
Are you reloading the entire outline view or just the specific items that need to be updated?Gloze
@Gloze I'm not explicitly reloading the outline view. The tree controller reloads it for me.Frighten
what object gets this message?. The relevant FS node representation or the controller?. Its a little unclear to me as to why you are firing the KVO here. Also how do you refresh the children. Can you show that. I built a quick tree controller/nsoutlineview and as long as i don't fire the KVO i don't get a view reset on update.Garnishment
@WarrenBurton 1. The code I posted is in the directory node. 2. I'm firing the KVO to tell the tree controller (which is observing every node) that the children have updated. And if you really need to see my code, it's on github at github.com/vindo-app/vindo/blob/fsevents/Vindo/DirectoryItem.mFrighten
J
10

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

Jodijodie answered 26/4, 2015 at 17:45 Comment(1)
I've decided to try the fourth option. Will probably award the bounty.Frighten
T
0

I can advise you to keep current scroll view offset and restore it after your KVO notification been sent and the outline view is updated.

- (void)updateOutlineView:(NSOutlineView *)outlineView
{
  // first save offset
  NSScrollView *scrollView = [outlineView enclosingScrollView];
  NSClipView *clipView = [scrollView contentView];
  NSPoint offset = clipView.bounds.origin;
  // send KVO notification of the 'children' keypath
  // ...
  // restore offset
  [clipView scrollPoint:offset];
}

Take a look that the scroll point is absolute value. You can calculate destination point according to updated outline view height.

//... before the notification sent
CGFloat height = [[[[scrollView documentView] frame] size] height];
CGFloat yValue = offset.y / height;
//... after the outline view updated
CGFloat newHeight = [[[[scrollView documentView] frame] size] height];
offset.y = newHeight * yValue;
[clipView scrollPoint:offset];
Typeface answered 24/4, 2015 at 6:33 Comment(1)
That won't work in my situation. To clarify, I've added the relevant code to my question.Frighten

© 2022 - 2024 — McMap. All rights reserved.