Omnipreferential: Rolling My Own Prefs Window, Instead of Using Omni Frameworks

After a long hiatus, I am once again looking at the project that first got me using both the Omni frameworks and unit tests.

Why now? An application already exists that does what my project’s app does, but that existing app is Classic-only. Classic seemed to be moseying along just fine until splat! it hit the brick wall of Macintel. No more Classic.

I was using the Omni frameworks primarily for how they made your application’s preferences window easier to implement. The trouble is, the Omni framework projects are sorely outdated. The version you’re supposed to use don’t have native targets, which is a requirement for building Universal binaries.

So it’s time to give them the heave-ho. Does that mean I have to make my own preferences implementation from scratch?

One alternative is to use the Preference Panes framework from Apple. It’s primarily for System Preferences panes, but the documentation makes it clear that it can be used for application preferences, too. The trouble? If you use it for your own application, you have to manage the window and the toolbar yourself. Since that’s most of the work, and since, if you use the Preferences Pane framework, you need to package each pane as a separate bundle, which is even more work, I give it a pass.

Uli Kusterer has UKPrefsPanel and UKToolbarFactory available in the source code portion of his Web site. They look promising. The prefs panel even uses an NSTabView as an easy way to deal with the prefs panes, which is how I was thinking of doing it. But it doesn’t support automatic resizing of a preferences window to the height of each individual preference pane, which is something I definitely want.

In fact, I want my preferences window to look and behave exactly like the preferences windows of standard Apple applications like Safari and Address Book.

So I decide to work directly with NSToolbar, and see what sort of progress I can make.

Apple’s ADC Web site has ToolbarSample source code that’s a good starting point.

Following that sample, I make the NSToolbar myself in code, and add it to the window by sending the window the message setToolbar:. That API actually expands the window by the height of the toolbar: I don’t have to leave room for it myself in the window’s nib. The window also fully takes care of the user hiding and reshowing the toolbar, and saving the selected toolbar item, as long as I send it a setAutosavesConfiguration:YES message. Neat.

There are a lot of things that need some tweaking, though:

• I want to use an NSWindowController to encapsulate the preferences window nib, a straightforward Cocoa technique. But the controller doesn’t seem to use NSWindow’s frameAutosaveName value, which is settable in the nib. In fact, it seems to set it to nil. Instead, if I want to go that route (which I don’t, see below) I need to set the controller’s windowFrameAutosaveName value, which can only be done in code. Annoying.

• The preferences windows of Safari et al erase themselves and then use a cool drawer-like animation to resize between preference panes. I can get the latter using setFrame:display:YES animate:YES, but not the former. I handle this by adding an extra, empty NSTabViewItem after all the other meaningful ones. I switch to that, do the resize, then switch to the NSTabViewItem I really want afterwards.

• I want the individual preference pane’s size info to be in the nib, not something I have recompile to alter. But every NSTabViewItem within an NSTabView is the same size. I work around this by putting my own custom NSView inside each NSTabViewItem, and then putting all my widgets in that subview.

In my code, I check for that subview’s size, and resize the window to match it, when switching to that NSTabViewItem.

• I need to be careful with resizing: it order to get the the effect I want — which is that the window title bar stays stationary, and the bottom of the window slides up or down — in the normal Quartz coordinate system, I need to change both the window size and its origin, because the origin is in the lower left-hand corner of the window. So, every time I select a toolbar item, as far as Cocoa is concerned, I’m both resizing and moving the window. But NSWindowController doesn’t catch this, leading to erroneous positioning when I close and reopen the application.

• If I handle user defaults manually instead of relying on NSWindowController, I can save the window’s frame to the user defaults (a) when I choose a new toolbar item (see the sample for how to associate toolbar items with actions), (b) when the window is moved around by the user (via the notification NSWindowDidMoveNotification), and (c) when the window is resized (via the notification NSWindowDidResizeNotification). Now, the window is not user-resizable, but this notification is still invoked when the user shows or hides the toolbar, because that automatically changes the size of the window.

But why do that much work? Instead, I listen for the notification NSApplicationWillTerminateNotification, and save off the window frame then. Since user defaults are generally only saved to disk upon application termination, this doesn’t lead to a mismatch even if you force-quit the application, because neither the toolbar user default changes that have already happened, nor my code’s user default changes that haven’t happened yet, are saved.

Upgrading the Omni Frameworks for Xcode 2.1

Last time I talked about the Omni frameworks, it was to warn you to use the .pbproj versions, instead of the more logically-named .xcode versions. That was all I needed to do to make them work with Mac OS X 10.3 “Panther.”

Now we’re up to Mac OS X 10.4 “Tiger” and Xcode 2.1. Is it more complicated this time? You bet.

I use cross-project references to refer to the Omni projects in my own project. Important tip: cross-project references only work in Xcode 2.1 if all projects have been upgraded to 2.1 project format. So I upgraded all three projects. Since I used the new .xcodeproj suffix for the upgraded projects, that meant I had to remove the old project references from my project and add the new ones, which meant I had to re-add all the dependencies and build products for my project’s targets. A bit of a pain.

Once I’d finished that, OmniBase built fine. OmniFoundation did not.

The OmniFoundation project refers to /usr/lib/libbz2.a, which is missing under Tiger, replaced by /usr/lib/libbz2.dylib. I don’t want the project to refer to the dynamic version of the library: that would presumably make it only work under Tiger. I want it to use the static version of the library.

For the C++ standard library, you can switch from the dynamic version (available 10.3.9 and up) to the static version (works in 10.3.8 and lower) by switching compilers. GCC 3.3 uses the static library, GCC 4.0 uses the dynamic library. This is why just changing the SDK from 10.4.0 to 10.3.9 (or even 10.2.8) isn’t enough if you want to support older versions of the OS.

Changing compilers doesn’t help with the bz2 library, however; instead, the SDK is the key here. The full static library is still available on a Tiger system, but not under /usr/lib. Instead, it’s available under /Developer/SDKs/MacOSX10.3.9.sdk/usr/lib. So I changed the project to refer to that.

There were a few other tweaks needed. I went into the OmniFoundation (Framework) JAM-based target and both unchecked the “Treat all warnings as errors” checkbox and, under the Expert View, removed -Werror from the WARNING_CFLAGS setting. Actually, first, I tried going in and fixing all the warnings, but some of them turned out to be quite obscure, so I finally gave up.

I had to follow the same steps for the OmniAppKit (Framework) target in the OmniAppKit project.

Could it be dangerous to let so many warnings go by? Maybe. But I hadn’t changed the code to produce the warnings; instead, they occurred because GCC 4.0 is more strict and produces more warnings than GCC 3.3. Should someone fix them eventually? Yep. For tonight, though, it’s good enough that all the Omni frameworks I have build successfully.

Finally, a note about how Xcode 2.1 builds targets referred to via cross-project references. Before, Xcode would build the targets in other projects without any build style at all. In 2.1, Xcode attempts to match build configuration names. If your project is building with the configuration “Development,” and the other project has a configuration named “Development,” it is used for the other project’s target, too. It’s a very nice improvement.

When We Last Saw Our Hero…

…he was riding into the sunset with the cry, “It’s time to try it out for real!”

Sigh.

The good news: after spending lots of time converting my application to the two-framework setup I talked about in my I’ve Been Framed! post, I have it working again.

The bad news: I continued having problems even after I solved the ones I talked about in my Looks Can Be Deceiving post.

Now, a lot of this is because I decided, as long as I was embedding my own frameworks, in addition I would try to embed the Omni frameworks I was also using. Not only embed those frameworks, but also put dependencies on those framework projects in my own project, so everything would always get built correctly.

Using that setup, I kept getting strange compilation errors that would go away or change after each clean rebuild. Undefined symbol errors, code logic compilation errors that only showed up after the third rebuild, inability to find the embedded frameworks. You name it.

In general, my advice is: keep fiddling with it, you’ll get it to work eventually. But don’t expect a cakewalk.

Specifically, about the app framework setup:

  1. main.c*/m* must be in the app itself. All other code can be in the app framework.
  2. MainMenu.nib must be in the app itself. All other nibs can be in the app framework.
  3. The Finder icons for the application and any of its documents must be in the app itself.

2. means that you may need to put a few tests in the app itself, not just in the tests framework, if you want to test something in MainMenu.nib.

And specifically, about the Omni frameworks:

  1. Add the Omni projects to your project and use the framework references within those projects – don’t also add the frameworks to your project under the Frameworks group.
  2. I found no way around modifying the Omni projects themselves to have an INSTALL_PATH of “@executable_path/../Frameworks”. It would be nice if you didn’t have to touch those projects at all for this purpose, but it looks like you do.
  3. You must link the Omni frameworks to both your app framework and the app itself, or you may get linker errors.
  4. As I warn in my Don’t Use the Brown Project File post, at this time, you really really should use the *.pbproj versions of the Omni projects, not the *.xcode versions.

Don’t Use the Brown Project File

I have an old project that I dusted off today that uses the Omni frameworks. It’s old enough that I remember a time when the frameworks built without a hitch.

Then they stopped building. I searched on the Omni Mailing Lists page and found a variety of build problems people were having.

But the message I needed to read was this one, which says “Even if you’re building with Xcode, you may want to stick to the .pbproj projects….” Turns out the *.xcode files are outdated, despite the fact that it’s a newer project file suffix. With the *.pbproj files, everything builds fine.

Should it have occurred to me to try the older files? Sure. But a Readme note, or removal of the outdated project files, or some update in the six months since this was last released, beyond an obscure mailing list message….would’ve saved me some trouble.