Tagged: Cocoa

Untied III: Furniture (Table) Polish

I used my last post to concentrate on the features I could implement with a data source-backed table, but not with a Cocoa bindings-backed table.

But the Category02.zip sample code from my last post has more to it than that.

For instance, in the data source-backed table, if you add a category – which starts with a blank name – and hit return without typing anything in, it deletes the category completely, under the assumption that you must not have wanted it if you weren’t going to give it a name.

However, if you erase the name of an existing category and hit return, it reverts it to the old name, just like the Finder does. This is a nice “cancel editing” shortcut, since Undo may not take you back all the way to the original name if you type enough.

One bit of polish I didn’t implement, but which I would want in a real app, is Escape key support. If you hit the Escape key or Command-period, it should abort editing.

I have a tendency to overdo my sample applications, so I think I’ll stop here with the category table features. Next up: the detail view of “thing” entries. (See the original post for the overall description of the Categorizer application.)

Untied II: Table Data Source Tricks

Last time around, I concentrated on what I could and couldn’t get a Cocoa bindings-based NSTableView to do for me.

Today, I’m going to do everything I mentioned last time, using the data source and delegate methods in listed in NSTableView.h.

As a starting parameter, I said that I wanted a special, unmodifiable, always-on-top Category entry in my table, called “All”. Making it uneditable is as easy as implementing the method tableView:shouldEditTableColumn:row: in my delegate class, and making it return NO if row == 0. I like this way better than making an “editable” Category accessor, because it puts the responsibility where it belongs, on the specific table that has the special entry, rather than putting a hack in the Category class to handle it.

Then, the issues. First up was sorting. I sort the contents of my Categories table the way I want, without having the pesky user being able to screw it up, by manually sorting the table’s contents in the data source method that’s called every time the user adds or changes a table entry, tableView:setObjectValue:forTableColumn:row:.

Because I never set the Sort Key in Interface Builder, as far as the table itself is concerned, the contents are completely unsorted. There’s no ascending or descending triangle in the column header.

Scott Anguish, in the comments of “Untied I”, suggested overriding NSArrayController to get the same behavior, and with another tweak (see that post’s comments) that does indeed get me exactly the same behavior. I implemented his suggestion in the Bindings part of the project that comes with today’s post. I complained in my reply that clicking on the column header still ends the editing session. Just a minor annoyance, to be sure, but the Mac is all about getting things exactly right. But it turns out my data source implementation has the same problem, so it’s a draw.

Second was editing, in three parts: starting an editing session immediately on add, resorting the entry immediately after the editing session ends, and beeping and not ending the editing session if the name is invalid.

To understand how I start the editing session, you need to know how I implement the action newCategory:. Before, this invoked the NSArrayController’s add method, which was asynchronous. Now, it does all the data model manipulation itself, and so it can go ahead and start the editing session without any worries afterward.

Resorting the array takes place, as stated above, in the data source method tableView:setObjectValue:forTableColumn:row:. The same code that does it when you first add a category also works when you rename a category.

For the last part, editing verification is available by implementing the NSControl delegate method control:textShouldEndEditing:. This method is not called if I’m using bindings.

The sample is called Categorizer02.zip.

Untied: When Bindings Aren’t Enough

I’ve found, when I try to use Cocoa bindings for my user interface, that it’s never quite flexible enough for what I need.

Instead of trying to list the hurdles I’ve encountered in the abstract, I’m going to walk you through the first steps of a simplified example, called Categorizer, which is also available for download. This way, you can see the things I’ve figured out how to do, in addition to the dealbreakers.

Categorizer initial table

The Categorizer’s data model consists of Things and Categories.

The UI pictured above is the master table, of Categories. The detail table, of Things in the selected Categories, will be to the left. We won’t get to the detail table today.

The first category, "All," is special: you can’t delete it or rename it. It will contain all the things, and is always listed at the top of the category table, which is otherwise alphabetically sorted. You "categorize" things by dragging them onto the rows you’ve made yourself in the category table.

Sorting
The first issue is sorting. As I said above, categories need to be sorted alphabetically except for the "All," which is always on top.

NSArrayController will automatically sort its array – the contents of the table – according to the Sort Key and Sort Selector of the columns in the table, which are set in Interface Builder. The Sort Key needs to be set to something, it can’t be left blank if I want sorting. So I can’t, for example, try to sort on the entire Category class; I need to pick an attribute. Normally I’d pick something like "name," but in my case I want to into account the special "All" status and name. So I make a "specialSortKey" Category method and specify that in IB.

The table doesn’t start out sorted. I fix this not by manipulating the table, but by manipulating the NSArrayController. I get the sortDescriptorPrototype from the column, stick it in an NSArray, and use it as the parameter for a setSortDescriptors: message to the controller. (See the download for the exact code.) This also selects the column header and sets the indicator image (the little up or down arrow).

Turns out, though, that I don’t want this to act like a normal table column. I want the contents to be sorted the same way all the time, so I don’t want the up or down arrow in the column header that the user can toggle between. Because this would put "All" at the bottom, for one. But because of the array/controller integration, it’s basically not possible to have one without the other if I want to use an NSArrayController. This, it turns out, is dealbreaker #1.

Editing
I also want specific behavior when adding a new category. The standard NSArrayController behavior, if I connect the "Add" button to the controller’s add: action, is that a new, empty instance is added to the array and to the table. What I want, in addition, is that you automatically start editing that instance.

My first thought is to connect the "Add" button to a method in my own delegate, newCategory:. This method invokes the controller’s add: method and then starts the editing session.

Unfortunately, the new row is not yet made by the time add: returns. I’m not sure why this would be an asynchronous process, but it is. Oh, wait, here we go, from the documentation: "Note: In Mac OS X v10.4 the target-action methods are deferred so that the error mechanism can provide feedback as a sheet."

I then try implementing MyDelegate’s key-value coding method insertObject:inCategoriesAtIndex: so that, after it does its business, it initiates editing of the added row. But for some reason (maybe because it’s a horrible hack) that doesn’t work either.

This, so far, is dealbreaker #2.

After I’m done editing, I want the renamed category to be re-sorted properly. This is pretty easy, but still involves a gotcha. On 10.4, the controlTextDidEndEditing: NSTableView delegate method is never invoked when you double-click a table cell and edit it in a table that uses an NSArrayController. In my tests, it seems to still work in a table that doesn’t use an NSArryController.

Luckily, there’s another Cocoa API in a superclass of NSTableView, NSControl’s controlTextDidEndEditing:. If I define that in my table delegate, I can use it to send the rearrangeObjects message to my controller, and the renamed category will be immediately resorted.

My last editing issue, and last issue for this post, since it’s getting long, is uniqueness. I don’t want you to be able to enter the same category name twice. If you try to end editing with an invalid name, I want the UI to beep and continue the editing session.

I try to do this in the key-value coding way by implementing the "name"-specific validation method validateName:error: in Category. There’s some finagling, since now Category needs to be able to access the full list of categories in MyDelegate. But, with that achieved, Category’s validateName:error: now returns NO if the new name is the same as any other name.

Can I make it beep? Nope. My only choice of error feedback for the user is an error dialog. There are plenty of options in NSError so I can customize what the dialog says, but no option to do something else.

Dealbreaker #3.

The download, called Categorizer01.zip, contains the work I’ve described so far, including the behavior I couldn’t get working right. It’s an Xcode 2.1 project, sorry, it can’t be opened in Xcode 2.0.

If my schedule permits, soon I will make a second Categorizer, one that doesn’t use Cocoa bindings, that customizes the behavior as I wanted.

Size Matters: Fixed-Height/Width Windows in Interface Builder

Has anyone else seen this?

Make a new Cocoa application project in Xcode 2.1. Open the MainMenu.nib file. Show the inspector for the window in that nib, and switch to the Size panel. Try to make the minimum and maximum sizes for the window the same, either by typing in numbers or hitting the “Current” buttons.

Can’t do it, can you? Whenever you try, Interface Builder automatically “fixes” the maximum value so it is 1 greater than the minimum value. The hell? What if I want the window not to be able to resize vertically or horizontally? It does this for every window, in every nib.

And while 1 pixel may not seem like much, it is definitely visible to the end user, which makes the whole interface look unpolished.

The workaround is to set the min and max values to the same thing programmatically, but that’s a pain in the ass, eh?

I didn’t see anyone complaining about this on the Apple mailing lists. Did this used to work? Am I missing something? Is this just a small enough deal that people live with it? (I’ve filed a bug.)

It also doesn’t look like I can make a little utility based on nibtool to fix this after I’ve made the nib in Interface Builder; while nibtool can show me the offending min/max values in text form, I can’t modify the text, then feed it back into nibtool to generate a changed nib.

I am using the Cocoa UI APIs and Interface Builder again after a hiatus, and I’m finding far more of these workarounds necessary than I would like for a “best of breed” technology.

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.

Saving My Bacon

I never used to be a fan of auto-saving.

The last time I really experienced it was with Word, probably back on OS 9. The documentation for Word said that auto-saving would make your file bigger, since Word would not save the whole file, but only a log of your most recent changes. And there was a noticeable pause each time the auto-save occurred that was really annoying.

No thanks, I thought.

But when a perpetrator in my household who shall remain nameless jiggled the power strip enough to turn it off when I was writing a blog post in MarsEdit, after the cursing was over, I did think to myself, “It would be nice if MarsEdit had auto-saved that.”

I hadn’t read the MarsEdit documentation, and I hadn’t experienced any pauses while I worked, so I had no inkling that MarsEdit was in fact auto-saving my drafts.

When I reopened MarsEdit, it notified me that it had detected an auto-saved file, and showed me that file.

Rock on!

A brief survey of the MarsEdit documentation doesn’t reveal anything about this feature. I should know this, but can Cocoa do this automatically now? Is it so mundane that it doesn’t warrant mention?

More interestingly, does the auto-save only kick in when there’s a lull in user activity? That would be cool. Otherwise, I guess I can chalk up the paucity of pauses to faster processors and hard drives.

File Imprecations

There’s no dedicated widget in Cocoa for specifying and displaying a file.

Both Cocoa and Carbon apps tend to cobble together custom UI for the task. A “Save” or “Open” button that triggers a Navigation Services dialog, plus a path text field, and sometimes an icon. Apps tend to be internally consistent – CodeWarrior uses the same aggregate widget everywhere, for instance – but none of them quite match each other.

Cocoa apps often do let you edit the path by hand. It seems to me that this reflects the greater NeXT reliance on the command line, and I can see the utility of it sometimes. But I dislike the potential to erase a path by hitting the delete key by mistake. It also seems to me that this sort of thing is better suited to developer apps, where the user can more safely be assumed to have command line expertise.

Carbon apps, with the old Toolbox deprecation of file paths, generally never allow users to type in the path of the file they want: you must use the Open or Save dialogs to specify the file, though the application might then show you the path afterward.

I can actually see something in-between: maybe in the Save/Open dialog, there is a text field, which reflects the full path of the file/folder you’ve currently chosen. You can type in it, and once a name matches, the Navigation Services portion of the dialog moves to that location specified by your typing. Maybe that’s overkill, but it is the kind of marriage of command-line and UI that I’d really like to see. Toss in tab-completion, and you’re all set!

I’m thinking about this because I’m thinking about how I would modify the UI of TADS Workbench for Macintosh if (more like when) I decide to take over its maintenance. I had my own widget in my Isthmus framework to specify and show a file – I even had a specialization of that widget to deal with special file paths, like the “{Compiler}” and “{Project}” paths in CodeWarrior. I will either have to redo that work for TADS Workbench, discard the concept completely, or – most likely – figure out a more scaled-down version. After all, it was trying to make things perfect that doomed Isthmus.