How to control drawing of NSTableView in NSScrollView?

627 views Asked by At

How can I get NSTableView to always show the same columns regardless of the horizontal scroller position? In the rightmost visible column I have custom cell views. I want the horizontal scroller to control what is being drawn in these custom views. The vertical scrolling should work normally.

I have tried several approaches without much success. For example, I can control the knob proportion of the horizontal scroller by making the table view wider, or by making the scroll view think its document view is actually wider than it is. One way is subclassing NSClipView and overriding -documentRect as follows:

-(NSRect)documentRect {
    NSRect rect = [super documentRect];
    rect.size.width += [[NSApp delegate] hiddenRangeWidth];
    return rect;
}

However, while the scroller knob looks as it should and I can drag it right without moving the table view, when I start scrolling in another direction, the knob returns to the left edge. I also have the problem that I can't get the horizontal scroller to appear automatically. This happens with the original classes as well, not just with my custom clip view. Could these problems be related?

I have also tried replacing the document view with a custom view that acts as a proxy between the clip view and the table view. Its -drawRect: calls the table view's -drawRect:. However, nothing is drawn. I guess this is because the table view now has no superview. If the table view were added to this proxy view as a subview, it would move with it. How would I make it stationary in horizontal axis?

So, to reiterate:

  1. What is the best way to make a table view scrollable, while always showing the same columns regardless of the horizontal scroller position?
  2. What is the best way to get the scroller position and knob proportion? Should I add an observer for the NSViewBoundsDidChangeNotification from NSClipView?
1

There are 1 answers

0
mhavu On

I finally managed to solve the problem by letting the scroll view and table view behave normally, and adding an NSScroller. In order to make hiding the scroller easier, I decided to use Auto Layout and add it in Interface Builder. (The Object library doesn't include a scroller, but you can add a custom view and set its class to NSScroller.) I set the height of the scroller as a constraint, and bound the scroller and the constraint to outlets in code:

@property (nonatomic, retain) IBOutlet NSScroller *scroller;
@property (nonatomic, unsafe_unretained) IBOutlet NSLayoutConstraint *scrollerHeightConstraint;

Now I can make the scroller visible or hide it when necessary:

if (_zoomedIn) {
    _scrollerHeightConstraint.constant = [NSScroller scrollerWidthForControlSize:NSRegularControlSize scrollerStyle:NSScrollerStyleOverlay];
    [_scroller setKnobProportion:(_visibleRange / _maxVisibleRange)];
    [_scroller setDoubleValue:_visibleRangePosition];
    [_scroller setEnabled:YES];
} else {
    _scrollerHeightConstraint.constant = 0.0;
}

Here the properties visibleRange, maxVisibleRange and visibleRangePosition are the length of the visible range (represented by the scroller knob), the total range (represented by the scroller slot), and the start of the visible range (the knob position), respectively. These can be read by binding the scroller's sent action to the following method in Interface Builder:

- (IBAction)scrollAction:(id)sender {
    switch (self.scroller.hitPart) {
        case NSScrollerNoPart:
            break;
        case NSScrollerDecrementPage:
            _visibleRangePosition = MAX(_visibleRangePosition - _visibleRange / _maxVisibleRange, 0.0);
            self.scroller.doubleValue = _visibleRangePosition;
            break;
        case NSScrollerIncrementPage:
            _visibleRangePosition = MIN(_visibleRangePosition + _visibleRange / _maxVisibleRange, 1.0);
            self.scroller.doubleValue = _visibleRangePosition;
            break;
        case NSScrollerKnob:
        case NSScrollerKnobSlot:
            _visibleRangePosition = self.scroller.doubleValue;
            break;
        default:
            NSLog(@"unsupported scroller part code %lu", (unsigned long)self.scroller.hitPart);
    }
    // Make the custom cell views draw themselves here.
}

In order to get the scrolling work with gestures, we need to implement -scrollWheel: in the custom cell view class:

- (void)scrollWheel:(NSEvent *)event {
    if (event.deltaX != 0.0) {
        NSScroller *scroller = appDelegate.scroller;
        if (scroller.isEnabled) {
            double delta = event.deltaX / (NSWidth(scroller.bounds) * (1.0 - scroller.knobProportion));
            scroller.doubleValue = MIN(MAX(scroller.doubleValue - delta, 0.0), 1.0);
        }
    }
    if (event.deltaY != 0.0) {
        [self.nextResponder scrollWheel:event];
    }
}

I thought I could've just passed the event to the scroller, but apparently it doesn't handle the event. The above code doesn't seem to handle bounce back, and momentum scrolling doesn't always work. Sometimes the knob just halts in the middle of the motion. I believe this has to do with the scroller style being NSScrollerStyleLegacy by default. Setting it to NSScrollerStyleOverlay would require changes to the layout, so I haven't tried it yet.

Another problem is that the scrollers don't blend into each other in the corner like they do in a scroll view (see below). Maybe NSScrollerStyleOverlay would fix this, too.

corner between the scrollers