Friday, December 4, 2009

NSScrollView - problems

I have been having a strange problem scrolling in the TimeLine view. The issue is down to my understanding of coordinate spaces under "normal" (not flipped) circumstances in cocoa. The coordinate space is much as I expect - the bottom left corner is (0, 0). However when you come to NSScrollView things are a little strange. If the position of the content view also starts from the bottom left corner. This is 100% logical. What I did not count on was the effect when the content view is smaller than the NSScrollView. So, of your list, is smaller than the NSScrollView you can easily have this situation. What happens is that the bottom of the content view is placed as (0, 0) within the NSScrollView.The problem can be seen quite clearly if I expanded my evolving TimeLine so that the NSScrollView is larger than the content area. It looks as follows:

It took me a while to figure out the fix. MBTableView dealt with the problem forcing all the views to be flipped - so that (0, 0) is the top-left so it did not provide any kind of example. To get an example I downloaded the source code to CocoaTron. CocoaTron is an open source implementation of the cocoa frameworks. It is a cool project (don't let the web-site fool you it is actively developed) and the source code is there for you. You can find it here.

I pulled the CocoaTron source and had a look at their implementation of NSTableView. The solution is to grow the content view so that it is never smaller. Each time you resize the NSScrollView the NSTableView recalculates and repositions its self.

The solution:Add an override to resizeWithOldSuperviewSize that makes the content at least the size of it's superview. My resizeWithOldSuperviewSize would up looking like this:

-(void)resizeWithOldSuperviewSize:(NSSize)oldSize {

[super resizeWithOldSuperviewSize:oldSize];

[self reloadData];

}


My reloadData has also changed:

- (void)reloadData

{

.... stuff

NSRect contentRect = NSMakeRect(0, 0, _numberOfColumns * _columnWidth, _numberOfRows *_rowHeight);

contentRect.size.height = MAX(contentRect.size.height, [[self superview] bounds].size.height);

[contentView setFrameSize:contentRect.size];

.... stuff

}



Thursday, December 3, 2009

TimeLine - sketching out more

I am slowly shapping up the timeline. I am working with a scratch target in the main project and doing occasional Git commits. The commits are little more than line in the sand that I can undo to. My approach is to start simple and to refine slowly.

Basic Topology (header areas)

Starting with the basic ytopology I refined it by adding NSScrollViews.

Basic topology (with scrolling)

The final shot of this post shows some dummy content. The beginings of what will happen in the row headers is there. I have coppied the draw code of the rows into the main content view so that I can debug the drawing. I have some strange problems that I guess are related to the coordinate system that is prelevant in cocoa - which to me (cooming from QuickDraw coordinates) still feels a litte odd.


Some dummy drawing of row headers and rows

Monday, November 23, 2009

TimeLine view - first brush strokes

I have started implementing the TimeLine view - and implementing it from the ground up. My approach is to start with the basic topology and then to add flesh to the bones. The PC view has row labels of user determined width. I can kick this off in a small sample project - I should be able to get the basic structure of what I need working outside of Cello. So I started by adding a new (temporary) target to XCode and sketched out a basic splitter with two views in interface builder.

The drawRect methods of the views just fill the two rectangles with grey.

As a next step I have broken the two views into several sub views. The row header, column header and content are all need to be within NSScrollViews. So I have added those.

On from this I have started sketching out the protocol for the DataSource modeling it roughly on the DataSource in the NSTableView. My idea is to start with a protocol that is close to how I am implementing the TimeLine - initially something very much like a table. My plan is then when the basic table functionality is working to shift directions and refine the protocol so it more closely resembles the data that is presented and required by Cello as it stands.

Sunday, November 22, 2009

NSTableView vs MBTableGrid - the verdict

I have spent some time looking at NSTableView and also the open source MBTableGrid. What I have been doing is playing around with them in test applications to try and get a measure of the two approaches. In the end I have decided to write my own view from the ground up. Looking at the MBTableGrid example has given em a really good idea of what it will take to do it - and the work will not be greater from what I will need to do to get MBTableGrid customized and wrinkled into the form that I need. MBTableGrid is closer to what I need than NSTableView.

Thursday, November 19, 2009

NSTableView vs MBTableGrid - the trial

The main cello UI is comprised of three views, FrameView TimeLine and ActionView. Together these views comprise the majority of the editing functionality. My intention was to start with the FrameView. The FrameView is used to draw the the items at a given point in time. So when you sketch, say, a box you do this in the FrameView. It seemed like the most important view to start with. There is, however a really close linkage between the views - so starting with the TimeLine view seems the simplest. We shall see:

In the PC version the time line looks like this:

It is more-or-less a list of a single column with row and column headings. The row headings contain active controls - as do the rows themselves. The column heading is a has the time-ruler and is also active.

As far as implementing this in cocoa I can see two choices
  • Implement this strange list like thing myself
  • Use the cocoa NSTableView
The best course does not seem immediately obvious to me. One of the benefits of writing a blog is that when writing things you can think about them in a slightly different way. One of the things that occurred to me while writing this is that the table is not actually a table of a single column - it can be thought of as a table of many many columns. Each time interval (typically something like 1/15th of a second) can be thought of as a single column - this would seem to offer some simplifications in the implementation.

In trying to determine the best course of action I have tried to enumerate the various things that need to be done with the time line.
  • Items in the row header need to be editable.
  • Items there are buttons and popup menus in the row headers
  • Rows need to be selectable
  • Cells have items that can be added/deleted and removed
  • Some items can stretch over more than one cell
  • Items that stretch over more than one cell appear as a "whole" - they are drawn as a whole
  • There is a row (the sound track row) that does not (currently) have any editable elements.
  • The width of the row area needs to be resizable
  • The header area is not a standard table header
Looking at this I can try and figure out what the best way forward should be. Also into the mix comes MBTableGrid - a set of classes that draws a "Spread Sheet" type widget written by Matt Ball - that could be a basis for a non-list approach. The Author has solved a number of the problems that I would face - there is a separate row/column area, it supports inline editing. The code can be found here.

The NSTable approach has a few stumbling blocks:
  • Tables don't have row headers - this could be be implemented by a separate parallel - list. you just have to sync the scrolling and selection.
  • I would need a different header - fortunately the table header can be sub-classed.

Monday, November 16, 2009

NSDocumentController - Reusing a single window across multiple documents

A further twist to having a single document visible at any one time is that I would like to keep a single window and reuse it. The way I have done this is as follows
  • When I get the NSApplicationWillFinishLaunchingNotification I create the window controller and window. I do this by reading in a nib file that has my subclass of a window controller and a window.
  • In the makeWindowControllers method of my document subclass I call addWindowController to add my global window controller.
And that is about it.

Sunday, November 15, 2009

NSDocumentController - Maintaining a single document

I have been trying to figure out the best way to structure things to support the concept of the "main frame". Under Windows applications generally seem to have a main frame. This is the single key window that is central to the application. The OSX world is different - you generally have multiple windows one for each document. As a Mac user, now working windows much of the time. I find the paradigm a little strange at first. That said once you are accustomed it is fine. Also as I have mentioned before it is something that you see in some consumer apps on OSX so I am keeping this behaviour.

The trick with these things is, as ever, to find the path of least resistance. Now that I am working with Cocoa I want to do the same on the Cocoa end. I don't want to fight AppKit I want to dovetail in as well as I can so that I have to do as little as possible.

Looking at AppKit I definitely want to retain an NSDocument. I want this as NSDocument hooks into the undo manager really nicely. I will have to somehow mesh Cello's undo manager with NSDocument but that is something that I will look at later. The NSDocument as support for all the open and saving and I can hook into this.

So what I have done is write my own subclass of NSDocumentController - this is the master controller for all of my documents (the thing is I only ever have one). So my sub class will ensure that there is just one document - so if you open a document it will close the existing one.

I started by adding two methods to my document controller

- (void)addDocument:(NSDocument *)document {
[super addDocument:document];
}

- (void)removeDocument:(NSDocument *)document {
[super removeDocument:document];
}

So I could add breakpoints to check where these things were called. I could add breakpoints using GDB to the NSDocumentController methods - but I like this way. I wrote a new method that closeAllDocuments to close all of the documents (there should only ever be one). And then call that at appropriate times.
  • - (IBAction)newDocument:(id)sender
  • - (IBAction)openDocument:(id)sender
Finally I made my document controller my application delegate (if I need to I can always change this) so that I could implement the two methods:
  • - (BOOL)application:(NSApplication *)sender openFile:(NSString *)filename
  • - (void)application:(NSApplication *)sender openFiles:(NSArray *)filenames
And that takes care of the best part of it.

While figuring this out I came accoss the following post that proved very usefull here. The post is in Japanese which I was not able to follow - but the code was usefull.