# 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`*](http://cocoadevcentral.com/articles/000063.php)
> If you are interested
in text editors, [Ink.app](https://github.com/gnustep/tests-examples/tree/master/gui/Ink) 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**
![](GSPT_files/Table-02.jpg)
![](GSPT_files/Table-01.jpg)
Check the "Horizontal" scroller.
**Figure 4-39. Attributes of `NSTableView`**
![](GSPT_files/Table-03.jpg)
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**
![](GSPT_files/Table-04.jpg)
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**
![](GSPT_files/Table-05.jpg)
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**
![](GSPT_files/Table-06.jpg)
![](GSPT_files/Table-07.jpg)
![](GSPT_files/Table-08.jpg)
Connect the outlet `tableView` to `NSTableView`.
**Figure 4-43. Connect outlet to table view**
![](GSPT_files/Table-09.jpg)
![](GSPT_files/Table-10.jpg)
![](GSPT_files/Table-11.jpg)
Save the Gorm file and quit Gorm.
## Basic data source
Add the new outlet in Document.h.
`Document.h:`
```objc
#import
#import
@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:`
```objc
- (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](http://gnustep.made-it.com/GSPT/Table/Table-1-src.tar.gz)
## Configuring the table view
Let's work on the interface first. `NSTableView` is a collection of
`NSTableColumn`s. 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:`
```objc
- (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](http://cocoadevcentral.com/articles/000003.php). 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
`NSDictionary`s. 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:`
```objc
#import
#import
@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*](http://www.gnustep.it/nicola/Tutorials/BasicClasses/).
`Document.m:`
```objc
- (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)*](http://developer.apple.com/techpubs/macosx/Cocoa/TasksAndConcepts/ProgrammingTopics/KeyValueCoding/index.html),
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.
```objc
- (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:`
```objc
- (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](http://gnustep.made-it.com/GSPT/Table/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.
------------------------------------------------------------------------