Wednesday, June 4, 2014

CPTableView: Step-by-Step Tutorial: Part 2

In the last part of this tutorial, we reviewed the work of a programmer that led me to a deeper understanding of the CPTableView.  Today, we start from scratch and we will build something of our own.  Because I work in a medical setting, I cannot use any projects that I am currently working on, so I will use a hobby of mine to make a practical example: role playing games.

Our Project

We are going to create a table of equipped weapons for our Role Playing Character.  Alone, this does not have much use, but it can be combined with other features to create a player sheet for your games.  Our table will ultimately be an implementation of this simple line drawing:
Our Objective for this Tutorial
For those who have not played Dungeons and Dragons, Pathfinder, or any other table-top RPG, the scenario is this:

You have a character who is defined by skills and attributes that are associated with die roles.  During the course of your character's "life" you will ultimately face many challenges; in most games, this means that you will have to fight, usually with weapons.  Because our memories are limited, we usually track the weapons we have and what they can do on our character sheets in a table or list like the one drawn above.  For this tutorial, we will use weapons and rules from the Song of Ice and Fire RPG by Ronin Games, modeled after the popular books by George R.R. Martin and the popular television show on HBO "Game of Thrones."

Starting Simple

First, create a new project.  By default, all new projects have a borderless bridge window, which will provide us a blank canvas on which to build our new table. We will create the table within the AppController, but in a production project, I would make a subclass of CPTableView and fill it with all the functions that I need to make this a really convenient view object.

Let's replace the content of applicationDidFinishLoading method with the following:
var theWindow = [[CPWindow alloc] initWithContentRect:CGRectMakeZero() styleMask:CPBorderlessBridgeWindowMask],
      contentView = [theWindow contentView];

var scrollView = [[CPScrollView alloc] initWithFrame:CGRectMake( 100, 50, 600, 200 )],
      tableView = [[CPTableView alloc] initWithFrame:CGRectMakeZero()],
      weaponColumn = [[CPTableColumn alloc] initWithIdentifier:@"Weapon"],
      damageColumn = [[CPTableColumn alloc] initWithIdentifier:@"Damage"],
      attackColumn = [[CPTableColumn alloc] initWithIdentifier:@"Attack"],
      qualityColumn = [[CPTableColumn alloc] initWithIdentifier:@"Quality"];

[[weaponColumn headerView] setStringValue:@"Weapon"];
[[damageColumn headerView] setStringValue:@"Damage"];
[[attackColumn headerView] setStringValue:@"Attack"];

//and just for the sake of showing the above three lines more clearly, we will do one differently
var qualityHeaderCell = [qualityColumn headerView];
[qualityHeaderCell setStringValue:@"Qualities"];

[tableView addTableColumn:weaponColumn];
[tableView addTableColumn:damageColumn];
[tableView addTableColumn:attackColumn];
[tableView addTableColumn:qualityColumn];

[scrollView setBorderType:CPLineBorder];

//creating a new variable for re-enforcement of the concept
var borderlessWindowBackgroundView = [theWindow contentView];

//Now, we build the view hierarchy
[scrollView setDocumentView:tableView];
[borderlessWindowBackgroundView addSubview:scrollView];

//this can also be written as [contentView addSubview:scrollView] with the same effect, because our borderlessWindowBackgroundView points to the same CPView object as contentView defined in the first variable declarations.

//display the window
[theWindow orderFront:self];
Load your application, and we will see the first part of our new creation:

Our table seems very boring right now.  There is no data; there are just column headers and an empty table.  This problem will fix itself once we add a data source and some data.

Adding A Data Source

In the last tutorial, we used the AppController as the data source.  For this, we will create a new data source.  Create a new file called "WeaponTableDataSource.j" and add the following code to the new file:
@import <Foundation/Foundation.j>
@implementation WeaponTableDataSource : CPObject
{
      CPArray weaponNames;
      CPArray damageValues;
      CPArray attackValues;
      CPArray qualityValues;
}
-(id)init
{
     self = [super init];
     if(self){
          weaponNames = [[CPArray alloc] init];
          damageValues = [[CPArray alloc] init];
          attackValues = [[CPArray alloc] init];
          qualityValues = [[CPArray alloc] init];
          [self fillDataValues];
     }
     return self;
}
-(void)fillDataValues
{
     //we will default to regular javascript here for a moment
     //this will help us as we move forward to maintain the concept of a data object, which right now
     //is broken into several different string arrays.
     var weaponValues = [
           ["Battleaxe", "Fighting + Axe Specialty", "Athletics", "Adaptable"],
           ["Morningstar", "Fighting + Bludgeon Specialty", "Athletics", "Shattering 1, Vicious"],
           ["Whip", "Fighting + Brawling Specialty", "Athletics - 1", "Slow"],
           ["Bow, Hunting", "Marksmanship + Bow Specialty", "Agility", "Long Range, Two-handed"]
     ];
     var curWeapon = nil, weaponNameIndex = 0, weaponAttackIndex = 1, weaponDamageIndex = 2, weaponQualitiesIndex = 3;
     for( var i = 0; i < weaponValues.length; i++ ){
          curWeapon = weaponValues[i];
          [weaponNames addObject: curWeapon[ weaponNameIndex ]];
          [damageValues addObject: curWeapon[ weaponDamageIndex ]];
          [attackValues addObject: curWeapon[ weaponAttackIndex ]];
          [qualityValues addObject: curWeapon[ weaponQualitiesIndex ]];
     }
}
//functions required by the data source interface or protocol
-(CPInteger)numberOfRowsInTableView:(CPTableView)aTableView
{
        /****************
 we are only using this data source in a single table, so we do not need to choose a different row count based on any identifying information in the table view.
it is important to note that we could use the same data source in multiple tables, and each table might be provided different data to display based on the table's identifier or any other unique table quality that helps us determine what value to return.
we set the same amount of data in all rows, so we don't need to do any complex calculation here.
      *****************/
      
        return [weaponNames count];
        //return weaponNames.length; //also works here
}
-(id)tableView:(CPTableView)aTable objectValueForTableColumn:(CPTableColumn)aColumn row:(CPInteger)rowNum
{
      /************
Again, here we can choose to return a completely different set of data for different table views, but we will only use one table view in this example and can ignore the tableView variable.
We will however use the column to determine which data the table needs to display, and we will return a different value based on which column we are currently reviewing.
     *************/
     var columnIdentifier = [aColumn identifier];
   
     //we set the column identifiers when we first initialized them.  We will use these same values as names or identifiers here to distinguish one column from the next
     switch( columnIdentifier ){
     case 'Weapon':
           return [weaponNames objectAtIndex: rowNum];
     case 'Damage':
           return [damageValues objectAtIndex: rowNum];
     case 'Attack':
           return [attackValues objectAtIndex: rowNum];
     case 'Quality':
           return [qualityValues objectAtIndex: rowNum];
     default:
           return @'Error String'; //we hope never to see this value.
     }
}
@end
Then, to the top of AppController.j add:
@import 'WeaponTableDataSource.j'
Inside our applicationDidFinishLaunching method, we will add the following:
var dataSource = [[WeaponTableDataSource alloc] init];
[tableView setDataSource: dataSource];
We will notice that I have not set a delegate function, because it is not necessary.  We set a delegate when we want to transfer some of the really advanced drawing to another object.  The CPTableView, however, does not require a delegate to handle these basic functions, and only needs a dataSource that responds to the proper selectors (a selector is a call to a specific method on an object).

We also notice, if we compare our function names to the NSTableViewDataSource Protocol that Cappuccino uses different selector names than its Cocoa counterpart.  If you make the mistake of using the Cocoa protocol definition as a strict guide, you will not receive any error from Cappuccino, because there is no error to report.  What does happen however, is that the data fails to load, because the CPTableView identifies that the selector:

-(id)tableView:(CPTableView)aTable objectValueForTableColumn:(CPTableColumn)aColumn row:(CPInteger)rowNum

does not exist, and therefore, it never makes the data call to obtain objects for the data views and the result is an empty table.

We have not made that mistake however, so when we reload the page, we see the following:
Aside from some cosmetics, we see that we are nearly done.  We never activated the alternating row color as we did in the previous example, so the table has a plain white background.  We are using the default data view with simple strings, so all the data appears in simple columns of a standard width and the excess string lengths are clipped from the view as they would be if we had made a text field that was too short.  We notice that we can change the dimensions of the columns to reveal the data, and if we choose to see the columns in a different order, CPTableView allows us to switch the columns on the fly, although unless we make a special effort to save and load these column order and size preferences, they disappear on the page reload.

This resulting table is also really close to our intended result, which we display again here for convenience.  While we could stop here, what would be the fun in that?!  Let's see what happens when we move forward and add some specialized views and change our data source to work with our custom object definitions.


Our Objective
Our Current View
In the next tutorial, we start separating our View and Model elements for an MVC design.  We get rid of these ugly arrays and start working with weapon objects instead.

No comments:

Post a Comment