CocoaSlideCollection/Controller/AAPLBrowserWindowController.m
/* |
Copyright (C) 2015 Apple Inc. All Rights Reserved. |
See LICENSE.txt for this sample’s licensing information |
Abstract: |
This is the browser window controller implementation. |
*/ |
#import "AAPLBrowserWindowController.h" |
#import "AAPLHeaderView.h" |
#import "AAPLImageCollection.h" |
#import "AAPLImageFile.h" |
#import "AAPLSlideCarrierView.h" |
#import "AAPLSlideTableBackgroundView.h" |
#import "AAPLTag.h" |
#import "AAPLSlideLayout.h" |
#import "AAPLCircularLayout.h" |
#import "AAPLLoopLayout.h" |
#import "AAPLScatterLayout.h" |
#import "AAPLWrappedLayout.h" |
#define HEADER_VIEW_HEIGHT 39 |
#define FOOTER_VIEW_HEIGHT 28 |
static NSString *selectionIndexPathsKey = @"selectionIndexPaths"; |
static NSString *tagsKey = @"tags"; |
static NSString *StringFromCollectionViewDropOperation(NSCollectionViewDropOperation dropOperation); |
static NSString *StringFromCollectionViewIndexPath(NSIndexPath *indexPath); |
@interface AAPLBrowserWindowController (Internals) |
- (void)showStatus:(NSString *)statusMessage; |
- (void)startObservingImageCollection; |
- (void)stopObservingImageCollection; |
- (void)handleImageFilesInsertedAtIndexPaths:(NSSet<NSIndexPath *> *)indexPaths; |
- (void)handleImageFilesRemovedAtIndexPaths:(NSSet<NSIndexPath *> *)indexPaths; |
- (void)handleTagsInsertedInCollectionAtIndexes:(NSIndexSet *)indexes; |
- (void)handleTagsRemovedFromCollectionAtIndexes:(NSIndexSet *)indexes; |
@end |
@implementation AAPLBrowserWindowController |
- (id)initWithRootURL:(NSURL *)newRootURL { |
self = [super initWithWindowNibName:@"BrowserWindow"]; |
if (self) { |
rootURL = [newRootURL copy]; |
groupByTag = NO; |
layoutKind = SlideLayoutKindWrapped; |
// Create an AAPLImageCollection for browsing our assigned folder. |
imageCollection = [[AAPLImageCollection alloc] initWithRootURL:rootURL]; |
/* |
Watch for changes in the imageCollection's imageFiles list. |
Whenever a new AAPLImageFile is added or removed, |
Key-Value Observing (KVO) will send us an |
-observeValueForKeyPath:ofObject:change:context: message, which we |
can respond to as needed to update the set of slides that we |
display. |
*/ |
[self startObservingImageCollection]; |
} |
return self; |
} |
// This important method, which is invoked after the AAPLBrowserWindowController has finished loading its BrowserWindow.nib file, is where we perform some important setup of our NSCollectionView. |
- (void)windowDidLoad { |
// Set the window's title to the name of the folder we're browsing. |
self.window.title = rootURL.lastPathComponent; |
// Set imageCollectionView.collectionViewLayout to match our desired layoutKind. |
[self updateLayout]; |
// Give the CollectionView a backgroundView. The CollectionView will insert this view behind its enclosing NSClipView, and automatically size it to always match the NSClipView's frame, producing a background that remains stationary as the content scrolls. |
NSView *backgroundView = [[AAPLSlideTableBackgroundView alloc] initWithFrame:NSMakeRect(0, 0, 100, 100)]; |
self.imageCollectionView.backgroundView = backgroundView; |
// Watch for changes to the CollectionView's selection, just so we can update our status display. |
[imageCollectionView addObserver:self forKeyPath:selectionIndexPathsKey options:0 context:NULL]; |
// Start scanning our assigned folder for image files. |
[imageCollection startOrRestartFileTreeScan]; |
// Configure our CollectionView for drag-and-drop. |
[self registerForCollectionViewDragAndDrop]; |
} |
- (void)registerForCollectionViewDragAndDrop { |
// Register for the dropped object types we can accept. |
[imageCollectionView registerForDraggedTypes:[NSArray arrayWithObject:NSURLPboardType]]; |
// Enable dragging items from our CollectionView to other applications. |
[imageCollectionView setDraggingSourceOperationMask:NSDragOperationEvery forLocal:NO]; |
// Enable dragging items within and into our CollectionView. |
[imageCollectionView setDraggingSourceOperationMask:NSDragOperationEvery forLocal:YES]; |
} |
@synthesize imageCollectionView; |
@synthesize statusTextField; |
- (BOOL)groupByTag { |
return groupByTag; |
} |
- (void)setGroupByTag:(BOOL)flag { |
if (groupByTag != flag) { |
/* |
We observe our imageCollection's properties differently, depending |
whether groupByTag is enabled. So stop observing before we toggle |
the value of groupByTag, then start observing again afterward. |
*/ |
[self stopObservingImageCollection]; |
groupByTag = flag; |
[self startObservingImageCollection]; |
/* |
Tell our CollectionView to reload, since items will now be |
reorganized into sections (or not), and thus will be identified by |
different NSIndexPaths. |
*/ |
[imageCollectionView reloadData]; |
if (groupByTag) { |
[[NSAnimationContext currentContext] setDuration:0.0]; // Suppress animation. |
[self setLayoutKind:SlideLayoutKindWrapped]; // Only our Wrapped layout is designed to deal with sections. |
} |
} |
} |
- (SlideLayoutKind)layoutKind { |
return layoutKind; |
} |
- (void)setLayoutKind:(SlideLayoutKind)newLayoutKind { |
if (layoutKind != newLayoutKind) { |
if (newLayoutKind != SlideLayoutKindWrapped && groupByTag) { |
[[NSAnimationContext currentContext] setDuration:0.0]; // Suppress animation. |
[self setGroupByTag:NO]; |
} |
layoutKind = newLayoutKind; |
[self updateLayout]; |
} |
} |
- (void)updateLayout { |
NSCollectionViewLayout *layout = nil; |
switch (layoutKind) { |
case SlideLayoutKindCircular: layout = [[AAPLCircularLayout alloc] init]; break; |
case SlideLayoutKindLoop: layout = [[AAPLLoopLayout alloc] init]; break; |
case SlideLayoutKindScatter: layout = [[AAPLScatterLayout alloc] init]; break; |
case SlideLayoutKindWrapped: layout = [[AAPLWrappedLayout alloc] init]; break; |
} |
if (layout) { |
if (NSAnimationContext.currentContext.duration > 0.0) { |
NSAnimationContext.currentContext.duration = 0.5; |
imageCollectionView.animator.collectionViewLayout = layout; |
} else { |
imageCollectionView.collectionViewLayout = layout; |
} |
} |
} |
- (void)suspendAutoUpdateResponse { |
autoUpdateResponseSuspended = YES; |
} |
- (void)resumeAutoUpdateResponse { |
autoUpdateResponseSuspended = NO; |
} |
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { |
if (object == imageCollectionView && [keyPath isEqual:selectionIndexPathsKey]) { |
/* |
We're being notified that our imageCollectionView's |
"selectionIndexPaths" property has changed. Update our status |
TextField with a summary (item count) of the new selection. |
*/ |
NSSet<NSIndexPath *> *newSelectedIndexPaths = imageCollectionView.selectionIndexPaths; |
[self showStatus:[NSString stringWithFormat:@"%lu items selected", (unsigned long)(newSelectedIndexPaths.count)]]; |
} else if (object == imageCollection && !autoUpdateResponseSuspended) { |
/* |
We're being notified that our imageCollection's contents have |
changed, and we haven't disabled our auto-update response, so we |
want to inform our imageCollectionView of the exact change that |
just took place. Identify the change by examining the "object", |
"keyPath", and "change" dictionary we've been given, then handle |
the change accordingly. For insertion or removal of items, the |
"change" dictionary will give us a set of "indexes" that specify |
what was added or removed from the parent "object" (which might be |
the imageCollection itself, or one of its AAPLTags). Part of what |
we may need to do is map these indices to corresponding |
(section,item) NSIndexPaths. |
*/ |
NSKeyValueChange kind = [change[NSKeyValueChangeKindKey] integerValue]; |
if (kind == NSKeyValueChangeInsertion || kind == NSKeyValueChangeRemoval) { |
NSIndexSet *indexes = change[@"indexes"]; |
NSMutableSet<NSIndexPath *> *indexPaths = [NSMutableSet<NSIndexPath *> setWithCollectionViewIndexPaths:[NSArray array]]; |
if ([keyPath isEqual:imageFilesKey]) { |
if (object == imageCollection) { |
// Our imageCollection's "imageFiles" array changed. |
[indexes enumerateIndexesUsingBlock:^(NSUInteger itemIndex, BOOL *stop) { |
[indexPaths addObject:[NSIndexPath indexPathForItem:itemIndex inSection:0]]; |
}]; |
} else if ([object isKindOfClass:[AAPLTag class]]) { |
// An AAPLTag's "imageFiles" array changed. |
NSUInteger sectionIndex = [imageCollection.tags indexOfObject:object]; |
if (sectionIndex != NSNotFound) { |
[indexes enumerateIndexesUsingBlock:^(NSUInteger itemIndex, BOOL *stop) { |
[indexPaths addObject:[NSIndexPath indexPathForItem:itemIndex inSection:sectionIndex]]; |
}]; |
} |
} |
// Notify our imageCollectionView of the change. |
if (kind == NSKeyValueChangeInsertion) { |
[self handleImageFilesInsertedAtIndexPaths:indexPaths]; |
} else { |
[self handleImageFilesRemovedAtIndexPaths:indexPaths]; |
} |
} else if ([keyPath isEqual:tagsKey]) { |
// Our imageCollection's "tags" array changed. |
if (kind == NSKeyValueChangeInsertion) { |
[self handleTagsInsertedInCollectionAtIndexes:indexes]; |
} else { |
[self handleTagsRemovedFromCollectionAtIndexes:indexes]; |
} |
} |
} else { |
// For NSKeyValueChangeSetting, we just reload everything. |
[self.imageCollectionView reloadData]; |
} |
} |
} |
// Invoked by the "File" -> "Refresh" menu item. |
- (void)refresh:(id)sender { |
/* |
Ask our imageCollection to check for new, changed, and removed asset |
files. This AAPLBrowserWindowController will be automatically notified |
of changes to the imageCollection via KVO, since we registered to |
observe the imageCollection's contents. |
*/ |
[imageCollection startOrRestartFileTreeScan]; |
} |
- (AAPLImageFile *)imageFileAtIndexPath:(NSIndexPath *)indexPath { |
if (groupByTag) { |
NSArray<AAPLTag *> *tags = imageCollection.tags; |
NSInteger sectionIndex = indexPath.section; |
if (sectionIndex < tags.count) { |
return tags[sectionIndex].imageFiles[indexPath.item]; |
} else { |
return imageCollection.untaggedImageFiles[indexPath.item]; |
} |
} else { |
return imageCollection.imageFiles[indexPath.item]; |
} |
} |
#pragma mark NSCollectionViewDataSource Methods |
// Each of these methods checks whether "groupByTag" is on, and modifies its behavior accordingly. |
- (NSInteger)numberOfSectionsInCollectionView:(NSCollectionView *)collectionView { |
if (groupByTag) { |
return imageCollection.tags.count + 1; // +1 for the special "Untagged" section we put at the end |
} else { |
return 1; |
} |
} |
- (NSInteger)collectionView:(NSCollectionView *)collectionView numberOfItemsInSection:(NSInteger)section { |
if (groupByTag) { |
NSArray<AAPLTag *> *tags = imageCollection.tags; |
if (section < tags.count) { |
// Return the number of ImageFiles in the AAPLTag with index "section". |
return tags[section].imageFiles.count; |
} else { |
// Return the number of ImageFiles in the special "Untagged" section we put at the end |
return imageCollection.untaggedImageFiles.count; |
} |
} else { |
// Return the number of ImageFiles in the collection (treated as a single, flat list). |
return imageCollection.imageFiles.count; |
} |
} |
- (NSCollectionViewItem *)collectionView:(NSCollectionView *)collectionView itemForRepresentedObjectAtIndexPath:(NSIndexPath *)indexPath { |
// Message back to the collectionView, asking it to make a @"Slide" item associated with the given item indexPath. The collectionView will first check whether an NSNib or item Class has been registered with that name (via -registerNib:forItemWithIdentifier: or -registerClass:forItemWithIdentifier:). Failing that, the collectionView will search for a .nib file named "Slide". Since our .nib file is named "Slide.nib", no registration is necessary. |
NSCollectionViewItem *item = [collectionView makeItemWithIdentifier:@"Slide" forIndexPath:indexPath]; |
AAPLImageFile *imageFile = [self imageFileAtIndexPath:indexPath]; |
item.representedObject = imageFile; |
return item; |
} |
- (nonnull NSView *)collectionView:(nonnull NSCollectionView *)collectionView viewForSupplementaryElementOfKind:(nonnull NSString *)kind atIndexPath:(nonnull NSIndexPath *)indexPath { |
NSString *identifier = nil; |
NSString *content = nil; |
NSArray<AAPLTag *> *tags = imageCollection.tags; |
NSInteger sectionIndex = indexPath.section; |
if (sectionIndex < tags.count) { |
AAPLTag *tag = tags[sectionIndex]; |
if ([kind isEqual:NSCollectionElementKindSectionHeader]) { |
content = tag.name; |
} else if ([kind isEqual:NSCollectionElementKindSectionFooter]) { |
content = [NSString stringWithFormat:@"%lu image files tagged \"%@\"", (unsigned long)(tag.imageFiles.count), tag.name]; |
} |
} else { |
if ([kind isEqual:NSCollectionElementKindSectionHeader]) { |
content = @"(Untagged)"; |
} else if ([kind isEqual:NSCollectionElementKindSectionFooter]) { |
content = [NSString stringWithFormat:@"%lu image files have no tags assigned", (unsigned long)(imageCollection.untaggedImageFiles.count)]; |
} |
} |
if ([kind isEqual:NSCollectionElementKindSectionHeader]) { |
identifier = @"Header"; |
} else if ([kind isEqual:NSCollectionElementKindSectionFooter]) { |
identifier = @"Footer"; |
} |
id view = identifier ? [collectionView makeSupplementaryViewOfKind:kind withIdentifier:identifier forIndexPath:indexPath] : nil; |
if (content && [view isKindOfClass:[AAPLHeaderView class]]) { |
NSTextField *titleTextField = [(AAPLHeaderView *)view titleTextField]; |
titleTextField.stringValue = content; |
} |
return view; |
} |
#pragma mark NSCollectionViewDelegateFlowLayout Methods |
// Implementing this delegate method tells a NSCollectionViewFlowLayout (such as our AAPLWrappedLayout) what size to make a "Header" supplementary view. (The actual size will be clipped to the CollectionView's width.) |
- (NSSize)collectionView:(NSCollectionView *)collectionView layout:(NSCollectionViewLayout*)collectionViewLayout referenceSizeForHeaderInSection:(NSInteger)section { |
return groupByTag ? NSMakeSize(10000, HEADER_VIEW_HEIGHT) : NSZeroSize; // If groupByTag is NO, we don't want to show a header. |
} |
// Implementing this delegate method tells a NSCollectionViewFlowLayout (such as our AAPLWrappedLayout) what size to make a "Footer" supplementary view. (The actual size will be clipped to the CollectionView's width.) |
- (NSSize)collectionView:(NSCollectionView *)collectionView layout:(NSCollectionViewLayout *)collectionViewLayout referenceSizeForFooterInSection:(NSInteger)section { |
return groupByTag ? NSMakeSize(10000, FOOTER_VIEW_HEIGHT) : NSZeroSize; // If groupByTag is NO, we don't want to show a footer. |
} |
#pragma mark NSCollectionViewDelegate Drag-and-Drop Methods |
/*******************/ |
/* Dragging Source */ |
/*******************/ |
/* |
1. When a CollectionView wants to begin a drag operation for some of its |
items, it first sends this message to its delegate. The delegate may return |
YES to allow the proposed drag to begin, or NO to prevent it. We want to |
allow the user to drag any and all items in the CollectionView, so we |
unconditionally return YES here. If you wish, however, you can return NO |
under certain circumstances, to prevent the items specified by "indexPaths" |
from being dragged. |
*/ |
- (BOOL)collectionView:(NSCollectionView *)collectionView canDragItemsAtIndexPaths:(NSSet<NSIndexPath *> *)indexPaths withEvent:(NSEvent *)event { |
return YES; |
} |
/* |
2. If the above method allows the drag to begin, the CollectionView will invoke |
this method once per item to be dragged, to request a pasteboard writer for |
the item's underlying model object. Some kinds of model objects (for |
example, NSURL) are themselves suitable pasteboard writers. |
*/ |
- (id <NSPasteboardWriting>)collectionView:(NSCollectionView *)collectionView pasteboardWriterForItemAtIndexPath:(NSIndexPath *)indexPath { |
AAPLImageFile *imageFile = [self imageFileAtIndexPath:indexPath]; |
return imageFile.url.absoluteURL; // An NSURL can be a pasteboard writer, but must be returned as an absolute URL. |
} |
/* |
3. After obtaining a pasteboard writer for each item to be dragged, the |
CollectionView will invoke this method to notify you that the drag is |
beginning. You aren't required to implement this delegate method, but it |
can provide a useful hook for one particular start-of-drag action you might |
want to perform: saving a copy fo the passed the indexPaths as an indication |
to yourself that the drag began in this CollectionView, which will prove |
useful if the same CollectionView ends up being the drop destination. |
*/ |
- (void)collectionView:(NSCollectionView *)collectionView draggingSession:(NSDraggingSession *)session willBeginAtPoint:(NSPoint)screenPoint forItemsAtIndexPaths:(NSSet<NSIndexPath *> *)indexPaths { |
/* |
Remember the indexPaths we're dragging, in case we end up being the drag |
destination too. Knowing that a drop originated from this |
CollectionView will enable us to handle it more efficiently, and with |
a "move items" operation instead of a |
*/ |
indexPathsOfItemsBeingDragged = [indexPaths copy]; |
// Indicate dragging state in our status TextField. |
[self showStatus:[NSString stringWithFormat:@"Dragging %lu items", (unsigned long)(indexPaths.count)]]; |
} |
/* |
If this CollectionView ends up also being the dragging destination, we'll |
receive the "Dragging Destination" messages as implemented below, before |
the dragging session ends. |
*/ |
/* |
6. Whether the drag is accepted, or the drag operation is cancelled, the |
CollectionView always sends this mesage to conclude the drag session. It's |
a good place to perform any necessary cleanup, such as clearing the |
"indexPathsOfItemsBeingDragged" we saved in the |
-collectionView:draggingSession:willBeginAtPoint:forItemsAtIndexPaths: |
method, above. |
*/ |
- (void)collectionView:(NSCollectionView *)collectionView draggingSession:(NSDraggingSession *)session endedAtPoint:(NSPoint)screenPoint dragOperation:(NSDragOperation)operation { |
// Clear the dragging indexPaths we saved earlier. |
indexPathsOfItemsBeingDragged = nil; |
// Indicate dragging state in our status TextField. |
[self showStatus:@"Dragging ended"]; |
} |
/************************/ |
/* Dragging Destination */ |
/************************/ |
/* |
4. When the user drags something around a CollectionView (whether the dragging |
source is the same CollectionView or some other view, potentially in a |
different process), the CollectionView will repeatedly invoke this method to |
propose dropping the dragging items at various places within within itself. |
(If the user mouses out of the CollectionView, the CollectionView stops |
sending this message. If the user mouses back into the CollectionView, the |
CollectionView starts sending this messsage again.) You return an |
NSDragOperation mask to specify what kinds of drag operations should be |
allowed for the proposed destination. You may also alter the |
proposedDropOperation and proposedDropIndexPath through the provided |
pointers, if desired. |
*/ |
- (NSDragOperation)collectionView:(NSCollectionView *)collectionView validateDrop:(id <NSDraggingInfo>)draggingInfo proposedIndexPath:(NSIndexPath **)proposedDropIndexPath dropOperation:(NSCollectionViewDropOperation *)proposedDropOperation { |
/* |
Interpret the proposedDropIndexPath in the context of the |
proposedDropOperation, and decide whether it's an operation we want to |
allow. A proposedDropOperation of NSCollectionViewDropOn indicates that |
the user is hovering over an existing item idntified by the |
proposedDropIndexPath. A proposedDropOperation of |
NSCollectionViewDropBefore indicates that the user is hovering in a gap |
between items, and inserting the dropped items at proposedDropIndexPath |
would place the dropped items in that gap. If you allow the user to |
manually order items, you might accept a "DropBefore" operation as |
proposed. If your items are automatically sorted according to some |
criteria, you might disregard the proposedDropIndexPath, and simply |
accept the drop as a drop into the CollectionView as a whole, choosing |
your own appropriate index paths at which to insert the dropped items. |
In this example, we want to allow drag-and-drop as a means of manually |
reordering items, and our items aren't able to act as containers, so we |
allow dropping between items only, not dropping onto them. |
*/ |
// Evaluate and possibly override the proposed drop operation. |
NSString *proposedActionDescription = [NSString stringWithFormat:@"Validate drop %@ item at indexPath=%@", StringFromCollectionViewDropOperation(*proposedDropOperation), StringFromCollectionViewIndexPath(*proposedDropIndexPath)]; |
if (*proposedDropOperation == NSCollectionViewDropOn) { |
*proposedDropOperation = NSCollectionViewDropBefore; |
proposedActionDescription = [proposedActionDescription stringByAppendingFormat:@" -- changed to drop before %@", StringFromCollectionViewIndexPath(*proposedDropIndexPath)]; |
} |
// Indicate dragging state in our status TextField. |
[self showStatus:proposedActionDescription]; |
/* |
If we're dragging items around within the CollectionView (i.e. this |
CollectionView is also the dragging source), the operation is Move. |
If not, the operation is Copy. |
This example doesn't yet support dragging items within a CollectionView |
in "Group by Tag" mode, so return NSDragOperationNone if that's what's |
proposed. |
*/ |
if (indexPathsOfItemsBeingDragged) { |
return groupByTag ? NSDragOperationNone : NSDragOperationMove; |
} else { |
return NSDragOperationCopy; |
} |
} |
/* |
5. If the user commits the proposed drop operation (by releasing the mouse |
button), the CollectionView invokes this method to instruct its delegate to |
make the proposed edit. Your implementation has the important |
responsibility of (1) modifying your model as proposed, and then |
(2) notifying the CollectionView of the edits. Return YES if you completed |
the drop successfully, NO if you could not complete the drop. |
*/ |
- (BOOL)collectionView:(NSCollectionView *)collectionView acceptDrop:(id <NSDraggingInfo>)draggingInfo indexPath:(NSIndexPath *)indexPath dropOperation:(NSCollectionViewDropOperation)dropOperation { |
BOOL result = NO; |
NSString *proposedActionDescription = [NSString stringWithFormat:@"Accept drop of %lu items from %@, %@ item at indexPath=%@", (unsigned long)[draggingInfo numberOfValidItemsForDrop], indexPathsOfItemsBeingDragged ? @"self" : @"elsewhere", StringFromCollectionViewDropOperation(dropOperation), StringFromCollectionViewIndexPath(indexPath)]; |
[self showStatus:proposedActionDescription]; |
/* |
Suspend our usual KVO response to ImageCollection changes. We want to |
notify the CollectionView of updates manually, so we can animate a |
"move" instead of a "delete" and "insert". |
*/ |
[self suspendAutoUpdateResponse]; |
// Is our own imageCollectionView the dragging source? |
if (indexPathsOfItemsBeingDragged) { |
// Yes, existing items are being dragged within our imageCollectionView. |
if (groupByTag) { |
/* |
This example doesn't yet support dragging items within a |
CollectionView in "Group by Tag" mode, so return NO if that's |
what's proposed. |
*/ |
result = NO; |
} else { |
/* |
Walk forward through fromItemIndex values > toItemIndex, to keep |
our "from" and "to" indexes valid as we go, moving items one at |
a time. |
*/ |
__block NSInteger toItemIndex = indexPath.item; |
[indexPathsOfItemsBeingDragged enumerateIndexPathsWithOptions:0 usingBlock:^(NSIndexPath *fromIndexPath, BOOL *stop) { |
NSInteger fromItemIndex = fromIndexPath.item; |
if (fromItemIndex > toItemIndex) { |
/* |
For each step: First, modify our model. |
*/ |
[imageCollection moveImageFileFromIndex:fromItemIndex toIndex:toItemIndex]; |
/* |
Next, notify the CollectionView of the change we just |
made to our model. |
*/ |
[[imageCollectionView animator] moveItemAtIndexPath:[NSIndexPath indexPathForItem:fromItemIndex inSection:[indexPath section]] toIndexPath:[NSIndexPath indexPathForItem:toItemIndex inSection:[indexPath section]]]; |
// Advance to maintain moved items in their original order. |
++toItemIndex; |
} |
}]; |
/* |
Walk backward through fromItemIndex values < toItemIndex, to |
keep our "from" and "to" indexes valid as we go, moving items |
one at a time. |
*/ |
__block NSInteger adjustedToItemIndex = indexPath.item - 1; |
[indexPathsOfItemsBeingDragged enumerateIndexPathsWithOptions:NSEnumerationReverse usingBlock:^(NSIndexPath *fromIndexPath, BOOL *stop) { |
NSInteger fromItemIndex = [fromIndexPath item]; |
if (fromItemIndex < adjustedToItemIndex) { |
/* |
For each step: First, modify our model. |
*/ |
[imageCollection moveImageFileFromIndex:fromItemIndex toIndex:adjustedToItemIndex]; |
/* |
Next, notify the CollectionView of the change we just |
made to our model. |
*/ |
NSIndexPath *adjustedToIndexPath = [NSIndexPath indexPathForItem:adjustedToItemIndex inSection:[indexPath section]]; |
[imageCollectionView.animator moveItemAtIndexPath:[NSIndexPath indexPathForItem:fromItemIndex inSection:indexPath.section] toIndexPath:adjustedToIndexPath]; |
// Retreat to maintain moved items in their original order. |
--adjustedToItemIndex; |
} |
}]; |
// We did it! |
result = YES; |
} |
} else { |
// Items are being dragged from elsewhere into our CollectionView. |
/* |
Examine the items to be dropped, as provided by the draggingInfo |
object. Accumulate the URLs among them into a "droppedObjects" |
array. |
*/ |
NSMutableArray *droppedObjects = [NSMutableArray array]; |
[draggingInfo enumerateDraggingItemsWithOptions:0 forView:collectionView classes:@[[NSURL class]] searchOptions:[NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithBool:YES], NSPasteboardURLReadingFileURLsOnlyKey, nil] usingBlock:^(NSDraggingItem *draggingItem, NSInteger idx, BOOL *stop) { |
NSURL *url = draggingItem.item; |
if ([url isKindOfClass:[NSURL class]]) { |
[droppedObjects addObject:url]; |
} |
}]; |
/* |
For each dropped URL: |
1. Create a corresponding AAPLImageFile. |
2. Insert the AAPLImageFile at the designated point in our |
imageCollection. |
3. Notify our CollectionView of the insertion. |
We check first whether the colleciton already contains an ImageFile |
with the given URL, and disallow duplicates. |
*/ |
NSInteger insertionIndex = indexPath.item; |
NSMutableArray *errors = [[NSMutableArray alloc] init]; |
for (NSURL *url in droppedObjects) { |
AAPLImageFile *imageFile = [imageCollection imageFileForURL:url]; |
if (imageFile == nil) { |
/* |
Copy the image file from the source URL into our |
imageCollection's folder. |
*/ |
NSURL *targetURL = [imageCollection.rootURL URLByAppendingPathComponent:url.lastPathComponent isDirectory:NO]; |
NSError *error; |
if ([[NSFileManager defaultManager] copyItemAtURL:url toURL:targetURL error:&error]) { |
/* |
Now create and insert an ImageFile that references the |
targetURL we copied to. |
*/ |
imageFile = [[AAPLImageFile alloc] initWithURL:targetURL]; |
if (imageFile) { |
/* |
For each item: First, modify our model. |
*/ |
[imageCollection insertImageFile:imageFile atIndex:insertionIndex]; |
/* |
Next, notify the CollectionView of the change we just |
made to our model. |
*/ |
[collectionView.animator insertItemsAtIndexPaths:[NSSet<NSIndexPath *> setWithCollectionViewIndexPath:indexPath]]; |
// We succeeded in accepting at least one item. |
result = YES; |
} |
} else { |
/* |
Copy failed. Remember the error, and notify the user of |
just the first failure, instead of pestering them about |
each of potentially several failures. |
*/ |
if (error) { |
[errors addObject:error]; |
} |
} |
} |
} |
if (errors.count > 0) { |
[imageCollectionView presentError:errors[0] modalForWindow:imageCollectionView.window delegate:nil didPresentSelector:NULL contextInfo:NULL]; |
} |
} |
// Resume normal KVO handling. |
[self resumeAutoUpdateResponse]; |
// Return indicating success or failure. |
return result; |
} |
#pragma mark Teardown |
- (void)windowWillClose:(NSNotification *)notification { |
[imageCollection stopWatchingFolder]; // Break retain cycle, allowing teardown. |
[self stopObservingImageCollection]; |
[imageCollectionView removeObserver:self forKeyPath:selectionIndexPathsKey]; |
} |
@end |
@implementation AAPLBrowserWindowController (Internals) |
- (void)showStatus:(NSString *)statusMessage { |
statusTextField.stringValue = statusMessage; |
} |
- (void)startObservingImageCollection { |
/* |
Sign up for Key-Value Observing (KVO) notifications, that will tell us |
when the content of our imageCollection changes. If we are showing |
its ImageFiles grouped by tag, we want to observe the imageCollection's |
"tags" array, and the "imageFiles" array of each AAPLTag. If we are |
showing our imageCollection's ImageFiles without grouping, we instead |
want to simply observe the imageCollection's "imageFiles" array. |
Whenever a change occurs, KVO will send us an |
-observeValueForKeyPath:ofObject:change:context: message, which we |
can respond to as needed to update the set of slides that we |
display. |
*/ |
if (groupByTag) { |
[imageCollection addObserver:self forKeyPath:tagsKey options:0 context:NULL]; |
for (AAPLTag *tag in imageCollection.tags) { |
[tag addObserver:self forKeyPath:imageFilesKey options:0 context:NULL]; |
} |
} else { |
[imageCollection addObserver:self forKeyPath:imageFilesKey options:0 context:NULL]; |
} |
} |
- (void)stopObservingImageCollection { |
if (groupByTag) { |
[imageCollection removeObserver:self forKeyPath:tagsKey]; |
for (AAPLTag *tag in imageCollection.tags) { |
[tag removeObserver:self forKeyPath:imageFilesKey]; |
} |
} else { |
[imageCollection removeObserver:self forKeyPath:imageFilesKey]; |
} |
} |
- (void)handleImageFilesInsertedAtIndexPaths:(NSSet<NSIndexPath *> *)indexPaths { |
NSAnimationContext.currentContext.duration = 0.25; |
[self.imageCollectionView.animator insertItemsAtIndexPaths:indexPaths]; |
} |
- (void)handleImageFilesRemovedAtIndexPaths:(NSSet<NSIndexPath *> *)indexPaths { |
NSAnimationContext.currentContext.duration = 0.25; |
[self.imageCollectionView.animator deleteItemsAtIndexPaths:indexPaths]; |
} |
- (void)handleTagsInsertedInCollectionAtIndexes:(NSIndexSet *)indexes { |
NSAnimationContext.currentContext.duration = 0.25; |
[self.imageCollectionView.animator insertSections:indexes]; |
} |
- (void)handleTagsRemovedFromCollectionAtIndexes:(NSIndexSet *)indexes { |
NSAnimationContext.currentContext.duration = 0.25; |
[self.imageCollectionView.animator deleteSections:indexes]; |
} |
@end |
static NSString *StringFromCollectionViewDropOperation(NSCollectionViewDropOperation dropOperation) { |
switch (dropOperation) { |
case NSCollectionViewDropBefore: |
return @"before"; |
case NSCollectionViewDropOn: |
return @"on"; |
default: |
return @"?"; |
} |
} |
static NSString *StringFromCollectionViewIndexPath(NSIndexPath *indexPath) { |
if (indexPath && indexPath.length == 2) { |
return [NSString stringWithFormat:@"(%ld,%ld)", (long)(indexPath.section), (long)(indexPath.item)]; |
} else { |
return @"(nil)"; |
} |
} |
Copyright © 2015 Apple Inc. All Rights Reserved. Terms of Use | Privacy Policy | Updated: 2015-09-16