Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

NSTableView: How to draw a custom separators before and after a selected row

This is my first question here, and I will try to do it as clear as possible.

I want to draw a custom gradient on a selected row in a view-based NSTableView, while adding a subtle raised effect. For this, I need to use a darker color for the grid-lines that are before and after the selected row (see here for an example). I have overrode drawSeparatorInRect: method in NSTableRowView to draw the custom separator line for the selected row (using isSelected method as a flag), but I cannot do the same for the above/below one (since I draw the line at the bottom/top).

I have tried several ways to tell the closest row that it should draw a darker separator line with no success since the display step does not follow the same order (I checked it with NSLogs in the drawSeparatorInRect:, and it seems that when you scroll a little this order changes). So, sometimes (mostly after scrolling) the row doesn't know that it should use a darker color since it draws itself before the selected one (I think at this point, the selected-row is not aware yet that it's selected, otherwise I don't understand what is going on).

Some of the things I tried:

  1. In the drawSeparatorInRect: method of the selected row, I have tried to access to the siblings views ([superview subviews]) and force the previous/next one to draw itself again.
  2. From the NSTableView subclass, modify directly the closest row when the selectedIndexes change.
  3. Drawing the line outside the selected row from within its drawSeparatorInRect: method as showing here.

Note that I did this having: a row view asking if the previous/next one is selected, a closestRowIsSelected flag or externally calling a method to "force" the dark color.

What I have now is that the selected row draws both top and bottom borders, so one of them is placed together to the previous/next row line... It's subtle but it still there.

Any help will be well received.

Thank you in advance.

! I didn't post any code since the problem is not there (it just calls [NSBezierPath fillRect:rect] with red color), I think... so I have nothing to show.

like image 583
Francisco Adasme Avatar asked Dec 09 '11 16:12

Francisco Adasme


1 Answers

I've also tried this and noticed that drawSeparatorInRect: can really only draw its bottom separator line as the position of the top separator line (which is the same as the bottom separator line of the preceding row) is one pixel outside (above) the clipRect of the row.

However, I got it working by subclassing NSTableRowView and having drawSeparatorInRect: as follows:

- (void)drawSeparatorInRect:(NSRect)dirtyRect
{
    // Define our drawing colors
    NSColor *normalColor = [NSColor colorWithCalibratedWhite:0.76 alpha:1.0]; // Default separator color
    NSColor *selectedTopColor = [NSColor colorWithCalibratedWhite:0.60 alpha:1.0]; // Color of the top separator line of selected row
    NSColor *selectedBottomColor = [NSColor colorWithCalibratedWhite:0.60 alpha:1.0]; // Color of the bottom separator line of selected row

    // Define coordinates of separator line
    NSRect drawingRect = [self frame]; // Ignore dirtyRect
    drawingRect.origin.y = drawingRect.size.height - 1.0;
    drawingRect.size.height = 1.0; // Height of the separator line we're going to draw at the bottom of the row

    // Get the table view and info on row index numbers
    NSTableView *tableView = (NSTableView*)[self superview]; // The table view the row is part of
    NSInteger selectedRowNumber = [tableView selectedRow];
    NSInteger ownRowNumber = [tableView rowForView:self];

    // Set the color of the separator line
    [normalColor set]; // Default
    if (([self isSelected]) && ((selectedRowNumber + 1) < [tableView numberOfRows])) [selectedBottomColor set]; // If the row is selected, use selectedBottomColor
    if ((![self isSelected]) && (selectedRowNumber > 0) && (ownRowNumber == (selectedRowNumber-1))) [selectedTopColor set]; // If the row is followed by the selected row, draw its bottom separator line in selectedTopColor

    // Draw separator line
    NSRectFill (drawingRect);

    // If the row is selected, tell the preceding row to redraw its bottom separator line (which is also the top line of the selected row)
    if (([self isSelected]) && (selectedRowNumber > 0)) [tableView setNeedsDisplayInRect:[tableView rectOfRow:selectedRowNumber-1]];
}

This method will (only) draw its own bottom separator line. If it is the selected row, it will draw the line not with the default color, but highlighted, it will then also tell the preceding row to redraw its separator line, i.e. the same as the top separator line of the selected row.

In order to get this working, the row above the selected row needs to redraw its bottom separator line once the selection moves. I achieved this by having this method in the NSTableView delegate:

// Tell the row above the row which is going to loose the selection to redraw its bottom separator line
- (BOOL)selectionShouldChangeInTableView:(NSTableView *)aTableView
{
    NSInteger selectedRowNumber = [aTableView selectedRow];
    if (selectedRowNumber > 0) {
    [aTableView setNeedsDisplayInRect:[aTableView rectOfRow:selectedRowNumber-1]];
    }
    return YES;
}

This delegate method tells the row above the still selected row to redraw its separator line. It is called immediately before the selection changes.

like image 136
Tim Avatar answered Sep 28 '22 15:09

Tim