Double or Nothing: Pitfalls with NSTableView’s doubleAction

One of the core capabilities of Interface Builder, when used as the resource editor of a Cocoa application, is connecting UI widgets to code with just a bit of click and drag. The code that is connected to consists of outlets and actions.

Outlets are straightforward, just pointers to objects. The basic thing to remember, as with all Interface Builder connections, is that you shouldn’t try to refer to them in your init methods; use awakeFromNib instead.

Actions are methods, more specifically methods referred to by name (in the nib file) or selector (at runtime) instead of (as in C/C++) by pointer. To connect an action in Interface Builder, you control-drag from the UI widget that will send the action message to the object that will receive it, and then you choose the method in that receiver whose message will be sent.

The reason actions are more complicated than outlets is because actions are really part of a target/action pair. Interface Builder needs to remember more than just the message, it needs to remember the target to send that message to.

Until recently, to me at least, the Interface Builder UI seemed to be a little fuzzy on that distinction. In the Info window for an object, under the Connections pane, there was an Outlets column and an Actions column. Under the latest Interface Builder in Panther, what was called Actions is now referred to as “Target/Action”, which states the actual situation more explicitly.

Another thing to keep in mind is that the Cocoa framework has the action/target pair functionality built into Interface Builder solely for itself. You can’t make your own method/object pointer pair variables in a class and set them via the same Interface Builder UI that Cocoa uses for action/target; there’s just no way to tell Interface Builder to treat your variables that way.

With me so far? Good, because it’s about to get a little more complicated.


A while ago, I made a specialized outline view. When you double-clicked some of the cells, instead of getting the usual text editor, for certain columns and under certain circumstances, you would get more specialized behavior, such as a popup menu or a sheet with multi-line text editing capabilities.

The way to get this behavior is (a) turn off the regular editing behavior for the cell by having the delegate for the outline view return NO for the method outlineView:shouldEditTableColumn:item: and (b) send the message setDoubleAction: to the outline view with, as its parameter, the selector to the method you want to use to invoke the custom editor.

Those of you who know how this works already will protest, “Hey, you missed a step!” And indeed I did. But the above steps work under special circumstances, and they work because of how target/action pairs work.

The control-drag that I mentioned for actions at the top of this post fills in two variables in NSControl and its subclasses. It fills in the target variable with a pointer to the object. And it fills in the action variable with the selector to the method. Now, doubleAction is action‘s cousin in NSTableView. It works exactly the same way. But you can’t control-drag to connect it via Interface Builder, you need to set it via code. So, you would probably guess correctly that setDoubleAction: only sets the method, not the target, and you might look around in vain for setDoubleTarget:. Nope, doesn’t exist. Instead, it also uses target!

Why would this work even when you haven’t set target to anything? Well, if the target isn’t set, the Cocoa framework tries to make a guess what the target should be. You can see the order in which it checks here. This order doesn’t mention the delegate for a control, but NSTableView may be a special case since not all controls have delegates. In any case, it’s a good idea to send a setTarget: message explicitly.

There you have it. As with the solution to most pitfalls, esp. in Cocoa, the solution is very simple, but might not be easy to come up with on your own. Enjoy!

List Searching Update: Minor Bugfix and Xcode

I’ve uploaded a minor bugfix to my applelist.pl script, which I first announced on this Nov. 25, 2003 post. Line 154 has been changed from

system "./getlinks.pl $daypage $dayDir ".*.txt$"";

to

system "\"./getlinks.pl\" \"$daypage\" \"$dayDir\" \".*\.txt$\"";

Yes, ladies and gentlemen, I was bitten by the bug that often hits Unix scriptwriters getting used to the “Mac Way”: the annoying habit of real users to put spaces in their directory names. (Real users like me.)

In Unix scripting, spaces are often delimiters between arguments. If you have a script that takes two arguments, the first being a full path and the second being something else, and you pass in a full path not enclosed in quotes as that first argument, the script will parse everything after the first space it finds in that path as the second argument. Ugh.

When you’re working directly in Terminal, using things like tab completion or dragging in file/folders to get their paths, the OS handles this sort of thing for you by escaping out the spaces like this:

My\ HD/My\ Folder/

but you don’t want to do that sort of find and replace in your paths, and quotes are easier, anyway.
Second item of the day: people have been grumbling for a while now on the xcode-users list, which is the replacement list for projectbuilder-users in the same way that Xcode is the replacement for Project Builder (whose corresponding Apple Web site link now points to Xcode – is that a sign, or what?), that it does not yet have an entry in Apple’s list search page.

Now, there isn’t that much content in the list yet, just three months worth, but esp. considering that this is Xcode’s teething period, it’s nice to be able to search for specific problems you’re having or issues you’re facing. So I’m proud to say my applelist.pl script is the only way currently to enable such a search! Yes, yes, I know this state of affairs won’t last, but it’s nice to have bragging rights for a little while, eh?

“I would prefer not to”: Preferences Files Done Badly

I wrote a post I didn’t upload two days ago about how CodeWarrior 6 was working natively in Panther (Mac OS X 10.3) when it hadn’t worked in Jaguar (10.2) and possibly earlier. The appropriate comp.sys.programmer.codewarrior thread can be found here.

CW 6 is probably most useful to developers who still need to do 68K builds. Yes, there are still professional developers who are getting paid to do this in certain cases, whose identities (the cases, not the developers) I will leave as an exercise for the reader.

So I wrote a whole post about the tempestuous history of CodeWarrior 6 on OS X, waxed nostalgic about WWDC 2000, and marveled how something Apple had done in Panther managed to make it work again.

But I was wrong.

A little later I opened CodeWarrior 8, which is a much more respectable version of CodeWarrior to be using in this day and age, and when I tried 6 again for the express reason of verifying the post I’d written, CodeWarrior 6 crashed again just like it had in 10.2.

If I remove the /Metrowerks/ folder that CodeWarrior 8 puts in my OS X ~/Library/Preferences/ folder, then CodeWarrior 6 can be opened again as a native OS X application without crashing.

The problem isn’t something in the OS. The problem is CodeWarrior 6 choking on something in a preferences file it didn’t expect.

So this post is going to be a little different.

I don’t know for a fact exactly why CW 6 is crashing, but it might be related to code like this, which gets a preferences value from a preferences file:

  SInt32 valueInt; // Where we're saving value

  CFTypeRef valueRef = CFPreferencesCopyValue(ValueKey, AppID, (CFStringRef)kCFPreferencesAnyUser, (CFStringRef)kCFPreferencesAnyHost);
  if(value != nil)
  {
    if(CFGetTypeID(valueRef) == CFNumberGetTypeID() && CFNumberGetValue((CFNumberRef)valueRef, kCFNumberSInt32Type, &valueInt))
    {
      // Success
    }

    CFRelease(valueRef);
  }

Now, this code does the right thing. (Sorry about the formatting.) It doesn’t assume that the value referred to by ValueKey exists. And (what developers get wrong) it doesn’t assume that if a value is found, that it’s of the assumed type. This is important because the OS X preferences file format is text, XML-based, easy for end users to get into and muck around with. If the type check wasn’t there, and the value were instead, say, a string? The application would most likely crash.

And this is just Carbon. The Cocoa framework takes care of all of this for you, reading preferences XML data into objects. Guess what! Cocoa apps also crash when you muck with the preferences file, just like badly written Carbon apps. There’s nothing you can do about it.

Now, this is not a hugely important detail. Users who muck with preferences files willy-nilly deserve what they get. But I would prefer that my apps get this sort of thing right, and I’d prefer that the apps I use do, too.

Searching Apple’s Mailing Lists

You know about Apple’s mailing lists, right? No? Wellll…okay, I forgive you. In fact, I’m putting helpful links at the bottom of this post, so you can go check them out for yourself (or have an easy way to get back to them, if you are already in the know).

Signing up for these lists today is a good start. But what about searching the past archives to make sure a question you want to ask hasn’t already been asked? (You are always going to do that right? No? This time, I don’t forgive you.)

The trouble is, Apple’s Web-based mailing list search is often excruciatingly slow, and doesn’t work very well anyway. If you want detailed, customizable searches, it seems to me a good technique is to cut out the middleman: download all the archive files yourself, and then search them with your own text editor. BBEdit and CodeWarrior, for example, can do directory searches and grep searches. Or you could use Apple’s content indexing and search from the Finder itself. Now, with circa 56,000 files in the current cocoa-dev archive, for example, this won’t be lightning fast until we have quantum hard drives and G9 processors, but it’s not that much slower than Apple’s site, and my impression is the search will be much more accurate.

So how do you download all these files? I seem to recall, at some point in the past, making the herculean effort to download all the files by hand. This was when the archives were stored as a single file per day. I went to each archive Web page and saved each link to a file. One after another after another. Less than pleasant. Now, the archives appear to be stored as a single file per post, 30+ files for busy days!

A better solution involves an application you already have on your system, and a little help.

If you asked most Unix-heads what to use to download links from a Web page, they’ll mention wget. Mac OS X doesn’t have wget, but it has something similar: curl. To find out about curl, you can go to its official Web site, at http://curl.haxx.se/.

One thing you can find there is helpful sample scripts. The one we want is getlinks.pl, which extracts all the links from an HTML page.

So we’re done? Not quite. We still need to point getlinks.pl at the right Web pages. For that, I (with gratefully accepted help from Dan Shiovitz and Gunther Schmidl) wrote my own Perl script, applelist.pl. It and getlinks.pl (be sure to change the name of that script file, since it downloads as “getlinks.txt”) should be put in the directory where you want the downloaded mailing lists to go, and applelist.pl should be run from there with the lists you want to download as command-line arguments.

For large lists like cocoa-dev and carbon-development, the download process will take 4+ hours on a broadband connection, or at least that was my experience, and will take up circa 250 Mb. of disk space.

Note the scripts have been written to be run repeatedly: they will check for the existence of downloaded files before downloading them again. Have a look to see for yourself how it works.

Some parting thoughts:
– Yes, this solution is not for everyone, esp. people with dial-up Internet connections, little need for repeated mailing list searches, or little patience.
– The script could probably use some improvement and augmentation. Feel free to improve it yourself, since it’s in the public domain.
– One improvement would be to strip out all the email header text when saving the files, and merge the files together into single-day, single-week, or single-month files. This would probably improve search speed quite a bit, at the expense of longer first-time downloading.

Enjoy!

All Apple Mailing Lists:
http://lists.apple.com/mailman/listinfo

carbon-development

Web Recent Threads:
http://lists.apple.com/mhonarc/carbon-development/threads.html
Search:
http://search.lists.apple.com/carbon-development
Text Archives:
http://lists.apple.com/archives/carbon-development/

cocoa-dev

Web Recent Threads:
http://lists.apple.com/mhonarc/cocoa-dev/threads.html
Search:
http://search.lists.apple.com/cocoa-dev
Text Archives:
http://lists.apple.com/archives/cocoa-dev/

Exchange Files Gotcha

Summary: FSpExchangeFiles() will happily exchange two files even if one is already open for writing, which can lead to some bad behavior.

In your cool whiz-bang application, you’re implementing “Save As” with the following steps:

  • Save document data to a temporary file.
  • If file exists at “Save As” location, swap the temporary file with the real file.
  • Delete the temporary file.

This works swimmingly — unless the real file is open for writing in another application. Let’s call that app “BusyBody”.

You’d think, in that case, you’d get an OS error when you attempt to swap files, wouldn’t you? The filesystem should prevent such access when a file’s open…shouldn’t it?

Turns out it doesn’t prevent such access. It will happily swap open-for-writing files all the live-long day. You won’t get an error until you get to the last step. Then, the OS will tell you the temp file is “busy” (fBsyErr), because, as far as it’s concerned, the temp file is open in BusyBody.

So, if your app is handling errors correctly, at that point it will tell the user “Can’t do that, file’s busy.” But the damage has already been done.

When the user tries to “Save As” again to the same file — because she’s an idiot, or she’s a QA tester — the save succeeds. Why? Since BusyBody thinks it’s got that temp file open, the real file is free and clear for writing. This might not be a problem, but if BusyBody attempts to save again, instead of saving to where the user thinks it should save to, it will save to an invisible temp file. Oops!

The easiest solution to this that I can see is that, before you attempt such a switch, try to opening for writing the real file. If there’s an error with that open step, stop and signal the user, before any harm is done. That works as it should!

I wonder how many developers do the “right thing” by using FSpExchangeFiles(), but fail to check for open files first? Hopefully not many.

Note: I have not tried this with FSExchangeObjects(), though I’ll be getting to that. When I know, I’ll update this!