# HG changeset patch # User Jens Alfke # Date 1238258206 25200 # Node ID 5a71993a1a70708b5ab733102a315888ed33e5a4 # Parent d6ab9f52b4d77ab280861ecf0eb2dbf1795cd743 Added some new utilities, taken from Murky. diff -r d6ab9f52b4d7 -r 5a71993a1a70 ImageAndTextCell.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/ImageAndTextCell.h Sat Mar 28 09:36:46 2009 -0700 @@ -0,0 +1,17 @@ +#import + +/** Subclass of NSTextFieldCell which can display text and an image simultaneously. + Taken directly from Apple sample code. */ +@interface ImageAndTextCell : NSTextFieldCell +{ + @private + NSImage *image; +} + +- (void)setImage:(NSImage *)anImage; +- (NSImage *)image; + +- (void)drawWithFrame:(NSRect)cellFrame inView:(NSView *)controlView; +- (NSSize)cellSize; + +@end diff -r d6ab9f52b4d7 -r 5a71993a1a70 ImageAndTextCell.m --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/ImageAndTextCell.m Sat Mar 28 09:36:46 2009 -0700 @@ -0,0 +1,175 @@ +/* + ImageAndTextCell.m + Copyright (c) 2001-2006, Apple Computer, Inc., all rights reserved. + Author: Chuck Pisula + + Milestones: + * 03-01-2001: Initial creation by Chuck Pisula + * 11-04-2005: Added hitTestForEvent:inRect:ofView: for better NSOutlineView support by Corbin Dunn + + Subclass of NSTextFieldCell which can display text and an image simultaneously. +*/ + +/* + IMPORTANT: This Apple software is supplied to you by Apple Computer, Inc. ("Apple") in + consideration of your agreement to the following terms, and your use, installation, + modification or redistribution of this Apple software constitutes acceptance of these + terms. If you do not agree with these terms, please do not use, install, modify or + redistribute this Apple software. + + In consideration of your agreement to abide by the following terms, and subject to these + terms, Apple grants you a personal, non-exclusive license, under AppleÕs copyrights in + this original Apple software (the "Apple Software"), to use, reproduce, modify and + redistribute the Apple Software, with or without modifications, in source and/or binary + forms; provided that if you redistribute the Apple Software in its entirety and without + modifications, you must retain this notice and the following text and disclaimers in all + such redistributions of the Apple Software. Neither the name, trademarks, service marks + or logos of Apple Computer, Inc. may be used to endorse or promote products derived from + the Apple Software without specific prior written permission from Apple. Except as expressly + stated in this notice, no other rights or licenses, express or implied, are granted by Apple + herein, including but not limited to any patent rights that may be infringed by your + derivative works or by other works in which the Apple Software may be incorporated. + + The Apple Software is provided by Apple on an "AS IS" basis. APPLE MAKES NO WARRANTIES, + EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, + MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS + USE AND OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS. + + IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS + OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, + REPRODUCTION, MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED AND + WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), STRICT LIABILITY OR + OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#import "ImageAndTextCell.h" +#import + +@implementation ImageAndTextCell + +- (id)init { + self = [super init]; + if( self ) { + [self setLineBreakMode:NSLineBreakByTruncatingTail]; + [self setSelectable:YES]; + } + return self; +} + +- (void)dealloc { + [image release]; + [super dealloc]; +} + +- (id)copyWithZone:(NSZone *)zone { + ImageAndTextCell *cell = (ImageAndTextCell *)[super copyWithZone:zone]; + // The image ivar will be directly copied; we need to retain or copy it. + cell->image = [image retain]; + return cell; +} + +- (void)setImage:(NSImage *)anImage { + if (anImage != image) { + [image release]; + image = [anImage retain]; + } +} + +- (NSImage *)image { + return image; +} + +- (NSRect)imageRectForBounds:(NSRect)cellFrame { + NSRect result; + if (image != nil) { + result.size = [image size]; + result.origin = cellFrame.origin; + result.origin.x += 3; + result.origin.y += ceil((cellFrame.size.height - result.size.height) / 2); + } else { + result = NSZeroRect; + } + return result; +} + +// We could manually implement expansionFrameWithFrame:inView: and drawWithExpansionFrame:inView: or just properly implement titleRectForBounds to get expansion tooltips to automatically work for us +- (NSRect)titleRectForBounds:(NSRect)cellFrame { + NSRect result; + if (image != nil) { + CGFloat imageWidth = [image size].width; + result = cellFrame; + result.origin.x += (3 + imageWidth); + result.size.width -= (3 + imageWidth); + } else { + result = NSZeroRect; + } + return result; +} + + +- (void)editWithFrame:(NSRect)aRect inView:(NSView *)controlView editor:(NSText *)textObj delegate:(id)anObject event:(NSEvent *)theEvent { + NSRect textFrame, imageFrame; + NSDivideRect (aRect, &imageFrame, &textFrame, 3 + [image size].width, NSMinXEdge); + [super editWithFrame: textFrame inView: controlView editor:textObj delegate:anObject event: theEvent]; +} + +- (void)selectWithFrame:(NSRect)aRect inView:(NSView *)controlView editor:(NSText *)textObj delegate:(id)anObject start:(NSInteger)selStart length:(NSInteger)selLength { + NSRect textFrame, imageFrame; + NSDivideRect (aRect, &imageFrame, &textFrame, 3 + [image size].width, NSMinXEdge); + [super selectWithFrame: textFrame inView: controlView editor:textObj delegate:anObject start:selStart length:selLength]; +} + +- (void)drawWithFrame:(NSRect)cellFrame inView:(NSView *)controlView { + if (image != nil) { + NSRect imageFrame; + NSSize imageSize = [image size]; + NSDivideRect(cellFrame, &imageFrame, &cellFrame, 3 + imageSize.width, NSMinXEdge); + if ([self drawsBackground]) { + [[self backgroundColor] set]; + NSRectFill(imageFrame); + } + imageFrame.origin.x += 3; + imageFrame.size = imageSize; + + if ([controlView isFlipped]) + imageFrame.origin.y += ceil((cellFrame.size.height + imageFrame.size.height) / 2); + else + imageFrame.origin.y += ceil((cellFrame.size.height - imageFrame.size.height) / 2); + + [image compositeToPoint:imageFrame.origin operation:NSCompositeSourceOver]; + } + [super drawWithFrame:cellFrame inView:controlView]; +} + +- (NSSize)cellSize { + NSSize cellSize = [super cellSize]; + cellSize.width += (image ? [image size].width : 0) + 3; + return cellSize; +} + +- (NSUInteger)hitTestForEvent:(NSEvent *)event inRect:(NSRect)cellFrame ofView:(NSView *)controlView { + NSPoint point = [controlView convertPoint:[event locationInWindow] fromView:nil]; + // If we have an image, we need to see if the user clicked on the image portion. + if (image != nil) { + // This code closely mimics drawWithFrame:inView: + NSSize imageSize = [image size]; + NSRect imageFrame; + NSDivideRect(cellFrame, &imageFrame, &cellFrame, 3 + imageSize.width, NSMinXEdge); + + imageFrame.origin.x += 3; + imageFrame.size = imageSize; + // If the point is in the image rect, then it is a content hit + if (NSMouseInRect(point, imageFrame, [controlView isFlipped])) { + // 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. + // By returning the correct parts, we allow NSTableView to correctly begin an edit when the text portion is clicked on. + return NSCellHitContentArea; + } + } + // 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. + return [super hitTestForEvent:event inRect:cellFrame ofView:controlView]; +} + + +@end + diff -r d6ab9f52b4d7 -r 5a71993a1a70 MYDirectoryWatcher.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/MYDirectoryWatcher.h Sat Mar 28 09:36:46 2009 -0700 @@ -0,0 +1,57 @@ +// +// MYDirectoryWatcher.h +// Murky +// +// Copyright 2008 Jens Alfke. All rights reserved. +// + +#import + + +/* A wrapper for FSEvents, which notifies its delegate when filesystem changes occur. */ +@interface MYDirectoryWatcher : NSObject +{ + NSString *_path; + id _target; + SEL _action; + UInt64 _lastEventID; + BOOL _historyDone; + CFTimeInterval _latency; + FSEventStreamRef _stream; +} + +- (id) initWithDirectory: (NSString*)path target: (id)target action: (SEL)action; + +@property (readonly,nonatomic) NSString* path; + +@property UInt64 lastEventID; +@property CFTimeInterval latency; + +- (BOOL) start; +- (void) pause; +- (void) stop; +- (void) stopTemporarily; // stop, but re-start on next runloop cycle + +@end + + + +@interface MYDirectoryEvent : NSObject +{ + MYDirectoryWatcher *watcher; + NSString *path; + UInt64 eventID; + UInt32 flags; +} + +@property (readonly, nonatomic) MYDirectoryWatcher *watcher; +@property (readonly, nonatomic) NSString *path, *relativePath; +@property (readonly, nonatomic) UInt64 eventID; +@property (readonly, nonatomic) UInt32 flags; + +@property (readonly, nonatomic) BOOL mustScanSubdirectories; +@property (readonly, nonatomic) BOOL eventsWereDropped; +@property (readonly, nonatomic) BOOL isHistorical; +@property (readonly, nonatomic) BOOL rootChanged; + +@end diff -r d6ab9f52b4d7 -r 5a71993a1a70 MYDirectoryWatcher.m --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/MYDirectoryWatcher.m Sat Mar 28 09:36:46 2009 -0700 @@ -0,0 +1,206 @@ +// +// MYDirectoryWatcher.m +// Murky +// +// Copyright 2008 Jens Alfke. All rights reserved. +// + +#import "MYDirectoryWatcher.h" +#import + + +static void directoryWatcherCallback(ConstFSEventStreamRef streamRef, + void *clientCallBackInfo, + size_t numEvents, + void *eventPaths, + const FSEventStreamEventFlags eventFlags[], + const FSEventStreamEventId eventIds[]); + +@interface MYDirectoryEvent () +- (id) _initWithWatcher: (MYDirectoryWatcher*)itsWatcher + path: (NSString*)itsPath + flags: (FSEventStreamEventFlags)itsFlags + eventID: (FSEventStreamEventId)itsEventID; +@end + + +@implementation MYDirectoryWatcher + + +- (id) initWithDirectory: (NSString*)path target: (id)target action: (SEL)action +{ + NSParameterAssert(path); + self = [super init]; + if (self != nil) { + _path = path.copy; + _target = target; + _action = action; + _latency = 5.0; + _lastEventID = kFSEventStreamEventIdSinceNow; + } + return self; +} + +- (void) dealloc +{ + [self stop]; + [_path release]; + [super dealloc]; +} + +- (void) finalize +{ + [self stop]; + [super finalize]; +} + + +@synthesize path=_path, latency=_latency, lastEventID=_lastEventID; + + +- (BOOL) start +{ + if( ! _stream ) { + FSEventStreamContext context = {0,self,NULL,NULL,NULL}; + _stream = FSEventStreamCreate(NULL, + &directoryWatcherCallback, &context, + (CFArrayRef)[NSArray arrayWithObject: _path], + _lastEventID, + _latency, + kFSEventStreamCreateFlagUseCFTypes); + if( ! _stream ) + return NO; + FSEventStreamScheduleWithRunLoop(_stream, CFRunLoopGetCurrent(), kCFRunLoopCommonModes); + if( ! FSEventStreamStart(_stream) ) { + [self stop]; + return NO; + } + _historyDone = (_lastEventID == kFSEventStreamEventIdSinceNow); + Log(@"MYDirectoryWatcher: Started on %@ (latency=%g, lastEvent=%llu)",_path,_latency,_lastEventID); + } + return YES; +} + +- (void) pause +{ + if( _stream ) { + FSEventStreamStop(_stream); + FSEventStreamInvalidate(_stream); + FSEventStreamRelease(_stream); + _stream = NULL; + Log(@"MYDirectoryWatcher: Stopped on %@ (lastEvent=%llu)",_path,_lastEventID); + } +} + +- (void) stop +{ + [self pause]; + _lastEventID = kFSEventStreamEventIdSinceNow; // so events from now till next start will be dropped + [NSObject cancelPreviousPerformRequestsWithTarget: self selector: @selector(start) object: nil]; +} + +- (void) stopTemporarily +{ + if( _stream ) { + [self stop]; + [self performSelector: @selector(start) withObject: nil afterDelay: 0.0]; + } +} + + +- (void) _notifyEvents: (size_t)numEvents + paths: (NSArray*)paths + flags: (const FSEventStreamEventFlags[])eventFlags + eventIDs: (const FSEventStreamEventId[])eventIDs +{ + for (size_t i=0; i + + +extern NSString* const MYTaskErrorDomain; +extern NSString* const MYTaskExitCodeKey; +extern NSString* const MYTaskObjectKey; +enum { + kMYTaskError = 2 +}; + + + +@interface MYTask : NSObject +{ + @private + NSString *_command; + NSMutableArray *_arguments; + NSString *_currentDirectoryPath; + NSTask *_task; + int _resultCode; + NSError *_error; + BOOL _ignoreOutput; + NSFileHandle *_outHandle, *_errHandle; + NSMutableData *_outputData, *_errorData; + NSString *_output; + NSMutableArray *_modes; + BOOL _isRunning, _taskRunning; +} + +- (id) initWithCommand: (NSString*)subcommand, ... NS_REQUIRES_NIL_TERMINATION; + +/* designated initializer (subclasses can override) */ +- (id) initWithCommand: (NSString*)subcommand + arguments: (NSArray*)arguments; + +- (id) initWithError: (NSError*)error; + +- (void) addArgument: (id)argument; +- (void) addArguments: (id)arg1, ... NS_REQUIRES_NIL_TERMINATION; +- (void) addArgumentsFromArray: (NSArray*)arguments; +- (void) prependArguments: (id)arg1, ... NS_REQUIRES_NIL_TERMINATION; + +- (void) ignoreOutput; + +@property (copy) NSString* currentDirectoryPath; + +- (BOOL) run; +- (BOOL) run: (NSError**)outError; + +- (BOOL) start; +- (void) stop; +- (BOOL) waitTillFinished; + +@property (readonly,nonatomic) BOOL isRunning; +@property (readonly,retain,nonatomic) NSError* error; +@property (readonly,nonatomic) NSString *output, *outputAndError; +@property (readonly,nonatomic) NSData *outputData; + +// protected: + +/** Subclasses can override this to add arguments or customize the task */ +- (NSTask*) createTask; + +/** Sets the error based on the message and parameters. Always returns NO. */ +- (BOOL) makeError: (NSString*)fmt, ...; + +/** Called when the task finishes, just before the isRunning property changes back to NO. + You can override this to do your own post-processing. */ +- (void) finished; + +@end diff -r d6ab9f52b4d7 -r 5a71993a1a70 MYTask.m --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/MYTask.m Sat Mar 28 09:36:46 2009 -0700 @@ -0,0 +1,380 @@ +// +// MYTask.m +// Murky +// +// Copyright 2008 Jens Alfke. All rights reserved. +// + +#import "MYTask.h" + +//FIX: NOTICE: This code was written assuming garbage collection. It will currently leak like a sieve without it. + + +NSString* const MYTaskErrorDomain = @"MYTaskError"; +NSString* const MYTaskExitCodeKey = @"MYTaskExitCode"; +NSString* const MYTaskObjectKey = @"MYTask"; + +#define MYTaskSynchronousRunLoopMode @"MYTask" + + +@interface MYTask () +@property (readwrite,nonatomic) BOOL isRunning; +@property (readwrite,retain,nonatomic) NSError *error; +- (void) _finishUp; +@end + + +@implementation MYTask + + +- (id) initWithCommand: (NSString*)command + arguments: (NSArray*)arguments +{ + NSParameterAssert(command); + self = [super init]; + if (self != nil) { + _command = command; + _arguments = arguments ?[arguments mutableCopy] :[NSMutableArray array]; + _modes = [NSMutableArray arrayWithObjects: NSDefaultRunLoopMode, NSModalPanelRunLoopMode, nil]; + } + return self; +} + + +- (id) initWithCommand: (NSString*)command, ... +{ + NSMutableArray *arguments = [NSMutableArray array]; + va_list args; + va_start(args,command); + id arg; + while( nil != (arg=va_arg(args,id)) ) + [arguments addObject: [arg description]]; + va_end(args); + + return [self initWithCommand: command arguments: arguments]; +} + + +- (id) initWithError: (NSError*)error +{ + self = [super init]; + if( self ) { + _error = error; + } + return self; +} + + +- (NSString*) description +{ + return [NSString stringWithFormat: @"%@ %@", + _command, [_arguments componentsJoinedByString: @" "]]; +} + + +- (void) addArgument: (id)argument +{ + [_arguments addObject: [argument description]]; +} + +- (void) addArgumentsFromArray: (NSArray*)arguments +{ + for( id arg in arguments ) + [_arguments addObject: [arg description]]; +} + +- (void) addArguments: (id)arg, ... +{ + va_list args; + va_start(args,arg); + while( arg ) { + [_arguments addObject: [arg description]]; + arg = va_arg(args,id); + } + va_end(args); +} + +- (void) prependArguments: (id)arg, ... +{ + va_list args; + va_start(args,arg); + int i=0; + while( arg ) { + [_arguments insertObject: [arg description] atIndex: i++]; + arg = va_arg(args,id); + } + va_end(args); +} + + +- (void) ignoreOutput +{ + _ignoreOutput = YES; +} + + +- (BOOL) makeError: (NSString*)fmt, ... +{ + va_list args; + va_start(args,fmt); + + NSString *message = [[NSString alloc] initWithFormat: fmt arguments: args]; + Log(@"MYTask Error: %@",message); + NSMutableDictionary *info = [NSMutableDictionary dictionaryWithObject: message + forKey: NSLocalizedDescriptionKey]; + _error = [NSError errorWithDomain: MYTaskErrorDomain code: kMYTaskError userInfo: info]; + + va_end(args); + return NO; +} + + +- (NSPipe*) _openPipeAndHandle: (NSFileHandle**)handle notifying: (SEL)selector +{ + NSPipe *pipe = [NSPipe pipe]; + *handle = [pipe fileHandleForReading]; + [[NSNotificationCenter defaultCenter] addObserver: self selector: selector + name: NSFileHandleReadCompletionNotification + object: *handle]; + [*handle readInBackgroundAndNotifyForModes: _modes]; + return pipe; +} + + +- (void) _close +{ + // No need to call -closeFile on file handles obtained from NSPipe (in fact, it can hang) + _outHandle = nil; + _errHandle = nil; + [[NSNotificationCenter defaultCenter] removeObserver: self + name: NSFileHandleReadCompletionNotification + object: nil]; +} + + +/** Subclasses can override this. */ +- (NSTask*) createTask +{ + NSAssert(!_task,@"createTask called twice"); + NSTask *task = [[NSTask alloc] init]; + task.launchPath = _command; + task.arguments = _arguments; + if( _currentDirectoryPath ) + task.currentDirectoryPath = _currentDirectoryPath; + return task; +} + + +- (BOOL) start +{ + NSAssert(!_task, @"Task has already been run"); + if( _error ) + return NO; + + _task = [self createTask]; + NSAssert(_task,@"createTask returned nil"); + + Log(@"Task: %@ %@",_task.launchPath,[_task.arguments componentsJoinedByString: @" "]); + + _task.standardOutput = [self _openPipeAndHandle: &_outHandle notifying: @selector(_gotOutput:)]; + _outputData = [[NSMutableData alloc] init]; + _task.standardError = [self _openPipeAndHandle: &_errHandle notifying: @selector(_gotStderr:)]; + _errorData = [[NSMutableData alloc] init]; + + [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(_exited:) + name: NSTaskDidTerminateNotification + object: _task]; + + @try{ + [_task launch]; + }@catch( id x ) { + Log(@"Task failed to launch: %@",x); + _resultCode = 666; + [self _close]; + return [self makeError: @"Exception launching %@: %@",_task.launchPath,x]; + } + _taskRunning = YES; + self.isRunning = YES; + + return YES; +} + + +- (void) stop +{ + [_task interrupt]; + [self _close]; + _taskRunning = NO; + self.isRunning = NO; +} + + +- (BOOL) _shouldFinishUp +{ + return !_task.isRunning && (_ignoreOutput || (!_outHandle && !_errHandle)); +} + + +- (void) _gotOutput: (NSNotification*)n +{ + NSData *data = [n.userInfo objectForKey: NSFileHandleNotificationDataItem]; + if( n.object == _outHandle ) { + if( data.length > 0 ) { + [_outHandle readInBackgroundAndNotifyForModes: _modes]; + LogTo(Task,@"Got %u bytes of output",data.length); + if( _outputData ) { + [self willChangeValueForKey: @"output"]; + [self willChangeValueForKey: @"outputData"]; + [_outputData appendData: data]; + _output = nil; + [self didChangeValueForKey: @"outputData"]; + [self didChangeValueForKey: @"output"]; + } + } else { + LogTo(Task,@"Closed output"); + _outHandle = nil; + if( [self _shouldFinishUp] ) + [self _finishUp]; + } + } +} + +- (void) _gotStderr: (NSNotification*)n +{ + if( n.object == _errHandle ) { + NSData *data = [n.userInfo objectForKey: NSFileHandleNotificationDataItem]; + if( data.length > 0 ) { + [_errHandle readInBackgroundAndNotifyForModes: _modes]; + LogTo(Task,@"Got %u bytes of stderr",data.length); + [self willChangeValueForKey: @"errorData"]; + [_errorData appendData: data]; + [self didChangeValueForKey: @"errorData"]; + } else { + LogTo(Task,@"Closed stderr"); + _errHandle = nil; + if( [self _shouldFinishUp] ) + [self _finishUp]; + } + } +} + +- (void) _exited: (NSNotification*)n +{ + _resultCode = _task.terminationStatus; + LogTo(Task,@"Exited with result=%i",_resultCode); + _taskRunning = NO; + if( [self _shouldFinishUp] ) + [self _finishUp]; + else + [self performSelector: @selector(_finishUp) withObject: nil afterDelay: 1.0]; +} + + +- (void) _finishUp +{ + [NSObject cancelPreviousPerformRequestsWithTarget: self selector: @selector(_finishUp) object: nil]; + [self _close]; + + LogTo(Task,@"Finished!"); + + if( _resultCode != 0 ) { + // Handle errors: + NSString *errStr = nil; + if( _errorData.length > 0 ) + errStr = [[NSString alloc] initWithData: _errorData encoding: NSUTF8StringEncoding]; + Log(@" *** task returned %i: %@",_resultCode,errStr); + if( errStr.length == 0 ) + errStr = [NSString stringWithFormat: @"Command returned status %i",_resultCode]; + NSString *desc = [NSString stringWithFormat: @"%@ command error", _task.launchPath.lastPathComponent]; + // For some reason the body text in the alert shown by -presentError: is taken from the + // NSLocalizedRecoverySuggestionErrorKey, not the NSLocalizedFailureReasonKey... + NSMutableDictionary *info = [NSMutableDictionary dictionaryWithObjectsAndKeys: + desc, NSLocalizedDescriptionKey, + errStr, NSLocalizedRecoverySuggestionErrorKey, + [NSNumber numberWithInt: _resultCode], MYTaskExitCodeKey, + self, MYTaskObjectKey, + nil]; + self.error = [[NSError alloc] initWithDomain: MYTaskErrorDomain + code: kMYTaskError + userInfo: info]; + } + + [self finished]; + + self.isRunning = NO; +} + +- (void) finished +{ + // This is a hook that subclasses can override to do post-processing. +} + + +- (BOOL) _waitTillFinishedInMode: (NSString*)runLoopMode +{ + // wait for task to exit: + while( _task.isRunning || self.isRunning ) + [[NSRunLoop currentRunLoop] runMode: MYTaskSynchronousRunLoopMode + beforeDate: [NSDate dateWithTimeIntervalSinceNow: 1.0]]; + return (_resultCode==0); +} + +- (BOOL) waitTillFinished +{ + return [self _waitTillFinishedInMode: _modes.lastObject]; +} + + +- (BOOL) run +{ + [_modes addObject: MYTaskSynchronousRunLoopMode]; + return [self start] && [self _waitTillFinishedInMode: MYTaskSynchronousRunLoopMode]; + +} + + +- (BOOL) run: (NSError**)outError +{ + BOOL result = [self run]; + if( outError ) *outError = self.error; + return result; +} + + +@synthesize currentDirectoryPath=_currentDirectoryPath, outputData=_outputData, error=_error, isRunning=_isRunning; + + +- (NSString*) output +{ + if( ! _output && _outputData ) { + _output = [[NSString alloc] initWithData: _outputData encoding: NSUTF8StringEncoding]; + // If output isn't valid UTF-8, fall back to CP1252, aka WinLatin1, a superset of ISO-Latin-1. + if( ! _output ) { + _output = [[NSString alloc] initWithData: _outputData encoding: NSWindowsCP1252StringEncoding]; + Log(@"Warning: Output of '%@' was not valid UTF-8; interpreting as CP1252",self); + } + } + return _output; +} + +- (NSString*) outputAndError +{ + NSString *result = self.output ?: @""; + NSString *errorStr = nil; + if( _error ) + errorStr = [NSString stringWithFormat: @"%@:\n%@", + _error.localizedDescription,_error.localizedRecoverySuggestion]; + else if( _errorData.length > 0 ) + errorStr = [[NSString alloc] initWithData: _errorData encoding: NSUTF8StringEncoding]; + if( errorStr ) + result = [NSString stringWithFormat: @"%@\n\n%@", errorStr,result]; + return result; +} + ++ (NSArray*) keyPathsForValuesAffectingOutputAndError +{ + return [NSArray arrayWithObjects: @"output", @"error", @"errorData",nil]; +} + + +@end diff -r d6ab9f52b4d7 -r 5a71993a1a70 MYURLFormatter.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/MYURLFormatter.h Sat Mar 28 09:36:46 2009 -0700 @@ -0,0 +1,23 @@ +// +// URLFormatter.h +// Murky +// +// Copyright 2008 Jens Alfke. All rights reserved. +// + +#import + + +/** An NSURLFormatter for text fields that let the user enter URLs. + The associated text field's objectValue will be an NSURL object. */ +@interface MYURLFormatter : NSFormatter +{ + NSArray *_allowedSchemes; +} + +@property (copy,nonatomic) NSArray *allowedSchemes; + ++ (void) beginFilePickerFor: (NSTextField*)field; ++ (void) beginNewFilePickerFor: (NSTextField*)field; + +@end diff -r d6ab9f52b4d7 -r 5a71993a1a70 MYURLFormatter.m --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/MYURLFormatter.m Sat Mar 28 09:36:46 2009 -0700 @@ -0,0 +1,114 @@ +// +// URLFormatter.m +// Murky +// +// Copyright 2008 Jens Alfke. All rights reserved. +// + +#import "MYURLFormatter.h" + + +@implementation MYURLFormatter + +@synthesize allowedSchemes=_allowedSchemes; + + +- (id) init +{ + self = [super init]; + if (self != nil) { + _allowedSchemes = [[NSArray alloc] initWithObjects: @"http",@"https",@"file",@"ssh",nil]; + } + return self; +} + +- (void) dealloc +{ + [_allowedSchemes release]; + [super dealloc]; +} + + +- (NSString *)stringForObjectValue:(id)obj +{ + if( ! [obj isKindOfClass: [NSURL class]] ) + return @""; + else if( [obj isFileURL] ) + return [obj path]; + else + return [obj absoluteString]; +} + + +- (BOOL)getObjectValue:(id *)obj forString:(NSString *)str errorDescription:(NSString **)outError +{ + *obj = nil; + NSString *error = nil; + if( str.length==0 ) { + } else if( [str hasPrefix: @"/"] ) { + *obj = [NSURL fileURLWithPath: str]; + if( ! *obj ) + error = @"Invalid filesystem path"; + } else { + NSURL *url = [NSURL URLWithString: str]; + NSString *scheme = [url scheme]; + if( url && scheme == nil ) { + if( [str rangeOfString: @"."].length > 0 ) { + // Turn "foo.com/bar" into "http://foo.com/bar": + str = [@"http://" stringByAppendingString: str]; + url = [NSURL URLWithString: str]; + scheme = [url scheme]; + } else + url = nil; + } + if( ! url || ! [url path] || url.host.length==0 ) { + error = @"Invalid URL"; + } else if( _allowedSchemes && ! [_allowedSchemes containsObject: scheme] ) { + error = [@"URL protocol must be %@" stringByAppendingString: + [_allowedSchemes componentsJoinedByString: @", "]]; + } + *obj = url; + } + if( outError ) *outError = error; + return (error==nil); +} + + ++ (void) beginFilePickerFor: (NSTextField*)field +{ + NSParameterAssert(field); + NSOpenPanel *open = [NSOpenPanel openPanel]; + open.canChooseDirectories = YES; + open.canChooseFiles = NO; + open.requiredFileType = (id)kUTTypeDirectory; + [open beginSheetForDirectory: nil + file: nil + modalForWindow: field.window + modalDelegate: self + didEndSelector: @selector(_filePickerDidEnd:returnCode:context:) + contextInfo: field]; +} + ++ (void) beginNewFilePickerFor: (NSTextField*)field +{ + NSParameterAssert(field); + NSSavePanel *save = [NSSavePanel savePanel]; + [save beginSheetForDirectory: nil + file: nil + modalForWindow: field.window + modalDelegate: self + didEndSelector: @selector(_filePickerDidEnd:returnCode:context:) + contextInfo: field]; +} + ++ (void) _filePickerDidEnd: (NSSavePanel*)save returnCode: (int)returnCode context: (void*)context +{ + [save orderOut: self]; + if( returnCode == NSOKButton ) { + NSTextField *field = context; + field.objectValue = [NSURL fileURLWithPath: save.filename]; + } +} + + +@end