Initial implementation of drag and drop download.
[pithos-macos] / pithos-macos / PithosBrowserController.m
1 //
2 //  PithosBrowserController.m
3 //  pithos-macos
4 //
5 // Copyright 2011 GRNET S.A. All rights reserved.
6 //
7 // Redistribution and use in source and binary forms, with or
8 // without modification, are permitted provided that the following
9 // conditions are met:
10 // 
11 //   1. Redistributions of source code must retain the above
12 //      copyright notice, this list of conditions and the following
13 //      disclaimer.
14 // 
15 //   2. Redistributions in binary form must reproduce the above
16 //      copyright notice, this list of conditions and the following
17 //      disclaimer in the documentation and/or other materials
18 //      provided with the distribution.
19 // 
20 // THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
21 // OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
22 // WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
23 // PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
24 // CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
25 // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
26 // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
27 // USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
28 // AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
29 // LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
30 // ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
31 // POSSIBILITY OF SUCH DAMAGE.
32 // 
33 // The views and conclusions contained in the software and
34 // documentation are those of the authors and should not be
35 // interpreted as representing official policies, either expressed
36 // or implied, of GRNET S.A.
37
38 #import "PithosBrowserController.h"
39 #import "PithosNode.h"
40 #import "PithosAccountNode.h"
41 #import "PithosContainerNode.h"
42 #import "PithosSubdirNode.h"
43 #import "PithosObjectNode.h"
44 #import "PithosEmptyNode.h"
45 #import "ImageAndTextCell.h"
46 #import "FileSystemBrowserCell.h"
47 #import "ASIPithosRequest.h"
48 #import "ASIPithosContainerRequest.h"
49 #import "ASIPithosObjectRequest.h"
50 #import "ASIPithosContainer.h"
51 #import "ASIPithosObject.h"
52 #import "PithosFileUtilities.h"
53
54 //@interface PithosBrowserCell : NSBrowserCell {}
55 @interface PithosBrowserCell : FileSystemBrowserCell {}
56 @end
57
58 @implementation PithosBrowserCell
59
60 - (id)init {
61     if ((self = [super init])) {
62         [self setLineBreakMode:NSLineBreakByTruncatingMiddle];
63     }
64     return self;
65 }
66
67 - (void)setObjectValue:(id)object {
68     if ([object isKindOfClass:[PithosNode class]]) {
69         PithosNode *node = (PithosNode *)object;
70         [self setStringValue:node.displayName];
71         [self setImage:node.icon];
72 //        // All cells are set as leafs because a branchingImage is already set!
73 //        // Maybe this cell is already inside an NSBrowserCell
74 //        [self setLeaf:YES];
75     } else {
76         [super setObjectValue:object];
77     }
78 }
79
80 @end
81
82 @interface PithosOutlineViewCell : ImageAndTextCell {}
83 @end
84
85 @implementation PithosOutlineViewCell
86
87 - (void)setObjectValue:(id)object {
88     if ([object isKindOfClass:[PithosNode class]]) {
89         PithosNode *node = (PithosNode *)object;
90         [self setStringValue:node.displayName];
91         [self setImage:node.icon];
92         [self setEditable:NO];
93     } else {
94         [super setObjectValue:object];
95     }
96 }
97
98 @end
99
100 @interface PithosBrowserController (Private) {}
101 - (void)resetContainers;
102 @end
103
104 @implementation PithosBrowserController
105 @synthesize outlineViewDataSourceArray, splitView, outlineView, browser;
106
107 #pragma mark -
108 #pragma Object Lifecycle
109
110 - (id)init {
111     return [super initWithWindowNibName:@"PithosBrowserController"];
112 }
113
114 - (void)dealloc {
115     [[NSNotificationCenter defaultCenter] removeObserver:self];
116     [sharedPreviewController release];
117     [outlineViewDataSourceArray release];
118     [accountNode release];
119     [rootNode release];
120     [browser release];
121     [splitView release];
122     [outlineView release];
123     [super dealloc];
124 }
125
126 - (void)awakeFromNib {
127     [super awakeFromNib];
128     
129     [browser setDraggingSourceOperationMask:NSDragOperationNone forLocal:YES];
130     [browser setDraggingSourceOperationMask:NSDragOperationCopy forLocal:NO];
131     
132     [browser setCellClass:[PithosBrowserCell class]];
133 }
134
135 - (void)resetContainers {
136     rootNode = nil;
137     [browser loadColumnZero];
138     self.outlineViewDataSourceArray = nil;
139     
140     // Create the outlineView tree
141     // CONTAINERS
142         NSTreeNode *containersTreeNode = [NSTreeNode treeNodeWithRepresentedObject:
143                             [[[PithosEmptyNode alloc] initWithDisplayName:@"CONTAINERS" icon:nil] autorelease]];
144 //    // CONTAINERS/pithos
145 //      [[containersTreeNode mutableChildNodes] addObject:
146 //     [NSTreeNode treeNodeWithRepresentedObject:
147 //      [[[PithosContainerNode alloc] initWithContainerName:@"pithos" 
148 //                                                     icon:[[NSWorkspace sharedWorkspace] iconForFileType:NSFileTypeForHFSTypeCode(kToolbarHomeIcon)]
149 //        ] autorelease]]];
150 //    // CONTAINERS/trash
151 //      [[containersTreeNode mutableChildNodes] addObject:
152 //     [NSTreeNode treeNodeWithRepresentedObject:
153 //      [[[PithosContainerNode alloc] initWithContainerName:@"trash"
154 //                                                     icon:[[NSWorkspace sharedWorkspace] iconForFileType:NSFileTypeForHFSTypeCode(kFullTrashIcon)]
155 //        ] autorelease]]];
156     // SHARED
157         NSTreeNode *sharedTreeNode = [NSTreeNode treeNodeWithRepresentedObject:
158                                       [[[PithosEmptyNode alloc] initWithDisplayName:@"SHARED" icon:nil] autorelease]];
159     // SHARED/my shared
160         [[sharedTreeNode mutableChildNodes] addObject:
161      [NSTreeNode treeNodeWithRepresentedObject:
162       [[[PithosEmptyNode alloc] initWithDisplayName:@"my shared" 
163                                                icon:[[NSWorkspace sharedWorkspace] iconForFileType:NSFileTypeForHFSTypeCode(kUserIcon)]
164         ] autorelease]]];
165     // SHARED/others shared
166         [[sharedTreeNode mutableChildNodes] addObject:
167      [NSTreeNode treeNodeWithRepresentedObject:
168       [[[PithosEmptyNode alloc] initWithDisplayName:@"others shared"
169                                                icon:[[NSWorkspace sharedWorkspace] iconForFileType:NSFileTypeForHFSTypeCode(kGroupIcon)]
170         ] autorelease]]];
171     
172     self.outlineViewDataSourceArray = [NSMutableArray arrayWithObjects:containersTreeNode, sharedTreeNode, nil];
173     
174         // Expand the folder outline view
175     [outlineView expandItem:nil expandChildren:YES];
176         [outlineView selectRowIndexes:[NSIndexSet indexSetWithIndex:1] byExtendingSelection:NO];
177     
178     // Create accountNode and trigger a refresh
179     accountNode = [[PithosAccountNode alloc] init];
180     accountNode.children;
181 }
182
183 - (void)windowDidLoad {
184     [super windowDidLoad];
185     
186     [[[outlineView tableColumns] objectAtIndex:0] setDataCell:[[[PithosOutlineViewCell alloc] init] autorelease]];
187     
188     // Register for updates
189     [[NSNotificationCenter defaultCenter] addObserver:self 
190                                              selector:@selector(pithosNodeChildrenUpdated:) 
191                                                  name:@"PithosContainerNodeChildrenUpdated" 
192                                                object:nil];
193     [[NSNotificationCenter defaultCenter] addObserver:self 
194                                              selector:@selector(pithosNodeChildrenUpdated:) 
195                                                  name:@"PithosSubdirNodeChildrenUpdated" 
196                                                object:nil];
197     [[NSNotificationCenter defaultCenter] addObserver:self 
198                                              selector:@selector(pithosAccountNodeChildrenUpdated:) 
199                                                  name:@"PithosAccountNodeChildrenUpdated" 
200                                                object:nil];
201     [[NSNotificationCenter defaultCenter] addObserver:self 
202                                              selector:@selector(resetContainers) 
203                                                  name:@"PithosAuthenticationCredentialsUpdated" 
204                                                object:nil];
205 }
206
207 #pragma mark -
208 #pragma Observers
209
210 - (void)pithosNodeChildrenUpdated:(NSNotification *)notification {
211     PithosNode *node = (PithosNode *)[notification object];
212     NSInteger lastColumn = [browser lastColumn];
213     for (NSInteger column = lastColumn; column >= 0; column--) {
214         if ([[browser parentForItemsInColumn:column] isEqualTo:node]) {
215             [browser reloadColumn:column];
216             if ((column == lastColumn - 1) && ([[browser parentForItemsInColumn:lastColumn] isLeafItem])) {
217                 // This reloads the preview column
218                 [browser setLastColumn:column];
219                 [browser addColumn];
220             }
221             return;
222         }
223     }
224 }
225
226 - (void)pithosAccountNodeChildrenUpdated:(NSNotification *)notification {
227     BOOL containerPithosFound = NO;
228     BOOL containerTrashFound = NO;
229     //NSMutableArray *containersTreeNodeChildren = [[outlineViewDataSourceArray objectAtIndex:0] mutableChildNodes];
230     NSMutableArray *containersTreeNodeChildren = [NSMutableArray array];
231     for (PithosContainerNode *containerNode in accountNode.children) {
232         if ([containerNode.pithosContainer.name isEqualToString:@"pithos"]) {
233             containerNode.icon = [[NSWorkspace sharedWorkspace] iconForFileType:NSFileTypeForHFSTypeCode(kToolbarHomeIcon)];
234             [containersTreeNodeChildren insertObject:[NSTreeNode treeNodeWithRepresentedObject:containerNode] atIndex:0];
235             containerPithosFound = YES;
236         } else if ([containerNode.pithosContainer.name isEqualToString:@"trash"]) {
237             containerNode.icon = [[NSWorkspace sharedWorkspace] iconForFileType:NSFileTypeForHFSTypeCode(kFullTrashIcon)];            
238             NSUInteger insertIndex = 1;
239             if (!containerPithosFound)
240                 insertIndex = 0;
241             [containersTreeNodeChildren insertObject:[NSTreeNode treeNodeWithRepresentedObject:containerNode] atIndex:insertIndex];
242             containerTrashFound = YES;
243         } else {
244             [containersTreeNodeChildren addObject:[NSTreeNode treeNodeWithRepresentedObject:containerNode]];
245         }
246     }
247     BOOL refreshAccountNode = NO;
248     if (!containerPithosFound) {
249         // create pithos
250         ASIPithosContainerRequest *containerRequest = [ASIPithosContainerRequest createOrUpdateContainerRequestWithContainerName:@"pithos"];
251         [containerRequest startSynchronous];
252         if ([containerRequest error]) {
253             NSLog(@"error:%@", [containerRequest error]);
254             // XXX do something on error
255         } else {
256             refreshAccountNode = YES;
257         }
258     }
259     if (!containerTrashFound) {
260         // create trash
261         ASIPithosContainerRequest *containerRequest = [ASIPithosContainerRequest createOrUpdateContainerRequestWithContainerName:@"trash"];
262         [containerRequest startSynchronous];
263         if ([containerRequest error]) {
264             NSLog(@"error:%@", [containerRequest error]);
265             // XXX do something on error
266         } else {
267             refreshAccountNode = YES;
268         }
269     }
270     if (refreshAccountNode) {
271         [accountNode invalidateChildren];
272         accountNode.children;
273     } else {
274         [[[outlineViewDataSourceArray objectAtIndex:0] mutableChildNodes] setArray:containersTreeNodeChildren];
275         self.outlineViewDataSourceArray = outlineViewDataSourceArray;
276         
277         // Expand the folder outline view
278         [outlineView expandItem:nil expandChildren:YES];
279         [outlineView selectRowIndexes:[NSIndexSet indexSetWithIndex:1] byExtendingSelection:NO];
280     }
281 }
282
283 #pragma mark -
284 #pragma Actions
285
286 - (IBAction)refresh:(id)sender {
287     for (NSInteger column = [browser lastColumn]; column >= 0; column--) {
288         [(PithosNode *)[browser parentForItemsInColumn:column] invalidateChildren];
289     }
290     [browser validateVisibleColumns];
291 }
292
293 #pragma mark -
294 #pragma NSBrowserDelegate
295
296 - (id)rootItemForBrowser:(NSBrowser *)browser {
297     return rootNode;    
298 }
299
300 - (NSInteger)browser:(NSBrowser *)browser numberOfChildrenOfItem:(id)item {
301     PithosNode *node = (PithosNode *)item;
302     return node.children.count;
303 }
304
305 - (id)browser:(NSBrowser *)browser child:(NSInteger)index ofItem:(id)item {
306     PithosNode *node = (PithosNode *)item;
307     return [node.children objectAtIndex:index];
308 }
309
310 - (BOOL)browser:(NSBrowser *)browser isLeafItem:(id)item {
311     PithosNode *node = (PithosNode *)item;
312     return node.isLeafItem;
313 }
314
315 - (id)browser:(NSBrowser *)browser objectValueForItem:(id)item {
316     PithosNode *node = (PithosNode *)item;
317     return node;
318 }
319
320 - (NSViewController *)browser:(NSBrowser *)browser previewViewControllerForLeafItem:(id)item {
321     if (sharedPreviewController == nil)
322         sharedPreviewController = [[NSViewController alloc] initWithNibName:@"PithosBrowserPreviewController" bundle:[NSBundle bundleForClass:[self class]]];
323     return sharedPreviewController;
324 }
325
326 //- (CGFloat)browser:(NSBrowser *)browser shouldSizeColumn:(NSInteger)columnIndex forUserResize:(BOOL)forUserResize toWidth:(CGFloat)suggestedWidth  {
327 //    if (!forUserResize) {
328 //        id item = [browser parentForItemsInColumn:columnIndex]; 
329 //        if ([self browser:browser isLeafItem:item]) {
330 //            suggestedWidth = 200; 
331 //        }
332 //    }
333 //    return suggestedWidth;
334 //}
335
336 - (BOOL)browser:(NSBrowser *)sender isColumnValid:(NSInteger)column {
337     return NO;
338 }
339
340 #pragma mark Drag and Drop
341
342 //- (BOOL)browser:(NSBrowser *)aBrowser canDragRowsWithIndexes:(NSIndexSet *)rowIndexes inColumn:(NSInteger)column 
343 //      withEvent:(NSEvent *)event {    
344 //    // XXX allow only objects? or when a subdir is dragged download the whole tree?
345 //    NSIndexPath *baseIndexPath = [browser indexPathForColumn:column]; 
346 //    for (NSUInteger i = [rowIndexes firstIndex]; i <= [rowIndexes lastIndex]; i = [rowIndexes indexGreaterThanIndex:i]) {
347 //        PithosNode *node = [browser itemAtIndexPath:[baseIndexPath indexPathByAddingIndex:i]];
348 //        if (node.pithosObject.subdir)
349 //            return NO;
350 //    }
351 //    return YES;
352 //}
353
354 - (BOOL)browser:(NSBrowser *)aBrowser writeRowsWithIndexes:(NSIndexSet *)rowIndexes inColumn:(NSInteger)column 
355    toPasteboard:(NSPasteboard *)pasteboard {
356     NSMutableArray *propertyList = [NSMutableArray arrayWithCapacity:[rowIndexes count]];
357     NSIndexPath *baseIndexPath = [browser indexPathForColumn:column]; 
358     for (NSUInteger i = [rowIndexes firstIndex]; i <= [rowIndexes lastIndex]; i = [rowIndexes indexGreaterThanIndex:i]) {
359         PithosNode *node = [browser itemAtIndexPath:[baseIndexPath indexPathByAddingIndex:i]];
360         [propertyList addObject:[node.pithosObject.name pathExtension]];
361     }
362
363     [pasteboard declareTypes:[NSArray arrayWithObject:NSFilesPromisePboardType] owner:self];
364     [pasteboard setPropertyList:propertyList forType:NSFilesPromisePboardType];
365     
366     return YES;
367 }
368
369 - (NSArray *)browser:(NSBrowser *)aBrowser namesOfPromisedFilesDroppedAtDestination:(NSURL *)dropDestination 
370 forDraggedRowsWithIndexes:(NSIndexSet *)rowIndexes inColumn:(NSInteger)column {
371     NSMutableArray *names = [NSMutableArray arrayWithCapacity:[rowIndexes count]];
372     NSIndexPath *baseIndexPath = [browser indexPathForColumn:column]; 
373     for (NSUInteger i = [rowIndexes firstIndex]; i <= [rowIndexes lastIndex]; i = [rowIndexes indexGreaterThanIndex:i]) {
374         PithosNode *node = [browser itemAtIndexPath:[baseIndexPath indexPathByAddingIndex:i]];
375         
376         
377         // If the node is a subdir ask if the whole tree should be downloaded
378         if (node.pithosObject.subdir) {
379             NSAlert *alert = [[NSAlert alloc] init];
380             [alert setMessageText:@"Download directory"];
381             [alert setInformativeText:[NSString stringWithFormat:@"'%@' is a directory, do you want to download its contents?", node.displayName]];
382             [alert addButtonWithTitle:@"OK"];
383             [alert addButtonWithTitle:@"Cancel"];
384             NSInteger choice = [alert runModal];
385             if (choice == NSAlertFirstButtonReturn) {
386                 NSArray *objectRequests = [PithosFileUtilities objectDataRequestsForSubdirWithContainerName:node.pithosContainer.name 
387                                                                                                  objectName:node.pithosObject.name 
388                                                                                                 toDirectory:[dropDestination path] 
389                                                                                               checkIfExists:YES];
390                 if (objectRequests) {
391                     for (ASIPithosObjectRequest *objectRequest in objectRequests) {
392                         [names addObject:[objectRequest.userInfo valueForKey:@"fileName"]];
393                         // XXX set delegates and queue
394                         [objectRequest setCompletionBlock:^{
395                             NSLog(@"dl completed: %@", [objectRequest url]);
396                         }];
397                         [objectRequest setFailedBlock:^{
398                             NSLog(@"dl failed: %@, error: %@", [objectRequest url], [objectRequest error]);
399                         }];
400                         [objectRequest startAsynchronous];
401                     }
402                 }
403             }
404         } else {
405             ASIPithosObjectRequest *objectRequest = [PithosFileUtilities objectDataRequestWithContainerName:node.pithosContainer.name 
406                                                                                                  objectName:node.pithosObject.name 
407                                                                                                 toDirectory:[dropDestination path] 
408                                                                                               checkIfExists:YES];
409             if (objectRequest) {
410                 [names addObject:[objectRequest.userInfo valueForKey:@"fileName"]];
411                 // XXX set delegates and queue
412                 [objectRequest setCompletionBlock:^{
413                     NSLog(@"dl completed: %@", [objectRequest url]);
414                 }];
415                 [objectRequest setFailedBlock:^{
416                     NSLog(@"dl failed: %@, error: %@", [objectRequest url], [objectRequest error]);
417                 }];
418                 [objectRequest startAsynchronous];
419             }
420         }
421     }
422     return names;
423 }
424
425 #pragma mark -
426 #pragma mark NSSplitViewDelegate
427
428 - (CGFloat)splitView:(NSSplitView *)splitView constrainMinCoordinate:(CGFloat)proposedMinimumPosition ofSubviewAt:(NSInteger)dividerIndex {
429     return 120;
430 }
431
432 - (CGFloat)splitView:(NSSplitView *)splitView constrainMaxCoordinate:(CGFloat)proposedMaximumPosition ofSubviewAt:(NSInteger)dividerIndex {
433     return 220;
434 }
435
436 #pragma mark -
437 #pragma mark NSOutlineViewDelegate
438
439 - (BOOL)outlineView:outlineView shouldSelectItem:(id)item {
440     return ([[item representedObject] isLeaf]);
441 }
442
443 - (BOOL)outlineView:(NSOutlineView *)outlineView isGroupItem:(id)item {
444         return (![[item representedObject] isLeaf]);
445 }
446
447 - (void)outlineViewSelectionDidChange:(NSNotification *)notification {
448     PithosNode *node = [[[outlineView itemAtRow:[outlineView selectedRow]] representedObject] representedObject];
449     if (node) {
450         rootNode = node;
451         [browser loadColumnZero];
452     }
453 }
454
455 @end