Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Displaying row index in an NSTableView bound to NSArrayController

I have an NSTableView which is bound to an NSArrayController. I would like to have one of the table columns showing the index of the table row. This is easy enough to do when you implement NSTableDataSource yourself but I can't figure it out with a bound table view. I guess I'm looking here for something like the @count key path which gives me the count of arrangedObjects (that is @index) but this is obviously missing.

Two clarifications:

  1. The index that is shown in each row is the index of that row and not related at all to the way the data is actually arranged in the model or array controller. For example, if the whole data is 10000 items then the indexes should go from 1 to 10000, if the user enters a search term and the table is showing only 4 of the items then the numbers should go from 1 to 4, even if the items shown are actually from all over the original array.
  2. I need this because I was asked to do this by the client :-). The client needs a way to be able to estimate the number of rows before or after a certain row. Hi might, for example, want to know how many rows were added.
like image 697
Eyal Redler Avatar asked Jan 02 '10 12:01

Eyal Redler


3 Answers

As I understand it, you could choose not to bind that table column, and use a datasource instead. I recall NSTableView supports this sort of "dual mode" operation, but can't find any docs to confirm it.

like image 111
Mike Abdullah Avatar answered Nov 01 '22 23:11

Mike Abdullah


I recently implemented this using an NSRuler subclass that draws the line numbers next to each line in the TableView. I based the code on something similar that I found here.

You can add this to your tableview using:

NSScrollView *scrollView = [tableView enclosingScrollView];
TableLineNumberRulerView *lineNumberView = [[TableLineNumberRulerView alloc] initWithTableView:tableView
                                                                           usingArrayController:arrayController];

[scrollView setVerticalRulerView:lineNumberView];
[scrollView setHasVerticalRuler:YES];
[scrollView setRulersVisible:YES];

Here's the interface file:

//
//  TableLineNumberRulerView
//  Line View Test
//
//  Created by Ben Golding, Object Craft Pty Ltd on 7 May 2014.
//  Based on code by Paul Kim on 9/28/08.


#import <Cocoa/Cocoa.h>

@interface TableLineNumberRulerView : NSRulerView<NSCoding>

@property (strong) NSArrayController *arrayController;

@property (strong) NSFont       *font;
@property (strong) NSColor  *textColor;
@property (strong) NSColor  *alternateTextColor;
@property (strong) NSColor  *backgroundColor;
@property (strong) NSDictionary *textAttributes;
@property (assign) NSUInteger   rowCount;

- (id)initWithTableView:(NSTableView *)tableView  usingArrayController:(NSArrayController *)arrayController;

@end

Here's the implementation:

//
//  TableLineNumberRulerView.m
//  Line View Test
//
//  Created by Ben Golding, Object Craft Pty Ltd on 7 May 2014.
//  Based on code by Paul Kim on 9/28/08.

#import "TableLineNumberRulerView.h"

#define DEFAULT_THICKNESS   22.0
#define RULER_MARGIN        5.0

@implementation TableLineNumberRulerView

@synthesize font;
@synthesize textColor;
@synthesize alternateTextColor;
@synthesize backgroundColor;
@synthesize textAttributes;
@synthesize rowCount;


- (id)initWithTableView:(NSTableView *)tableView usingArrayController:(NSArrayController *)arrayController
{
    NSScrollView *scrollView = [tableView enclosingScrollView];

    if ((self = [super initWithScrollView:scrollView orientation:NSVerticalRuler]) == nil)
        return nil;

    [self setClientView:tableView];

    self.arrayController = arrayController;
    [arrayController addObserver:self forKeyPath:@"arrangedObjects" options:NSKeyValueObservingOptionNew context:nil];

    self.font = [NSFont labelFontOfSize:[NSFont systemFontSizeForControlSize:NSMiniControlSize]];
    self.textColor = [NSColor colorWithCalibratedWhite:0.42 alpha:1.0];
    self.alternateTextColor = [NSColor whiteColor];
    self.textAttributes = @{
        NSFontAttributeName: [self font],
        NSForegroundColorAttributeName: [self textColor]
    };

    self.rowCount = [[arrayController arrangedObjects] count];

    return self;
}

- (void)awakeFromNib
{
    [self setClientView:[[self scrollView] documentView]];      // this will be an NSTableView instance
}

- (void)finalize
{
    [self.arrayController removeObserver:self forKeyPath:@"arrangedObjects"];
}

#pragma mark -
#pragma mark Key-Value observing of changes to array controller

/*
 * This picks up changes to the arrayController's arrangedObjects using KVO.
 * We check the size of the old and new rowCounts and compare them to see if the number
 * digits has changed, and if so, we adjust the ruler width.
 */

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    if ([keyPath isEqualToString:@"arrangedObjects"]) {
        NSUInteger newRowCount = [[self.arrayController arrangedObjects] count];

        if ((int)log10(self.rowCount) != (int)log10(newRowCount))
            [self setRuleThickness:[self requiredThickness]];
        self.rowCount = newRowCount;
        // we need to redisplay because line numbers may change or disappear in view
        [self setNeedsDisplay:YES];
    }
}


- (CGFloat)requiredThickness
{
    NSUInteger      lineCount = [[self.arrayController arrangedObjects] count],
                    digits = (unsigned)log10((lineCount < 1) ? 1: lineCount) + 1;
    NSMutableString *sampleString = [NSMutableString string];
    NSSize          stringSize;

    for (NSUInteger i = 0; i < digits; i++) {
        // Use "8" since it is one of the fatter numbers. Anything but "1"
        // will probably be ok here. I could be pedantic and actually find the fattest
        // number for the current font but nah.
        [sampleString appendString:@"8"];
    }

    stringSize = [sampleString sizeWithAttributes:[self textAttributes]];

    // Round up the value. There is a bug on 10.4 where the display gets all wonky when scrolling if you don't
    // return an integral value here.
    return ceil(MAX(DEFAULT_THICKNESS, stringSize.width + RULER_MARGIN * 2));
}

- (void)drawHashMarksAndLabelsInRect:(NSRect)aRect
{
    NSTableView *tableView = (NSTableView *)[self clientView];
    NSRect bounds = [self bounds];
    NSRect visibleRect = [[tableView enclosingScrollView] documentVisibleRect];
    NSRange visibleRowRange = [tableView rowsInRect:visibleRect];
    CGFloat yinset = NSHeight([[tableView headerView] bounds]);

    if (backgroundColor != nil) {
        [backgroundColor set];
        NSRectFill(bounds);

        [[NSColor colorWithCalibratedWhite:0.58 alpha:1.0] set];
        [NSBezierPath strokeLineFromPoint:NSMakePoint(NSMaxX(bounds) - 0/5, NSMinY(bounds))
                                  toPoint:NSMakePoint(NSMaxX(bounds) - 0.5, NSMaxY(bounds))];
    }

//    NSLog(@"drawHashMarksAndLabelsInRect: bounds %@, ruleThickness %lf", NSStringFromRect(bounds), [self ruleThickness]);

    for (NSUInteger row = visibleRowRange.location; NSLocationInRange(row, visibleRowRange); row++) {
        // Line numbers are internally stored starting at 0
        NSString *labelText = [NSString stringWithFormat:@"%lu", row + 1];
        NSSize stringSize = [labelText sizeWithAttributes:self.textAttributes];
        NSRect rowRect = [tableView rectOfRow:row];
        CGFloat ypos = yinset + NSMinY(rowRect) - NSMinY(visibleRect);

        [labelText drawInRect:NSMakeRect(NSWidth(bounds) - stringSize.width - RULER_MARGIN,
                                         ypos + (NSHeight(rowRect) - stringSize.height) / 2.0,
                                         NSWidth(bounds) - RULER_MARGIN * 2.0, NSHeight(rowRect))
               withAttributes:self.textAttributes];
    }
}

#pragma mark -
#pragma mark NSCoding methods

#define FONT_CODING_KEY         @"font"
#define TEXT_COLOR_CODING_KEY       @"textColor"
#define ALT_TEXT_COLOR_CODING_KEY   @"alternateTextColor"
#define BACKGROUND_COLOR_CODING_KEY @"backgroundColor"

- (id)initWithCoder:(NSCoder *)decoder
{
    if ((self = [super initWithCoder:decoder]) != nil) {
        if ([decoder allowsKeyedCoding]) {
            font = [decoder decodeObjectForKey:FONT_CODING_KEY];
            textColor = [decoder decodeObjectForKey:TEXT_COLOR_CODING_KEY];
            alternateTextColor = [decoder decodeObjectForKey:ALT_TEXT_COLOR_CODING_KEY];
            backgroundColor = [decoder decodeObjectForKey:BACKGROUND_COLOR_CODING_KEY];
        } else {
            font = [decoder decodeObject];
            textColor = [decoder decodeObject];
            alternateTextColor = [decoder decodeObject];
            backgroundColor = [decoder decodeObject];
        }
    }
    return self;
}

- (void)encodeWithCoder:(NSCoder *)encoder
{
    [super encodeWithCoder:encoder];

    if ([encoder allowsKeyedCoding]) {
        [encoder encodeObject:font forKey:FONT_CODING_KEY];
        [encoder encodeObject:textColor forKey:TEXT_COLOR_CODING_KEY];
        [encoder encodeObject:alternateTextColor forKey:ALT_TEXT_COLOR_CODING_KEY];
        [encoder encodeObject:backgroundColor forKey:BACKGROUND_COLOR_CODING_KEY];
    } else {
        [encoder encodeObject:font];
        [encoder encodeObject:textColor];
        [encoder encodeObject:alternateTextColor];
        [encoder encodeObject:backgroundColor];
    }
}

@end
like image 23
Ben Golding Avatar answered Nov 01 '22 21:11

Ben Golding


Assuming that you aim to replicate the behavior of iTunes, your column only needs to display 1 to (number of visible rows). It does not actually need to relate to your model at all.

So, give your controller a dynamically-generated array property, and make it depend on the array of actual model objects.

For this property, implement the array accessor methods, with the indexed getter simply computing idx + 1, boxing it up, and returning that object. You may also need to implement the whole-array getter to satisfy KVC.

Make sure the column is set as non-editable, and the binding set to not conditionally set enabled. Otherwise, you'll get an exception (for not having setters for this property) when the user tries to enter a new index for a row.

One note of caution: This may cause problems, as NSTableView doesn't expect its columns to be bound to discrete arrays; it expects all of its columns to be bound to different properties of the model objects in a single array. You may need to bind the content of the table view itself to your array of model objects, in addition to binding the content bindings of the columns, and I've had trouble with that before.

like image 45
Peter Hosey Avatar answered Nov 01 '22 23:11

Peter Hosey