SEÑOR STAFF
DEVELOPMENT BLOG

automatically expanding the Save dialog

February 25th, 2007

By default, opening a save dialog in a Cocoa app will give you the following window:

save.png

I’ve never particularly liked the collapsed version of this dialog - I much prefer the three-column mini-Finder that appears when the user clicks the disclosure triangle. However, I wasn’t able to find anything on the NSSavePanel API to control the default appearance.

I noticed that it seemed to maintain its state between runs of the application, which led me to suspect that it was being controlled by a preference. Sure enough, it’s the NSNavPanelExpandedStateForSaveMode preference. (Searching for this preference by name after the fact turned up plenty of tips on setting this globally for all apps, but nothing that addressed setting it as an app developer.)

Here’s how you would make this the default for your app:

MEWindowController.m
  1. NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
  2. NSDictionary *appDefaults = [NSDictionary dictionaryWithObject:@"YES"
  3.     forKey:@"NSNavPanelExpandedStateForSaveMode"];
  4. [defaults registerDefaults:appDefaults];

This code goes in the initialize method of one of your classes - I’m using my main document controller class.

Frankly, I’m of the belief that Apple should have made this a system-wide preference rather than a per-app preference. I can’t think of a compelling reason to maintain different defaults across apps, and it would save me the (admittedly very minor) inconvenience of having to expand the panel the first time I use any given app.

float coordinates vs. Aqua widgets

February 24th, 2007

Today, I attempted to add a button for editing drum kits. Here’s what it came out looking like:

bad buttons

The center of the button is a pixel too high, and the corners are blurry and have some lines sticking out of them. If you look closely, the small “X” button has the same problems, which I’d noticed before but never thought much of. In fact, the drop-down arrows, checkboxes, and text field are all blurry, too.

I tracked down the problem to the following code, wherein I was placing this whole view dynamically:

MEWindowController.m
  1. [[staff rulerView] setFrameOrigin:NSMakePoint(0,
  2.   [StaffController baseOf:staff] - [StaffController lineHeightOf:staff] * 6.0)];

baseOf: and lineHeightOf: both return floats, which turned out to be the root of the problem. The y-coordinate of the view’s frame origin was being set to some value very slightly off from an integer, causing all of the Aqua widget drawing to be completely thrown off. The solution:

MEWindowController.m
  1. [[staff rulerView] setFrameOrigin:NSMakePoint(0,
  2.   (int)([StaffController baseOf:staff] - [StaffController lineHeightOf:staff] * 6.0))];

One hopes that Apple has fixed this in anticipation of resolution independence in Leopard.

the joy of Help Viewer debugging

January 10th, 2007

I’ve reached (probably passed) the point where I need to start thinking about adding help docs to the app.

The Apple Help system is quite easy to set up; I followed Andy Matuschak’s tutorial. Being able to write the help as regular HTML pages is a nice bonus.

The problem with Apple Help is that if one small thing goes wrong, you’re presented with a blank Help Viewer and no console output. Silent failure.

After several wasted hours, I finally tracked down the problem: My HTML file contained two instances of the character ñ. That caused the Help Indexer app to fail (silently!), which caused Help Viewer not to be able to read my index (silently!).

For anyone starting a Cocoa project, I’d like to dispense some advice I wish I’d had at the start of this project: Don’t pick a name with any non-ASCII characters in it. Modern computers support extended character sets everywhere but where it really counts.

NSToolbar made easy

December 16th, 2006

Adding a toolbar to a Cocoa app is a non-trivial task - Interface Builder has no built-in support for it, so it has to be done programmatically.

Rumor has it that IB 3.0, shipping with Leopard, solves this problem. Until then, I didn’t want to clutter up my code with calls to the clunky NSToolbar API, so I wrote a nicer wrapper around it.

My initial thought was to make a category on NSWindowController, but unfortunately, categories don’t allow you to add member variables to a class - I was stuck creating a subclass instead.

The API looks like this:

ToolbarHelperController.h
  1. - (void) addToolbarItemWithImage:(NSImage *)image
  2.         identifier:(NSString *)identifier
  3.         label:(NSString *)label
  4.         paletteLabel:(NSString *)paletteLabel
  5.         toolTip:(NSString *)toolTip
  6.         target:(id)target
  7.         action:(SEL)action
  8.         keyEquiv:(NSString *)keyEquiv
  9.         isDefault:(BOOL)isDefault;
  10.  
  11. - (void) addToolbarItemWithView:(NSView *)view
  12.         identifier:(NSString *)identifier
  13.         label:(NSString *)label
  14.         paletteLabel:(NSString *)paletteLabel
  15.         toolTip:(NSString *)toolTip
  16.         target:(id)target
  17.         action:(SEL)action
  18.         isDefault:(BOOL)isDefault;
  19.  
  20. - (void) addToolbarSeparator;
  21. - (void) addToolbarSpace;
  22. - (void) addToolbarFlexibleSpace;
  23.  
  24. - (void) allowToolbarSeparator;
  25. - (void) allowToolbarSpace;
  26. - (void) allowToolbarFlexibleSpace;
  27.  
  28. - (NSToolbar *) initToolbarWithIdentifier:(NSString *)identifier customizable:(BOOL)customizable;
  29.  
  30. - (BOOL)handleToolbarKeystroke:(NSEvent *)event;

initToolbarWithIdentifier is called first from some initialization code in the window controller; it performs all of the necessary toolbar initialization.

The first two methods are used to add an item to the toolbar - the first adds a regular item with an image, and the second adds a custom view. The arguments to these methods mostly line up with the NSToolbar API. keyEquiv allows you to specify a keystroke for the item (formatted as a Cocoa key binding string), and isDefault specifies whether or not the item should be in the toolbar by default (as opposed to simply being available for customization).

The next six methods are convenience methods for toolbar miscellanea; the “add” methods add the appropriate item to the default toolbar, while the “allow” methods make them available for customization.

Finally, handleToolbarKeystroke will invoke the appropriate item, if it finds one, for the event passed in. It returns YES if the keystroke was consumed.

I’m sure this will all be useful to someone other than me, so I’m releasing the code into the public domain. Download here:

ToolbarHelperController.h
ToolbarHelperController.m

adding icons to an NSPopUpButton

November 26th, 2006

I wanted the NSPopUpButton used to change the duration of the note being placed to look like this:

Unfortunately, Interface Builder doesn’t provide any easy way to add an icon to an NSMenuItem. I could have left the menu blank in IB and constructed all of the items programmatically, but that would have been rather tedious.

What I did instead was to add a category to NSPopUpButton to insert the icons at runtime. The NSMenuItems are created in IB as usual, but the name is prepended with the icon filename (e.g., “{wnote.png} Whole note”). In awakeFromNib, my controller calls a method defined on the category, which replaces the bit in curly braces with the given icon.

I thought this might be handy for other people, so I’ve placed the source code in the public domain - feel free to use it in your own projects:

NSPopupButton+Images.h
NSPopupButton+Images.m

triplets

October 28th, 2006

I settled on a fairly straightforward way to implement triplets. I’m representing the duration of a note with an integer, which is the reciprocal of the note’s duration (1 for a whole note, 4 for a quarter note, etc.). A quarter note in a triplet, then, has a duration of 6 - no changes to the class hierarchy, or really to the core note methods - required.

The devil, as always, was in the details. At various points I need to know the “true” (or “effective”) duration of a note, e.g. 0.25 for a quarter note - in particular, when playing a song through MIDI, or calculating which notes fit in a particular measure. Introducing notes whose effective duration was a non-terminating decimal caused some serious floating-point rounding errors.

The somewhat hacky solution to this was simply to multiply all effective durations by 3, since the effective duration of any note is the reciprocal of either a power of 2, or 3 times a power of 2. The effective duration would then be divided by 3 again when playing out to MIDI, since the rounding error at that point could easily be ignored.

Unsurprisingly, this broke 40-some unit tests, which had been using the effective durations to make assertions about various notes they were manipulating (or were directly testing the effective duration calculation). With the exception of the latter, nothing about the functionality these tests are intended to test had broken, which is the textbook indication of a bad test.

For the time being I’ve just gone through and multiplied all the floating point constants in those assertions by 3. I think the correct solution here, though, is to write a utility function for the unit tests that computes the effective duration for a note in the same way that the note code does - or ideally, using the note code itself. In the former case, I’d only have one place to change in the tests if the effective duration concept changes again, and in the latter case I wouldn’t have to change the tests at all.

a handy little NSView method

October 22nd, 2006

An important consideration in any direct-manipulation interface is keeping the user’s view scrolled to include the area on which the user is focused, and the area on which they’re likely to operate next; often, scrolling must be done programatically in response to a user action. Usually this involves a lot of pain-in-the-ass coordinate math.

Thankfully, I stumbled upon a nice little gem on NSView in the autocomplete list (whilst writing essentially the same method myself). scrollRectToVisible: does all of the annoying work, including finding the containing scroll view of the view it’s called on; all you have to do is pass in an NSRect. In my case, I’m simply passing in the calculated bounds of, say, a measure that the user just added a note to (or more likely, a slightly expanded copy of same, so that it’s not pressed right up against the side).

sometimes I miss NPEs

October 10th, 2006

While fixing a UI bug related to time signatures today, I noticed some disturbing behavior. I have an NSTextField and an NSStepper next to each other for editing the top number of a time signature; these both trigger an event on the corresponding Measure to update the time signature in the model. I noticed that if I changed the value of the text field, then clicked on the stepper, it was stepping from the previous value of the text field rather than the new value.

I searched around for a solution in vain, then I went back and took a look at my code. It looked pretty reasonable:

Measure.m
  1. - (IBAction)timeSigTopChanged:(id)sender{
  2.         [[self undoManager] setActionName:@"changing time signature"];
  3.         int value = [sender intValue];
  4.         if(value < 1) value = 1;
  5.         [timeSigTopStep setIntValue:value];
  6.         [timeSigTopText setIntValue:value];
  7.         [staff timeSigChangedAtMeasure:self top:[timeSigTopText intValue] bottom:[[[timeSigBottom selectedItem] title] intValue]];
  8. }

Updating the value of either control would set the value of both controls to the new value. What could possibly be going wrong?

Finally, in desperation, I fired up the old debugger. That’s when I noticed that timeSigTopStep (the NSStepper) was set to nil. I’d forgotten to hook it up in Interface Builder, or the connection had gotten jostled by some code change. Objective-C had helpfully neglected to inform me of this fact, since sending a message to nil is perfectly reasonable Objective-C behavior.

Thanks, Objective-C! I didn’t have anything better to do with that half hour anyway.

in praise of Cocoa’s undo manager

October 8th, 2006

As GEF is the MVC framework I’ve most recently worked with, it was a little tough for me to adjust to Cocoa’s undo paradigm. In GEF, you define user-level commands at the controller level, each of which is responsible for defining what happens when it’s executed, what happens when it’s undone, and what happens when it’s redone. In Cocoa, on the other hand, you typically handle undo at the model level, registering more atomic undo actions which are automatically coalesced by the undo manager within each user action.

It’s a harder model to work your head around, as you need to structure model-level operations in such a way that they’ll undo correctly. It’s also tricky because a redo is just undoing an undo, so you need to make sure that whatever code path the undo hits will set up the proper undo actions as well.

My strategy in instrumenting the Señor Staff model classes for undo has been to register the undo actions on the classes’ low-level mutator functions:

Measure.m
  1. - (void)prepUndo{
  2.         [[[self undoManager] prepareWithInvocationTarget:self] setNotes:[NSMutableArray arrayWithArray:notes]]
  3. }
  4.  
  5. - (void)setNotes:(NSMutableArray *)_notes{
  6.         [self prepUndo];
  7.         if(![notes isEqual:_notes]){
  8.                 [notes release];
  9.                 notes = [_notes retain];
  10.                 [staff cleanEmptyMeasures];
  11.         }
  12.         [self sendChangeNotification];
  13. }

Any higher-level methods on Measure which modify its Notes simply calls prepUndo before doing so; this copies the current Notes and sets up the undo action to restore them. Since setNotes itself calls prepUndo, the redo is likewise set up correctly.

A nice result of this whole approach has revealed itself as I’ve built the Chord class. Most of Chord’s methods simply delegate to its component Notes. As a result, there’s very little undo code within Chord itself, as the Notes will all register the appropriate undo actions, and the undo manager will coalesce them correctly.

my kingdom for a “map”

October 4th, 2006

There’s nothing like writing a data structure whose methods mostly involve delegating to a list of components to make you wish you were writing Ruby.

Most of the methods on the Chord model just call the corresponding method on each note in the chord. I didn’t feel like writing out a bunch of for loops (as I’ve been spoiled by IntelliJ IDEA’s automatic code templates), so I searched around for a more functional-programming style approach.

Cocoa provides methods on NSArray called makeObjectsPerformSelector: and makeObjectsPerformSelector:withObject: which are almost what I was looking for. The drawbacks are that the mapped method can only take at most one argument, the argument must be an object (not a primitive), and the whole selector syntax is a bit wonky.

What I found as a suitable replacement was Chomp, a nice open-source higher order messaging framework. Chomp extends NSArray and friends to provide “trampoline” methods, which use some NSProxy trickery to implement all your favorite functional programming standbys.

Check out how simple Chord becomes:

Chord.m
  1. - (void)transposeBy:(int)transposeAmount{
  2.         [[notes do] transposeBy:transposeAmount];
  3. }
  4.  
  5. - (void)prepareForDelete{
  6.         [[notes do] prepareForDelete];
  7. }

Honestly, it’s 2006. Why should we still be dealing with enumerator objects and array index loops?