Main

December 09, 2007

Always - on Print Preview

Some software is, in my experience, not quite WYSIWYG when it comes to the final printed output so I have gotten into the habit of viewing a PDF in preview before I actually send a document to my printer. This has saved many trees.

Apple makes it fairly easy to do this. Just command-P and click on the weird-looking PDF button to get the drop down menu to select "Open PDF In Preview."

preview.jpg

I would like to see most every print job automatically open in preview without needing to take my hands off of the keyboard. One can make this happen with a little bit of effort, some help from Folder Actions, and a little piece of free third-party software. Here's how I do it.

First, download and install Cups PDF per the instructions on its home page. This will create a printer and driver that dumps a pdf file into ~/Desktop/cups-pdf/. Set your default printer to CUPS-PDF.

Next, you will setup a folder action on the ~/Desktop/cups-pdf/ to open each file that gets dumped in there in preview. I keep my folder actions scripts in ~/Desktop/Folder\ Actions/, so I put the following applescript (derived from a similar script that ships with Leopard) there.

  
(*
add - new item alert

This Folder Action handler is triggered whenever items are added to the attached folder.
The script will display an alert containing the number of items added and offering the user
the option to reveal the added items in Finder.

Copyright © 2002–2007 Apple Inc.

You may incorporate this Apple sample code into your program(s) without
restriction. This Apple sample code has been provided "AS IS" and the
responsibility for its operation is yours. You are not permitted to
redistribute this Apple sample code as "Apple sample code" after having
made changes. If you're going to redistribute the code, we require
that you make it clear that the code was descended from Apple sample
code, but that you've made changes.
*)

property dialog_timeout : 5 -- set the amount of time before dialogs auto-answer.

on adding folder items to this_folder after receiving added_items
try
tell application "Finder"
--get the name of the folder
set the folder_name to the name of this_folder
end tell

-- find out how many new items have been placed in the folder
set the item_count to the number of items in the added_items
--create the alert string
set alert_message to ("Folder Actions Alert:" & return & return) as Unicode text
if the item_count is greater than 1 then
set alert_message to alert_message & (the item_count as text) & " new items have "
else
set alert_message to alert_message & "One new item has "
end if
set alert_message to alert_message & "been placed in folder " & «data utxt201C» & the folder_name & «data utxt201D» & "."
set the alert_message to (the alert_message & return & return & "Would you like to view the added items?")

display dialog the alert_message buttons {"Yes", "No"} default button 1 with icon 1 giving up after dialog_timeout
set the user_choice to the button returned of the result

if user_choice is "Yes" then
tell application "Preview"
--fire it up
activate
--open the items
open the added_items
end tell
end if
end try
end adding folder items to

Ctrl (or right) click the cups-pdf folder, Enable Folder Actions, then Configure Folder Actions via the contextual menues seen below.


EnableFolderActions.jpg

When you select "Configure Folder Actions" you'll set the script that runs every time a new file is added to that folder to the one above.

folderActions.jpg


Now every time you hit command - P to print in a application, you'll hit enter to print to your default CUPS-PDF printer, which will write a PDF to the cups-pdf folder on your desktop. The folder action will then pop-up this window (for five seconds).

PreviewMe.jpg


All you have to do is hit enter.

So! Every time I print I hit command - P, enter, wait a sec, enter. Instant preview.

December 03, 2007

Poking around in others' software is sometimes useful (and thanks to Zorn)

I need an NSNumberFormatter subclass for a PyObjC project I'm working on that reformats an NSNumber to hours:minutes:seconds. Thinking I've seen this before (and kind of hoping that there was some voodoo I was missing somewhere to make this simple), I decided to poke around in applications that deal with time. After a little head-scratching, I was reminded of that most useful time-tracking and invoicing application I've grown to love called Billable (Zorn!). On this screenshot (from the Clickable Bliss site) we see a field labeled "Time Spent:" with a "Start" button next to it. Thinking to myself, "I want that formatter!" I fired up F-Script Anywhere, injected it into Billable and dug down until I was the class name for the formatter. CBTimeLengthFormatter.jpg Time to pray to google. Ah! Zorn! You beautiful helpful coding-type person. You've pasted it for us. Thank you! Now all that is left is to pythonify it. (I made a few modifications to the behavior, but it's the same general idea)
#
#  CBTimeLengthFormatter.py
#  PuppyTracker
#
#  Created by Jonathan Saggau on 12/3/07.
#  Copyright (c) 2007 __MyCompanyName__. All rights reserved.


from Foundation import *
from math import floor

#modified from http://paste.lisp.org/display/21854
class CBTimeLengthFormatter(NSNumberFormatter):

    def stringForObjectValue_(self, anObject):
        if (not (anObject.isKindOfClass_(NSNumber))):
            return(None)
            
        if (anObject.intValue() <= 0):
            return("00:00:00")
        intval = int(floor(anObject))
        
        hours = intval / (60*60)
        minutes = (intval - (hours * 60 * 60)) / 60
        seconds = intval - (minutes * 60) - (hours * 60 * 60)
        string = "%02i:%02i:%02i" %((hours), (minutes), (seconds))
        return(string)
    
    def getObjectValue_forString_errorDescription_(self, objVal, inString, err):
        """Take a string like "00:00:00" and turns it into a NSNumber and returns YES
           Also able to handle 10:10 (as 10 minutes, 10 seconds) and 10 (as 10 seconds)"""
        
        string = NSString.stringWithString_(inString)
        
        #catch for nil or empty string
        if (string == None or string.isEqualToString_("")):
            return True, 0, None
        
        stringList = string.split(":")
        #make seconds first, instead of hours
        stringList.reverse()
        
        #turn each into an integer, filtering out empty strings
        try:
            stringList = [int(each) for each in stringList if each is not u'']
            
        #if we can't make any part of this into an int, bail
        except ValueError, e:
            return False, 0, None
        
        #make sure we have Seconds, Hours, Minutes by padding the list with zeros
        #in case we have (say) Seconds, Hours only
        while ( len(stringList) < 3):
            stringList.append(0)
        
                        #sec            #min               #hour
        timeInSeconds = stringList[0] + stringList[1]*60 + stringList[2]*60*60
        return True, timeInSeconds, None

November 24, 2007

Living with multiple Macs

I have three machines running os X:
  • My Macbook Pro for travelin' to them clients; it lives wherever I am.
  • My iMac for writing code on a big screen; it lives on my desk
  • My Mini for watching movies, listening to music, serving web pages, serving mail, serving my calendars; it lives in my Middle Atlantic rack (highly recommended rack manufacturer, by the way. Nice stuff.)
Synchronizing my data (especially my home directly) has recently become much more important to me. I've been spending rather more time physically at clients and can't afford the "I think I edited that on the desktop machine, which is turned off at home" problem. For code, this is handled quite nicely by subversion, but I also need a simple way to synchronize my home directories between my laptop and my desktop. (I don't so much care about the contents of my home directory in my server because I don't really produce content there). My friend Noah Gift, who really is a gift to my world would tell you to use nfs and apple's offline synchronization to do this for you. This is fancy and fine voodoo, and something I used to do way back when I used Linux primarily (gasp) but I didn't really want to serve my home directory from a central location. Version controlled home is also overkill for me and dealing with conflicting files in svn can be a bit of a pain (yes, Hal, I thought about your suggestion as well). I really want my laptop to act kind of like my iPhone. I want to come home, plug in, deal with a few conflicts if and only if I care at that point, and sync without having to also think.
What is need is a simple directory synchronizer. Ok. Shouldn't be hard to find, right? Wrong. Every (payware, even) GUI file synchronizer I've tried has ended in the spinning beachball of death. Grumble Grumble. Ok again. Time to slide off into command-line utilities. I thought maybe I would use git instead of svn, but I don't want to use git (yet). I'm resistant to use anything designed for version control for this purpose and I'm too lazy to learn yet another one, even though git is supposed to be very fast at resolving differences between trees. Maybe later. Sorry Linus.
Enter a little-known synchronizing tool called Unison. I Love this software.
From the Unison site:
  • Unison runs on both Windows and many flavors of Unix (Solaris, Linux, OS X, etc.) systems. Moreover, Unison works across platforms, allowing you to synchronize a Windows laptop with a Unix server, for example.
  • Unlike simple mirroring or backup utilities, Unison can deal with updates to both replicas of a distributed directory structure. Updates that do not conflict are propagated automatically. Conflicting updates are detected and displayed.
  • Unlike a distributed filesystem, Unison is a user-level program: there is no need to modify the kernel or to have superuser privileges on either host.
  • Unison works between any pair of machines connected to the internet, communicating over either a direct socket link or tunneling over an encrypted ssh connection. It is careful with network bandwidth, and runs well over slow links such as PPP connections. Transfers of small updates to large files are optimized using a compression protocol similar to rsync.
  • Unison is resilient to failure. It is careful to leave the replicas and its own private structures in a sensible state at all times, even in case of abnormal termination or communication failures.
  • Unison has a clear and precise specification.
  • Unison is free; full source code is available under the GNU Public License.
In other words, exactly what I need. :) Good instructions for setting it up for this purpose are available on the Linux Journal site. It's easy to keep it from synchronizing certain directories (I leave ~/Library and ~/svnCheckouts alone, for example) and it's easy to keep it from synchronizing certain file types as well (.DS_Store, .Spotlight*, .Trashes, etc.). If I reorganize my entire home folder on the train, that reorganization is mirrored on the desktop. No muss. No fuss. No "svn move."
So! When I come home, I shutdown my laptop (it's usually sleeping in my bag), restart holding the T key, which turns it into a very expensive external drive, connect a firewire 800 cable between it and my desktop and synchronize. Happy me.

No more Finder, no more Dock

I don't use the either anymore.
I find the Terminal sufficient for most large-scale file movings (especially with the help of tree). Path Finder is where I live to browse my filesystem. Launching applications (also searching the web and making coffee) belongs to Quicksilver, which was recently open-sourced!
FYI: tree compiles on OSX Leopard if you make the following very small changes to tree.c:
--- ./tree-1.5.1.1/tree.c
+++ ./tree-1.5.1.1JS/tree.c
@@ -17,7 +17,7 @@
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

-#include 
+//#include 
#include 
#include 
#include 
@@ -182,7 +182,7 @@
#ifdef CYGWIN
extern int MB_CUR_MAX;
#else
-extern size_t MB_CUR_MAX;
+extern int __mb_cur_max;
#endif

int main(int argc, char **argv)

November 11, 2007

More Leopard calDAV fun. Migration!

As you know from my last post, I have lots and lots of calendars. It used to be much much worse. Sharing calendars between my three Macs before Leopard server was a pain in the ass. Sure, you could put a calendar on a regular DAV server and subscribe to that calendar on your other machines, but you could not write to that calendar from any machine other than that which created it initially. All subscribed machines were read-only. What does this mean to me? You know about my 14 calendars, right? Triple it. That's right. I used to have 42 calendars on each of my 3 machines. 14 calendars to which I could write and 28 to which I was subscribed so I could read any appointments from my other two machines. What a mess.

Leopard calendar server has brought me back to a debatably sane 14 calendars. But how does one migrate all of those calendars spread across multiple machines onto the shiny new calDAV server? Like so:

1. Subscribe to your shiny new calDAV server (under the Accounts tab) in iCal preferences.
2. Create a new calendar on your calDAV server through iCal (File Menu -> new Calendar -> your server) for each of your 14 calendars.
3. Create a *.ics file from your old calendar by:
a. Selecting the calendars you want to export by using the export functionality in iCal (File Menu -> Export) on each of your machines or...
b. Mounting your DAV server and downloading all of 'em at once (assuming you're keeping your calendars on a DAV server now, which I was, this method is a lot easier for obvious reasons)...
4. Select the calendar from the source list (one residing on your new calDAV server) and import the *.ics files you wish to store in the new calendar (File Menu -> import).

Your appointments are now on your calDAV server and will show up on all of your subscribed machines fully editable from all.

November 06, 2007

Leopard server, calDAV, and Mozilla Sunbird

For those wishing to use Mozilla Sunbird with the calDAV server that comes with Leopard, it's not setup in Sunbird in (quite) the same way as it is in iCal.

Let's say you have the calDAV Account URL working in ical (which will subscribe you to all of your calendars) and its URL is

 https://somedomain.com:8443/principals/users/username/ 

In Sunbird, you'll have to subscribe to the your calendars individually. Every calDAV user I've setup so far in Leopard server has been given a default calendar called simply "calendar", which you can subscribed to from Sunbird at

https://somedomain.com:8443/calendars/users/username/calendar/
Easy enough. However, if you add a new calendar in iCal, it will create a calendar named with a UUID. In order to figure out what that UUID is, it's easiest to just mount the calDAV in the finder (Command - K) at
https://somedomain.com:8443/calendars/users/username/
You'll see several directories in there: calendar, dropbox, oubox, notifications, etc. The one that looks like a UUID (for example, 037D3206-9C1D-4C6C-8E54-B3E3CAF90ABF) is the directory you'll want to subscribe to in Sunbird. In the example above, you would subscribe to
https://somedomain.com:8443/calendars/users/username/037D3206-9C1D-4C6C-8E54-B3E3CAF90ABF/

October 26, 2007

Yeah, it breathes

Wow. Alex.SpeechVoice is creepy. Apple's latest "read your email to you" speech synthesizer breathes (!) before it (he?) starts speaking. It's also, by far, the largest file in my installation of Leopard.

See?

LepprSpace.jpg

Update:

The software generating the view above is called GrandPerspective and I love it. Thanks to DeRay for emailing me about this.

September 04, 2007

MarsEdit 2 is suhweet

Having been a MarsEdit user since before its Red-Sweater-ization, of course I just bought me an upgrade for that there brand spankin' new MarsEdit 2.0. It's awesome, as expected.

Daniel Rocks.

Carry on, punkass.

August 14, 2007

iPhone Native Pong Application: the real "sweet" SDK

The native iPhone hack I put together for the C4[1] conference is in the process of a "wow, I hope somebody can read this code someday" cleanup. I will release the source in the coming days. Stay tuned for a download link.

In the meantime, here are a couple of teasers.

Screenshots:

scrnsht1.jpg


scrnsht2.jpg


And here is some code for playing a sound file from your native iPhone Application's bundle (with the class-dump'd headers you'll need, too). The iPhone, as was mentioned by everyone with a native hack at C4, has some really intelligent API. Hats off, ladies and gentlemen, to the iPhone team. They must be having a grand ol' time internally playing with this thing.



//
// PongAudio.h
//
// Created by Jonathan Saggau on 2007-08-12.
// Copyright (c) Jonathan Saggau All rights reserved.
//

#import <Foundation/Foundation.h>
@class AVItem;
@class AVController;
@class AVQueue;

@interface PongAudio : NSObject
{
AVItem *bounce;
AVItem *loser;
AVQueue *q;
AVController *controller;
}

-(void)playBounce;
-(void)playLoser;

-(void)stop;
@end

//
// PongAudio.m
//
// Created by Jonathan Saggau on 2007-08-12.
// Copyright (c) 2007 __MyCompanyName__. All rights reserved.
//

#import "PongAudio.h"
#import "AVItem.h"
#import "AVController.h"
#import "AVQueue.h"

//From Aaron hillegass
#define LogMethod() NSLog(@"-[%@ %s]", self, _cmd)

@interface PongAudio (PrivateAPI)
-(void)play:(AVItem *)item;
@end

@implementation PongAudio


- (id)init
{
self = [super init];
if (nil!= self)
{
NSError *err;
NSString *path = [[NSBundle mainBundle] pathForResource:@"PongBounce" ofType:@"wav" inDirectory:@"/"];
bounce = [[AVItem alloc] initWithPath:path error:&err];
if (nil != err)
{
NSLog(@"err! = %@ \n item = [[AVItem alloc] initWithPath:path error:&err];", err);
exit(1);
}

path = [[NSBundle mainBundle] pathForResource:@"PongLose" ofType:@"wav" inDirectory:@"/"];
loser = [[AVItem alloc] initWithPath:path error:&err];
if (nil != err)
{
NSLog(@"err! = %@ \n item = [[AVItem alloc] initWithPath:path error:&err];", err);
exit(1);
}

controller = [[AVController alloc] init];
[controller setDelegate:self];
q = [[AVQueue alloc] init];
[q appendItem:bounce error:&err];
if (nil != err)
{
NSLog(@"err! = %@ \n [q appendItem:item error:&err];", err);
exit(1);
}

[q appendItem:loser error:&err];
if (nil != err)
{
NSLog(@"err! = %@ \n [q appendItem:item error:&err];", err);
exit(1);
}
}
return self;
}

- (void)dealloc
{
[bounce release]; bounce = nil;
[loser release]; loser = nil;
[q release]; q = nil;
[controller release]; controller = nil;
[super dealloc];
}

-(void)playBounce
{
[self play:bounce];
}

-(void)playLoser
{
[self play:loser];
}

-(void)play:(AVItem *)item;
{
[controller setCurrentItem:item];

//play NOW
[controller setCurrentTime:(double)0.0];
//should probably check this eventually, too.
//- (BOOL)isCurrentItemReady;
NSError *err;
[controller play:&err];
if(nil != err)
{
NSLog(@"err! = %@ [controller play:&err];", err);
exit(1);
}
}

-(void)stop;
{
[controller pause];
}

- (void)queueItemWasAdded:(id)fp8
{
LogMethod();
}
@end


//AVController.h
/*
* Generated by class-dump 3.1.1.
*
* class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2006 by Steve Nygard.
*/


#import <Foundation/Foundation.h>
#import "CDStructures.h"

@interface AVController : NSObject
{
struct AVControllerPrivate *_priv;
}

+ (void)setEnableNetworkMode:(BOOL)fp8;
+ (id)avController;
+ (id)avControllerWithQueue:(id)fp8 error:(id *)fp12;
- (id)initWithError:(id *)fp8;
- (void)setAVItemClass:(Class)fp8;
- (id)init;
- (void)dealloc;
- (struct AVControllerPrivate *)privateStorage;
- (BOOL)isNewImageAvailableForTime:(const CDAnonymousStruct1 *)fp8 willNeverBeAvailable:(char *)fp12;
- (long)copyImageForTime:(struct __CVBuffer **)fp8 time:(const CDAnonymousStruct1 *)fp12;
- (void)itemPropertyDidChangeNotification:(id)fp8;
- (void)scheduleQueueSpaceCheck;
- (void)checkQueueSpace;
- (void)queueItemWasAdded:(id)fp8;
- (void)queueItemWasAddedNotification:(id)fp8;
- (void)queueItemWillBeRemovedNotification:(id)fp8;
- (void)cancelPrepareForPlayback;
- (void)setQueue:(id)fp8;
- (id)queue;
- (void)feederRangeWasInserted:(id)fp8;
- (void)feederRangeWasRemoved:(id)fp8;
- (void)feederInvalidatedWithCurrentItemMoved:(id)fp8;
- (void)setQueueFeeder:(id)fp8 withIndex:(int)fp12;
- (void)setQueueFeeder:(id)fp8;
- (id)queueFeeder;
- (BOOL)setRepeatMode:(int)fp8;
- (int)repeatMode;
- (BOOL)havePlayedCurrentItem;
- (id)currentItem;
- (id)currentItemPriv;
- (void)setCurrentItem:(id)fp8 preservingRate:(BOOL)fp12;
- (void)setCurrentItem:(id)fp8;
- (void)makeCurrentItemReady;
- (BOOL)isCurrentItemReady;
- (void)continueAfterRepeatGap;
- (void)cancelContinueAfterRepeatGap;
- (void)doCancelContinueAfterRepeatGap;
- (void)doScheduleContinueAfterRepeatGap:(id)fp8;
- (BOOL)beginRepeatGap;
- (void)itemFailedPlaying;
- (void)itemFinishedPlaying:(id)fp8;
- (void)itemCompletedDecode;
- (BOOL)play:(id *)fp8;
- (void)pause;
- (void)dequeueFirstItem;
- (unsigned int)indexOfCurrentQueueFeederItem;
- (BOOL)setIndexOfCurrentQueueFeederItem:(unsigned int)fp8 error:(id *)fp12;
- (id)addNextFeederItemToQueue;
- (BOOL)playNextItem:(id *)fp8;
- (float)rate;
- (BOOL)shouldBeginPlayingItem:(id)fp8 error:(id *)fp12;
- (BOOL)setRate:(float)fp8 error:(id *)fp12;
- (BOOL)resumePlayback:(double)fp8 error:(id *)fp16;
- (id)errorWithDescription:(id)fp8 code:(long)fp12;
- (void)makeError:(id *)fp8 withDescription:(id)fp12 code:(long)fp16;
- (BOOL)beginInterruption:(id *)fp8;
- (BOOL)activate:(id *)fp8;
- (void)endInterruptionWithStatus:(id)fp8;
- (float)volume;
- (void)setVolume:(float)fp8;
- (double)currentTime;
- (void)setCurrentTime:(double)fp8;
- (BOOL)muted;
- (void)setMuted:(BOOL)fp8;
- (void)setEQPreset:(int)fp8;
- (int)eqPreset;
- (struct OpaqueFigVisualContext *)visualContext;
- (void)setVisualContext:(struct OpaqueFigVisualContext *)fp8;
- (id)outputQTESFilePath;
- (void)setOutputQTESFilePath:(id)fp8;
- (id)lkLayer;
- (void)setLayer:(id)fp8;
- (id)attributeForKey:(id)fp8;
- (BOOL)setAttribute:(id)fp8 forKey:(id)fp12 error:(id *)fp16;
- (struct _LKImageQueue *)lkImageQueue;
- (struct _LKImageQueue *)lkEnsureQueueForWidth:(unsigned int)fp8 Height:(unsigned int)fp12;
- (double)lkServerTime;
- (BOOL)okToNotifyFromThisThread;
- (void)fmpTimeJumped;
- (void)fmpRateDidChange;
- (void)rateDidChangeToRate:(float)fp8;
- (void)repeatModeHasChanged:(int)fp8;
- (void)currentItemWillChangeToItem:(id)fp8 oldItemCurrentTime:(double)fp12;
- (void)currentItemHasChanged:(id)fp8;
- (void)itemHasFinishedPlayingNotification:(id)fp8;
- (void)resynchronizeTiming;
- (id)delegate;
- (void)setDelegate:(id)fp8;
- (BOOL)setItemAttribute:(id)fp8 value:(id)fp12 forKey:(id)fp16 error:(id *)fp20;
- (id)itemAttribute:(id)fp8 forKey:(id)fp12;
- (id)initWithQueue:(id)fp8 error:(id *)fp12;
- (BOOL)isValid;
- (id)initWithQueue:(id)fp8 fmpType:(unsigned long)fp12 error:(id *)fp16;
- (void)applyAttributesFromItem:(id)fp8;
- (void)fmpRelease:(id)fp8;
- (void)failPlayback:(id)fp8 reason:(long)fp12 notifyClient:(unsigned char)fp16;
- (void)prepareForPlaybackReply:(long)fp8;
- (int)instantiateFMPRef:(struct opaqueFigMoviePlaybackRef **)fp8 forItem:(id)fp12;
- (void)maybeDumpPerformanceDictionary:(struct opaqueFigMoviePlaybackRef *)fp8;
- (void)removeFMPRefListeners:(struct opaqueFigMoviePlaybackRef *)fp8;
- (void)shutdownFMPRef:(struct opaqueFigMoviePlaybackRef *)fp8;
- (void)updateTimeMarkerObservations;
- (void)scheduleUpdateTimeMarkerObservations;
- (void)registerTimeMarkerObserver:(id)fp8 forItem:(id)fp12 times:(id)fp16 context:(id)fp20;
- (void)removeObserver:(id)fp8 fromTMOArray:(id)fp12;
- (void)unregisterTimeMarkerObserver:(id)fp8 forItem:(id)fp12;

@end

//AVExternalAudio.h
/*
* Generated by class-dump 3.1.1.
*
* class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2006 by Steve Nygard.
*/

#import <Foundation/Foundation.h>
#import "CDStructures.h"

@interface AVExternalAudio : NSObject
{
struct AVExternalAudioPrivate *_priv;
}

+ (id)avExternalAudio:(id)fp8;
- (id)initWithDelegate:(id)fp8;
- (void)dealloc;
- (id)attributeForKey:(id)fp8;
- (BOOL)setAttribute:(id)fp8 forKey:(id)fp12 error:(id *)fp16;
- (void)makeError:(id *)fp8 withDescription:(id)fp12 code:(long)fp16;
- (void)postServerConnectionDiedNotification:(id)fp8;
- (void)fmpServerConnectionDied;
- (BOOL)okToNotifyFromThisThread;
- (BOOL)activate:(id *)fp8;
- (float)volume;
- (BOOL)isActive;
- (void)postUserVolumeChangedNotification:(id)fp8;
- (void)fmpUserVolumeDidChange;
- (void)fmpChangeConnectionActive:(BOOL)fp8;

@end

//AVItem.h
/*
* Generated by class-dump 3.1.1.
*
* class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2006 by Steve Nygard.
*/

#import <Foundation/Foundation.h>
#import "CDStructures.h"

@interface AVItem : NSObject
{
struct AVItemPrivate *_priv;
}

+ (id)avItem;
+ (id)avItemWithPath:(id)fp8 error:(id *)fp12;
- (id)initWithError:(id *)fp8;
- (id)init;
- (id)initWithPath:(id)fp8 error:(id *)fp12;
- (void)dealloc;
- (BOOL)setPath:(id)fp8 error:(id *)fp12;
- (int)_instantiateItem;
- (id)path;
- (double)duration;
- (struct CGSize)naturalSize;
- (float)volume;
- (void)setVolume:(float)fp8;
- (void)setEQPreset:(int)fp8;
- (int)eqPreset;
- (id)attributeForKey:(id)fp8;
- (BOOL)setAttribute:(id)fp8 forKey:(id)fp12 error:(id *)fp16;

@end

//AVQueue.h
/*
* Generated by class-dump 3.1.1.
*
* class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2006 by Steve Nygard.
*/

#import <Foundation/Foundation.h>

@class NSMutableArray, NSRecursiveLock;

@interface AVQueue : NSObject
{
NSRecursiveLock *_mutex;
NSMutableArray *_items;
void *_reserved;
unsigned int _flags;
}

+ (id)avQueue;
+ (id)queueWithArray:(id)fp8 error:(id *)fp12;
- (id)initWithError:(id *)fp8;
- (id)init;
- (id)initWithArray:(id)fp8 error:(id *)fp12;
- (void)dealloc;
- (int)_instantiateItem;
- (unsigned int)itemCount;
- (id)itemAtIndex:(unsigned int)fp8;
- (unsigned int)indexOfItem:(id)fp8;
- (id)itemAfterItem:(id)fp8;
- (BOOL)appendItemsFromArray:(id)fp8 error:(id *)fp12;
- (BOOL)appendItem:(id)fp8 error:(id *)fp12;
- (void)itemWasAdded:(id)fp8;
- (BOOL)insertItem:(id)fp8 atIndex:(unsigned int)fp12 error:(id *)fp16;
- (BOOL)insertItem:(id)fp8 afterItem:(id)fp12 error:(id *)fp16;
- (void)itemWillBeRemoved:(id)fp8;
- (BOOL)removeItem:(id)fp8;
- (BOOL)removeItemAtIndex:(unsigned int)fp8;
- (void)removeItemsInRange:(struct _NSRange)fp8;
- (void)removeAllItems;

@end

July 01, 2007

My other iphone is a rental.

This post may be short, given that I'm learning to type with this thing. I do have to make a little confession. This is my backup iPhone. That's right. Thinking a client (who, I've learned, got his before me) would want one, and realizing I could return a sealed product, I grabbed two when I went to the Apple store. With the number transfer and activation problems, I made an impulsive decision to pay the restocking fee on this one and unboxed iPhone number 2. Now I think ATT got suspicious of me trying to activate two phones on two plans... Well, let's just say that the prepaid plans are no great deal. (yes, they accepted my credit the first time around). Oh well. My other iPhone is a rental.

March 07, 2007

Webkit and cocoa bindings.

During the C4 conference, I remember Brent Simmons (NewsGator, NetNewswire) and others talking about various interesting possible ways of using a web view in cocoa. I remember thinking about how cool it might be to build some custom UI in a web browser window using html, css, and javascript instead of using drawInRect and friends in my custom views. Wouldn't it be terribly cool if cocoa bindings would be easy to connect our cocoa data model to various bits of (say) an html form. This would allow us to sucker our web designer friends into making us a pretty skin for our app. Slick. (Not that this idea is so new... reference the iTunes store).

Jim Speth, the guy who inadvertently taught me PyObjc with his PyInjector project, has been working on a bit of code on top of WebKit that accomplishes the above by adding cocoa bindings to the DOM of an embedded web view. Originally written in PyObjC (woot!) it's a framework containing a subclass of NSTreeController and a custom WebView to allow us direct, Cocoa bindings aware, hierarchical access to the DOM. Cool stuff. Recently, he's been invited to add this functionality to WebKit itself. Good on ya, Jim! This functionality is now available in the current nightly builds of WebKit. Jim has also been kind enough to have recently posted an example app that shows us how to access and modify the width and height of images in a webview through cocoa bindings and some javascript. It requires that you download and mount the nightly dmg of WebKit to run. It's very pretty. Download it! You get to stretch Jim's face.

In my own app I would like to present the user with a fairly simple form-based web interface for entering data that I (or perhaps the power user) can then skin with custom css to allow easy export to a regular web site or blog. All of the data will live in a coredata store and I would really like the web view to seamlessly integrate with the rest of cocoa. I really don't want the app to look like a glorified web browser. With this in mind, I twisted Jim's example around to connect a slider and an NSTextField to a text input field in a WebView. It's available in my subversion repository at http://jonathansaggau.com/svn/JavaScriptBindings and is mostly Jim Speth's code.
Jim's code includes an Objective C class that fires KVC/KVO notifications in Cocoa for a corresponding javascript function. It allows us to call JSKVC.willChange(document, 'boxInfo'); from our javascript to fire the Cocoa KVC methods, which in turn allow us to use Cocoa bindings to connect everything together.

@implementation JSKVC
+ (BOOL)isSelectorExcludedFromWebScript:(SEL)aSelector
{
    if (aSelector == @selector(object:willChangeValueForKey:))
        return NO;
    if (aSelector == @selector(object:didChangeValueForKey:))
        return NO;
    return YES;
}
+ (NSString *)webScriptNameForSelector:(SEL)aSelector
{
    if (aSelector == @selector(object:willChangeValueForKey:))
        return @"willChange";
    if (aSelector == @selector(object:didChangeValueForKey:))
        return @"didChange";
    return nil;
}
- (void)object:(id)object willChangeValueForKey:(NSString *)key
{
    [object willChangeValueForKey:key];
}
- (void)object:(id)object didChangeValueForKey:(NSString *)key
{
    [object didChangeValueForKey:key];
}

The HTML is just a common text input box that we give a unique ID so that javascript can identify it.
<html>
<head>
<title>bindings example</title>
<script type="text/javascript" language="javascript" src="boxinfo.js"></script>
</head>
<body>
    <p><input type="text" id="box" value="0" maxlength="4" name="contentBox" /></p>
</body>
</html>
</pre>

We need a little javascript magic now to model our text box and connect things up to the KVC notifier class. First we build a simple prototype class to define accessors for the text box value.

// bindings.js BoxInfo.prototype = { get content() { if (!this.box) return null; return this.box.value; }, set content(w) { if (!this.box) return; this.box.value = w; }, }
Then we'll need a method that will call our cocoa KVO willChange and didChange methods. JSKVC will get injected into the javascript runtime from cocoa below.
function BoxInfo() {
    this.box = null;
    this.setBox = function(newBox) {
        // store the new box value, with KVO notifcations
        JSKVC.willChange(document, 'boxInfo');
        this.box = newBox;
        JSKVC.didChange(document, 'boxInfo');
    }
}
To finish things off in javascript, we need to tell it which box to deal with. Jim's code has a nice bit of functionality that allows the user to choose what he/she is modifying by clicking on an image. Mine is simply getting the box we're interested in through its unique ID.
function installBoxInfo() {
    // create an BoxInfo object for the (DOM) document, using KVO notifications
    JSKVC.willChange(document, 'boxInfo');
    document.boxInfo = new BoxInfo();
    JSKVC.didChange(document, 'boxInfo');
    
    var boxElement = document.getElementById( 'box' );
    document.boxInfo.setBox(boxElement);
    //boxElement.setAttribute('style', 'outline: -3px solid white;');
    boxElement.onblur = function() { 
        JSKVC.willChange(document, 'boxInfo');
        JSKVC.didChange(document, 'boxInfo');
    };
}
The boxElement.onblur event (for those of us, like me who aren't familiar with javascript) is the event that gets triggered when the user leaves the text box. Since the user is likely to have changed the contents of the text box, we call willChange and didChange to let cocoa know it needs to update the other widgets and doodads. More on this in a bit.

Now we need to get all of this into the cocoa runtime. In the WebFrameLoadDelegate of the WebView, we need to inject some javascript code. This is where things get really interesting. We're evaluating our boxinfo.js file and injecting the JSKVC class into javascript so that we can fire KVC notifications in Cocoa.
- (void)webView:(WebView *)sender windowScriptObjectAvailable:(WebScriptObject *)windowScriptObject
{
    // as soon as the window object is available, inject the code for the boxInfo object
    NSString *stringForBoxInfo = [self stringForResource:@"boxinfo" ofType:@"js"];
    [windowScriptObject evaluateWebScript:stringForBoxInfo];
    // add the key-value notification object to the script environment
    [windowScriptObject setValue:[[JSKVC alloc] init] forKey:@"JSKVC"];
}
Then, once the web frame is loaded, we need to setup our BoxInfo object by calling the installBoxInfo(); function
- (void)webView:(WebView *)sender didFinishLoadForFrame:(WebFrame *)frame
{
    // modify the DOM document each time a new page loads
    [[webView windowScriptObject] evaluateWebScript:@"installBoxInfo();"];
}
And, should we ever want to (say) call a method in javascript from cocoa, we set up a WebScriptObject by evaluating a small bit of javascript code corresponding to the object we want to message (document.boxInfo below) and then call- (id)callWebScriptMethod:(NSString *)name withArguments:(NSArray *)args;
WebScriptObject *boxInfo = [[webView windowScriptObject] evaluateWebScript:@"document.boxInfo"];
[boxInfo callWebScriptMethod:@"setBox" withArguments:[NSArray arrayWithObject:obj]];
That's about all of the code. Now let's take a look at the NIB file.

The interesting bits in the window below will be the webview, the NSTextField, and the slider. The other views are bound to various parts of the DOM.

webWindow.jpg

There is a DomTreeController from Jim and the WebKit folks and a simple NSObjectController to hold the boxInfo class (from javascript)

WebJavaNibClasses.jpg

The DomTreeController is bound to a new attribute of the WebView called mainFrameDocument. This gives us access to the whole of the web page DOM through cocoa bindings.

bindingJavsc1.jpg

The NSObjectController is bound to the mainFrameDocument.boxInfo object that we defined in javascript. (Which would be document.boxInfo in JS)

bindingJavsc2.jpg

All the is left is to bind both of the cocoa controls (NSTextView and the NSSlider) to the boxInfo NSObjectController (boxInfo.selection.content) and, well, Bob's your uncle (hi Jo).

bindingJavsc3.jpg

Yo run the code we need to have the nightly build of WebKit mounted on our machine. We can set the following environment variables on our executable in xcode and run from there or we can just dump them in a script and allow that script to run the project. We're telling dyld to look for frameworks in the WebKit.app (more on this in a later post) and to unset the system WebKit framework path (causing dlyd to ignore the older version that's installed on our machine).
#!/bin/tcsh
set PATH=`dirname $0`
setenv DYLD_FRAMEWORK_PATH /Volumes/WebKit/WebKit.app/Contents/Resources/
setenv WebKit_UNSET_DYLD_FRAMEWORK_PATH YES
$PATH/build/debug/JavaScriptBindings.app/Contents/MacOS/JavaScriptBindings

And now for my favorite screenshot.

JavaScriptBoundAndGagged.jpg

Thanks to Jim's magic WebKit additions and cocoa bindings. The value in the NSTextField, the NSSlider and in the text input field of the WebView are all synchronized. We can even use the web page to input data.

In a couple of days, I'll revisit this topic for a short post on how to build WebKit as an embeddable framework so you can use this now in a shipping application (at 24MB, it's pretty big) and how the nightly builds of WebKit work. Stay tuned.

February 27, 2007

Core Data Double Linked List How To

I have been working on a core data app lately and stumbled upon some data that should maintain relative order. Since core data is a collection of objects stored in a database of sorts, this is not supported out of the box. There are a couple of ways to accomplish this.

Most of us who find ourselves in this situation will keep an incremental index as an instance variable in each member of our collection and sort based on that index number. Under certain circumstances, this is the best way to do it. However, A kind of nasty problem arises when we want to reorder the collection of objects. Either we're forced to auto-increment the indices by something other than 1 to allow us a little wiggle room for local insertion or reordering of objects or we're going to have to re-index most (or all) of the objects in our collection any time the order changes or an object is inserted. For instance, if I were to insert an object at the front of an indexed list, I would probably have to iterate through the entire collection to increment the index number. No fun.

While reading the long back and forth on the cocoadev list from June of 2005, I stumbled upon a post by Bill Bumgarner that I found quite interesting.

Actually, depending on context of implementation, using a linked list 
is both quite easy and very efficient.  Obviously, inserting/deleting 
objects into a linked list is trivial and fast if you have both a 
next and a previous pointer.  Given that Core Data will maintain the 
inverse relationship automatically, it will also maintain the linked 
list pointers automatically.


Given that many UI type usage contexts will require all of the objects to be faulted into memory anyway, walking the "next" relations and filling an NSMutableArray for display purposes is about three lines of code.

... In this particular example, the amount of overhead in the form of additional lines of code incurred by the developer is pretty trivial. ... b.bum
He's writing about a common data structure called a double (or doubly) linked list wherein each object in the list holds a pointer to the next and previous objects that can, in certain situations, be more useful for ordering data than an indexed set. We can enumerate in either direction very easily, asking each object in the chain for next or previous until we get nil and we can insert, remove and reorder objects with little hassle. All we have to do is make sure that next and previous values of adjacent objects are set appropriately (more on this in a second). Unfortunately, We can't (efficiently) get an object at an index like we can with an indexed list because we're purposely not indexing the collection. Also, remember that much of cocoa will want a data structure that responds to objectAtIndex which, as Bill mentiones above, will require us to iterate the chain and put it into an array. We also can't place an object in the chain at two different positions (but two objects with identical attributes can coexist in the chain).

To use core data for ordered collections we have a decision to make. Either we can make insertion and reordering fast or we can make getting an object at an index fast. If you like the indexed set idea you can go here:

Jesse Grosjean's implementation and Uli Kusterer's as well

If you like the double linked list idea, stick around. You can download the whole package from my svn repository by getting subversion and typing the following into the terminal:
svn co https://jonathansaggau.com/svn/MOLinkedList
Data Model

Model.jpg There are four entities here:
JSMOLLContainer (abstract entity):
  Contains most of the logic of inserting, deleting, reordering, etc.
  Relationship:
     Nodes: To-Many relationship
            Delete Rule: cascade (removing the container removes the nodes)
ThingContainer (subentity of JSMOLLContainer):
  You can replace this with your subentity, including any attributes you might want to add.
JSMONode(abstract entity)::
  Contains only getters and setters for next and previous.
  Relationships:
     Container (JSMOLLContainer):
            Delete Rule: Nullify
     Next (JSMONode):
            Delete Rule: Nullify
     Prev (JSMONode):
            Delete Rule: Nullify
Thing (subentity of JSMONode):
  You can replace this with your subentity, including any attributes you might want to add.  I've chosen to add a "name" attribute (a string) as a demonstration.
Implementation

I chose to use Jonathan "Wolf" Rentzsch's mogenerator to create default implementation files. It's awesome. Default accessors are built for you and placed in a machine-generated superclass for each of your objects. Mogenerator's code keeps the managed object model consistent for us and allows us to implement specific functionality in our class files. This is magic.

First tries are always throw away code, right? When I tried to implement a double linked list through messaging the node objects (say calling setNext directly on a node object to insert a new node), things became difficult in a hurry. Basic functionality was simple; the problems started creeping in when I realized that I had to guard against a lot of strange edge cases when inserting and removing objects. Coredata objects like to stick around for undo / redo purposes, so I was constantly making sure I didn't call any objects that responded to isDeleted with YES. Needless to say, I couldn't get the unit tests to work reliably talking just to the nodes.

If one tries to implement a double linked list by forcing the user to message a managing container (something we're used to doing with arrays anyway), life gets much simpler. Here we quickly rub up against a very nice feature of coredata (and also the most frustrating thing with regard to debugging). It will happily fire inverse relationships for us to keep the object graph consistent. When we call setNext on our JSMONode subclass, coredata calls setPrevious on the inverse entity. If we forget that core data is doing that for us, we get into infinite loops.

Here's the interface file for the custom parts of the Container

 
#import "_JSMOLLContainer.h"
//_JSMOLLContainer is the machine generated code from mogenerator.  It has only
//the accessors and the KVO notifications within.

@interface JSMOLLContainer : _JSMOLLContainer 
{
    //we cache first and last values because the fetch takes a while
    @private
    JSMONode *_cachedFirst;
    JSMONode *_cachedLast;
}

- (void)addNodesObjectAtFront:(JSMONode*)value_;
- (void)addNodesObjectAtEnd:(JSMONode*)value_;
- (void) moveNode: (JSMONode *)moveMe
           before: (JSMONode *)beforeMe;
- (void) moveNode: (JSMONode *)moveMe
            after: (JSMONode *)afterMe;
- (JSMONode *)first;
- (JSMONode *)last;
- (NSArray *)nodesArray;
- (NSEnumerator *)nodesEnumerator;
- (NSEnumerator *)reverseNodesEnumerator;
@end
Adding a node


To add a node object we first add it to our collection of nodes by calling the mogenerater generated superclass method. This fires all of the appropriate KVO/KVC notifications. You'll no doubt notice that there are ivars (_cachedFirst and _cachedLast) for caching the first and last nodes in the chain. It turns out that we need to know first and last pretty often. Fetching this from core data can be painfully slow, so we cache this information when we can.
- (void)addNodesObject: (JSMONode*)value_
{
    //add node object at the END of the chain.
    [super addNodesObject: value_];
    JSMONode *last = [self last];
    JSMONode *newNext = [last next];  // should be null
    [value_ setPrev: last];
    [value_ setNext: newNext];
    [self set_cachedLast: value_];
    if (IsEmpty(last))
        [self set_cachedFirst:value_];
}
There is a similar method for putting a node object at the head of the chain.

Removing a node


To remove a node object we must first remove it from the superclass (this removes it from our collection) and set the bordering objects' next and previous values appropriately to keep the chain consistent. The appropriate inverse relationship methods are fired for us. (Remember those infinite loops I was talking about?).
- (void)removeNodesObject: (JSMONode*)value_ 
{
    [super removeNodesObject: value_];
    //set next and prev as needed on bordering objs
    if (!IsEmpty([value_ prev]))
        [[value_ prev] setNext: [value_ next]];
    else
        [[value_ next] setPrev: [value_ prev]];
}
Reordering
The easiest way to reorder a node is first to remove it (thus keeping the chain consistent with regard to adjacent objects), then tell the new previous node to set its next object to the node being moved and tell the node being moved to set its next object accordingly. (At 3:00 AM, this gets hard on the mind).
- (void) moveNode: (JSMONode *)moveMe
           before: (JSMONode *)beforeMe
{
    [self removeNodesObject: moveMe];
    
    JSMONode *myNewPrev = [beforeMe prev];
    if(!IsEmpty(myNewPrev))
    {
        //if we are not moving moveMe to the front of the chain, this works
        //otherwise there IS NO myNewPrev and we have to use the method below.
        [myNewPrev setNext: moveMe];
        [moveMe setNext: beforeMe];
    }
    else
    {
        [moveMe setPrev: myNewPrev];
        [beforeMe setPrev: moveMe];
    }
}
The rest of the implementation deals with building up simple enumerators and also with fetching the first and last nodes (where prev is Null or next is Null respectively) from the Managed Object Context when it turns out we haven't cached it appropriately. Eventually, the fetch requests will go away. Right now, they'll only likely be needed when we've done some reordering as that code doesn't update the cached first and last values as it should. To make sure that we're really returning the first node, we simply check that the _cachedFirst node's prev value is Null (actually [NSNull null]); that's what IsEmpty does here.
- (NSArray *)_nodesFilteredUsingPredicate: (NSPredicate *)predicate
{
    if(0 == [[self nodesSet] count]) return [NSArray array];
    NSEntityDescription * entity = [NSEntityDescription entityForName: @"JSMONode" 
                                               inManagedObjectContext: [self managedObjectContext]];
    NSFetchRequest * fetch = [[NSFetchRequest alloc] init]; 
    [fetch setEntity: entity]; 
    [fetch setPredicate: predicate]; 
    
    NSManagedObjectContext *context = [self managedObjectContext];
    NSArray * results = [context executeFetchRequest: fetch error: nil];
    [fetch release];
    return results;
}

- (JSMONode *)first
{
    if(0 == [[self nodesSet] count]) return nil;
    //If we haven't yet cachedFirst we need to find it.
    //IF cachedFirst previous value is not null, cachedFirst is no longer actually first.
    if (!IsEmpty(_cachedFirst) && IsEmpty([_cachedFirst prev]))
        return _cachedFirst;
    
    JSMONode *first;
    NSPredicate *predicate = [NSPredicate predicateWithFormat: @"container == %@ AND prev == %@", self, [NSNull null]];
    NSArray *results = [self _nodesFilteredUsingPredicate: predicate];
    
    if (0 == [results count])
    {
        [self set_cachedFirst: nil];
        return nil;
    }
    
    NSAssert(([results count] == 1), @"There should be one or fewer firstThing(s)");
    first = [results objectAtIndex: 0];
    [self set_cachedFirst: first];
    return first;
}

- (JSMONode *)last
{
    if(0 == [[self nodesSet] count]) return nil;
    if (!IsEmpty(_cachedLast) && IsEmpty([_cachedLast next]))
        return _cachedLast;
    
    JSMONode *last;
    NSPredicate *predicate = [NSPredicate predicateWithFormat: @"container == %@ AND next == %@", self, [NSNull null]];
    NSArray *results = [self _nodesFilteredUsingPredicate: predicate];    
    if (0 == [results count])
    {
        [self set_cachedLast: nil];
        return nil;
    }
    
    NSAssert(([results count] == 1), @"There should be one or fewer lastThing(s)");
    last = [results objectAtIndex: 0];
    [self set_cachedLast: last];
    return last;
}
Meanwhile, back at the Hall of Justice: usage So how do we use it? We simply insert a JSMOLLContainer subclass (in the case of the diagram above, it's a ThingContainer) into the managed object context like so:

container = [NSEntityDescription insertNewObjectForEntityForName:@"ThingContainer" 
                                                               inManagedObjectContext:moc];


Then we start putting node objects into it like so:

thingZero = [[NSEntityDescription insertNewObjectForEntityForName:@"Thing" 
                                                inManagedObjectContext:moc]

     //perhaps we want to set an attribute
     [thingZero setName:@"Thing Zero Here"];
     [container addNodesObject:thingZero];

     thingOne = [[NSEntityDescription insertNewObjectForEntityForName:@"Thing" 
                                                inManagedObjectContext:moc]

     //perhaps we want to set an attribute
     [thingOne setName:@"Thing One Here"];
     [container addNodesObject: thingOne];
Now we've a container with two things in this order: thingZero -> thingOne.

If we wanted to iterate through a list, we have a couple of options: 1. We can ask the container for its object enumerator and do it "cocoa style"
    JSMONode *currentThing;
    NSEnumerator *nodesEnumerator = [container nodesEnumerator];
    while (currentThing = [nodesEnumerator nextObject])
    {
        //do something
    }

2. We can ask the container for the first object and keep asking for next
    JSMONode *currentThing = [container first];
    
    while (!IsEmpty(currentThing))
    {
        //do something
        currentThing = [currentThing next];
    }

At this point, I should probably show you the IsEmpty function. This is mostly stolen from Wil Shipley's blog . I just added the test for [NSNull null], which is the "empty" object for insertion into cocoa collections (like a core data graph, for instance).
//Thanks Wil
static inline BOOL IsEmpty(id thing) {
    return thing == nil
    || ([thing isEqual:[NSNull null]]) //JS addition for coredata
    || ([thing respondsToSelector:@selector(length)]
        && [(NSData *)thing length] == 0)
    || ([thing respondsToSelector:@selector(count)]
        && [(NSArray *)thing count] == 0);
}


Inserting an object before or after another object currently in the chain is (for now) a two step process. Let's say you have a node called "node3" in a linked list and you want to add a node called "node7" to the list after node 3. Let's pretend that we have nodes zero through six already in the list. (node0 -> node1 -> node2 -> ... node6) node7 (outside)

1. Put node7 in the container by simply adding it to the end
[container addNodesObject: node7]
2. Then place it after node3
[container moveNode: node7
                   after: node3;]


That's it. It's somewhat suboptimal in that the container will insert at end, then remove, then reinsert the node behind the scenes, but I haven't had to optimize this code yet in practice and it keeps the API simple.

So there you have it. Make a subentity (or several) of the JSMOLLContainer and the of JSMONode objects and enjoy. Take a look at the subversion repository. It includes test code that will give you a good idea of how everything works in practice. Oh. Do what you want with it. It's offered AS IS. No Warranty.

November 11, 2006

Graphviz rocks

I needed to come up with an innovative (pun intended, for those who get the joke) way of tracking time by task for one of my clients recently. I considered dusting off DIA or perhaps omnigraffle, but I realized that the tedium involved in laying out the hierarchical structure I wanted to see would probably kill me. So, I thought I would give Graphviz a shot.

The nice thing here is that it will lay out a hierarchy for you and flow your chart in an efficient manner. I would have gone slowly mad doing this by hand.

Click on the image below to see a larger version.


GraphvizLG.jpg

You can see the markup I gave graphviz in the extended portion of the post.

Continue reading "Graphviz rocks" »