I just can't figure out how to bind a Cocoa NSMatrix properly.
I want to make a set of radio buttons that look like this on screen:
- "Computer Name and Time"
- "Text"
- "File"
- "URL"
I want this set of radio buttons to be associated with a preference called "textMode" and take on the string values "date", "literal", "file", or "url". That is, when the first radio button is selected, I want the "textMode" preference set to the string "date", and not to the string "Computer Name and Time". Likewise, when the window comes up, I want the first radio button to be the selected one if the "textMode" preference already has the value "date".
I'm making the radio buttons like this: <lj-cut text=" --More--(24%) ">
- NSButtonCell *proto = [[NSButtonCell alloc] init];
[proto setButtonType:NSRadioButton];
NSMatrix *matrix = [[NSMatrix alloc]
initWithFrame:rect
mode:NSRadioModeMatrix
prototype:proto
numberOfRows:4
numberOfColumns:1];
NSArrayController *cvalues =
[[NSArrayController alloc] initWithContent:nil];
[cvalues addObject:@"date"];
[cvalues addObject:@"literal"];
[cvalues addObject:@"file"];
[cvalues addObject:@"url"];
[matrix bind:@"content"
toObject:cvalues
withKeyPath:@"arrangedObjects"
options:nil];
[matrix bind:@"selectedObject"
toObject:userDefaultsController
withKeyPath:@"values.textMode"
options:nil];
Now how do I set the damned labels? Based on some cargo-cult googling, I tried this, and it doesn't work:
- NSArray *cnames = [NSArray arrayWithObjects:
@"Computer Name and Time",
@"Text", @"File", @"URL", nil];
[matrix bind:@"contentValues"
toObject:cnames
withKeyPath:@"arrangedObjects.title"
options:nil];
Where is this magic "arrangedObjects" string documented, and what does the second word in the string actually mean?
I also tried this, and it makes the strings show up, but also causes the radio buttons to come up with none of them selected by default, regardless of the value of the preference. Clicking on them does appear to set the preference, however, which is weird:
- NSCell *cell;
cell = [matrix cellAtRow:0 column:0];
[cell setTitle:@"Host Name and Time"];
cell = [matrix cellAtRow:1 column:0];
[cell setTitle:@"Text"];
...etc.
Update: I finally just gave up, and bound it to "selectedIndex", meaning my preference takes on a numerical value instead of a string, and then I have magic knowledge about what those numbers mean in a couple places. This is annoying, but I didn't get any simple suggestions for how else to fix it. (I got a few really complicated suggestions, that may or may not have worked, but I didn't try them.)
Bindings are weird. Ask cocoa-dev?
'arrangedObjects' is an accessor inside NSArrayController that is used to get at the objects (the arranged objects by sort order and predicate - by default, there's no sort order or predicate, so you just get every object). arrangedObjects is seemingly undocumented at first. It's used in examples in the documentation and is listed here: http://developer.apple.com/documentation/Cocoa/Conceptual/CocoaBindings/Tasks/onerelation.html#//apple_ref/doc/uid/20002301
The key path starts with the NSArrayController, gets its arrangedObjects (here, it'll try bindings as well as accessors), and asks that result, in turn, for its title (since the result is an array, the array is smart enough to ask its contained objects for the key 'title' and return all results in an array. This whole system is called Key-Value Coding and it's an integral part of how bindings tick in the first place.
I've spent an hour experimenting with getting this to work, and it seems that it's really tricky. The trick seems to be to use an NSValueTransformer subclass to swap in/out the name and defaults value in the correct binding, but so far I've had no luck.
Who designs these APIs? Why are they so insane?
I'm fairly sure that the people who designed the bindings system were at the very least criminally insane. The big problem with bindings is that they aren't as of yet very flexible, and that a lot of controls simply have poor bindings support (like NSMatrix). Luckily, OS X 10.4 has shown some very tangible improvements over 10.3 when it comes to bindings, and I hope 10.5 continues down this road in the fall.
At the end of the day, bindings is based on an ingenious idea - to formalize very common glue code (the controller part in MVC) and make it part of the framework. It's not unlike Perl in its philosophy - to make easy things easy and hard things possible. It'll take some time to mold this into something you can actually use with as little resistance as possible, and it's a pain in the ass a lot of the time because you simply don't think the same way about things as bindings require you to think, but it's a brave effort that is already something I - and many others who do it for a living - can't live without. I hope it succeeds.
Well, I'll definitely admit that bindings and NSMatrix are not as straightforward as binding to other objects such as NSTableViews.
What's important is that NSMatrix expects you to bind each button to an object. And, if you want, you can provide a key *within*that*object* to use as the title. If you do not provide that key, it just calls -description on the object you bound, and uses that.
So, in your example, you want to just use a string as the object, and since there is no key/value pair within a string to use as a title, you get both.
To do what you want to do, you need to bind the content key of your NSMatrix to dictionary objects, and each dictionary can have a key-value pair that provides a title. It's probably a lot easier to see than to do, so I wrote up a quick project that does what you're looking for and I've uploaded it here:
http://www.blakeseely.com/source/NSMatrixBindings.zip
The real challenge in what you want to do is storing a preferences value that is BOTH just a simple string AND different than the contentValue (your title). I would recommend either using the title as your preferences value, or storing a dictionary with both a title value and a "prefsValue" and the prefsValue key holds the string you're using. I've implemented this second method in the project above - the preferences holds a dictionary that has two keys - one is the title, the other is the "prefsValue" that you want to use above. If you instead decided to go with just a single string, you could bind the selectedValue instead of selectedObject - this would store just your title string instead of a whole object. (also, instead of using the button title as your prefs, or instead of using the whole object, you can bind to the currently selected tag - which is an integer you can specify for each object, or you can bind to an index, which obviously depends on the order of your buttons).
But, the bottom line is that you bind an object for each button, and the title needs to be the value of a key WITHIN that object - so using two totally separate strings like you're trying to do won't work.
If that doesn't make sense, or if you have more questions, drop me a line at blakeseely at mac dot com.
Actually, I was wrong in what I said above - you can get exactly the behavior you're looking for. You will still need to put the two string values you want inside another object - I used an NSDictionary. But then you can get a title and prefs value that are both simple strings. I updated my code to do exactly this and it's at the same url as above.