Wednesday, June 11, 2014

CPSegmentedControl: A Better Choice Than CPRadio

We addressed CPRadio buttons in a previous post (CPRadio Button Tutorial).  In my personal opinion, the CPSegmentedControl provides a much simpler interface, which is also more user friendly.

While we have learned to use radio buttons and are never surprised when we see them as users, they do cause problems for layout and design.  One particular problem is the association of the radio button and the text that describes it.  If there is space to display the options as a vertical list, the choice is clear, but if all the choices must fit horizontally on a single line, then there becomes issue with which choice a button is associated with.  We overcome this difficulty often by placing additional visual space between various options so that there is a closeness between the button and the associated text that is not present on the other options.

With the CPSegmentedControl, we present the user with a grouped list of options, of which the user can select one.  The default behavior of this control is to deselect all options when a new option is selected.  While this behavior can likely be overridden, it provides us with a very useful and very simple implementation that can replace radio box arrangements.

As an added benefit to users familiar with the iOS interface, this control is much more familiar, as it is used regularly in iOS applications.

A Simple Example

In a new project, remove the "Hello World" label, and add the following to applicationDidFinishLaunching:
    var segmentedControl = [[CPSegmentedControl alloc] initWithFrame:CGRectMake(10,10,200,30)];
    [contentView addSubview:segmentedControl];
[segmentedControl setSegmentCount:3];
[segmentedControl setLabel:@"Option 1" forSegment:0];
[segmentedControl setLabel:@"Option 2" forSegment:1];
    [segmentedControl setLabel:@"Option 3" forSegment:2];
  
    var genderSelection = [[CPSegmentedControl alloc] initWithFrame:CGRectMake(10,50,200,30)];
    [contentView addSubview:genderSelection];
    [genderSelection setSegmentCount:2];
    [genderSelection setLabel:@"Male" forSegment:0];
    [genderSelection setLabel:@"Female" forSegment:1];

Load you project to see this:
 The code creates two segmented controls.  Each control, sadly, must be defined with a frame.  This is problematic, only because a frame that is too small will clip the control's images providing an ugly appearance, and a frame that is too large makes positioning difficult for right-aligned or centered layouts.  To overcome these problems, I would recommend creating a subclass of the CPSegmentedControl, that has a sizeToFit method, which should loop through the various segments and creates a new frame based on the frame of each segment contained within the object.

The segments are automatically sized to match any label value that is inserted into them, or they can be set directly by calling setWidth:(int)forSegment:(int).

The CPSegmentedControl is a CPControl object, which means that all the usual setTarget, setAction options are available for this object as well as various other responder functions to handle clicks, hovering, etc.

CPTabView: A Quick Overlook

Once we have a handle on views in general (Click here for a review of CPView), the CPTabView is rather simple to understand.  Remove the "Hello World" label code and copy-and-paste the code here below into your applicationDidFinishLaunching function of a new project.  Then add the method that follows to your AppController implementation.  Reload the page, and we will discuss and play:

    var tabView = [[CPTabView alloc] initWithFrame:CGRectMake(10,10,500,300)],
        tabFrame = [tabView frame];
    //Note, we use the contentView defined in the auto generated code, so don't replace everything.
    [contentView addSubview:tabView];
  
    var tab1 = [[CPTabViewItem alloc] initWithIdentifier:@"Tab1"],
        tab2 = [[CPTabViewItem alloc] initWithIdentifier:@"Tab2"],
        tab3 = [[CPTabViewItem alloc] initWithIdentifier:@"Tab3"];
  
    [tab1 setLabel:@"Tab 1"];
    [tab2 setLabel:@"Tab 2"];
    [tab3 setLabel:@"Tab 3"];
  
    var view1 = [[CPView alloc] initWithFrame:tabFrame],
        view2 = [[CPView alloc] initWithFrame:tabFrame],
        view3 = [[CPView alloc] initWithFrame:tabFrame];
  
    [self addText:@"This is tab 1" toView:view1];
    [self addText:@"This is tab 2" toView:view2];
    [self addText:@"This is tab 3" toView:view3];
  
    [tab1 setView:view1];
    [tab2 setView:view2];
    [tab3 setView:view3];
  
    [tabView addTabViewItem: tab1];
    [tabView addTabViewItem: tab2];
    [tabView addTabViewItem: tab3];
Add this method to AppController:
-(void)addText:(CPString)stringValue toView:(CPView)aView
{
    var center = [aView center],
        textField = [[CPTextField alloc] initWithFrame:CGRectMakeZero()];
  
    [textField setStringValue:stringValue];
    [textField sizeToFit];
    [textField setCenter:center];
    [aView addSubview:textField];
This is what we have created:

 Understanding the CPTabView

CPTabView creates the grey background.  Without the CPTabViewItems, all that exists is this grey canvass, and nothing else.  CPTabView is a CPView object, which means that we have access to all the wonderful functions that are associated with CPViews, including the ability to request the current frame.

In this code, we make that request to create the variable tabFrame.  I want the tab items to have a content area that fills the entire area of the tab view, so the frame's should have the same size and be positioned at (0,0) ideally.  For the purpose of this demo, I have skipped this rather important step.  If we were to set a background color to the views: view1, view2, and view3, we would see that the views are offset from the corner of the tab view area by 10 units on the left edge and 10 units on the top edge.

CPTabViewItem is what provides the tab view the labels for the tabs and the views for the content area.  CPTabViewItem is not a CPView object, and therefore has no ability to become part of the display.  Also, if we fail to set a label on the tab view item, the table does not create a tab to represent the object.  Try commenting out one of the setLabel methods, and you will see that the corresponding tab disappears altogether!  For these reasons, we must remember to always set a label on each tab item as well as a view for that tab.  Without these two critical components, the tab view item will not provide the tab view with the required information to be functional.

Within the tab view, we are able to construct any view that we desire.  If the content will be a larger content area than the defined frame, we can use a CPScrollView as our tab view to add scrollbars to the display area.

Tab Options

Tab Type

If you do not wish to see your tabs at the top of your display, we have some options.  Using setTabViewType, we can switch between five different display options.  The first option is default:
  • CPTopTabsBezelBorder
  • CPBottomTabsBezelBorder
  • CPNoTabsBezelBorder
  • CPNoTabsLineBorder
  • CPNoTabsNoBorder
These display types are displayed here:
CPTopTabsBezelBorder
CPBottomTabsBezelBorder

CPNoTabsBezelBorder

CPNoTabsLineBorder

CPNoTabsNoBorder
When the tabs are removed, one can change the tab programmatically by calling:
  • selectFirstTabViewItem:(id)aSender
  • selectLastTabViewItem:(id)aSender
  • selectNextTabViewItem:(id)aSender
  • selectPreviousTabViewItem:(id)aSender
  • selectTabViewItem:(CPTabViewItem)aTabViewItem
Of these functions, only selectTabViewItem requires a specific parameter.  For this method, the parameter is a copy of the tab view instance that should be selected when the method completes its operations.

All the other methods are ideally paired with buttons or other screen elements.  They take the signal sender as an argument and then work only with the current tab view item array to move either to the very first or last items or to change the index by one in either direction.  The method names are sufficiently descriptive that we will not need to identify which ones perform which actions.

setFont

Want to change the font of the tabs?  Pass your font to the method CPTabView :: setFont, and it is done.

Summary

CPTabView is a useful means of putting many displays in one content area, when the displays do not need to share the screen at the same time.  An alternative to this display type, which is also popular, is the CPAccordianView.  The class is delightfully simple to use and manipulate, and requires a basic understanding of CPView operations, which are covered in detail here: CPView Overview.


Tuesday, June 10, 2014

CPWebView: Displaying Customized HTML

Cappuccino converts the view hierarchy into a nested stack of <DIV> elements, which is both deep and impossible to predict when it comes to adding custom HTML into our page.  For those moments that we need to enhance the capabilities of Cappuccino by using additional features available in other frameworks, we can turn to the CPWebView class to insert these elements into our page.

At the end of the article, I will share a git resource to an amazing example of this kind of modification, which I recommend to all who need a text editor in their application, because Cappuccino has no objects to represent a <TEXTAREA></TEXTAREA> input field.

Create A New Project

Our typical starting place: Hello World!
As we have done so many times before, create a brand new project in a new folder.  I am calling this project CPWebView, so that I can keep all these tutorials separate from one another.  When I load my new project, I see nothing more than the usual "Hello World" screen. 

Our First WebView example

As usual, open AppController.j and edit the applicationDidFinishLaunching method.  Remove all the lines pertaining to the label in the center of the screen, but leave the variable definition of our window and content view elements. You should also leave the orderFront message at the end of the method.

After the variable definition and before the orderFront method call, add the following code:
var margin = 20,
        windowWidth = CGRectGetWidth([contentView frame]),
        contentWidth = windowWidth - 2 * margin, /*left and right margins */
        xPos = 0 + margin, /* 0 is our current starting point, we shift by one margin */
        yPos = 100,
        height = 300,
        webView = [[CPWebView alloc] initWithFrame:CGRectMake(xPos,yPos,contentWidth,height)],
        HTMLString = "<style type=\"text/css\">body{ background-color:lightgreen; }</style><h1>This is Our Custom Header</h1><p id=\"paragraph1\">This paragraph was inserted manually, and is a custom id which can be referenced in javascript or used programmaticaly to make changes.</p>";
[webView loadHTMLString:HTMLString];
[contentView addSubview:webView];
Reloading the page shows a green background which is positioned 20 units from the left and right edges of the page.  On the green background is the title, which we set with the substring "<h1>This is Our Custom Header</h1>", and a paragraph which we set with the substring "<p id=\"paragraph1\">This paragraph was inserted manually, and is a custom id which can be referenced in javascript or used programmaticaly to make changes.</p>".

This web view does not keep the 20 unit margin it is created with when we change the dimensions of the browser window, but we can change that by adding one line of code:

[webView setAutoresizingMask: CPViewWidthSizable ];
After adding this line of code, our web view now changes along with the browser window, after a brief delay.

What is interesting to note about this view, is that the style element, which addresses the <BODY></BODY> element, only has affect on the content view window that we originally defined, which is as wide as the browser window (less the margins) and 300 units tall.  This subview is a web-page unto itself.  It is integrated into our design by an <IFRAME></IFRAME> element, which allows it to operate completely independent of the surrounding elements.

Using a page inspector (I have used Chrome in this example; see image), we see both the layout of this iFrame and also note that our paragraph has an id attribute that is set to what we have defined in our HTML string, which gives us the control we need to implement some advanced functionality outside of Cappuccino's limitations.
Our custom web page elements are within an iframe element and displays the HTML as we defined it.
Defining a webpage within the quotations of a string definition is tiresome and not generally recommended.  Like any good web view should, the Cappuccino CPWebView class allows us to reference a file located at a URL instead of relying on setting string values all the time.  Let's change our code to the following:
    /* COMMENTED OUT ****
    [webView loadHTMLString:HTMLString];
    *************/
    [webView setMainFrameURL:@"http://localhost/CPWebView/examplePage.php"];
Let us also create the file examplePage.php in our project root:
<!DOCTYPE html>
<html>
<head>
        <title>An Example Page</title>
</head>
<body>
<h1>Eureka!</h1>
<p>This document is dual purposed. It shows that any URL can be used, and that t
he url need not point to a static HTML document. Sever-side scripting can be use
d to generate dynamic content for your Web View content.</p>
<p>This document was generated at: <?php $date = new DateTime("now", new DateTimeZone("America/Los_Angeles")); echo $date->format("M d, Y H:i:s"); ?></p>
</body>
</html> 
Before we reload the page, I want to make an important note: the url I have used here is for the system as I have set it up on my machine. If you are reading this tutorial, I hope you have a basic understanding of URL's, and you should be able to write a proper URL to point to the examplePage.php file, wherever it is that you have saved this document.  Please correct the URL to match your setup before continuing.

Also, this particular example uses PHP as a server-side processing.  If you do not have PHP installed on your web server, omit the second paragraph in examplePage.php.  You should be able to still use the PHP file extension, because the web browser does not use the file extension to determine that HTML is being presented, so the content of the file will display as a regular HTML file in this case.

If you have set the URL to your page correctly, when you reload the page you will see the following:
We use our CPWebView to call on a web page stored elsewhere in order to fill content in our page.
For those who have PHP installed on the web server, they will notice that the date and time displays, which shows that we can use the CPWebView to call on any web page that we might display using a regular browser, and this content can be put into the framework of our page as a subView to any view element.

This means that we can create handy HUD editors like the one that we create next using WKTextView, which is a class developed outside of Cappucccino using the Google Closure framework within a CPWebView.

A Practical and Terrific Example of Using CPWebView

Getting WKTextView

Before we continue, I am giving a big kudos Alexander Ljungberg, who developed WKTextView in response to Cappuccino's lacking implementation of a NSTextField that wraps while one is editing text (CPTextField does wrap, but when one edits the text value, the text displays on a single line, making editing the text field very difficult).  Mr. Ljungberg not only handles this issue, but also provides us methods, which allow us to created toolbar functionality that changes font styles, bold settings, and more.

The WKTextView application relies on the Google Closure library.  I had difficulty getting everything to work on my system until I installed the closure library per the instructions at Google.  Please take a moment to follow the installation instructions of Google Closure at this location: Google Closure.

Download or clone a copy of WKTextView from: https://github.com/wireload/WKTextView.git.  In my web-root folder, using the Terminal.app (MacOS X), I have typed the following in order to clone a copy of the WKTextView code:

DevLaptop:Documents jaredclemence$ git clone https://github.com/wireload/WKTextView.git ./WKTextView
This resulted in the following output, confirming the transfer:
Cloning into './WKTextView'...
remote: Reusing existing pack: 526, done.
remote: Total 526 (delta 0), reused 0 (delta 0)
Receiving objects: 100% (526/526), 103.73 KiB | 0 bytes/s, done.
Resolving deltas: 100% (287/287), done.
Checking connectivity... done. 
From within the WKTextView directory, I type the following per the README file in the packet:
DevLaptop:WKTexTView jaredclemence$ git submodule init
Submodule 'auxiliary/google-closure-library' (git://github.com/aljungberg/google-closure-library.git) registered for path 'auxiliary/google-closure-library'

And then:
DevLaptop:WKTexTView jaredclemence$ git submodule update
Cloning into 'auxiliary/google-closure-library'...
remote: Counting objects: 32709, done.
remote: Compressing objects: 100% (5576/5576), done.
remote: Total 32709 (delta 26852), reused 32709 (delta 26852)
Receiving objects: 100% (32709/32709), 15.10 MiB | 3.44 MiB/s, done.
Resolving deltas: 100% (26852/26852), done.
Checking connectivity... done.
Submodule path 'auxiliary/google-closure-library': checked out '926fcba10539dad4c6bbb6aadfa5a39940833053'
After the project was installed, I first ran the file "auxilliary/build.sh" with sudo.  Some changes were needed to the variables within to point to the google closure installer that I had just previously installed.  This file, build.sh creates a javascript file called "closure-editor.js" and puts it in the Resources folder for use by the editor.html file.

From within the WKTextView folder, after file modifications type:
sudo auxilliary/build.sh

Next, I ran "./samplify.sh", also with sudo.

sudo ./samplify.sh
Then, to make the files easier to navigate with Mac Finder, I changed the folder name of the new folder  from "sample.dist" to "sampleFolder."  This folder contains a sample program using the WKTextField.j object.  We are creating our own, so for now, just copy the WKTextView.j file and the resources folder into our project.

Reviewing WKTextView

The first thing that we should notice is that the WKTextView class is derived from _WKWebView, which is derived from CPWebView.  We have all the usual methods available to us for class definition, including initWithFrame, which goes through the process of setting up our default layout within the CGRect we provide.

Also, we notice that the object refers to the Resource folder for an HTML file that contains the primary editor on this line:
[self setMainFrameURL:[[CPBundle bundleForClass:[self class]] pathForResource:"WKTextView/editor.html"]];
The CPBundle class has not yet been reviewed in this tutorial.  In general, we will say that this object helps in resource management.  Ultimately, this line does the same thing as passing a url to the file editor.html, which is currently in the path "Resources/WKTextView/editor.html" with respect to our web root.

The class also has several well commented methods defined which handle various scenarios which affect redisplaying the editor.html page when necessary as well as when to allow and when not to allow editing on the document area.

Next, we observe that a method is defined to return a CPString; this method is named htmlValue, and it likely will return the content of the editor window.  Similarly, there is a setHTMLValue: method which should let us load the editor with previously entered text.

Lastly, we observe that there are several delegate functions defined, which allow us to receive signals about the status of the WKTextView editor.  These delegate functions include:

  • textViewDidLoad:
  • textViewDidChange:
  • textViewDidBeginEditing:
  • textViewDidEndEditing:
  • textViewCursorDidMove:

Using WKTextView

After we have spent some time reviewing the well written code inside WKTextView.j, we can begin adding WKTextView to our current project.  Edit AppController.j to contain the following (warning, this is a bigger file, most of which is copied from the sample application):
/*
 * AppController.j
 * CPWebView
 *
 * Created by You on June 10, 2014.
 * Copyright 2014, Your Company All rights reserved.
 */
@import <Foundation/Foundation.j>
@import <AppKit/AppKit.j>
@import 'WKTextView.j' /* we must import the object class to use it */
/* these constants are defined in the sample.dist of WKTextview */
var NewToolbarItemIdentifier = "NewToolbarItemIdentifier",
    BoldToolbarItemIdentifier = "BoldToolbarItemIdentifier",
    ItalicsToolbarItemIdentifier = "ItalicsToolbarItemIdentifier",
    UnderlineToolbarItemIdentifier = "UnderlineToolbarItemIdentifier",
    StrikethroughToolbarItemIdentifier = "StrikethroughToolbarItemIdentifier",
    AlignLeftToolbarItemIdentifier = "AlignLeftToolbarItemIdentifier",
    AlignRightToolbarItemIdentifier = "AlignRightToolbarItemIdentifier",
    AlignCenterToolbarItemIdentifier = "AlignCenterToolbarItemIdentifier",
    AlignFullToolbarItemIdentifier = "AlignFullToolbarItemIdentifier",
    InsertLinkToolbarItemIdentifier = "InsertLinkToolbarItemIdentifier",
    UnlinkToolbarItemIdentifier = "UnlinkToolbarItemIdentifier",
    InsertImageToolbarItemIdentifier = "InsertImageToolbarItemIdentifier",
    FontToolbarItemIdentifier = "FontToolbarItemIdentifier",
    BulletsToolbarItemIdentifier = "BulletsToolbarItemIdentifier",
    NumbersToolbarItemIdentifier = "NumbersToolbarItemIdentifier",
    RandomTextToolbarItemIdentifier = "RandomTextToolbarItemIdentifier";
@implementation AppController : CPObject
{
    /* we should add a handle to the HUD view that we are going to implement, as well as the primary window */
    CPWindow mainWindow;
    CPPanel hudEditor;
    CPTextField textDisplay;
    WKTextField editorView;
    CPToolBar toolbar;
}
- (void)applicationDidFinishLaunching:(CPNotification)aNotification
{
    mainWindow = [[CPWindow alloc] initWithContentRect:CGRectMakeZero() styleMask:CPBorderlessBridgeWindowMask];
    hudEditor = [[CPPanel alloc] initWithContentRect:CGRectMake(40,40,700,300) styleMask:CPHUDBackgroundWindowMask | CPResizableWindowMask | CPClosableWindowMask];
  
    [self fillHud: hudEditor];
    [self layoutMainWindow: mainWindow];
    [mainWindow orderFront:self];
    // Uncomment the following line to turn on the standard menu bar.
    //[CPMenu setMenuBarVisible:YES];
}
-(void)fillHud:(CPPanel)aPanel
{
    var pContentView = [aPanel contentView],
        pFrame = [pContentView frame],
        margin = 20,
        topMargin = 40,
        eWidth = CGRectGetWidth( pFrame ) - 2 * margin,
        eXpos = margin,
        eYpos = topMargin,
        eHeight = CGRectGetHeight(pFrame) - margin - topMargin,
        eFrame = CGRectMake( eXpos, eYpos, eWidth, eHeight ),
        editor = [[WKTextView alloc] initWithFrame:eFrame];
    toolbar = [[CPToolbar alloc] initWithIdentifier:"Styling"];
    [toolbar setDelegate:self];
    [toolbar setVisible:YES];
    [aPanel setToolbar:toolbar];

    [aPanel setTitle:@"Example Text Editor"];
    [editor setAutoresizingMask: CPViewWidthSizable | CPViewHeightSizable | CPViewMinYMargin];
    [editor setDelegate: self];
    [pContentView addSubview:editor];
    editorView = editor;
}
-(void)textViewDidChange:(WKTextView)anEditor
{
    var htmlString = [anEditor htmlValue];
    [textDisplay setStringValue:htmlString];
    [textDisplay sizeToFit];
}
-(void)layoutMainWindow:(CPWindow)aWindow
{
    var button = [CPButton buttonWithTitle:"Show Editor"],
        contentView = [aWindow contentView],
        contentFrame = [contentView frame],
        cWidth = CGRectGetWidth( contentFrame ),
        cHeight = CGRectGetHeight( contentFrame ),
        buttonFrame = [button frame],
        bWidth = CGRectGetWidth( buttonFrame ),
        bHeight = CGRectGetHeight( buttonFrame ),
        margin = 20,
        buttonXpos = cWidth - margin - bWidth,
        buttonYpos = cHeight - margin - bHeight,
        buttonOrigin = CGPointMake( buttonXpos, buttonYpos );
    textDisplay = [[CPTextField alloc] initWithFrame:CGRectMakeZero()];
    [textDisplay setEditable:NO];
    [textDisplay setLineBreakMode:CPLineBreakByWordWrapping];
    [textDisplay setFrameSize:CPSizeMake(400,20)];
    [textDisplay setCenter:[contentView center]];
    [button setTarget:self];
    [button setAction:@selector(showEditor:)];
    [button setFrameOrigin:buttonOrigin];
    [contentView addSubview:textDisplay];
    [contentView addSubview:button];
}
-(void)showEditor:(CPButton)aButton
{
    [hudEditor orderFront:self];
}
// Return an array of toolbar item identifier (all the toolbar items that may be present in the toolbar)
- (CPArray)toolbarAllowedItemIdentifiers:(CPToolbar)aToolbar
{
    return [NewToolbarItemIdentifier, CPToolbarSpaceItemIdentifier, BoldToolbarItemIdentifier, ItalicsToolbarItemIdentifier, UnderlineToolbarItemIdentifier, StrikethroughToolbarItemIdentifier, CPToolbarSpaceItemIdentifier, AlignLeftToolbarItemIdentifier, AlignRightToolbarItemIdentifier, AlignCenterToolbarItemIdentifier, AlignFullToolbarItemIdentifier, CPToolbarSpaceItemIdentifier, BulletsToolbarItemIdentifier, NumbersToolbarItemIdentifier, InsertLinkToolbarItemIdentifier, UnlinkToolbarItemIdentifier, InsertImageToolbarItemIdentifier, FontToolbarItemIdentifier, CPToolbarFlexibleSpaceItemIdentifier];
}
// Return an array of toolbar item identifier (the default toolbar items that are present in the toolbar)
- (CPArray)toolbarDefaultItemIdentifiers:(CPToolbar)aToolbar
{
    return [self toolbarAllowedItemIdentifiers:aToolbar];
}
// Create the toolbar item that is requested by the toolbar.
- (CPToolbarItem)toolbar:(CPToolbar)aToolbar itemForItemIdentifier:(CPString)anItemIdentifier willBeInsertedIntoToolbar:(BOOL)aFlag
{
    // Create the toolbar item and associate it with its identifier
    var toolbarItem = [[CPToolbarItem alloc] initWithItemIdentifier:anItemIdentifier];
    var mainBundle = [CPBundle mainBundle];
    var actionMap =
    {
        NewToolbarItemIdentifier:           { 'image': 'page_white.png',        'label': 'New',     'target': editorView,   'action':@selector(clearText:) },
        BoldToolbarItemIdentifier:          { 'image': 'text_bold.png',         'label': 'Bold',    'target': editorView,   'action':@selector(boldSelection:) },
        ItalicsToolbarItemIdentifier:       { 'image': 'text_italic.png',       'label': 'Italics', 'target': editorView,   'action':@selector(italicSelection:) },
        UnderlineToolbarItemIdentifier:     { 'image': 'text_underline.png',    'label': 'Under',   'target': editorView,   'action':@selector(underlineSelection:) },
        RandomTextToolbarItemIdentifier:    { 'image': 'page_white_text.png',   'label': 'Lorem',   'target': self,         'action':@selector(setRandomText:) },
        StrikethroughToolbarItemIdentifier: { 'image': 'text_strikethrough.png','label': 'Strike',  'target': editorView,   'action':@selector(strikethroughSelection:) },
        AlignLeftToolbarItemIdentifier:     { 'image': 'text_align_left.png',   'label': 'Left',    'target': editorView,   'action':@selector(alignSelectionLeft:) },
        AlignRightToolbarItemIdentifier:    { 'image': 'text_align_right.png',  'label': 'Right',   'target': editorView,   'action':@selector(alignSelectionRight:) },
        AlignCenterToolbarItemIdentifier:   { 'image': 'text_align_center.png', 'label': 'Center',  'target': editorView,   'action':@selector(alignSelectionCenter:) },
        AlignFullToolbarItemIdentifier:     { 'image': 'text_align_justify.png','label': 'Justify', 'target': editorView,   'action':@selector(alignSelectionFull:) },
        BulletsToolbarItemIdentifier:       { 'image': 'text_list_bullets.png', 'label': 'Bullets', 'target': editorView,   'action':@selector(insertUnorderedList:) },
        NumbersToolbarItemIdentifier:       { 'image': 'text_list_numbers.png', 'label': 'Numbers', 'target': editorView,   'action':@selector(insertOrderedList:) },
        InsertLinkToolbarItemIdentifier:    { 'image': 'link.png',              'label': 'Link',    'target': self,         'action':@selector(doLink:) },
        UnlinkToolbarItemIdentifier:        { 'image': 'link_break.png',        'label': 'Unlink',  'target': editorView,   'action':@selector(unlinkSelection:) },
        InsertImageToolbarItemIdentifier:   { 'image': 'picture.png',           'label': 'Image',   'target': self,         'action':@selector(doImage:) },
    };
    var action = actionMap[anItemIdentifier];
    if (action)
    {
        var image = [[CPImage alloc] initWithContentsOfFile:[mainBundle pathForResource:@"silk/"+action['image']] size:CPSizeMake(16, 16)];
        [toolbarItem setImage:image];
        [toolbarItem setTarget:action['target']];
        [toolbarItem setAction:action['action']];
        [toolbarItem setLabel:action['label']];
        [toolbarItem setMinSize:CGSizeMake(16, 16)];
        [toolbarItem setMaxSize:CGSizeMake(16, 16)];
    }
    else if (anItemIdentifier == FontToolbarItemIdentifier)
    {
        [toolbarItem setMinSize:CGSizeMake(160, 24)];
        [toolbarItem setMaxSize:CGSizeMake(160, 24)];
        var dropdown = [[CPPopUpButton alloc] initWithFrame:CGRectMake(0, 0, 160, 24) pullsDown:NO];
        [dropdown setTarget:self];
        [dropdown setAction:@selector(doFont:)];
        var fonts = [[CPFontManager sharedFontManager] availableFonts];
        for(var i=0; i<fonts.length; i++)
        {
            var fontName = fonts[i],
                menuItem = [[CPMenuItem alloc] initWithTitle:fontName action:nil keyEquivalent:nil];
            [dropdown addItem:menuItem];
        }
        [dropdown setTitle:@"Select Font..."];
        [toolbarItem setView:dropdown];
        [toolbarItem setLabel:"Font"];
    }
    return toolbarItem;
}
- (void)doFont:(id)button {
    var fontName = [button titleOfSelectedItem];
    [editorView setFontNameForSelection:fontName];
}
- (@action)doLink:(id)sender
{
    var link = prompt("Enter a link: ", "http://www.280north.com");
    if (link)
        [editorView linkSelectionToURL:link];
}
- (@action)doImage:(id)sender
{
    var link = prompt("Enter an image URL: ", "http://objective-j.org/images/cappuccino-icon.png");
    if (link)
        [editorView insertImageWithURL:link];
}
- (@action)setRandomText:(id)sender
{
    [editorView setHtmlValue:"<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut est urna, vulputate sed viverra dignissim, consequat vitae eros. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Suspendisse ut sapien enim, et pellentesque elit. In commodo facilisis est, et tempus lacus aliquam vitae. Maecenas quam nulla, elementum ut tristique quis, cursus a nisi. Duis mollis risus vel velit molestie convallis nec a purus. Donec neque arcu, suscipit sit amet mattis eu, fringilla ac sapien. Ut lorem nibh, mollis in tincidunt at, volutpat ut turpis. Maecenas nulla est, tincidunt pharetra consectetur vel, laoreet sed nibh. Pellentesque tempor diam vel elit commodo aliquet. Donec congue fringilla eros a tincidunt. Praesent accumsan mi tincidunt arcu ultricies nec pellentesque dolor faucibus. Mauris sed nisl in ligula porta congue et quis turpis. Suspendisse in lorem at felis tempus semper. In porta enim a ipsum aliquet consectetur.</p><p>Mauris ac tellus orci. Aenean egestas porta ornare. Cras nisl lorem, vulputate ac pellentesque eu, aliquet ac leo. Proin eros libero, tincidunt sed sodales eget, elementum non augue. Praesent convallis auctor venenatis. Suspendisse id urna quam. Aliquam sagittis, leo commodo laoreet interdum, arcu felis dictum velit, a sodales justo tortor a erat. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis nibh magna, consequat et congue eu, bibendum id nisi. Cras gravida risus in nulla pharetra sagittis. Cras neque eros, consectetur nec bibendum eget, bibendum dictum libero. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Nam rutrum dictum neque vel eleifend. Vivamus tempus, lorem vel ultricies ullamcorper, ante risus imperdiet massa, nec aliquam orci ipsum ut nisl. Aliquam id justo eu lorem dapibus tincidunt. Donec suscipit consequat metus, sed venenatis lorem malesuada sit amet. Ut at risus ut ligula vulputate auctor. Vivamus rutrum elementum porttitor. Fusce quam arcu, tristique eget consectetur eu, iaculis in urna.</p><p>Donec a metus ac elit faucibus sagittis non a ligula. In aliquet, lectus sed pulvinar bibendum, justo ligula faucibus sem, vestibulum eleifend lacus augue a eros. Suspendisse potenti. Phasellus vehicula blandit ultrices. Donec tortor nulla, fermentum nec viverra id, consequat non metus. Fusce nunc urna, aliquet sit amet varius ut, dapibus a sem. Aliquam erat volutpat. Vestibulum at enim et magna lacinia sollicitudin id nec dolor. Sed ultricies urna ut justo blandit tincidunt. Sed sit amet orci et justo pellentesque iaculis accumsan ac quam.</p><p>Nunc tristique felis quis leo blandit eget iaculis lacus hendrerit. Maecenas euismod consequat lacus quis porttitor. Quisque consequat, metus eu interdum vulputate, quam dolor porttitor dui, non faucibus quam nibh nec erat. Integer sit amet gravida quam. Proin nunc eros, tincidunt sit amet accumsan laoreet, dictum vel sapien. Praesent at fringilla orci. Etiam vehicula lacinia nisi, et molestie justo congue molestie. Maecenas tempus, quam nec placerat suscipit, lorem sapien feugiat augue, id pharetra augue enim eu nisl. Morbi ullamcorper lacus ac dolor ultricies vel pellentesque odio consequat. Maecenas a pellentesque nunc. Phasellus a varius massa. Vestibulum eget tortor eget ante iaculis molestie. Pellentesque eu augue metus, ut pellentesque purus. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Curabitur consequat feugiat tincidunt. Proin tellus tortor, pharetra vel rhoncus ac, varius eget nisi. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Cras et nunc metus. Pellentesque tincidunt iaculis erat id porta. Curabitur eget magna et velit eleifend tempor.</p><p>Vestibulum ultricies leo at augue malesuada congue. Maecenas laoreet metus et nunc consectetur placerat. Nulla facilisi. Duis iaculis tristique feugiat. Ut quis consectetur justo. Praesent condimentum sagittis dui, in lobortis tellus accumsan quis. In aliquam lacus non dolor accumsan rutrum. Etiam sed urna dolor. Donec consectetur lacus eu ante sodales feugiat. Aliquam aliquet nibh vel massa mattis pellentesque. Curabitur et tortor nisl, ut consequat felis. Mauris non orci at tortor ultrices condimentum. Aliquam erat volutpat. Mauris porttitor, diam convallis semper hendrerit, erat mi tempor dolor, id semper augue justo fermentum odio. Sed vitae nulla eu augue fringilla pellentesque vel ac neque. Nullam arcu nibh, auctor ut accumsan ac, ullamcorper eu velit. Integer in ligula nec felis auctor viverra. In commodo malesuada volutpat. Etiam justo elit, tincidunt ac semper sed, eleifend eu odio. Cras ac nulla eget lorem tempor venenatis.</p>"];
}
- (void)textViewDidLoad:(WKTextView)textView
{
    // Update the selected font.
    [self textViewCursorDidMove:textView];
}
- (void)textViewCursorDidMove:(WKTextView)textView
{
    var items = [toolbar visibleItems];
    for (i=0; i<items.length; i++)
    {
        var item = items[i];
        if ([item itemIdentifier] == FontToolbarItemIdentifier)
        {
            var font = [editorView font];
            [[item view] selectItemWithTitle:font];
        }
    }
}

@end 

Most of the code there builds the toolbar and handles requests such as font type changes and more.  The example code uses a CPToolBar to create a list of clickable menu items.  The buttons are pointed to functions and methods made available on the WKTextEditor to enact their predefined functionality, with a few exceptions that require additional processing.  For example, requests for setting a font style, a link, or an image requires more information than a direct function call will provide, so we point these buttons to a handler function in another object so that we can do some pre-processing before invoking the editor method.

I have found that the delegate function textViewDidEndEditing: is extremely difficult to trigger, so we use textViewDidChange: to give us an opportunity to remove the text from the editor and update our textField on the main window.  You will notice as you type that characters, which would be interpreted as HTML, are encoded so as not to disturb the CPWebView's display of the data.  This ensures that a less-than sign is always interpreted as a less than sign and not the beginning of an HTML tag.

However, if you play with bold, italic, or font settings, you will see that HTML is evident in the text, and is used to display the text as it has been formatted.  Ideally, we would save the text in this manner for easy access, and we could then use a CPWebView to display the text any time we needed to display the text in exactly the same manner in which it was originally formatted.

This screen shot shows an example of unformatted text with some encoded characters at the end:

Final Thoughts

I had a particularly difficult time getting the WKTextView example to work, because I did not realize how dependent it is on the Google Closure library. Overall, the results were worth the time and effort struggling with the closure installation and making changes to WKTextView to handle my localized settings.

This editor and the CPWebView can be powerful tools in an application that requires a text editor interface and provides the user ability to change or update text formatting within the document.

It is extremely important to note that the string value returned by the text editor is html encoded text, so it must be specially processed to remove formatting tags before the raw text becomes available.  The WKTextView class does not provide any functions to get the raw string data without the embedded formatting.

Friday, June 6, 2014

CPTableView: Step-by-Step Tutorial: Part 4

At the end of part 3, we observed that there were some problems in my model design.  Because the model does not truly affect cappuccino, I have made some changes to the underlying model, which I will show here with UML, and allow you to download from GitHub here:   https://github.com/JaredClemence/CPTableView-Tutorial-Part-4---Starting-Code.git

We added two equation objects which replace the erroneous Skill and SkillSpecialty attributes. UML created in MagicDraw PE.
The Skill and SkillSpecialty attributes are generalized into instances of a deeper extraction called Attribute. UML created in MagicDraw PE.

Current State of Work

After making some changes to the base code, we reload our page, and we have the following:
Data Table After Modifications To the Code. Download The Starting Code From GitHub, and read "Current State of Work" to discover the changes that were made since the last part.
What happened!  Don't fret, this is actually what we expect, once we learn that we have changed the data source to return objects instead of strings.  At the end of the last part, we noted that one of the problems with the design was that the Model elements were processing data to return a formatted string for the display, and we further noted that we were going to change the design so that we separate the View and Model elements more completely.

That is exactly what we have done.  While changing the underlying model, we also changed the data source so that instead of returning processed strings, we just return an object itself.

The table does no processing with these objects, instead, it hands the objects over to the default view, and the default view fails to handle the objects the way that we would desire, so it instead calls the description method on the object and displays the string that is returned.  Interestingly, at least to me, we see that the quality, which was returned as a CPArray, defaults to a comma separated list of it's contents.  In this case, the array is a list of strings, so we end up seeing something very similar to what we initially intended when the Model was processing the data to create a similar output.

Creating Special Views

In this tutorial, we endeavor to truly divide the Model and View segments by creating special views that will process the attack and damage objects in their native object format instead of a modified "viewable" format.  We see already what happens when the default view handles the display of our objects, but what can we do?

We are going to create special views for three of our four columns.  At first, we are going to just create some blank views.  Then we are going to just replicate the string output of the previous version; afterward, we will change the shape of the cells and add images to the design.

First, blank views

Let us create a file called WeaponTableViewElements.j.  Into this file, we will create several CPView derivatives, for each of our columns.  Because I have plans for the direction this tutorial will take down the road (alluded to in the model changes shown above), I have added another layer of separation between two of the views and the CPView parent.  The Damage and Attack views will each rely on this secondary object, which each will use to produce special views later.

Into WeaponTableViewElements.j, add the following:
@import <Foundation/Foundation.j>
@import <AppKit/AppKit.j>
@implementation AttribListBasedDataView : CPView
{
AttributeList attributeList @accessors;
}
-(void)init
{
self = [super init];
if( self ){
attributeList = Nil;
}
return self;
}
@end
@implementation WeaponNameCellView : CPView
{
}
-(void)setObjectValue:(id)aWeapon
{
    [self setBackgroundColor:[CPColor greenColor]];
}
@end
@implementation DamageTotalCellView : AttribListBasedDataView
{
}
-(void)setObjectValue:(id)valueObject
{
    [self setBackgroundColor:[CPColor blueColor]];
}
@end
@implementation AttackRollCellView : AttribListBasedDataView
{
}
-(void)setObjectValue:(id)diceRoll
{
    [self setBackgroundColor:[CPColor redColor]];
}
@end
@implementation QualityCellView : CPView
{
}
-(void)setObjectValue:(id)quality
{
}
@end

@implementation WeaponTrainingCellView : CPView
{
}
-(void)setObjectValue:(CPNumber)training
{
}
@end
Each of these view objects has a unique setObjectValue method.  This is the method that the CPTableView will call to pass the object to the view that needs displaying.  Because we don't actually do anything with the object, nor do we build our views, the cells will render as blank and empty areas.  In order to add a little more effect to this phase of the tutorial, we do add some background color to our areas.  We also will define our frame backgrounds differently in the next section to see what effect the frame has on the cell.

Next, we need to add our new views to our columns so that the columns offer them to the CPTableView on request.  Make the following changes to AppController.j:

1. Include our new views by adding this to the import section:
@import 'WeaponTableViewElement.j'
2. Create empty instances of these views, and then insert them into the corresponding columns.  Add the following code after the column definitions:
var weaponCellView = [[WeaponNameCellView alloc] initWithFrame:CGRectMake(0,0,100,200)],
        attackCellView = [[AttackRollCellView alloc] initWithFrame:CGRectMake(0,0,100,100)],
        damageCellView = [[DamageTotalCellView alloc] initWithFrame:CGRectMake(0,0,20,100)];
    [weaponColumn setDataView:weaponCellView];
    [attackColumn setDataView:attackCellView];
    [damageColumn setDataView:damageCellView];

 We have not even tested this part of the tutorial yet, and already I have started playing with elements in order to do two things: (1) teach a valuable lesson, and (2) speed up your understanding of CPTableView.  Reload the page, and you will find this:

We changed the background color, and we see that each of our views is correctly being used instead of the default views, except for quality which is still using the default view as before.  We also made our template views of different frame sizes, but that seems to have had no effect.  One possible thing that is going on here is that our view instance is not actually used in the table.  In fact, this is true.  Our instance is archived by the table view so that a template can be made, from that template many new instances are created, but it would seem that the frame is not the same as our original template.

A second possibility is that the cell, after it is created from our template, is forced to match the table's row and column format and that the cell has no control over it's own shape, location and dimensions, to find out, we will go back into our WeaponTableViewElements.j file and make two changes.

Add the following to setObjectValue method of WeaponNameCellView:

[self setFrame:CGRectMake(0,0,20,100)];
And add the following to setObjectValue method of DamageTotalCellView:
[self setFrame:CGRectMake(10,10,20,100)];
As the maker of this tutorial, I already knew that this would have an effect, but the results surprised me. What I initially expected to happen was that the green cell would still be confined to within the row in height, and it would appear to only be 20 units wide.  The blue field would similarly be clipped into the cell area, but there would be a white border due to the (10,10) offset of the origin.  What we actually see is this:
What we learn is that the cell views do have control over their own frame and origin, but surprisingly, we learn that they are each positioned with respect to the top left corner (the origin) of the clipped view in which the table forms.

We have not yet addressed this important fact, but what the TableView does with each cellView is calculates the origin that makes the new view appear to be in a table, and it sets that frame definition.  After setting that frame definition, the CPTableView calls setObjectValue to allow the view to finish creating the data display.  What we know from views already is that if we do not change the view's frame during our processing, anything that falls outside that view frame will be clipped.

Getting Our Strings Back

First, let's get our objects back to normal.  Remove from our view definitions the background color and the special frames, and just to reinforce the fact that frames are reset by the table view, let's change our AppController so that all our views are created with CGRectMakeZero();

After making our changes, our CPTableView should look like this:
Our table view after we have removed the coloring and other formatting from the previous demonstration in this tutorial.
Now that we have everything "back to normal", we can use what we have learned on previous tutorials to build string views.  Every view will pretty much be the same for this trial, so we will create a helper object to create our view after we have used the object to create our text display.  This is a temporary object, so we will create it in our WeaponTableViewElements.j file.

Add the following to our WeaponTableViewElements.j file:
The following will be added to setObjectValue of each indicated object class:

WeaponNameCellView:
    var stringValue = [aWeapon name],
        textField = [ViewHelper getTextDisplay:stringValue outerFrame:[self frame]];
    [self addSubview: textField];

DamageTotalCellView:
    var damageValue = [valueObject value],
        textValue = Nil;
    if( damageValue ){
        textValue = damageValue.toString();
    }else{
        var skill = [valueObject skillName],
            modifier = [valueObject modifier];
        textValue = "";
        if( skill ) textValue = skill;
        if( modifier ){
            if( modifier < 0 ){
                var temp = modifier * -1;
                textValue = textValue + " - " + temp.toString();
            }else{
                textValue = textValue + " + " + modifier.toString();
            }
        }
    }
    var textField = [ViewHelper getTextDisplay:textValue outerFrame:[self frame]];
    [self addSubview:textField];

AttackRollCellView:
    var testDiceNum = [diceRoll testDice],
        bonusDiceNum = [diceRoll bonusDice],
        modifier = [diceRoll modifier],
        textValue = Nil;
    if( testDiceNum || bonusDiceNum ){
        textValue = "";
        if( testDiceNum ) textValue = textValue + testDiceNum.toString() + "D";
        if( bonusDiceNum ){
            if( textValue != "" ){
                textValue = textValue + " + ";
            }
            textValue = textValue + bonusDiceNum.toString() + "B";
        }
        if( modifier ){
            if( modifier < 0 ){
                var temp = modifier * -1;
                textValue = textValue + " - " + temp.toString();
            }else{
                textValue = textValue + " + " + modifier.toString();
            }
        }
    }else{
        var skill = [diceRoll skillName],
            specialty = [diceRoll specialtyName],
            modifier = [diceRoll modifier];
        textValue = "";
        if( skill ) textValue = skill;
        if( specialty ){
            if( textValue != "" ){
                textValue = textValue + " + ";
            }
            textValue = textValue + specialty + " Specialty";
        }
        if( modifier ){
            if( modifier < 0 ){
                var temp = modifier * -1;
                textValue = textValue + " - " + temp.toString();
            }else{
                textValue = textValue + " + " + modifier.toString();
            }
        }
    }
    var textField = [ViewHelper getTextDisplay:textValue outerFrame:[self frame]];
    [self addSubview:textField];
 Reload your application and see the following:
Our application on first look is exactly the same as our previous incarnation in the last tutorial.  However, on closer looks under the hood, we see that the Model objects no longer have anything to do with the display of data.  They provide data as strings, numbers and other objects, and from there, our view objects grab the data that is needed and orient it for display.

For our experiment, we pass a Weapon object to the first column, a SkillBasedData object to the second column, a DiceEquation object to the third column and a CPArray object to the fourth column.  Because our custom views handle the processing, we just as easily could pass the weapon to every column cell and let the cell extract from the weapon what is needed.  It is in our dataSource that we get to choose how and what we are going to communicate with the view.

If you read the code very closely, you will see there is far too much code to just display what we are showing here.  In fact, the damage and the attack views process their objects twice.  First, they check the objects to see if one set of values can be retrieved, and on failure, they default to displaying a generic text representation that matches our previously expected text.  This is one step closer to what we were alluding earlier in this tutorial.  Later, we are going to learn to use the delegate function to customize displays even further.  One table will display in our standard format, and the second table will display data differently as a result of the delegate.

In fact, we have already alluded to how we would add images and other effects to the inside of a single table cell, and exploring delegates sounds like fun, so let's do that now, and we will add visual character to our table in a few moments.

One more important note!  If you attempt to move the columns, you will notice that the table calls setObjectValue on the existing view, which means that the view overwrites itself, which does not look good.  As a result, we must be cognizant when building our view to always ensure that we start with a fresh view palate.  I do this by calling [self setSubviews:[]].  For this small application, there is nothing wrong with this behavior.  Ideally, you can save a lot of processing steps by reusing the text fields that are already set; save a reference to the items that will change, and if they exist already, just change the text values other visual references of those specific items instead of rendering the view all over again.

Using Delegates

In the very first part of this tutorial, when we reviewed Mr. Connor Denman's work, I noted that he sets a delegate for his table, but he never uses it, and I further claimed that the line of code could be removed and there would be no effect to his work.  In fact, you will see if you review what we have done so far, we have not once used a delegate for our table view...until now.

In Cocoa, we are told that a delegate can be used to control the behavior of the table.  The data view controls the view cell that the table places, and the delegate changes the way that the table performs various functions.  What I aim to do is to change our table view so that we can link it to a character's attribute list and change how our special cells display.

Ultimately, if the table is linked to an attribute list, we will see a more practical display of information.  For the damage, we will see a single number reflecting the amount of damage performed, and for the attack roll, we will see the number of dice and bonus dice required.  These new values will be based on the character's specific values, and if the character list is changed or if the list is pointed to a new list, then the values change.  Similarly, if the link is removed, the table reverts to the generic display we see now.

For a full list of delegate functions, please see the post titled "CPTableView: A Ponderings and Guesses Regarding Delegate Functions".  We are going to make use of a function called tableView:willDisplayView:forTableColumn:row:

Create a file called Delegate.j and insert the following code:
@import <Foundation/Foundation.j>
@implementation TableEnhancer : CPObject
{
    CharacterProfile aCharacter @accessors;
}
-(void)tableView:(CPTableView)aTable willDisplayView:(id)aCell forTableColumn:(CPTableColumn)aColumn row:(CPInteger)aRowIndex
{
    if( [aCell respondsToSelector:@selector( setAttributeList: ) ] ){
        [aCell setAttributeList: [aCharacter attributes]];
    }
}
@end
Also, we need to make some small changes to our DamageTotalCellView and our AttackRollCellView classes.  Make the following changes in WeaponTableViewElements.j:

  1. change the function name from setObjectValue: to rebuildView: in the two classes indicated above: DamageTotalCellView and our AttackRollCellView.
  2. Add the line [self setSubviews:[]]; to the beginning of each of your new rebuildView: methods.
  3. In the base class, add the function setObjectValue and setAttributeList as follows:

-(void)setObjectValue:(id)anObject
{
    aSavedObject = anObject;
    var specialPath = NO;
    if( [anObject respondsToSelector:@selector( setAttributeList: )] ){
        specialPath = YES;
        [anObject setAttributeList:attributeList];
    }
    [self rebuildView:anObject];
    if( specialPath ){
        [anObject setAttributeList:Nil];
        //clear the attribute list, just in case the value is being provided to multiple sources.  Probably not necessary, but what the heck.
    }
}
-(void)setAttributeList:(AttributeList)aList
{
    //override accessor function
    attributeList = aList;
    [self setObjectValue: aSavedObject];
}
To the AppController.j file, make the following changes:

1. Add the following to the import section:
@import 'Delegate.j'
@import 'CharacterProfile.j'
2. Somewhere after tableView is defined in applicationDidFinishLaunching, add the following:
    var aDelegate = [[TableEnhancer alloc] init],
        aCharacter = [[CharacterProfile alloc] init];
  
    [aDelegate setACharacter:aCharacter];
    [tableView setDelegate:aDelegate]; 

Now, reload your project, take a look at what you have gotten.  Then edit AppController.j and comment out the line "[aDelegate setACharacter:aCharacter];".  Reload the page again.  You should see the following in each of the page reloads:

When a character is assigned to our Delegate, the delegate changes the table by filling in character specific values.
When the Delegate has no Character Profile, it makes no changes and the generic information is displayed.
Amazing!  Using the delegate functions, we are able to make the information more specific to the user depending on whether a character profile is loaded.  Furthermore, we can "unload" the character profile, and our customized views handle the change perfectly by defaulting to a generic display of data.  This is all done without any change to our view or to our data.  It is just how the view is designed to operate as a default behavior.

When I change the default values for affected attributes or specialties, I change the resulting table.  Play with CharacterProfile.j using the generic function data to interpret the Attribute and Specialty names.  I'm going to change several values, and reload the page.










There is a lot of power in this ability to combine data sources and delegates with the underlying base classes in order to change functionality.  Because the delegate is not a subclass of the tableView, we can even offer a list of delegates to our user so that the user can choose a more custom view simply by selecting a delegate preference.  Of course, our user will not know that is what he or she is doing, but for us, it means that we can free ourselves from redesigning tables every time that we need a slightly altered view.

Getting rid of dead space

Our CPTableView is getting closer and closer to a decent looking display.  I have not yet found a way to use the delegate to effectively cause a dynamic resizing of the columns and rows, which will be important as we play with images and other aspects that will go beyond the tables row and column width.  However, during that search I did stumble on two settings that help to fill the scroll area with column, so that we don't have the dead space to the right.

Calling the function setColumnAutoresizingStyle allows us to set one of three style settings.  The first is the default, so we will not cover it, the other two are CPTableViewFirstColumnOnlyAutoresizingStyle and CPTableViewLastColumnOnlyAutoresizingStyle. Pass either of these constants to the table view via the function listed, and you will expand either the first or last column to fill the entirety of the space so that the table now looks like this:
The Qualities column is now stretched to fill the entire scroll area.  Note, the stretch happens regardless of whether the underlying views are clipped or not.  These styles do not cause all columns to find an optimum width to display the most unclipped content possible.

Adding Some Graphics

We have done a lot with our tables, but so far, our table still just contains string data, and it looks very plain. What if I wanted to add buttons or images or some other view item to the cell area? How to I ensure that the extra image data or control data is not clipped by the cell's div element?

I have already shown you that we can create custom views, and on previous tutorials, you have seen that with a view, we can add as many subviews as we like to fully customize our area within the indicated frame or content bounds.

If you downloaded the starter code at the beginning of this tutorial, then you already have the images that we will use, which I modified from icons I found on Deviant Art and designed by user 7Soul1. These are not icons I would use in a personal design, but they meet our needs perfectly, and they are not bad at all.

Let's make some changes now:

1. In AppController.j, add the following somewhere between the point that the columns are initialized and the point where they are added to the tableView:
    [damageColumn setWidth:50];
    [attackColumn setWidth:70];
    [qualityColumn setWidth:150];
    [tableView setRowHeight: 50];
2. Let's alter our DamageTotalCellView in WeaponTableViewElements.j, we keep the bulk of the code, but there is sufficient changes that I will just add the whole implementation here:
@implementation DamageTotalCellView : AttribListBasedDataView
{
    Weapon aWeapon;
}
-(id)init
{
    self = [super init];
    if( self ){
        aWeapon = Nil;
    }
    return self;
}
-(void)setObjectValue:(id)weapon
{
    if( [weapon class] == "Weapon" ){
        aWeapon = weapon;
        [super setObjectValue:[weapon damage]];
    }else{
        [super setObjectValue:weapon];
    }
}
-(void)rebuildView:(id)valueObject
{
    [self setSubviews:[]];
    var damageValue = [valueObject value],
        textValue = Nil;
    if( damageValue ){
        textValue = damageValue.toString();
    }else{
        var skill = [valueObject skillName],
            modifier = [valueObject modifier];
        textValue = "";
        if( skill ) textValue = skill;
        if( modifier ){
            if( modifier < 0 ){
                var temp = modifier * -1;
                textValue = textValue + " - " + temp.toString();
            }else{
                textValue = textValue + " + " + modifier.toString();
            }
        }
    }
    var textField = [ViewHelper getTextDisplay:textValue outerFrame:[self frame]];
    if( damageValue ){
        //lets put an image here to make our number look less lonely.
        var imageView = [self getImageForItem:aWeapon];
        if( imageView ){
            var imageFrame = [imageView frame],
                rightImageEdge = imageFrame.origin.x + imageFrame.size.width,
                leftTextEdge = rightImageEdge + 5,
                textFrame = [textField frame];
            textFrame.origin.x = leftTextEdge;
            [textField setFrame:textFrame];
            [self addSubview:imageView];
        }
    }
    [self addSubview:textField];
}
-(CPView)getImageForItem:(Weapon)weapon
{
    if( !weapon ) return Nil;
    var attackRoll = [weapon attack],
        primarySkill = [attackRoll skillName];
    if( !primarySkill ) return Nil;
    var filePath = @"Resources/" + primarySkill + @".png",
        image = [[CPImage alloc] initByReferencingFile:filePath size:CGSizeMake(35,35)],
        imageView = [[CPImageView alloc] initWithFrame:CGRectMake(10,15,20,20)];
    [imageView setImage:image];
    return imageView;
}
@end 
3.  Lastly, let's change our data source so that it provides the whole weapon for the damage column instead of just the damage attribute.  In WeaponTabledataSource.j we change our switch block to the following:
switch(colId)
{
case 'Weapon':
     return weapon; //Weapon
case 'Training':
     return [weapon training]; //CPInteger
case 'Attack':
     return [weapon attack]; //DiceEquation
case 'Damage':
     return weapon; //Weapon
case 'Quality':
     return [weapon quality]; //CPArray
default:
     return "ERROR!";
}
Reload your project and find the following:
Our new view scrolled to the bottom and one row selected.
The changes we made to AppController really were gratuitous.  I wanted to show you how to change the size of each row, and also that we can define the width of the columns upfront.  By doing this, we reduce the wasted space and make the data appear much more natural in its positions.

To the damage cell and the data provider we made some other adjustments.  I wanted to make that damage number look a little less lonely, so I added a simple graphic, which changes based on the primary skill of the attack roll for the weapon selected.  To do this, the cell needs to have access to the weapon, and not just the damage value, so that it can access both the damage and the attack attributes.  In this example, I use a sword for "Fighting" skill and a bow for the "Marksmanship" skill.  In retrospect, the "Fighting" skill would probably have better been served by an image of a fist, because it looks strange to see a sword on a line attributed to a non-edged weapon like the "Whip".  Or, even better yet, I could have a unique image for each weapon and instead of showing the primary skill, I could have just shown an image of the weapon itself.  The point is not what I chose to display, but that I have FULL control over my table cells and I can build whatever it is that I desire using these tools.

Some people may wonder why there is only three rows in my captured image.  Don't worry! The four rows are all present and accounted for.  The new row height was too large to allow all the rows to display within the scroll view, so I had to scroll to the bottom of the list to display the bow and the sword icons together.  The fourth weapon is above the scroll view forming what appears to be a margin at the top of the table; it's not a margin; it's just the bottom of a row that is clipped.

There is much more to learn about table views, for example, we have not looked at handling selection events, turning on multiple selection, drag-n-drop event handling and much, much more.  However, I hope that this has been sufficient to at least get you started on your exploration.  We will continue to add more notes on these objects and views as I have a need to explore them further, but for the time being, I have spent enough of my time on this particular view element and will move on to another aspect of our Cappuccino education.

You can download the final code using git with the following URL:

      https://github.com/JaredClemence/CPTableView-Tutorial-Part-4---Final-Code.git

One Last Thing - Reloading Data

After posting this article, I realized that there is so much that was not yet covered.  On one of my work projects, I could not figure out why the table would not update data as the data was changed.  The error turned out to be with the programmer, who updated the wrong data object (thus the table had nothing to change).

Regardless what caused the error, it made me realize that through the course of this tutorial, we never once addressed forcing the table to reload the data.  Many CPTableView functions call various other functions within the table to cause it to update its view.  If ever you have a need to redisplay the data, you can call "[yourTableVariable reloadData];".  This method call will cause the table to fetch all records again, lay out the table again, and call the delegate functions again.  There is no need to use methods like setNeedsLayout or setNeedsDisplay:YES on the table when you are using the reloadData method.

I hope this information is helpful.  Have fun with your tables, and if you have any great examples or lessons to share, please do so in the comments below.