Sunday, April 23
Tonight the gang and I (not to be confused with "Cool and the Gang") went to go see Thomas Dolby at the Fenix here in Seattle. I've never been to the Fenix, as I'm not a huge club-goer (shocking!) and I actually haven't been to a concert since, uh,
Pink Floyd in 1989.
It's a pretty small venue, and there was apparently another, much louder band downstairs. Still, it was a great show. Thomas (and I think I can call him Thomas, because, uh, he doesn't read this and thus can't complain) really does have the funk.
It was pretty strange to watch him up on stage, with hundreds of adoring fans, because, you know, I've met him and talked to him a couple times. It's not like I'd pretend he's my friend; in fact, if anything, I'd say he probably finds me kind of annoying, since every year I try to start conversations with him at TED and he's always trying to escape to the other side of the room. (I'd like to point out, in his defense, I'm usually kind of drunk and my conversations are all, like, "Hey, ever think about making more music?" which I'm sure he
never gets.)
So I thought about that tonight, as I swayed to his phat beats and clapped and hollered myself hoarse... here I am, enjoying myself to the tunes by someone who I actually
know doesn't particularly like me. Ok, that sounds conceited, let me back up a bit.
My friend Mike said something really smart about someone we know who acts irrationally a lot: "Remember, he's the star of his movie." Each of us is. We have a movie of our lives going on in our heads, and we're always the star. I'm Peter in Family Guy, not Quagmire. I'm Samuel L. Jackson in every movie he's ever made, and Michael Caine in every movie that's left. I don't understand why I don't always get the girl and solve the crime, because
I'm the star, dammit.
I think when we worship celebrities it's because we think, "Damn, I could hang with him! He'd like me! We'd have some beers, talk about chix..." But what do you do when you've met the celebrity and he pretty much was nonplussed with you?
Don't get me wrong... it's totally his or anyone else's right to not like me. I'm not mad at him about this. It just leaves me in this odd situation -- here's a person I've worshipped forever; how do I reconcile that with his disinterest once I've met him?
My favorite bit from the movie "Adaptation" was where the neurotic twin tells the fat twin how the girl that the fat one had had a crush on in high school always laughed at the fat one behind his back... the fat one said, "I know." Why did you continue to love her, then? The answer, and I have to paraphrase, was that his love wasn't contingent on it being returned; because that's not love.
So, Thomas Dolby rocked. And I enjoyed his show, and thought he was awesomely cool. And, sure, there's part of me that still thinks, if I
really got to know him, he'd want to hang with me.
But, if not, that's ok. He's allowed his privacy. I'll still go to his concerts, because he's a great musician, and he speaks to me, and that's what matters.
Labels: stories
Saturday, April 15
While I'm
almost always a cheerleader for Cocoa (and Apple in general) in public, there are, of course, boneheaded things in Cocoa that grind at me every time I run up against them. Some annoyances exist for historical reasons (eg, Why does NSImage have twenty different ways to draw itself, each of which has slightly different semantics?), and some are apparently boundary problems between engineering groups (eg, Why doesn't NSImage have a way to create itself from a CGImageRef? Why are NSRect and CGRect exactly the same yet different? Why can't I ask an NSCachedImageRep for its CGContextRef?).
And, of course, some things are just plain old slap-your-head-duh mistakes...
Bindings, for instance, are my second-favorite feature in all of Cocoa (after CoreData). They're great. I love 'em. So much so that when any little part of them is less-than-perfect, it's incredibly grating.
NSTreeController is a new class designed to drive an NSOutlineView using bindings, which parallels NSArrayController's ability to drive an NSTableView. (Not surprisingly, they are both subclasses of NSController.) However, NSTreeController's design has a number of problems, most of which stem from this very questionable design decision (from Apple docs, emphasis mine):
- (id)arrangedObjects; // opaque root node representation of all displayed objects. This is just for binding to or passing around. At this time, developers should not make any assumption about what methods this object responds to.Let me clarify this: if you hook an NSTreeController up to your object model, and you populate your tree with instances of class, say, "Shelf" (just picking a COMPLETELY random example), then the
-arrangedObjects method will return... any guesses? Maybe, instances of class "Shelf"? Wrong! In fact, it returns objects of type "nothing-you-can-understand."
*Cough*Apoordesignsayswhat*cough*?
What the heck is going on here? Apparently someone at Apple decided it'd be easier to create strange little shadow objects that point to your REAL objects when you put real objects into an NSTreeController, but because that's such an ugly thing to do they decided not to document these little shadow objects, so you can't actually DO anything with them.
This is kind of like not signaling when making an illegal left turn, with the notion that if you don't signal it's not, you know, wrong.
Here are a bunch of problems that fell out from the the opaque-shadow-object design of NSTreeController:
• First, there is no
-[NSTreeController setSelectedObjects:] method, because, of course, the objects YOU have access to are not the same ones that the ones that NSTreeController is messing with. So, uh, good luck setting the selection in code!
Oh, wait, there's
-setSelectionIndexPaths:, which takes an 'NSIndexPath'... except there are two problems with this method, which unfortunately make it almost entirely useless. One is, NSIndexPath is a pretty obtuse class and it's always immutable, meaning if you want to build up a path to a particular object you're going to be writing a ton of code. The other is, if you change the NSTreeController's 'sortDescriptors' or its 'contents', any NSIndexPath you've socked away is invalidated, so you can't, say, store a selection and then re-create that selection later with any certainty.
Nor can you really select a particular object, because, uh, how do you know the path to your object when NSTreeController's opaque arrangedObjects can be in a different order than your source objects? Ok, sure, you can read off the sort order and replicate the sort on your objects, and build up a NSIndexPath to an object by also reading off the method NSTreeController is going to use to discover the children of your objects, as well... does this sound like a pain to you? Well, it is.
• Next, it's almost impossible to figure out how to get an NSTreeController to 'select none' or 'select all'. These should ideally take one line of code, and do with NSArrayController.
But when trying to select nothing on a NSTreeController, one cannot easily construct an NSIndexPath that is the empty set; all NSIndexPath creation methods are similar to
+indexPathWithIndex:, which obviously creates a path that already has one entry (eg, is not empty), so you'd have to do something like
[[NSIndexPath indexPathWithIndex:0] indexPathByRemovingLastIndex], which is fugly beyond all belief. If you call
[NSIndexPath indexPathWithIndexes:NULL length:0] it crashes. (Possibly
[[[NSIndexPath alloc] init] autorelease] works, I have to admit I didn't think of it until just now and it's very late and I've pretty much had it with screwing around with NSIndexPath.)
On the other hand, when trying to select all, you have to manually create an array containing a butt-ton of NSIndexPaths to cover every node in your tree! Ugh! And, since NSTreeController returns an OPAQUE objects, you again have to re-create the sorting and children-getting that NSTreeController is going to use when you're creating your array of indexPaths, which honestly takes a lot of the fun out of having an object in NIB.
• Similarly, almost all NSOutlineView delegate methods are almost useless, which means there's really no easy way to implement drag and drop. Consider the following NSOutlineView callbacks:
- (NSDragOperation)outlineView:(NSOutlineView*)outlineView
validateDrop:(id )info proposedItem:(id)item proposedChildIndex:(int)index;
- (NSDragOperation)outlineView:(NSOutlineView*)outlineView
validateDrop:(id )info proposedItem:(id)item proposedChildIndex:(int)index;
- (BOOL)outlineView:(NSOutlineView*)outlineView acceptDrop:(id )info
item:(id)item childIndex:(int)index;Now consider that the 'item' parameters above are of an opaque class! Wait, what EXACTLY are you proposing? You'd like to drop this pasteboard onto an item in my NSOutlineView whose class is undocumented, so I have no idea where this drop is happening? Uh, let's see... Yes? No? Maybe? 3? Kentucky?
Similarly, it's hard to implement this NSOutlineView delegate method:
- (BOOL)outlineView:(NSOutlineView *)outlineView
shouldEditTableColumn:(NSTableColumn *)tableColumn item:(id)item;When, again, you can't actually look at the item that might be edited. I mean, if you don't actually CARE which PARTICULAR item is going to be edited, then you'd assumedly set the whole damn NSOutlineView to be editable or not, yes? But if you've gone to the trouble to implement this method, I'm guessing you are actually going to want to, you know, QUERY the 'item' a bit, ask about its family, see if it's been tested... make sure that it's really the kind of thing you want to be editing.
--
Ok, phew. I'm sure some engineer at Apple hates me now. (Sorry, overworked engineer dude. You can make fun of my code if you'd like.)
And, in fact, we have a nickname for people at Delicious who complain about stuff and don't
do anything to make the situation better. So, for the record, I did, in fact, file a very detailed bug report with Apple on this in RADAR, so I'm not just complaining to the crowd - I honestly can't stand it when people report bugs in my stuff to the net instead of me, and I wouldn't do that to Apple.
But that's not enough, in my mind. I shouldn't just complain, I should fix this! Why don't I post some code that works around all these ills, so everyone can use NSTreeControllers the way they were intended, until Apple fixes the API in Liger (right, Apple?). Hey, good idea, me. I like the cut of your jib.
I have two categories here, one for NSOutlineView and one for NSTreeController. They contain different methods but some similar code, and I admit that I probably could merge their implementations a bit if I tried, but I also admit that there's only so much cleaning I'm going to do on code I've written to work around API limitations that I'm SURE will go away in the next release. But, please, if you do come up with better versions of these, post 'em in the comments or send 'em to me. They are appreciated!
First off, here's a category of 'NSTreeController' that adds a
-setSelectedObjects: method which takes your REAL objects - not the phony shadow ones - from anywhere in your content tree, and also adds an
-indexPathToObject:: method which will tell you where one of your REAL objects is in the NSTreeController given the controller's current sorting and children-getting-method and all that, so you don't have to get your hands all sticky with NSIndexPath gunk.
|
// NSTreeController-DMExtensions.h // Library // // Created by William Shipley on 3/10/06. // Copyright 2006 Delicious Monster Software, LLC. Some rights reserved, // see Creative Commons license on wilshipley.com
#import <Cocoa/Cocoa.h>
@interface NSTreeController (DMExtensions) - (void)setSelectedObjects:(NSArray *)newSelectedObjects; - (NSIndexPath *)indexPathToObject:(id)object; @end
// NSTreeController-DMExtensions.m // Library // // Created by William Shipley on 3/10/06. // Copyright 2006 Delicious Monster Software, LLC. Some rights reserved, // see Creative Commons license on wilshipley.com
#import "NSTreeController-DMExtensions.h"
@interface NSTreeController (DMExtensions_Private) - (NSIndexPath *)_indexPathFromIndexPath:(NSIndexPath *)baseIndexPath inChildren:(NSArray *)children childCount:(unsigned int)childCount toObject:(id)object; @end
@implementation NSTreeController (DMExtensions)
- (void)setSelectedObjects:(NSArray *)newSelectedObjects; { NSMutableArray *indexPaths = [NSMutableArray array]; unsigned int selectedObjectIndex; for (selectedObjectIndex = 0; selectedObjectIndex < [newSelectedObjects count]; selectedObjectIndex++) { id selectedObject = [newSelectedObjects objectAtIndex:selectedObjectIndex]; NSIndexPath *indexPath = [self indexPathToObject:selectedObject]; if (indexPath) [indexPaths addObject:indexPath]; } [self setSelectionIndexPaths:indexPaths]; }
- (NSIndexPath *)indexPathToObject:(id)object; { NSArray *children = [self content]; return [self _indexPathFromIndexPath:nil inChildren:children childCount:[children count] toObject:object]; }
@end
@implementation NSTreeController (DMExtensions_Private)
- (NSIndexPath *)_indexPathFromIndexPath:(NSIndexPath *)baseIndexPath inChildren:(NSArray *)children childCount:(unsigned int)childCount toObject:(id)object; { unsigned int childIndex; for (childIndex = 0; childIndex < childCount; childIndex++) { id childObject = [children objectAtIndex:childIndex]; NSArray *childsChildren = nil; unsigned int childsChildrenCount = 0; NSString *leafKeyPath = [self leafKeyPath]; if (!leafKeyPath || [[childObject valueForKey:leafKeyPath] boolValue] == NO) { NSString *countKeyPath = [self countKeyPath]; if (countKeyPath) childsChildrenCount = [[childObject valueForKey:leafKeyPath] unsignedIntValue]; if (!countKeyPath || childsChildrenCount != 0) { NSString *childrenKeyPath = [self childrenKeyPath]; childsChildren = [childObject valueForKey:childrenKeyPath]; if (!countKeyPath) childsChildrenCount = [childsChildren count]; } } BOOL objectFound = [object isEqual:childObject]; if (!objectFound && childsChildrenCount == 0) continue; NSIndexPath *indexPath = (baseIndexPath == nil) ? [NSIndexPath indexPathWithIndex:childIndex] : [baseIndexPath indexPathByAddingIndex:childIndex];
if (objectFound) return indexPath; NSIndexPath *childIndexPath = [self _indexPathFromIndexPath:indexPath inChildren:childsChildren childCount:childsChildrenCount toObject:object]; if (childIndexPath) return childIndexPath; } return nil; }
@end |
Believe me,
-setSelectedObjects: is handy as heck. Sewiously.
Next up, an NSOutlineView category that adds one crucial method:
-realItemForOpaqueItem:, so when you're given an item from bizarro-world in an NSOutlineView delegate or datasource callback, you can just turn it into one of your objects, and speak to it in
your language. You can thank me later! (Or, hell, thank me now; honestly, I'm not super-particular about the whole 'thanking' thing.)
|
// NSOutlineView-DMExtensions.h // Library // // Created by William Shipley on 3/10/06. // Copyright 2006 Delicious Monster Software, LLC. Some rights reserved, // see Creative Commons license on wilshipley.com
#import <Cocoa/Cocoa.h>
@interface NSOutlineView (DMExtensions) - (void)setSelectedObjects:(NSArray *)newSelectedObjects; - (NSIndexPath *)indexPathToObject:(id)object; @end
// NSOutlineView-DMExtensions.m // Library // // Created by William Shipley on 3/10/06. // Copyright 2006 Delicious Monster Software, LLC. Some rights reserved, // see Creative Commons license on wilshipley.com
#import "NSOutlineView-DMExtensions.h"
@interface NSOutlineView (DMExtensions_Private) - (NSTreeController *)_treeController; - (id)_realItemForOpaqueItem:(id)findOpaqueItem outlineRowIndex:(int *)outlineRowIndex items:(NSArray *)items; @end
@implementation NSOutlineView (DMExtensions)
- (id)realItemForOpaqueItem:(id)opaqueItem; { int outlineRowIndex = 0; return [self _realItemForOpaqueItem:opaqueItem outlineRowIndex:&outlineRowIndex items:[[self _treeController] content]]; }
@end
@implementation NSOutlineView (DMExtensions)
- (NSTreeController *)_treeController; { return (NSTreeController *)[[self infoForBinding:contentAttributeKey] objectForKey:@"NSObservedObject"]; }
- (id)_realItemForOpaqueItem:(id)findOpaqueItem outlineRowIndex:(int *)outlineRowIndex items:(NSArray *)items; { unsigned int itemIndex; for (itemIndex = 0; itemIndex < [items count] && *outlineRowIndex < [self numberOfRows]; itemIndex++, (*outlineRowIndex)++) { id realItem = [items objectAtIndex:itemIndex]; id opaqueItem = [self itemAtRow:*outlineRowIndex]; if (opaqueItem == findOpaqueItem) return realItem; if ([self isItemExpanded:opaqueItem]) { realItem = [self _realItemForOpaqueItem:findOpaqueItem outlineRowIndex:outlineRowIndex items:[realItem valueForKeyPath:[[self _treeController] childrenKeyPath]]]; if (realItem) return realItem; } }
return nil; }
@end |
Let me finish by saying that I really don't like being negative towards Apple, both because I actually don't enjoy picking on other people's work unless they ask me to do it, and because Apple engineers have done so much incredible stuff that I would
never have thought of myself, so it seems disingenuous to complain if some little bit of it is non-optimal.
On the other hand, I also don't want to come off as an indiscriminate cheerleader for anything and everything with Apple's imprimatur; that wouldn't help me or Apple or anyone who is considering following my advice. If an API is bad, I need license to say that it's bad, because otherwise it means nothing when I say that something good is good.
But on the OTHER other hand (
the gripping hand, as it were), I know that Apple's detractors tend to seize upon any scrap of criticism that one of the faithful might express towards Apple and blow it out of proportion - witness the recent spate of witless articles about the supposed "backlash" (their term) against Boot Camp, which is really about as tight a product as you can get and is free and IS A FREAKING BETA, AND IF YOU HAVE A PROBLEM WITH THE NOTION OF PUTTING WINDOWS ON YOUR MAC JUST DON'T DO IT, DON'T START A FREAKING PETITION LIKE A WHINEY LITT...
Ok, I'm calm now. At any rate, please don't quote me as saying, "I've had it with Cocoa! Apple sucks!" Cocoa and bindings and CoreData are 90% of what makes Delicious Library 2 so tiggity-tight, and I simply could not have created Library 1 or 2 without them. Period. I won't port to Windows because I can't, not because I hate people who use Windows. It's just too hard. It's not like people TRY to make bad software for Windows. It
ends up bad because if you have to spend all your time just fighting to get the most basic functionality working, of COURSE you're not going to have any time to polish.
So, much love to my homies on the bindings team. Love to
all my Cocoa brothers and sisters.
And, please, fix NSTreeController for Liger. Sewiously!
Labels: code
Monday, April 3
A number of people (two is a number!) have asked me for links to Al Gore's incredibly moving speech on the very poorly-named "global warming" crisis. Unfortunately, I didn't have such a link.
Until NOW. Dum dum dum!
Honestly, going into Al's presentation (and I think I know him well enough to call him Al, assuming
he will call me Betty), I thought listening to a wooden dude drone on about the weather getting
nicer for an hour was going to be the most deadly-dull thing I'd ever done. I thought, "Oh, boy, I love being lectured about what a bad person I am for living and breathing and how we're all going to heck and there's nothing I can do. It brightens up my day!"
I was wrong. Al's presentation was funny, enlightening, and most of all moving. He presented raw data, pictures, and yet more data, and yet more pictures, all of which points to our world being irreversibly changed in the next several years, for the much, much worse. There is a vast and overwhelming amount of evidence for this, and it is cited for your perusal if you are skeptical. The social and economic fallout will be devastating to life as we know it. This isn't about us enjoying lovely sunshine year-round -- this is about hurricanes, plagues of insects, massive species die-offs, new diseases -- real armageddon stuff, folks.
Most scientific reports I've seen now agree that the Sahara desert was created by man, thousands of years ago. We did that. We had too many cattle, we overgrazed the land, it dried out and started a cycle that perpetuated itself and even accelerated. The giant sand trap in middle of Africa, visible from space, that has plunged that entire nation into poverty for thousands of years... that was our fault.
Let's learn from our mistakes. Just this once, when it matters most.
PS: Go see Snakes on a plane it's going to rock.
--
Update:
The trailer is finally out.Labels: stories