-1

How do I generate indentation marker for NSOutlineView?

NSOutlineview markers

I am not sure if this is an inbuilt functionality because it appears in other apps like Instruments


Update

I tried solving the problem by iterating all the children of the item that the row represents and show the marker on all children rows based on indentation level, but I faced a few problems

  1. How to handle the case where the item has thousands of children. One simply cannot draw marker to every row as NSOutlineView would draw rows as they are displayed

  2. When I scroll the NSOutlineView, the mouse moves out of the specified row but mouseExited is not being called. Thus the user has to manually move the mouse to reload the highlighting.

I had solved this problem but my solution looks hacky hence wanted to know if there is a better solution. And hence the question

Kaunteya
  • 3,107
  • 1
  • 35
  • 66
  • 1
    This is not a built-in feature of `NSOutlineView`, you'll have to bake it yourself. – James Bucanek Jun 16 '20 at 16:58
  • Frist you ask "how to do it" and then update the question do "ah yeah, sure, I did that before, but I need a implementation that handles 100k rows". If so, do some work, show your implementation details, show Instrument profile highlighting performance issue. – catlan Jun 20 '20 at 17:46
  • All children of the row are recursively iterated, `[_outlineView rowViewAtRow:[_outlineView rowForItem:item] makeIfNecessary:NO]` returns nil if it isn't visible. **makeIfNecessary:NO**. Sure there is place for optimizing, but ask a new question. – catlan Jun 20 '20 at 17:47
  • You ask about *mouseEnter:/mouseExit:*, but don't mentions what area you are tracking! If you had tried the implementation from the answer you would notice: A *mouseEnter:* is called on the row where the curser ends up updating the state. – catlan Jun 20 '20 at 17:48

1 Answers1

3

First to receive mouseEntered: and mouseExited: events you need to setup a tracking rect using NSTrackingArea.

I would start with a subclass of NSTableRowView thats overwrites setFrame: making sure the tracking rect gets updated when the view is resize:

@interface TableRowView : NSTableRowView {
    NSBox *_box;
    NSTrackingArea *_trackingArea;
}
@property (weak) id owner;
@property (copy) NSDictionary<id, id> *userInfo;
@property (nonatomic) CGFloat indentation;
@property (nonatomic) BOOL indentationMarkerHidden;
@end

@implementation TableRowView

- (void)setFrame:(NSRect)frame
{
    [super setFrame:frame];
    if (_trackingArea) {
        [self removeTrackingArea:_trackingArea];
    }
    _trackingArea = [[NSTrackingArea alloc] initWithRect:[self bounds] options:NSTrackingMouseEnteredAndExited|NSTrackingActiveInKeyWindow owner:[self owner] userInfo:[self userInfo]];
    [self addTrackingArea:_trackingArea];
}

@end

To use the NSTableRowView subclass, implement the NSOutlineViewDelegate messages like this:

 - (NSTableRowView *)outlineView:(NSOutlineView *)outlineView rowViewForItem:(id)item
{
    TableRowView *view = [[TableRowView alloc] init];
    view.owner = self;
    view.userInfo = item;
    return view;
}

With this in place you're ready to receive mouseEntered: and mouseExited: events. Use NSOutlineView levelForItem: together with indentationPerLevel to calculate the position of the marker NSBox.:

- (void)mouseEntered:(NSEvent *)event
{
    id item = [event userData];
    CGFloat indentation = [_outlineView levelForItem:item] * [_outlineView indentationPerLevel];
    [self setIndentationMarker:indentation hidden:NO item:item];
}

- (void)mouseExited:(NSEvent *)event
{
    id item = [event userData];
    [self setIndentationMarker:0.0 hidden:YES item:item];
}

Now you get the NSTableRowView subclass by rowViewAtRow:makeIfNecessary: and recursively do the same for all children in your data:

- (void)setIndentationMarker:(CGFloat)indentation hidden:(BOOL)hidden item:(NSDictionary *)item
{
    TableRowView *view = [_outlineView rowViewAtRow:[_outlineView rowForItem:item] makeIfNecessary:NO];
    view.indentationMarkerHidden = hidden;
    view.indentation = indentation;
    for (NSMutableDictionary *child in [item objectForKey:@"children"]) {
        [self setIndentationMarker:indentation hidden:hidden item:child];
    }
}

Now layout the NSBox the NSTableRowView subclass:

@implementation TableRowView

- (instancetype)init
{
    self = [super init];
    if (self) {
        _indentationMarkerHidden = YES;
        
        _box = [[NSBox alloc] init];
        _box.boxType = NSBoxCustom;
        _box.borderWidth = 0.0;
        _box.fillColor = [NSColor tertiaryLabelColor];
        _box.hidden = _indentationMarkerHidden;
        
        [self addSubview:_box];
    }
    return self;
}

- (void)layout
{
    [super layout];
    
    NSRect rect = [self bounds];
    rect.origin.x = _indentation + 7;
    rect.size.width = 10;
    _box.frame = rect;
}

- (void)setIndentation:(CGFloat)indentation
{
    _indentation = indentation;
    [self setNeedsLayout:YES];
}

- (void)setIndentationMarkerHidden:(BOOL)indentationMarkerHidden
{
    if (_indentationMarkerHidden != indentationMarkerHidden) {
        _indentationMarkerHidden = indentationMarkerHidden;
        _box.hidden = indentationMarkerHidden;
    }
}

@end

This enough to make a basic version like here:

NSOutlineView Indentation Marker Demo

catlan
  • 25,100
  • 8
  • 67
  • 78