From: <ho...@us...> - 2009-03-12 13:22:34
|
Revision: 14693 http://bibdesk.svn.sourceforge.net/bibdesk/?rev=14693&view=rev Author: hofman Date: 2009-03-12 13:22:14 +0000 (Thu, 12 Mar 2009) Log Message: ----------- Rename FileView to FVFileView and add an umbrella header for the FileView framework Modified Paths: -------------- trunk/bibdesk_vendorsrc/amaxwell/FileView/FVColorMenuView.m trunk/bibdesk_vendorsrc/amaxwell/FileView/FVIconOperation.h trunk/bibdesk_vendorsrc/amaxwell/FileView/FVIconOperation.m trunk/bibdesk_vendorsrc/amaxwell/FileView/FVInvocationOperation.m trunk/bibdesk_vendorsrc/amaxwell/FileView/FVPreviewer.m trunk/bibdesk_vendorsrc/amaxwell/FileView/FileView Program/Controller.h trunk/bibdesk_vendorsrc/amaxwell/FileView/FileView Program/Controller.m trunk/bibdesk_vendorsrc/amaxwell/FileView/FileView.xcodeproj/project.pbxproj Added Paths: ----------- trunk/bibdesk_vendorsrc/amaxwell/FileView/FVFileView.h trunk/bibdesk_vendorsrc/amaxwell/FileView/FVFileView.m Removed Paths: ------------- trunk/bibdesk_vendorsrc/amaxwell/FileView/FileView.h trunk/bibdesk_vendorsrc/amaxwell/FileView/FileView.m Modified: trunk/bibdesk_vendorsrc/amaxwell/FileView/FVColorMenuView.m =================================================================== --- trunk/bibdesk_vendorsrc/amaxwell/FileView/FVColorMenuView.m 2009-03-12 04:58:48 UTC (rev 14692) +++ trunk/bibdesk_vendorsrc/amaxwell/FileView/FVColorMenuView.m 2009-03-12 13:22:14 UTC (rev 14693) @@ -38,7 +38,7 @@ #import "FVColorMenuView.h" #import "FVUtilities.h" -#import <FileView/FVFinderLabel.h> +#import "FVFinderLabel.h" static NSString * const FVColorNameUpdateNotification = @"FVColorNameUpdateNotification"; Copied: trunk/bibdesk_vendorsrc/amaxwell/FileView/FVFileView.h (from rev 14692, trunk/bibdesk_vendorsrc/amaxwell/FileView/FileView.h) =================================================================== --- trunk/bibdesk_vendorsrc/amaxwell/FileView/FVFileView.h (rev 0) +++ trunk/bibdesk_vendorsrc/amaxwell/FileView/FVFileView.h 2009-03-12 13:22:14 UTC (rev 14693) @@ -0,0 +1,200 @@ +// +// FVFileView.h +// FileView +// +// Created by Adam Maxwell on 06/23/07. +/* + This software is Copyright (c) 2007-2009 + Adam Maxwell. All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + - Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + - Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + + - Neither the name of Adam Maxwell nor the names of any + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#import <Cocoa/Cocoa.h> + +enum { + FVZoomInMenuItemTag = 1001, + FVZoomOutMenuItemTag = 1002, + FVAutoScalesMenuItemTag = 1003, + FVQuickLookMenuItemTag = 1004, + FVOpenMenuItemTag = 1005, + FVRevealMenuItemTag = 1006, + FVChangeLabelMenuItemTag = 1007, + FVDownloadMenuItemTag = 1008, + FVRemoveMenuItemTag = 1009 +}; + +typedef enum _FVDropOperation { + FVDropOn, + FVDropBefore, + FVDropAfter +} FVDropOperation; + +@class FVSliderWindow, FVOperationQueue; + +@interface FVFileView : NSView +{ +@private + id _delegate; + id _dataSource; + NSMutableDictionary *_iconCache; + CFMutableDictionaryRef _iconIndexMap; + CFMutableDictionaryRef _iconURLMap; + NSUInteger _numberOfColumns; + NSUInteger _numberOfRows; + NSColor *_backgroundColor; + CFRunLoopTimerRef _zombieTimer; + NSMutableIndexSet *_selectedIndexes; + CGLayerRef _selectionOverlay; + NSUInteger _lastClickedIndex; + NSUInteger _dropOperation; + NSUInteger _dropIndex; + NSRect _rubberBandRect; + BOOL _isMouseDown; + NSSize _padding; + NSSize _iconSize; + NSPoint _lastMouseDownLocInView; + BOOL _isEditable; + BOOL _isRescaling; + BOOL _isDrawingDragImage; + CFAbsoluteTime _timeOfLastOrigin; + NSPoint _lastOrigin; + CFMutableDictionaryRef _trackingRectMap; + NSButtonCell *_leftArrow; + NSButtonCell *_rightArrow; + NSRect _leftArrowFrame; + NSRect _rightArrowFrame; + FVSliderWindow *_sliderWindow; + NSTrackingRectTag _topSliderTag; + NSTrackingRectTag _bottomSliderTag; + FVOperationQueue *_operationQueue; + + CFMutableDictionaryRef _activeDownloads; + CFRunLoopTimerRef _progressTimer; + NSArray *_iconURLs; + + BOOL _autoScales; +} + +// bindings compatibility, although this can be set directly +- (void)setIconURLs:(NSArray *)anArray; +- (NSArray *)iconURLs; + +// this is the only way to get selection information at present +- (NSIndexSet *)selectionIndexes; +- (void)setSelectionIndexes:(NSIndexSet *)indexSet; + +// bind a slider or other control to this +- (CGFloat)iconScale; +- (void)setIconScale:(CGFloat)scale; + +- (BOOL)autoScales; +- (void)setAutoScales:(BOOL)flag; + +- (NSUInteger)numberOfRows; +- (NSUInteger)numberOfColumns; +- (void)reloadIcons; + +// default is Mail's source list color +- (NSColor *)backgroundColor; +- (void)setBackgroundColor:(NSColor *)aColor; + +// actions that NSResponder doesn't declare +- (IBAction)selectPreviousIcon:(id)sender; +- (IBAction)selectNextIcon:(id)sender; +- (IBAction)delete:(id)sender; + +// sender must implement -tag to return a valid Finder label integer (0-7); non-file URLs are ignored +- (IBAction)changeFinderLabel:(id)sender; +- (IBAction)openSelectedURLs:(id)sender; + +- (BOOL)isEditable; +- (void)setEditable:(BOOL)flag; + +- (void)setDropIndex:(NSUInteger)anIndex dropOperation:(FVDropOperation)anOperation; + +// required for drag-and-drop support +- (void)setDataSource:(id)obj; +- (id)dataSource; + +- (void)setDelegate:(id)obj; +- (id)delegate; + +- (BOOL)allowsDownloading; +- (void)setAllowsDownloading:(BOOL)flag; + +@end + + +// dataSource must conform to this +@interface NSObject (FileViewDataSource) + +// delegate must return an NSURL or nil (a missing value) for each index < numberOfFiles +- (NSUInteger)numberOfURLsInFileView:(FVFileView *)aFileView; +- (NSURL *)fileView:(FVFileView *)aFileView URLAtIndex:(NSUInteger)anIndex; + +// optional method for a subtitle +- (NSString *)fileView:(FVFileView *)aFileView subtitleAtIndex:(NSUInteger)anIndex; + +@end + +// datasource must implement all of these methods or dropping/rearranging will be disabled +@interface NSObject (FileViewDragDataSource) + +// implement to do something (or nothing) with the dropped URLs +- (void)fileView:(FVFileView *)aFileView insertURLs:(NSArray *)absoluteURLs atIndexes:(NSIndexSet *)aSet forDrop:(id <NSDraggingInfo>)info dropOperation:(FVDropOperation)operation; + +// the datasource may replace the files at the given indexes +- (BOOL)fileView:(FVFileView *)aFileView replaceURLsAtIndexes:(NSIndexSet *)aSet withURLs:(NSArray *)newURLs forDrop:(id <NSDraggingInfo>)info dropOperation:(FVDropOperation)operation; + +// rearranging files in the view +- (BOOL)fileView:(FVFileView *)aFileView moveURLsAtIndexes:(NSIndexSet *)aSet toIndex:(NSUInteger)anIndex forDrop:(id <NSDraggingInfo>)info dropOperation:(FVDropOperation)operation; + +// does not delete the file from disk; this is the datasource's responsibility +- (BOOL)fileView:(FVFileView *)aFileView deleteURLsAtIndexes:(NSIndexSet *)indexSet; + +@end + +@interface NSObject (FileViewDelegate) + +// Called immediately before display. The anIndex parameter will be NSNotFound if there is not a URL at the mouse event location. If you remove all items, the menu will not be shown. +- (void)fileView:(FVFileView *)aFileView willPopUpMenu:(NSMenu *)aMenu onIconAtIndex:(NSUInteger)anIndex; + +// In addition, it can be sent the WebUIDelegate method webView:contextMenuItemsForElement:defaultMenuItems: + +// If unimplemented or returns YES, fileview will open the URL using NSWorkspace +- (BOOL)fileView:(FVFileView *)aFileView shouldOpenURL:(NSURL *)aURL; + +// If unimplemented, fileview will use a system temporary directory; if returns nil, cancels download. Used with FVDownloadMenuItemTag menu item. +- (NSURL *)fileView:(FVFileView *)aFileView downloadDestinationWithSuggestedFilename:(NSString *)filename; + +// If unimplemented, uses the proposedDragOperation +- (NSDragOperation)fileView:(FVFileView *)aFileView validateDrop:(id <NSDraggingInfo>)info draggedURLs:(NSArray *)draggedURLs proposedIndex:(NSUInteger)anIndex proposedDropOperation:(FVDropOperation)dropOperation proposedDragOperation:(NSDragOperation)dragOperation; + +@end Copied: trunk/bibdesk_vendorsrc/amaxwell/FileView/FVFileView.m (from rev 14692, trunk/bibdesk_vendorsrc/amaxwell/FileView/FileView.m) =================================================================== --- trunk/bibdesk_vendorsrc/amaxwell/FileView/FVFileView.m (rev 0) +++ trunk/bibdesk_vendorsrc/amaxwell/FileView/FVFileView.m 2009-03-12 13:22:14 UTC (rev 14693) @@ -0,0 +1,3156 @@ +// +// FVFileView.m +// FileView +// +// Created by Adam Maxwell on 06/23/07. +/* + This software is Copyright (c) 2007-2009 + Adam Maxwell. All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + - Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + - Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + + - Neither the name of Adam Maxwell nor the names of any + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#import "FVFileView.h" +#import "FVFinderLabel.h" +#import "FVPreviewer.h" + +#import <QTKit/QTKit.h> +#import <WebKit/WebKit.h> + +#import "FVIcon.h" +#import "FVArrowButtonCell.h" +#import "FVUtilities.h" +#import "FVOperationQueue.h" +#import "FVIconOperation.h" +#import "FVDownload.h" +#import "FVSlider.h" +#import "FVColorMenuView.h" +#import "FVBitmapContextCache.h" +#import "FVAccessibilityIconElement.h" + + +static NSString *FVWeblocFilePboardType = @"CorePasteboardFlavorType 0x75726C20"; + +static const NSSize DEFAULT_ICON_SIZE = { 64.0, 64.0 }; +static const NSSize DEFAULT_PADDING = { 10.0, 4.0 }; +static const CGFloat DEFAULT_MARGIN = 4.0; + +// don't bother removing icons from the cache if there are fewer than this value +static const NSUInteger ZOMBIE_CACHE_THRESHOLD = 100; + +// thin the icons if we have more than this value; 25 is a good value, but 5 is good for cache testing +static const NSUInteger RELEASE_CACHE_THRESHOLD = 25; + +// check the icon cache every five minutes and get rid of stale icons +static const CFTimeInterval ZOMBIE_TIMER_INTERVAL = 300.0; + +// time interval for indeterminate download progress indicator updates +static const CFTimeInterval PROGRESS_TIMER_INTERVAL = 0.1; + +static NSDictionary *_titleAttributes = nil; +static NSDictionary *_labeledAttributes = nil; +static NSDictionary *_subtitleAttributes = nil; +static CGFloat _titleHeight = 0.0; +static CGFloat _subtitleHeight = 0.0; +static CGColorRef _shadowColor = NULL; + +#pragma mark - + +@interface FVFileView (Private) +// wrapper that calls bound array or datasource transparently; for internal use +// clients should access the datasource or bound array directly +- (NSURL *)iconURLAtIndex:(NSUInteger)anIndex; +- (NSUInteger)numberOfIcons; + +// only declare methods here to shut the compiler up if we can't rearrange +- (FVIcon *)_cachedIconForURL:(NSURL *)aURL; +- (NSSize)_paddingForScale:(CGFloat)scale; +- (void)_recalculateGridSize; +- (void)_getRangeOfRows:(NSRange *)rowRange columns:(NSRange *)columnRange inRect:(NSRect)aRect; +- (void)_showArrowsForIconAtIndex:(NSUInteger)anIndex; +- (void)_hideArrows; +- (BOOL)_hasArrows; +- (void)_cancelActiveDownloads; +- (void)_addDownload:(FVDownload *)fvDownload; +- (void)_invalidateProgressTimer; + +@end + +#pragma mark - + +@implementation FVFileView + ++ (void)initialize +{ + FVINITIALIZE(FVFileView); + + NSMutableDictionary *ta = [NSMutableDictionary dictionary]; + [ta setObject:[NSFont systemFontOfSize:12.0] forKey:NSFontAttributeName]; + [ta setObject:[NSColor darkGrayColor] forKey:NSForegroundColorAttributeName]; + NSMutableParagraphStyle *ps = [[NSParagraphStyle defaultParagraphStyle] mutableCopy]; + // Apple uses this in IKImageBrowserView + [ps setLineBreakMode:NSLineBreakByTruncatingTail]; + [ps setAlignment:NSCenterTextAlignment]; + [ta setObject:ps forKey:NSParagraphStyleAttributeName]; + [ps release]; + _titleAttributes = [ta copy]; + + [ta setObject:[NSColor blackColor] forKey:NSForegroundColorAttributeName]; + _labeledAttributes = [ta copy]; + + [ta setObject:[NSFont systemFontOfSize:10.0] forKey:NSFontAttributeName]; + [ta setObject:[NSColor grayColor] forKey:NSForegroundColorAttributeName]; + _subtitleAttributes = [ta copy]; + + NSLayoutManager *lm = [[NSLayoutManager alloc] init]; + _titleHeight = [lm defaultLineHeightForFont:[_titleAttributes objectForKey:NSFontAttributeName]]; + _subtitleHeight = [lm defaultLineHeightForFont:[_subtitleAttributes objectForKey:NSFontAttributeName]]; + [lm release]; + + CGColorSpaceRef cspace = CGColorSpaceCreateDeviceRGB(); + CGFloat shadowComponents[] = { 0, 0, 0, 0.4 }; + _shadowColor = CGColorCreate(cspace, shadowComponents); + CGColorSpaceRelease(cspace); + + // QTMovie raises if +initialize isn't sent on the AppKit thread + [QTMovie class]; + + // binding an NSSlider in IB 3 results in a crash on 10.4 + [self exposeBinding:@"iconScale"]; + [self exposeBinding:@"autoScales"]; + [self exposeBinding:@"iconURLs"]; + [self exposeBinding:@"selectionIndexes"]; +} + ++ (NSColor *)defaultBackgroundColor +{ + // from Mail.app on 10.4 + CGFloat red = (231.0f/255.0f), green = (237.0f/255.0f), blue = (246.0f/255.0f); + return [[NSColor colorWithCalibratedRed:red green:green blue:blue alpha:1.0] colorUsingColorSpaceName:NSDeviceRGBColorSpace]; +} + ++ (BOOL)accessInstanceVariablesDirectly { return NO; } + +// not part of the API because padding is private, and that's a can of worms +- (CGFloat)_columnWidth { return _iconSize.width + _padding.width; } +- (CGFloat)_rowHeight { return _iconSize.height + _padding.height; } + +- (void)_commonInit { + _iconCache = [[NSMutableDictionary alloc] init]; + _iconSize = DEFAULT_ICON_SIZE; + _autoScales = NO; + _padding = [self _paddingForScale:1.0]; + _lastMouseDownLocInView = NSZeroPoint; + // the next two are set to an illegal combination to indicate that no drop is in progress + _dropIndex = NSNotFound; + _dropOperation = FVDropBefore; + _isRescaling = NO; + _selectedIndexes = [[NSMutableIndexSet alloc] init]; + _lastClickedIndex = NSNotFound; + _rubberBandRect = NSZeroRect; + _isMouseDown = NO; + _iconURLs = nil; + _isEditable = NO; + [self setBackgroundColor:[[self class] defaultBackgroundColor]]; + _selectionOverlay = NULL; + _numberOfColumns = 1; + _numberOfRows = 1; + + CFAllocatorRef alloc = CFAllocatorGetDefault(); + + // I'm not removing the timer in viewWillMoveToSuperview:nil because we may need to free up that memory, and the frequency is so low that it's insignificant overhead + CFAbsoluteTime fireTime = CFAbsoluteTimeGetCurrent() + ZOMBIE_TIMER_INTERVAL; + // runloop will retain this timer, but we'll retain it too and release in -dealloc + _zombieTimer = FVCreateWeakTimerWithTimeInterval(ZOMBIE_TIMER_INTERVAL, fireTime, self, @selector(_zombieTimerFired:)); + CFRunLoopAddTimer(CFRunLoopGetCurrent(), _zombieTimer, kCFRunLoopDefaultMode); + + _lastOrigin = NSZeroPoint; + _timeOfLastOrigin = CFAbsoluteTimeGetCurrent(); + _trackingRectMap = CFDictionaryCreateMutable(alloc, 0, &FVIntegerKeyDictionaryCallBacks, &FVIntegerValueDictionaryCallBacks); + + _iconIndexMap = CFDictionaryCreateMutable(alloc, 0, &FVIntegerKeyDictionaryCallBacks, &kCFTypeDictionaryValueCallBacks); + _iconURLMap = CFDictionaryCreateMutable(alloc, 0, &FVIntegerKeyDictionaryCallBacks, &kCFTypeDictionaryValueCallBacks); + + _leftArrow = [[FVArrowButtonCell alloc] initWithArrowDirection:FVArrowLeft]; + [_leftArrow setTarget:self]; + [_leftArrow setAction:@selector(leftArrowAction:)]; + + _rightArrow = [[FVArrowButtonCell alloc] initWithArrowDirection:FVArrowRight]; + [_rightArrow setTarget:self]; + [_rightArrow setAction:@selector(rightArrowAction:)]; + + _leftArrowFrame = NSZeroRect; + _rightArrowFrame = NSZeroRect; + + // this is created lazily when needed + _sliderWindow = nil; + // always initialize this to -1 + _topSliderTag = -1; + _bottomSliderTag = -1; + + _activeDownloads = NULL; + _progressTimer = NULL; + + _operationQueue = [FVOperationQueue new]; +} + +- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context +{ + if ([keyPath isEqualToString:@"selectionIndexes"]) { + if ([FVPreviewer isPreviewing] && NSNotFound != [_selectedIndexes firstIndex]) { + [FVPreviewer setWebViewContextMenuDelegate:[self delegate]]; + [FVPreviewer previewURL:[self iconURLAtIndex:[_selectedIndexes firstIndex]]]; + } + [self setNeedsDisplay:YES]; + } +} + +#pragma mark NSView overrides + +- (id)initWithFrame:(NSRect)frame { + self = [super initWithFrame:frame]; + [self _commonInit]; + return self; +} + +- (id)initWithCoder:(NSCoder *)coder { + self = [super initWithCoder:coder]; + [self _commonInit]; + return self; +} + +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self]; + [_leftArrow release]; + [_rightArrow release]; + [_iconURLs release]; + CFRelease(_iconIndexMap); + CFRelease(_iconURLMap); + CFRunLoopTimerInvalidate(_zombieTimer); + CFRelease(_zombieTimer); + [_iconCache release]; + [_selectedIndexes release]; + [_backgroundColor release]; + [_sliderWindow release]; + // this variable is accessed in super's dealloc, so set it to NULL + CFRelease(_trackingRectMap); + _trackingRectMap = NULL; + // takes care of the timer as well + if (_activeDownloads != NULL) { + [self _cancelActiveDownloads]; + CFRelease(_activeDownloads); + } + [_operationQueue terminate]; + [_operationQueue release]; + CGLayerRelease(_selectionOverlay); + [super dealloc]; +} + +- (BOOL)isOpaque { return YES; } +- (BOOL)isFlipped { return YES; } + +- (void)setBackgroundColor:(NSColor *)aColor; +{ + if (_backgroundColor != aColor) { + [_backgroundColor release]; + _backgroundColor = [aColor copy]; + } +} + +- (NSColor *)backgroundColor +{ + return _backgroundColor; +} + +#pragma mark API + +- (void)setIconScale:(CGFloat)scale; +{ + if (_autoScales == NO) { + FVAPIAssert(scale > 0, @"scale must be greater than zero"); + _iconSize.width = DEFAULT_ICON_SIZE.width * scale; + _iconSize.height = DEFAULT_ICON_SIZE.height * scale; + + // arrows out of place now, they will be added again when required when resetting the tracking rects + [self _hideArrows]; + + CGLayerRelease(_selectionOverlay); + _selectionOverlay = NULL; + + // the full view will likely need repainting, this also recalculates the grid + [self reloadIcons]; + + // Schedule a reload so we always have the correct quality icons, but don't do it while scaling in response to a slider. + // This will also scroll to the first selected icon; maintaining scroll position while scaling is too jerky. + if (NO == _isRescaling) { + _isRescaling = YES; + // this is only sent in the default runloop mode, so it's not sent during event tracking + [self performSelector:@selector(_rescaleComplete) withObject:nil afterDelay:0.0]; + } + } +} + +- (CGFloat)iconScale; +{ + return _iconSize.width / DEFAULT_ICON_SIZE.width; +} + +- (CGFloat)_autoScaleIconScale; +{ + return _iconSize.width / DEFAULT_ICON_SIZE.width; +} + +- (void)_registerForDraggedTypes +{ + if (_isEditable && _dataSource) { + const SEL selectors[] = + { + @selector(fileView:insertURLs:atIndexes:forDrop:dropOperation:), + @selector(fileView:replaceURLsAtIndexes:withURLs:forDrop:dropOperation:), + @selector(fileView:moveURLsAtIndexes:toIndex:forDrop:dropOperation:), + @selector(fileView:deleteURLsAtIndexes:) + }; + NSUInteger i, iMax = sizeof(selectors) / sizeof(SEL); + for (i = 0; i < iMax; i++) + FVAPIAssert1([_dataSource respondsToSelector:selectors[i]], @"datasource must implement %@", NSStringFromSelector(selectors[i])); + + [self registerForDraggedTypes:[NSArray arrayWithObjects:NSFilenamesPboardType, NSURLPboardType, FVWeblocFilePboardType, (NSString *)kUTTypeURL, (NSString *)kUTTypeUTF8PlainText, NSStringPboardType, nil]]; + } else { + [self registerForDraggedTypes:nil]; + } +} + +- (BOOL)autoScales { + return _autoScales; +} + +- (void)setAutoScales:(BOOL)flag { + if (_autoScales != flag) { + _autoScales = flag; + + // arrows out of place now, they will be added again when required when resetting the tracking rects + [self _hideArrows]; + + // the full view will likely need repainting, this also recalculates the grid + [self reloadIcons]; + } +} + +- (void)awakeFromNib +{ + if ([[FVFileView superclass] instancesRespondToSelector:@selector(awakeFromNib)]) + [super awakeFromNib]; + // if the datasource connection is made in the nib, the drag type setup doesn't get done + [self _registerForDraggedTypes]; +} + +- (void)setDataSource:(id)obj; +{ + if (obj) { + FVAPIAssert1([obj respondsToSelector:@selector(numberOfURLsInFileView:)], @"datasource must implement %@", NSStringFromSelector(@selector(numberOfURLsInFileView:))); + FVAPIAssert1([obj respondsToSelector:@selector(fileView:URLAtIndex:)], @"datasource must implement %@", NSStringFromSelector(@selector(fileView:URLAtIndex:))); + } + _dataSource = obj; + // convenient time to do this, although the timer would also handle it + [_iconCache removeAllObjects]; + CFDictionaryRemoveAllValues(_iconIndexMap); + CFDictionaryRemoveAllValues(_iconURLMap); + + // make sure these get cleaned up; if the datasource is now nil, we're probably going to deallocate soon + [self _cancelActiveDownloads]; + [_operationQueue cancel]; + _padding = [self _paddingForScale:[self iconScale]]; + + [self _registerForDraggedTypes]; +} + +- (id)dataSource { return _dataSource; } + +- (BOOL)isEditable +{ + return _isEditable; +} + +- (void)setEditable:(BOOL)flag +{ + if (_isEditable != flag) { + _isEditable = flag; + + [self _registerForDraggedTypes]; + } +} + +- (BOOL)allowsDownloading +{ + return _activeDownloads != NULL; +} + +- (void)setAllowsDownloading:(BOOL)flag +{ + if (flag && _activeDownloads == NULL) { + _activeDownloads = CFDictionaryCreateMutable(NULL, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); + } else if (flag == NO && _activeDownloads != NULL) { + [self _cancelActiveDownloads]; + CFRelease(_activeDownloads); + _activeDownloads = NULL; + } +} + +- (void)setDelegate:(id)obj; +{ + _delegate = obj; +} + +- (id)delegate { return _delegate; } + +// overall borders around the view +- (CGFloat)_leftMargin { return _padding.width / 2 + DEFAULT_MARGIN; } +- (CGFloat)_rightMargin { return _padding.width / 2 + DEFAULT_MARGIN; } +- (CGFloat)_topMargin { return _titleHeight; } +- (CGFloat)_bottomMargin { return 0.0; } + +- (NSUInteger)numberOfRows; +{ + return _numberOfRows; +} + +- (NSUInteger)numberOfColumns; +{ + return _numberOfColumns; +} + +- (NSSize)_paddingForScale:(CGFloat)scale; +{ + // ??? magic number here... using a fixed padding looked funny at some sizes, so this is now adjustable + NSSize size = NSZeroSize; + + // if we autoscale, we should always derive the scale from the current bounds, but rather the current bounds. This calculation basically inverts the calculation in _recalculateGridSize + size.width = DEFAULT_PADDING.width + FVRound(4.0 * scale); + size.height = size.width + DEFAULT_PADDING.height - DEFAULT_PADDING.width + _titleHeight; + if ([_dataSource respondsToSelector:@selector(fileView:subtitleAtIndex:)]) + size.height += _subtitleHeight; + return size; +} + +- (FVSliderWindow *)_sliderWindow { + if (_sliderWindow == nil) { + _sliderWindow = [[FVSliderWindow alloc] init]; + FVSlider *slider = [_sliderWindow slider]; + // binding & unbinding is handled in viewWillMoveToSuperview: + [slider setMaxValue:16.0]; + [slider setMinValue:0.5]; + if ([self superview]) + [[_sliderWindow slider] bind:@"value" toObject:self withKeyPath:@"iconScale" options:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleSliderMouseExited:) name:FVSliderMouseExitedNotificationName object:slider]; + } + return _sliderWindow; +} + +#define MIN_SLIDER_WIDTH 50.0 +#define MAX_SLIDER_WIDTH 200.0 +#define SLIDER_HEIGHT 15.0 +#define TOP_SLIDER_OFFSET 1.0 +#define BOTTOM_SLIDER_OFFSET 19.0 + +- (NSRect)_topSliderRect +{ + NSRect r = [self visibleRect]; + CGFloat l = FVFloor( NSMidX(r) - FVMax( MIN_SLIDER_WIDTH / 2, FVMin( MAX_SLIDER_WIDTH / 2, NSWidth(r) / 5 ) ) ); + r.origin.x += l; + r.origin.y += TOP_SLIDER_OFFSET; + r.size.width -= 2 * l; + r.size.height = SLIDER_HEIGHT; + return r; +} + +- (NSRect)_bottomSliderRect +{ + NSRect r = [self visibleRect]; + CGFloat l = FVFloor( NSMidX(r) - FVMax( MIN_SLIDER_WIDTH / 2, FVMin( MAX_SLIDER_WIDTH / 2, NSWidth(r) / 5 ) ) ); + r.origin.x += l; + r.origin.y += NSHeight(r) - BOTTOM_SLIDER_OFFSET; + r.size.width -= 2 * l; + r.size.height = SLIDER_HEIGHT; + return r; +} + +// This is the square rect the icon is drawn in. It doesn't include padding, so rects aren't contiguous. +// Caller is responsible for any centering before drawing. +- (NSRect)_rectOfIconInRow:(NSUInteger)row column:(NSUInteger)column; +{ + NSPoint origin = [self bounds].origin; + CGFloat leftEdge = origin.x + [self _leftMargin] + [self _columnWidth] * column; + CGFloat topEdge = origin.y + [self _topMargin] + [self _rowHeight] * row; + return NSMakeRect(leftEdge, topEdge, _iconSize.width, _iconSize.height); +} + +- (NSRect)_rectOfTextForIconRect:(NSRect)iconRect; +{ + NSRect textRect = NSMakeRect(NSMinX(iconRect), NSMaxY(iconRect), NSWidth(iconRect), _padding.height); + // allow the text rect to extend outside the grid cell + return NSInsetRect(textRect, -_padding.width / 3.0, 2.0); +} + +- (void)_setNeedsDisplayForIconInRow:(NSUInteger)row column:(NSUInteger)column { + NSRect dirtyRect = [self _rectOfIconInRow:row column:column]; + dirtyRect = NSUnionRect(NSInsetRect(dirtyRect, -2.0 * [self iconScale], -[self iconScale]), [self _rectOfTextForIconRect:dirtyRect]); + [self setNeedsDisplayInRect:dirtyRect]; +} + +static void _removeTrackingRectTagFromView(const void *key, const void *value, void *context) +{ + [(NSView *)context removeTrackingRect:(NSTrackingRectTag)key]; +} + +- (void)_removeAllTrackingRects +{ + if (_trackingRectMap) { + CFDictionaryApplyFunction(_trackingRectMap, _removeTrackingRectTagFromView, self); + CFDictionaryRemoveAllValues(_trackingRectMap); + } + if (-1 != _topSliderTag) + [self removeTrackingRect:_topSliderTag]; + if (-1 != _bottomSliderTag) + [self removeTrackingRect:_bottomSliderTag]; +} + +// We assume that all existing tracking rects and tooltips have been removed prior to invoking this method, so don't call it directly. Use -[NSWindow invalidateCursorRectsForView:] instead. +- (void)_resetTrackingRectsAndToolTips +{ + // no guarantee that we have a window, in which case these will all be wrong + if (nil != [self window]) { + NSRect visibleRect = [self visibleRect]; + NSUInteger r, rMin = 0, rMax = [self numberOfRows]; + NSUInteger c, cMin = 0, cMax = [self numberOfColumns]; + NSUInteger i, iMin = 0, iMax = [self numberOfIcons]; + NSPoint mouseLoc = [self convertPoint:[[self window] mouseLocationOutsideOfEventStream] fromView:nil]; + NSUInteger mouseIndex = NSNotFound; + + for (r = rMin, i = iMin; r < rMax; r++) + { + for (c = cMin; c < cMax && i < iMax; c++, i++) + { + NSRect iconRect = NSIntersectionRect(visibleRect, [self _rectOfIconInRow:r column:c]); + + if (NSIsEmptyRect(iconRect) == NO) { + BOOL mouseInside = NSPointInRect(mouseLoc, iconRect); + + if (mouseInside) + mouseIndex = i; + + // Getting the location from the mouseEntered: event isn't reliable if you move the mouse slowly, so we either need to enlarge this tracking rect, or keep a map table of tag->index. Since we have to keep a set of tags anyway, we'll use the latter method. + NSTrackingRectTag tag = [self addTrackingRect:iconRect owner:self userData:NULL assumeInside:mouseInside]; + CFDictionarySetValue(_trackingRectMap, (const void *)tag, (const void *)i); + + // don't pass the URL as owner, as it's not retained; use the delegate method instead + [self addToolTipRect:iconRect owner:self userData:NULL]; + } + } + } + + FVIcon *anIcon = mouseIndex == NSNotFound ? nil : [self _cachedIconForURL:[self iconURLAtIndex:mouseIndex]]; + if ([anIcon pageCount] > 1) + [self _showArrowsForIconAtIndex:mouseIndex]; + else + [self _hideArrows]; + + if (_autoScales == NO) { + NSRect sliderRect = NSIntersectionRect([self _topSliderRect], visibleRect); + _topSliderTag = [self addTrackingRect:sliderRect owner:self userData:[self _sliderWindow] assumeInside:NSPointInRect(mouseLoc, sliderRect)]; + sliderRect = NSIntersectionRect([self _bottomSliderRect], visibleRect); + _bottomSliderTag = [self addTrackingRect:sliderRect owner:self userData:[self _sliderWindow] assumeInside:NSPointInRect(mouseLoc, sliderRect)]; + } + } +} + +// Here again, use -[NSWindow invalidateCursorRectsForView:] instead of calling this directly. +- (void)_discardTrackingRectsAndToolTips +{ + [self _removeAllTrackingRects]; + [self removeAllToolTips]; +} + +/* + 10.4 docs say "You need never invoke this method directly; it's invoked automatically before the receiver's cursor rectangles are reestablished using resetCursorRects." + 10.5 docs say "You need never invoke this method directly; neither is it typically invoked during the invalidation of cursor rectangles. [...] This method is invoked just before the receiver is removed from a window and when the receiver is deallocated." + + This is a pretty radical change that makes -discardCursorRects sound pretty useless. Maybe that explains why cursor rects have always sucked in Apple's apps and views? Anyway, I'm explicitly discarding before resetting, just to be safe. I'm also telling the window to invalidate cursor rects for this view explicitly whenever the grid changes due to number of icons or resize. Even though I don't use cursor rects right now, this is a convenient funnel point for tracking rect handling. + + It is important to note that discardCursorRects /has/ to be safe during dealloc (hence the _trackingRectMap is explicitly set to NULL). + + */ +- (void)discardCursorRects +{ + [super discardCursorRects]; + [self _discardTrackingRectsAndToolTips]; +} + +// automatically invoked as needed after -[NSWindow invalidateCursorRectsForView:] +- (void)resetCursorRects +{ + [super resetCursorRects]; + [self _discardTrackingRectsAndToolTips]; + [self _resetTrackingRectsAndToolTips]; +} + +- (void)_rebuildIconIndexMap +{ + CFDictionaryRemoveAllValues(_iconIndexMap); + CFDictionaryRemoveAllValues(_iconURLMap); + + // -[FVFileView _cachedIconForURL:] + id (*cachedIcon)(id, SEL, id); + cachedIcon = (id (*)(id, SEL, id))[self methodForSelector:@selector(_cachedIconForURL:)]; + + // -[FVFileView iconURLAtIndex:] + id (*iconURLAtIndex)(id, SEL, NSUInteger); + iconURLAtIndex = (id (*)(id, SEL, NSUInteger))[self methodForSelector:@selector(iconURLAtIndex:)]; + + NSUInteger i, iMax = [self numberOfIcons]; + + for (i = 0; i < iMax; i++) { + NSURL *aURL = iconURLAtIndex(self, @selector(iconURLAtIndex:), i); + FVIcon *icon = cachedIcon(self, @selector(_cachedIconForURL:), aURL); + NSParameterAssert(nil != icon); + CFDictionarySetValue(_iconIndexMap, (const void *)i, (const void *)icon); + CFDictionarySetValue(_iconURLMap, (const void *)i, (const void *)aURL ?: (const void *)[NSNull null]); + } +} + +- (void)reloadIcons; +{ + // Problem exposed in BibDesk: select all, scroll halfway down in file pane, then change selection to a single row. FVFileView content didn't update correctly, even though reloadIcons was called. Logging drawRect: indicated that the wrong region was being updated, but calling _recalculateGridSize here fixed it. + [self _recalculateGridSize]; + [self _rebuildIconIndexMap]; + + // grid may have changed, so do a full redisplay + [self setNeedsDisplay:YES]; + + /* + Any time the number of icons or scale changes, cursor rects are garbage and need to be reset. The approved way to do this is by calling invalidateCursorRectsForView:, and the docs say to never invoke -[NSView resetCursorRects] manually. Unfortunately, tracking rects are still active even though the window isn't key, and we show buttons for non-key windows. As a consequence, if the number of icons just changed from (say) 3 to 1 in a non-key view, it can receive mouseEntered: events for the now-missing icons. Possibly we don't need to reset cursor rects since they only change for the key window, but we'll reset everything manually just in case. Allow NSWindow to handle it if the window is key. + */ + NSWindow *window = [self window]; + [window invalidateCursorRectsForView:self]; + if ([window isKeyWindow] == NO) + [self resetCursorRects]; +} + +#pragma mark Binding support + +- (void)bind:(NSString *)binding toObject:(id)observable withKeyPath:(NSString *)keyPath options:(NSDictionary *)options; +{ + [super bind:binding toObject:observable withKeyPath:keyPath options:options]; + if ([binding isEqualToString:@"iconScale"] || [binding isEqualToString:@"autoScales"] || [binding isEqualToString:@"iconURLs"]) { + [self reloadIcons]; + } +} + + +- (void)_handleSuperviewDidResize:(NSNotification *)notification { + NSScrollView *scrollView = [self enclosingScrollView]; + if ((scrollView && [[notification object] isEqual:[self superview]]) || (scrollView == nil && [[notification object] isEqual:self])) + [self _recalculateGridSize]; +} + +- (void)viewWillMoveToSuperview:(NSView *)newSuperview +{ + [super viewWillMoveToSuperview:newSuperview]; + + // mmalc's example unbinds here for a nil superview, but that causes problems if you remove the view and add it back in later (and also can cause crashes as a side effect, if we're not careful with the datasource) + if (nil == newSuperview) { + [self removeObserver:self forKeyPath:@"selectionIndexes"]; + [[NSNotificationCenter defaultCenter] removeObserver:self name:FVWebIconUpdatedNotificationName object:nil]; + + // break a retain cycle; binding is retaining this view + [[_sliderWindow slider] unbind:@"value"]; + } + else { + [self addObserver:self forKeyPath:@"selectionIndexes" options:0 context:NULL]; + + // bind here (noop if we don't have a slider) + [[_sliderWindow slider] bind:@"value" toObject:self withKeyPath:@"iconScale" options:nil]; + + // special case; see FVWebViewIcon for posting and comments + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(_handleWebIconNotification:) + name:FVWebIconUpdatedNotificationName object:nil]; + } + + [[NSNotificationCenter defaultCenter] removeObserver:self name:NSViewFrameDidChangeNotification object:nil]; + [[NSNotificationCenter defaultCenter] removeObserver:self name:NSViewBoundsDidChangeNotification object:nil]; +} + +- (void)viewDidMoveToSuperview { + NSView *superview = [self superview]; + NSView *observedView = [self enclosingScrollView] ? superview : self; + + // this can be send in a dealloc when the view hierarchy is decomposed + if (superview) { + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_handleSuperviewDidResize:) name:NSViewFrameDidChangeNotification object:observedView]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_handleSuperviewDidResize:) name:NSViewBoundsDidChangeNotification object:observedView]; + + [self _recalculateGridSize]; + [[self window] invalidateCursorRectsForView:self]; + if ([self window] && [[self window] isKeyWindow] == NO) + [self resetCursorRects]; + } +} + +- (void)unbind:(NSString *)binding +{ + [super unbind:binding]; + if ([binding isEqualToString:@"iconScale"] || [binding isEqualToString:@"autoScales"] || [binding isEqualToString:@"iconURLs"]) { + [self reloadIcons]; + } +} + +- (void)setIconURLs:(NSArray *)anArray; +{ + [_iconURLs autorelease]; + _iconURLs = [anArray copy]; + [self _cancelActiveDownloads]; + // datasource methods all trigger a redisplay, so we have to do the same here + [self reloadIcons]; +} + +- (NSArray *)iconURLs; +{ + return _iconURLs; +} + +- (void)setSelectionIndexes:(NSIndexSet *)indexSet; +{ + FVAPIAssert(nil != indexSet, @"index set must not be nil"); + [_selectedIndexes autorelease]; + _selectedIndexes = [indexSet mutableCopy]; + NSAccessibilityPostNotification(NSAccessibilityUnignoredAncestor(self), NSAccessibilityFocusedUIElementChangedNotification); +} + +- (NSIndexSet *)selectionIndexes; +{ + return [[_selectedIndexes copy] autorelease]; +} + +#pragma mark Binding/datasource wrappers + +// following two methods are for binding compatibility with the datasource methods + +// this returns nil when the datasource or bound array returns NSNull, or else we end up with exceptions everywhere +- (NSURL *)iconURLAtIndex:(NSUInteger)anIndex +{ + NSParameterAssert(anIndex < [self numberOfIcons]); + NSURL *aURL = [[self iconURLs] objectAtIndex:anIndex]; + if (nil == aURL) + aURL = [_dataSource fileView:self URLAtIndex:anIndex]; + if (nil == aURL || [NSNull null] == (id)aURL) + aURL = [FVIcon missingFileURL]; + return aURL; +} + +- (NSUInteger)numberOfIcons +{ + return nil == _iconURLs ? [_dataSource numberOfURLsInFileView:self] : [_iconURLs count]; +} + +- (FVIcon *)_cachedIconForURL:(NSURL *)aURL; +{ + // datasource returns nil for nonexistent paths, so cache that in the dictionary as a normal key + if (nil == aURL) + aURL = (id)[NSNull null]; + + // we don't cache paths, but we do cache icons + FVIcon *icon = [_iconCache objectForKey:aURL]; + if (nil == icon) { + icon = [FVIcon iconWithURL:aURL size:_iconSize]; + [_iconCache setObject:icon forKey:aURL]; + } + return icon; +} + +// use this instead of iterating _cachedIconForURL: when you want more than a few icons, since it may fetch in bulk +- (NSArray *)iconsAtIndexes:(NSIndexSet *)indexes +{ + // I was using [_iconCache objectsForKeys:notFoundMarker], but that assumed that _iconCache was fully populated (and it's filled lazily). Likewise, using -iconURLs directly causes problems since it may contain NSNull, so there's really no way to get icons in bulk here. + NSMutableArray *icons = [NSMutableArray arrayWithCapacity:[indexes count]]; + + // -[NSMutableArray addObject:] + void (*addObject)(id, SEL, id); + addObject = (void (*)(id, SEL, id))[icons methodForSelector:@selector(addObject:)]; + + NSUInteger buffer[512]; + NSRange range = NSMakeRange([indexes firstIndex], [indexes lastIndex] - [indexes firstIndex] + 1); + NSUInteger i, iMax; + + // ??? why isn't this created initially when bindings are used? + if (0 == CFDictionaryGetCount(_iconIndexMap)) + [self _rebuildIconIndexMap]; + + NSParameterAssert(CFDictionaryGetCount(_iconIndexMap) == (CFIndex)[self numberOfIcons]); + + while ((iMax = [indexes getIndexes:buffer maxCount:sizeof(buffer)/sizeof(NSUInteger) inIndexRange:&range]) > 0) { + + for (i = 0; i < iMax; i++) { + NSUInteger indexInView = buffer[i]; + FVIcon *icon = (id)CFDictionaryGetValue(_iconIndexMap, (const void *)indexInView); + NSParameterAssert(nil != icon); + addObject(icons, @selector(addObject:), icon); + } + } + + return icons; +} + +- (NSArray *)_selectedURLs +{ + NSMutableArray *array = [NSMutableArray array]; + NSUInteger idx = [_selectedIndexes firstIndex]; + while (NSNotFound != idx) { + [array addObject:[self iconURLAtIndex:idx]]; + idx = [_selectedIndexes indexGreaterThanIndex:idx]; + } + return array; +} + +#pragma mark Drawing layout + +- (void)_recalculateGridSize +{ + NSScrollView *scrollView = [self enclosingScrollView]; + NSSize contentSize = scrollView ? [scrollView contentSize] : [self bounds].size; + NSUInteger numIcons = [self numberOfIcons]; + + if (_autoScales) { + + CGFloat iconScale = FVMax( 0.1, ( contentSize.width - DEFAULT_PADDING.width - 2 * DEFAULT_MARGIN ) / DEFAULT_ICON_SIZE.width ); + _padding = [self _paddingForScale:iconScale]; + + _numberOfColumns = 1; + _numberOfRows = numIcons; + + iconScale = FVMax( 0.1, ( contentSize.width - [self _leftMargin] - [self _rightMargin] ) / DEFAULT_ICON_SIZE.width ); + _iconSize = NSMakeSize(iconScale * DEFAULT_ICON_SIZE.width, iconScale * DEFAULT_ICON_SIZE.height); + + CGLayerRelease(_selectionOverlay); + _selectionOverlay = NULL; + + } else { + + _padding = [self _paddingForScale:[self iconScale]]; + + _numberOfColumns = MAX( 1, (NSInteger)FVFloor( ( contentSize.width - [self _leftMargin] - [self _rightMargin] + _padding.width ) / [self _columnWidth] ) ); + _numberOfRows = ( [self numberOfIcons] + _numberOfColumns - 1 ) / _numberOfColumns; + } + + if (scrollView) { + NSRect frame = { NSZeroPoint, contentSize }; + frame.size.width = FVMax( FVCeil( [self _columnWidth] * _numberOfColumns - _padding.width + [self _leftMargin] + [self _rightMargin] ), contentSize.width ); + frame.size.height = FVMax( FVCeil( [self _rowHeight] * _numberOfRows + [self _topMargin] + [self _bottomMargin] ), contentSize.height ); + if (NSEqualRects([self frame], frame) == NO) { + [super setFrame:frame]; + if (_autoScales && [scrollView autohidesScrollers] && FVAbs(NSHeight(frame) - contentSize.height) <= [NSScroller scrollerWidth]) + [scrollView tile]; + } + } + + // ??? move to -reloadIcons + NSUInteger lastSelIndex = [_selectedIndexes lastIndex]; + if (lastSelIndex != NSNotFound && lastSelIndex >= numIcons) { + NSMutableIndexSet *tmpIndexes = [_selectedIndexes mutableCopy]; + [tmpIndexes removeIndexesInRange:NSMakeRange(numIcons, lastSelIndex + 1 - numIcons)]; + [self setSelectionIndexes:tmpIndexes]; + [tmpIndexes release]; + } +} + +- (NSUInteger)_indexForGridRow:(NSUInteger)rowIndex column:(NSUInteger)colIndex; +{ + // nc * (r-1) + c + // assumes all slots are filled, so check numberOfIcons before returning a value + NSUInteger fileIndex = rowIndex * [self numberOfColumns] + colIndex; + return fileIndex >= [self numberOfIcons] ? NSNotFound : fileIndex; +} + +- (BOOL)_getGridRow:(NSUInteger *)rowIndex column:(NSUInteger *)colIndex ofIndex:(NSUInteger)anIndex; +{ + NSUInteger cMax = [self numberOfColumns], rMax = [self numberOfRows]; + + if (0 == cMax || 0 == rMax) + return NO; + + // initialize all of these, in case we don't make it to the inner loop + NSUInteger r, c = 0, i = 0; + + // iterate columns within each row + for (r = 0; r < rMax && i <= anIndex; r++) + { + for (c = 0; c < cMax && i <= anIndex; c++) + { + i++; + } + } + + // grid row/index are zero based + r--; + c--; + + if (i <= [self numberOfIcons]) { + if (NULL != rowIndex) + *rowIndex = r; + if (NULL != colIndex) + *colIndex = c; + return YES; + } + return NO; +} + +// this is only used for hit testing, so we should ignore padding +- (BOOL)_getGridRow:(NSUInteger *)rowIndex column:(NSUInteger *)colIndex atPoint:(NSPoint)point; +{ + // check for this immediately + if (point.x <= [self _leftMargin] || point.y <= [self _topMargin]) + return NO; + + // column width is padding + icon width + // row height is padding + icon width + NSUInteger idx, nc = [self numberOfColumns], nr = [self numberOfRows]; + + idx = 0; + CGFloat start; + + while (idx < nc) { + + start = [self _leftMargin] + (_iconSize.width + _padding.width) * idx; + if (start < point.x && point.x < (start + _iconSize.width)) + break; + idx++; + + if (idx == nc) + return NO; + } + + if (colIndex) + *colIndex = idx; + + idx = 0; + + while (idx < nr) { + + start = [self _topMargin] + (_iconSize.height + _padding.height) * idx; + if (start < point.y && point.y < (start + _iconSize.height)) + break; + idx++; + + if (idx == nr) + return NO; + } + + if (rowIndex) + *rowIndex = idx; + + return YES; +} + +#pragma mark Cache thread + +- (void)_rescaleComplete; +{ + NSUInteger scrollIndex = [_selectedIndexes firstIndex]; + if (NSNotFound != scrollIndex) { + NSUInteger r, c; + [self _getGridRow:&r column:&c ofIndex:scrollIndex]; + // this won't necessarily trigger setNeedsDisplay:, which we need unconditionally + [self scrollRectToVisible:[self _rectOfIconInRow:r column:c]]; + } + [self setNeedsDisplay:YES]; + _isRescaling = NO; +} + +- (void)_enqueueReleaseOperationForIcons:(NSArray *)icons; +{ + NSUInteger i, iMax = [icons count]; + NSMutableArray *operations = [[NSMutableArray alloc] initWithCapacity:iMax]; + FVIcon *icon; + for (i = 0; i < iMax; i++) { + icon = [icons objectAtIndex:i]; + if ([icon canReleaseResources]) { + FVReleaseOperation *op = [[FVReleaseOperation alloc] initWithIcon:icon view:nil]; + [op setQueuePriority:FVOperationQueuePriorityLow]; + [operations addObject:op]; + [op release]; + } + } + if ([operations count]) + [_operationQueue addOperations:operations]; + [operations release]; +} + +- (void)_enqueueRenderOperationForIcons:(NSArray *)icons withPriority:(FVOperationQueuePriority)priority; +{ + NSUInteger i, iMax = [icons count]; + NSMutableArray *operations = [[NSMutableArray alloc] initWithCapacity:iMax]; + FVIcon *icon; + for (i = 0; i < iMax; i++) { + icon = [icons objectAtIndex:i]; + if ([icon needsRenderForSize:_iconSize]) { + FVRenderOperation *op = [[FVRenderOperation alloc] initWithIcon:icon view:self]; + [op setQueuePriority:priority]; + [operations addObject:op]; + [op release]; + } + } + if ([operations count]) + [_operationQueue addOperations:operations]; + [operations release]; +} + +- (void)iconUpdated:(FVIcon *)updatedIcon; +{ + // Only iterate icons in the visible range, since we know the overall geometry + NSRange rowRange, columnRange; + [self _getRangeOfRows:&rowRange columns:&columnRange inRect:[self visibleRect]]; + + NSUInteger iMin, iMax = [self numberOfIcons]; + + // _indexForGridRow:column: returns NSNotFound if we're in a short row (empty column) + iMin = [self _indexForGridRow:rowRange.location column:columnRange.location]; + if (NSNotFound == iMin) + iMin = [self numberOfIcons]; + else + iMax = MIN([self numberOfIcons], iMin + rowRange.length * [self numberOfColumns]); + + NSUInteger i; + + // If an icon isn't visible, there's no need to redisplay anything. Similarly, if 20 icons are displayed and only 5 updated, there's no need to redraw all 20. Geometry calculations are much faster than redrawing, in general. + for (i = iMin; i < iMax; i++) { + + FVIcon *anIcon = (id)CFDictionaryGetValue(_iconIndexMap, (const void *)i); + if (anIcon == updatedIcon) { + NSUInteger r, c; + if ([self _getGridRow:&r column:&c ofIndex:i]) + [self _setNeedsDisplayForIconInRow:r column:c]; + } + } +} + +// drawRect: uses -releaseResources on icons that aren't visible but present in the datasource, so we just need a way to cull icons that are cached but not currently in the datasource +- (void)_zombieTimerFired:(CFRunLoopTimerRef)timer +{ + NSUInteger i, iMax = [self numberOfIcons]; + + // don't do anything unless there's a meaningful discrepancy between the number of items reported by the datasource and our cache + if ((iMax + ZOMBIE_CACHE_THRESHOLD) < [_iconCache count]) { + + NSMutableSet *iconURLsToKeep = [NSMutableSet set]; + for (i = 0; i < iMax; i++) { + NSURL *aURL = [self iconURLAtIndex:i]; + if (aURL) [iconURLsToKeep addObject:aURL]; + } + + NSMutableSet *toRemove = [NSMutableSet setWithArray:[_iconCache allKeys]]; + [toRemove minusSet:iconURLsToKeep]; + + // anything remaining in toRemove is not present in the dataSource, so remove it from the cache + NSEnumerator *keyEnum = [toRemove objectEnumerator]; + NSURL *aURL; + while ((aURL = [keyEnum nextObject])) + [_iconCache removeObjectForKey:aURL]; + } +} + +- (void)_handleWebIconNotification:(NSNotification *)aNote +{ + [self iconUpdated:[aNote object]]; +} + +#pragma mark Drawing + +// no save/restore needed because of when these are called in -drawRect: (this is why they're private) + +- (void)_drawDropHighlight; +{ + CGFloat lineWidth = 2.0; + NSBezierPath *p; + NSUInteger r, c; + NSRect aRect = NSZeroRect; + + switch (_dropOperation) { + case FVDropOn: + if (_dropIndex == NSNotFound) { + aRect = [self visibleRect]; + } else { + [self _getGridRow:&r column:&c ofIndex:_dropIndex]; + aRect = [self _rectOfIconInRow:r column:c]; + } + break; + case FVDropBefore: + [self _getGridRow:&r column:&c ofIndex:_dropIndex]; + aRect = [self _rectOfIconInRow:r column:c]; + // aRect size is 6, and should be centered between icons horizontally + aRect.origin.x -= _padding.width / 2 + 3.0; + aRect.size.width = 6.0; + break; + case FVDropAfter: + [self _getGridRow:&r column:&c ofIndex:_dropIndex]; + aRect = [self _rectOfIconInRow:r column:c]; + // aRect size is 6, and should be centered between icons horizontally + aRect.origin.x += _iconSize.width + _padding.width / 2 - 3.0; + aRect.size.width = 6.0; + break; + default: + break; + } + + if (NSIsEmptyRect(aRect) == NO) { + aRect = [self centerScanRect:aRect]; + + [[[NSColor alternateSelectedControlColor] colorWithAlphaComponent:0.2] setFill]; + [[[NSColor alternateSelectedControlColor] colorWithAlphaComponent:0.8] setStroke]; + + if (_dropOperation == FVDropOn) { + // it's either a drop on the whole table or on top of a cell + p = [NSBezierPath fv_bezierPathWithRoundRect:NSInsetRect(aRect, 0.5 * lineWidth, 0.5 * lineWidth) xRadius:7 yRadius:7]; + [p fill]; + } + else { + // similar to NSTableView's between-row drop indicator + CGFloat radius = NSWidth(aRect) / 2; + NSPoint point = NSMakePoint(NSMidX(aRect), NSMaxY(aRect)); + p = [NSBezierPath bezierPath]; + [p appendBezierPathWithArcWithCenter:point radius:radius startAngle:-90 endAngle:270]; + point.y = NSMinY(aRect); + [p appendBezierPathWithArcWithCenter:point radius:radius startAngle:90 endAngle:450]; + } + [p setLineWidth:lineWidth]; + [p stroke]; + [p setLineWidth:1.0]; + } +} + +- (void)_drawHighlightInRect:(NSRect)aRect; +{ + CGContextRef drawingContext = [[NSGraphicsContext currentContext] graphicsPort]; + + // drawing into a CGImage and then overlaying it keeps the rubber band highlight much more responsive + if (NULL == _selectionOverlay) { + + _selectionOverlay = CGLayerCreateWithContext(drawingContext, CGSizeMake(NSWidth(aRect), NSHeight(aRect)), NULL); + CGContextRef layerContext = CGLayerGetContext(_selectionOverlay); + NSRect imageRect = NSZeroRect; + CGSize layerSize = CGLayerGetSize(_selectionOverlay); + imageRect.size.height = layerSize.height; + imageRect.size.width = layerSize.width; + CGContextClearRect(layerContext, *(CGRect *)&imageRect); + + [NSGraphicsContext saveGraphicsState]; + NSGraphicsContext *nsContext = [NSGraphicsContext graphicsContextWithGraphicsPort:layerContext flipped:YES]; + [NSGraphicsContext setCurrentContext:nsContext]; + [nsContext saveGraphicsState]; + + NSColor *strokeColor = [[NSColor colorWithCalibratedWhite:1.0 alpha:0.8] colorUsingColorSpaceName:NSDeviceRGBColorSpace]; + NSColor *fillColor = [[NSColor colorWithCalibratedWhite:0.0 alpha:0.2] colorUsingColorSpaceName:NSDeviceRGBColorSpace]; + [strokeColor setStroke]; + [fillColor setFill]; + imageRect = NSInsetRect(imageRect, 1.0, 1.0); + NSBezierPath *p = [NSBezierPath fv_bezierPathWithRoundRect:imageRect xRadius:5 yRadius:5]; + [p setLineWidth:2.0]; + [p fill]; + [p stroke]; + [p setLineWidth:1.0]; + + [nsContext restoreGraphicsState]; + [NSGraphicsContext restoreGraphicsState]; + } + // make sure we use source over for drawing the image + CGContextSaveGState(drawingContext); + CGContextSetBlendMode(drawingContext, kCGBlendModeNormal); + CGContextDrawLayerInRect(drawingContext, *(CGRect *)&aRect, _selectionOverlay); + CGContextRestoreGState(drawingContext); +} + +- (void)_drawRubberbandRect +{ + [[NSColor colorWithCalibratedWhite:1.0 alpha:0.3] setFill]; + NSRect r = [self centerScanRect:NSInsetRect(_rubberBandRect, 0.5, 0.5)]; + NSRectFillUsingOperation(r, NSCompositeSourceOver); + // NSFrameRect doesn't respect setStroke + [[NSColor lightGrayColor] setFill]; + NSFrameRectWithWidth(r, 1.0); +} + +- (void)_drawDropMessage; +{ + NSRect aRect = [self centerScanRect:NSInsetRect([self visibleRect], 20, 20)]; + NSBezierPath *path = [NSBezierPath fv_bezierPathWithRoundRect:aRect xRadius:10 yRadius:10]; + CGFloat pattern[2] = { 12.0, 6.0 }; + + // This sets all future paths to have a dash pattern, and it's not affected by save/restore gstate on Tiger. Lame. + CGFloat previousLineWidth = [path lineWidth]; + // ??? make this a continuous function of width <= 3 + [path setLineWidth:(NSWidth(aRect) > 100 ? 3.0 : 2.0)]; + [path setLineDash:pattern count:2 phase:0.0]; + [[NSColor lightGrayColor] setStroke]; + [path stroke]; + [path setLineWidth:previousLineWidth]; + [path setLineDash:NULL count:0 phase:0.0]; + + NSBundle *bundle = [NSBundle bundleForClass:[FVFileView class]]; + NSString *message = NSLocalizedStringFromTableInBundle(@"Drop Files Here", @"FileView", bundle, @"placeholder message for empty file view"); + NSMutableAttributedString *attrString = [[[NSMutableAttributedString alloc] initWithString:message] autorelease]; + CGFloat fontSize = 24.0; + [attrString addAttribute:NSFontAttributeName value:[NSFont boldSystemFontOfSize:fontSize] range:NSMakeRange(0, [attrString length])]; + [attrString addAttribute:NSForegroundColorAttributeName value:[NSColor lightGrayColor] range:NSMakeRange(0, [attrString length])]; + + NSMutableParagraphStyle *ps = [[NSParagraphStyle defaultParagraphStyle] mutableCopy]; + [ps setAlignment:NSCenterTextAlignment]; + [attrString addAttribute:NSParagraphStyleAttributeName value:ps range:NSMakeRange(0, [attrString length])]; + [ps release]; + + // avoid drawing text right up to the path at really small widths + aRect = NSInsetRect(aRect, NSWidth(aRect) / 10, 0); + + CGFloat singleLineHeight = NSHeight([attrString boundingRectWithSize:aRect.size options:0]); + + // NSLayoutManager'... [truncated message content] |