4.2 - Document Editor#

We now have the skeleton of Money.app. I want it be a spreadsheet-like application to track the expense. Spreadsheet-like applications need a table. NSTableView is a good start. NSTableView is a more complicated user interface than NSButton, NSTextField, etc. So do NSBrowser, NSOutlineView, NSMatrix, etc. GNUstep does a great job to make it very easy to use. I’ll try to explain it step by step.

Here is a related article: Getting Started With NSTableView

If you are interested in text editors, Ink.app is a good example.

Creating the table view#

Use Gorm to open Document.gorm. Add a table view into the window. Try to resize it until it fit the whole window.

Figure 4-38. Add table into window

Check the “Horizontal” scroller.

Figure 4-39. Attributes of NSTableView

Look at the Size panel in the inspector of NSTableView. Click the line in the Autosizing box to make them springs.

Figure 4-40. Set resize attribute of table view

The box represent the NSTableView. The straight line or spring represent the distance relationship. Line outside the box is the distance between NSTableView and its superview. It is the window in this case. The line inside the box is the size of the NSTableView. Straight line means the distance is fixed, the spring means it is resizable. In this case, when window is resized, since the distance between NSTableView and window is fixed, NSTableView will be resized according to the window. That’s the behavior I want.

You can change the title of the column by double-click on it. But it is not necessary for now. You will find that it is still hard to control the interface of NSTableView from Gorm. I’ll do that programmingly. Therefore, I need a outlet connected to this NSTableView from NSOwner.

Add an outlet, tableView, in the class Document.

Figure 4-41. Add outlet for table view

Set NSOwner as the data source and delegate of the NSTableView. I’ll explain the data source later.

Figure 4-42. Connect data source and delegate of table view

Connect the outlet tableView to NSTableView.

Figure 4-43. Connect outlet to table view

Save the Gorm file and quit Gorm.

Basic data source#

Add the new outlet in Document.h.

Document.h:

#import <AppKit/AppKit.h>
#import <AppKit/NSDocument.h>

@interface Document : NSDocument
{
    id tableView;
}
@end

The way NSTableView works is that when it needs to display, it will ask its data source to provide the data it needs. So we need to implement two methods to provide NSTableView the data it need:

Document.m:

- (int) numberOfRowsInTableView: (NSTableView*) view {
    return 5;
}

- (id)              tableView: (NSTableView*) view
    objectValueForTableColumn: (NSTableColumn*) column
                          row: (int) row
{
    return [NSString stringWithFormat: @"column %@ row %d", 
        [column identifier], row]; 
}

The method -numberOfRowsInTableView: returns how many rows NSTableView should display – in this case, we’ll display 5 rows. The method -tableView:objectValueForTableColumn:row: returns the value shown in a certain cell in the table.

Now, this application is ready to run, even though it does nothing but display 5 rows of “column 0 row 0”. This is merely a demonstration of how NSTableView works. I provide the number of rows, and the object in a given column and row. As long as these two kinds of data are provided, the NSTableView can display anything, even a image in the cell. I’ll talk about more details about data sources later on.

Here is the source code: Table-1-src.tar.gz

Configuring the table view#

Let’s work on the interface first. NSTableView is a collection of NSTableColumns. I want three columns for the date, item and amount. By default, there are two columns. Therefore, I need to add a NSTableColumn into our NSTableView.

Document.m:

- (void) windowControllerDidLoadNib: (NSWindowController*) controller
{
    NSTableColumn *column;
    NSArray *columns = [tableView tableColumns];

    column = [columns objectAtIndex: 0];
    [column setWidth: 100];
    [column setEditable: NO];
    [column setResizable: YES];
    [column setIdentifier: @"date"];
    [[column headerCell] setStringValue: @"Date"];

    column = [columns objectAtIndex: 1];
    [column setWidth: 100];
    [column setEditable: NO];
    [column setResizable: YES];
    [column setIdentifier: @"item"];
    [[column headerCell] setStringValue: @"Item"];

    column = [[NSTableColumn alloc] initWithIdentifier: @"amount"];
    [column setWidth: 100];
    [column setEditable: NO];
    [column setResizable: YES];
    [[column headerCell] setStringValue: @"Amount"];
    [tableView addTableColumn: column];
    RELEASE(column);

    [tableView sizeLastColumnToFit];
    [tableView setAutoresizesAllColumnsToFit: YES];
}

We adjust the interface of our NSTableView in the method -windowControllerDidLoadNib:, which guarantees that the Gorm file is loaded. This is similar to -awakeFromNib. First, we get the existing columns and change their properties. Second, we create a new NSTableColumn and add it into our NSTableView. Finally, we adjust the layout of our NSTableView. This way, we can programatically adjust our NSTableView without using Gorm to adjust it. Run this application again, and you will see the new column.

Note

At the time this tutorial was originnally written, Gorm did not allow you to configure NSTableView.

An important property of NSTableColumn is its identifier. Each NSTableColumn has an unique identifier to distinguish them. The identifier can be any object, but it’s usually an NSString. The identifier does not have to be the same as the header of the column, but should being the same for easier management. So we access the NSTableColumn via its identifier. Many GNUstep objects have identifiers.

Functional data source#

Now that we’ve finished the interface, we can set up a data source. The data source is an object which provides the data for NSTableView. Therefore, the data source is the model in the MVC (Model-View-Controller) paradigm. Depending on the behavior of NSTableView, we need to implement the proper methods in the data source of NSTableView. We already implemented those methods, but they give the useless “column 0 row 0” messages, instead of useful data from our “Money Document”.

The data for NSTableView can be considered as an NSArray of NSDictionarys. The object in each index of NSArray corresponds to each row of NSTableView. And the object of each NSDictionary with a given key corresponds to each NSTableColumn with a given identifier. That’s the simplest way to build the model for NSTableView. Therefore, I add an NSMutableArray in Document class.

Document.h:

#import <AppKit/AppKit.h>
#import <AppKit/NSDocument.h>

@interface Document : NSDocument
{
    id tableView;
    NSMutableArray* records;
}
@end

The “records” will store the data of NSTableView. For more information about the usage of NSMutableArray, read Basic GNUstep Base Library Classes.

Document.m:

- (id) init
{
    self = [super init];
    records = [NSMutableArray new];
    return self;
}

- (void) dealloc
{
    RELEASE(records);
    [super dealloc];
}

- (int) numberOfRowsInTableView: (NSTableView*) view
{
    return [records count] + 1;
}

- (id)              tableView: (NSTableView*) view
    objectValueForTableColumn: (NSTableColumn*) column
                          row: (int) row
{
    if (row >= [records count]) {
        return @""; 
    } else {
        return [[records objectAtIndex: row] 
                          objectForKey: [column identifier]];
    }
}

We create the instance of NSMutableArray in the method -init, and release it in -dealloc, which will destroy it if no other object needs it. In the method -numberOfRowsInTableView:, we return one plus the amount of records because we want it to display an extra empty row for the user to add new records. Hence, in the method -tableView:objectValueForTableColumn:row:, I have to check whether the row the NSTableView requests is larger than the number of actual records. If so, it is a request for data to show in the empty row. Therefore, we just return an empty string (@""). We are using an array of dictionaries to make the key of the dictionary the same as the identifier of the NSTableColumn. So I can get the object directly by knowing the identifier of NSTableColumn. If you are not using an NSDictionary for each row, you can consider Key Value Coding (KVC), which offers a similar way to get the right object. Otherwise, you have to use if-else to get the right object. The advantage of NSDictionary (or KVC) will be more clear for data input.

Data input#

Now, we’ll add the functionary of data input. First, we have to set the NSTableColumn to editable.

- (void) windowControllerDidLoadNib: (NSWindowController*) controller {
    NSTableColumn *column;
    NSArray *columns = [tableView tableColumns];

    column = [columns objectAtIndex: 0];
    [column setWidth: 100];
    [column setEditable: YES];
    [column setResizable: YES];
    [column setIdentifier: @"date"];
    [[column headerCell] setStringValue: @"Date"];

    column = [columns objectAtIndex: 1];
    [column setWidth: 100];
    [column setEditable: YES];
    [column setResizable: YES];
    [column setIdentifier: @"item"];
    [[column headerCell] setStringValue: @"Item"];

    column = [[NSTableColumn alloc] initWithIdentifier: @"amount"];
    [column setWidth: 100];
    [column setEditable: YES];
    [column setResizable: YES];
    [[column headerCell] setStringValue: @"Amount"];
    [tableView addTableColumn: column];
    RELEASE(column);

    [tableView sizeLastColumnToFit];
    [tableView setAutoresizesAllColumnsToFit: YES];
}

When the user double-clicks a cell, the user can edit the contents of the cell. When the user finishes, the table view will send -tableView:setObjectValue:forTableColumn:row: to the data source.

Document.m:

- (void)      tableView: (NSTableView*) view
         setObjectValue: (id) object
         forTableColumn: (NSTableColumn*) column
                    row: (int) row
{
    if (row >= [records count]) {
        [records addObject: [NSMutableDictionary new]];
    }
    [[records objectAtIndex: row] setObject: object
                                     forKey: [column identifier]];
    [tableView reloadData];
}

Again, we need to take care of the special situation where user input in the last empty row. Since it is not in the records, I need to add a new dictionary item to the records, to represent a new row. Whenever the user inputs the data, it will be store into records according its row and the identifier of the column. And the key in the dictionary is the same as the identifier of the NSTableColumn. Hence I can retrieve the data according to the identifier of the column. Finally I ask the NSTableView to reload the data in order to reflect the change of data source.

Now you can play around this application and input the data.

Here is the source code: Table-2-src.tar.gz.

This example shows how easy it is to make a real document-based application without worrying about the management of multiple documents and windows.