// // ArtSaverView.m // // Copyright (c) 2006-2077 Gabriel Zachmann. All rights reserved. // #import #import #import #import #import #import #import #import #import #import #import #import #import #import "iPhoto.h" #import "ArtSaverView.h" // Important note! // It can well happen that several instances of this class are created at the same time!! // And they all run in the same thread! // For instance, if the user clicks 'Test' in System Preferences, // then stopAnimation will be called for the first instance (that gets displayed in the little preview window), // the new instance is created and initialized with initWithFrame and startAnimation; after the test is finished (.e.g, when the user presses a key), // stopAnimation is called on the new instance, and startAnimation on the "old" instance. // Or, for instance, when there are multiple screens attached, then (I suspect) multiple instances are created, // one for each screen. const float MinimalImageSizeDisplayed = 0.43; const float QueryProgressUpdateTime = 0.25; static ArtSaverView * first_instance = nil; // Just a helper class to provide sort of functor for the -drawLayer: method @interface TextLayerDelegate : NSObject { ArtSaverView * saver_; } - (void) drawLayer: (CALayer *) theLayer inContext: (CGContextRef) theContext; - (id) initWith: (ArtSaverView*) saver; @end @implementation TextLayerDelegate - (void) drawLayer: (CALayer *) theLayer inContext: (CGContextRef) ctxt { NSMutableString * mesg; if ( !saver_.displayBasename_ && !saver_.showImagePath_ && !saver_.errmsgIsError_ ) // just clear the message mesg = [[NSMutableString alloc] initWithString: @" "]; else mesg = [[NSMutableString alloc] initWithString: saver_.errmesg_]; if ( saver_.paused_ ) [mesg appendString: @" (paused)"]; // show 'query is gathering' symbol static int query_progress = 0; if ( saver_.queryIsInProgress_ ) { query_progress ++ ; if ( query_progress > 3 ) query_progress = 0; NSString * progr_str = nil; switch ( query_progress ) { case 0: progr_str = @"| "; break; case 1: progr_str = @"/ "; break; case 2: progr_str = @"- "; break; case 3: progr_str = @"\\ "; break; } [mesg insertString: progr_str atIndex: 0]; } CGContextSetShouldAntialias( ctxt, TRUE ); CGContextSetRGBFillColor( ctxt, 1.0, 1.0, 1.0, 1.0 ); CGContextSelectFont( ctxt, "Andale Mono", saver_.fontSize_, kCGEncodingMacRoman ); const char * str = [mesg cStringUsingEncoding: NSMacOSRomanStringEncoding]; CGColorRef blue = CGColorCreateGenericRGB( 0.0, 0.0, 1.0, 1.0 ); CGContextSetShadowWithColor( ctxt, CGSizeMake(0,0), 10, blue ); float offset = 7.0f; // if ( saver_.drawRect_.size.height > 900 ) // should be synonymous if ( theLayer.bounds.size.height > 900 ) offset = 15.0f; CGContextShowTextAtPoint( ctxt, theLayer.bounds.origin.x + offset, theLayer.bounds.origin.y + offset, str, strlen(str) ); [mesg release]; } - (id) initWith: (ArtSaverView*) saver { if ( ! [super init] ) return nil; saver_ = saver; return self; } @end @implementation ArtSaverView @synthesize errmesg_; @synthesize errmsgIsError_; @synthesize showImagePath_; @synthesize displayBasename_; @synthesize queryIsInProgress_; @synthesize fontSize_; @synthesize paused_; @synthesize drawRect_; #pragma mark - #pragma mark Init: // This gets called only once per instance - (id) initWithFrame: (NSRect) frameRect isPreview: (BOOL) preview { if ( ! [super initWithFrame: frameRect isPreview: preview] ) return nil; // init stuff that should be init'ed only once errmesg_ = [[NSString stringWithString: @" "] retain]; errmsgIsError_ = NO; log_client_ = asl_open( NULL, NULL, 0 ); if ( ! log_client_ ) syslog( LOG_ERR, "ArtSaver: asl_open failed: %m!" ); else { log_msg_ = asl_new( ASL_TYPE_MSG ); asl_set( log_msg_, ASL_KEY_SENDER, "ArtSaver"); // maybe, we should also output the ID of the ArtSaver instance } drawRect_ = frameRect; // origin is not necessarily (0,0)! especially not in the preview window // output version number NSBundle * bundle = [NSBundle bundleForClass:[self class]]; NSString * version; if ( ! bundle ) { [self logMessage: @"BUG: NSBundle bundleForClass failed!" asError: YES]; version = @"?"; } else { version = [[bundle infoDictionary] objectForKey:@"CFBundleVersion"]; if ( ! version ) { [self logMessage: @"CFBundleVersion not found!" asError: YES]; version = @"?"; } } NSString * welcomemsg = [NSString stringWithFormat: @"ArtSaver started (version: %@) (id = %p)", version, (id) self]; [self logMessage: welcomemsg asError: NO]; exec_version_ = [[bundle infoDictionary] objectForKey:@"CFBundleShortVersionString"]; if ( ! exec_version_ ) // can happen if it's a *very* old version { [self logMessage: @"CFBundleShortVersionString not found!" asError: YES]; exec_version_ = @"?"; exec_version_num_ = 0; } else exec_version_num_ = (unsigned int) ([exec_version_ floatValue] * 10); // create some layers // make the view layer-hosting and become the delegate for the root layer mainLayer_ = [CALayer layer]; mainLayer_.zPosition = 0.0; mainLayer_.delegate = self; [mainLayer_ setNeedsDisplay]; // causes the layer content to be drawn in -drawRect: [self setLayer: mainLayer_]; self.wantsLayer = YES; [self setNeedsDisplay: YES]; // insert a dummy layer as a placeholder for the image layer later currentLayer_ = [CALayer layer]; [mainLayer_ addSublayer: currentLayer_]; textLayer_ = [CALayer layer]; textLayer_.zPosition = 0.0; textLayer_.bounds = NSRectToCGRect( drawRect_ ); // Warning: if bounds==nil or = the empty rectangle, then the delegate's drawLayer: won't get called! TextLayerDelegate * textlayer_delegate = [[TextLayerDelegate alloc] initWith: self]; CFRetain( textlayer_delegate ); // otherwise, it seems the textlayer_delegate gets collected away by the GC .. textLayer_.delegate = textlayer_delegate; textLayer_.position = CGPointMake( 0,0 ); textLayer_.anchorPoint = CGPointMake( 0, 0 ); textLayer_.opacity = 1.0; [textLayer_ setNeedsDisplay]; [mainLayer_ addSublayer: textLayer_ ]; filename_ = nil; displayPreviousImage_ = NO; previousImageIndex_ = 0; for ( int i = 0; i < history_size; i ++ ) history_[i] = 0; paused_ = FALSE; img_index_ = 0; allowBlackBorders_ = TRUE; zoomIn_ = YES; rescan_ = NO; queryIsInProgress_ = NO; query_ = nil; fmanager_ = [[NSFileManager defaultManager] retain]; // Check whether another instance is already running (this must be one that is running in System Preferences' preview window, // or one that is rendering to a different display) AND it is doing a Spotlight search (so it must be the one // in the preview window). if ( first_instance && [first_instance queryIsInProgress_] ) { // Then, we don't do anything but display a message askUserToQuit_ = YES; [self logMessage: @"another instance detected, and it's doing a Spotlight search right now" asError: NO]; [self setAnimationTimeInterval: [self animationTimeInterval]]; lastFrameTime_ = [[NSDate dateWithTimeIntervalSinceNow: -1000] retain]; return self; } if ( !first_instance && preview ) first_instance = self; // we are the first instance, and we are running in the preview window askUserToQuit_ = NO; askUserToWait_ = 0; // Because it takes a little while, init the iPhoto variables only when a config sheet is opened, // i.e., when we are running in the System Preferences, and the user has clicked on "Options" iPhoto_ = nil; albumNames_ = [[NSArray alloc] init]; albums_ = nil; selectedAlbum_ = -1; // initialize the random number generator srandomdev(); // default config values NSArray * objects = [NSArray arrayWithObjects: [NSHomeDirectory() stringByAppendingPathComponent: @"Pictures/"], // ImageDirectory [NSNumber numberWithInteger: 15], // DurationPerImage [NSNumber numberWithFloat: 15], // DurationPerCycle [NSNumber numberWithFloat: 1.1], // InitZoomInFactor [NSNumber numberWithInteger: 300], // ExcludeSize [NSNumber numberWithInteger: 5], // ExcludekB [NSNumber numberWithBool: YES], // ShowImagePath [NSNumber numberWithBool: YES], // UseSpotlight [NSNumber numberWithBool: NO], // DisplayBasename [NSNumber numberWithBool: YES], // ZoomIsOn [NSNumber numberWithInteger: 14], // FontSize [NSNumber numberWithBool: NO], // UseAlbum @"*/thumb*", // SkipPatterns [NSNumber numberWithBool: NO], // DontRandomize [NSNumber numberWithBool: YES], // AllowBlackBorders [NSNumber numberWithFloat: 0.0], // BlackBorderAvoidance [NSNumber numberWithInteger: 10], // version number @"", // ImageList nil ]; NSArray * keys = [NSArray arrayWithObjects: @"ImageDirectory", @"DurationPerImage", @"DurationPerCycle", @"InitZoomInFactor", @"ExcludeSize", @"ExcludekB", @"ShowImagePath", @"UseSpotlight", @"DisplayBasename", @"ZoomIsOn", @"FontSize", @"UseAlbum", @"SkipPatterns", @"DontRandomize", @"AllowBlackBorders", @"BlackBorderAvoidance", @"VersionNumber", @"ImageList", nil ]; NSDictionary * appDefaults = [NSDictionary dictionaryWithObjects: objects forKeys: keys]; defaults_ = [[ScreenSaverDefaults defaultsForModuleWithName: @"de.zach.ArtSaver"] retain]; if ( ! defaults_ ) [self logMessage: @"Could not create a defaults object!" asError: YES]; [defaults_ registerDefaults: appDefaults]; [defaults_ synchronize]; // now try to read the user preferences directoryLocation_ = [[defaults_ stringForKey: @"ImageDirectory"] retain]; durationPerImage_ = [defaults_ floatForKey: @"DurationPerImage"]; durationPerCycle_ = [defaults_ floatForKey: @"DurationPerCycle"]; initZoomInFactor_ = [defaults_ floatForKey: @"InitZoomInFactor"]; excludeSize_ = [defaults_ integerForKey: @"ExcludeSize"]; excludekB_ = [defaults_ integerForKey: @"ExcludekB"]; showImagePath_ = [defaults_ boolForKey: @"ShowImagePath"]; useSpotlight_ = [defaults_ boolForKey: @"UseSpotlight"]; displayBasename_ = [defaults_ boolForKey: @"DisplayBasename"]; zoomIsOn_ = [defaults_ boolForKey: @"ZoomIsOn"]; fontSize_ = [defaults_ integerForKey: @"FontSize"]; useAlbum_ = [defaults_ boolForKey: @"UseAlbum"]; dontRandomize_ = [defaults_ boolForKey: @"DontRandomize"]; allowBlackBorders_ = [defaults_ boolForKey: @"AllowBlackBorders"]; blackBorderAvoidance_ = [defaults_ floatForKey: @"BlackBorderAvoidance"]; plist_version_num_ = [defaults_ integerForKey: @"VersionNumber"]; NSString * patterns = [defaults_ stringForKey: @"SkipPatterns"]; skipPatterns_ = [[patterns componentsSeparatedByString: @"\n"] retain]; NSString * filelist = [defaults_ stringForKey: @"ImageList"]; imagefiles_ = [[filelist componentsSeparatedByString: @"\n"] retain]; [self logMessage: [NSString stringWithFormat: @"num images from config = %u", [imagefiles_ count]] asError: NO]; if ( imagefiles_ == nil || [imagefiles_ count] == 0 || [[imagefiles_ objectAtIndex:0] length] == 0 ) { imagefiles_ = [[NSArray alloc] init]; [self scanDirectory: directoryLocation_]; [self saveImageList]; } // init more state from the preferences [self changeDir: directoryLocation_]; lastFrameTime_ = [[NSDate dateWithTimeIntervalSinceNow: -durationPerImage_] retain]; [self setAnimationTimeInterval: [self animationTimeInterval]]; // CFArrayRef types = CGImageSourceCopyTypeIdentifiers(); // show the supported image types // CFShow(types); // which does not necessarily mean that Spotlight finds them, too // determine max texture size supported by OpenGL // (code stolen from "Determining the OpenGL Capabilities Supported by the Hardware" in "OpenGL Programming Guide for Mac OS X") maxTextureSize_ = 0; // max texture dimensions the graphics card can handle maxRectangleTextureSize_ = 0; CGDirectDisplayID display = CGMainDisplayID(); CGOpenGLDisplayMask myDisplayMask = CGDisplayIDToOpenGLDisplayMask( display ); CGLPixelFormatAttribute attribs[] = { kCGLPFADisplayMask, myDisplayMask, (CGLPixelFormatAttribute) 0 }; CGLPixelFormatObj pixelFormat = NULL; GLint numPixelFormats = 0; CGLContextObj myCGLContext = 0; CGLContextObj curr_ctx = CGLGetCurrentContext(); CGLChoosePixelFormat( attribs, &pixelFormat, &numPixelFormats ); if ( pixelFormat ) { CGLCreateContext( pixelFormat, NULL, &myCGLContext ); CGLDestroyPixelFormat( pixelFormat ); CGLSetCurrentContext( myCGLContext ); if ( myCGLContext ) { glGetIntegerv( GL_MAX_TEXTURE_SIZE, &maxTextureSize_ ); glGetIntegerv( GL_MAX_RECTANGLE_TEXTURE_SIZE_ARB, &maxRectangleTextureSize_ ); } } CGLDestroyContext( myCGLContext ); CGLSetCurrentContext( curr_ctx ); // restore previous OpenGL context [self logMessage: [NSString stringWithFormat: @"max texture size = %d , max rectangle texture size = %d", maxTextureSize_, maxRectangleTextureSize_] asError: NO]; return self; } - (void) dealloc { if ( first_instance == self ) first_instance = nil; // very important! System Preferences seems to create a new instance of ScreenSaverView everytime the user switches screensaver modules! [super dealloc]; } - (void) finalize { if ( first_instance == self ) first_instance = nil; // dito [super finalize]; } // This could be called several times, too, for instance, by System Preferences, whenever the config sheet is brought up - (void) startAnimation { [super startAnimation]; } - (void) stopAnimation { [super stopAnimation]; } #pragma mark - #pragma mark Display: // Clear the root layer (which is behind the other layers) - (void) drawRect: (NSRect) rect { [[NSColor blackColor] set]; [NSBezierPath fillRect: rect]; } - (void) logMessage: (NSString*) msg asError: (bool) err { //NSLog( @"%@", msg ); // only necessary, if you want the messages also in SaverLab's own little console window // shouldn't be necessary, though, because you can always get the messages in a terminal with 'syslog -w -k Sender ArtSaver' (with a little delay) int loglevel = ASL_LEVEL_NOTICE; // LEVEL_INFO does not get through :-( if ( err ) loglevel = ASL_LEVEL_ERR; if ( log_msg_ ) asl_log( NULL, log_msg_, loglevel, "%s", [msg cStringUsingEncoding: NSASCIIStringEncoding] ); // Note: slashes in pathnames will appear as ':' in Unix shells; // but I keep the "\/" (which was substituted for the ':' in printFilename:), because I'm lazy. // Also, asl_log apparently escapes the \ again by another \, so pathnames will look like "w\\/ slash/image.jpg". } // Store msg in errmesg_ and flag textLayer_ - (void) displayMessage: (NSString*) msg { [errmesg_ release]; errmesg_ = [[NSMutableString stringWithString: msg] retain]; errmsgIsError_ = NO; [textLayer_ setNeedsDisplay]; } // Output msg also to log file - (void) printMessage: (NSString*) msg asError: (bool) err { [self displayMessage: msg]; errmsgIsError_ = err; if ( [ errmesg_ length] > 0 ) [self logMessage: errmesg_ asError: errmsgIsError_ ]; } - (void) printMessage: (NSString*) msg1 with: (NSString*) msg2 asError: (bool) err { NSMutableString * msg = [[NSMutableString alloc] initWithString: msg1 ]; if ( msg2 ) [ msg appendString: msg2 ]; [self printMessage: msg asError: err]; } // Display filename at the bottom left of the screen (on the second layer) // Replaces ':' by '\/' - (void) printFilename: (NSString*) filename { NSString * fn = [filename stringByReplacingOccurrencesOfString: @":" withString: @"\\/"]; if ( displayBasename_ ) [self printMessage: fn asError: NO]; else if ( showImagePath_ ) [self printMessage: fn asError: NO]; else { [self printMessage: @"" asError: NO]; // just clear the old message [self logMessage: filename asError: NO]; // but still write the image file name to the log } } // Could be called in SaverLab, if user resizes the window - (void) setFrameSize: (NSSize) newSize { [super setFrameSize: newSize]; drawRect_ = [self frame]; } - (NSTimeInterval) animationTimeInterval { return 0.2; } // This is the work-horse of the whole program; // due to lots of options, its logic is a bit complicated. - (void) animateOneFrame { if ( queryIsInProgress_ ) return; // do nothing if time for current image is not up yet if ( fabs( [lastFrameTime_ timeIntervalSinceNow] ) < durationPerImage_ ) return; // do also nothing if paused if ( paused_ ) return; [lastFrameTime_ release]; lastFrameTime_ = [[NSDate date] retain]; if ( plist_version_num_ < 21 ) { fontSize_ = 16; [self displayErrorMessage: @"Please press Rescan in Options again." with: nil]; return; } if ( askUserToQuit_ ) { fontSize_ = 16; [self displayErrorMessage: @"Please wait until image search in the preview window is finished. Then press 'Test' again." with: nil]; return; } if ( [imagefiles_ count] < 1 ) { filename_ = nil; [self printMessage: @"No images found. Please choose directory other than " with: directoryLocation_ asError: YES]; return; } // determine new image (and fading duration) float fading_duration; if ( displayPreviousImage_ ) { // user has just pressed arrow left or arrow right displayPreviousImage_ = NO; img_index_ = history_[previousImageIndex_]; fading_duration = 0.5; } else { fading_duration = 2.0; // choose new image if ( dontRandomize_ ) { if ( previousImageIndex_ ) img_index_ = history_[0]; img_index_ ++ ; if ( img_index_ >= [imagefiles_ count] ) img_index_ = 0; } else img_index_ = random() % [imagefiles_ count]; for ( int i = history_size-1; i > 0; i -- ) history_[i] = history_[i-1]; history_[0] = img_index_; previousImageIndex_ = 0; // at the front of the queue again } filename_ = [imagefiles_ objectAtIndex: img_index_]; // load new image from disk NSURL * url = [NSURL fileURLWithPath: filename_ isDirectory: NO]; // this escapes any characters that are not allowed in URLs (space, &, etc.) if ( url == NULL ) { [self displayErrorMessage: @"File name contains weird char: " with: filename_]; return; } CGImageSourceRef sourceRef = CGImageSourceCreateWithURL( (CFURLRef) url, NULL ); if ( sourceRef == NULL ) { [self displayErrorMessage: @"Image gone: " with: filename_]; return; } // check image dimensions if it fits in OpenGL's textures int imgwidth = 0, imgheight = 0; CFDictionaryRef fileProps = CGImageSourceCopyPropertiesAtIndex( sourceRef, 0, NULL ); CFNumberRef num = (CFNumberRef) CFDictionaryGetValue( fileProps, kCGImagePropertyPixelWidth ); if ( num ) CFNumberGetValue( num, kCFNumberIntType, & imgwidth ); num = (CFNumberRef) CFDictionaryGetValue( fileProps, kCGImagePropertyPixelHeight ); if ( num ) CFNumberGetValue( num, kCFNumberIntType, & imgheight ); int img_orientation = 0; num = (CFNumberRef) CFDictionaryGetValue( fileProps, kCGImagePropertyOrientation ); if ( num ) CFNumberGetValue( num, kCFNumberIntType, & img_orientation ); if ( img_orientation < 1 || img_orientation > 8 ) img_orientation = 1; if ( imgwidth > maxTextureSize_ || imgwidth > maxRectangleTextureSize_ || imgheight > maxTextureSize_ || imgheight > maxRectangleTextureSize_ ) { [self displayErrorMessage: @"Image too large for graphics memory: " with: filename_]; return; } // now actually load the image CGImageRef imageRef = CGImageSourceCreateImageAtIndex( sourceRef, 0, NULL ); CFRelease(sourceRef); if ( imageRef == NULL ) { [self displayErrorMessage: @"Image loading failed: " with: filename_]; return; } // now display image file name [self printFilename: filename_]; // schedule a timer to initiate garbage collection in 2.5 seconds' time, so the old image gets freed // seems to be a bit smoother if we do it after the old layer is completely gone (after the animation) // instead of doing it right after the new image has been loaded and freed again (in makeImageLayer) [NSTimer scheduledTimerWithTimeInterval: 2.3 target: self selector: @selector(doGarbageCollection:) userInfo: nil repeats: NO]; // create new layer that centeres the new image CALayer * newlayer; if ( ! zoomIsOn_ ) { newlayer = [self makeImageLayer: imageRef withOrientation: img_orientation]; if ( ! [self allowBlackBordersWidth: imgwidth height: imgheight orientation: img_orientation] ) newlayer.contentsGravity = kCAGravityResizeAspectFill; } else if ( imgwidth < MinimalImageSizeDisplayed * drawRect_.size.width && imgheight < MinimalImageSizeDisplayed * drawRect_.size.height && allowBlackBorders_ ) { // start with scaled-up image, if it is too small zoomIn_ = YES; NSSize startsize = drawRect_.size; startsize.width *= MinimalImageSizeDisplayed; startsize.height *= MinimalImageSizeDisplayed; NSSize endsize = startsize; endsize.width *= 1.5; endsize.height *= 1.5; // evtl. neue option daraus machen? newlayer = [self makeImageLayer: imageRef fromSize: startsize toSize: endsize withOrientation: img_orientation]; } else { // reverse zoom zoomIn_ = ! zoomIn_; NSSize smallsize = drawRect_.size; NSSize largesize = smallsize; if ( [self allowBlackBordersWidth: imgwidth height: imgheight orientation: img_orientation] ) { smallsize.width /= initZoomInFactor_; smallsize.height /= initZoomInFactor_; } largesize.width *= initZoomInFactor_; // assumption: initZoomInFactor_ > 1 ! largesize.height *= initZoomInFactor_; if ( zoomIn_ ) // start with small image, then grow it newlayer = [self makeImageLayer: imageRef fromSize: smallsize toSize: largesize withOrientation: img_orientation]; else newlayer = [self makeImageLayer: imageRef fromSize: largesize toSize: smallsize withOrientation: img_orientation]; if ( ! [self allowBlackBordersWidth: imgwidth height: imgheight orientation: img_orientation] ) newlayer.contentsGravity = kCAGravityResizeAspectFill; } // swap the old and the new layer (causes fade animation) [CATransaction begin]; [CATransaction setValue: [NSNumber numberWithFloat: fading_duration] forKey: kCATransactionAnimationDuration ]; [mainLayer_ replaceSublayer: currentLayer_ with: newlayer]; currentLayer_ = newlayer; [CATransaction commit]; } // Remove current layer, so we get a black screen; print error message on screen and in log; // set lastFrameTime_ so that next image will be tried in 3 seconds' time - (void) displayErrorMessage: (NSString*) msg1 with: (NSString*) msg2 { [self printMessage: msg1 with: msg2 asError: YES]; // remove the current layer if ( currentLayer_ ) { CALayer * emptylayer = [CALayer layer]; [mainLayer_ replaceSublayer: currentLayer_ with: emptylayer]; currentLayer_ = emptylayer; } if ( durationPerImage_ > 3.0 ) { [lastFrameTime_ release]; lastFrameTime_ = [[NSDate dateWithTimeIntervalSinceNow: - (durationPerImage_ - 3.0)] retain]; // try next image in 3 seconds' time } return; } - (CALayer *) makeImageLayer: (CGImageRef) img withOrientation: (int) orientation { // create new layer containing the image CALayer * imgLayer = [CALayer layer]; imgLayer.contents = (id) img; imgLayer.contentsGravity = kCAGravityResizeAspect; // kCAGravityCenter; imgLayer.delegate = nil; imgLayer.opacity = 1.0; imgLayer.position = CGPointMake( drawRect_.size.width/2.0, drawRect_.size.height/2.0 ); imgLayer.anchorPoint = CGPointMake( 0.5, 0.5 ); imgLayer.zPosition = -1.0; // makes the image appear behind the text // set up a transform to handle the orientation if ( orientation < 1 || orientation > 8 ) orientation = 1; if ( orientation >= 5 ) // swap width & height imgLayer.bounds = CGRectMake(drawRect_.origin.x, drawRect_.origin.y, drawRect_.size.height, drawRect_.size.width); else imgLayer.bounds = NSRectToCGRect( drawRect_ ); CGAffineTransform trf; switch ( orientation ) { case 1: // 1 = 0th row is at the top, and 0th column is on the left. // Orientation Normal trf = CGAffineTransformMake(1.0, 0.0, 0.0, 1.0, 0.0, 0.0); break; case 2: // 2 = 0th row is at the top, and 0th column is on the right. // Flip Horizontal trf = CGAffineTransformMake(-1.0, 0.0, 0.0, 1.0, 0.0, 0.0); break; case 3: // 3 = 0th row is at the bottom, and 0th column is on the right. // Rotate 180 degrees trf = CGAffineTransformMake(-1.0, 0.0, 0.0, -1.0, 0.0, 0.0); break; case 4: // 4 = 0th row is at the bottom, and 0th column is on the left. // Flip Vertical trf = CGAffineTransformMake(1.0, 0.0, 0, -1.0, 0.0, 0.0); break; case 5: // 5 = 0th row is on the left, and 0th column is the top. // Rotate -90 degrees and Flip Vertical trf = CGAffineTransformMake(0.0, -1.0, -1.0, 0.0, 0.0, 0.0); break; case 6: // 6 = 0th row is on the right, and 0th column is the top. // Rotate 90 degrees trf = CGAffineTransformMake(0.0, -1.0, 1.0, 0.0, 0.0, 0.0); break; case 7: // 7 = 0th row is on the right, and 0th column is the bottom. // Rotate 90 degrees and Flip Vertical trf = CGAffineTransformMake(0.0, 1.0, 1.0, 0.0, 0.0, 0.0); break; case 8: // 8 = 0th row is on the left, and 0th column is the bottom. // Rotate -90 degrees trf = CGAffineTransformMake(0.0, 1.0,-1.0, 0.0, 0.0, 0.0); break; } imgLayer.transform = CATransform3DMakeAffineTransform( trf ); CGImageRelease( img ); // very important! otherwise, we'd have a memory leak! at least in the non-garbage collected environment return imgLayer; } // Make a new layer containing the image with an animation for the size - (CALayer *) makeImageLayer: (CGImageRef) img fromSize: (NSSize) startsize toSize: (NSSize) endsize withOrientation: (int) orientation { CALayer * imgLayer = [self makeImageLayer: img withOrientation: orientation]; // create animation for growing/shrinking CABasicAnimation * anim = [CABasicAnimation animationWithKeyPath: @"bounds.size"]; anim.timingFunction = [CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseOut]; anim.duration = durationPerCycle_; anim.autoreverses = YES; anim.repeatCount = 1e100; // = forever if ( orientation < 1 || orientation > 8 ) orientation = 1; if ( orientation <= 4 ) { anim.fromValue = [NSValue valueWithSize: startsize]; anim.toValue = [NSValue valueWithSize: endsize]; } else { // swap width & height, because the image's orientation is a transposition or rotation anim.fromValue = [NSValue valueWithSize: NSMakeSize(startsize.height, startsize.width) ]; anim.toValue = [NSValue valueWithSize: NSMakeSize(endsize.height, endsize.width) ]; } [imgLayer addAnimation: anim forKey: nil]; // we could remove this anim in stopAnimation, i.e., when the config sheet is up, but I don't think that's more user friendly return imgLayer; } // Determine whether or not an image is eligible for scaling so that ArtSaver tries to avoid black borders (take orientation into account) // Return true if image = "too much vertical" <=> aspect(image) < aspect(display) - offset(=blackBorderAvoidance_) // (My MacBookPro's display has aspect ratio 1.6, but most cameras seem to have aspect ratio 1.5) - (bool) allowBlackBordersWidth: (int) width height: (int) height orientation: (int) orientation { if ( allowBlackBorders_ ) return TRUE; if ( orientation < 1 || orientation > 8 ) orientation = 1; if ( orientation >= 5 ) { // swap width & height int h = width; width = height; height = h; } return ( (float)width / (float)height ) < ( (float)drawRect_.size.width / (float)drawRect_.size.height - blackBorderAvoidance_); } - (void) doGarbageCollection: (NSTimer *) theTimer { if ( [NSGarbageCollector defaultCollector] ) [[NSGarbageCollector defaultCollector] collectExhaustively]; // seems to work better than collectIfNeeded } #pragma mark - #pragma mark Scanning for Images: - (void) scanDirectory: (NSString*) dir { [self logMessage: [NSString stringWithFormat: @"scan directory %@", dir] asError: NO]; if ( useSpotlight_ ) { [self logMessage: @"starting directory scan using spotlight ..." asError: NO]; [self scanDirectorySpotlight: dir]; } else { [self logMessage: @"starting directory scan 'by hand' ..." asError: NO]; [self scanDirectoryByHand: dir]; } } - (void) scanDirectorySpotlight: (NSString*) dir { queryIsInProgress_ = YES; searchProgress_ = 0; askUserToWait_ = 0; query_ = [[NSMetadataQuery alloc] init]; NSPredicate * predicate = [NSPredicate predicateWithFormat: @"(kMDItemContentTypeTree = 'public.image') && (kMDItemFSSize > %u) && (kMDItemPixelHeight > %u) && (kMDItemPixelWidth > %u)", excludekB_ * 1024, excludeSize_, excludeSize_ ]; [query_ setSearchScopes: [NSArray arrayWithObject: dir]]; [query_ setPredicate: predicate]; [query_ startQuery]; queryProgressTimer_ = [NSTimer scheduledTimerWithTimeInterval: QueryProgressUpdateTime target: self selector: @selector(checkOnQuery:) userInfo: nil repeats: YES]; } - (void) checkOnQuery: (NSTimer *) theTimer { if ( askUserToWait_ ) { askUserToWait_ ++ ; if ( askUserToWait_ > 10 ) // display error message for about 3 seconds askUserToWait_ = 0; [self displayErrorMessage: @"Please wait until search finished." with: nil]; } if ( ! [query_ isGathering] ) { [query_ disableUpdates]; [queryProgressTimer_ invalidate]; [self printMessage: @"finished searching; starting collecting." asError: NO]; collect_index_ = 0; [imagefiles_ release]; imagefiles_ = [[NSMutableArray arrayWithCapacity: 1000] retain]; // start new timer to repeatedly call -collectQueryResults: queryProgressTimer_ = [NSTimer scheduledTimerWithTimeInterval: QueryProgressUpdateTime target: self selector: @selector(collectQueryResults:) userInfo: nil repeats: YES]; } else { searchProgress_ ++ ; if ( ! askUserToWait_ ) { NSMutableString * mesg = [[NSMutableString alloc] initWithString: @"searching "]; int i; for (i = 0; i < searchProgress_ / 10; i ++ ) [mesg appendString: @"."]; [self displayMessage: mesg]; [mesg release]; } } } - (void) collectQueryResults: (NSTimer *) theTimer { if ( askUserToWait_ ) { askUserToWait_ ++ ; if ( askUserToWait_ > 10 ) // display error message for about 3 seconds askUserToWait_ = 0; [self displayErrorMessage: @"Please wait until search finished." with: nil]; return; } // strip common path prefix, and filter results by skip patterns // (all other exclude options went into the spotlight search query string) unsigned int prefixlen = [directoryLocation_ length] + 1; NSDate * start = [NSDate date]; unsigned int i; for ( i = collect_index_; i < [query_ resultCount]; i ++ ) { NSString * path = [[query_ resultAtIndex: i] valueForAttribute: @"kMDItemPath"]; if ( ! [self matchesPattern: path] ) { [ imagefiles_ addObject: [[path substringFromIndex: prefixlen] retain] ]; } // quit for now, if time is up (resume with next call from timer) if ( [start timeIntervalSinceNow] < - (QueryProgressUpdateTime-0.07) ) // maybe we could subtract 0.05 or less, so we can spend more time collecting { collect_index_ = i + 1; if ( ! askUserToWait_ ) [self displayMessage: [NSString stringWithFormat: @"collecting (%u) ...", collect_index_]]; return; } } // clean up [queryProgressTimer_ invalidate]; [query_ stopQuery]; [query_ release]; query_ = nil; [self logMessage: [NSString stringWithFormat: @"num images = %u", [imagefiles_ count]] asError: NO]; //[self listImages]; // for debugging [self changeDir: directoryLocation_]; [self saveImageList]; // must do it here, not in closeSheet! queryIsInProgress_ = NO; } // this is just for debugging purposes - (void) queryNote: (NSNotification *) note { // The NSMetadataQuery will send back a note when updates are happening. By looking at the [note name], we can tell what is happening if ( [[note name] isEqualToString: NSMetadataQueryDidStartGatheringNotification] ) { // The query has just started! NSLog(@"Started gathering."); } else if ( [[note name] isEqualToString: NSMetadataQueryDidFinishGatheringNotification] ) { // At this point, the query will be done. You may recieve an update later on. NSLog(@"Finished gathering."); } else if ( [[note name] isEqualToString: NSMetadataQueryGatheringProgressNotification] ) { // The query is still gathering results... NSLog(@"Progressing..."); } else if ( [[note name] isEqualToString: NSMetadataQueryDidUpdateNotification] ) { // An update will happen when Spotlight notices that a file was added, removed, or modified that affected the search results. // this shouldn't happen since we did disableUpdates NSLog(@"An update happened."); } } - (bool) changeDir: (NSString*) dir { [self logMessage: [NSString stringWithFormat: @"changing to image dir %@.", dir] asError: NO]; bool success = [ fmanager_ changeCurrentDirectoryPath: dir ]; if ( success ) return YES; [self printMessage: @"failed to chdir to " with: dir asError: YES]; [imagefiles_ release]; // clear image array, because their paths are relative imagefiles_ = [[NSMutableArray arrayWithCapacity: 0] retain]; // to dir, so they are now meaningless return NO; } // this is just for debugging purposes - (void) listImages { unsigned int i = 0, n = [imagefiles_ count]; n = n > 900 ? 900 : n ; for ( i = 0; i < n; i ++ ) [self logMessage: [NSString stringWithFormat: @"%u. file = %@", i, [imagefiles_ objectAtIndex: i]] asError: NO]; } - (void) scanDirectoryByHand: (NSString*) dir { if ( ! [self changeDir: dir] ) return; NSDirectoryEnumerator * enumerator = [fmanager_ enumeratorAtPath: directoryLocation_]; NSString *file; [imagefiles_ release]; imagefiles_ = [[NSMutableArray arrayWithCapacity: 1000] retain]; NSAutoreleasePool * pool; unsigned int n = 0; while ( file = [enumerator nextObject] ) { pool = [[NSAutoreleasePool alloc] init]; // must be regular file NSDictionary *fattrs = [enumerator fileAttributes]; if ( ! fattrs ) { [self logMessage: [NSString stringWithFormat: @"Path %@ is incorrect!", file] asError: YES]; [pool release]; return; } if ( [fattrs objectForKey: NSFileType] != NSFileTypeRegular ) { [pool release]; continue; } // check whether it's an image NSString * suffix = [file pathExtension]; if ( [suffix caseInsensitiveCompare: @"jpg"] != NSOrderedSame && [suffix caseInsensitiveCompare: @"gif"] != NSOrderedSame && [suffix caseInsensitiveCompare: @"tif"] != NSOrderedSame && [suffix caseInsensitiveCompare: @"png"] != NSOrderedSame ) { [pool release]; continue; } // check file size NSNumber *fsize; fsize = [fattrs objectForKey: NSFileSize]; if ( ! fsize ) { [self logMessage: [NSString stringWithFormat: @" Failed to obtain size of file %@!", file] asError: YES]; [pool release]; continue; } unsigned long long fsize_i = [fsize unsignedLongLongValue]; if ( fsize_i <= excludekB_ * 1024 ) { //NSLog( @"skipping file %@ size = %qu.\n", file, fsize_i ); [pool release]; continue; } // check whether file name matches any of the skip patterns if ( [self matchesPattern: file] ) { [pool release]; continue; } #if 0 // we don't check image size in this function any more (takes too long); this is done only // if Spotlight usage is switched on, which should be used anyway. NSData * tData = [[NSData alloc] initWithContentsOfMappedFile: file ]; if ( tData == nil ) { [self logMessage: [NSString stringWithFormat: @"Failed to read data from %@!", file] asError: YES]; [pool release]; continue; } NSBitmapImageRep * image = [[NSBitmapImageRep alloc] initWithData: tData]; if ( image == nil ) { [self logMessage: [NSString stringWithFormat: @"Failed to interpret image data from %@!", file] asError: YES]; [tData release]; [pool release]; continue; } unsigned int image_width = [image pixelsWide]; unsigned int image_height = [image pixelsHigh]; [image release]; [tData release]; if ( image_width < excludeSize_ && image_height < excludeSize_ ) { //NSLog( @"Image size %u x %u too small.\n", image_width, image_height ); [pool release]; continue; } #endif [ imagefiles_ addObject: file ]; // progress message; type 'syslog -w -k Sender ArtSaver' in Terminal to watch this n ++ ; if ( n >= 1000 ) { [self logMessage: [NSString stringWithFormat: @"num images so far = %u ...\n", [imagefiles_ count]] asError: NO]; n = 0; } [pool release]; } [self logMessage: [NSString stringWithFormat: @"finished directory scan by hand. Num images = %u", [imagefiles_ count]] asError: NO]; // [self listImages]; [self saveImageList]; } // check whether file name matches any of the skip patterns - (bool) matchesPattern: (NSString *) file { unsigned int npatterns = [skipPatterns_ count]; unsigned int i; for ( i = 0; i < npatterns; i ++ ) { const char * pattern = [[skipPatterns_ objectAtIndex: i] cStringUsingEncoding: NSASCIIStringEncoding]; // we assume that the patterns are plain ascii if ( ! pattern ) return FALSE; // convert file name to c string, making sure it is plain ascii NSData * fn_data = [file dataUsingEncoding: NSASCIIStringEncoding allowLossyConversion: YES]; char fn_buf[1000]; unsigned int len = [fn_data length]; len = (len < 999) ? len : 999; [fn_data getBytes: fn_buf length: len]; fn_buf[len] = 0; if ( fnmatch( pattern, fn_buf, 0 /*FNM_LEADING_DIR*/ ) == 0 ) { // NSLog(@"match %s - %@", pattern, file ); return TRUE; } } return FALSE; } #pragma mark - #pragma mark Configuration: // This method gets asked every time the user presses the Options button, and once at the very beginning. // (This is not documented, but I have tested, as of 10.6; so this is actually an assumption about future OS versions!) // If this returns FALSE, then the Options button in System Preferences is greyed out. // However(!), once that button is greyed out, there is no way to undo that again, because this function will never get asked again! ;-( - (BOOL) hasConfigureSheet { return YES; } // Same assumption as above - (NSWindow*) configureSheet { if ( queryIsInProgress_ ) { askUserToWait_ = 1; // initiate display of err mesg return nil; } if ( configureSheet_ ) return configureSheet_; [NSBundle loadNibNamed: @"ConfigureSheet" owner: self]; if ( directoryLocation_ == nil ) { // can not happen directoryLocation_ = [[NSMutableString alloc] initWithString: @"---" ]; [self logMessage: @"BUG: configureSheet: directoryLocation_ = nil!" asError: YES]; } [directoryTextView_ setStringValue: directoryLocation_]; [durationPerImageView_ setFloatValue: durationPerImage_]; [durationPerCycleView_ setFloatValue: durationPerCycle_]; [initZoomInFactorView_ setFloatValue: initZoomInFactor_]; [excludeSizeView_ setIntValue: excludeSize_]; [excludekBView_ setIntValue: excludekB_]; [showImagePathView_ setState: showImagePath_]; [useSpotlightView_ setState: useSpotlight_]; [displayBasenameView_ setState: displayBasename_]; [fontSizeView_ setIntValue: fontSize_]; [zoomIsOnView_ setState: zoomIsOn_]; [dontRandomizeView_ setState: ! dontRandomize_]; [allowBlackBordersView_ setState: ! allowBlackBorders_]; [blackBordersThresholdSlider_ setFloatValue: blackBorderAvoidance_ ]; NSString * patterns = [[NSString alloc] initWithString: [skipPatterns_ componentsJoinedByString: @"\n"]]; [[[skipPatternsView_ textStorage] mutableString] setString: patterns]; [patterns release]; [self enableZoomParameters: zoomIsOn_]; [self setAlbumDirectoryCheckBox: useAlbum_]; [self enableExcudeSizeOption: useSpotlight_]; // display version number in the config sheet NSString * version = [@"Version " stringByAppendingString: exec_version_]; [version_label_ setStringValue: version]; // Load help text into text view NSString * path = [[NSBundle bundleForClass:[self class]] pathForResource:@"Help_ReadMe_Changelog" ofType:@"rtf"]; if ( ! path ) [self logMessage: @"locating Help_ReadMe_Changelog.rtf failed" asError: YES]; else [helpView_ readRTFDFromFile: path]; return configureSheet_; } - (void) saveConfig { [defaults_ setObject: directoryLocation_ forKey: @"ImageDirectory"]; [defaults_ setFloat: durationPerImage_ forKey: @"DurationPerImage"]; [defaults_ setFloat: durationPerCycle_ forKey: @"DurationPerCycle"]; [defaults_ setFloat: initZoomInFactor_ forKey: @"InitZoomInFactor"]; [defaults_ setInteger: excludeSize_ forKey: @"ExcludeSize"]; [defaults_ setInteger: excludekB_ forKey: @"ExcludekB"]; [defaults_ setBool: showImagePath_ forKey: @"ShowImagePath"]; [defaults_ setBool: useSpotlight_ forKey: @"UseSpotlight"]; [defaults_ setBool: displayBasename_ forKey: @"DisplayBasename"]; [defaults_ setBool: zoomIsOn_ forKey: @"ZoomIsOn"]; [defaults_ setInteger: fontSize_ forKey: @"FontSize"]; [defaults_ setBool: useAlbum_ forKey: @"UseAlbum"]; [defaults_ setBool: dontRandomize_ forKey: @"DontRandomize"]; [defaults_ setBool: allowBlackBorders_ forKey: @"AllowBlackBorders"]; [defaults_ setFloat: blackBorderAvoidance_ forKey: @"BlackBorderAvoidance"]; [defaults_ setInteger: exec_version_num_ forKey: @"VersionNumber"]; // not plist_version_number_ ! plist_version_num_ = exec_version_num_; [defaults_ setObject: [skipPatterns_ componentsJoinedByString:@"\n"] forKey: @"SkipPatterns"]; [defaults_ synchronize]; } - (void) saveImageList { [defaults_ setObject: [imagefiles_ componentsJoinedByString:@"\n"] forKey: @"ImageList"]; [defaults_ synchronize]; } - (IBAction) closeSheet: (id) sender { // read data from text fields ( in order to retrieve data the user has entered without moving focus in the config sheet) // bools don't exhibit this problem [self setDurationPerImage: nil]; [self setDuractionPerCycle: nil]; [self setInitZoomInfactor: nil]; [self setExcludeSize: nil]; [self setExcludekB: nil]; [self setSkipPatterns: nil]; [self setFontSize: nil]; if ( useAlbum_ && selectedAlbum_ < 0 ) { useAlbum_ = NO; rescan_ = NO; } if ( rescan_ == YES ) { if ( useAlbum_ ) { directoryLocation_ = [self scanIPhotoPaths: selectedAlbum_]; [directoryTextView_ setStringValue: directoryLocation_]; } else { // use directory tree [self scanDirectory: directoryLocation_]; } rescan_ = NO; } [NSApp endSheet: configureSheet_]; [self saveConfig]; } - (IBAction) rescanDirectory: (id) sender { rescan_ = YES; [self closeSheet: sender]; } - (IBAction) selectDirectory: (id) sender { NSOpenPanel *oPanel = [NSOpenPanel openPanel]; int result; [oPanel setAllowsMultipleSelection: NO]; [oPanel setCanChooseDirectories: YES]; [oPanel setCanChooseFiles: NO]; result = [oPanel runModalForDirectory: directoryLocation_ file: nil types: nil]; if (result != NSOKButton) return; NSString *aFile = [[oPanel filenames] objectAtIndex: 0]; if ( [aFile isEqualToString: directoryLocation_] == TRUE ) return; [directoryLocation_ release]; directoryLocation_ = [[NSString alloc] initWithString: aFile]; [directoryTextView_ setStringValue: directoryLocation_]; rescan_ = YES; } - (IBAction) setDurationPerImage: (id) sender { float tDuration; tDuration = [durationPerImageView_ floatValue]; if ( tDuration >= 0.1f ) { durationPerImage_ = tDuration; [self startNewCycle]; } [durationPerImageView_ setFloatValue: durationPerImage_]; } // Helper: set lastFrameTime_ so that new cycle starts with next invokation of animateOneFrame - (void) startNewCycle { [lastFrameTime_ release]; lastFrameTime_ = [[NSDate dateWithTimeIntervalSinceNow: -durationPerImage_] retain]; } - (IBAction) setDuractionPerCycle: (id) sender { float tzoom; tzoom = [durationPerCycleView_ floatValue]; if ( tzoom > 0.1 ) { durationPerCycle_ = tzoom; [self startNewCycle]; } [durationPerCycleView_ setFloatValue: durationPerCycle_]; } - (IBAction) setInitZoomInfactor: (id) sender { float tfactor; tfactor = [initZoomInFactorView_ floatValue]; if ( tfactor > 1.0 ) { initZoomInFactor_ = tfactor; [self startNewCycle]; } else initZoomInFactor_ = 1.1; [initZoomInFactorView_ setFloatValue: initZoomInFactor_]; } - (IBAction) setExcludeSize: (id) sender { unsigned int tsize; tsize = [excludeSizeView_ intValue]; if ( tsize != excludeSize_ ) rescan_ = YES; if ( tsize >= 1 ) excludeSize_ = tsize; [excludeSizeView_ setIntValue: excludeSize_]; } - (IBAction) setExcludekB: (id) sender { unsigned int tkb = [excludekBView_ intValue]; if ( tkb != excludekB_ ) rescan_ = YES; excludekB_ = tkb; [excludekBView_ setIntValue: excludekB_]; } - (void) setSkipPatterns: (id) sender { NSString * oldpatterns = [[skipPatterns_ componentsJoinedByString: @"\n"] retain]; NSString * tpatterns = [[NSString alloc] initWithString: [[skipPatternsView_ textStorage] string] ]; if ( [tpatterns isEqualToString: oldpatterns] == NO ) rescan_ = YES; [skipPatterns_ release]; skipPatterns_ = [[tpatterns componentsSeparatedByString: @"\n"] retain]; [oldpatterns release]; [tpatterns release]; //NSLog(@"patterns = %@", skipPatterns_ ); } - (IBAction) setShowImagePath: (id) sender { showImagePath_ = [showImagePathView_ state]; if (showImagePath_ ) displayBasename_ = FALSE; [self setPathButtonsState]; } - (IBAction) setDisplayBasename: (id) sender { displayBasename_ = [displayBasenameView_ state]; if ( displayBasename_ ) showImagePath_ = FALSE; [self setPathButtonsState]; } - (void) setPathButtonsState { [displayBasenameView_ setState: displayBasename_]; [showImagePathView_ setState: showImagePath_]; bool state = displayBasename_ || showImagePath_; NSColor * bg; if ( state ) bg = [NSColor whiteColor]; else bg = [NSColor lightGrayColor]; [fontSizeView_ setEditable: state]; [fontSizeView_ setBackgroundColor: bg]; if ( filename_ ) [self printFilename: filename_]; } - (IBAction) setFontSize: (id) sender { fontSize_ = [fontSizeView_ intValue]; if ( fontSize_ < 10 ) fontSize_ = 10; if ( fontSize_ > 72 ) fontSize_ = 72; [fontSizeView_ setIntValue: fontSize_]; [textLayer_ setNeedsDisplay]; } - (IBAction) setUseSpotlight: (id) sender { useSpotlight_ = [useSpotlightView_ state]; rescan_ = YES; [self enableExcudeSizeOption: useSpotlight_]; } - (void) enableExcudeSizeOption: (bool) state { // enable/disable image size exclusion option NSColor * bg; if ( state ) bg = [NSColor whiteColor]; else bg = [NSColor lightGrayColor]; [excludeSizeView_ setEditable: state]; [excludeSizeView_ setBackgroundColor: bg]; } - (IBAction) setZoomIsOn: (id) sender; { zoomIsOn_ = [zoomIsOnView_ state]; [self enableZoomParameters: zoomIsOn_]; [self startNewCycle]; } - (void) enableZoomParameters: (bool) state { NSColor * bg; if ( state ) bg = [NSColor whiteColor]; else bg = [NSColor lightGrayColor]; [durationPerCycleView_ setEditable: state]; [durationPerCycleView_ setBackgroundColor: bg]; [initZoomInFactorView_ setEditable: state]; [initZoomInFactorView_ setBackgroundColor: bg]; } - (IBAction) dontRandomize: (id) sender { dontRandomize_ = ! [dontRandomizeView_ state]; } - (IBAction) allowBlackBorders: (id) sender { allowBlackBorders_ = ! [allowBlackBordersView_ state]; } // Warning: whenever you change the formula here, you *must* also change it in blackBorderThresholdToSliderValue! - (IBAction) newBlackBordersThreshold: (NSSlider*) slider { blackBorderAvoidance_ = [slider floatValue]; } //- (float) blackBorderThresholdToSliderValue: (float) threshold_offset //{ // float t = (threshold_offset + 1.0f) / 2.5; // return t*t * 100.0f; //} /* Erstellung des Donate buttons: * unter Paypal -> Profil -> Verkäufereinstellungen -> Meine gespeicherten Buttons -> Neuen Button erstellen * erstellt man einen neuen Button. Der wird dann im Paypal-Account gespeichert und mit einer ID versehen. * Dort kann man auch weitere Optionen fuer diesen Button einstellen. * In dem Dokument "Website Payments Standard Integration Guide" bei Paypal findet man ab Seite 329 weitere Variablen, * die man bei dem gespeicherten Button optional eintragen kann. */ - (IBAction) donatePaypal: (id) sender { NSURL * url = [NSURL URLWithString: @"https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=8719163"]; if ( ! url ) [self logMessage: @"creating paypal URL failed" asError: YES]; (void) [[NSWorkspace sharedWorkspace] openURL: url]; } - (IBAction) feedback: (id) sender { NSURL * url = [NSURL URLWithString: @"mailto:snoopy.67.z@googlemail.com?subject=ArtSaver%20Feedback"]; if ( ! url ) [self logMessage: @"creating mailto URL failed" asError: YES]; (void) [[NSWorkspace sharedWorkspace] openURL: url]; } // Display help sheet, which should be filled with text by awakeFromNib - (IBAction) showHelpSheet: (id) sender { [helpSheet_ orderFront: self]; // [NSApp beginSheet: helpSheet_ // modalForWindow: [self window] // modalDelegate: self // didEndSelector: NULL // contextInfo: nil ]; // [NSApp runModalForWindow: helpSheet_]; // [NSApp endSheet: helpSheet_]; // [helpSheet_ orderOut: nil]; } #pragma mark - #pragma mark iPhoto handling: - (IBAction) setUseDirectoryTree: (id) sender { useAlbum_ = FALSE; rescan_ = TRUE; [self setAlbumDirectoryCheckBox: useAlbum_]; } - (IBAction) setUseAlbum: (id) sender { useAlbum_ = TRUE; selectedAlbum_ = -1; if ( ! iPhoto_ ) iPhoto_ = [[SBApplication applicationWithBundleIdentifier:@"com.apple.iPhoto"] retain]; if ( ! iPhoto_ ) { [self logMessage: @"Scripting bridge to iPhoto failed!" asError: YES]; useAlbum_ = FALSE; [self setAlbumDirectoryCheckBox: useAlbum_]; return; } [self setAlbumDirectoryCheckBox: useAlbum_]; // [iPhoto activate]; // activate would bring iPhoto to the foreground // do the following stuff here and only once, because we need them later-on several times if ( albums_ ) [albums_ release]; albums_ = [[[iPhoto_ albums] get] retain]; [self logMessage: [NSString stringWithFormat: @"num albums = %d", [albums_ count]] asError: NO]; [albumNames_ release]; albumNames_ = [[albums_ arrayByApplyingSelector: @selector(name) ] retain]; //NSLog( @"album names = %@", albumNames_ ); [albumView_ reloadData]; // entails invokation of numberOfRowsInTableView and tableView:objectValueForTableColumn:row: } - (void) setAlbumDirectoryCheckBox: (bool) albumIsOn { [useAlbumView_ setState: albumIsOn]; if ( ! albumIsOn ) { if ( albums_ ) [albums_ release]; albums_ = nil; [albumNames_ release]; albumNames_ = [[NSArray alloc] init]; [albumView_ reloadData]; } [useDirectoryTreeView_ setState: ! albumIsOn]; [useSpotlightView_ setEnabled: ! albumIsOn]; [changeDirectoryView_ setEnabled: ! albumIsOn]; [rescanDirectoryView_ setEnabled: ! albumIsOn]; // enable/disable exclude options NSColor * bg; if ( ! albumIsOn ) bg = [NSColor whiteColor]; else bg = [NSColor lightGrayColor]; [excludeSizeView_ setEditable: ! albumIsOn]; [excludeSizeView_ setBackgroundColor: bg]; [excludekBView_ setEditable: ! albumIsOn]; [excludekBView_ setBackgroundColor: bg]; [skipPatternsView_ setEditable: ! albumIsOn]; [skipPatternsView_ setBackgroundColor: bg]; } - (IBAction) selectAlbum: (id) sender { int row = [sender selectedRow]; if ( row != selectedAlbum_ ) { selectedAlbum_ = [sender selectedRow]; rescan_ = YES; } } - (int) numberOfRowsInTableView: (NSTableView *) tableView { if ( iPhoto_ ) return [albumNames_ count]; return 0; } - (id) tableView: (NSTableView *) tableView objectValueForTableColumn: (NSTableColumn *) tableColumn row: (int) row { if ( iPhoto_ ) { if ( row >= [albumNames_ count] ) { // can't happen [self logMessage: [NSString stringWithFormat: @"BUG: tableView: row = %d, albumNames_ count = %d!", row, [albumNames_ count]] asError: YES]; return @""; } return [albumNames_ objectAtIndex: row]; } else // shouldn't happen return @""; } - (NSString *) scanIPhotoPaths: (int) album_nr { iPhotoAlbum * a = [albums_ objectAtIndex: album_nr]; [self logMessage: [NSString stringWithFormat: @"album = %@.", [a name]] asError: NO]; SBElementArray * photos = [a photos]; NSArray * photoPaths = [photos arrayByApplyingSelector: @selector(imagePath) ]; // find common prefix for all image paths NSEnumerator * photoEnum = [photoPaths objectEnumerator]; NSString * p0 = [photoEnum nextObject]; unsigned int prefix = [p0 length]; NSString * s; while ( s = [photoEnum nextObject] ) { for (unsigned int c = 0; c < prefix; c ++ ) { if ( [s characterAtIndex: c] != [p0 characterAtIndex: c] ) { prefix = c; break; } } } // Now go back to nearest '/', and remove a trailing '/' NSString * pref = [p0 substringToIndex: prefix]; if ( [p0 characterAtIndex: [pref length] - 1] == '/' ) pref = [pref substringToIndex: [pref length] - 1]; else pref = [pref stringByDeletingLastPathComponent]; [pref retain]; [self logMessage: [NSString stringWithFormat: @"Album images path prefix = %@.", pref] asError: NO]; // strip common prefix from all (absolute) paths [imagefiles_ release]; imagefiles_ = [[NSMutableArray arrayWithCapacity: 1000] retain]; unsigned int prefixlen = [pref length] + 1; // +1 because we also want to remove the '/' after the prefix photoEnum = [photoPaths objectEnumerator]; while ( s = [photoEnum nextObject] ) [ imagefiles_ addObject: [[s substringFromIndex: prefixlen] retain] ]; // applying the skip patterns does not make sense with photos [self logMessage: [NSString stringWithFormat: @"Finished album scan. Num images = %u", [imagefiles_ count]] asError: NO]; //[self listImages]; [self changeDir: pref]; [self saveImageList]; // we do it here, because scanDirectorySpotlight must do it itself, too return pref; } #pragma mark - #pragma mark User interaction: - (void) keyDown: (NSEvent *) theEvent { if ( askUserToQuit_ ) [super keyDown: theEvent]; // quit because there are no images being displayed // Copied from "Cocoa Event-Handling Guide", chapter "Handling Key Events" if ( [theEvent modifierFlags] & NSNumericPadKeyMask ) { [self interpretKeyEvents: [NSArray arrayWithObject:theEvent] ]; [self startNewCycle]; } else if ( [[theEvent characters] isEqual: @" "] ) { // start/stop animation paused_ = ! paused_; [textLayer_ setNeedsDisplay]; // show/remove "(paused)" at bottom of screen } else if ( [[theEvent characters] isEqual: @"\r"] && filename_ ) { // show current image in Finder NSMutableString * path = [NSMutableString stringWithString: directoryLocation_]; [path appendString: @"/"]; [path appendString: filename_]; NSWorkspace * ws = [NSWorkspace sharedWorkspace]; [ws selectFile: path inFileViewerRootedAtPath: nil]; [super keyDown: theEvent]; // will quit the screen saver, if not in preview mode (System Preferences), } // or bring up the password dialogue, if the user has configured the preferences that way else { [super keyDown: theEvent]; } } - (IBAction) moveUp: (id) sender { // NOP } - (IBAction) moveDown: (id) sender { // NOP } - (IBAction) moveLeft: (id) sender { displayPreviousImage_ = YES; previousImageIndex_ ++ ; if ( previousImageIndex_ >= history_size ) previousImageIndex_ = history_size-1; } - (IBAction) moveRight: (id) sender { if ( previousImageIndex_ == 0 ) // user has not stepped back through previous images return; displayPreviousImage_ = YES; previousImageIndex_ -- ; } @end