ChartPlotter: Filtering

This entry is part 6 of 7 in the series ChartPlotter

Note: All code/live demo can be found at ChartPlotter

This app uses a search to filter the objects in the listview. When a search is typed all the columns of each row of objects are checked to see if there is a match. If so, that entire row is kept in the view. In order to provide a second layer of filtering, when a search is typed the filterbar is lowered. This allows a user to search only within a certain column of each row.

When a search is typed the searchChanged: function is called. When a new filterbar button is clicked filterBarSelectionDidChange: is called. These are target-action calls. searchChanged: is set in AppController and filters the listview by the user input search, but what is interesting is that filterBarSelectionDidChange is actually being set within FilterBar.j via the delegate variable:

1
2
3
4
5
6
7
8
9
 ...snippet from initWithFrame:...
          [thisRadio setTarget:self];
          [thisRadio setAction:@selector(filterBy:)];
...end snippet...
 
- (void)filterBy:(id)sender
{
    [delegate filterBarSelectionDidChange:self];
}

Therefore, even though this function is a delegate, it still gets an (id)sender as a variable passed to it. Note: the id type in Cappuccino is a pointer, so (id)sender is a pointer to the variable that triggered the action. Let’s see what these functions do:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
- (void)searchChanged:(id)sender
{
    if (sender)
        searchString = [[sender stringValue]  lowercaseString];
 
	if(searchString){
		objsToDisplay = [];
		var count = 0;
		for(var i=0;i < [objs count];i++)
			if([self matchFound:objs[i] withString:searchString]){
				objsToDisplay[count] = objs[i];
				count++;
			}
		[[CPNotificationCenter defaultCenter]
			postNotificationName:showFilterBarNoti object:nil];
		[[CPNotificationCenter defaultCenter]
			postNotificationName:reloadTableNoti object:nil];
	}
	else{
		objsToDisplay = objs;
		[[CPNotificationCenter defaultCenter]
			postNotificationName:reloadTableNoti object:nil];
		[[CPNotificationCenter defaultCenter]
			postNotificationName:hideFilterBarNoti object:nil];
	}
}
- (void) filterBarSelectionDidChange:(id)sender
{
	selFilter = [sender selectedFilter];
	[self searchChanged:nil];
}

filterBarSelectionDidChange: sets the global selFilter variable which holds the column tag to filter by. Initially this is 0, which corresponds to the “All” column. Then it calls searchChanged:, which is called directly by the searchfilter.

searchChanged: initially checks if there is a sender, because if it is called by filterBarSelectionDidChange: there will be no sender. Then it checks is a searchstring is given, there will be no search string in the case of a searchfilter clear. If there is filter it loops through objs and adds all of those with a match to objsToDisplay, then reloads the table. If not, objsToDisplay just becomes all the objs. Also, this is the function that lowers/raises the filterbar. This function uses matchFound:withString to test if a row or row,col element matches the string:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (BOOL)matchFound:(CPDictionary)aDict withString:(CPString)aString
{
	var isFound = NO;
	if(selFilter){
		if([aDict objectForKey:[columnHeaders objectAtIndex:selFilter-1]] != [CPNull null])
			if([[aDict objectForKey:[columnHeaders objectAtIndex:selFilter-1]] lowercaseString].match(aString))
				isFound = YES;
	}
	else
		for(var i=0;i < [aDict count];i++)
			if([[aDict allValues] objectAtIndex:i] != [CPNull null])
				if([[[aDict allValues] objectAtIndex:i] lowercaseString].match(aString))
					isFound = YES;
	return isFound;
}

This function checks if there is a selectionFilter. If so, then it only checks if the element in that column has a match, if not it checks all the columns for a match.

Filed under: Cappuccino, ChartPlotter

ChartPlotter: Viewing Details

This entry is part 7 of 7 in the series ChartPlotter

Note: All code/live demo can be found at ChartPlotter

This app has 4 ways of viewing the details of the objects

  1. View details in the bottom right hand view for selected(one-click) object
  2. View details in a separate window for selected(double-click) object
  3. View details in a separate window for a selected collection of plots(highlight, then click “Plot Selected”)
  4. View details of all the plots listed in a separate window(click “Plot All”)

In this case the details come from a website within a webview, but you could generate the details however you like, ie the details could be images or objj generated content. Let’s start with #1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (void)tableViewSelectionDidChange:(CPNotification)aNotification
{
	if(groupView === [aNotification object]){
		var i = [[[aNotification object] selectedRowIndexes] firstIndex];
		[listDS getList:[[groupDS objs] objectAtIndex:i]];
		[searchField setStringValue:@""];
		[self hideFilterBar:nil];
	}
	else{
		var i = [[[aNotification object] selectedRowIndexes] firstIndex];
		if(i > -1){
			var row = [[listDS objsToDisplay] objectAtIndex:i];
			[webView setMainFrameURL:@"php/tradeReport.php?group="+[row objectForKey:groupColHeaderName]+"&file="+[row objectForKey:"Name"]];
		}
	}
}

The CPTableView class automatically create a notification for tableViewSelectionDidChange:, all wee need to do is implement it. Remember though that there are two tableviews in this app. One for the listview and one for the groupview. Line 3 checks which object is sending the notification, if it is groupview then the function tells listDS to get a new list from the server of only the files from within the selected group. You could, however, just filter the objects without a server call if you have a way to tell which group each file belongs to. This app does, but I chose to make a new server call because the backend is dynamic, and this forces a sort-of auto refresh every time the group changes. Also the call is generally fast enough that the user can’t even tell. Just be aware of my choice, so you can make your own. After it sends the call to get a new list, it resets the searchfield and raises the filterbar, again a choice. Now let’s move on to #2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- (@action)openDetailInNewWindow:(id)sender
{
	var newWindow = [[CPWindow alloc] initWithContentRect:CGRectMake(100, 100, 800, 600) styleMask:CPTitledWindowMask|CPClosableWindowMask|CPMiniaturizableWindowMask|CPResizableWindowMask];
	[newWindow setMinSize:CGSizeMake(300, 300)];
 
	var platformWindow = [[CPPlatformWindow alloc] initWithContentRect:CGRectMake(100, 100, 800, 600)];
	[newWindow setPlatformWindow:platformWindow];
	[newWindow setFullBridge:YES];
 
	var contentView = [newWindow contentView],
		webViewWin = [[DetailsWebView alloc] initWithFrame:[contentView bounds]];
 
	[webViewWin setAutoresizingMask:CPViewWidthSizable|CPViewHeightSizable];
	[contentView addSubview:webViewWin];
 
	[newWindow orderFront:self];
	[newWindow setDelegate:webViewWin];
 
	var i = [[tableView selectedRowIndexes] firstIndex];
	var row = [[listDS objsToDisplay] objectAtIndex:i];
	[webViewWin setMainFrameURL:@"php/tradeReport.php?group="+[row objectForKey:groupColHeaderName]+"&file="+[row objectForKey:"Name"]];
}

There is alot going on this function, and it is a great example of the power of Cappuccino. First we create a newWindow to hold the webView with the details. Remember, this is a content window, not a platform window or “bridge” in Cappuccino speak. So we then make a platformWindow to contain the newWindow. In lines 7,8 we are placing the newWindow in the platformWindow rather than the current bridge, then we are telling the window to take on the full size of the bridge. Now we have a new bridge with a window, so let’s place some content in this newWindow. We create a new webViewWin in line 11, then add it to the newWindow. Then we just bring the window up in line 16. The last step, of course, is to set the URL of the webViewWin. This is unique to this app, but in short my php page accepts the group and file names as parameters to look up the plot I need. So I am just creating a URL based on the group/file in the selected row. Finally, let’s tackle #3,#4 together:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
- (@action)openDetailsInNewWindow:(id)sender
{
	var platformWindow = [[CPPlatformWindow alloc] initWithContentRect:CGRectMake(0, 0, 600, 800)];
	var newWindow = [[CPWindow alloc] initWithContentRect:CGRectMakeZero() styleMask:CPBorderlessBridgeWindowMask];
	[newWindow setPlatformWindow:platformWindow];
	[newWindow setFullBridge:YES];
	[newWindow orderFront:self];
	var contentView = [newWindow contentView],
		bounds = [contentView bounds];
 
	var detsView = [[CPCollectionView alloc] initWithFrame:bounds];
	[detsView setAutoresizingMask:CPViewWidthSizable];
	[detsView setMinItemSize:CGSizeMake(collViewWidth, collViewHeight)];
	[detsView setMaxItemSize:CGSizeMake(collViewWidth, collViewHeight)];
 
	var itemPrototype = [[CPCollectionViewItem alloc] init],
            detView = [[DetailsWebView alloc] initWithFrame:CGRectMakeZero()];
 
    [itemPrototype setView:detView];
    [detsView setItemPrototype:itemPrototype];
 
	var scrollViewDetails = [[CPScrollView alloc] initWithFrame:bounds];
	[scrollViewDetails setDocumentView:detsView];
	[scrollViewDetails setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable];
	[scrollViewDetails setAutohidesScrollers:YES];
	[contentView addSubview:scrollViewDetails];
 
	var urls = [];
	if([sender title] == showAll){
		for(var i=0;i < [[listDS objsToDisplay] count];i++){
			var row = [[listDS objsToDisplay] objectAtIndex:i];
			urls[i] = @"php/tradeReport.php?group="+[row objectForKey:groupColHeaderName]+"&file="+[row objectForKey:"Name"];
		}
	}
	else{
		var indices = [tableView selectedRowIndexes];
		var index = [indices firstIndex];
		for(var i=0;i < [indices count];i++){
			var row = [[listDS objsToDisplay] objectAtIndex:index];
			urls[i] = @"php/tradeReport.php?group="+[row objectForKey:groupColHeaderName]+"&file="+[row objectForKey:"Name"];
			index = [indices indexGreaterThanIndex:index];
		}
	}
	[detsView setContent:urls];
}

There are quite a few Cappuccino idiosyncrasies here, so pay close attention to the details. Up to line 9 everything is pretty much the same as before, just creating a new bridge/window, but now comes the good stuff. In order to display multiple plots properly I have chosen to place them all inside a CPCollectionView. This is a choice, and the reason for it is the positioning power of a collectionview. The collectionview can organizes the objects into columns, and as the browser expand it will automatically add or remove columns. This allows the user to see the most possible plots for his/her resolution/bridge size without any more effort on my part. In lines 11-14 we are creating the collectionview, making it autoresize properly, and setting the size of each object. In lines 22-26 we create a scrollview to place the collectionview in so the page scrolls with nice bars.

In lines 16,17 we create a prototype for the items in the collectionview. This is tricky because this is not just a way of telling the collectionview what is going inside it, but rather a way of telling the collectionview what to create each time an object is added. In this example we use a webview as a prototype. This means we cannot fill the collectionview with webviews because it does that for us. Rather, we need to just tell the collectionview how many webviews we want, then set the URLs for each webview. So what we do is create an array of the URLs we want. Then we set the content of the collectionview to this array. The content refers to what makes each webview different, not the views themselves, in this case it is obviously the URLs. What setContent does is create a prototype based object for each object in the array. Then it calls setRepresentedObject:(id) once for each item in the collectionview. The question is, where is setRepresentedObject:(id)? Well that is a function that the collectionview assumes is within the class of the prototype object. But ofosho, there this function does not exist in CPWebView? Correct, we need to subclass CPWebView, which we did with DetailsWebView. I put this subclass within the AppController.j file because it is so short and not very useful again, but you can put it in a separate file. All setRepresentedObject: does is set the mainFrameURL of the webview. Take a second and explore the code, all this should click.

One last interesting note is the creation of the URL array. In the case of “Plot All” we just traverse all of objsToDisplay and create a URL for each object, but how do we traverse just the selected objects? Cappuccino has a nice class CPIndexSet that is just a collection of selected indices. So we set the first index to the firstIndex in the CPIndexSet. Then on each iteration from 0 to the amount of indices we have we set the index to the next index in the list using, indexGreaterThanIndex:index, where index is our current index. I am not sure if CPIndexSet is sorted in general, but in this case it is because table rows are ordered. If there is a better way, let me know, but this works just fine.

Well that concludes the posts on the ChartPlotter that I can think would be useful. However, if there are alot of questions or interest, I will gladly add more, just ask! I hope that at this point you are comfortable with Cappuccino and ready to start creating your own apps.

-O

PS, Let me know if you use/fork this app!

Filed under: Cappuccino, ChartPlotter, ,

ChartPlotter: Notifications

This entry is part 5 of 7 in the series ChartPlotter

Note: All code/live demo can be found at ChartPlotter

At this point I won’t talk too much as to what a notification is, but rather just let you know what each of the ones in ChartPlotter do. Let’s look at the string definitions in globals.j:

1
2
3
4
5
addColumnsNoti = @"AddColumnsNotification";
reloadTableNoti = @"ReloadTableNotification";
reloadGroupsNoti = @"ReloadGroupsNotification";
showFilterBarNoti = @"ShowFilterBarNotification";
hideFilterBarNoti = @"HideFilterBarNotification";

AddColumns: when the program starts there are no columns. After the initial data is retrieved the columnheaders are parsed from the JSON. This parseing is done within ListDataSource.j, so we need some way to tell AppController that the columns are ready to be created. This is only done on the first data retrieval, so the columns should not change between JSON strings.

ReloadTable: ListDataSource is in charge of sorting and gathering the data. This notification tell AppController that there is new data and to refresh the table.

ReloadGroups: Same as ReloadTable but the groupview gets reloaded.

Show/HideFilter: These notifications tell AppController to either lower or raise the filterbar. When a search is entered, show filter is sent, and when the search is canceled or cleared, hidefilter is sent.

The most interesting notification function is addColumns:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- (void)addColumns:(CPNotification)aNotification
{
	for(var i=0;i < [[listDS columnHeaders] count];i++){
		var headerKey = [[listDS columnHeaders] objectAtIndex:i];
		var desc = [CPSortDescriptor sortDescriptorWithKey:headerKey ascending:NO];
		var column = [[CPTableColumn alloc] initWithIdentifier:headerKey];
		[[column headerView] setStringValue:headerKey];
		[column setWidth:140.0];
		[column setEditable:YES];
		[column setSortDescriptorPrototype:desc];
		[[column headerView] setBackgroundColor:headerColor];
		[tableView addTableColumn:column];
	}
 
    [scrollView setDocumentView:tableView];
	[tableView reloadData];
	[self createFilterBar];
}

This function iterates through the array of columnHeaders that we initialized in ListDataSource.j and creates a new column for each header. This new column is then added to the tableview. This method allows us to accept any JSON string, and more importantly provides much less code for us to manage as compared to hard-coding each column. After the columns are added to the tableview, the tableview is then added to the scrollview. This begs the question, why not add the tableview to the scrollview when first initializing the scrollview? Glad you asked, this is actually a workaround b/c for whatever reason, if we don’t do this the columnheader row does not show up until a window resize occurs. A discussion of this was started on the mailing list, but not much came of it. After this, the tableview is reloaded in order for it to reflect our changes, and finally we can create the filterbar:

1
2
3
4
5
6
- (void)createFilterBar
{
	filterBar = [[FilterBar alloc] initWithFrame:CGRectMake(0, 0, 400, 32) colHeaders:[listDS columnHeaders]];
    [filterBar setAutoresizingMask:CPViewWidthSizable];
    [filterBar setDelegate:listDS];
}

Now that we know we have columnHeaders ready, we can use the initWithFrame:colHeaders: function. Most of this class is just drawing the filterbar, but let’s take a look at the radio button creation:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- (CPArray)createRadioButtons:(CPArray)titles
{
	var tempRadio = [CPRadio radioWithTitle:allName];
	[tempRadio setTag:0];
	[tempRadio setFrameOrigin:CGPointMake(minFilterBarGap, minFilterBarY)];
	[tempRadio setState:CPOnState];
	radioGroup = [tempRadio radioGroup];
 
	var radioButtons = new Array(tempRadio);
	for(var i=0;i < [titles count];i++){
		tempRadio = [CPRadio radioWithTitle:[titles objectAtIndex:i]];
		[tempRadio setRadioGroup:radioGroup];
		[tempRadio setFrameOrigin:CGPointMake(CGRectGetMaxX([radioButtons[i] frame])+minFilterBarGap, minFilterBarY)];
		[tempRadio setTag:i+1];
		radioButtons[i+1] = tempRadio;
	}
 
	return radioButtons;
}

We start by creating an initial “All” button. This is because we need to create a radio group(line 7) to contain all the groups. To do this, we must first  create a button and then use its group as a base to add the rest of the buttons to.  Then we traverse the list of headers to create a button for each header. Remember, radioButtons already contains the “All” button, which is not a header, so we need to add new buttons to the “i+1″ element of the array. Again, this function is used in place of hard coding in order to reduce code size and to be able to accept any JSON array.

Filed under: Cappuccino, ChartPlotter

ChartPlotter: Delegates

This entry is part 4 of 7 in the series ChartPlotter

Note: All code/live demo can be found at ChartPlotter

Before we continue, we need to make sure we understand what a delegate is. Thomas also has a cast on these at #4 Understanding delegates. Essentially certain classes have some functions that are not implemented, but must be. It is therefore the job of the delegate class to contain the appropriate function implementation. The best example of this is the CPTableViews that are being used to display the groups and the objects (plots in this case). Each of these tables needs to get their data from somewhere, so if you take a look at the CPTableView Class Reference you will notice that in the detailed description:

CPTableView requires you to set a dataSource which implements numberOfRowsInTableView: and tableView:objectValueForTableColumn:row:CPTableView

Basically this is saying that when the tableview is populated the class is going to call a function to get the number of rows in the table, then for each row it will call a function to get the data for that row. For this app I want to get a list of groups of the groupview, and a list of plots for the listview. To make things as modular as possible I created two classes, ListDataSource.j and GroupDataSource.j. These were written to be as generic as possible, so feel free to use them as you like. ListDataSource makes a call to my server and accepts a JSON encoded string of items. If you notice in the Issues app all the columns are generated before hand, however in ChartPlotter the columns are generated based on the JSON items. Therefore, you can send any JSON string you like and the columns should be populated properly. Here is an example string:

1
2
3
4
5
6
7
[{"Name":"blank","Folder":"ofosho","Owner":"3987","Group":"170","Size":"0","Modified":"November 30, 2010, 11:56 pm","Permissions":"-rw-r--r--"},
"Name":"test","Folder":"lol","Owner":"3987","Group":"170","Size":"0","Modified":"December 1, 2010, 7:33 pm","Permissions":"-rw-r--r--"},
"Name":"stock","Folder":"testing","Owner":"3987","Group":"170","Size":"54047","Modified":"November 29, 2010, 11:06 pm","Permissions":"-rw-r--r--"},
"Name":"port","Folder":"testing","Owner":"3987","Group":"170","Size":"10362","Modified":"November 29, 2010, 11:06 pm","Permissions":"-rw-r--r--"},
"Name":"blank2","Folder":"newfolder","Owner":"3987","Group":"170","Size":"0","Modified":"December 1, 2010, 1:01 pm","Permissions":"-rw-r--r--"},
"Name":"blank3","Folder":"newfolder","Owner":"3987","Group":"170","Size":"0","Modified":"December 1, 2010, 1:01 pm","Permissions":"-rw-r--r--"},
"Name":"blank1","Folder":"newfolder","Owner":"3987","Group":"170","Size":"0","Modified":"December 1, 2010, 1:01 pm","Permissions":"-rw-r--r--"}]

The php to generate this string is getJSONList.php found in the php folder. The php is going into the directory with my plots and created a JSON array with the data about each file. You can use whatever backend code you like as long as the it returns a JSON string formatted with column:field arrays as in the example. For the GroupDataSource, I created a predefined column “group” because I know I only need one column here. This class expects just a one-level JSON encoded array with groups as follows:

1
["All","lol","newfolder","ofosho","testing","testing0","testing1","testing2","testing3","testing4","testing5","testing6","testing7","testing8","testing9"]

This can be any array of items, it does not matter. In this case it is the list of folders containing my files, and can be seen in getJSONGroup.php.

Let’s take a look at how ListDataSource.j works. If you understand this, GroupDataSource.j should be easy enough to figure out. Let’s start with the implementation:

1
2
3
4
5
6
@implementation ListDataSource : CPObject
{
	JSObject objs;
	JSObject objsToDisplay @accessors;
	CPArray columnHeaders @accessors;
}

Here we declare 3 variables. objs, to hold the full list of plots;objsToDisplay, to hold the list of filtered plots;columnHeaders, to hold the list of headers we need to tell AppController.j to populate. Take note of the @accessors. These are well explained at Synthesizing Accessor Methods, so I won’t explain them further, but take a look so you know what is going on. Let’s go to the init code:

1
2
3
4
5
6
7
8
9
10
- (id)init
{
	if(self = [super init])
	{
		objs = [];
		objsToDisplay = [];
		[self getList:@""];
	}
	return self;
}

Here we are initializing the variables and calling getList with a null group because we want all the plots, but what is interesting is line 3. Here we are telling this class to call the constructor of the parent class (CPObject). We don’t have to do this, but without doing it some memory issues can occur. Also notice that init needs to return the object we just created, so we return self. So now we have a new ListDataSource object, let’s put some data in it via an Ajax request:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
- (void)getList:(CPString)aGroup
{
	if(aGroup == "All")
		aGroup = "";
 
	var request = [CPURLRequest requestWithURL:requestListURL+"?group="+aGroup];
	[[CPURLConnection alloc] initWithRequest:request delegate:self];
}
- (void)connection:(CPURLConnection)aConnection didReceiveData:(CPString)data
{
	objs = [];
    var JSONLists = CPJSObjectCreateWithJSON(data);
    // loop through everything and create a dictionary in place of the JSObject adding it to the array
    for (var i = 0; i < JSONLists.length; i++)
        objs[i] = [CPDictionary dictionaryWithJSObject:JSONLists[i] recursively:NO];
 
	objsToDisplay = objs;
 
	if([objs count] && !columnHeaders){
		columnHeaders = [objs[0] allKeys];
		[[CPNotificationCenter defaultCenter]
			postNotificationName:addColumnsNoti object:nil];
	}
	else{
		[[CPNotificationCenter defaultCenter]
			postNotificationName:reloadTableNoti object:nil];
	}
}
- (void)connection:(CPURLConnection)aConnection didFailWithError:(CPString)error
{
    alert(error) ;
}

We start by creating a request in line 6. This request is just the URL we want to send our request to. Unlike other frameworks there is no parameters list, so we create the full path ourselves. Then in line 7 we actually make the connection to the server using the request we just made. We also set the connection’s delegate to self meaning that we plan to have the delegate functions from CPURLConnection implemented within this class, and as you can see from the next two functions we do. In connection:didFailWithError: we are handling an error response from the server(just alerting the user of this). In connection:didReceiveData: we are handling a proper response from the server.  First we reset objs in line 11, then in line 12 we create a javascript array from the JSON string using the built in function CPJSObjectCreateWithJSON(). Just to make things easier for us in the future we convert this array into an array of CPDictionaries and place it into objs. Then if columnHeaders did not already exist we send the addColumnsNoti. This will notify AppController to populate the columns and reload the table. If the columns have already been populated, then we just reload the table by sending AppController the reloadTableNoti. When the table gets loaded/reloaded the delegate functions get called, so let’s look at their implementation. Remember, we will eventually tell ListView to use this class as the delegate, so we must have the delegate functions implemented within ListDataSource:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- (int)numberOfRowsInTableView:(CPTableView)tableView
{
	return [objsToDisplay count];
}
- (id)tableView:(CPTableView)tableView objectValueForTableColumn:(CPTableColumn)tableColumn row:(int)row
{
	var key = [tableColumn identifier];
	return [objsToDisplay[row] objectForKey:key];
}
- (void)tableView:(CPTableView)aTableView sortDescriptorsDidChange:(CPArray)oldDescriptors
{
    // first we figure out how we're suppose to sort, then sort the data array
    var newDescriptors = [aTableView sortDescriptors];
    [objs sortUsingDescriptors:newDescriptors];
 
	[aTableView reloadData];
}

First we implement numberOfRowsInTableView:, which just returns the size of the array of objects we want to display, which we created previously. Then we implement tableView:objectValueForTableColumn:row:, which will return the object at the given row and column in the objectsToDisplay list. This is why we made an array of CPDictionaries, we can just call the array at row, then get the item at the given key, so our work has paid off. Lastly, if we have implemented sortDescriptors (a way to sort the column on header click) we need to also implement the delegate that is called when the descriptors change. Here we just tell the array to sort itself, then reload the table. Take a look at addColumns in AppController to see how CPSortDescriptors are initialized. The remaining functions will be covered in Filtering.

If I did my job, you should have a firm grasp of delegates and data sources now, so try looking through GroupDataSource.j and seeing if you can make sense of it. It should be clear now.

Filed under: Cappuccino, ChartPlotter,

ChartPlotter: Initialization

This entry is part 3 of 7 in the series ChartPlotter

Note: All code/live demo can be found at ChartPlotter

After Cappuccino App is finished loading, the first function it notifies is applicationDidFinishLaunching, located in the AppController.j file. Let’s take a look at that code now:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
- (void)applicationDidFinishLaunching:(CPNotification)aNotification
{
	var theWindow = [[CPWindow alloc] initWithContentRect:CGRectMakeZero() styleMask:CPBorderlessBridgeWindowMask],
		contentView = [theWindow contentView];
 
	listDS = [[ListDataSource alloc] init];
	groupDS = [[GroupDataSource alloc] init];
	headerColor = [CPColor colorWithPatternImage:[[CPImage alloc] initWithContentsOfFile:[[CPBundle mainBundle] pathForResource:@"button-bezel-center.png"]]]; 
 
	[self initNotifications];
	[self createSearchField];
	[self splitPage:[contentView bounds]];
	[self createGroupView];
	[self createListView];
	[self createWebView];
 
	[self combineViews];
 
	// add vertical splitter (entire page) to contentview
	[contentView addSubview:verticalSplitter];
 
	[self createMenu];
	[theWindow orderFront:self];
}

The first thing to take note of is that objective-j does not call functions in the standard way of: myVar.fooBar(var1, var2). Instead, objj uses the following equivalent syntax: [myVar foor:var1 bar:var2]. For more on this, have a look at Objective-J Tutorial. As an example, take a look at line 3. We are initializing the main window. A more C-like version of this would be:

CPWindow theWindow = new CPWindow;
theWindow.initWithContectRectStyleMask(CGRectMakeZero(),CPBorderlessBridgeWindowMask);

Once this syntactical concept is understood objective-j becomes a lot more readable. In line 6 and 7 we are initializing the data sources for the tables. These will be discussed in detail in a separate post about the tables. Let’s move on to lines 10-18. These are all functions that are part of this class, hence the self variable. I have not found many Cappuccino apps that use this style of initialization, most just write the full initialization code directly into applicationDidFinishLaunching. This practice of having extraordinarily long functions is, however, against my religion, so I have split the initialization code into 8 serperate functions. I think this makes the code flow easier to see, and more importantly allows for more efficient debugging. Feel free to do whatever you like with your code though.

In line 10 we are initializing the notification center. In Cappuccino, for one class to communicate with another they can send notifications between themselves. Thomas has put up some nice video casts about notifications at Suit My Mind Cappuccino Casts, casts 6,7. For now, lets take a look at an example from the code:

1
2
3
4
5
	[[CPNotificationCenter defaultCenter ]
            addObserver:self
               selector:@selector(reloadGroups:)
                   name:reloadGroupsNoti
                 object:nil];

All this code is doing is calling reloadGroups (a function that takes no parameters, hence just the ‘:’) when the reloadGroupsNoti is sent. These notification variables are defined in globals.j. We’ll discuss why this is a necessary notification later, for reference though GroupDataSource.j sends this notification.

The createSearchField function initializes the view to our specification. Let’s see what it is doing:

1
2
3
4
5
6
7
8
9
10
11
12
- (void)createSearchField
{
        searchField = [[CPSearchField alloc] initWithFrame:CGRectMake(0, 10, 200, 30)];
	[searchField setEditable:YES];
	[searchField setPlaceholderString:@"search and hit enter"];
	[searchField setBordered:YES];
	[searchField setBezeled:YES];
	[searchField setFont:[CPFont systemFontOfSize:12.0]];
	[searchField setTarget:listDS];
	[searchField setAction:@selector(searchChanged:)];
	[searchField setSendsWholeSearchString:NO];
}

Most of the search field creation code is fairly self-explanatory, but what is interesting is the setTarget/Action code. This is just saying that when the “action” occurs call searchChanged:. The location of searchChanged is set by the setTarget parameter. So we are saying, when “action” occurs call searchChanged: from inside of the listDS variable. The only remaining question is, what is the “action”? This is defined by the implementing class, in this case CPSearchField. The “action” that was chosen is every time the search field changes, however if we set setSendsWholeSearchString: to YES, then the action would be when the user hits enter.

If you have understood the objj syntax and the layout of the page, you should now be able to follow the flow of the functions in applicationDidFinishLaunching to see how the page is getting initialized and laid out. If there are functions that aren’t named obviously enough, the Cappuccino documentation should be enough to get by, but you can also use the Cocoa documentation as Cappuccino is supposed to follow it almost exactly. Just replace the CP with an NS such as with NSScrollView.

The next step is adding delegates for all the objects we have created in order to populate the views with data.

Filed under: Cappuccino, ChartPlotter

ChartPlotter: Overview

This entry is part 2 of 7 in the series ChartPlotter

Note: All code/live demo can be found at ChartPlotter

The point of this app is to be able to view many different plots. Each plot is actually just a website that creates a plot based on the URL. Therefore, a better way of thinking about this app is that it is a way to view a list of groups that each contain a list of objects. When an object is in the list is clicked details about the object are shown in the bottom. When the object is double clicked the details appear in a new window. When multiple objects are selected they are shown side by side in another window. I wrote the code in hopes that it can be as generic as possible, so re-purposing this code should be relatively painless. I will get into how to do that as we go on.

For now, lets take a look at the way the page is split. For a good tutorial on layout in Cappuccino look at Cappuccino Web Framework – Automatic Layout. Essentially the cappuccino hierarchy is as follows:

CPPlatformWindow -> CPWindow -> CPView

The CPPlatformWindow is actually the browser, and this class does not have much documentaion, but you really only need it to open a new window. A CPWindow is actually a div object that will contain the views of your page. In cappuccino all the content of the page is contained in views. A page can contain many windows, or just one large window. This is very useful for creating “popups” that are actually just closable divs, or a “tools/data” container. A nice example of a window within a window can be seen in the Scrapbook part II app. The main page is the large window with the black background and editable photo. The secondary window is the Photos selection box.

Now that we know about the windows, it is time to discuss the content. In Cappuccino most everything is a view. For example, a table is a CPTableView. If you want it to scroll, you place it within a CPScrollView. Let’s take a look at the way the ChartPlotter is split up:

(this image may be dated, but still shows the general layout)

The first thing to notice is the menu bar. This is actually a pre-defined menu that Cappuccino always makes. All you need to do is tell Cappuccino to show it. The one in the above imaged has been modified in order to remove the standard buttons, and only show a custom “Plot All” function. Everything else on the page is contained in the main and only window. Take a look at the dotted lines. The red line is splitting the page the page into a left view and a right view. What is actually doing this is a CPSplitView set to vertical mode. The right view created by the vertical splitter is being split by another CPSplitView set to horizontal mode. This horizontal splitter has now split the right view into a top view and a bottom view. Getting it?

The top view is actually a table containing all the potential plots. In order to make this table we create a CPTableView. There will be more on that later, but essentially this is just a table. In order to get it to scroll, we place it into a CPScrollView. One final object in this top view is the FilterBar. This view is based on the view from the Issues app. This filter bar only appears when something is searched. In order to accommodate the bar, the scrollview must shrink. Therefore it must be contained in another view, or else the entire top view will shrink leaving us no more room. This will be explained in more detail later, just keep in mind the top view is now a parent view containing a filter bar and a scroll view(which contains the table).

The bottom view is a CPWebView, which is just a view that displays a website. If you are planning on re-purposing this app you can just have this view link to your desired pages, or you could choose to dynamically create the content when an object in the list is clicked.

As for the left view, this is actually a  CPSearchField on top of another CPTableView (within a CPScrollView). There is no splitter here, the search is placed into the left view followed by the table and they are automatically stacked.

That should be enough to give you a solid idea of what is going on. The code that brings all this together is the AppController.j. In the next post we will take a closer look at this code. Please comment as to anything that is unclear or needs revision.

Filed under: Cappuccino, ChartPlotter

Cappuccino Anyone?

This entry is part 1 of 7 in the series ChartPlotter

I was recently asked to create an app for viewing multiple plots from a directory at the same time. While I was looking for the best framework for the job I stumbled upon Cappuccino, and more importantly the Issues app developed with it. I thought that this would be a great way to create the GUI for the charts, and I began trying to write the app. Then I hit a giant wall. I have programmed for the web since I can remember, and I am a C/C++ programmer by birth, however the syntax for Cappucino is based on Objective-C, which is syntactically different then pretty much every other language. There are some good tutorials and demos for Cappuccino, but it still took quite an effort to get comfortable with everything. I finally finished the prototype for the ChartPlotter and I am now tasked with writing the documentation for it. I figure that if I am going to write the documentation anyway, I might as well make it a tutorial and potentially help out the next guy trying to learn Cappuccino. Anyway, this is the first of many posts that are going to essentially walk everyone through the source of the app, and how it works.

Please keep in mind that this app is still under construction, so some code samples that I use may change. I will try to post the commit that the code snippets come from so you can always clone my repo and revert to that version. All the necessary files to have the app fully running should be in the repo at all times.

Hope someone enjoys this, and please comment as to any improvements you think I can make.

-O

Filed under: Cappuccino, ChartPlotter