/* |
File: ATComplexOutlineController.m |
Abstract: |
The main controller for the "Complex Outline View" example window. |
|
Version: 1.3 |
|
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 "ATComplexOutlineController.h" |
#import "ATTableCellView.h" |
#import "ATColorView.h" |
|
@implementation ATComplexOutlineController |
|
- (NSString *)windowNibName { |
return @"ATComplexOutlineWindow"; |
} |
|
- (void)dealloc { |
[_rootContents release]; |
_rootContents = nil; |
[super dealloc]; |
} |
|
- (void)windowDidLoad { |
[super windowDidLoad]; |
NSURL *url = [NSURL fileURLWithPath:@"/Library/Desktop Pictures"]; |
_rootContents = [[ATDesktopFolderEntity alloc] initWithFileURL:url]; |
[_outlineView reloadData]; |
[_outlineView registerForDraggedTypes:[NSArray arrayWithObjects:(id)kUTTypeURL, nil]]; |
[_outlineView setDraggingSourceOperationMask:NSDragOperationEvery forLocal:NO]; |
} |
|
- (void)pathCtrlValueChanged:(id)sender { |
NSURL *url = [_pathCtrlRootDirectory objectValue]; |
_rootContents = [[ATDesktopFolderEntity alloc] initWithFileURL:url]; |
[_outlineView reloadData]; |
} |
|
- (NSInteger)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(id)item { |
if (item == nil) { |
return _rootContents.children.count; |
} else if ([item isKindOfClass:[ATDesktopFolderEntity class]]) { |
return ((ATDesktopFolderEntity *)item).children.count; |
} else { |
return 0; |
} |
} |
|
- (id)outlineView:(NSOutlineView *)outlineView child:(NSInteger)index ofItem:(id)item { |
if (item == nil) { |
return [_rootContents.children objectAtIndex:index]; |
} else { |
return [((ATDesktopFolderEntity *)item).children objectAtIndex:index]; |
} |
} |
|
- (BOOL)outlineView:(NSOutlineView *)outlineView isItemExpandable:(id)item { |
return [item isKindOfClass:[ATDesktopFolderEntity class]]; |
} |
|
- (id)outlineView:(NSOutlineView *)outlineView objectValueForTableColumn:(NSTableColumn *)tableColumn byItem:(id)item { |
// Every regular view uses bindings to the item. The "Date Cell" needs to have the date extracted from the fileURL |
if ([[tableColumn identifier] isEqualToString:@"DateCell"]) { |
id dateValue; |
if ([[item fileURL] getResourceValue:&dateValue forKey:NSURLContentModificationDateKey error:NULL]) { |
return dateValue; |
} else { |
return nil; |
} |
} |
return item; |
} |
|
- (BOOL)outlineView:(NSOutlineView *)outlineView isGroupItem:(id)item { |
return [item isKindOfClass:[ATDesktopFolderEntity class]]; |
} |
|
- (NSView *)outlineView:(NSOutlineView *)outlineView viewForTableColumn:(NSTableColumn *)tableColumn item:(id)item { |
if ([item isKindOfClass:[ATDesktopFolderEntity class]]) { |
// Everything is setup in bindings |
return [outlineView makeViewWithIdentifier:@"GroupCell" owner:self]; |
} else { |
NSView *result = [outlineView makeViewWithIdentifier:[tableColumn identifier] owner:self]; |
if ([result isKindOfClass:[ATTableCellView class]]) { |
ATTableCellView *cellView = (ATTableCellView *)result; |
// setup the color; we can't do this in bindings |
cellView.colorView.drawBorder = YES; |
cellView.colorView.backgroundColor = [item fillColor]; |
} |
// Use a shared date formatter on the DateCell for better performance. Otherwise, it is encoded in every NSTextField |
if ([[tableColumn identifier] isEqualToString:@"DateCell"]) { |
[(id)result setFormatter:_sharedDateFormatter]; |
} |
return result; |
} |
return nil; |
} |
|
- (id <NSPasteboardWriting>)outlineView:(NSOutlineView *)outlineView pasteboardWriterForItem:(id)item { |
return (id <NSPasteboardWriting>)item; |
} |
|
- (void)outlineView:(NSOutlineView *)outlineView draggingSession:(NSDraggingSession *)session willBeginAtPoint:(NSPoint)screenPoint forItems:(NSArray *)draggedItems { |
[_itemBeingDragged release]; |
_itemBeingDragged = nil; |
|
// If only one item is being dragged, mark it so we can reorder it with a special pboard indicator |
if (draggedItems.count == 1) { |
_itemBeingDragged = [[draggedItems lastObject] retain]; |
} |
} |
|
- (NSDictionary *)_pasteboardReadingOptions { |
// Only file urls that contain images or folders |
NSMutableArray *fileTypes = [NSMutableArray arrayWithObject:(id)kUTTypeFolder]; |
[fileTypes addObjectsFromArray:[NSImage imageTypes]]; |
NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithBool:YES], NSPasteboardURLReadingFileURLsOnlyKey, fileTypes, NSPasteboardURLReadingContentsConformToTypesKey, nil]; |
return options; |
} |
|
/* When validating the contents of the pasteboard, it is best practice to use -canReadObjectForClasses:arrayWithObject:options: since it is possible for it to avoid reading and creating objects for every pasteboard item. |
*/ |
- (BOOL)_containsAcceptableURLsFromPasteboard:(NSPasteboard *)draggingPasteboard { |
return [draggingPasteboard canReadObjectForClasses:[NSArray arrayWithObject:[NSURL class]] options:[self _pasteboardReadingOptions]]; |
} |
|
- (NSDragOperation)outlineView:(NSOutlineView *)outlineView validateDrop:(id <NSDraggingInfo>)info proposedItem:(id)item proposedChildIndex:(NSInteger)index { |
// Only let dropping on the entire table or a folder |
if (item == nil || [item isKindOfClass:[ATDesktopFolderEntity class]]) { |
// If the sender is ourselves, then we accept it as a move or copy, depending on the modifier key |
if ([info draggingSource] == outlineView) { |
BOOL isCopy = [info draggingSourceOperationMask] == NSDragOperationCopy; |
if (isCopy) { |
info.animatesToDestination = YES; |
return NSDragOperationCopy; |
} else { |
if (_itemBeingDragged) { |
// We have a single item being dragged to move; validate if we can move it or not |
// A move is only valid if the target isn't a child of the thing being dragged. We validate that now |
id itemWalker = item; |
while (itemWalker) { |
if (itemWalker == _itemBeingDragged) { |
return NSDragOperationNone; // Can't do it! |
} |
itemWalker = [outlineView parentForItem:itemWalker]; |
} |
return NSDragOperationMove; |
} else { |
// For multiple items, we do a copy and don't allow moving |
info.animatesToDestination = YES; |
return NSDragOperationCopy; |
} |
} |
} else { |
// Only accept drops that have at least one URL on the pasteboard which contains an image or a folder |
if ([self _containsAcceptableURLsFromPasteboard:[info draggingPasteboard]]) { |
info.animatesToDestination = YES; |
return NSDragOperationCopy; |
} |
} |
} |
return NSDragOperationNone; |
} |
|
// Multiple item dragging support. Implementation of this method is required to change the drag images into what we want them to look like when over our view |
- (void)outlineView:(NSOutlineView *)outlineView updateDraggingItemsForDrag:(id <NSDraggingInfo>)draggingInfo { |
if ([draggingInfo draggingSource] != outlineView) { |
// The source isn't us, so update the drag images |
// We will be doing an insertion; update the dragging items to have an appropriate image. We also iterate over generic pasteboard items, and set the imageComponentsProvider to nil so they will fade out. |
NSArray *classes = [NSArray arrayWithObjects:[ATDesktopEntity class], [NSPasteboardItem class], nil]; |
|
// Create a copied temporary cell to draw to images |
NSTableColumn *tableColumn = [_outlineView outlineTableColumn]; |
|
// Create a new cell frame based on the basic attributes |
NSRect cellFrame = NSMakeRect(0, 0, [tableColumn width], [outlineView rowHeight]); |
|
// Subtract out the intercellSpacing from the width only. The rowHeight is sans-spacing |
cellFrame.size.width -= [outlineView intercellSpacing].width; |
|
// Grab a basic view to use for creating sample images and data; we will reuse it for each dragged item |
ATTableCellView *tableCellView = [outlineView makeViewWithIdentifier:[tableColumn identifier] owner:self]; |
|
__block NSInteger validCount = 0; |
[draggingInfo enumerateDraggingItemsWithOptions:0 forView:_outlineView classes:classes searchOptions:nil usingBlock:^(NSDraggingItem *draggingItem, NSInteger index, BOOL *stop) { |
if ([draggingItem.item isKindOfClass:[ATDesktopEntity class]]) { |
ATDesktopEntity *entity = (ATDesktopEntity *)draggingItem.item; |
draggingItem.draggingFrame = cellFrame; |
draggingItem.imageComponentsProvider = ^(void) { |
// Force the image to be generated right now, instead of lazily doing it |
if ([entity isKindOfClass:[ATDesktopImageEntity class]]) { |
((ATDesktopImageEntity *)entity).image = [[[NSImage alloc] initByReferencingURL:entity.fileURL] autorelease]; |
} |
// Setup the cell with this temporary data |
tableCellView.objectValue = entity; // This is what bindings normally does for us. Our sub-views are bound to this value. |
tableCellView.frame = cellFrame; |
// Ask the cell view for the image components from that cell |
return [tableCellView draggingImageComponents]; |
}; |
validCount++; |
} else { |
// Non-valid item (a generic NSPasteboardItem). |
// Make the drag images go away |
draggingItem.imageComponentsProvider = nil; |
} |
}]; |
draggingInfo.numberOfValidItemsForDrop = validCount; |
} |
} |
|
|
- (void)_performInsertWithDragInfo:(id <NSDraggingInfo>)info parentItem:(ATDesktopFolderEntity *)destinationFolderEntity childIndex:(NSInteger)childIndex { |
// NSOutlineView's root is nil |
id outlineParentItem = destinationFolderEntity == _rootContents ? nil : destinationFolderEntity; |
|
NSInteger outlineColumnIndex = [[_outlineView tableColumns] indexOfObject:[_outlineView outlineTableColumn]]; |
|
// Enumerate all items dropped on us and create new model objects for them |
NSArray *classes = [NSArray arrayWithObject:[ATDesktopEntity class]]; |
__block NSInteger insertionIndex = childIndex; |
|
[info enumerateDraggingItemsWithOptions:0 forView:_outlineView classes:classes searchOptions:[self _pasteboardReadingOptions] usingBlock:^(NSDraggingItem *draggingItem, NSInteger index, BOOL *stop) { |
// the item is our new model object -- created by the classes via the pasteboard reading support |
ATDesktopEntity *entity = (ATDesktopEntity *)draggingItem.item; |
|
// Add it to the model |
[destinationFolderEntity.children insertObject:entity atIndex:insertionIndex]; |
|
// Tell the outlineview of the change |
[_outlineView insertItemsAtIndexes:[NSIndexSet indexSetWithIndex:insertionIndex] inParent:outlineParentItem withAnimation:NSTableViewAnimationEffectGap]; |
|
// Update the final frame of the dragging item |
NSInteger row = [_outlineView rowForItem:entity]; |
draggingItem.draggingFrame = [_outlineView frameOfCellAtColumn:outlineColumnIndex row:row]; |
|
// Insert all children one after another |
insertionIndex++; |
}]; |
} |
|
- (void)_performDragReorderWithDragInfo:(id <NSDraggingInfo>)info parentItem:(ATDesktopFolderEntity *)destinationFolderEntity childIndex:(NSInteger)childIndex { |
ATDesktopFolderEntity *oldParent = [_outlineView parentForItem:_itemBeingDragged]; |
if (oldParent == nil) oldParent = _rootContents; |
NSInteger fromIndex = [oldParent.children indexOfObject:_itemBeingDragged]; |
[oldParent.children removeObjectAtIndex:fromIndex]; |
if (oldParent == destinationFolderEntity) { |
// Consider the item being deleted before it is being inserted. |
// This is because we are inserting *before* childIndex, and *not* after it (which is what the move API does). |
if (fromIndex < childIndex) { |
childIndex--; |
} |
} |
|
[destinationFolderEntity.children insertObject:_itemBeingDragged atIndex:childIndex]; |
|
// NSOutlineView doesn't have a way of setting the root item |
if (oldParent == _rootContents) oldParent = nil; |
if (destinationFolderEntity == _rootContents) destinationFolderEntity = nil; |
[_outlineView moveItemAtIndex:fromIndex inParent:oldParent toIndex:childIndex inParent:destinationFolderEntity]; |
} |
|
|
- (BOOL)outlineView:(NSOutlineView *)outlineView acceptDrop:(id <NSDraggingInfo>)info item:(ATDesktopEntity *)item childIndex:(NSInteger)childIndex { |
ATDesktopFolderEntity *destinationFolderEntity = nil; |
if (item == nil) { |
destinationFolderEntity = _rootContents; |
} else if ([item isKindOfClass:[ATDesktopFolderEntity class]]) { |
destinationFolderEntity = (ATDesktopFolderEntity *)item; |
} else { |
NSAssert(NO, @"Internal error: expecting a folder entity for dropping onto!"); |
} |
|
// If it was a drop "on", then we add it at the start |
if (childIndex == NSOutlineViewDropOnItemIndex) { |
childIndex = 0; |
} |
|
[_outlineView beginUpdates]; |
// Are we copying the data or moving something? |
if (_itemBeingDragged == nil || [info draggingSourceOperationMask] == NSDragOperationCopy) { |
// Yes, this is an insert from the pasteboard (even if it is a copy of _itemBeingDragged) |
[self _performInsertWithDragInfo:info parentItem:destinationFolderEntity childIndex:childIndex]; |
} else { |
[self _performDragReorderWithDragInfo:info parentItem:destinationFolderEntity childIndex:childIndex]; |
} |
[_outlineView endUpdates]; |
|
[_itemBeingDragged release]; |
_itemBeingDragged = nil; |
|
return YES; |
} |
|
- (void)_removeItemAtRow:(NSInteger)row { |
id item = [_outlineView itemAtRow:row]; |
ATDesktopFolderEntity *parent = (ATDesktopFolderEntity *)[_outlineView parentForItem:item]; |
if (parent == nil) { |
parent = _rootContents; |
} |
NSInteger indexInParent = [parent.children indexOfObject:item]; |
[parent.children removeObjectAtIndex:indexInParent]; |
|
if (parent == _rootContents) { |
parent = nil; |
} |
[_outlineView removeItemsAtIndexes:[NSIndexSet indexSetWithIndex:indexInParent] inParent:parent withAnimation:NSTableViewAnimationEffectFade | NSTableViewAnimationSlideLeft]; |
} |
|
- (IBAction)btnDeleteRowClicked:(id)sender { |
NSInteger row = [_outlineView rowForView:sender]; |
if (row != -1) { |
// Take care of the case of the user clicking on a row that was in the middle of being deleted |
[self _removeItemAtRow:row]; |
} |
} |
|
- (IBAction)btnDeletedSelectedRowsClicked:(id)sender { |
[_outlineView beginUpdates]; |
[_outlineView.selectedRowIndexes enumerateIndexesWithOptions:NSEnumerationReverse usingBlock:^(NSUInteger index, BOOL *stop) { |
[self _removeItemAtRow:index]; |
}]; |
[_outlineView endUpdates]; |
} |
|
|
- (IBAction)btnInCellClicked:(id)sender { |
NSInteger row = [_outlineView rowForView:sender]; |
ATDesktopEntity *entity = [_outlineView itemAtRow:row]; |
[[NSWorkspace sharedWorkspace] selectFile:[entity.fileURL path] inFileViewerRootedAtPath:nil]; |
} |
|
- (IBAction)btnDemoMove:(id)sender { |
// Move the selected item down one |
NSInteger selectedRow = [_outlineView selectedRow]; |
if (selectedRow != -1) { |
id item = [[[_outlineView itemAtRow:selectedRow] retain] autorelease]; // retain the item as we are removing it from our array |
// Grab the parent for this item |
ATDesktopFolderEntity *parent = [_outlineView parentForItem:item]; |
// The parent may be nil, so we use the root if it is |
if (parent == nil) { |
parent = _rootContents; |
} |
// Find out where it currently is |
NSInteger indexInParent = [parent.children indexOfObject:item]; |
// Then remove it |
[parent.children removeObjectAtIndex:indexInParent]; |
|
// Move it one index further down, or back to the start, if it would already be at the end. |
NSInteger targetIndexInParent = indexInParent + 1; |
if (targetIndexInParent > [parent.children count]) { |
targetIndexInParent = 0; // back to the start |
} |
[parent.children insertObject:item atIndex:targetIndexInParent]; |
|
// Tell outlineview about our change to our model; but of course, it uses 'nil' as the root item so we have to move back to nil if we were using the root as the parent. |
if (parent == _rootContents) { |
parent = nil; |
} |
|
[_outlineView moveItemAtIndex:indexInParent inParent:parent toIndex:targetIndexInParent inParent:parent]; |
} else { |
NSRunAlertPanel(@"Select something!", @"Select a row for an example of moving it down...", @"OK", nil, nil); |
} |
} |
|
- (IBAction)btnDemoBatchedMoves:(id)sender { |
// Swap all the children of the first two expandable items |
ATDesktopFolderEntity *firstParent = nil; |
ATDesktopFolderEntity *secondParent = nil; |
for (ATDesktopEntity *entity in _rootContents.children) { |
if ([entity isKindOfClass:[ATDesktopFolderEntity class]]) { |
ATDesktopFolderEntity *folderEntity = (ATDesktopFolderEntity *)entity; |
if (firstParent == nil) { |
firstParent = folderEntity; |
} else { |
secondParent = folderEntity; |
break; |
} |
} |
} |
if (firstParent && secondParent) { |
[_outlineView beginUpdates]; |
// Move all the first children to the second array |
for (NSInteger i = 0; i < firstParent.children.count; i++) { |
[_outlineView moveItemAtIndex:0 inParent:firstParent toIndex:i inParent:secondParent]; |
} |
// Move all the children from the second to the first. We have to account for the fact that we just moved all the first items to this one. |
NSInteger childrenOffset = firstParent.children.count; |
for (NSInteger i = 0; i < secondParent.children.count; i++) { |
[_outlineView moveItemAtIndex:childrenOffset inParent:secondParent toIndex:i inParent:firstParent]; |
} |
// Do the changes on our model, and tell the OV we are done |
NSMutableArray *firstParentChildren = [firstParent.children retain]; |
firstParent.children = secondParent.children; |
secondParent.children = firstParentChildren; |
[firstParentChildren release]; |
[_outlineView endUpdates]; |
} else { |
NSRunAlertPanel(@"Expand something!!", @"Couldn't find two parents to do demo move with. Expand some items!", @"OK", nil, nil); |
} |
} |
|
- (IBAction)chkbxFloatGroupRowsClicked:(id)sender { |
BOOL checked = [(NSButton *)sender state] == 1; |
[_outlineView setFloatsGroupRows:checked]; |
} |
|
- (IBAction)clrWellChanged:(id)sender { |
NSColor *color = [sender color]; |
[_outlineView setBackgroundColor:color]; |
} |
|
@end |