# 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. ------------------------------------------------------------------------