Added some new utilities, taken from Murky.
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