Saturday, December 24

Pimp My Code, Part 6: The Pimp Before Christmas

Matt W. submitted this stopwatch display class for pimping:
Original CTStopwatchView header
/* CTStopwatchView */

#import

@interface CTStopwatchView : NSView
{
NSString *myCurrentTime;
NSFont *timerFont;
IBOutlet id textDisplay;
NSImage *bgImage;
float fontSize;
}
- (void)updateWithTimeInterval:(NSTimeInterval)time;
- (NSString *)stringFromTimeInterval:(NSTimeInterval)time;
- (void)awakeFromNib;
- (void)doGradient:(NSRect)rect;
- (void)createBackgroundImage;

- (void)viewResized:(NSNotification *)notification;
- (void)viewWillStartLiveResize;
- (void)viewDidEndLiveResize;

- (void)calculateFontSize;

- (NSString *)currentTime;
- (void)setCurrentTime:(NSString *)newTime;
@end

All right, so we see a class that apparently draws a time interval in the format of HH:MM:SS on a washed background. How do we make it better? Let's go through it bit-by-bit.

This header needs a LOT of work. Why are we storing the myCurrentTime as an NSString? That's a warning sign to me right there. Time intervals should be stored as, ahem, NSTimeIntervals. It's their native format. And we see that there is in fact a method called -updateWithTimeInterval: which takes a time interval, and I think (based on my analysis of the code) that, in fact, this is the only method that ever gets called from outside the class. So this class gets called with a nice, tiny NSTimeInterval (4 bytes) and then it immediately store this as an enormous, inefficient, less-precise NSString? I'm voting no.

Whenever I see a font being cached I also think, "uh, no." Because, seriously, creating NSFonts is practically a no-op. It's not like Cocoa is actually fetching the damn font off the disk every time you ask for it. That shit is cached down to the lowest levels. So timerFont is toast, too.

I'd toast textDisplay on general principle, because if you have an NSView subclass pointing at another NSView, you've probably got some bad architecture. In this case I'm going to leave it, because I don't want to change what this class actually does, or I'm kind of failing my mission. I am going to require that we point at an NSCell instead of an 'id', because we certainly call methods on textDisplay that require it to be of type NSCell. (And, it should be renamed to something like mirroringCell if it were to stay in real life, which it wouldn't.)

bgImage? Nope. Unless you're doing some impressive-ass drawing of your background, don't cache that crap. First off, because it can be hugely inefficient; the window already caches you once, and if the window is big, then you're just double-caching a huge image, which is gonna be inefficient. And, if the window is small, well, WHAT THE HELL IS WRONG WITH YOU that you're writing background drawing code that's so slow? bgImage joins the dodo bird and honest politicians on the extinct list.

Surely I'll keep fontSize, though, right? Hah! You underestimate how much I hate instance variables. I hate them a lot. They confuse everything because their scope is global within the class file, even if they are only used in one or two methods. So, you can't easily tell where they are set, used, and modified. You have to search all over for them. Any time you can take them and make them local to a method (or two) instead, you've won. I'm downsizing fontSize.

But what about the method declarations? Well, we have three major problems here:
  1. They aren't grouped in any meaningful way,
  2. Inherited methods are redundantly declared here, and, most egregiously,
  3. Private methods appear in the header file!
I decide that -updateTimeInterval: was probably the only method that got called from the outside world, but I don't much care for the word "update", so I renamed it to something more standard for a setter method — -setTimeInterval:. Then, because I'm crazy that way, I added a matching accessor method, -timeInterval. That's how I roll, baby.

Here's my new header class. Notice how it's, like, almost empty?

PIMPED CTStopwatchView header
// CTStopwatchView

#import

@interface CTStopwatchView : NSView
{
IBOutlet NSCell *mirroringCell;

NSTimeInterval timeInterval;
}

// API
- (void)setTimeInterval:(NSTimeInterval)newTimeInterval;
- (NSTimeInterval)timeInterval;

@end

Ahh. Smell the deleted-code goodness. Smells like... readability.

"But," you exclaim, "you can't just delete code willy-nilly and expect everything to work! How are you going to back these changes up?" And my answer is: "Don't call me willy-nilly."

Here's the original class file:

Original CTStopwatchView class
#import "CTStopwatchView.h"

#define FONT_SIZE_RATIO 1.2
#define BASE_FONT_SIZE 30.0

@implementation CTStopwatchView
- (void)awakeFromNib
{
myCurrentTime = @"0:00:00";
[textDisplay setEnabled:NO];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(viewResized:)
name:NSViewFrameDidChangeNotification object:self];
[self calculateFontSize];
}

- (id)initWithFrame:(NSRect)frameRect
{
if ((self = [super initWithFrame:frameRect]) != nil) {
// Add initialization code here
[self createBackgroundImage];
}
return self;
}

- (void)drawRect:(NSRect)rect
{
NSRect bounds = [self bounds];

[bgImage compositeToPoint:bounds.origin fromRect:bounds operation:NSCompositeSourceOver
fraction:1.0];

NSShadow *shadow = [NSShadow alloc];
[shadow setShadowOffset:NSMakeSize(2.0,-2.0)];
[shadow setShadowBlurRadius:3.0];
[shadow setShadowColor:[NSColor colorWithDeviceWhite:0.0 alpha:0.7]];

NSDictionary *attrs = [NSDictionary dictionaryWithObjectsAndKeys:
[NSFont fontWithName:@"Lucida Grande" size:fontSize], NSFontAttributeName,
shadow, NSShadowAttributeName,
[NSColor whiteColor], NSForegroundColorAttributeName,
nil];

NSAttributedString *string = [[NSAttributedString alloc] initWithString:myCurrentTime
attributes:attrs];

NSSize size = [string size];

[string drawInRect:NSMakeRect((bounds.size.width - size.width) / 2, ((bounds.size.height
- size.height) / 2) + (size.height / 15), size.width, size.height)];

[string release];
[shadow release];
}

- (NSString *)currentTime;
{
return myCurrentTime;
}

- (void)setCurrentTime:(NSString *)newTime;
{
if (myCurrentTime != newTime) {
[newTime retain];
[myCurrentTime release];
myCurrentTime = newTime;
}
}

- (void)updateWithTimeInterval:(NSTimeInterval)time
{
[self setCurrentTime:[self stringFromTimeInterval:time]];
[self setNeedsDisplay:YES];
[textDisplay setTitle:[self currentTime]];
}

- (NSString *)stringFromTimeInterval:(NSTimeInterval)time
{
int seconds, minutes, hours;
hours = time / 3600;
time = time - (hours * 3600);
minutes = time / 60;
time = time - (minutes * 60);
seconds = time;

return [NSString stringWithFormat:@"%d:%02d:%02d", hours, minutes, seconds];
}

- (BOOL) acceptsFirstResponder;
{
return YES;
}

- (void)createBackgroundImage
{
[bgImage release];
NSRect bounds = [self bounds];
bgImage = [[NSImage alloc] initWithSize:bounds.size];
[bgImage lockFocus];
[self doGradient:bounds];
[bgImage unlockFocus];
}

- (void)doGradient:(NSRect)rect
{
NSColor *lightColor = [NSColor selectedControlColor];
NSColor *darkColor = [NSColor alternateSelectedControlColor];

float height = rect.size.height;

[darkColor set];
[NSBezierPath fillRect:rect];
[NSBezierPath setDefaultLineWidth:0.5];
float i;
for (i = 0; i < height; i++) {
[[lightColor colorWithAlphaComponent:(i / height)] set];
[NSBezierPath strokeLineFromPoint:NSMakePoint(0,i + 0.5)
toPoint:NSMakePoint(rect.size.width, i + 0.5)];
}
for (i = height; i > height / 2; i--) {
[[NSColor colorWithDeviceWhite:1.0 alpha:((height - i) / (height * 1.2))] set];
[NSBezierPath strokeLineFromPoint:NSMakePoint(0, i + 0.5)
toPoint:NSMakePoint(rect.size.width, i + 0.5)];
}
[lightColor release];
[darkColor release];
}

- (void)calculateFontSize
{
NSRect bounds = [self bounds];
NSDictionary *attrs = [NSDictionary dictionaryWithObjectsAndKeys:
[NSFont fontWithName:@"Lucida Grande" size:BASE_FONT_SIZE], NSFontAttributeName,
nil];

NSAttributedString *string = [[NSAttributedString alloc] initWithString:myCurrentTime
attributes:attrs];

NSSize size = [string size];
float newFontSize;
if ((size.width / bounds.size.width) > (size.height / bounds.size.height)) {
// Size text by width
float newWidth = bounds.size.width / FONT_SIZE_RATIO;
newFontSize = floorf(BASE_FONT_SIZE * (newWidth / size.width));
} else {
// Size text by height
float newHeight = bounds.size.height / FONT_SIZE_RATIO;
newFontSize = floorf(BASE_FONT_SIZE * (newHeight / size.height));
}

[string release];
fontSize = newFontSize;
}

- (void)viewResized:(NSNotification *)notification
{
[self createBackgroundImage];
[self calculateFontSize];
[self setNeedsDisplay:YES];
}

- (void)viewWillStartLiveResize
{
[super viewWillStartLiveResize];
}

- (void)viewDidEndLiveResize
{
[self createBackgroundImage];
[self calculateFontSize];
[self setNeedsDisplay:YES];
[super viewDidEndLiveResize];
}
@end

This is the first time I've actually taken the code and put it into a project and rewritten it to make sure my pimping works, because I made such extensive changes.

Let's pick this apart method-by-method, then I'll show you my rewritten file.

#import "CTStopwatchView.h"

#define FONT_SIZE_RATIO 1.2
#define BASE_FONT_SIZE 30.0
Don't define these constants here. They are not important enough to be at the top of the file, and they confuse the reader who sees them before anything else. Putting them here makes me think you're going to use them all over; whereas if you define them right when you use them, the scope is more obvious.

- (void)awakeFromNib
{
myCurrentTime = @"0:00:00";
[textDisplay setEnabled:NO];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(viewResized:)
name:NSViewFrameDidChangeNotification object:self];
[self calculateFontSize];
}
Don't bother registering for the NSViewFrameDidChangeNotification notification. You get called with -drawRect: whenever your size changes, and that's the correct time to recache any images at a new size, because the mere fact that your frame changed does NOT necessarily mean you're going to be redrawn, and you don't want to do excessive caching of images you aren't going to use. Also, locality is better. Also, less registrations is better. Also, if you're going to register for a notification, you damn well better call -removeObserver:self in your -dealloc method, which you don't do, which gums up the registration machinery and can cause crashes.

- (id)initWithFrame:(NSRect)frameRect
{
if ((self = [super initWithFrame:frameRect]) != nil) {
// Add initialization code here
[self createBackgroundImage];
}
return self;
}
Again, you should just create the background image at the last second in -drawRect: if the size has changed since the last time the view was drawn. That is, if you need to create a background image at all, which you usually don't. I'm nuking this whole method.

- (void)drawRect:(NSRect)rect
{
NSRect bounds = [self bounds];

[bgImage compositeToPoint:bounds.origin fromRect:bounds operation:NSCompositeSourceOver
fraction:1.0];

NSShadow *shadow = [NSShadow alloc];
[shadow setShadowOffset:NSMakeSize(2.0,-2.0)];
[shadow setShadowBlurRadius:3.0];
[shadow setShadowColor:[NSColor colorWithDeviceWhite:0.0 alpha:0.7]];

NSDictionary *attrs = [NSDictionary dictionaryWithObjectsAndKeys:
[NSFont fontWithName:@"Lucida Grande" size:fontSize], NSFontAttributeName,
shadow, NSShadowAttributeName,
[NSColor whiteColor], NSForegroundColorAttributeName,
nil];

NSAttributedString *string = [[NSAttributedString alloc] initWithString:myCurrentTime
attributes:attrs];

NSSize size = [string size];

[string drawInRect:NSMakeRect((bounds.size.width - size.width) / 2, ((bounds.size.height
- size.height) / 2) + (size.height / 15), size.width, size.height)];

[string release];
[shadow release];
}
The NSShadow is the same every time you create it. I'd use a class variable: having these four lines of code in -drawRect: makes the viewer scan them closely to see if anything changes in the shadow from one call to the next, which it doesn't. If something's a constant, make it clear that it's a constant by only doing it once. (Also, we're going to rewrite this method a ton when we get rid of the cached bgImage and the cached fontSize instance variables.)

- (NSString *)currentTime;
{
return myCurrentTime;
}

- (void)setCurrentTime:(NSString *)newTime;
{
if (myCurrentTime != newTime) {
[newTime retain];
[myCurrentTime release];
myCurrentTime = newTime;
}
}
Nothing too wrong with these, except I hate instance variables that start with 'my' (of COURSE it's yours, whose else would it be?) and we're keeping an NSString instead of an NSTimeInterval and your -set... method uses the large if(){} block instead of returning early AND it does not correctly call -setNeedsDisplay: or update the dependent view you defined, yet it appears to be a public method that someone might innocently call. Also, you should use -copy when retaining a string that's been passed to you, not -retain, so that, if newTime was mutable, you'll make sure it won't mutate out from under you. Note that if newTime is not mutable -copy is exactly the same code as -retain, so it's no slower to be safe.

- (void)updateWithTimeInterval:(NSTimeInterval)time
{
[self setCurrentTime:[self stringFromTimeInterval:time]];
[self setNeedsDisplay:YES];
[textDisplay setTitle:[self currentTime]];
}
This is going to become our new -setTimeInterval: method. It's ok, except for its name.

- (NSString *)stringFromTimeInterval:(NSTimeInterval)time
{
int seconds, minutes, hours;
hours = time / 3600;
time = time - (hours * 3600);
minutes = time / 60;
time = time - (minutes * 60);
seconds = time;

return [NSString stringWithFormat:@"%d:%02d:%02d", hours, minutes, seconds];
}
Too long for what it does, and it should be a private method, and it should just use the cached NSTimeInterval that's replacing the string.

- (BOOL) acceptsFirstResponder;
{
return YES;
}
This is a method subclassed from NSView. Really, really ought to say so. And be grouped with other NSView methods.

- (void)createBackgroundImage
{
[bgImage release];
NSRect bounds = [self bounds];
bgImage = [[NSImage alloc] initWithSize:bounds.size];
[bgImage lockFocus];
[self doGradient:bounds];
[bgImage unlockFocus];
}
Gone in sixty seconds.

- (void)doGradient:(NSRect)rect
{
NSColor *lightColor = [NSColor selectedControlColor];
NSColor *darkColor = [NSColor alternateSelectedControlColor];

float height = rect.size.height;

[darkColor set];
[NSBezierPath fillRect:rect];
[NSBezierPath setDefaultLineWidth:0.5];
float i;
for (i = 0; i < height; i++) {
[[lightColor colorWithAlphaComponent:(i / height)] set];
[NSBezierPath strokeLineFromPoint:NSMakePoint(0,i + 0.5)
toPoint:NSMakePoint(rect.size.width, i + 0.5)];
}
for (i = height; i > height / 2; i--) {
[[NSColor colorWithDeviceWhite:1.0 alpha:((height - i) / (height * 1.2))] set];
[NSBezierPath strokeLineFromPoint:NSMakePoint(0, i + 0.5)
toPoint:NSMakePoint(rect.size.width, i + 0.5)];
}
[lightColor release];
[darkColor release];
}
This method uses NSBezierPath's -strokeLineFromPoint:toPoint: when it really wanted to use NSRectFillUsingOperation(), which is optimized for drawing straight lines and thus a zillion times faster. Also, what you should really, really do is use CGShadingCreateAxial(), but that's a lot more code. Still, should be WAY faster, if you're worried about speed. I was, frankly, too lazy to do this, but I'll (-cough-) leave it as an exercise to the reader. (In my timing tests NSRectFillUsingOperation() was plenty fast enough.) Also, you're releasing lightColor and darkColor here, but you don't have a retain on them; methods like +selectedControlColor return a (conceptually) autoreleased instance of the color. In this case you might not be crashing because the class method is actually returning a single, unique color instance that refuses to ever deallocate itself; you lucked out.

- (void)calculateFontSize
{
NSRect bounds = [self bounds];
NSDictionary *attrs = [NSDictionary dictionaryWithObjectsAndKeys:
[NSFont fontWithName:@"Lucida Grande" size:BASE_FONT_SIZE], NSFontAttributeName,
nil];

NSAttributedString *string = [[NSAttributedString alloc] initWithString:myCurrentTime
attributes:attrs];

NSSize size = [string size];
float newFontSize;
if ((size.width / bounds.size.width) > (size.height / bounds.size.height)) {
// Size text by width
float newWidth = bounds.size.width / FONT_SIZE_RATIO;
newFontSize = floorf(BASE_FONT_SIZE * (newWidth / size.width));
} else {
// Size text by height
float newHeight = bounds.size.height / FONT_SIZE_RATIO;
newFontSize = floorf(BASE_FONT_SIZE * (newHeight / size.height));
}

[string release];
fontSize = newFontSize;
}
Way too many lines for what this does, AND we're caching a size, which I just hate. Look, if we're drawing this less than a million times a second, it's just not going to make a difference, and there's glue code all over this dang class to make sure the font size and background image and string and everything stay updated, and that stuff just makes code harder to read, harder to change, unstable, fragile, and rigid.

- (void)viewResized:(NSNotification *)notification
{
[self createBackgroundImage];
[self calculateFontSize];
[self setNeedsDisplay:YES];
}
We're nuking this method, because we do all our calculations in -drawRect:.

- (void)viewWillStartLiveResize
{
[super viewWillStartLiveResize];
}

- (void)viewDidEndLiveResize
{
[self createBackgroundImage];
[self calculateFontSize];
[self setNeedsDisplay:YES];
[super viewDidEndLiveResize];
}
@end
These last two methods actually do nothing right now, and appear to just be vestigial from when the author was trying to get resizing to work correctly. Unsurprisingly, they're nuked.

Ok, so here's the new class. Note how methods got renamed a bit and moved around according to where they come from. Note also that logic lines (like the math to calculate font sizes from the bounds) tend to be a little bit shorter. It's very important to write "logic" parts of your program in as few lines as possible, because these are the lines that are hardest to apprehend at a glance, and lead to the most errors.

PIMPED CTStopwatchView class
#import "CTStopwatchView.h"


@interface CTStopwatchView (Private)
- (NSString *)_stringFromTimeInterval;
- (void)_drawGradientInRect:(NSRect)bounds;
@end;


@implementation CTStopwatchView

static NSShadow *CTStopwatchViewTextShadow = nil;


// NSObject (class)

+ (void)initialize;
{
CTStopwatchViewTextShadow = [[NSShadow alloc] init];
[CTStopwatchViewTextShadow setShadowOffset:NSMakeSize(2.0,-2.0)];
[CTStopwatchViewTextShadow setShadowBlurRadius:3.0];
[CTStopwatchViewTextShadow setShadowColor:[NSColor colorWithDeviceWhite:0.0 alpha:0.7]];
}


// NSObject (NSNibAwaking)

- (void)awakeFromNib
{
[self setTimeInterval:2 * 3600 + 18 * 60 + 43];
[mirroringCell setEnabled:NO];
}


// NSView

- (BOOL)acceptsFirstResponder;
{
return YES;
}

- (void)drawRect:(NSRect)rect
{
#define FONT_SIZE_RATIO (1.2)

NSRect bounds = [self bounds];

// draw background wash
[self _drawGradientInRect:bounds];

// calculate font size for stopwatch text
NSString *timeIntervalString = [self _stringFromTimeInterval];
const float baseFontSize = 30.0;
NSString *fontName = @"Lucida Grande";

NSAttributedString *attributedString = [[NSAttributedString alloc]
initWithString:timeIntervalString attributes:[NSDictionary dictionaryWithObjectsAndKeys:
[NSFont fontWithName:fontName size:baseFontSize], NSFontAttributeName, nil]];
NSSize stringSize = [attributedString size];
[attributedString release];

float biggestRatio = MAX(stringSize.width / NSWidth(bounds),
stringSize.height / NSHeight(bounds));
float fontSize = (baseFontSize / biggestRatio) / FONT_SIZE_RATIO;

// draw stopwatch text
attributedString = [[NSAttributedString alloc] initWithString:timeIntervalString
attributes:[NSDictionary dictionaryWithObjectsAndKeys:[NSFont fontWithName:fontName
size:fontSize], NSFontAttributeName, CTStopwatchViewTextShadow, NSShadowAttributeName,
[NSColor whiteColor], NSForegroundColorAttributeName, nil]];
stringSize = [attributedString size];

[attributedString drawInRect:NSMakeRect((NSWidth(bounds) - stringSize.width) / 2,
(NSHeight(bounds) - stringSize.height) / 2 + stringSize.height / 15, stringSize.width,
stringSize.height)];

[attributedString release];
}


// API

- (void)setTimeInterval:(NSTimeInterval)newTimeInterval;
{
if (ABS(timeInterval - newTimeInterval) < 0.1)
return;

timeInterval = newTimeInterval;
[self setNeedsDisplay:YES];
[mirroringCell setTitle:[self _stringFromTimeInterval]];
}

- (NSTimeInterval)timeInterval;
{
return timeInterval;
}

@end


@implementation CTStopwatchView (Private)

- (NSString *)_stringFromTimeInterval;
{
return [NSString stringWithFormat:@"%d:%02d:%02d", (int)(timeInterval / 3600),
((int)timeInterval % 3600) / 60, (int)timeInterval % 60];
}

- (void)_drawGradientInRect:(NSRect)bounds;
{
[[NSColor alternateSelectedControlColor] set];
NSRectFill(bounds);

NSColor *lightColor = [NSColor selectedControlColor];
float height = NSHeight(bounds);
unsigned int row;
for (row = 0; row < height; row++) {
[[lightColor colorWithAlphaComponent:((float)row / height)] set];
NSRectFillUsingOperation(NSMakeRect(0, row, NSWidth(bounds), 1), NSCompositeSourceOver);
}

for (row = height; row > height / 2; row--) {
[[NSColor colorWithDeviceWhite:1.0 alpha:((height - row) / (height * 1.2))] set];
NSRectFillUsingOperation(NSMakeRect(0, row, NSWidth(bounds), 1), NSCompositeSourceOver);
}
}

@end
This rewrite is a lot fewer actual lines of code and a lot more whitespace and comments. I think it's easier to read and modify without sacrificing speed.

Thanks to Matt submitting his code to such cruelty. You're a braver man than me, sir.

Can you do better on this pimping? I only had so much time, and I think this could be a little pimpier. What's your input?

Labels:

Thursday, December 22

Pause for These Important Messages [category: philosophy]

I recently tried to write a piece on how discouraged I am with the current administration in the United States. I came up against a couple limits. One was that I'm too passionate about the issues, so I can't talk about the current American president without using cuss-words, which would undermine my credibility. The other is I am damn busy/lazy to do all the research necessary to write a really compelling essay; I can wave my hands and say, "We have corporations running the EPA and destroying the actual air we breath," but that kind of statement is easily ignored by people who have already made up their minds against me; they just assume I'm playing politics by using vague, non-disprovable assertions.

Well, a reader forwarded me an essay from Robert F. Kennedy, Jr. that is amazing to me. Robert talks the way I wish I talked. And he's done the research I wish I had the time and/or non-laziness to do. He cares about this stuff even more than I do; he makes it his life. I've never been a fan of anything with the name "Kennedy" before, but now I think their whole clan is justified by this one man.

I don't think you can disagree with this speech. I don't care who you voted for. You simply can't say, "Yes, I want to be slowly poisoned in a world that's dying, so that 8 or 10 guys can be a little bit richer."

I know it's cheap to just post links in one's blog. But, honestly, Robert's said it better than I can, than I ever could. I bow to him. I thought there weren't people like this left in the world; people who speak without blustering, passionate yet civil. I haven't been this moved since reading Thomas Jefferson. (And, for the record, I think Michael Moore is an annoying blow-hard, no matter how much he and I might agree on GWB2.)

Please read this. At least part of it. Read the middle if you don't have much time. Skim it. If you disagree, flame me all you want. But read it first.

Wednesday, December 14

5P3LL1NG B33? [category: drivel]

Custom license plates carry with them the stigma of pretentiousness, which I think is unfairly earned for a couple of reasons. First, in Washington state at least, the $75 custom plate fee goes to wildlife conservation, so if someone blaaaarts by you in a Civic with a 6" pipe and a whale-fin straight off a '93 Supra and his plate proclaims him "2FAST", at least you can think, "Well, that's another bird saved AND another bird earned," as you flip him off.

Second, I don't understand why attaching mall-bought plate frames or mass-made bumper stickers with trite slogans on them to your car is considered OK, but if you attempt to express your own individuality by picking a message for yourself, it's considered an act of hubris. You'd rather be sailing? Damn. That's deep, man.

Thus it is that I recently purchased my first custom license plate, of which I was inordinately proud.

Was, that is, until I went to the Child's Play charity dinner last night, at which I attempted and ultimately failed to buy my way to fame, losing by $4,000 to the gentleman stockbroker who happened sitting next to me, and who had happened to have been hit by a diesel truck when he was 13 and spent 5 years under the knife at Children's. The man had a karmic debt to pay, he said, and far be it from me to stand in his way. Also, $20K is a lot of cabbage. At least we were able to give Gabe a five-foot cardboard tube filled with Pocky, and Tycho a two-foot Pepperidge Farms sausage of DOOM, as a way of thanking them for doing some much good.

So, at the end of the evening, dejected and sad from my failed bid at immortality, my possé made its way to the parking lot under Meydenbauer center, where Mary spotted a car with the same license plate as mine, except with an alternate spelling! Imagine my chagrin! I couldn't have been more embarrassed if we'd been wearing the same dress. Clearly, a gamer at the auction was a man with a mien like mine.

I leave it to you, gentle readers, to decide which spelling you prefer.


vs.

Monday, December 12

Sock Puppet Marketing...

Just today I was taught the wonderful term "Sock Puppeting," which is when someone creates extra accounts to post in online forums and agree with themselves: "Oh, man, I totally support Bob's crazy position... he's so smart!"

Well, I'd like to introduce the term "sock puppet marketing," which is when someone from a company reviews his own products and fails to disclose he's not a neutral party.

I started noticing that every time I see Delicious Library mentioned on any site on the web, someone inevitably posts a comment about MediaMan, which is a program for Windows that completely ripped off our interface. Like, enough so that we should sue. I mean, go ahead, go to their site, and look at their shelves. Tell me that's not copyright infringement. And I should mention, as it will come up in a bit, that MediaMan used to be free, but now costs $39.95. (Seriously... they even copied our price.)

Now, sure, some mentions of competitors are to be expected, but we have several competitors on Mac OS X and several more on Windows and Linux, yet the one I keep seeing mentioned in the comments, over and over, is MediaMan.

So let's look at some web forums I found that wrote articles about Delicious Library, and the comments left on those sites by user(s):

From Delicious Construction Kit | B.Mann Consulting
Delicious Idea!
Submitted by Christoph (not verified) on July 4, 2005 - 6:45am.

Very interesting thoughts, Boris. The idea of creating new libraries is really good, and so is the "post to blog" feature.

BTW, did you know that there's a similar software for Windows users? => MediaMan at www.imediaman.com.

Best regards

Christoph

Hmm. Sure, Christoph could just be a fan of MediaMan. I mean, hey, people recommend Delicious Library all the time, so people could be recommending MediaMan.

Wait, though...

From Vestal Design Blog: Deliciousmonster, not Delicious Monster

1 Comments:


Christoph said...

There's a similar software available for Windows users - MediaMan. Just in case this is of interest to you.

5:12 PM

Wow, that Christoph likes MediaMan so much so that he was the only person to post a response here. It seems like now matter how little the site, Christoph is the man on the scene if they mention Delicious Library...

From VMUNIX Blues » Delicious Monster and Delicious Lattes
Christoph  |  June 29th, 2005 at 5:20 pm

Just in case you’re interested, a similar piece of software is available for Windows:

MediaMan

I think it looks & feels so good, you could think you’re using a Mac. ;-) And it’s free, BTW.

Boy, you can't beat that free price, can you, Christoph! I mean, if it were true. And you were one of only two people who left comments here! But, I wonder, did you catch that mention of Delicious Library in "Richard's Notes"?

From Richard's Notes >> Blog Archive >> Delicious Monster
Christoph says:
June 29th, 2005 at 8:25 PM

Richard, David,

there's a similar piece of software for Windows called MediaMan. It works with all available Amazon stores US, UK, Germany, Japan, France & Canada. And it's even free! :)

Regards

Christoph

Free! Wow! That sounds great, except it's not.

--

Now, I invite the reader to click on Christoph's name up there, to see what Christoph set as his home page in Blogger. It's a live link that I copied from the actual web pages. Christoph's home page is imediaman.com. Notice he doesn't mention any affiliation with that company in his text, when he was gushing over how great the look and feel of MediaMan is.

Could this Christoph be none other than Christoph Janz, whose e-mail is <cjanz@imediaman.com>, according to this now-deleted, Google-cached Word document I found from iMediaMan site? (And is this the same Christoph Janz of the blog Christoph Janz on Web 2.0)?

Honestly, that's not a rhetorical question... it seems likely to me, but I can't know, of course. And it's not like I'm accusing Chistoph of doing anything illegal here, although if he were working for iMediaMan back in June or July, I believe what he did highly unethical. Some might find it just to be a clever marketing, but they'll certainly never get a job with my companies.

--

But, hey, the phrase "Sock Puppet" is funny, like, "Monkey" or "Galooly."

Labels: , ,

Saturday, December 10

The Silly Season.

Ok, normally I'm not in the rumor-debunking business. Because, well, I like a good rumor as much as the next guy, and, unbelievably, Apple doesn't actually call me up and tell me all their plans any more often than they call you. (Note to Bertrand Serlet: Ok, they call you a lot, but not the rest of us.)

Also, it's a bad idea for a developer to say anything about rumors, pro or con, because even touching the field gets one the reputation as a purveyor of rumors, and then all your Apple friends stop talking to you, lest they be seen associating with someone of your caste.

And, heck, it is the lead-up to MacWorld Expo (booth 710!), so we should expect some crazy rumors. Hell, the ones I believed the least have been the ones to come true, recently. For instance, up until the midnight before WWDC 2005 I was still explaining to all my friends why Apple would never announce Intel processors at a developer conference one year in advance. (Note to friends: sorry about that.)

Plus, the whole reason people plant fake rumors is so they can point out how many times it gets mentioned, so my mentioning it is really just feeding their ego.

But, seriously, sometimes a rumor is SO OBVIOUSLY A FAKE that it makes me grit my teeth, and when I see people discussing it I just want to grab them all and conk their heads together.

So it is with the Dharma / Yellow Box rumor currently mentioned on a surprising number of sites. Let's take this apart, shall we?

Starting with the really obvious:

1) The person says the project's new codename is "Dharma" just after it was revealed to be the codename of the project in "Lost", which just showed up in iTunes. The person signs his name as "John Locke," who is a fictional character from a TV show and "somewhere near Hawaii," which is the state in which Lost is filmed.

Oh, but wait, some will say, what if that's just his clever way of having fun with the secrecy around this project? And what if the project name is a double-triple cross, like that one episode of X-files where the government uses pro wrestlers as their men in black, so nobody ever believes you when you try to tell them you were visited by them?

Well, then, how about:

2) Real rumors don't spend most of their text justifying why they make sense. If someone has real information, they just say it. A real rumor would be more of the form, "someone close to the project told me at a conference that they've revived the Yellow Box..." not, "As we all know, in the beginning of computer history there were many architectures, which determine the lowest-level language that a computer understands... blah blah blah... and THAT's why you should believe me about Dharma."

Or, maybe you'd believe:

3) The letter-writer explains that he's leaking this information, anonymously, to a French site because he's afraid of the DMCA. Except, you know, he's anonymous. And he's leaking the information from the United States anyways, so he could still get nabbed if he weren't. And U.S.-based rumors sites are still going full-swing, so it's not like they wouldn't run his piece because it's too-hot-for-television.

Or,

4) Bertrand Serlet is supposed to be running the project, while he's also Senior V.P. of Software at Apple. Yes, he's got time to be running a project which is essentially just a tune-up of an old compatibility layer. That'd be a good use for a V.P. Also, he's in the States, although he does still have a great French accent.

Or,

5) IT MAKES NO SENSE FOR APPLE TO BRING BACK YELLOW BOX AT THIS POINT.

Seriously, people. Apple doesn't WANT your current Intel machine to run Mac OS X software. If it could, they wouldn't be able to sell you a new machine in June of 2006. Trace the dollars! How would Apple profit from this?

It's been demonstrated (by the previous Yellow Box) that big developers won't just write their software once for Yellow Box and then "call it good" on Windows. Hell, Adobe can hardly be convinced to come out of the CFM closet, much less dump their Windows codebase in favor of a Cocoa one.

Sure, it'd be great for small developers. Heck, I think mine was the only company in the world to actually SELL consumer software bundled with Yellow Box in the old days (OmniWeb for Windows!), and, to be truthful, most of the copies we sold were from big organizations who wanted Yellow Box for their custom applications and discovered it was cheaper to buy OmniWeb for Windows and just use its Yellow Box license than to buy licenses from Apple.

But, Apple doesn't really need to attract small developers to Cocoa. They've got a pretty rich small developer community already. Small developers are starting to figure out on their own that using Microsoft's tools to write software for Microsoft's operating system and then compete with Microsoft's monolithic consumer apps is putting your head in the lion's mouth. (If you disagree, let me make you a deal: I'll sell you all the tools you need to write software to compete with Delicious Library. Just send me a check for $600 every year and, uh, I'll get back to you... maybe. I totally promise the tools will be really great, too. Really.)

--

Now, I should give the standard disclaimers: I don't have any inside information, I've just been in the industry for a long time. Maybe I'm wrong. Maybe, come January, Apple will announce they're releasing Yellow Box and it's at a low enough price that we developers can bundle with it and it's small enough that we don't have to require users to download and install 5GBs of libraries with our apps.

And, I promise, this will be the LAST RUMOR I EVER BOTHER TO DECONSTRUCT.

Until the next one.

Labels: ,