// // IKImageBrowserView.m // ImageKit // // Created by H. Nikolaus Schaller on 16.11.07. // Copyright 2007 Golden Delicious Computers GmbH&Co. KG. All rights reserved. // #import "IKImageBrowserView.h" @implementation IKImageBrowserView - (id) initWithFrame:(NSRect) frame { if((self = [super initWithFrame:frame])) { // Initialization code here. } return self; } - (void) dealloc; { [_selectionIndexes release]; [super dealloc]; } - (void) drawRect:(NSRect) rect { // Drawing code here. } - (BOOL) allowsEmptySelection; { return _allowsEmptySelection; } - (BOOL) allowsMultipleSelection; { return _allowsMultipleSelection; } - (BOOL) allowsReordering; { return _allowsReordering; } - (BOOL) animates; { return _animates; } - (NSSize) cellSize; { return _cellSize; } - (NSUInteger) cellsStyleMask; { return _cellsStyleMask; } - (void) collapseGroupAtIndex:(NSUInteger) index; { } - (BOOL) constrainsToOriginalSize; { return _constrainsToOriginalSize; } - (NSUInteger) contentResizingMask; { return _contentResizingMask; } - (id) dataSource; { return _dataSource; } - (id) delegate; { return _delegate; } - (id) draggingDestinationDelegate; { return _draggingDestinationDelegate; } - (void) expandGroupAtIndex:(NSUInteger) index; { } - (NSUInteger) indexAtLocationOfDroppedItem; { return 0; } - (NSInteger) indexOfItemAtPoint:(NSPoint) point; { return 0; } - (BOOL) isGroupExpandedAtIndex:(NSUInteger) index; { } - (NSRect) itemFrameAtIndex:(NSInteger) index; { } - (void) reloadData; { } - (void) scrollIndexToVisible:(NSInteger) index; { } - (NSIndexSet *) selectionIndexes; { return _selectionIndexes; } - (void) setAllowsEmptySelection: (BOOL) flag; { _allowsEmptySelection=flag; } - (void) setAllowsMultipleSelection: (BOOL) flag; { _allowsMultipleSelection=flag; } - (void) setAllowsReordering: (BOOL) flag; { _allowsReordering=flag; } - (void) setAnimates: (BOOL) flag; { _animates=flag; } - (void) setCellSize:(NSSize) size; { _cellSize=size; /* redraw? */ } - (void) setCellsStyleMask:(NSUInteger) mask; { _cellsStyleMask=mask; } - (void) setConstrainsToOriginalSize: (BOOL) flag; { _constrainsToOriginalSize=flag; } - (void) setContentResizingMask:(NSUInteger) mask; { _contentResizingMask=mask; } - (void) setDataSource:(id) source; { _dataSource=source; } - (void) setDelegate:(id) delegate; { _delegate=delegate; } - (void) setDraggingDestinationDelegate:(id) delegate; { _draggingDestinationDelegate=delegate; } - (void) setSelectionIndexes:(NSIndexSet *) indexes byExtendingSelection:(BOOL) extend; { } - (void) setZoomValue:(float) zoom; { if(_zoomValue != zoom) { _zoomValue=zoom; // redraw } } - (float) zoomValue; { return _zoomValue; } - (void) encodeWithCoder:(NSCoder *) coder; { } - (id) initWithCoder:(NSCoder *) coder; { return self; } - (void) mouseDown:(NSEvent *) event; { // tracking, selection, D&D } @end #if MATERIAL // ---------------------------------------------------------------------------- // MUPhotoView // // Copyright (c) 2006 Blake Seely // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated // documentation files (the "Software"), to deal in the Software without restriction, including without limitation // the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, // and to permit persons to whom the Software is furnished to do so, subject to the following conditions: // * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. // * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF // OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // * You include a link to http://www.blakeseely.com in your final product. // ---------------------------------------------------------------------------- #import "MUPhotoView.h" @implementation MUPhotoView #pragma mark - // Initializers and Dealloc #pragma mark Initializers and Dealloc + (void)initialize { [self exposeBinding:@"photosArray"]; [self exposeBinding:@"selectedPhotoIndexes"]; [self exposeBinding:@"backgroundColor"]; [self exposeBinding:@"photoSize"]; [self exposeBinding:@"useShadowBorder"]; [self exposeBinding:@"useOutlineBorder"]; [self exposeBinding:@"useShadowSelection"]; [self exposeBinding:@"useOutlineSelection"]; [self setKeys:[NSArray arrayWithObject:@"backgroundColor"] triggerChangeNotificationsForDependentKey:@"shadowBoxColor"]; } - (id)initWithFrame:(NSRect)frameRect { if ((self = [super initWithFrame:frameRect]) != nil) { delegate = nil; sendsLiveSelectionUpdates = YES; useHighQualityResize = YES; photosArray = nil; photosFastArray = nil; currentSelection = [[NSMutableIndexSet alloc] init]; [self setBackgroundColor:[NSColor grayColor]]; useShadowBorder = YES; useOutlineBorder = NO; borderShadow = [[NSShadow alloc] init]; [borderShadow setShadowOffset:NSMakeSize(2.0,-3.0)]; [borderShadow setShadowBlurRadius:5.0]; noShadow = [[NSShadow alloc] init]; [noShadow setShadowOffset:NSMakeSize(0,0)]; [noShadow setShadowBlurRadius:0.0]; [self setBorderOutlineColor:[NSColor colorWithCalibratedWhite:0.5 alpha:1.0]]; useShadowSelection = NO; useBorderSelection = YES; //[self setSelectionBorderColor:[NSColor selectedControlColor]]; [self setSelectionBorderColor:[[NSColor colorWithDeviceRed:0.0/255.0 green:204.0/255.0 blue:0.0/255.0 alpha:1.0] retain]]; selectionBorderWidth = 3.0; [self setShadowBoxColor:[NSColor colorWithCalibratedWhite:0.0 alpha:0.5]]; //spotlightColor = [[NSColor colorWithCalibratedRed:250/255.0 green:254/255.0 blue:224/255.0 alpha:1.0] retain]; spotlightColor = [NSColor lightGrayColor]; #if 0 // dim spotlight spotlightBackground=[[NSColor colorWithCalibratedWhite:0.2 alpha:1.0] retain]; #else spotlightBackground = nil; #endif useSpotlight = YES; photoSize = 100.0; photoVerticalSpacing = 25.0; photoHorizontalSpacing = 25.0; photoResizeTimer = nil; photoResizeTime = [[NSDate date] retain]; isDonePhotoResizing = YES; nameBeingEdited = NSNotFound; dropDestinationIndex = NSNotFound; #if 0 NSLog(@"MUPhotoView %p initWithFrame: %@", self, NSStringFromRect(frameRect)); #endif } return self; } - (void) awakeFromNib; { // enclosing scroll view is now initialized [self updateGridAndFrame]; } - (void)dealloc { [self setBorderOutlineColor:nil]; [self setSelectionBorderColor:nil]; [self setShadowBoxColor:nil]; [self setBackgroundColor:nil]; [self setPhotosArray:nil]; [currentSelection release]; // [self setSelectedPhotoIndexes:nil]; [photoResizeTime release]; [dragSelectedPhotoIndexes release]; dragSelectedPhotoIndexes = nil; [super dealloc]; } #pragma mark - // Drawing Methods #pragma mark Drawing Methods - (BOOL) isOpaque { return NO; // we don't know... } - (BOOL)isFlipped { return YES; } - (void) reloadData; { unsigned count = [delegate photoCountForPhotoView:self]; [(NSMutableIndexSet *) currentSelection removeIndexesInRange:NSMakeRange(count, NSNotFound-count-1)]; // remove everything beyond existing indices [self updateGridAndFrame]; // may have changed! [self setNeedsDisplayInRect:[self visibleRect]]; } - (void)drawRect:(NSRect)rect { // draw the background color if (useSpotlight && spotlightBackground) [spotlightBackground set]; else [[self backgroundColor] set]; [NSBezierPath fillRect:rect]; // fill background // get the number of photos unsigned photoCount = [self photoCount]; if (0 == photoCount) return; // any other setup if (useHighQualityResize) { [[NSGraphicsContext currentContext] setImageInterpolation:NSImageInterpolationHigh]; } /**** BEGIN Drawing Photos ****/ NSRange rangeToDraw = [self photoIndexRangeForRect:rect]; // adjusts for photoCount if the rect goes outside my range unsigned index; unsigned lastIndex = rangeToDraw.location + rangeToDraw.length; for (index = rangeToDraw.location; index <= lastIndex; index++) { // Get the image at the current index - a red square anywhere in the view means it asked for an image, but got nil for that index BOOL spotlighted=YES; NSImage *photo = nil; NSString *name; if ([self inLiveResize]) { photo = [self fastPhotoAtIndex:index]; } if (nil == photo) { photo = [self photoAtIndex:index]; } if (nil == photo) { photo = [[[NSImage alloc] initWithSize:NSMakeSize(photoSize,photoSize)] autorelease]; [photo lockFocus]; [[NSColor redColor] set]; [NSBezierPath fillRect:NSMakeRect(0,0,photoSize,photoSize)]; [photo unlockFocus]; } // set it to draw correctly in a flipped view (will restore it after drawing) BOOL isFlipped = [photo isFlipped]; [photo setFlipped:YES]; // scale it to the appropriate size, this method should automatically set high quality if necessary photo = [self scalePhoto:photo]; // get all the appropriate positioning information NSRect rawGridRect = [self gridRectForIndex:index]; NSRect gridRect = [self centerScanRect:rawGridRect]; // NSRect gridRect = rawGridRect; // NSSize scaledSize = [self scaledPhotoSizeForSize:[photo size]]; // NSRect rawPhotoRect = [self rectCenteredInRect:gridRect withSize:scaledSize]; // NSRect photoRect = [self centerScanRect:rawPhotoRect]; // NSRect photoRect = rawPhotoRect; NSRect photoRect = [self photoRectForIndex:index]; photoRect = [self centerScanRect:photoRect]; #if 0 if (index == 0) { NSLog(@"gridRect=%@", NSStringFromRect(rawGridRect)); NSLog(@"centered gridRect=%@", NSStringFromRect(gridRect)); NSLog(@"scaledSize=%@", NSStringFromSize(scaledSize)); NSLog(@"photoRect=%@", NSStringFromRect(rawPhotoRect)); NSLog(@"centered photoRect=%@", NSStringFromRect(photoRect)); } #endif //**** BEGIN Background Drawing - any drawing that technically goes under the image ****/ // kSelectionStyleShadowBox draws a semi-transparent rounded rect behind/around the image if ([currentSelection containsIndex:index] && [self useShadowSelection]) { NSBezierPath *shadowBoxPath = [self shadowBoxPathForRect:gridRect]; [shadowBoxColor set]; [shadowBoxPath fill]; } if (useSpotlight) { if (spotlightBackground) { [spotlightBackground set]; NSRectFill(gridRect); } if ((spotlighted=[delegate photoView:self spotlightPhotoAtIndex:index])) { // spotlight NSBezierPath *spotlightPath = [self shadowBoxPathForRect:gridRect]; [spotlightColor set]; [spotlightPath fill]; } } //**** END Background Drawing ****/ // kBorderStyleShadow - set the appropriate shadow if ([self useShadowBorder]) { [borderShadow set]; } // draw the current photo NSRect imageRect = NSMakeRect(0, 0, [photo size].width, [photo size].height); [photo drawInRect:photoRect fromRect:imageRect operation:(useOutlineBorder?NSCompositeCopy:NSCompositeSourceOver) fraction:spotlighted?1.0:1.0]; // restore the photo's flipped status [photo setFlipped:isFlipped]; // kBorderStyleShadow - remove the shadow after drawing the image [noShadow set]; // draw the photo name (if any) name = [delegate photoView:self photoNameAtIndex:index]; if (name) { // draw photo name centered NSRect nameRect = [self nameRectForIndex:index forName:name fullWidth:NO]; [name drawInRect:nameRect withAttributes:[self nameAttributes:!spotlightBackground || useSpotlight]]; } //**** BEGIN Foreground Drawing - includes outline borders, selection rectangles ****/ // drag destination overrides current selection if (index == dropDestinationIndex) { NSBezierPath *selectionBorder = [NSBezierPath bezierPathWithRect:NSInsetRect(photoRect,-3.0,-3.0)]; [selectionBorder setLineWidth:[self selectionBorderWidth]]; [[NSColor selectedControlColor] set]; [selectionBorder stroke]; } else if ([currentSelection containsIndex:index] && [self useBorderSelection]) { NSBezierPath *selectionBorder = [NSBezierPath bezierPathWithRect:NSInsetRect(photoRect,-3.0,-3.0)]; [selectionBorder setLineWidth:[self selectionBorderWidth]]; [[self selectionBorderColor] set]; [selectionBorder stroke]; } else if ([self useOutlineBorder]) { photoRect = NSInsetRect(photoRect,0.5,0.5); // line up the 1px border so it completely fills a single row of pixels NSBezierPath *outline = [NSBezierPath bezierPathWithRect:photoRect]; [outline setLineWidth:1.0]; [borderOutlineColor set]; [outline stroke]; } NSRectCorner corner; for(corner = NSBottomLeftCorner; corner <= NSBottomRightCorner; corner++) { NSImage *cornerTag=[delegate photoView:self cornerTagAtIndex:index corner:corner]; if (cornerTag) { NSPoint p; NSSize sz = [cornerTag size]; switch(corner) { case NSTopLeftCorner: p=photoRect.origin; break; case NSBottomLeftCorner: p=photoRect.origin; p.y+=photoRect.size.height; break; case NSBottomRightCorner: p=photoRect.origin; p.y+=photoRect.size.height; p.x+=photoRect.size.width; break; case NSTopRightCorner: p=photoRect.origin; p.x+=photoRect.size.width; break; } p.x-=sz.width/2.0; p.y+=sz.height/2.0; [cornerTag dissolveToPoint:p fraction:0.8]; } } //**** END Foreground Drawing ****// } //**** END Drawing Photos ****// //**** BEGIN Selection Rectangle ****// if (drawSelectionRectangle) { [noShadow set]; [[NSColor whiteColor] set]; float minX = (mouseDownPoint.x < mouseCurrentPoint.x) ? mouseDownPoint.x : mouseCurrentPoint.x; float minY = (mouseDownPoint.y < mouseCurrentPoint.y) ? mouseDownPoint.y : mouseCurrentPoint.y; float maxX = (mouseDownPoint.x > mouseCurrentPoint.x) ? mouseDownPoint.x : mouseCurrentPoint.x; float maxY = (mouseDownPoint.y > mouseCurrentPoint.y) ? mouseDownPoint.y : mouseCurrentPoint.y; NSRect selectionRectangle = NSMakeRect(minX,minY,maxX-minX,maxY-minY); [NSBezierPath strokeRect:selectionRectangle]; [[NSColor colorWithDeviceRed:0.8 green:0.8 blue:0.8 alpha:0.5] set]; [NSBezierPath fillRect:selectionRectangle]; } if (nameBeingEdited != NSNotFound) { // draw first responder box around field editor NSText *fe = [[self window] fieldEditor:NO forObject:self]; NSBezierPath *selectionBorder = [NSBezierPath bezierPathWithRect:NSInsetRect([fe frame], -3.0, -3.0)]; [selectionBorder setLineWidth:[self selectionBorderWidth]]; [[self selectionBorderColor] set]; [selectionBorder stroke]; } //**** END Selection Rectangle ****// } #pragma mark - // Delegate Accessors #pragma mark Delegate Accessors - (id)delegate { return delegate; } - (void)setDelegate:(id)del { [self willChangeValueForKey:@"delegate"]; delegate = del; [self didChangeValueForKey:@"delegate"]; } #pragma mark - // Photos Methods #pragma mark Photo Methods - (NSArray *)photosArray { //NSLog(@"in -photosArray, returned photosArray = %@", photosArray); return [[photosArray retain] autorelease]; } - (void)setPhotosArray:(NSArray *)aPhotosArray { //NSLog(@"in -setPhotosArray:, old value of photosArray: %@, changed to: %@", photosArray, aPhotosArray); if (photosArray != aPhotosArray) { [photosArray release]; [self willChangeValueForKey:@"photosArray"]; photosArray = [aPhotosArray mutableCopy]; [self didChangeValueForKey:@"photosArray"]; // update live resize array if (nil != photosFastArray) { [photosFastArray release]; } photosFastArray = [[NSMutableArray alloc] initWithCapacity:[aPhotosArray count]]; unsigned i; for (i = 0; i < [photosArray count]; i++) { [photosFastArray addObject:[NSNull null]]; } // update internal grid size, adjust height based on the new grid size [self scrollPoint:([self frame].origin)]; [self setNeedsDisplayInRect:[self visibleRect]]; } } #pragma mark - // Selection Management #pragma mark Selection Management - (void) scrollToPhotoAtIndex:(unsigned) idx; { if (idx != NSNotFound) [self scrollRectToVisible:[self gridRectForIndex:idx]]; } - (NSIndexSet *)currentSelection { return currentSelection; } - (void)changeSelection:(NSIndexSet *)indexes { // change selection [self setCurrentSelection:indexes]; // update selection [delegate photoView:self changeSelection:indexes]; // and notify delegate } - (void)setCurrentSelection:(NSIndexSet *)indexes { [self dirtyDisplayRectsForNewSelection:indexes oldSelection:currentSelection]; [currentSelection autorelease]; currentSelection = [indexes mutableCopy]; unsigned count = [delegate photoCountForPhotoView:self]; [(NSMutableIndexSet *) currentSelection removeIndexesInRange:NSMakeRange(count, NSNotFound-count-1)]; // remove everything beyond existing indices #if REMOVE NSMutableIndexSet *oldSelection = nil; // Set the new selection, but save the old selection so we know exactly what to redraw if (nil != [self selectedPhotoIndexes]) { oldSelection = [[self selectedPhotoIndexes] retain]; [self setSelectedPhotoIndexes:indexes]; } else if (nil != delegate) { // We have to iterate through the photos to figure out which ones the delegate thinks are selected - that's the only way to know the old selection when in delegate mode oldSelection = [[NSMutableIndexSet alloc] init]; int i, count = [self photoCount]; for( i = 0; i < count; i += 1 ) { if ([self isPhotoSelectedAtIndex:i]) { [oldSelection addIndex:i]; } } // Now update the selection indexes = [delegate photoView:self willsetCurrentSelection:indexes]; [self setSelectedPhotoIndexes:indexes]; [delegate photoView:self didsetCurrentSelection:indexes]; } [self dirtyDisplayRectsForNewSelection:indexes oldSelection:oldSelection]; [oldSelection release]; #endif } #pragma mark - // Selection Style #pragma mark Selection Style - (BOOL)allowsMultipleSelection; { return allowsMultipleSelection; } - (void)setAllowsMultipleSelection:(BOOL)flag; { allowsMultipleSelection = flag; } - (BOOL)useBorderSelection { //NSLog(@"in -useBorderSelection, returned useBorderSelection = %@", useBorderSelection ? @"YES": @"NO"); return useBorderSelection; } - (void)setUseBorderSelection:(BOOL)flag { //NSLog(@"in -setUseBorderSelection, old value of useBorderSelection: %@, changed to: %@", (useBorderSelection ? @"YES": @"NO"), (flag ? @"YES": @"NO")); [self willChangeValueForKey:@"useBorderSelection"]; useBorderSelection = flag; [self didChangeValueForKey:@"useBorderSelection"]; [self setNeedsDisplayInRect:[self visibleRect]]; } - (NSColor *)selectionBorderColor { //NSLog(@"in -selectionBorderColor, returned selectionBorderColor = %@", selectionBorderColor); return [[selectionBorderColor retain] autorelease]; } - (void)setSelectionBorderColor:(NSColor *)aSelectionBorderColor { //NSLog(@"in -setSelectionBorderColor:, old value of selectionBorderColor: %@, changed to: %@", selectionBorderColor, aSelectionBorderColor); if (selectionBorderColor != aSelectionBorderColor) { [selectionBorderColor release]; [self willChangeValueForKey:@"selectionBorderColor"]; selectionBorderColor = [aSelectionBorderColor copy]; [self didChangeValueForKey:@"selectionBorderColor"]; } } - (BOOL)useShadowSelection { //NSLog(@"in -useShadowSelection, returned useShadowSelection = %@", useShadowSelection ? @"YES": @"NO"); return useShadowSelection; } - (void)setUseShadowSelection:(BOOL)flag { //NSLog(@"in -setUseShadowSelection, old value of useShadowSelection: %@, changed to: %@", (useShadowSelection ? @"YES": @"NO"), (flag ? @"YES": @"NO")); [self willChangeValueForKey:@"useShadowSelection"]; useShadowSelection = flag; [self willChangeValueForKey:@"useShadowSelection"]; [self setNeedsDisplayInRect:[self visibleRect]]; } - (BOOL)usesSpotlight; { return useSpotlight; } - (void)setUseSpotlight:(BOOL)flag; { useSpotlight = flag; } #pragma mark - // Appearance #pragma mark Appearance - (BOOL)useShadowBorder { //NSLog(@"in -useShadowBorder, returned useShadowBorder = %@", useShadowBorder ? @"YES": @"NO"); return useShadowBorder; } - (void)setUseShadowBorder:(BOOL)flag { //NSLog(@"in -setUseShadowBorder, old value of useShadowBorder: %@, changed to: %@", (useShadowBorder ? @"YES": @"NO"), (flag ? @"YES": @"NO")); [self willChangeValueForKey:@"useShadowBorder"]; useShadowBorder = flag; [self didChangeValueForKey:@"useShadowBorder"]; [self setNeedsDisplayInRect:[self visibleRect]]; } - (BOOL)useOutlineBorder { //NSLog(@"in -useOutlineBorder, returned useOutlineBorder = %@", useOutlineBorder ? @"YES": @"NO"); return useOutlineBorder; } - (void)setUseOutlineBorder:(BOOL)flag { //NSLog(@"in -setUseOutlineBorder, old value of useOutlineBorder: %@, changed to: %@", (useOutlineBorder ? @"YES": @"NO"), (flag ? @"YES": @"NO")); [self willChangeValueForKey:@"useOutlineBorder"]; useOutlineBorder = flag; [self didChangeValueForKey:@"useOutlineBorder"]; [self setNeedsDisplayInRect:[self visibleRect]]; } - (NSColor *)backgroundColor { //NSLog(@"in -backgroundColor, returned backgroundColor = %@", backgroundColor); return [[backgroundColor retain] autorelease]; } - (void)setBackgroundColor:(NSColor *)aBackgroundColor { //NSLog(@"in -setBackgroundColor:, old value of backgroundColor: %@, changed to: %@", backgroundColor, aBackgroundColor); if (backgroundColor != aBackgroundColor) { [backgroundColor release]; [self willChangeValueForKey:@"backgroundColor"]; backgroundColor = [aBackgroundColor copy]; [self didChangeValueForKey:@"backgroundColor"]; // adjust the shadow box selection color based on the background color. values closer to white use black and vice versa NSColor *newShadowBoxColor; float whiteValue = 0.0; if ([backgroundColor numberOfComponents] >= 3) { float red, green, blue; [backgroundColor getRed:&red green:&green blue:&blue alpha:NULL]; whiteValue = (red + green + blue) / 3; } else if ([backgroundColor numberOfComponents] >= 1) { [backgroundColor getWhite:&whiteValue alpha:NULL]; } if (0.5 > whiteValue) newShadowBoxColor = [NSColor colorWithDeviceWhite:1.0 alpha:0.5]; else newShadowBoxColor = [NSColor colorWithDeviceWhite:0.0 alpha:0.5]; [self setShadowBoxColor:newShadowBoxColor]; } } - (float)photoSize { #if 0 NSLog(@"in -photoSize, returned photoSize = %f", photoSize); #endif return photoSize; } - (void)setPhotoSize:(float)aPhotoSize { if (photoSize == aPhotoSize) return; // unchanged #if 0 NSLog(@"in -setPhotoSize, old value of photoSize: %f, changed to: %f", photoSize, aPhotoSize); #endif [self willChangeValueForKey:@"photoSize"]; photoSize = aPhotoSize; [self didChangeValueForKey:@"photoSize"]; [self reloadData]; #if OLD // update internal grid size, adjust height based on the new grid size // to make sure the same photos stay in view, get a visible photos' index, then scroll to that photo after the update NSRect visibleRect = [self visibleRect]; float heightRatio = visibleRect.origin.y / [self frame].size.height; visibleRect.origin.y = heightRatio * [self frame].size.height; [self scrollRectToVisible:visibleRect]; [self setNeedsDisplayInRect:[self visibleRect]]; // update time for live resizing if (nil != photoResizeTime) { [photoResizeTime release]; photoResizeTime = nil; } isDonePhotoResizing = NO; photoResizeTime = [[NSDate date] retain]; if (nil == photoResizeTimer) { photoResizeTimer = [NSTimer scheduledTimerWithTimeInterval:0.5 target:self selector:@selector(updatePhotoResizing) userInfo:nil repeats:YES]; } #endif } - (NSDictionary *) nameAttributes:(BOOL) spotlighted; { return [NSDictionary dictionaryWithObjectsAndKeys: spotlighted?[NSColor blackColor]:[NSColor whiteColor], NSForegroundColorAttributeName, nil, NSFontAttributeName, nil]; } - (float)heightOfNameField { return 22.0; } - (void)sizeToFit; { // resize to fit the enclosing view #if 0 NSLog(@"MUPhotoView %p sizeToFit: %@", self, NSStringFromRect([[self superview] bounds])); #endif [super setFrame:[[self superview] bounds]]; [self updateGridAndFrame]; // force update [self setNeedsDisplayInRect:[self visibleRect]]; } - (void)resizeWithOldSuperviewSize:(NSSize)oldBoundsSize { #if 0 NSLog(@"MUPhotoView %p resizeWithOldSuperviewSize: %@", self, NSStringFromSize(oldBoundsSize)); NSLog(@"autoresizingMask: %u", [self autoresizingMask]); #endif [super resizeWithOldSuperviewSize:oldBoundsSize]; [self updateGridAndFrame]; // force update [self setNeedsDisplayInRect:[self visibleRect]]; // [self sizeToFit]; // track size of superview } #pragma mark - // Don't Mess With Texas #pragma mark Dont Mess With Texas // haven't tested changing these behaviors yet - there's no reason they shouldn't work... but use at your own risk. - (float)nameFieldSpacing { return 10.0; } - (float)photoVerticalSpacing { //NSLog(@"in -photoVerticalSpacing, returned photoVerticalSpacing = %f", photoVerticalSpacing); return photoVerticalSpacing; } - (void)setPhotoVerticalSpacing:(float)aPhotoVerticalSpacing { //NSLog(@"in -setPhotoVerticalSpacing, old value of photoVerticalSpacing: %f, changed to: %f", photoVerticalSpacing, aPhotoVerticalSpacing); [self willChangeValueForKey:@"photoVerticalSpacing"]; photoVerticalSpacing = aPhotoVerticalSpacing; [self didChangeValueForKey:@"photoVertificalSpacing"]; // update internal grid size, adjust height based on the new grid size NSRect visibleRect = [self visibleRect]; float heightRatio = visibleRect.origin.y / [self frame].size.height; visibleRect.origin.y = heightRatio * [self frame].size.height; [self scrollRectToVisible:visibleRect]; [self setNeedsDisplayInRect:[self visibleRect]]; // update time for live resizing if (nil != photoResizeTime) { [photoResizeTime release]; photoResizeTime = nil; } isDonePhotoResizing = NO; photoResizeTime = [[NSDate date] retain]; if (nil == photoResizeTimer) { photoResizeTimer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(updatePhotoResizing) userInfo:nil repeats:YES]; } } - (float)photoHorizontalSpacing { //NSLog(@"in -photoHorizontalSpacing, returned photoHorizontalSpacing = %f", photoHorizontalSpacing); return photoHorizontalSpacing; } - (void)setPhotoHorizontalSpacing:(float)aPhotoHorizontalSpacing { //NSLog(@"in -setPhotoHorizontalSpacing, old value of photoHorizontalSpacing: %f, changed to: %f", photoHorizontalSpacing, aPhotoHorizontalSpacing); [self willChangeValueForKey:@"photoHorizontalSpacing"]; photoHorizontalSpacing = aPhotoHorizontalSpacing; [self didChangeValueForKey:@"photoHorizontalSpacing"]; // update internal grid size, adjust height based on the new grid size NSRect visibleRect = [self visibleRect]; float heightRatio = visibleRect.origin.y / [self frame].size.height; visibleRect.origin.y = heightRatio * [self frame].size.height; [self scrollRectToVisible:visibleRect]; [self setNeedsDisplayInRect:[self visibleRect]]; // update time for live resizing if (nil != photoResizeTime) { [photoResizeTime release]; photoResizeTime = nil; } isDonePhotoResizing = NO; photoResizeTime = [[NSDate date] retain]; if (nil == photoResizeTimer) { photoResizeTimer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(updatePhotoResizing) userInfo:nil repeats:YES]; } } - (NSColor *)borderOutlineColor { //NSLog(@"in -borderOutlineColor, returned borderOutlineColor = %@", borderOutlineColor); return [[borderOutlineColor retain] autorelease]; } - (void)setBorderOutlineColor:(NSColor *)aBorderOutlineColor { //NSLog(@"in -setBorderOutlineColor:, old value of borderOutlineColor: %@, changed to: %@", borderOutlineColor, aBorderOutlineColor); if (borderOutlineColor != aBorderOutlineColor) { [borderOutlineColor release]; [self willChangeValueForKey:@"borderOutlineColor"]; borderOutlineColor = [aBorderOutlineColor copy]; [self didChangeValueForKey:@"borderOutlineColor"]; [self setNeedsDisplayInRect:[self visibleRect]]; } } - (NSColor *)shadowBoxColor { //NSLog(@"in -shadowBoxColor, returned shadowBoxColor = %@", shadowBoxColor); return [[shadowBoxColor retain] autorelease]; } - (void)setShadowBoxColor:(NSColor *)aShadowBoxColor { //NSLog(@"in -setShadowBoxColor:, old value of shadowBoxColor: %@, changed to: %@", shadowBoxColor, aShadowBoxColor); if (shadowBoxColor != aShadowBoxColor) { [shadowBoxColor release]; shadowBoxColor = [aShadowBoxColor copy]; [self setNeedsDisplayInRect:[self visibleRect]]; } } - (float)selectionBorderWidth { //NSLog(@"in -selectionBorderWidth, returned selectionBorderWidth = %f", selectionBorderWidth); return selectionBorderWidth; } - (void)setSelectionBorderWidth:(float)aSelectionBorderWidth { //NSLog(@"in -setSelectionBorderWidth, old value of selectionBorderWidth: %f, changed to: %f", selectionBorderWidth, aSelectionBorderWidth); selectionBorderWidth = aSelectionBorderWidth; } #pragma mark - // Mouse Event Methods #pragma mark Mouse Event Methods - (void) textDidEndEditing: (NSNotification *)aNotification { // we are the delegate of the field editor if (nameBeingEdited != NSNotFound) { NSText *fe = [[self window] fieldEditor:NO forObject:self]; NSString *string=[[[fe string] copy] autorelease]; // removeFromSuoerview will clear the string! #if 0 NSLog(@"textDidEndEditing:%@", aNotification); NSLog(@"string=%@", string); #endif [self setNeedsDisplayInRect:NSInsetRect([fe frame], -10.0, -10.0)]; // redraw everything inc. space covered by focus box... [fe removeFromSuperview]; [delegate photoView:self setPhotoName:string atIndex:nameBeingEdited]; [self setNeedsDisplayInRect:[self gridRectForIndex:nameBeingEdited]]; // redraw after updating nameBeingEdited=NSNotFound; } } #if 1 // workaround - since after deleting from the end in a multiline text field editor, it does not redraw properly the background - (void) textDidBeginEditing: (NSNotification *)aNotification { // we are the delegate of the field editor if (nameBeingEdited != NSNotFound) { NSText *fe = [[self window] fieldEditor:NO forObject:self]; feFrame = [fe frame]; // initial size } } - (void) textDidChange: (NSNotification *)aNotification { // we are the delegate of the field editor if (nameBeingEdited != NSNotFound) { NSText *fe = [[self window] fieldEditor:NO forObject:self]; #if 0 NSLog(@"textDidChange:%@", aNotification); #endif [self setNeedsDisplayInRect:NSInsetRect(NSUnionRect(feFrame, [fe frame]), -10.0, -10.0)]; feFrame = [fe frame]; // may have been reduced in size } } #endif - (void) mouseDown:(NSEvent *) event { drawSelectionRectangle = allowsMultipleSelection; mouseDownPoint = [self convertPoint:[event locationInWindow] fromView:nil]; mouseCurrentPoint = mouseDownPoint; lastMouseCurrentPoint = mouseCurrentPoint; unsigned clickedIndex = [self photoIndexForPoint:mouseDownPoint]; NSRect photoRect = [self photoRectForIndex:clickedIndex]; NSString *name = [delegate photoView:self photoNameAtIndex:clickedIndex]; NSRect nameRect = [self nameRectForIndex:clickedIndex forName:name fullWidth:NO]; unsigned int flags = [event modifierFlags]; NSMutableIndexSet* indexes = [currentSelection mutableCopy]; BOOL imageHit = NSPointInRect(mouseDownPoint, photoRect); if (nameRect.size.width < 30.0) nameRect.size.width = 30.0; // force minimum width if (imageHit) { if (flags & NSCommandKeyMask) { // Flip current image selection state. if ([indexes containsIndex:clickedIndex]) { // is contained, remove [indexes removeIndex:clickedIndex]; } else { // is not contained, add if (!allowsMultipleSelection) [indexes removeAllIndexes]; [indexes addIndex:clickedIndex]; } } else if (allowsMultipleSelection && (flags & NSShiftKeyMask) != 0) { // Add range to selection. if ([indexes count] == 0) { [indexes addIndex:clickedIndex]; // is first } else { unsigned int origin = (clickedIndex < [indexes lastIndex]) ? clickedIndex :[indexes lastIndex]; unsigned int length = (clickedIndex < [indexes lastIndex]) ? [indexes lastIndex] - clickedIndex : clickedIndex - [indexes lastIndex]; length++; [indexes addIndexesInRange:NSMakeRange(origin, length)]; } } else { // no modifier - if click on non-selected item, reduce multi-selection to single-selection if (![indexes containsIndex:clickedIndex]) { // is not any of the selected [indexes removeAllIndexes]; [indexes addIndex:clickedIndex]; } } potentialDrag = [delegate photoViewAllowsDrag:self]; } else if (NSPointInRect(mouseDownPoint, nameRect)) { // name area below the photo NSWindow *w; NSText *fe = [[self window] fieldEditor:YES forObject:self]; #if 0 NSLog(@"hit name %@ - fe=%@", name, fe); NSLog(@"fe window=%@", [fe window]); NSLog(@"fe superview=%@", [fe superview]); #endif if ((w = [fe window])) { [w makeFirstResponder:w]; // make sure that we release first responder status if fe did exist and was active } [self addSubview:fe]; // make it our subview nameBeingEdited=clickedIndex; [self setFieldEditor:fe frameForIndex:clickedIndex]; [fe setAlignment:NSCenterTextAlignment]; [(NSTextView *) fe setAllowsUndo:YES]; [fe setTextColor:[NSColor blackColor]]; [fe setString:name]; // insert current text value [fe setDelegate:self]; [fe setSelectedRange:(NSRange){0, [name length]}]; // select all [[self window] makeFirstResponder:fe]; // make the field editor the first responder [fe setNeedsDisplay:YES]; feFrame = [fe frame]; // save // [fe mouseDown:event]; // pass first click to field editor return; // don't change selection } else { // clicked outside of any image if ((flags & (NSShiftKeyMask|NSCommandKeyMask)) == 0) { [indexes removeAllIndexes]; // neither shift nor command key } potentialDrag = NO; } [self setCurrentSelection:indexes]; // update selection [indexes release]; } - (void)mouseDragged:(NSEvent *)event { mouseCurrentPoint = [self convertPoint:[event locationInWindow] fromView:nil]; if (NSEqualPoints(mouseCurrentPoint, lastMouseCurrentPoint)) return; // not really dragged lastMouseCurrentPoint = mouseCurrentPoint; // if the mouse has moved less than 5px in either direction, don't register the drag yet float xFromStart = fabs((mouseDownPoint.x - mouseCurrentPoint.x)); float yFromStart = fabs((mouseDownPoint.y - mouseCurrentPoint.y)); if ((xFromStart < 5) && (yFromStart < 5)) { return; } else if (potentialDrag && (nil != delegate)) { // create a drag image unsigned clickedIndex = [self photoIndexForPoint:mouseDownPoint]; NSImage *clickedImage = [self photoAtIndex:clickedIndex]; BOOL flipped = [clickedImage isFlipped]; [clickedImage setFlipped:NO]; NSSize scaledSize = [self scaledPhotoSizeForSize:[clickedImage size]]; if (nil == clickedImage) { // creates a red image, which should let the user/developer know something is wrong clickedImage = [[[NSImage alloc] initWithSize:NSMakeSize(photoSize,photoSize)] autorelease]; [clickedImage lockFocus]; [[NSColor redColor] set]; [NSBezierPath fillRect:NSMakeRect(0,0,photoSize,photoSize)]; [clickedImage unlockFocus]; } NSImage *dragImage = [[NSImage alloc] initWithSize:scaledSize]; // draw the drag image as a semi-transparent copy of the image the user dragged, and optionally a red badge indicating the number of photos [dragImage lockFocus]; [clickedImage drawInRect:NSMakeRect(0,0,scaledSize.width,scaledSize.height) fromRect:NSMakeRect(0,0,[clickedImage size].width,[clickedImage size].height) operation:NSCompositeCopy fraction:0.5]; [dragImage unlockFocus]; [clickedImage setFlipped:flipped]; // if there's more than one image, put a badge on the photo if ([currentSelection count] > 1) { NSMutableDictionary *attributes = [[NSMutableDictionary alloc] init]; [attributes setObject:[NSColor whiteColor] forKey:NSForegroundColorAttributeName]; [attributes setObject:[NSFont fontWithName:@"Helvetica" size:12] forKey:NSFontAttributeName]; NSAttributedString *badgeString = [[NSAttributedString alloc] initWithString:[[NSNumber numberWithInt:[currentSelection count]] stringValue] attributes:attributes]; NSSize stringSize = [badgeString size]; int diameter = stringSize.width; if (stringSize.height > diameter) diameter = stringSize.height; diameter = stringSize.width; if (stringSize.height > diameter) { diameter = stringSize.height; } diameter += 3; // calculate the badge circle int minY = 0; int maxY = minY + diameter; int minX = (diameter - stringSize.width) / 2 - 1; int maxX = minX + diameter; NSBezierPath *circle = [NSBezierPath bezierPathWithOvalInRect:NSMakeRect(minX,minY,maxX-minX,maxY-minY)]; // draw the circle [dragImage lockFocus]; [[NSColor colorWithDeviceRed:1 green:0.1 blue:0.1 alpha:0.7] set]; [circle fill]; [dragImage unlockFocus]; // draw the string NSPoint point; point.x = maxX - ((maxX - minX) / 2) - (stringSize.width / 2); point.y = (maxY - minY) / 2 - (stringSize.height / 2) + 1; [dragImage lockFocus]; [badgeString drawAtPoint:point]; [dragImage unlockFocus]; [badgeString release]; [attributes release]; } // get the supported drag data types from the delegate NSArray *types = [delegate pasteboardDragTypesForPhotoView:self]; if (nil != types) { // get the pasteboard and register the returned types with delegate as the owner NSPasteboard *pb = [NSPasteboard pasteboardWithName:NSDragPboard]; NSString *type; NSEnumerator *te = [types objectEnumerator]; [pb declareTypes:types owner:delegate]; while((type = [te nextObject])) [delegate photoView:self writePasteboard:pb forPhotoAtIndexes:currentSelection dataType:type]; // place the cursor in the center of the drag image NSPoint p = [self convertPoint:[event locationInWindow] fromView:nil]; NSSize imageSize = [dragImage size]; p.x = p.x - imageSize.width / 2; p.y = p.y + imageSize.height / 2; [self dragImage:dragImage at:p offset:NSMakeSize(0,0) event:event pasteboard:pb source:self slideBack:YES]; } [dragImage release]; } else { // adjust the mouse current point so that it's not outside the frame NSRect frameRect = [self frame]; if (mouseCurrentPoint.x < NSMinX(frameRect)) mouseCurrentPoint.x = NSMinX(frameRect); if (mouseCurrentPoint.x > NSMaxX(frameRect)) mouseCurrentPoint.x = NSMaxX(frameRect); if (mouseCurrentPoint.y < NSMinY(frameRect)) mouseCurrentPoint.y = NSMinY(frameRect); if (mouseCurrentPoint.y > NSMaxY(frameRect)) mouseCurrentPoint.y = NSMaxY(frameRect); // determine the rect for the current drag area float minX, maxX, minY, maxY; minX = (mouseCurrentPoint.x < mouseDownPoint.x) ? mouseCurrentPoint.x : mouseDownPoint.x; minY = (mouseCurrentPoint.y < mouseDownPoint.y) ? mouseCurrentPoint.y : mouseDownPoint.y; maxX = (mouseCurrentPoint.x > mouseDownPoint.x) ? mouseCurrentPoint.x : mouseDownPoint.x; maxY = (mouseCurrentPoint.y > mouseDownPoint.y) ? mouseCurrentPoint.y : mouseDownPoint.y; if (maxY > NSMaxY(frameRect)) maxY = NSMaxY(frameRect); if (maxX > NSMaxX(frameRect)) maxX = NSMaxX(frameRect); NSRect selectionRect = NSMakeRect(minX,minY,maxX-minX,maxY-minY); unsigned minIndex = [self photoIndexForPoint:NSMakePoint(minX, minY)]; unsigned xRun = [self photoIndexForPoint:NSMakePoint(maxX, minY)] - minIndex + 1; unsigned yRun = [self photoIndexForPoint:NSMakePoint(minX, maxY)] - minIndex + 1; unsigned selectedRows = (yRun / columns); // Save the current selection (if any), then populate the drag indexes // this allows us to shift band select to add to the current selection. dragSelectedPhotoIndexes = [currentSelection mutableCopy]; // add indexes in the drag rectangle int i; for (i = 0; i <= selectedRows; i++) { unsigned rowStartIndex = (i * columns) + minIndex; int j; for (j = rowStartIndex; j < (rowStartIndex + xRun); j++) { if (NSIntersectsRect([self photoRectForIndex:j],selectionRect)) { if(!allowsMultipleSelection) [dragSelectedPhotoIndexes removeAllIndexes]; [dragSelectedPhotoIndexes addIndex:j]; } } } // if requested, set the selection. this could cause a rapid series of KVO notifications, so if this is false, the view tracks // the selection internally, but doesn't pass it to the bindings or the delegates until the drag is over. // This will cause an appropriate redraw. if (sendsLiveSelectionUpdates) { [self setCurrentSelection:dragSelectedPhotoIndexes]; } // autoscrolling if (autoscrollTimer == nil) { autoscrollTimer = [NSTimer scheduledTimerWithTimeInterval:0.2 target:self selector:@selector(autoscroll) userInfo:nil repeats:YES]; } // [[self superview] autoscroll:event]; [self autoscroll:event]; [self setNeedsDisplayInRect:[self visibleRect]]; // update the selection rectangle } } - (void)mouseUp:(NSEvent *)event { #if 0 NSLog(@"event = %@", event); #endif if ([dragSelectedPhotoIndexes count] > 0) { // finishing a drag selection // move the drag indexes into the main selection indexes [self changeSelection:dragSelectedPhotoIndexes]; [dragSelectedPhotoIndexes release]; dragSelectedPhotoIndexes = nil; [self setNeedsDisplayInRect:[self visibleRect]]; // remove any dragging hints } else if ([event clickCount] == 2) { // Double-click Handling [delegate photoView:self doubleClick:currentSelection]; } else { [delegate photoView:self singleClick:currentSelection]; } if (autoscrollTimer != nil) { [autoscrollTimer invalidate]; autoscrollTimer = nil; } drawSelectionRectangle = NO; } - (unsigned int)draggingSourceOperationMaskForLocal:(BOOL)isLocal { if (nil != delegate) return [delegate photoView:self draggingSourceOperationMaskForLocal:isLocal]; else return NSDragOperationNone; } - (void)autoscroll { // timeout to simulate additional mouseMoved Events NSEvent *event = [NSApp currentEvent]; mouseCurrentPoint = [self convertPoint:[event locationInWindow] fromView:nil]; // [[self superview] autoscroll:event]; [self mouseDragged:event]; } #pragma mark - // Responder Method #pragma mark Responder Methods - (BOOL)acceptsFirstResponder { return([self photoCount] > 0); } - (BOOL)resignFirstResponder { [self setNeedsDisplay:YES]; return YES; } - (BOOL)becomeFirstResponder { [self setNeedsDisplay:YES]; return YES; } - (void)keyDown:(NSEvent *)theEvent { NSString* eventKey = [theEvent charactersIgnoringModifiers]; unichar keyChar = 0; if ([eventKey length] == 1) { keyChar = [eventKey characterAtIndex:0]; if (keyChar == ' ') { [delegate photoView:self doubleClick:currentSelection]; return; } } [self interpretKeyEvents:[NSArray arrayWithObject:theEvent]]; } - (void)deleteBackward:(id)sender { if (0 < [currentSelection count]) { [self removePhotosAtIndexes:currentSelection]; } } - (void)selectAll:(id)sender { if (0 < [self photoCount]) { [self changeSelection:[[[NSIndexSet alloc] initWithIndexesInRange:NSMakeRange(0, [self photoCount])] autorelease]]; } } - (void)insertTab:(id)sender { [[self window] selectKeyViewFollowingView:self]; } - (void)insertBackTab:(id)sender { [[self window] selectKeyViewPrecedingView:self]; } // FIXME: these menthods should all call - (void)moveLeft:(id)sender { NSIndexSet* indexes = currentSelection; NSMutableIndexSet* newIndexes = [[NSMutableIndexSet alloc] init]; if (([indexes count] > 0) && (![indexes containsIndex:0])) { [newIndexes addIndex:[indexes firstIndex] - 1]; } else { if (([indexes count] == 0) && ([self photoCount] > 0)) { [newIndexes addIndex:[self photoCount] - 1]; } } if ([newIndexes count] > 0) { [self changeSelection:newIndexes]; [self scrollRectToVisible:[self gridRectForIndex:[newIndexes firstIndex]]]; } [newIndexes release]; } - (void)moveLeftAndModifySelection:(id)sender { NSIndexSet *indexes = currentSelection; if (([indexes count] > 0) && (![indexes containsIndex:0])) { NSMutableIndexSet *newIndexes = [indexes mutableCopy]; [newIndexes addIndex:([newIndexes firstIndex] - 1)]; [self changeSelection:newIndexes]; [self scrollRectToVisible:[self gridRectForIndex:[newIndexes firstIndex]]]; [newIndexes release]; } } - (void)moveRight:(id)sender { NSIndexSet* indexes = currentSelection; NSMutableIndexSet* newIndexes = [[NSMutableIndexSet alloc] init]; if (([indexes count] > 0) && (![indexes containsIndex:[self photoCount] - 1])) { [newIndexes addIndex:[indexes lastIndex] + 1]; } else { if (([indexes count] == 0) && ([self photoCount] > 0)) { [newIndexes addIndex:0]; } } if ([newIndexes count] > 0) { [self changeSelection:newIndexes]; [self scrollRectToVisible:[self gridRectForIndex:[newIndexes lastIndex]]]; } [newIndexes release]; } - (void)moveRightAndModifySelection:(id)sender { NSIndexSet *indexes = currentSelection; if (([indexes count] > 0) && (![indexes containsIndex:([self photoCount] - 1)])) { NSMutableIndexSet *newIndexes = [indexes mutableCopy]; [newIndexes addIndex:([newIndexes lastIndex] + 1)]; [self changeSelection:newIndexes]; [self scrollRectToVisible:[self gridRectForIndex:[newIndexes lastIndex]]]; [newIndexes release]; } } - (void)moveDown:(id)sender { NSIndexSet* indexes = currentSelection; NSMutableIndexSet* newIndexes = [[NSMutableIndexSet alloc] init]; unsigned int destinationIndex = [indexes lastIndex] + columns; unsigned int lastIndex = [self photoCount] - 1; if (([indexes count] > 0) && (destinationIndex <= lastIndex)) { [newIndexes addIndex:destinationIndex]; } else { if (([indexes count] == 0) && ([self photoCount] > 0)) { [newIndexes addIndex:0]; } } if ([newIndexes count] > 0) { [self changeSelection:newIndexes]; [self scrollRectToVisible:[self gridRectForIndex:[newIndexes lastIndex]]]; } [newIndexes release]; } - (void)moveDownAndModifySelection:(id)sender { NSIndexSet *indexes = currentSelection; unsigned int destinationIndex = [indexes lastIndex] + columns; unsigned int lastIndex = [self photoCount] - 1; if (([indexes count] > 0) && (destinationIndex <= lastIndex)) { NSMutableIndexSet *newIndexes = [indexes mutableCopy]; NSRange addRange; addRange.location = [indexes lastIndex] + 1; addRange.length = columns; [newIndexes addIndexesInRange:addRange]; [self changeSelection:newIndexes]; [self scrollRectToVisible:[self gridRectForIndex:[newIndexes lastIndex]]]; [newIndexes release]; } } - (void)moveUp:(id)sender { NSIndexSet* indexes = currentSelection; NSMutableIndexSet* newIndexes = [[NSMutableIndexSet alloc] init]; if (([indexes count] > 0) && ([indexes firstIndex] >= columns)) { [newIndexes addIndex:[indexes firstIndex] - columns]; } else { if (([indexes count] == 0) && ([self photoCount] > 0)) { [newIndexes addIndex:[self photoCount] - 1]; } } if ([newIndexes count] > 0) { [self changeSelection:newIndexes]; [self scrollRectToVisible:[self gridRectForIndex:[newIndexes firstIndex]]]; } [newIndexes release]; } - (void)moveUpAndModifySelection:(id)sender { NSMutableIndexSet *indexes = [currentSelection mutableCopy]; if (([indexes count] > 0) && ([indexes firstIndex] >= columns)) { [indexes addIndexesInRange:NSMakeRange(([indexes firstIndex] - columns), columns + 1)]; [self changeSelection:indexes]; [self scrollRectToVisible:[self gridRectForIndex:[indexes firstIndex]]]; } [indexes release]; } - (void)scrollToEndOfDocument:(id)sender { [self scrollRectToVisible:[self gridRectForIndex:([self photoCount] - 1)]]; } - (void)scrollToBeginningOfDocument:(id)sender { [self scrollPoint:NSZeroPoint]; } - (void)moveToEndOfLine:(id)sender { NSIndexSet *indexes = currentSelection; if ([indexes count] > 0) { unsigned int destinationIndex = ([indexes lastIndex] + columns) - ([indexes lastIndex] % columns) - 1; if (destinationIndex >= [self photoCount]) { destinationIndex = [self photoCount] - 1; } NSIndexSet *newIndexes = [[NSIndexSet alloc] initWithIndex:destinationIndex]; [self changeSelection:newIndexes]; [self scrollRectToVisible:[self gridRectForIndex:destinationIndex]]; [newIndexes release]; } } - (void)moveToEndOfLineAndModifySelection:(id)sender { NSMutableIndexSet *indexes = [currentSelection mutableCopy]; if ([indexes count] > 0) { unsigned int destinationIndexPlusOne = ([indexes lastIndex] + columns) - ([indexes lastIndex] % columns); if (destinationIndexPlusOne >= [self photoCount]) { destinationIndexPlusOne = [self photoCount]; } [indexes addIndexesInRange:NSMakeRange(([indexes lastIndex]), (destinationIndexPlusOne - [indexes lastIndex]))]; [self changeSelection:indexes]; [self scrollRectToVisible:[self gridRectForIndex:[indexes lastIndex]]]; } [indexes release]; } - (void)moveToBeginningOfLine:(id)sender { NSIndexSet *indexes = currentSelection; if ([indexes count] > 0) { unsigned int destinationIndex = [indexes firstIndex] - ([indexes firstIndex] % columns); NSIndexSet *newIndexes = [[NSIndexSet alloc] initWithIndex:destinationIndex]; [self changeSelection:newIndexes]; [self scrollRectToVisible:[self gridRectForIndex:destinationIndex]]; [newIndexes release]; } } - (void)moveToBeginningOfLineAndModifySelection:(id)sender { NSMutableIndexSet *indexes = [currentSelection mutableCopy]; if ([indexes count] > 0) { unsigned int destinationIndex = [indexes firstIndex] - ([indexes firstIndex] % columns); [indexes addIndexesInRange:NSMakeRange(destinationIndex, ([indexes firstIndex] - destinationIndex))]; [self changeSelection:indexes]; [self scrollRectToVisible:[self gridRectForIndex:destinationIndex]]; } [indexes release]; } - (void)moveToBeginningOfDocument:(id)sender { if (0 < [self photoCount]) { [self changeSelection:[NSIndexSet indexSetWithIndex:0]]; [self scrollPoint:NSZeroPoint]; } } - (void)moveToBeginningOfDocumentAndModifySelection:(id)sender { NSMutableIndexSet *indexes = [currentSelection mutableCopy]; if ([indexes count] > 0) { [indexes addIndexesInRange:NSMakeRange(0, [indexes firstIndex])]; [self changeSelection:indexes]; [self scrollRectToVisible:NSZeroRect]; } [indexes release]; } - (void)moveToEndOfDocument:(id)sender { if (0 < [self photoCount]) { [self changeSelection:[NSIndexSet indexSetWithIndex:([self photoCount] - 1)]]; [self scrollRectToVisible:[self gridRectForIndex:([self photoCount] - 1)]]; } } - (void)moveToEndOfDocumentAndModifySelection:(id)sender { NSMutableIndexSet *indexes = [currentSelection mutableCopy]; if ([indexes count] > 0) { [indexes addIndexesInRange:NSMakeRange([indexes lastIndex], ([self photoCount] - [indexes lastIndex]))]; [self changeSelection:indexes]; [self scrollRectToVisible:[self gridRectForIndex:[indexes lastIndex]]]; } } #pragma mark NSDraggingDestination - (NSDragOperation)draggingEntered:(id )sender { // dragged image has entered our frame return [self draggingUpdated:sender]; } - (NSDragOperation)draggingUpdated:(id )sender { NSPoint mouse = [sender draggingLocation]; // mouse position in window coordinates NSPoint mousePoint = [self convertPoint:mouse fromView:nil]; // convert from window coordinates [self setNeedsDisplayInRect:[self gridRectForIndex:dropDestinationIndex]]; // update previous dropDestinationIndex = [self photoIndexForPoint:mousePoint]; NSRect photoRect = [self photoRectForIndex:dropDestinationIndex]; BOOL imageHit = NSPointInRect(mousePoint, photoRect); if (!imageHit) { dropDestinationIndex = NSNotFound; return NSDragOperationNone; // can't drop outside the real image frame } [self setNeedsDisplayInRect:[self gridRectForIndex:dropDestinationIndex]]; // update new location // we should ask the delegate if it really accepts the specific drop, i.e. if the list of tags dropped is already attached... // FIXME: // add more visual feedback than changing the + emblem // highlight as a drag destination // return [sender draggingSourceOperationMask] & NSDragOperationCopy; } - (void)draggingEnded:(id )sender { // dragging ended somewhere else [self draggingUpdated:sender]; } - (void)draggingExited:(id )sender { // image has left our frame [self draggingUpdated:sender]; } - (BOOL)wantsPeriodicDraggingUpdates { return NO; } - (BOOL)prepareForDragOperation:(id )sender { // finally agree or refuse the operation return YES; } - (BOOL)performDragOperation:(id )sender { // call delegate to really import the data NSPoint mouse = [sender draggingLocation]; // mouse position in window coordinates NSPoint mousePoint = [self convertPoint:mouse fromView:nil]; // convert from window coordinates unsigned photoIndex = [self photoIndexForPoint:mousePoint]; return [delegate photoView:self acceptDrop:sender onPhotoIndex:photoIndex]; } - (void)concludeDragOperation:(id )sender { // dragging finally done [self setNeedsDisplayInRect:[self gridRectForIndex:dropDestinationIndex]]; // update previous dropDestinationIndex = NSNotFound; } @end #pragma mark - // Delegate Default Implementations #pragma mark Delegate Default Implementations @implementation NSObject (MUPhotoViewDelegate) // will only get called if photoArray has not been set, or has not been bound - (unsigned)photoCountForPhotoView:(MUPhotoView *)view { return 0; } - (BOOL) photoViewAllowsDrag:(MUPhotoView *)view { return YES; } - (NSString *)photoView:(MUPhotoView *)view photoNameAtIndex:(unsigned)index { return nil; } - (void)photoView:(MUPhotoView *)view setPhotoName:(NSString *) name atIndex:(unsigned)index; { return; // ignore } - (BOOL)photoView:(MUPhotoView *)view spotlightPhotoAtIndex:(unsigned)index; { return NO; } - (NSImage *)photoView:(MUPhotoView *)view cornerTagAtIndex:(unsigned)index corner:(NSRectCorner) corner; { return nil; } - (NSImage *)photoView:(MUPhotoView *)view photoAtIndex:(unsigned)index { return nil; } - (NSImage *)photoView:(MUPhotoView *)view fastPhotoAtIndex:(unsigned)index { return [self photoView:view photoAtIndex:index]; } // selection - (BOOL)photoView:(MUPhotoView *)view isPhotoSelectedAtIndex:(unsigned)index; { return [[view currentSelection] containsIndex:index]; } // drag - (unsigned int)photoView:(MUPhotoView *)view draggingSourceOperationMaskForLocal:(BOOL)isLocal { return NSDragOperationNone; } - (NSArray *)pasteboardDragTypesForPhotoView:(MUPhotoView *)view { return [NSArray array]; } - (void)photoView:(MUPhotoView *)view writePasteboard:(NSPasteboard *) pb forPhotoAtIndexes:(NSIndexSet *)index dataType:(NSString *)type; { return; } - (BOOL)photoView:(MUPhotoView *)view acceptDrop:(id )info onPhotoIndex:(unsigned) photoIndex; { return NO; // reject by default } // did dragg a selection box - (void) photoView:(MUPhotoView *)view changeSelection:(NSIndexSet *)indexes; { } // single click - (void)photoView:(MUPhotoView *)view singleClick:(NSIndexSet *) indexes { } // double-click - (void)photoView:(MUPhotoView *)view doubleClick:(NSIndexSet *) indexes { } // photo removal support - (NSIndexSet *)photoView:(MUPhotoView *)view willRemovePhotosAtIndexes:(NSIndexSet *)indexes { return [NSIndexSet indexSet]; } - (void)photoView:(MUPhotoView *)view didRemovePhotosAtIndexes:(NSIndexSet *)indexes { } @end #pragma mark - // Private #pragma mark Private @implementation MUPhotoView (PrivateAPI) - (BOOL)shouldDelayWindowOrderingForEvent:(NSEvent *)theEvent { NSPoint mouseEventLocation; mouseEventLocation = [self convertPoint:[theEvent locationInWindow] fromView:nil]; unsigned clickedIndex = [self photoIndexForPoint:mouseEventLocation]; NSRect photoRect = [self photoRectForIndex:clickedIndex]; return(NSPointInRect(mouseEventLocation, photoRect)); } - (BOOL)acceptsFirstMouse:(NSEvent *)theEvent { // CEsfahani - If acceptsFirstMouse unconditionally returns YES, then it is possible to lose the selection if // the user clicks in the content of the window without hitting one of the selected images. This is // the Finder's behavior, and it bothers me. // It seems I have two options: unconditionally return YES, or only return YES if we clicked in an image. // But, does anyone rely on losing the selection if I bring a window forward? NSPoint mouseEventLocation; mouseEventLocation = [self convertPoint:[theEvent locationInWindow] fromView:nil]; unsigned clickedIndex = [self photoIndexForPoint:mouseEventLocation]; NSRect photoRect = [self photoRectForIndex:clickedIndex]; return NSPointInRect(mouseEventLocation, photoRect); } - (void)viewDidEndLiveResize { [self setNeedsDisplayInRect:[self visibleRect]]; } - (NSRect) nameRectForIndex:(unsigned) index forName:(NSString *) name fullWidth:(BOOL) flag; { // get bounding box of given name NSRect photoRect = [self photoRectForIndex:index]; NSSize textSize = name?[name sizeWithAttributes:[self nameAttributes:NO]]:NSMakeSize(20.0, [self heightOfNameField]); if (flag) // as wide as the photo width return NSMakeRect(photoRect.origin.x, photoRect.origin.y+photoRect.size.height+[self nameFieldSpacing], photoRect.size.width, textSize.height); else // as wide as needed return NSMakeRect(photoRect.origin.x+(photoRect.size.width-textSize.width)/2, photoRect.origin.y+photoRect.size.height+[self nameFieldSpacing], textSize.width, textSize.height); } - (void) setFieldEditor:(NSText *) fe frameForIndex:(unsigned) index; { if (index != NSNotFound) { NSString *name = [delegate photoView:self photoNameAtIndex:index]; NSRect nameRect = [self nameRectForIndex:index forName:name fullWidth:YES]; [fe setFrame:nameRect]; [self setNeedsDisplay:YES]; } } - (void)setFrame:(NSRect)frame { #if 0 NSLog(@"MUPhotoView %p setFrame: %@", self, NSStringFromRect(frame)); #endif [super setFrame:frame]; // adjust [self updateGridAndFrame]; // force update (which may readjust the frame again!) } - (void)updateGridAndFrame { NSRect frame = [self frame]; #if DEBUG NSLog(@"updateGridAndFrame"); #endif /**** BEGIN Dimension calculations and adjustments ****/ // get the number of photos unsigned photoCount = [self photoCount]; // calculate the base grid size gridSize.height = [self photoSize] + [self nameFieldSpacing] + [self heightOfNameField] + [self photoVerticalSpacing]; gridSize.width = [self photoSize] + [self photoHorizontalSpacing]; // calculate the number of columns that fit into our width columns = frame.size.width / gridSize.width; // minimum 1 column if (columns < 1) columns = 1; // adjust the grid size width for any extra space gridSize.width = frame.size.width / columns; // if we have fewer photos than columns, adjust downwards if (photoCount > 0 && photoCount < columns) columns = photoCount; // calculate the number of rows of photos based on the total count and the number of columns rows = photoCount / columns; if (0 < (photoCount % columns)) rows++; // add one partial row // adjust my frame height to contain all the photos float h = rows * gridSize.height; // required height for all entries if (h < [[self superview] bounds].size.height) h = [[self superview] bounds].size.height; // at least as large as the clipview if (frame.size.height != h) { // set my new frame height #if DEBUG NSLog(@"updateGridAndFrame - set new height"); #endif frame.size.height = h; // adjust [super setFrame:frame]; // may be called in [self setFrame]! } // FIXME: outer scroll bars are note updated synchronuously!! NSScrollView *s = [self enclosingScrollView]; [s reflectScrolledClipView:[s contentView]]; // adjust scrollers [s setNeedsDisplay:YES]; /**** END Dimension calculations and adjustments ****/ } // will fetch from the internal array if not nil, from delegate otherwise - (unsigned)photoCount { if (nil != [self photosArray]) return [[self photosArray] count]; else if (nil != delegate) return [delegate photoCountForPhotoView:self]; else return 0; } - (NSImage *)photoAtIndex:(unsigned)index { if ((nil != [self photosArray]) && (index < [self photoCount])) return [[self photosArray] objectAtIndex:index]; else if ((nil != delegate) && (index < [self photoCount])) return [delegate photoView:self photoAtIndex:index]; else return nil; } - (void)updatePhotoResizing { NSTimeInterval timeSinceResize = [[NSDate date] timeIntervalSinceReferenceDate] - [photoResizeTime timeIntervalSinceReferenceDate]; if (timeSinceResize > 1) { isDonePhotoResizing = YES; [photoResizeTimer invalidate]; photoResizeTimer = nil; } [self setNeedsDisplayInRect:[self visibleRect]]; } - (BOOL)inLiveResize { return ([super inLiveResize]) || (!isDonePhotoResizing); } - (NSImage *)fastPhotoAtIndex:(unsigned)index { NSImage *fastPhoto; if ((nil != [self photosArray]) && (index < [[self photosArray] count])) { fastPhoto = [photosFastArray objectAtIndex:index]; if ((NSNull *)fastPhoto == [NSNull null]) { // Change this if you want higher/lower quality fast photos float fastPhotoSize = 100.0; NSImageRep *fullSizePhotoRep = [[self scalePhoto:[self photoAtIndex:index]] bestRepresentationForDevice:nil]; // Figure out what the scaled size is float longSide = [fullSizePhotoRep pixelsWide]; if (longSide < [fullSizePhotoRep pixelsHigh]) longSide = [fullSizePhotoRep pixelsHigh]; float scale = fastPhotoSize / longSide; NSSize scaledSize; scaledSize.width = [fullSizePhotoRep pixelsWide] * scale; scaledSize.height = [fullSizePhotoRep pixelsHigh] * scale; // Draw the full-size image into our fast, small image. fastPhoto = [[NSImage alloc] initWithSize:scaledSize]; [fastPhoto setFlipped:YES]; [fastPhoto lockFocus]; [fullSizePhotoRep drawInRect:NSMakeRect(0.0, 0.0, scaledSize.width, scaledSize.height)]; [fastPhoto unlockFocus]; // Save it off [photosFastArray replaceObjectAtIndex:index withObject:fastPhoto]; [fastPhoto autorelease]; } } else if ((nil != delegate) && ([delegate respondsToSelector:@selector(photoView:fastPhotoAtIndex:)])) { fastPhoto = [delegate photoView:self fastPhotoAtIndex:index]; } // if the above calls failed, try to just fetch the full size image if (nil == fastPhoto) { fastPhoto = [self photoAtIndex:index]; } return fastPhoto; } // placement and hit detection - (NSSize)scaledPhotoSizeForSize:(NSSize)size { float longSide = size.width; if (longSide < size.height) longSide = size.height; float scale = [self photoSize] / longSide; NSSize scaledSize; // scaledSize.width = floor(size.width * scale); // scaledSize.height = floor(size.height * scale); scaledSize.width = size.width * scale; scaledSize.height = size.height * scale; return scaledSize; } - (NSImage *)scalePhoto:(NSImage *)image { // calculate the new image size based on the scale NSSize newSize; NSImageRep *bestRep = [image bestRepresentationForDevice:nil]; newSize.width = [bestRep pixelsWide]; newSize.height = [bestRep pixelsHigh]; #if 0 NSLog(@"%@", NSStringFromSize(newSize)); #endif // resize the image [image setScalesWhenResized:YES]; [image setSize:newSize]; return image; } - (unsigned)photoIndexForPoint:(NSPoint)point { unsigned column = point.x / gridSize.width; unsigned row = point.y / gridSize.height; return ((row * columns) + column); } - (NSRange)photoIndexRangeForRect:(NSRect)rect { unsigned start = [self photoIndexForPoint:rect.origin]; unsigned finish = [self photoIndexForPoint:NSMakePoint(NSMaxX(rect), NSMaxY(rect))]; if (finish >= [self photoCount]) finish = [self photoCount] - 1; return NSMakeRange(start, finish-start); } - (NSRect)gridRectForIndex:(unsigned)index { unsigned row = index / columns; unsigned column = index % columns; float x = column * gridSize.width; float y = row * gridSize.height; return NSMakeRect(x, y, gridSize.width, gridSize.height); } - (NSRect)rectCenteredInRect:(NSRect)rect withSize:(NSSize)size { float x = rect.origin.x + ((rect.size.width - size.width) / 2); float y = rect.origin.y + ((rect.size.height - size.height) / 2); // return NSMakeRect(x, y, size.width, size.height); return NSMakeRect(floor(x), floor(y), size.width, size.height); } - (NSRect)photoRectForIndex:(unsigned)index { if ([self photoCount] == 0) return NSZeroRect; // get the grid rect for this index NSRect gridRect = [self gridRectForIndex:index]; // get the actual image NSImage *photo = [self photoAtIndex:index]; if (nil == photo) return NSZeroRect; // scale to the current photoSize photo = [self scalePhoto:photo]; // scale the dimensions NSSize scaledSize = [self scaledPhotoSizeForSize:[photo size]]; // get the photo rect centered in the grid less space for the name gridRect.size.height -= ([self nameFieldSpacing] + [self heightOfNameField])/2; NSRect photoRect = [self rectCenteredInRect:gridRect withSize:scaledSize]; return photoRect; } - (NSBezierPath *)shadowBoxPathForRect:(NSRect)rect { NSRect inset = NSInsetRect(rect,5.0,5.0); //float radius = 15.0; float radius = 0.0; float minX = NSMinX(inset); float midX = NSMidX(inset); float maxX = NSMaxX(inset); float minY = NSMinY(inset); float midY = NSMidY(inset); float maxY = NSMaxY(inset); NSBezierPath *path = [[NSBezierPath alloc] init]; [path moveToPoint:NSMakePoint(midX, minY)]; [path appendBezierPathWithArcFromPoint:NSMakePoint(maxX,minY) toPoint:NSMakePoint(maxX,midY) radius:radius]; [path appendBezierPathWithArcFromPoint:NSMakePoint(maxX,maxY) toPoint:NSMakePoint(midX,maxY) radius:radius]; [path appendBezierPathWithArcFromPoint:NSMakePoint(minX,maxY) toPoint:NSMakePoint(minX,midY) radius:radius]; [path appendBezierPathWithArcFromPoint:NSMakePoint(minX,minY) toPoint:NSMakePoint(midX,minY) radius:radius]; return [path autorelease]; } // photo removal - (void)removePhotosAtIndexes:(NSIndexSet *)indexes { // let the delegate know that we're about to delete, give it a chance to modify the indexes we'll delete NSIndexSet *modifiedIndexes = indexes; if ((nil != delegate) && ([delegate respondsToSelector:@selector(photoView:willRemovePhotosAtIndexes:)])) { modifiedIndexes = [delegate photoView:self willRemovePhotosAtIndexes:indexes]; } // if using bindings, do the removal if ((0 < [modifiedIndexes count]) && (nil != [self photosArray])) { [self willChangeValueForKey:@"photosArray"]; [photosArray removeObjectsAtIndexes:modifiedIndexes]; [self didChangeValueForKey:@"photosArray"]; } if ((nil != delegate) && ([delegate respondsToSelector:@selector(photoView:didRemovePhotosAtIndexes:)])) { [delegate photoView:self didRemovePhotosAtIndexes:modifiedIndexes]; } } #if UNUSED - (NSImage *)scaleImage:(NSImage *)image toSize:(float)size { NSImageRep *fullSizePhotoRep = [[self scalePhoto:image] bestRepresentationForDevice:nil]; float longSide = [fullSizePhotoRep pixelsWide]; if (longSide < [fullSizePhotoRep pixelsHigh]) longSide = [fullSizePhotoRep pixelsHigh]; float scale = size / longSide; NSSize scaledSize; scaledSize.width = [fullSizePhotoRep pixelsWide] * scale; scaledSize.height = [fullSizePhotoRep pixelsHigh] * scale; NSImage *fastPhoto = [[NSImage alloc] initWithSize:scaledSize]; [fastPhoto setFlipped:YES]; [fastPhoto lockFocus]; NSLog(@"compositing = %d", [[NSGraphicsContext currentContext] compositingOperation]); [fullSizePhotoRep drawInRect:NSMakeRect(0.0, 0.0, scaledSize.width, scaledSize.height)]; [fastPhoto unlockFocus]; return [fastPhoto autorelease]; } #endif - (void)dirtyDisplayRectsForNewSelection:(NSIndexSet *)newSelection oldSelection:(NSIndexSet *)oldSelection { NSRect visibleRect = [self visibleRect]; // Figure out how the selection changed and only update those areas of the grid NSMutableIndexSet *changedIndexes = [NSMutableIndexSet indexSet]; if (oldSelection && newSelection) { // First, see which of the old are different than the new unsigned int index = [newSelection firstIndex]; while (index != NSNotFound) { if (![oldSelection containsIndex:index]) { [changedIndexes addIndex:index]; } index = [newSelection indexGreaterThanIndex:index]; } // Next, see which of the new are different from the old index = [oldSelection firstIndex]; while (index != NSNotFound) { if (![newSelection containsIndex:index]) { [changedIndexes addIndex:index]; } index = [oldSelection indexGreaterThanIndex:index]; } // Loop through the changes and dirty the rect for each index = [changedIndexes firstIndex]; while (index != NSNotFound) { NSRect photoRect = [self gridRectForIndex:index]; if (NSIntersectsRect(visibleRect, photoRect)) { [self setNeedsDisplayInRect:photoRect]; } index = [changedIndexes indexGreaterThanIndex:index]; } } else { [self setNeedsDisplayInRect:visibleRect]; } } @end #endif