1
votes

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
This sounds strange. Could you describe the effect you want to achieve in more detail? Be thorough. There may be other ways to do this...Joshua Nozzi
I use bindings to fill the table. Currently it has two columns, and the right one consists of custom cell views that contain XY plots. I use Core Plot for drawing the plots. I want to be able to scroll through the table rows just as in a normal table view. However, I want to disconnect the horizontal scroller from the table view, and use it to control the plot range instead. I have two reasons for this: 1) The left column is much like a row header. 2) The plot data set is huge. I don't want to be drawing anything that is not visible, because it has a deleterious effect on the performance.mhavu
Do you think it would be better to add the horizontal NSScroller programmatically as an independent element? It would then be a sibling of the NSScrollView, and I wouldn't need to alter the behaviour of the scroll view and clip view.mhavu
Have you tried two table views side by side? The left one would not show scrollers. You would keep the scroll views synchronized vertically as described in Synchronizing Scroll Views.Ken Thomases
Two table views side by side would certainly be a way to implement row headers. However, the original problem (how to disconnect the horizontal scroller from the view, and make it control the drawing of the cell views instead) would still persist with the right table view.mhavu

1 Answers

0
votes

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