Using Text Tables

The Cocoa text system supports text tables in OS X version 10.4 and later. The main classes involved are NSTextTable, which represents a table, NSTextTableBlock, which represents a block of text appearing as a cell in the table, and its superclass, NSTextBlock. This article explains how to add table support to your application.

Adding the Text Table Panel

NSTextView has built-in support for text tables, which provides the easiest way to add table support to your text view. This table support is in the form of the action method orderFrontTablePanel:. This method inserts a table into the text view and opens a modeless utility window that floats over the application windows. This table panel enables the user to manipulate attributes of a table while the cursor or selection is in the table. The table panel is shown in “Text table panel.”

Figure 1  Text table panel
Text table panel

The user can change other aspects of the table, such as cell size and contents, by direct manipulation with the cursor.

To make the text table panel available in your text view, use Interface Builder to add the orderFrontTablePanel: action method to your first responder and connect it to a menu item, as shown in “Connecting the action method.”

Figure 2  Connecting the action method
Connecting the action methodConnecting the action method

NSTextView defines similar action methods for opening list, link, and paragraph spacing panels.

Supporting Text Tables Programmatically

If you don’t want to use the text table panel, you can support tables programmatically by using NSTextTable and related classes directly. The basic class in this group is NSTextBlock, which represents a block of text laid out in a subregion of a text container. When working with tables, you use its subclass, NSTextTableBlock, which represents a text block that appears as a cell in a table. The table itself is represented by a separate class, NSTextTable. All of the NSTextTableBlock objects, representing the cells in the table, refer to the NSTextTable object, which controls their size and positioning.

Text blocks appear as attributes on paragraphs, as part of the paragraph style. An NSParagraphStyle object can have an array of text blocks representing the table cells that contain the paragraph. The paragraph style uses an array because table cells can be nested, and the text blocks are ordered in the array from outermost to innermost. For example, if block1 contains four paragraphs, and the middle two are also in inner block2, then the text blocks array for first and fourth paragraphs is (block1) which the array for the second and third paragraphs is (block1, block2).

You add text blocks to a paragraph style object using the NSMutableParagraphStyle method setTextBlocks:, and the NSParagraphStyle method textBlocks returns the array.

To implement a text table programmatically, use the following sequence of steps:

  1. Create an attributed string for the table.

  2. Create the table object, setting the number of columns.

  3. Create the text table block for the first cell of the row, referring to the table object.

  4. Set the attributes for the text block.

  5. Create a paragraph style object for the cell, setting the text block as an attribute (along with any other paragraph attributes, such as alignment).

  6. Create an attributed string for the cell, adding the paragraph style as an attribute. The cell string must end with a paragraph marker, such as a newline character.

  7. Append the cell string to the table string.

  8. Repeat steps 3–7 for each cell in the table.

The methods shown in “Table creation method” and “Table cell creation method” perform the steps from the preceding list. (All of the example methods in this article are defined in the NSDocument subclass of a document-based application, but they could as easily belong to another object, such as a text view.) “Table cell creation method” performs steps 3–6 for each cell in the table, using fat borders and contrasting colors for illustrative purposes.

Listing 1  Table creation method

- (NSMutableAttributedString *) tableAttributedString
{
    // tableString is an ivar declared in the header file as NSMutableAttributedString *tableString;
    tableString = [[NSMutableAttributedString alloc] initWithString:@"\n\n"];
    NSTextTable *table = [[NSTextTable alloc] init];
    [table setNumberOfColumns:2];
 
    [tableString appendAttributedString:[self tableCellAttributedStringWithString:@"Cell1\n"
        table:table
        backgroundColor:[NSColor greenColor]
        borderColor:[NSColor magentaColor]
        row:0
        column:0]];
 
    [tableString appendAttributedString:[self tableCellAttributedStringWithString:@"Cell2\n"
        table:table
        backgroundColor:[NSColor yellowColor]
        borderColor:[NSColor blueColor]
        row:0
        column:1]];
 
    [tableString appendAttributedString:[self tableCellAttributedStringWithString:@"Cell3\n"
        table:table
        backgroundColor:[NSColor lightGrayColor]
        borderColor:[NSColor redColor]
        row:1
        column:0]];
 
    [tableString appendAttributedString:[self tableCellAttributedStringWithString:@"Cell4\n"
        table:table
        backgroundColor:[NSColor cyanColor]
        borderColor:[NSColor orangeColor]
        row:1
        column:1]];
 
    [table release];
    return [tableString autorelease];
}

Listing 2  Table cell creation method

- (NSMutableAttributedString *) tableCellAttributedStringWithString:(NSString *)string
        table:(NSTextTable *)table
        backgroundColor:(NSColor *)backgroundColor
        borderColor:(NSColor *)borderColor
        row:(int)row
        column:(int)column
{
    NSTextTableBlock *block = [[NSTextTableBlock alloc]
        initWithTable:table
        startingRow:row
        rowSpan:1
        startingColumn:column
        columnSpan:1];
    [block setBackgroundColor:backgroundColor];
    [block setBorderColor:borderColor];
    [block setWidth:4.0 type:NSTextBlockAbsoluteValueType forLayer:NSTextBlockBorder];
    [block setWidth:6.0 type:NSTextBlockAbsoluteValueType forLayer:NSTextBlockPadding];
 
    NSMutableParagraphStyle *paragraphStyle =
        [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
    [paragraphStyle setTextBlocks:[NSArray arrayWithObjects:block, nil]];
    [block release];
 
    NSMutableAttributedString *cellString =
        [[NSMutableAttributedString alloc] initWithString:string];
    [cellString addAttribute:NSParagraphStyleAttributeName
        value:paragraphStyle
        range:NSMakeRange(0, [cellString length])];
    [paragraphStyle release];
 
    return [cellString autorelease];
}

The code in “Table creation method” and “Table cell creation method” produces the table shown in “Table output.”

Figure 3  Table output
Table output

To insert the table in text displayed in a text view, implement an action method such as the one shown in Listing 3. This method inserts the table string constructed in “Table creation method” in the text view, replacing the current selection (or at the insertion point, if there's no selection), and ensures that the proper notifications and delegate messages are sent.

Listing 3  Table insertion action method

- (void) insertMyTable:(id)sender
{
    NSRange charRange = [myTextView rangeForUserTextChange];
    NSTextStorage *myTextStorage = [myTextView textStorage];
 
    if ([myTextView isEditable] && charRange.location != NSNotFound)
        {
            NSMutableAttributedString *attrStringToInsert = [self tableAttributedString];
            if ([myTextView shouldChangeTextInRange:charRange replacementString:nil])
                {
                    [myTextStorage replaceCharactersInRange:charRange
                        withAttributedString:attrStringToInsert];
                    [myTextView setSelectedRange:NSMakeRange(charRange.location, 0)
                        affinity:NSSelectionAffinityUpstream stillSelecting:NO];
                    [myTextView didChangeText];
                }
        }
}

NSAttributedString has the following convenience methods you can use to determine the range in the string that is covered by a text block or table:

rangeOfTextBlock:atIndex:

rangeOfTextTable:atIndex:

If the given location is not in the specified block or table, these methods return a range of (NSNotFound, 0) .

The Text Table Model

The Cocoa text table model is derived primarily from the table model defined by HTML and CSS, in which tables are built up from rows of cells. For a description of the CSS table model, refer to the following URL:

http://www.w3.org/TR/CSS21/tables.html

This affinity provides another way to create tables in text. You can define a table in HTML and use that data to initialize an attributed string. The attributes of that string then define the table for rendering by the Cocoa text system. You can use the following NSAttributedString initialization methods for this purpose:

initWithHTML:documentAttributes:

initWithHTML:options:documentAttributes:

initWithHTML:baseURL:documentAttributes:

initWithData:options:documentAttributes:error:

Controlling Text Block Appearance

The position of a text block is determined by its text container or containing block. In the case of a text table block, which represents a cell in a table, size and position are controlled by the text table and the block’s relation to other blocks in the table. When you initialize an NSTextTableBlock object, you specify its row and column position as a cell within its table, and you also specify whether it spans multiple rows or columns. The NSTextTableBlock initialization method is:

initWithTable:startingRow:rowSpan:startingColumn:columnSpan:

“Table cell creation method” shows this method in use.

In addition, you can specify the value of a number of dimensions for each block, either as an absolute value or as a percentage of the containing block. These dimensions include the following:

The default value for each of these dimensions is 0 , indicating no padding, border, or margin, and the natural width and height. Natural width and height of a single text block extend to the width and height of its containing block (or text container); natural width and height of multiple blocks divide the space of their containing block evenly.

The following methods specify or return values associated with these dimensions:

setValue:type:forDimension:

valueForDimension:

valueTypeForDimension:

setWidth:type:forLayer:

setWidth:type:forLayer:edge:

widthForLayer:edge:

widthValueTypeForLayer:edge:

In these methods, the value type refers to absolute or percentage values. The dimension refers to minimum, maximum, and full width and height of the block. The layer refers to padding, border, and margin. These parameters are specified by constants described in NSTextBlock

NSTextBlock provides the following methods to specify and return the background and border color of the block:

backgroundColor

setBackgroundColor:

borderColorForEdge:

setBorderColor:forEdge:

setBorderColor:

By default the color values are nil , meaning no color. Note that a border with no color is invisible.

Table Layout Process

During text layout, the typesetter works with NSTextBlock to determine the layout rectangles for the text block. If the text block is an instance of NSTextTableBlock, it calls its containing NSTextTable instance to perform the calculations. The typesetter stores the results of these calculations in its layout manager. There are methods in NSTextBlock, NSTextTable, and NSLayoutManager specific to this layout process that you can use if you need to intervene in the process.

To begin the text block layout process, the typesetter proposes a large rectangle within which the text block should fit. For the outermost block, this is determined by the text container; for inner blocks, it is determined by the containing block. The block object then decides what area within the proposed rectangle it should actually occupy.

The text block actually determines two rectangles: first, the layout rectangle, within which the text in the block is to be laid out; second, the bounds rectangle, which contains additional space for padding, borders, border decoration, and margins. The text block calculates the layout rectangle immediately before the typesetter lays out the first glyph because it is needed for all subsequent layout of text in the block. The layout rectangle is often quite tall because, at this point, the height of the text to be laid out has not yet been determined. The text block calculates the bounds rectangle immediately after the last glyph in the block has been laid out, and it is based on the actual rectangle used for the text within the block. Under some circumstances, the bounds rectangle may be adjusted subsequently as additional blocks in the same table are laid out.

To find the layout and bounds rectangles, the typesetter calls the following NSTextBlock methods:

rectForLayoutAtPoint:inRect:textContainer:characterRange:

boundsRectForContentRect:inRect:textContainer:characterRange:

An NSTextTableBlock object, in turn, calls its NSTextTable object to perform these calculations, using the following methods:

rectForBlock:layoutAtPoint:inRect:textContainer:characterRange:

boundsRectForBlock:contentRect:inRect:textContainer:characterRange:

The typesetter stores the results of these methods in the layout manager using the following methods:

setLayoutRect:forTextBlock:glyphRange:

setBoundsRect:forTextBlock:glyphRange:

The typesetter uses the following NSLayoutManager methods when it needs to determine the space used by previously laid text blocks:

layoutRectForTextBlock:glyphRange:

layoutRectForTextBlock:atIndex:effectiveRange:

boundsRectForTextBlock:glyphRange:

boundsRectForTextBlock:atIndex:effectiveRange:

The preceding methods cause glyph generation but do not force layout. This avoids an infinite recursion when the methods are called during layout. For the same reason, the following variants of existing NSLayoutManager methods have an option to prevent them from causing layout:

lineFragmentRectForGlyphAtIndex:effectiveRange:withoutAdditionalLayout:

lineFragmentUsedRectForGlyphAtIndex:effectiveRange:withoutAdditionalLayout:

textContainerForGlyphAtIndex:effectiveRange:withoutAdditionalLayout:

If no rectangle has been set, the preceding methods return NSZeroRect .

At display time the text is drawn as usual, as described in ““The Layout Manager,”” except that the text block draws background and border decoration while glyph backgrounds are being drawn, using the following method:

drawBackgroundWithFrame:inView:characterRange:layoutManager:

If the text block is an NSTextTableBlock object, it calls its text table for this purpose, using the following NSTextTable method:

drawBackgroundForBlock:withFrame:inView:characterRange:layoutManager: