ATContentController.m
/* |
File: ATContentController.m |
Abstract: The basic controller for the demo app. An instance exists inside the MainMenu.xib file. |
Version: 1.1 |
Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple |
Inc. ("Apple") in consideration of your agreement to the following |
terms, and your use, installation, modification or redistribution of |
this Apple software constitutes acceptance of these terms. If you do |
not agree with these terms, please do not use, install, modify or |
redistribute this Apple software. |
In consideration of your agreement to abide by the following terms, and |
subject to these terms, Apple grants you a personal, non-exclusive |
license, under Apple's copyrights in this original Apple software (the |
"Apple Software"), to use, reproduce, modify and redistribute the Apple |
Software, with or without modifications, in source and/or binary forms; |
provided that if you redistribute the Apple Software in its entirety and |
without modifications, you must retain this notice and the following |
text and disclaimers in all such redistributions of the Apple Software. |
Neither the name, trademarks, service marks or logos of Apple Inc. may |
be used to endorse or promote products derived from the Apple Software |
without specific prior written permission from Apple. Except as |
expressly stated in this notice, no other rights or licenses, express or |
implied, are granted by Apple herein, including but not limited to any |
patent rights that may be infringed by your derivative works or by other |
works in which the Apple Software may be incorporated. |
The Apple Software is provided by Apple on an "AS IS" basis. APPLE |
MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION |
THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS |
FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND |
OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS. |
IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL |
OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, |
MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED |
AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), |
STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE |
POSSIBILITY OF SUCH DAMAGE. |
Copyright (C) 2012 Apple Inc. All Rights Reserved. |
*/ |
#import <QuartzCore/QuartzCore.h> |
#import "ATContentController.h" |
#import "ATImageTextCell.h" |
#import "ATDynamicTableView.h" |
#import "ATColorCell.h" |
#import "ATColorView.h" |
#import "ATFilterBrowserController.h" |
@interface ATContentController(ATPrivate) |
// Private forward declarations |
- (void)_animateImageFromRow:(NSInteger)row; |
@end |
@implementation ATContentController |
- (void)dealloc { |
[_tableContents release]; |
// Stop any observations that we may have |
for (ATDesktopEntity *imageEntity in _observedVisibleItems) { |
if ([imageEntity isKindOfClass:[ATDesktopImageEntity class]]) { |
[imageEntity removeObserver:self forKeyPath:ATEntityPropertyNamedThumbnailImage]; |
} |
} |
[_observedVisibleItems release]; |
[_windowForAnimation release]; |
[_filterBrowserController release]; |
[super dealloc]; |
} |
- (void)awakeFromNib { |
NSURL *url = [NSURL fileURLWithPath:@"/Library/Desktop Pictures" isDirectory:YES]; |
ATDesktopFolderEntity *primaryFolder = [[ATDesktopFolderEntity alloc] initWithFileURL:url]; |
// Create a flat array of ATDesktopFolderEntity and ATDesktopImageEntity objects to display |
_tableContents = [NSMutableArray new]; |
// We first do a pass over the children and add all the images under the "Desktop Pictures" category |
[_tableContents addObject:primaryFolder]; |
for (ATDesktopEntity *entity in primaryFolder.children) { |
if ([entity isKindOfClass:[ATDesktopImageEntity class]]) { |
[_tableContents addObject:entity]; |
} |
} |
// Then do another pass through and add all the folders -- including their children. |
// A recursive loop could be used too, but we want to only go one level deep |
for (ATDesktopEntity *entity in primaryFolder.children) { |
if ([entity isKindOfClass:[ATDesktopFolderEntity class]]) { |
[_tableContents addObject:entity]; |
ATDesktopFolderEntity *subFolder = (ATDesktopFolderEntity *)entity; |
for (ATDesktopEntity *subFolderChildEntity in subFolder.children) { |
if ([subFolderChildEntity isKindOfClass:[ATDesktopImageEntity class]]) { |
[_tableContents addObject:subFolderChildEntity]; |
} |
} |
} |
} |
// Initialize the main image view to our current desktop background. |
NSImage *initialImage = [[NSImage alloc] initByReferencingURL:[[NSWorkspace sharedWorkspace] desktopImageURLForScreen:[NSScreen mainScreen]]]; |
[_imageViewMain setImage:initialImage]; |
[initialImage release]; |
[primaryFolder release]; |
} |
- (ATDesktopEntity *)_entityForRow:(NSInteger)row { |
return (ATDesktopEntity *)[_tableContents objectAtIndex:row]; |
} |
- (ATDesktopImageEntity *)_imageEntityForRow:(NSInteger)row { |
id result = row != -1 ? [_tableContents objectAtIndex:row] : nil; |
if ([result isKindOfClass:[ATDesktopImageEntity class]]) { |
return result; |
} |
return nil; |
} |
// NSTableView delegate and datasource methods |
- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView { |
return _tableContents.count; |
} |
- (id)tableView:(NSTableView *)tableView objectValueForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row { |
return [self _entityForRow:row].title; |
} |
- (void)tableView:(NSTableView *)tableView willDisplayCell:(id)cell forTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row { |
ATDesktopImageEntity *entity = [self _imageEntityForRow:row]; |
if (entity != nil) { |
// Setup the image and fill color |
ATImageTextCell *imageTextCell = (ATImageTextCell *)cell; |
imageTextCell.image = entity.thumbnailImage; |
imageTextCell.fillColor = entity.fillColor; |
imageTextCell.fillColorName = entity.fillColorName; |
} |
} |
// We pre-load the images in batches. We could easily use this as a point to stop loading |
// rows that are no longer visible and don't have the images fully loaded. We use this as |
// an entry point to start/stop watching the image for the visible items to see when it changes. |
// |
- (void)dynamicTableView:(ATDynamicTableView *)tableView changedVisibleRowsFromRange:(NSRange)oldVisibleRows toRange:(NSRange)newVisibleRows { |
// First, stop observing all prior things |
for (ATDesktopEntity *imageEntity in _observedVisibleItems) { |
if ([imageEntity isKindOfClass:[ATDesktopImageEntity class]]) { |
[imageEntity removeObserver:self forKeyPath:ATEntityPropertyNamedThumbnailImage]; |
} |
} |
// Now, observe things that are newly visible and kick off a request to load the image |
[_observedVisibleItems release]; |
_observedVisibleItems = (NSMutableArray *)[[_tableContents subarrayWithRange:newVisibleRows] retain]; |
for (ATDesktopEntity *imageEntity in _observedVisibleItems) { |
if ([imageEntity isKindOfClass:[ATDesktopImageEntity class]]) { |
[(ATDesktopImageEntity *)imageEntity loadImage]; |
[imageEntity addObserver:self forKeyPath:ATEntityPropertyNamedThumbnailImage options:0 context:NULL]; |
} |
} |
} |
- (NSView *)dynamicTableView:(ATDynamicTableView *)tableView viewForRow:(NSInteger)row { |
// Return a spinner for rows that are loading |
NSProgressIndicator *result = nil; |
ATDesktopImageEntity *entity = [self _imageEntityForRow:row]; |
if (entity != nil && entity.thumbnailImage == nil) { |
NSRect cellFrame = [tableView frameOfCellAtColumn:0 row:row]; |
ATImageTextCell *imageCell = (ATImageTextCell *)[tableView preparedCellAtColumn:0 row:row]; |
NSRect imageFrame = [imageCell imageRectForBounds:cellFrame]; |
result = [[[NSProgressIndicator alloc] initWithFrame:imageFrame] autorelease]; |
[result setIndeterminate:YES]; |
[result setStyle:NSProgressIndicatorSpinningStyle]; |
[result setControlSize:NSRegularControlSize]; |
[result sizeToFit]; |
[result startAnimation:nil]; |
NSRect progressFrame = [result frame]; |
// Center it in the image frame |
progressFrame.origin.x = NSMinX(imageFrame) + floor((NSWidth(imageFrame) - NSWidth(progressFrame)) / 2.0); |
progressFrame.origin.y = NSMinY(imageFrame) + floor((NSHeight(imageFrame) - NSHeight(progressFrame)) / 2.0); |
[result setFrame:progressFrame]; |
} |
return result; |
} |
- (void)_reloadRowForEntity:(id)object { |
NSInteger row = [_tableContents indexOfObject:object]; |
if (row != NSNotFound) { |
[_tableViewMain reloadDataForRowIndexes:[NSIndexSet indexSetWithIndex:row] columnIndexes:[NSIndexSet indexSetWithIndex:0]]; |
// Animate if that item is selected and we now have an image for it |
if ([_tableViewMain selectedRow] == row) { |
[self _animateImageFromRow:row]; |
} |
} |
} |
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { |
if (keyPath == ATEntityPropertyNamedThumbnailImage) { |
// Find the row and reload it. |
// Note that KVO notifications may be sent from a background thread |
// (in this case, we know they will be) |
// We should only update the UI on the main thread, and in addition, we use |
// NSRunLoopCommonModes to make sure the UI updates when a modal window is up. |
[self performSelectorOnMainThread:@selector(_reloadRowForEntity:) withObject:object waitUntilDone:NO modes:[NSArray arrayWithObject:NSRunLoopCommonModes]]; |
} |
} |
// We want to make "group rows" for the folders |
- (BOOL)tableView:(NSTableView *)tableView isGroupRow:(NSInteger)row { |
if ([[self _entityForRow:row] isKindOfClass:[ATDesktopFolderEntity class]]) { |
return YES; |
} else { |
return NO; |
} |
} |
// We want a regular text field cell that we setup in the nib for the group rows, and the default one setup for the tablecolumn for all others |
- (NSCell *)tableView:(NSTableView *)tableView dataCellForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row { |
if (tableColumn != nil) { |
if ([[self _entityForRow:row] isKindOfClass:[ATDesktopFolderEntity class]]) { |
// Use a shared cell setup in IB via an IBOutlet |
return _sharedGroupTitleCell; |
} else { |
return [tableColumn dataCell]; |
} |
} else { |
// A nil table column is for a "full width" table column which we don't need (since we only ever have one column) |
return nil; |
} |
} |
// We make the "group rows" have the standard height, while all other image rows have a larger height |
- (CGFloat)tableView:(NSTableView *)tableView heightOfRow:(NSInteger)row { |
if ([[self _entityForRow:row] isKindOfClass:[ATDesktopFolderEntity class]]) { |
return [tableView rowHeight]; |
} else { |
return 75.0; |
} |
} |
- (void)_animationDoneTimerFired:(NSTimer *)timer { |
[_animationDoneTimer release]; |
_animationDoneTimer = nil; |
// Set the normal one to have the final image and alpha value. |
// Set the image and update us before ordering out the animation window. |
[_imageViewMain setImage:[_imageViewForTransition image]]; |
[_imageViewMain setAlphaValue:1.0]; |
// This displays right now, and prevents flicker if the animation window orders |
// out before our display happened |
[_imageViewMain.window displayIfNeeded]; |
// Hide the animation window |
[_windowForAnimation orderOut:nil]; |
} |
- (void)_ensureAnimationWindowCreated { |
if (_windowForAnimation == nil) { |
NSRect contentRect = _imageViewForTransition.frame; |
contentRect.origin = NSZeroPoint; |
_imageViewForTransition.frame = contentRect; |
_windowForAnimation = [[NSWindow alloc] initWithContentRect:contentRect styleMask:NSBorderlessWindowMask backing:NSBackingStoreBuffered defer:NO]; |
[_windowForAnimation setReleasedWhenClosed:NO]; |
[_windowForAnimation setOpaque:NO]; |
[_windowForAnimation setBackgroundColor:[NSColor clearColor]]; |
// For ease of use, we setup the _imageViewForTransition in the nib and add it as a subview |
[[_windowForAnimation contentView] addSubview:_imageViewForTransition]; |
} |
} |
- (NSRect)_screenImageRectForRow:(NSInteger)row { |
// Figure out the location of the cell's image |
NSRect cellFrame = [_tableViewMain frameOfCellAtColumn:0 row:row]; |
// Grab the fully setup cell |
ATImageTextCell *imageTextCell = (ATImageTextCell *)[_tableViewMain preparedCellAtColumn:0 row:row]; |
// As it for its frame |
NSRect cellImageFrame = [imageTextCell imageRectForBounds:cellFrame]; |
// Convert that cell image frame to the right coordinate system |
NSRect initialImageFrame = [_tableViewMain convertRect:cellImageFrame toView:nil]; |
initialImageFrame.origin = [_tableViewMain.window convertBaseToScreen:initialImageFrame.origin]; |
return initialImageFrame; |
} |
- (NSRect)_finalScreenImageRect { |
// We are animating to right over the image view's frame. Convert to the right screen coordinates and animate the window there. |
NSRect finalImageFrame = [_imageViewMain.superview convertRect:_imageViewMain.frame toView:nil]; |
finalImageFrame.origin = [_imageViewMain.window convertBaseToScreen:finalImageFrame.origin]; |
return finalImageFrame; |
} |
- (void)_stopExistingTimerIfNeeded { |
// We want to stop any previous animations |
if (_animationDoneTimer != nil) { |
[_animationDoneTimer invalidate]; |
[_animationDoneTimer release]; |
_animationDoneTimer = nil; |
} |
} |
- (void)_animateImageFromRow:(NSInteger)row { |
[self _stopExistingTimerIfNeeded]; |
// We create a window to do the animation. The purpose of using a window is to allow an |
// animation to happen from a non-layer backed view to over a layer-backed view. |
// We easily could use a sibling view if everything was layer backed, or non-layer backed. |
// |
[self _ensureAnimationWindowCreated]; |
// Grab our model object for this row |
ATDesktopImageEntity *entity = [self _imageEntityForRow:row]; |
// Set some initial state |
NSRect startingWindowFrame = [self _screenImageRectForRow:row]; |
[_windowForAnimation setFrame:startingWindowFrame display:NO]; |
[_imageViewForTransition setImage:entity.thumbnailImage]; |
[_imageViewMain setAlphaValue:1.0]; |
// Bring the window above our existing window |
[_windowForAnimation orderFront:nil]; |
// We want to sync all the animations together. We use a grouping to do that. |
[NSAnimationContext beginGrouping]; |
{ |
NSTimeInterval animationDuration = 0.4; |
// Do a slow animation if the shift key is down |
if (([NSEvent modifierFlags] & NSShiftKeyMask) != 0) { |
animationDuration *= 4; |
} |
[[NSAnimationContext currentContext] setDuration:animationDuration]; |
NSRect finalImageFrame = [self _finalScreenImageRect]; |
[[_windowForAnimation animator] setFrame:finalImageFrame display:YES]; |
// Alpha/opacity animations only work for layer-backed views |
[[_imageViewMain animator] setAlphaValue:0.25]; |
// Also, animate the background color. This is done with the layer. See ATColorView.h/.m |
[[_colorViewMain animator] setBackgroundColor:entity.fillColor]; |
// At the end of the animation we want to do some cleanup. We keep track of the |
// timer so we can stop the operation if we need to. |
_animationDoneTimer = [[NSTimer scheduledTimerWithTimeInterval:animationDuration |
target:self |
selector:@selector(_animationDoneTimerFired:) |
userInfo:nil |
repeats:NO] retain]; |
} |
[NSAnimationContext endGrouping]; |
} |
- (void)_performAnimationFromSelectedRowToImage { |
NSInteger selectedRow = [_tableViewMain selectedRow]; |
// We don't animate the group rows |
if (selectedRow != -1 && ![self tableView:_tableViewMain isGroupRow:selectedRow]) { |
[_tableViewMain scrollRowToVisible:selectedRow]; |
// Only animate if the thumbnail image is loaded |
ATDesktopImageEntity *entity = [_tableContents objectAtIndex:selectedRow]; |
if (entity.thumbnailImage != nil) { |
[self _animateImageFromRow:selectedRow]; |
} |
} else { |
[_imageViewMain setImage:nil]; |
} |
} |
- (void)tableViewSelectionDidChange:(NSNotification *)notification { |
[self _performAnimationFromSelectedRowToImage]; |
} |
- (IBAction)btnSetAsDesktopWallpaperClick:(id)sender { |
NSInteger selectedRow = [_tableViewMain selectedRow]; |
if (selectedRow != -1) { |
ATDesktopEntity *entity = [_tableContents objectAtIndex:selectedRow]; |
if ([entity isKindOfClass:[ATDesktopImageEntity class]]) { |
ATDesktopImageEntity *desktopImageEntity = (ATDesktopImageEntity *)entity; |
NSError *error; |
NSURL *imageURL = desktopImageEntity.fileURL; |
NSColor *fillColor = desktopImageEntity.fillColor; |
NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:fillColor, NSWorkspaceDesktopImageFillColorKey, [NSNumber numberWithBool:NO], NSWorkspaceDesktopImageAllowClippingKey, [NSNumber numberWithInteger:NSImageScaleProportionallyUpOrDown], NSWorkspaceDesktopImageScalingKey, nil]; |
BOOL result = [[NSWorkspace sharedWorkspace] setDesktopImageURL:imageURL forScreen:[[NSScreen screens] lastObject] options:options error:&error]; |
if (!result) { |
[NSApp presentError:error]; |
} |
} |
} |
} |
- (NSIndexSet *)tableView:(NSTableView *)tableView selectionIndexesForProposedSelection:(NSIndexSet *)proposedSelectionIndexes { |
// We don't want to change the selection if the user clicked in the fill color area |
NSInteger row = [tableView clickedRow]; |
if (row != -1 && ![self tableView:tableView isGroupRow:row]) { |
ATImageTextCell *cell = (ATImageTextCell *)[tableView preparedCellAtColumn:0 row:row]; |
// Use the hit testing API with our own special marker to find out what we hit |
NSUInteger hitTest = [cell hitTestForEvent:[NSApp currentEvent] inRect:[tableView frameOfCellAtColumn:0 row:row] ofView:tableView]; |
if ((hitTest & ATCellHitTestColorRect) != 0) { |
// Don't allow the selection change |
return [tableView selectedRowIndexes]; |
} |
} else { |
// If the selection is just a group row, don't let it be selected; return the original selection |
if (proposedSelectionIndexes.count > 0 && [self tableView:tableView isGroupRow:[proposedSelectionIndexes lastIndex]]) { |
return [tableView selectedRowIndexes]; |
} |
} |
return proposedSelectionIndexes; |
} |
- (BOOL)tableView:(NSTableView *)tableView shouldTrackCell:(NSCell *)cell forTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row { |
// Always allow tracking if the cell wants to do it, even if the row isn't selected |
NSInteger column = tableColumn ? [[tableView tableColumns] indexOfObject:tableColumn] : -1; |
NSUInteger hitTest = [cell hitTestForEvent:[NSApp currentEvent] inRect:[tableView frameOfCellAtColumn:column row:row] ofView:tableView]; |
return (hitTest & NSCellHitTrackableArea) != 0; |
} |
- (void)dynamicTableView:(ATDynamicTableView *)tableView setObjectValue:(id)object forTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row property:(NSString *)propertyName { |
// Update our model and refresh that row |
ATDesktopImageEntity *entity = [self _imageEntityForRow:row]; |
if ([propertyName isEqualToString:ATEntityPropertyNamedFillColor]) { |
entity.fillColor = object; |
} else if ([propertyName isEqualToString:ATEntityPropertyNamedFillColorName]) { |
entity.fillColorName = object; |
} |
[_tableViewMain reloadDataForRowIndexes:[NSIndexSet indexSetWithIndex:row] columnIndexes:[NSIndexSet indexSetWithIndex:0]]; |
} |
- (IBAction)_takeFilteredImageFrom:(ATFilterBrowserController *)filterBrowser { |
// Update the model and the UI with the new image |
NSImage *image = _filterBrowserController.filteredImage; |
ATDesktopImageEntity *entity = [self _imageEntityForRow:_tableViewMain.selectedRow]; |
if (entity != nil) { |
// Write the file to disk so we can set it as the wallpaper |
NSURL *imageURL = [NSURL URLWithString:NSTemporaryDirectory()]; |
imageURL = [imageURL URLByAppendingPathComponent:@"Filtered Image.tiff"]; |
[[image TIFFRepresentation] writeToURL:imageURL atomically:NO]; |
// Update the URL in our model and reload the image |
entity.fileURL = imageURL; |
entity.image = nil; |
[entity loadImage]; |
} |
[[_filterBrowserController window] orderOut:self]; |
} |
- (IBAction)imgViewMainDoubleClick:(id)sender { |
if (_filterBrowserController == nil) { |
_filterBrowserController = [[ATFilterBrowserController alloc] initWithWindowNibName:@"ATFilterBrowser"]; |
[_filterBrowserController setTarget:self]; |
[_filterBrowserController setApplyAction:@selector(_takeFilteredImageFrom:)]; |
} |
_filterBrowserController.sourceImage = [_imageViewMain image]; |
[[_filterBrowserController window] orderFront:self]; |
} |
- (CGFloat)splitView:(NSSplitView *)splitView constrainMinCoordinate:(CGFloat)proposedMinimumPosition ofSubviewAt:(NSInteger)dividerIndex { |
return 200; |
} |
- (CGFloat)splitView:(NSSplitView *)splitView constrainMaxCoordinate:(CGFloat)proposedMaximumPosition ofSubviewAt:(NSInteger)dividerIndex { |
// Make sure the view on the right has at least 200 px wide |
CGFloat splitViewWidth = splitView.bounds.size.width; |
return splitViewWidth - 200; |
} |
@end |
Copyright © 2012 Apple Inc. All Rights Reserved. Terms of Use | Privacy Policy | Updated: 2012-05-31