manicwave

Surf the wave

Links for 2010-01-03

Permalink

[alias] lg = log --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr)%Creset' --abbrev-commit --date=relative

(tags: git alias shortcuts)

PHGraph is a very simple cocoa framework to display scientific plots in the plane, for osX tiger and leopard (universal binary).

Why another framework ? Because the plotting frameworks I've been able to find here or there did not fulfill my expectations, including the ability to display quickly and efficiently plots with a large number of points.

(tags: graphing plot cocoa view framework)

(tags: xcode plugin google cocoa toolchain)

Unraveling the Mysteries of NSSplitView - Part 2

Permalink

In part 1 of the series, we covered the very basics of NSSplitView. We ended with a NSSplitView with two subviews. We were able to constrain the minimum size of each.

In this part of the series, we are going to add collapsible subviews.

Collapsible subviews

There are several ways to support collapsible subviews. The first is supported directly by NSSplitView. As the user drags a divider to its minimum position, if subviews are collapsible, additional movement will result in the subview snapping shut.

We can support this behavior by adding the canCollapseSubview method.

- (BOOL)splitView:(NSSplitView *)splitView canCollapseSubview:(NSView *)subview;
{
    NSView* rightView = [[splitView subviews] objectAtIndex:1];
    NSLog(@"%@:%s returning %@",[self class], _cmd, ([subview isEqual:rightView])?@"YES":@"NO");
    return ([subview isEqual:rightView]);
}

Dragging the divider to the right most edge of the window will result in the right subview snapping shut. The divider will still be show on the rightmost margin of the view. This collapsing behavior is our first bit of good news. When we collapse and uncollapse our right subviews, the layout is not munged as it was in part one when we effectively reduced the right subview width to zero. We'll use the observation later when we add programmatic collapsing.

NSSplitView-Part1a.png NSSplitView-Part1a.png We can hide the divider when the right subview is collapsed.

- (BOOL)splitView:(NSSplitView *)splitView shouldHideDividerAtIndex:(NSInteger)dividerIndex;
{
    NSLog(@"%@:%s returning YES",[self class], _cmd);
    return YES;
}

NSSplitView-Part1a.png

If you keep you're finger on the mouse while moving the divider, you can observe it collapse the right subview and then moving the divider back to the left - uncollapse it.

If you didn't keep your finger on the mouse, the right view collapsed and there is no way now to get the view back. Hovering over the right edge of the window won't do it. There aren't any buttons to press, nothing to double click.

Before we fix this issue, change shouldHideDividerAtIndex back to returning NO.

We'll add one more bit of goodness before we tackle programming collapsing.

Double Click to collapse

NSSplitViewDelegate includes an optional method shouldCollapseSubview:forDoubleClickOnDividerAtIndex: - it does its name suggests. If we answer YES, double-clicking on the divider will collapse the right subview. Double-clicking on the divider while the right subview is collapsed will uncollapse the right subview.

Here's the implementation:

- (BOOL)splitView:(NSSplitView *)splitView shouldCollapseSubview:(NSView *)subview forDoubleClickOnDividerAtIndex:(NSInteger)dividerIndex;
{
    NSView* rightView = [[splitView subviews] objectAtIndex:1];
    NSLog(@"%@:%s returning %@",[self class], _cmd, ([subview isEqual:rightView])?@"YES":@"NO");
    return ([subview isEqual:rightView]);
}

Sweet!

Programmatic Double Click

Programmatic double-click will allow us to hook the action selector of a menu item, a toolbar button or other UI control to a method that will toggle the collapsed state of the collapsible subview.

First we'll add a toolbar to our window to have a convenient place to put the toggle button. Interface Builder.png

In MySplitViewController, we'll add the logic to toggle right subview.

-(IBAction)toggleRightView:(id)sender;
{
    BOOL rightViewCollapsed = [[self mySplitView] isSubviewCollapsed:[[[self mySplitView] subviews] objectAtIndex: 1]];
    NSLog(@"%@:%s toggleInspector isCollapsed: %@",[self class], _cmd, rightViewCollapsed?@"YES":@"NO");
    if (rightViewCollapsed) {
        [self uncollapseRightView];
    } else {
        [self collapseRightView];
    }
}  
-(void)collapseRightView
{  
    NSView *right = [[[self mySplitView] subviews] objectAtIndex:1];
    NSView *left  = [[[self mySplitView] subviews] objectAtIndex:0];
    NSRect leftFrame = [left frame];
    NSRect overallFrame = [[self mySplitView] frame]; //???
    [right setHidden:YES];
    [left setFrameSize:NSMakeSize(overallFrame.size.width,leftFrame.size.height)];
    [[self mySplitView] display];
}

toggleRightView: is the public method that we'll connect UI elements to. It queries the NSSplitView to see if the right subview is collapsed. If not, it calls collapseRightView otherwise, we'll uncollapseRightView:. The key element here is to ensure that the 'collapsed' status of the right view is set correctly. My first several attempts to make this work involved setting the frame width of the right view to 0. NSSplitView did not answer YES to isSubviewCollapsed: under those circumstances. Remember the observation earlier that when we snapped the right view closed and reopened it the view had not been mangled by autoresize logic? That suggests that the view was never shrunk to zero width, but rather hidden. I confirmed this with a F-Script session.

uncollapseRightView: is equally straight forward --

-(void)uncollapseRightView
{
    NSView *left  = [[[self mySplitView] subviews] objectAtIndex:0];
    NSView *right = [[[self mySplitView] subviews] objectAtIndex:1];
    [right setHidden:NO];  
    CGFloat dividerThickness = [[self mySplitView] dividerThickness];  
    // get the different frames
    NSRect leftFrame = [left frame];
    NSRect rightFrame = [right frame];
    // Adjust left frame size
    leftFrame.size.width = (leftFrame.size.width-rightFrame.size.width-dividerThickness);
    rightFrame.origin.x = leftFrame.size.width + dividerThickness;
    [left setFrameSize:leftFrame.size];
    [right setFrame:rightFrame];
    [[self mySplitView] display];
}

The cool thing about all of this is that we're not saving frame rects or adding additional data structures to hold this state. I can assure you that my first several attempts to understand NSSplitView had loads of code to deal with view frames, collapse states, notifications to enable/disable view resizing etc. It is true, at least in this case, that if you're fighting the frameworks, there may be a more enlightened path awaiting discovery.

We can now go back and hide the divider when the right subview is collapsed. The app should now look like this.

NSSplitView-Part2.png

Conclusion and Next Steps

We've accomplished quite a bit. I know, it's hard coded to collapse only the right subview and only handles a single split, but the concepts at work are illustrated plainly. The next few topics for future posts will include adding animated adjustments, controlling the effective drag area, customizing dividers and generalizing the solution.

The code for this segment can be downloaded here: Part-2

Unraveling the Mysteries of NSSplitView - Part 1

Permalink

There are many examples of Mac applications that leverage split views to great effect. Apple uses split views in many applications that we use every day - Mail.app, Preview.app, Xcode.app etc. Many third parties use NSSplitView as well. Gauging from the number of posts on the net, there is also a lot of confusion about how to accomplish the basics (or the expected) using the default NSSplitView class provided in the SDK. This point is accentuated by the presence of several third party frameworks that provide these expected behaviors.

Pre-leopard behavior of NSSplitView gave rise to several of the third party frameworks. In 10.5 Apple cleaned up the implementation of NSSplitView and ostensibly provides everything one needs to use split views effectively.

My experiences (and occasional frustrations) with third party frameworks led me to dig in and try to understand what Apple offers, how it works and how (if) I could package that up in a reusable form for use in my own applications.

Requirements

Everyone has there own pet list of requirements for a usable split view solution, but my list includes the following:

  • collapsible subviews - A common UI capability today is a button or key combination that hides a subview
  • Intelligent subview resizing - There are a lot of documented issues with autoresizing Cocoa views - we need a splitview solution that doesn't trash subviews when they are collapsed
  • Ability to specify additional drag areas for splitters
  • Ability to specify custom divider visuals
  • Sexy animation when toggling the visibility of subviews First, it's worth looking at a few of the frameworks that are out there, what problems they solve and those that they don't (from my perspective).

RBSplitView

The excellent RBSplitView emerged in 2004 in the pre-leopard days. In addition to providing the runtime framework, RBSplitView includes an Interface Builder palette that supports IB modification of most of the desired behaviors, including divider selection, collapsible views, and min and max settings for view sizes.

RBSplitView also includes support for animating view adjustments such as collapse/uncollapse.

There are a long list of applications that use RBSplitView .

RBSplitView is, from my experiments, very solid code. Capabilities such as toggling splits works, preserving subview layouts.

The primary issue with RBSplitView is that the animation used is pre Core Animation. Running on my 8 core mac pro, this is not an issue, but it gets a bit jumpy on my MacBook Pro. Further, by implementing the animations in the framework, RBSplitView limits ones ability to leverage a consistent CA based animation scheme throughout the application.

Bottom line - in a 10.6 world, is RBSplitView still the best solution?

BWSplitView

I have to say that I really want to like BWSplitView - Brandon Walkin's framework that provides a modern NSSplitView based solution. On the surface, it meets all of the requirements outlined above. It is supported by a rich IB palette with a slew of conveniences. Brandon's site includes some great screencasts illustrating the promise of BWSplitView.

That said, I struggled in real world usage of BWSplitView. Documented open issues around the splitter drag behavior created some showstopper issues for me.

In fact, it was my attempts to fix some of the BWSplitView issues that led me to explore how NSSplitView works.

Digging in

The canonical example of splitter view behavior is perhaps Mail.app. Two splitters, a vertical split for the Mailboxes and main view. The main view is horizontally split into the messages list and the message viewer.

To understand what NSSplitView offers out of the box, let's build a sample app with a single splitter. If we can understand what is going on with a single split view, we can generalize that as we add additional split views.

In order to verify the subview resizing challenges, one of our subviews will have several standard cocoa controls with autoresizing turned on.

The app looks like this:

NSSplitView-Part1-initial.png

Dragging the divider all the way to the right and then back towards the middle quickly borks up the autoresizing behavior.

NSSplitView-Part1-borked.png

To end part 1 of this exploration, let's add minimum sizes for our split view.

We instantiate a new controller class, we'll call it MySplitViewController (as it stands now, its just a delegate but we'll be adding other functionality to it as we move along).

Instantiate an instance of this class in the XIB and hook the delegate outlet of the NSSplitView outlet to the new class.

There are a bunch of optional methods declared in the NSSplitViewDelegate - we'll implement only two. constrainMinCoordinate and constrainMaxCoordinate. The documentation is sparse on the functionality of these methods, but the header file for NSSplitView does a pretty good job of laying out the basics.

constrainMin... and constrainMax are called repeatedly while the divider is being dragged. In the example implementation, we'll answer proposedMinimum+200 for the constrainMinCoordinate call, which in our example will have the effect of limiting the left subview to 200 pixels. Judicious use of NSLog will show that the proposedMinimum will be 0 -- we're adding 200 and hence the minimum position of the divider will be 200 pixels. When you begin having splitviews with more than two subviews, the concept is the same, but you are now responsible for determining which subview is being referenced. In our example there is only one divider and its index will always be zero.

The constrainMaxCoordinate method works in a similar fashion. The splitview sends in a proposedMaximumPosition that is the width of splitview - in this case, the entire width of the window. We'll answer with that value - 100 resulting in a maximum divider position of the window width - 100 pixels.

@implementation MySplitViewController  
/*
 * Controls the minimum size of the left subview (or top subview in a horizonal NSSplitView)
 */
- (CGFloat)splitView:(NSSplitView *)splitView constrainMinCoordinate:(CGFloat)proposedMinimumPosition ofSubviewAt:(NSInteger)dividerIndex;
{
    NSLog(@"%@:%s proposedMinimum: %f",[self class], _cmd, proposedMinimumPosition);
    return proposedMinimumPosition + 200;
}  
/*
 * Controls the minimum size of the right subview (or lower subview in a horizonal NSSplitView)
 */
- (CGFloat)splitView:(NSSplitView *)splitView constrainMaxCoordinate:(CGFloat)proposedMaximumPosition ofSubviewAt:(NSInteger)dividerIndex;
{
    NSLog(@"%@:%s proposedMaximum: %f",[self class], _cmd, proposedMaximumPosition);
    return proposedMaximumPosition - 100;
}

left-size-constrained.png

right-size-constrained.png

After compiling and running, we can now see that both the left and right subview are indeed constrained. Although the 100 pixel extent of the right view does not prevent the view from looking terrible, it does prevent the autoresize funkiness of the first example.

Conclusion and Next Steps

We've only scratched the surface of NSSplitView. We limited the complexity of our NSSplitView example to the simplest possible case and illustrated some of the challenges in using NSSplitView. In the end, we were able to introduce a few delegate methods that exert additional control over the behavior of NSSplitView.

In the next article, we'll add support for collapsible subviews and then add programatic collapsing. Additional topics for future posts will cover adding animated adjustments, controlling the effective drag area, customizing dividers and generalizing the solution.

The code for this segment can be downloaded here: Part 1 Sample Projects

Links for 2009-12-24

Permalink

As you probably have guessed from the title of this post, I am not happy with this behavior, so I have written a class, CLAlert (MIT License) that displays alerts the way I think it should be done. I.e. a note icon for an informational alert, a caution icon for a warning alert and a stop icon for a critical alert as you can see on these screenshots.

(tags: cocoa nsalert mitlicense opensource)

When you want to localize your application, you can take several routes.

(tags: cocoa l10n i18n tips ibtool genstrings)

Links for 2009-12-23

Permalink

What I really wanted was a version of Smalltalk that used the same object modal as Objective-C, like Pragmatic Smalltalk of Etoilé, or even MacRuby on Mac OS. Most of all, I wanted it to treat me like an adult: I don’t need a class browser, thank you, I’m more comfortable working with plain-old files. The persistent image construct in Smalltalk really makes me uncomfortable.

(tags: smalltalk cocoa osx)

(tags: nstextcell cocoa badge customdrawing)

(tags: database etl conversion migration)