Added some new utilities, taken from Murky.
authorJens Alfke <jens@mooseyard.com>
Sat Mar 28 09:36:46 2009 -0700 (2009-03-28)
changeset 205a71993a1a70
parent 18 d6ab9f52b4d7
child 21 88d7e2455a7f
Added some new utilities, taken from Murky.
ImageAndTextCell.h
ImageAndTextCell.m
MYDirectoryWatcher.h
MYDirectoryWatcher.m
MYTask.h
MYTask.m
MYURLFormatter.h
MYURLFormatter.m
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/ImageAndTextCell.h	Sat Mar 28 09:36:46 2009 -0700
     1.3 @@ -0,0 +1,17 @@
     1.4 +#import <Cocoa/Cocoa.h>
     1.5 +
     1.6 +/** Subclass of NSTextFieldCell which can display text and an image simultaneously.
     1.7 +    Taken directly from Apple sample code. */
     1.8 +@interface ImageAndTextCell : NSTextFieldCell
     1.9 +{
    1.10 +    @private
    1.11 +    NSImage *image;
    1.12 +}
    1.13 +
    1.14 +- (void)setImage:(NSImage *)anImage;
    1.15 +- (NSImage *)image;
    1.16 +
    1.17 +- (void)drawWithFrame:(NSRect)cellFrame inView:(NSView *)controlView;
    1.18 +- (NSSize)cellSize;
    1.19 +
    1.20 +@end
     2.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     2.2 +++ b/ImageAndTextCell.m	Sat Mar 28 09:36:46 2009 -0700
     2.3 @@ -0,0 +1,175 @@
     2.4 +/*
     2.5 +    ImageAndTextCell.m
     2.6 +    Copyright (c) 2001-2006, Apple Computer, Inc., all rights reserved.
     2.7 +    Author: Chuck Pisula
     2.8 +
     2.9 +    Milestones:
    2.10 +    * 03-01-2001: Initial creation by Chuck Pisula
    2.11 +    * 11-04-2005: Added hitTestForEvent:inRect:ofView: for better NSOutlineView support by Corbin Dunn
    2.12 +
    2.13 +    Subclass of NSTextFieldCell which can display text and an image simultaneously.
    2.14 +*/
    2.15 +
    2.16 +/*
    2.17 + IMPORTANT:  This Apple software is supplied to you by Apple Computer, Inc. ("Apple") in
    2.18 + consideration of your agreement to the following terms, and your use, installation, 
    2.19 + modification or redistribution of this Apple software constitutes acceptance of these 
    2.20 + terms.  If you do not agree with these terms, please do not use, install, modify or 
    2.21 + redistribute this Apple software.
    2.22 + 
    2.23 + In consideration of your agreement to abide by the following terms, and subject to these 
    2.24 + terms, Apple grants you a personal, non-exclusive license, under AppleÕs copyrights in 
    2.25 + this original Apple software (the "Apple Software"), to use, reproduce, modify and 
    2.26 + redistribute the Apple Software, with or without modifications, in source and/or binary 
    2.27 + forms; provided that if you redistribute the Apple Software in its entirety and without 
    2.28 + modifications, you must retain this notice and the following text and disclaimers in all 
    2.29 + such redistributions of the Apple Software.  Neither the name, trademarks, service marks 
    2.30 + or logos of Apple Computer, Inc. may be used to endorse or promote products derived from 
    2.31 + the Apple Software without specific prior written permission from Apple. Except as expressly
    2.32 + stated in this notice, no other rights or licenses, express or implied, are granted by Apple
    2.33 + herein, including but not limited to any patent rights that may be infringed by your 
    2.34 + derivative works or by other works in which the Apple Software may be incorporated.
    2.35 + 
    2.36 + The Apple Software is provided by Apple on an "AS IS" basis.  APPLE MAKES NO WARRANTIES, 
    2.37 + EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, 
    2.38 + MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS 
    2.39 + USE AND OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS.
    2.40 + 
    2.41 + IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL OR CONSEQUENTIAL 
    2.42 + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS 
    2.43 + OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, 
    2.44 + REPRODUCTION, MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED AND 
    2.45 + WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), STRICT LIABILITY OR 
    2.46 + OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
    2.47 +*/
    2.48 +
    2.49 +#import "ImageAndTextCell.h"
    2.50 +#import <AppKit/NSCell.h>
    2.51 +
    2.52 +@implementation ImageAndTextCell
    2.53 +
    2.54 +- (id)init {
    2.55 +    self = [super init];
    2.56 +    if( self ) {
    2.57 +        [self setLineBreakMode:NSLineBreakByTruncatingTail];
    2.58 +        [self setSelectable:YES];
    2.59 +    }
    2.60 +    return self;
    2.61 +}
    2.62 +
    2.63 +- (void)dealloc {
    2.64 +    [image release];
    2.65 +    [super dealloc];
    2.66 +}
    2.67 +
    2.68 +- (id)copyWithZone:(NSZone *)zone {
    2.69 +    ImageAndTextCell *cell = (ImageAndTextCell *)[super copyWithZone:zone];
    2.70 +    // The image ivar will be directly copied; we need to retain or copy it.
    2.71 +    cell->image = [image retain];
    2.72 +    return cell;
    2.73 +}
    2.74 +
    2.75 +- (void)setImage:(NSImage *)anImage {
    2.76 +    if (anImage != image) {
    2.77 +        [image release];
    2.78 +        image = [anImage retain];
    2.79 +    }
    2.80 +}
    2.81 +
    2.82 +- (NSImage *)image {
    2.83 +    return image;
    2.84 +}
    2.85 +
    2.86 +- (NSRect)imageRectForBounds:(NSRect)cellFrame {
    2.87 +    NSRect result;
    2.88 +    if (image != nil) {
    2.89 +        result.size = [image size];
    2.90 +        result.origin = cellFrame.origin;
    2.91 +        result.origin.x += 3;
    2.92 +        result.origin.y += ceil((cellFrame.size.height - result.size.height) / 2);
    2.93 +    } else {
    2.94 +        result = NSZeroRect;
    2.95 +    }
    2.96 +    return result;
    2.97 +}
    2.98 +
    2.99 +// We could manually implement expansionFrameWithFrame:inView: and drawWithExpansionFrame:inView: or just properly implement titleRectForBounds to get expansion tooltips to automatically work for us
   2.100 +- (NSRect)titleRectForBounds:(NSRect)cellFrame {
   2.101 +    NSRect result;
   2.102 +    if (image != nil) {
   2.103 +        CGFloat imageWidth = [image size].width;
   2.104 +        result = cellFrame;
   2.105 +        result.origin.x += (3 + imageWidth);
   2.106 +        result.size.width -= (3 + imageWidth);
   2.107 +    } else {
   2.108 +        result = NSZeroRect;
   2.109 +    }
   2.110 +    return result;
   2.111 +}
   2.112 +
   2.113 +
   2.114 +- (void)editWithFrame:(NSRect)aRect inView:(NSView *)controlView editor:(NSText *)textObj delegate:(id)anObject event:(NSEvent *)theEvent {
   2.115 +    NSRect textFrame, imageFrame;
   2.116 +    NSDivideRect (aRect, &imageFrame, &textFrame, 3 + [image size].width, NSMinXEdge);
   2.117 +    [super editWithFrame: textFrame inView: controlView editor:textObj delegate:anObject event: theEvent];
   2.118 +}
   2.119 +
   2.120 +- (void)selectWithFrame:(NSRect)aRect inView:(NSView *)controlView editor:(NSText *)textObj delegate:(id)anObject start:(NSInteger)selStart length:(NSInteger)selLength {
   2.121 +    NSRect textFrame, imageFrame;
   2.122 +    NSDivideRect (aRect, &imageFrame, &textFrame, 3 + [image size].width, NSMinXEdge);
   2.123 +    [super selectWithFrame: textFrame inView: controlView editor:textObj delegate:anObject start:selStart length:selLength];
   2.124 +}
   2.125 +
   2.126 +- (void)drawWithFrame:(NSRect)cellFrame inView:(NSView *)controlView {
   2.127 +    if (image != nil) {
   2.128 +        NSRect	imageFrame;
   2.129 +        NSSize imageSize = [image size];
   2.130 +        NSDivideRect(cellFrame, &imageFrame, &cellFrame, 3 + imageSize.width, NSMinXEdge);
   2.131 +        if ([self drawsBackground]) {
   2.132 +            [[self backgroundColor] set];
   2.133 +            NSRectFill(imageFrame);
   2.134 +        }
   2.135 +        imageFrame.origin.x += 3;
   2.136 +        imageFrame.size = imageSize;
   2.137 +
   2.138 +        if ([controlView isFlipped])
   2.139 +            imageFrame.origin.y += ceil((cellFrame.size.height + imageFrame.size.height) / 2);
   2.140 +        else
   2.141 +            imageFrame.origin.y += ceil((cellFrame.size.height - imageFrame.size.height) / 2);
   2.142 +
   2.143 +        [image compositeToPoint:imageFrame.origin operation:NSCompositeSourceOver];
   2.144 +    }
   2.145 +    [super drawWithFrame:cellFrame inView:controlView];
   2.146 +}
   2.147 +
   2.148 +- (NSSize)cellSize {
   2.149 +    NSSize cellSize = [super cellSize];
   2.150 +    cellSize.width += (image ? [image size].width : 0) + 3;
   2.151 +    return cellSize;
   2.152 +}
   2.153 +
   2.154 +- (NSUInteger)hitTestForEvent:(NSEvent *)event inRect:(NSRect)cellFrame ofView:(NSView *)controlView {
   2.155 +    NSPoint point = [controlView convertPoint:[event locationInWindow] fromView:nil];
   2.156 +    // If we have an image, we need to see if the user clicked on the image portion.
   2.157 +    if (image != nil) {
   2.158 +        // This code closely mimics drawWithFrame:inView:
   2.159 +        NSSize imageSize = [image size];
   2.160 +        NSRect imageFrame;
   2.161 +        NSDivideRect(cellFrame, &imageFrame, &cellFrame, 3 + imageSize.width, NSMinXEdge);
   2.162 +        
   2.163 +        imageFrame.origin.x += 3;
   2.164 +        imageFrame.size = imageSize;
   2.165 +        // If the point is in the image rect, then it is a content hit
   2.166 +        if (NSMouseInRect(point, imageFrame, [controlView isFlipped])) {
   2.167 +            // We consider this just a content area. It is not trackable, nor it it editable text. If it was, we would or in the additional items.
   2.168 +            // By returning the correct parts, we allow NSTableView to correctly begin an edit when the text portion is clicked on.
   2.169 +            return NSCellHitContentArea;
   2.170 +        }        
   2.171 +    }
   2.172 +    // At this point, the cellFrame has been modified to exclude the portion for the image. Let the superclass handle the hit testing at this point.
   2.173 +    return [super hitTestForEvent:event inRect:cellFrame ofView:controlView];    
   2.174 +}
   2.175 +
   2.176 +
   2.177 +@end
   2.178 +
     3.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     3.2 +++ b/MYDirectoryWatcher.h	Sat Mar 28 09:36:46 2009 -0700
     3.3 @@ -0,0 +1,57 @@
     3.4 +//
     3.5 +//  MYDirectoryWatcher.h
     3.6 +//  Murky
     3.7 +//
     3.8 +//  Copyright 2008 Jens Alfke. All rights reserved.
     3.9 +//
    3.10 +
    3.11 +#import <Cocoa/Cocoa.h>
    3.12 +
    3.13 +
    3.14 +/* A wrapper for FSEvents, which notifies its delegate when filesystem changes occur. */
    3.15 +@interface MYDirectoryWatcher : NSObject 
    3.16 +{
    3.17 +    NSString *_path;
    3.18 +    id _target;
    3.19 +    SEL _action;
    3.20 +    UInt64 _lastEventID;
    3.21 +    BOOL _historyDone;
    3.22 +    CFTimeInterval _latency;
    3.23 +    FSEventStreamRef _stream;
    3.24 +}
    3.25 +
    3.26 +- (id) initWithDirectory: (NSString*)path target: (id)target action: (SEL)action;
    3.27 +
    3.28 +@property (readonly,nonatomic) NSString* path;
    3.29 +
    3.30 +@property UInt64 lastEventID;
    3.31 +@property CFTimeInterval latency;
    3.32 +
    3.33 +- (BOOL) start;
    3.34 +- (void) pause;
    3.35 +- (void) stop;
    3.36 +- (void) stopTemporarily;               // stop, but re-start on next runloop cycle
    3.37 +
    3.38 +@end
    3.39 +
    3.40 +
    3.41 +
    3.42 +@interface MYDirectoryEvent : NSObject
    3.43 +{
    3.44 +    MYDirectoryWatcher *watcher;
    3.45 +    NSString *path;
    3.46 +    UInt64 eventID;
    3.47 +    UInt32 flags;
    3.48 +}
    3.49 +
    3.50 +@property (readonly, nonatomic) MYDirectoryWatcher *watcher;
    3.51 +@property (readonly, nonatomic) NSString *path, *relativePath;
    3.52 +@property (readonly, nonatomic) UInt64 eventID;
    3.53 +@property (readonly, nonatomic) UInt32 flags;
    3.54 +
    3.55 +@property (readonly, nonatomic) BOOL mustScanSubdirectories;
    3.56 +@property (readonly, nonatomic) BOOL eventsWereDropped;
    3.57 +@property (readonly, nonatomic) BOOL isHistorical;   
    3.58 +@property (readonly, nonatomic) BOOL rootChanged;
    3.59 +
    3.60 +@end
     4.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     4.2 +++ b/MYDirectoryWatcher.m	Sat Mar 28 09:36:46 2009 -0700
     4.3 @@ -0,0 +1,206 @@
     4.4 +//
     4.5 +//  MYDirectoryWatcher.m
     4.6 +//  Murky
     4.7 +//
     4.8 +//  Copyright 2008 Jens Alfke. All rights reserved.
     4.9 +//
    4.10 +
    4.11 +#import "MYDirectoryWatcher.h"
    4.12 +#import <CoreServices/CoreServices.h>
    4.13 +
    4.14 +
    4.15 +static void directoryWatcherCallback(ConstFSEventStreamRef streamRef,
    4.16 +                                     void *clientCallBackInfo,
    4.17 +                                     size_t numEvents,
    4.18 +                                     void *eventPaths,
    4.19 +                                     const FSEventStreamEventFlags eventFlags[],
    4.20 +                                     const FSEventStreamEventId eventIds[]);
    4.21 +
    4.22 +@interface MYDirectoryEvent ()
    4.23 +- (id) _initWithWatcher: (MYDirectoryWatcher*)itsWatcher
    4.24 +                   path: (NSString*)itsPath 
    4.25 +                  flags: (FSEventStreamEventFlags)itsFlags
    4.26 +                eventID: (FSEventStreamEventId)itsEventID;
    4.27 +@end
    4.28 +
    4.29 +
    4.30 +@implementation MYDirectoryWatcher
    4.31 +
    4.32 +
    4.33 +- (id) initWithDirectory: (NSString*)path target: (id)target action: (SEL)action
    4.34 +{
    4.35 +    NSParameterAssert(path);
    4.36 +    self = [super init];
    4.37 +    if (self != nil) {
    4.38 +        _path = path.copy;
    4.39 +        _target = target;
    4.40 +        _action = action;
    4.41 +        _latency = 5.0;
    4.42 +        _lastEventID = kFSEventStreamEventIdSinceNow;
    4.43 +    }
    4.44 +    return self;
    4.45 +}
    4.46 +
    4.47 +- (void) dealloc
    4.48 +{
    4.49 +    [self stop];
    4.50 +    [_path release];
    4.51 +    [super dealloc];
    4.52 +}
    4.53 +
    4.54 +- (void) finalize
    4.55 +{
    4.56 +    [self stop];
    4.57 +    [super finalize];
    4.58 +}
    4.59 +
    4.60 +
    4.61 +@synthesize path=_path, latency=_latency, lastEventID=_lastEventID;
    4.62 +
    4.63 +
    4.64 +- (BOOL) start
    4.65 +{
    4.66 +    if( ! _stream ) {
    4.67 +        FSEventStreamContext context = {0,self,NULL,NULL,NULL};
    4.68 +        _stream = FSEventStreamCreate(NULL, 
    4.69 +                                      &directoryWatcherCallback, &context,
    4.70 +                                      (CFArrayRef)[NSArray arrayWithObject: _path], 
    4.71 +                                      _lastEventID, 
    4.72 +                                      _latency, 
    4.73 +                                      kFSEventStreamCreateFlagUseCFTypes);
    4.74 +        if( ! _stream )
    4.75 +            return NO;
    4.76 +        FSEventStreamScheduleWithRunLoop(_stream, CFRunLoopGetCurrent(), kCFRunLoopCommonModes);
    4.77 +        if( ! FSEventStreamStart(_stream) ) {
    4.78 +            [self stop];
    4.79 +            return NO;
    4.80 +        }
    4.81 +        _historyDone = (_lastEventID == kFSEventStreamEventIdSinceNow);
    4.82 +        Log(@"MYDirectoryWatcher: Started on %@ (latency=%g, lastEvent=%llu)",_path,_latency,_lastEventID);
    4.83 +    }
    4.84 +    return YES;
    4.85 +}
    4.86 +
    4.87 +- (void) pause
    4.88 +{
    4.89 +    if( _stream ) {
    4.90 +        FSEventStreamStop(_stream);
    4.91 +        FSEventStreamInvalidate(_stream);
    4.92 +        FSEventStreamRelease(_stream);
    4.93 +        _stream = NULL;
    4.94 +        Log(@"MYDirectoryWatcher: Stopped on %@ (lastEvent=%llu)",_path,_lastEventID);
    4.95 +    }
    4.96 +}
    4.97 +
    4.98 +- (void) stop
    4.99 +{
   4.100 +    [self pause];
   4.101 +    _lastEventID = kFSEventStreamEventIdSinceNow;   // so events from now till next start will be dropped
   4.102 +    [NSObject cancelPreviousPerformRequestsWithTarget: self selector: @selector(start) object: nil];
   4.103 +}
   4.104 +
   4.105 +- (void) stopTemporarily
   4.106 +{
   4.107 +    if( _stream ) {
   4.108 +        [self stop];
   4.109 +        [self performSelector: @selector(start) withObject: nil afterDelay: 0.0];
   4.110 +    }
   4.111 +}
   4.112 +
   4.113 +
   4.114 +- (void) _notifyEvents: (size_t)numEvents
   4.115 +                 paths: (NSArray*)paths
   4.116 +                 flags: (const FSEventStreamEventFlags[])eventFlags
   4.117 +              eventIDs: (const FSEventStreamEventId[])eventIDs
   4.118 +{
   4.119 +    for (size_t i=0; i<numEvents; i++) {
   4.120 +        NSString *path = [paths objectAtIndex: i];
   4.121 +        FSEventStreamEventFlags flags = eventFlags[i];
   4.122 +        FSEventStreamEventId eventID = eventIDs[i];
   4.123 +        if( flags & (kFSEventStreamEventFlagMount | kFSEventStreamEventFlagUnmount) ) {
   4.124 +            if( flags & kFSEventStreamEventFlagMount )
   4.125 +                Log(@"MYDirectoryWatcher: Volume mounted: %@",path);
   4.126 +            else
   4.127 +                Log(@"MYDirectoryWatcher: Volume unmounted: %@",path);
   4.128 +        } else if( flags & kFSEventStreamEventFlagHistoryDone ) {
   4.129 +            Log(@"MYDirectoryWatcher: Event #%llu History done",eventID);
   4.130 +            _historyDone = YES;
   4.131 +        } else {
   4.132 +            Log(@"MYDirectoryWatcher: Event #%llu flags=%02x path=%@",eventID,flags,path);
   4.133 +            if( _historyDone )
   4.134 +                flags |= kFSEventStreamEventFlagHistoryDone;
   4.135 +            
   4.136 +            MYDirectoryEvent *event = [[MYDirectoryEvent alloc] _initWithWatcher: self
   4.137 +                                                                        path: path 
   4.138 +                                                                       flags: flags
   4.139 +                                                                     eventID: eventID];
   4.140 +            [_target performSelector: _action withObject: event];
   4.141 +            [event release];
   4.142 +        }
   4.143 +        _lastEventID = eventIDs[i];
   4.144 +    }
   4.145 +}
   4.146 +
   4.147 +
   4.148 +static void directoryWatcherCallback(ConstFSEventStreamRef streamRef,
   4.149 +                                     void *watcher,
   4.150 +                                     size_t numEvents,
   4.151 +                                     void *eventPaths,
   4.152 +                                     const FSEventStreamEventFlags eventFlags[],
   4.153 +                                     const FSEventStreamEventId eventIDs[])
   4.154 +{
   4.155 +    [(MYDirectoryWatcher*)watcher _notifyEvents: numEvents
   4.156 +                                          paths: (NSArray*)eventPaths
   4.157 +                                          flags: eventFlags
   4.158 +                                       eventIDs: eventIDs];
   4.159 +}
   4.160 +
   4.161 +
   4.162 +
   4.163 +@end
   4.164 +
   4.165 +
   4.166 +
   4.167 +
   4.168 +@implementation MYDirectoryEvent
   4.169 +
   4.170 +- (id) _initWithWatcher: (MYDirectoryWatcher*)itsWatcher
   4.171 +                   path: (NSString*)itsPath 
   4.172 +                  flags: (FSEventStreamEventFlags)itsFlags
   4.173 +                eventID: (FSEventStreamEventId)itsEventID
   4.174 +{
   4.175 +    self = [super init];
   4.176 +    if (self != nil) {
   4.177 +        watcher = itsWatcher;
   4.178 +        path = itsPath.copy;
   4.179 +        flags = itsFlags;
   4.180 +        eventID = itsEventID;
   4.181 +    }
   4.182 +    return self;
   4.183 +}
   4.184 +
   4.185 +- (void) dealloc
   4.186 +{
   4.187 +    [path release];
   4.188 +    [super dealloc];
   4.189 +}
   4.190 +
   4.191 +@synthesize watcher,path,flags,eventID;
   4.192 +
   4.193 +- (NSString*) relativePath
   4.194 +{
   4.195 +    NSString *base = watcher.path;
   4.196 +    if( ! [path hasPrefix: base] )
   4.197 +        return nil;
   4.198 +    int length = base.length;
   4.199 +    while( length < path.length && [path characterAtIndex: length]=='/' )
   4.200 +        length++;
   4.201 +    return [path substringFromIndex: length];
   4.202 +}
   4.203 +
   4.204 +- (BOOL) mustScanSubdirectories     {return (flags & kFSEventStreamEventFlagMustScanSubDirs) != 0;}
   4.205 +- (BOOL) eventsWereDropped          {return (flags & (kFSEventStreamEventFlagUserDropped|kFSEventStreamEventFlagKernelDropped)) != 0;}
   4.206 +- (BOOL) isHistorical               {return (flags & kFSEventStreamEventFlagHistoryDone)==0;}
   4.207 +- (BOOL) rootChanged                {return (flags & kFSEventStreamEventFlagRootChanged)!=0;}
   4.208 +
   4.209 +@end
   4.210 \ No newline at end of file
     5.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     5.2 +++ b/MYTask.h	Sat Mar 28 09:36:46 2009 -0700
     5.3 @@ -0,0 +1,78 @@
     5.4 +//
     5.5 +//  MYTask.h
     5.6 +//  Murky
     5.7 +//
     5.8 +//  Copyright 2008 Jens Alfke. All rights reserved.
     5.9 +//
    5.10 +
    5.11 +#import <Cocoa/Cocoa.h>
    5.12 +
    5.13 +
    5.14 +extern NSString* const MYTaskErrorDomain;
    5.15 +extern NSString* const MYTaskExitCodeKey;
    5.16 +extern NSString* const MYTaskObjectKey;
    5.17 +enum {
    5.18 +    kMYTaskError = 2
    5.19 +};
    5.20 +
    5.21 +
    5.22 +
    5.23 +@interface MYTask : NSObject 
    5.24 +{
    5.25 +    @private
    5.26 +    NSString *_command;
    5.27 +    NSMutableArray *_arguments;
    5.28 +    NSString *_currentDirectoryPath;
    5.29 +    NSTask *_task;
    5.30 +    int _resultCode;
    5.31 +    NSError *_error;
    5.32 +    BOOL _ignoreOutput;
    5.33 +    NSFileHandle *_outHandle, *_errHandle;
    5.34 +    NSMutableData *_outputData, *_errorData;
    5.35 +    NSString *_output;
    5.36 +    NSMutableArray *_modes;
    5.37 +    BOOL _isRunning, _taskRunning;
    5.38 +}
    5.39 +
    5.40 +- (id) initWithCommand: (NSString*)subcommand, ... NS_REQUIRES_NIL_TERMINATION;
    5.41 +
    5.42 +/* designated initializer (subclasses can override) */
    5.43 +- (id) initWithCommand: (NSString*)subcommand
    5.44 +             arguments: (NSArray*)arguments;
    5.45 +
    5.46 +- (id) initWithError: (NSError*)error;
    5.47 +
    5.48 +- (void) addArgument: (id)argument;
    5.49 +- (void) addArguments: (id)arg1, ... NS_REQUIRES_NIL_TERMINATION;
    5.50 +- (void) addArgumentsFromArray: (NSArray*)arguments;
    5.51 +- (void) prependArguments: (id)arg1, ... NS_REQUIRES_NIL_TERMINATION;
    5.52 +
    5.53 +- (void) ignoreOutput;
    5.54 +
    5.55 +@property (copy) NSString* currentDirectoryPath;
    5.56 +
    5.57 +- (BOOL) run;
    5.58 +- (BOOL) run: (NSError**)outError;
    5.59 +
    5.60 +- (BOOL) start;
    5.61 +- (void) stop;
    5.62 +- (BOOL) waitTillFinished;
    5.63 +
    5.64 +@property (readonly,nonatomic) BOOL isRunning;
    5.65 +@property (readonly,retain,nonatomic) NSError* error;
    5.66 +@property (readonly,nonatomic) NSString *output, *outputAndError;
    5.67 +@property (readonly,nonatomic) NSData *outputData;
    5.68 +
    5.69 +// protected:
    5.70 +
    5.71 +/** Subclasses can override this to add arguments or customize the task */
    5.72 +- (NSTask*) createTask;
    5.73 +
    5.74 +/** Sets the error based on the message and parameters. Always returns NO. */
    5.75 +- (BOOL) makeError: (NSString*)fmt, ...;
    5.76 +
    5.77 +/** Called when the task finishes, just before the isRunning property changes back to NO.
    5.78 +    You can override this to do your own post-processing. */
    5.79 +- (void) finished;
    5.80 +
    5.81 +@end
     6.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     6.2 +++ b/MYTask.m	Sat Mar 28 09:36:46 2009 -0700
     6.3 @@ -0,0 +1,380 @@
     6.4 +//
     6.5 +//  MYTask.m
     6.6 +//  Murky
     6.7 +//
     6.8 +//  Copyright 2008 Jens Alfke. All rights reserved.
     6.9 +//
    6.10 +
    6.11 +#import "MYTask.h"
    6.12 +
    6.13 +//FIX: NOTICE: This code was written assuming garbage collection. It will currently leak like a sieve without it.
    6.14 +
    6.15 +
    6.16 +NSString* const MYTaskErrorDomain = @"MYTaskError";
    6.17 +NSString* const MYTaskExitCodeKey = @"MYTaskExitCode";
    6.18 +NSString* const MYTaskObjectKey = @"MYTask";
    6.19 +
    6.20 +#define MYTaskSynchronousRunLoopMode @"MYTask"
    6.21 +
    6.22 +
    6.23 +@interface MYTask ()
    6.24 +@property (readwrite,nonatomic) BOOL isRunning;
    6.25 +@property (readwrite,retain,nonatomic) NSError *error;
    6.26 +- (void) _finishUp;
    6.27 +@end
    6.28 +
    6.29 +
    6.30 +@implementation MYTask
    6.31 +
    6.32 +
    6.33 +- (id) initWithCommand: (NSString*)command
    6.34 +             arguments: (NSArray*)arguments
    6.35 +{
    6.36 +    NSParameterAssert(command);
    6.37 +    self = [super init];
    6.38 +    if (self != nil) {
    6.39 +        _command = command;
    6.40 +        _arguments = arguments ?[arguments mutableCopy] :[NSMutableArray array];
    6.41 +        _modes = [NSMutableArray arrayWithObjects: NSDefaultRunLoopMode, NSModalPanelRunLoopMode, nil];
    6.42 +    }
    6.43 +    return self;
    6.44 +}
    6.45 +
    6.46 +
    6.47 +- (id) initWithCommand: (NSString*)command, ...
    6.48 +{
    6.49 +    NSMutableArray *arguments = [NSMutableArray array];
    6.50 +    va_list args;
    6.51 +    va_start(args,command);
    6.52 +    id arg;
    6.53 +    while( nil != (arg=va_arg(args,id)) )
    6.54 +        [arguments addObject: [arg description]];
    6.55 +    va_end(args);
    6.56 +    
    6.57 +    return [self initWithCommand: command arguments: arguments];
    6.58 +}
    6.59 +
    6.60 +
    6.61 +- (id) initWithError: (NSError*)error
    6.62 +{
    6.63 +    self = [super init];
    6.64 +    if( self ) {
    6.65 +        _error = error;
    6.66 +    }
    6.67 +    return self;
    6.68 +}
    6.69 +
    6.70 +
    6.71 +- (NSString*) description
    6.72 +{
    6.73 +    return [NSString stringWithFormat: @"%@ %@", 
    6.74 +            _command, [_arguments componentsJoinedByString: @" "]];
    6.75 +}
    6.76 +
    6.77 +
    6.78 +- (void) addArgument: (id)argument
    6.79 +{
    6.80 +    [_arguments addObject: [argument description]];
    6.81 +}
    6.82 +
    6.83 +- (void) addArgumentsFromArray: (NSArray*)arguments
    6.84 +{
    6.85 +    for( id arg in arguments )
    6.86 +        [_arguments addObject: [arg description]];
    6.87 +}
    6.88 +
    6.89 +- (void) addArguments: (id)arg, ...
    6.90 +{
    6.91 +    va_list args;
    6.92 +    va_start(args,arg);
    6.93 +    while( arg ) {
    6.94 +        [_arguments addObject: [arg description]];
    6.95 +        arg = va_arg(args,id);
    6.96 +    }
    6.97 +    va_end(args);
    6.98 +}
    6.99 +
   6.100 +- (void) prependArguments: (id)arg, ...
   6.101 +{
   6.102 +    va_list args;
   6.103 +    va_start(args,arg);
   6.104 +    int i=0;
   6.105 +    while( arg ) {
   6.106 +        [_arguments insertObject: [arg description] atIndex: i++];
   6.107 +        arg = va_arg(args,id);
   6.108 +    }
   6.109 +    va_end(args);
   6.110 +}
   6.111 +
   6.112 +
   6.113 +- (void) ignoreOutput
   6.114 +{
   6.115 +    _ignoreOutput = YES;
   6.116 +}
   6.117 +
   6.118 +
   6.119 +- (BOOL) makeError: (NSString*)fmt, ...
   6.120 +{
   6.121 +    va_list args;
   6.122 +    va_start(args,fmt);
   6.123 +
   6.124 +    NSString *message = [[NSString alloc] initWithFormat: fmt arguments: args];
   6.125 +    Log(@"MYTask Error: %@",message);
   6.126 +    NSMutableDictionary *info = [NSMutableDictionary dictionaryWithObject: message
   6.127 +                                                                   forKey: NSLocalizedDescriptionKey];
   6.128 +    _error = [NSError errorWithDomain: MYTaskErrorDomain code: kMYTaskError userInfo: info];
   6.129 +
   6.130 +    va_end(args);
   6.131 +    return NO;
   6.132 +}
   6.133 +
   6.134 +
   6.135 +- (NSPipe*) _openPipeAndHandle: (NSFileHandle**)handle notifying: (SEL)selector
   6.136 +{
   6.137 +    NSPipe *pipe = [NSPipe pipe];
   6.138 +    *handle = [pipe fileHandleForReading];
   6.139 +    [[NSNotificationCenter defaultCenter] addObserver: self selector: selector
   6.140 +                                                 name: NSFileHandleReadCompletionNotification
   6.141 +                                               object: *handle];
   6.142 +    [*handle readInBackgroundAndNotifyForModes: _modes];
   6.143 +    return pipe;
   6.144 +}
   6.145 +
   6.146 +
   6.147 +- (void) _close
   6.148 +{
   6.149 +    // No need to call -closeFile on file handles obtained from NSPipe (in fact, it can hang)
   6.150 +    _outHandle = nil;
   6.151 +    _errHandle = nil;
   6.152 +    [[NSNotificationCenter defaultCenter] removeObserver: self 
   6.153 +                                                    name: NSFileHandleReadCompletionNotification
   6.154 +                                                  object: nil];
   6.155 +}
   6.156 +
   6.157 +
   6.158 +/** Subclasses can override this. */
   6.159 +- (NSTask*) createTask
   6.160 +{
   6.161 +    NSAssert(!_task,@"createTask called twice");
   6.162 +    NSTask *task = [[NSTask alloc] init];
   6.163 +    task.launchPath = _command;
   6.164 +    task.arguments = _arguments;
   6.165 +    if( _currentDirectoryPath )
   6.166 +        task.currentDirectoryPath = _currentDirectoryPath;
   6.167 +    return task;
   6.168 +}    
   6.169 +
   6.170 +
   6.171 +- (BOOL) start
   6.172 +{
   6.173 +    NSAssert(!_task, @"Task has already been run");
   6.174 +    if( _error )
   6.175 +        return NO;
   6.176 +    
   6.177 +    _task = [self createTask];
   6.178 +    NSAssert(_task,@"createTask returned nil");
   6.179 +    
   6.180 +    Log(@"Task: %@ %@",_task.launchPath,[_task.arguments componentsJoinedByString: @" "]);
   6.181 +    
   6.182 +    _task.standardOutput = [self _openPipeAndHandle: &_outHandle notifying: @selector(_gotOutput:)];
   6.183 +    _outputData =  [[NSMutableData alloc] init];
   6.184 +    _task.standardError  = [self _openPipeAndHandle: &_errHandle notifying: @selector(_gotStderr:)];
   6.185 +    _errorData =  [[NSMutableData alloc] init];
   6.186 +    
   6.187 +    [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(_exited:)
   6.188 +                                                 name: NSTaskDidTerminateNotification
   6.189 +                                               object: _task];
   6.190 +    
   6.191 +    @try{
   6.192 +        [_task launch];
   6.193 +    }@catch( id x ) {
   6.194 +        Log(@"Task failed to launch: %@",x);
   6.195 +        _resultCode = 666;
   6.196 +        [self _close];
   6.197 +        return [self makeError: @"Exception launching %@: %@",_task.launchPath,x];
   6.198 +    }
   6.199 +    _taskRunning = YES;
   6.200 +    self.isRunning = YES;
   6.201 +    
   6.202 +    return YES;
   6.203 +}
   6.204 +
   6.205 +
   6.206 +- (void) stop
   6.207 +{
   6.208 +    [_task interrupt];
   6.209 +    [self _close];
   6.210 +    _taskRunning = NO;
   6.211 +    self.isRunning = NO;
   6.212 +}
   6.213 +
   6.214 +
   6.215 +- (BOOL) _shouldFinishUp
   6.216 +{
   6.217 +    return !_task.isRunning && (_ignoreOutput || (!_outHandle && !_errHandle));
   6.218 +}
   6.219 +
   6.220 +
   6.221 +- (void) _gotOutput: (NSNotification*)n
   6.222 +{
   6.223 +    NSData *data = [n.userInfo objectForKey: NSFileHandleNotificationDataItem];
   6.224 +    if( n.object == _outHandle ) {
   6.225 +        if( data.length > 0 ) {
   6.226 +            [_outHandle readInBackgroundAndNotifyForModes: _modes];
   6.227 +            LogTo(Task,@"Got %u bytes of output",data.length);
   6.228 +            if( _outputData ) {
   6.229 +                [self willChangeValueForKey: @"output"];
   6.230 +                [self willChangeValueForKey: @"outputData"];
   6.231 +                [_outputData appendData: data];
   6.232 +                _output = nil;
   6.233 +                [self didChangeValueForKey: @"outputData"];
   6.234 +                [self didChangeValueForKey: @"output"];
   6.235 +            }
   6.236 +        } else {
   6.237 +            LogTo(Task,@"Closed output");
   6.238 +            _outHandle = nil;
   6.239 +            if( [self _shouldFinishUp] )
   6.240 +                [self _finishUp];
   6.241 +        }
   6.242 +    }
   6.243 +}
   6.244 +
   6.245 +- (void) _gotStderr: (NSNotification*)n
   6.246 +{
   6.247 +    if( n.object == _errHandle ) {
   6.248 +        NSData *data = [n.userInfo objectForKey: NSFileHandleNotificationDataItem];
   6.249 +        if( data.length > 0 ) {
   6.250 +            [_errHandle readInBackgroundAndNotifyForModes: _modes];
   6.251 +            LogTo(Task,@"Got %u bytes of stderr",data.length);
   6.252 +            [self willChangeValueForKey: @"errorData"];
   6.253 +            [_errorData appendData: data];
   6.254 +            [self didChangeValueForKey: @"errorData"];
   6.255 +        } else {
   6.256 +            LogTo(Task,@"Closed stderr");
   6.257 +            _errHandle = nil;
   6.258 +            if( [self _shouldFinishUp] )
   6.259 +                [self _finishUp];
   6.260 +        }
   6.261 +    }
   6.262 +}
   6.263 +
   6.264 +- (void) _exited: (NSNotification*)n
   6.265 +{
   6.266 +    _resultCode = _task.terminationStatus;
   6.267 +    LogTo(Task,@"Exited with result=%i",_resultCode);
   6.268 +    _taskRunning = NO;
   6.269 +    if( [self _shouldFinishUp] )
   6.270 +        [self _finishUp];
   6.271 +    else
   6.272 +        [self performSelector: @selector(_finishUp) withObject: nil afterDelay: 1.0];
   6.273 +}
   6.274 +
   6.275 +
   6.276 +- (void) _finishUp
   6.277 +{
   6.278 +    [NSObject cancelPreviousPerformRequestsWithTarget: self selector: @selector(_finishUp) object: nil];
   6.279 +    [self _close];
   6.280 +
   6.281 +    LogTo(Task,@"Finished!");
   6.282 +
   6.283 +    if( _resultCode != 0 ) {
   6.284 +        // Handle errors:
   6.285 +        NSString *errStr = nil;
   6.286 +        if( _errorData.length > 0 )
   6.287 +            errStr = [[NSString alloc] initWithData: _errorData encoding: NSUTF8StringEncoding];
   6.288 +        Log(@"    *** task returned %i: %@",_resultCode,errStr);
   6.289 +        if( errStr.length == 0 )
   6.290 +            errStr = [NSString stringWithFormat: @"Command returned status %i",_resultCode];
   6.291 +        NSString *desc = [NSString stringWithFormat: @"%@ command error", _task.launchPath.lastPathComponent];
   6.292 +        // For some reason the body text in the alert shown by -presentError: is taken from the
   6.293 +        // NSLocalizedRecoverySuggestionErrorKey, not the NSLocalizedFailureReasonKey...
   6.294 +        NSMutableDictionary *info = [NSMutableDictionary dictionaryWithObjectsAndKeys:
   6.295 +                                     desc,                                  NSLocalizedDescriptionKey,
   6.296 +                                     errStr,                                NSLocalizedRecoverySuggestionErrorKey,
   6.297 +                                     [NSNumber numberWithInt: _resultCode], MYTaskExitCodeKey,
   6.298 +                                     self,                                  MYTaskObjectKey,
   6.299 +                                     nil];
   6.300 +        self.error = [[NSError alloc] initWithDomain: MYTaskErrorDomain 
   6.301 +                                                code: kMYTaskError
   6.302 +                                            userInfo: info];
   6.303 +    }
   6.304 +
   6.305 +    [self finished];
   6.306 +
   6.307 +    self.isRunning = NO;
   6.308 +}
   6.309 +
   6.310 +- (void) finished
   6.311 +{
   6.312 +    // This is a hook that subclasses can override to do post-processing.
   6.313 +}
   6.314 +
   6.315 +
   6.316 +- (BOOL) _waitTillFinishedInMode: (NSString*)runLoopMode
   6.317 +{
   6.318 +    // wait for task to exit:
   6.319 +    while( _task.isRunning || self.isRunning )
   6.320 +        [[NSRunLoop currentRunLoop] runMode: MYTaskSynchronousRunLoopMode
   6.321 +                                 beforeDate: [NSDate dateWithTimeIntervalSinceNow: 1.0]];
   6.322 +    return (_resultCode==0);
   6.323 +}
   6.324 +
   6.325 +- (BOOL) waitTillFinished
   6.326 +{
   6.327 +    return [self _waitTillFinishedInMode: _modes.lastObject];
   6.328 +}
   6.329 +
   6.330 +
   6.331 +- (BOOL) run
   6.332 +{
   6.333 +    [_modes addObject: MYTaskSynchronousRunLoopMode];
   6.334 +    return [self start] && [self _waitTillFinishedInMode: MYTaskSynchronousRunLoopMode];
   6.335 +    
   6.336 +}    
   6.337 +
   6.338 +
   6.339 +- (BOOL) run: (NSError**)outError
   6.340 +{
   6.341 +    BOOL result = [self run];
   6.342 +    if( outError ) *outError = self.error;
   6.343 +    return result;
   6.344 +}
   6.345 +
   6.346 +
   6.347 +@synthesize currentDirectoryPath=_currentDirectoryPath, outputData=_outputData, error=_error, isRunning=_isRunning;
   6.348 +
   6.349 +
   6.350 +- (NSString*) output
   6.351 +{
   6.352 +    if( ! _output && _outputData ) {
   6.353 +        _output = [[NSString alloc] initWithData: _outputData encoding: NSUTF8StringEncoding];
   6.354 +        // If output isn't valid UTF-8, fall back to CP1252, aka WinLatin1, a superset of ISO-Latin-1.
   6.355 +        if( ! _output ) {
   6.356 +            _output = [[NSString alloc] initWithData: _outputData encoding: NSWindowsCP1252StringEncoding];
   6.357 +            Log(@"Warning: Output of '%@' was not valid UTF-8; interpreting as CP1252",self);
   6.358 +        }
   6.359 +    }
   6.360 +    return _output;
   6.361 +}
   6.362 +
   6.363 +- (NSString*) outputAndError
   6.364 +{
   6.365 +    NSString *result = self.output ?: @"";
   6.366 +    NSString *errorStr = nil;
   6.367 +    if( _error )
   6.368 +        errorStr = [NSString stringWithFormat: @"%@:\n%@",
   6.369 +                    _error.localizedDescription,_error.localizedRecoverySuggestion];
   6.370 +    else if( _errorData.length > 0 )
   6.371 +        errorStr = [[NSString alloc] initWithData: _errorData encoding: NSUTF8StringEncoding];
   6.372 +    if( errorStr )
   6.373 +        result = [NSString stringWithFormat: @"%@\n\n%@", errorStr,result];
   6.374 +    return result;
   6.375 +}
   6.376 +
   6.377 ++ (NSArray*) keyPathsForValuesAffectingOutputAndError
   6.378 +{
   6.379 +    return [NSArray arrayWithObjects: @"output", @"error", @"errorData",nil];
   6.380 +}
   6.381 +
   6.382 +
   6.383 +@end
     7.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     7.2 +++ b/MYURLFormatter.h	Sat Mar 28 09:36:46 2009 -0700
     7.3 @@ -0,0 +1,23 @@
     7.4 +//
     7.5 +//  URLFormatter.h
     7.6 +//  Murky
     7.7 +//
     7.8 +//  Copyright 2008 Jens Alfke. All rights reserved.
     7.9 +//
    7.10 +
    7.11 +#import <Cocoa/Cocoa.h>
    7.12 +
    7.13 +
    7.14 +/** An NSURLFormatter for text fields that let the user enter URLs.
    7.15 +    The associated text field's objectValue will be an NSURL object. */
    7.16 +@interface MYURLFormatter : NSFormatter
    7.17 +{
    7.18 +    NSArray *_allowedSchemes;
    7.19 +}
    7.20 +
    7.21 +@property (copy,nonatomic) NSArray *allowedSchemes;
    7.22 +
    7.23 ++ (void) beginFilePickerFor: (NSTextField*)field;
    7.24 ++ (void) beginNewFilePickerFor: (NSTextField*)field;
    7.25 +
    7.26 +@end
     8.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     8.2 +++ b/MYURLFormatter.m	Sat Mar 28 09:36:46 2009 -0700
     8.3 @@ -0,0 +1,114 @@
     8.4 +//
     8.5 +//  URLFormatter.m
     8.6 +//  Murky
     8.7 +//
     8.8 +//  Copyright 2008 Jens Alfke. All rights reserved.
     8.9 +//
    8.10 +
    8.11 +#import "MYURLFormatter.h"
    8.12 +
    8.13 +
    8.14 +@implementation MYURLFormatter
    8.15 +
    8.16 +@synthesize allowedSchemes=_allowedSchemes;
    8.17 +
    8.18 +
    8.19 +- (id) init
    8.20 +{
    8.21 +    self = [super init];
    8.22 +    if (self != nil) {
    8.23 +        _allowedSchemes = [[NSArray alloc] initWithObjects: @"http",@"https",@"file",@"ssh",nil];
    8.24 +    }
    8.25 +    return self;
    8.26 +}
    8.27 +
    8.28 +- (void) dealloc
    8.29 +{
    8.30 +    [_allowedSchemes release];
    8.31 +    [super dealloc];
    8.32 +}
    8.33 +
    8.34 +
    8.35 +- (NSString *)stringForObjectValue:(id)obj
    8.36 +{
    8.37 +    if( ! [obj isKindOfClass: [NSURL class]] )
    8.38 +        return @"";
    8.39 +    else if( [obj isFileURL] )
    8.40 +        return [obj path];
    8.41 +    else
    8.42 +        return [obj absoluteString];
    8.43 +}
    8.44 +
    8.45 +
    8.46 +- (BOOL)getObjectValue:(id *)obj forString:(NSString *)str errorDescription:(NSString **)outError
    8.47 +{
    8.48 +    *obj = nil;
    8.49 +    NSString *error = nil;
    8.50 +    if( str.length==0 ) {
    8.51 +    } else if( [str hasPrefix: @"/"] ) {
    8.52 +        *obj = [NSURL fileURLWithPath: str];
    8.53 +        if( ! *obj )
    8.54 +            error = @"Invalid filesystem path";
    8.55 +    } else {
    8.56 +        NSURL *url = [NSURL URLWithString: str];
    8.57 +        NSString *scheme = [url scheme];
    8.58 +        if( url && scheme == nil ) {
    8.59 +            if( [str rangeOfString: @"."].length > 0 ) {
    8.60 +                // Turn "foo.com/bar" into "http://foo.com/bar":
    8.61 +                str = [@"http://" stringByAppendingString: str];
    8.62 +                url = [NSURL URLWithString: str];
    8.63 +                scheme = [url scheme];
    8.64 +            } else
    8.65 +                url = nil;
    8.66 +        }
    8.67 +        if( ! url || ! [url path] || url.host.length==0 ) {
    8.68 +            error = @"Invalid URL";
    8.69 +        } else if( _allowedSchemes && ! [_allowedSchemes containsObject: scheme] ) {
    8.70 +            error = [@"URL protocol must be %@" stringByAppendingString:
    8.71 +                                    [_allowedSchemes componentsJoinedByString: @", "]];
    8.72 +        }
    8.73 +        *obj = url;
    8.74 +    }
    8.75 +    if( outError ) *outError = error;
    8.76 +    return (error==nil);
    8.77 +}
    8.78 +
    8.79 +
    8.80 ++ (void) beginFilePickerFor: (NSTextField*)field
    8.81 +{
    8.82 +    NSParameterAssert(field);
    8.83 +    NSOpenPanel *open = [NSOpenPanel openPanel];
    8.84 +    open.canChooseDirectories = YES;
    8.85 +    open.canChooseFiles = NO;
    8.86 +    open.requiredFileType = (id)kUTTypeDirectory;
    8.87 +    [open beginSheetForDirectory: nil
    8.88 +                            file: nil
    8.89 +                  modalForWindow: field.window
    8.90 +                   modalDelegate: self
    8.91 +                  didEndSelector: @selector(_filePickerDidEnd:returnCode:context:)
    8.92 +                     contextInfo: field];
    8.93 +}
    8.94 +
    8.95 ++ (void) beginNewFilePickerFor: (NSTextField*)field
    8.96 +{
    8.97 +    NSParameterAssert(field);
    8.98 +    NSSavePanel *save = [NSSavePanel savePanel];
    8.99 +    [save beginSheetForDirectory: nil
   8.100 +                            file: nil
   8.101 +                  modalForWindow: field.window
   8.102 +                   modalDelegate: self
   8.103 +                  didEndSelector: @selector(_filePickerDidEnd:returnCode:context:)
   8.104 +                     contextInfo: field];
   8.105 +}
   8.106 +
   8.107 ++ (void) _filePickerDidEnd: (NSSavePanel*)save returnCode: (int)returnCode context: (void*)context
   8.108 +{
   8.109 +    [save orderOut: self];
   8.110 +    if( returnCode == NSOKButton ) {
   8.111 +        NSTextField *field = context;
   8.112 +        field.objectValue = [NSURL fileURLWithPath: save.filename];
   8.113 +    }
   8.114 +}
   8.115 +
   8.116 +
   8.117 +@end