Initial implementation of browser context menu.
[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 - (void)getInfo:(NSMenuItem *)sender;
103 @end
104
105 @implementation PithosBrowserController
106 @synthesize outlineViewDataSourceArray, splitView, outlineView, browser;
107
108 #pragma mark -
109 #pragma Object Lifecycle
110
111 - (id)init {
112     return [super initWithWindowNibName:@"PithosBrowserController"];
113 }
114
115 - (void)dealloc {
116     [[NSNotificationCenter defaultCenter] removeObserver:self];
117     [browserMenu release];
118     [sharedPreviewController release];
119     [outlineViewDataSourceArray release];
120     [accountNode release];
121     [rootNode release];
122     [super dealloc];
123 }
124
125 - (void)awakeFromNib {
126     [super awakeFromNib];
127     
128     [browser setDraggingSourceOperationMask:NSDragOperationNone forLocal:YES];
129     [browser setDraggingSourceOperationMask:NSDragOperationCopy forLocal:NO];
130     
131     [browser setCellClass:[PithosBrowserCell class]];
132     
133     browserMenu = [[NSMenu alloc] init];
134     [browserMenu setDelegate:self];
135     [browser setMenu:browserMenu];
136 }
137
138 - (void)resetContainers {
139     rootNode = nil;
140     [browser loadColumnZero];
141     self.outlineViewDataSourceArray = nil;
142     
143     // Create the outlineView tree
144     // CONTAINERS
145         NSTreeNode *containersTreeNode = [NSTreeNode treeNodeWithRepresentedObject:
146                             [[[PithosEmptyNode alloc] initWithDisplayName:@"CONTAINERS" icon:nil] autorelease]];
147 //    // CONTAINERS/pithos
148 //      [[containersTreeNode mutableChildNodes] addObject:
149 //     [NSTreeNode treeNodeWithRepresentedObject:
150 //      [[[PithosContainerNode alloc] initWithContainerName:@"pithos" 
151 //                                                     icon:[[NSWorkspace sharedWorkspace] iconForFileType:NSFileTypeForHFSTypeCode(kToolbarHomeIcon)]
152 //        ] autorelease]]];
153 //    // CONTAINERS/trash
154 //      [[containersTreeNode mutableChildNodes] addObject:
155 //     [NSTreeNode treeNodeWithRepresentedObject:
156 //      [[[PithosContainerNode alloc] initWithContainerName:@"trash"
157 //                                                     icon:[[NSWorkspace sharedWorkspace] iconForFileType:NSFileTypeForHFSTypeCode(kFullTrashIcon)]
158 //        ] autorelease]]];
159     // SHARED
160         NSTreeNode *sharedTreeNode = [NSTreeNode treeNodeWithRepresentedObject:
161                                       [[[PithosEmptyNode alloc] initWithDisplayName:@"SHARED" icon:nil] autorelease]];
162     // SHARED/my shared
163         [[sharedTreeNode mutableChildNodes] addObject:
164      [NSTreeNode treeNodeWithRepresentedObject:
165       [[[PithosEmptyNode alloc] initWithDisplayName:@"my shared" 
166                                                icon:[[NSWorkspace sharedWorkspace] iconForFileType:NSFileTypeForHFSTypeCode(kUserIcon)]
167         ] autorelease]]];
168     // SHARED/others shared
169         [[sharedTreeNode mutableChildNodes] addObject:
170      [NSTreeNode treeNodeWithRepresentedObject:
171       [[[PithosEmptyNode alloc] initWithDisplayName:@"others shared"
172                                                icon:[[NSWorkspace sharedWorkspace] iconForFileType:NSFileTypeForHFSTypeCode(kGroupIcon)]
173         ] autorelease]]];
174     
175     self.outlineViewDataSourceArray = [NSMutableArray arrayWithObjects:containersTreeNode, sharedTreeNode, nil];
176     
177         // Expand the folder outline view
178     [outlineView expandItem:nil expandChildren:YES];
179         [outlineView selectRowIndexes:[NSIndexSet indexSetWithIndex:1] byExtendingSelection:NO];
180     
181     // Create accountNode and trigger a refresh
182     accountNode = [[PithosAccountNode alloc] init];
183     accountNode.children;
184 }
185
186 - (void)windowDidLoad {
187     [super windowDidLoad];
188     
189     [[[outlineView tableColumns] objectAtIndex:0] setDataCell:[[[PithosOutlineViewCell alloc] init] autorelease]];
190     
191     // Register for updates
192     [[NSNotificationCenter defaultCenter] addObserver:self 
193                                              selector:@selector(pithosNodeChildrenUpdated:) 
194                                                  name:@"PithosContainerNodeChildrenUpdated" 
195                                                object:nil];
196     [[NSNotificationCenter defaultCenter] addObserver:self 
197                                              selector:@selector(pithosNodeChildrenUpdated:) 
198                                                  name:@"PithosSubdirNodeChildrenUpdated" 
199                                                object:nil];
200     [[NSNotificationCenter defaultCenter] addObserver:self 
201                                              selector:@selector(pithosAccountNodeChildrenUpdated:) 
202                                                  name:@"PithosAccountNodeChildrenUpdated" 
203                                                object:nil];
204     [[NSNotificationCenter defaultCenter] addObserver:self 
205                                              selector:@selector(resetContainers) 
206                                                  name:@"PithosAuthenticationCredentialsUpdated" 
207                                                object:nil];
208 }
209
210 #pragma mark -
211 #pragma Observers
212
213 - (void)pithosNodeChildrenUpdated:(NSNotification *)notification {
214     PithosNode *node = (PithosNode *)[notification object];
215     NSInteger lastColumn = [browser lastColumn];
216     for (NSInteger column = lastColumn; column >= 0; column--) {
217         if ([[browser parentForItemsInColumn:column] isEqualTo:node]) {
218             [browser reloadColumn:column];
219             if ((column == lastColumn - 1) && ([[browser parentForItemsInColumn:lastColumn] isLeafItem])) {
220                 // This reloads the preview column
221                 [browser setLastColumn:column];
222                 [browser addColumn];
223             }
224             return;
225         }
226     }
227 }
228
229 - (void)pithosAccountNodeChildrenUpdated:(NSNotification *)notification {
230     BOOL containerPithosFound = NO;
231     BOOL containerTrashFound = NO;
232     //NSMutableArray *containersTreeNodeChildren = [[outlineViewDataSourceArray objectAtIndex:0] mutableChildNodes];
233     NSMutableArray *containersTreeNodeChildren = [NSMutableArray array];
234     for (PithosContainerNode *containerNode in accountNode.children) {
235         if ([containerNode.pithosContainer.name isEqualToString:@"pithos"]) {
236             containerNode.icon = [[NSWorkspace sharedWorkspace] iconForFileType:NSFileTypeForHFSTypeCode(kToolbarHomeIcon)];
237             [containersTreeNodeChildren insertObject:[NSTreeNode treeNodeWithRepresentedObject:containerNode] atIndex:0];
238             containerPithosFound = YES;
239         } else if ([containerNode.pithosContainer.name isEqualToString:@"trash"]) {
240             containerNode.icon = [[NSWorkspace sharedWorkspace] iconForFileType:NSFileTypeForHFSTypeCode(kFullTrashIcon)];            
241             NSUInteger insertIndex = 1;
242             if (!containerPithosFound)
243                 insertIndex = 0;
244             [containersTreeNodeChildren insertObject:[NSTreeNode treeNodeWithRepresentedObject:containerNode] atIndex:insertIndex];
245             containerTrashFound = YES;
246         } else {
247             [containersTreeNodeChildren addObject:[NSTreeNode treeNodeWithRepresentedObject:containerNode]];
248         }
249     }
250     BOOL refreshAccountNode = NO;
251     if (!containerPithosFound) {
252         // create pithos
253         ASIPithosContainerRequest *containerRequest = [ASIPithosContainerRequest createOrUpdateContainerRequestWithContainerName:@"pithos"];
254         [containerRequest startSynchronous];
255         if ([containerRequest error]) {
256             NSLog(@"error:%@", [containerRequest error]);
257             // XXX do something on error
258         } else {
259             refreshAccountNode = YES;
260         }
261     }
262     if (!containerTrashFound) {
263         // create trash
264         ASIPithosContainerRequest *containerRequest = [ASIPithosContainerRequest createOrUpdateContainerRequestWithContainerName:@"trash"];
265         [containerRequest startSynchronous];
266         if ([containerRequest error]) {
267             NSLog(@"error:%@", [containerRequest error]);
268             // XXX do something on error
269         } else {
270             refreshAccountNode = YES;
271         }
272     }
273     if (refreshAccountNode) {
274         [accountNode invalidateChildren];
275         accountNode.children;
276     } else {
277         [[[outlineViewDataSourceArray objectAtIndex:0] mutableChildNodes] setArray:containersTreeNodeChildren];
278         self.outlineViewDataSourceArray = outlineViewDataSourceArray;
279         
280         // Expand the folder outline view
281         [outlineView expandItem:nil expandChildren:YES];
282         [outlineView selectRowIndexes:[NSIndexSet indexSetWithIndex:1] byExtendingSelection:NO];
283     }
284 }
285
286 #pragma mark -
287 #pragma Actions
288
289 - (IBAction)refresh:(id)sender {
290     for (NSInteger column = [browser lastColumn]; column >= 0; column--) {
291         [(PithosNode *)[browser parentForItemsInColumn:column] invalidateChildren];
292     }
293     [browser validateVisibleColumns];
294 }
295
296 #pragma mark -
297 #pragma NSBrowserDelegate
298
299 - (id)rootItemForBrowser:(NSBrowser *)browser {
300     return rootNode;    
301 }
302
303 - (NSInteger)browser:(NSBrowser *)browser numberOfChildrenOfItem:(id)item {
304     PithosNode *node = (PithosNode *)item;
305     return node.children.count;
306 }
307
308 - (id)browser:(NSBrowser *)browser child:(NSInteger)index ofItem:(id)item {
309     PithosNode *node = (PithosNode *)item;
310     return [node.children objectAtIndex:index];
311 }
312
313 - (BOOL)browser:(NSBrowser *)browser isLeafItem:(id)item {
314     PithosNode *node = (PithosNode *)item;
315     return node.isLeafItem;
316 }
317
318 - (id)browser:(NSBrowser *)browser objectValueForItem:(id)item {
319     PithosNode *node = (PithosNode *)item;
320     return node;
321 }
322
323 - (NSViewController *)browser:(NSBrowser *)browser previewViewControllerForLeafItem:(id)item {
324     if (sharedPreviewController == nil)
325         sharedPreviewController = [[NSViewController alloc] initWithNibName:@"PithosBrowserPreviewController" bundle:[NSBundle bundleForClass:[self class]]];
326     return sharedPreviewController;
327 }
328
329 //- (CGFloat)browser:(NSBrowser *)browser shouldSizeColumn:(NSInteger)columnIndex forUserResize:(BOOL)forUserResize toWidth:(CGFloat)suggestedWidth  {
330 //    if (!forUserResize) {
331 //        id item = [browser parentForItemsInColumn:columnIndex]; 
332 //        if ([self browser:browser isLeafItem:item]) {
333 //            suggestedWidth = 200; 
334 //        }
335 //    }
336 //    return suggestedWidth;
337 //}
338
339 - (BOOL)browser:(NSBrowser *)sender isColumnValid:(NSInteger)column {
340     return NO;
341 }
342
343 #pragma mark Drag and Drop
344
345 //- (BOOL)browser:(NSBrowser *)aBrowser canDragRowsWithIndexes:(NSIndexSet *)rowIndexes inColumn:(NSInteger)column 
346 //      withEvent:(NSEvent *)event {    
347 //    // XXX allow only objects? or when a subdir is dragged download the whole tree?
348 //    NSIndexPath *baseIndexPath = [browser indexPathForColumn:column]; 
349 //    for (NSUInteger i = [rowIndexes firstIndex]; i <= [rowIndexes lastIndex]; i = [rowIndexes indexGreaterThanIndex:i]) {
350 //        PithosNode *node = [browser itemAtIndexPath:[baseIndexPath indexPathByAddingIndex:i]];
351 //        if (node.pithosObject.subdir)
352 //            return NO;
353 //    }
354 //    return YES;
355 //}
356
357 - (BOOL)browser:(NSBrowser *)aBrowser writeRowsWithIndexes:(NSIndexSet *)rowIndexes inColumn:(NSInteger)column 
358    toPasteboard:(NSPasteboard *)pasteboard {
359     NSMutableArray *propertyList = [NSMutableArray arrayWithCapacity:[rowIndexes count]];
360     NSIndexPath *baseIndexPath = [browser indexPathForColumn:column]; 
361     for (NSUInteger i = [rowIndexes firstIndex]; i <= [rowIndexes lastIndex]; i = [rowIndexes indexGreaterThanIndex:i]) {
362         PithosNode *node = [browser itemAtIndexPath:[baseIndexPath indexPathByAddingIndex:i]];
363         [propertyList addObject:[node.pithosObject.name pathExtension]];
364     }
365
366     [pasteboard declareTypes:[NSArray arrayWithObject:NSFilesPromisePboardType] owner:self];
367     [pasteboard setPropertyList:propertyList forType:NSFilesPromisePboardType];
368     
369     return YES;
370 }
371
372 - (NSArray *)browser:(NSBrowser *)aBrowser namesOfPromisedFilesDroppedAtDestination:(NSURL *)dropDestination 
373 forDraggedRowsWithIndexes:(NSIndexSet *)rowIndexes inColumn:(NSInteger)column {
374     NSMutableArray *names = [NSMutableArray arrayWithCapacity:[rowIndexes count]];
375     NSIndexPath *baseIndexPath = [browser indexPathForColumn:column]; 
376     for (NSUInteger i = [rowIndexes firstIndex]; i <= [rowIndexes lastIndex]; i = [rowIndexes indexGreaterThanIndex:i]) {
377         PithosNode *node = [browser itemAtIndexPath:[baseIndexPath indexPathByAddingIndex:i]];
378         
379         
380         // If the node is a subdir ask if the whole tree should be downloaded
381         if (node.pithosObject.subdir) {
382             NSAlert *alert = [[NSAlert alloc] init];
383             [alert setMessageText:@"Download directory"];
384             [alert setInformativeText:[NSString stringWithFormat:@"'%@' is a directory, do you want to download its contents?", node.displayName]];
385             [alert addButtonWithTitle:@"OK"];
386             [alert addButtonWithTitle:@"Cancel"];
387             NSInteger choice = [alert runModal];
388             if (choice == NSAlertFirstButtonReturn) {
389                 NSArray *objectRequests = [PithosFileUtilities objectDataRequestsForSubdirWithContainerName:node.pithosContainer.name 
390                                                                                                  objectName:node.pithosObject.name 
391                                                                                                 toDirectory:[dropDestination path] 
392                                                                                               checkIfExists:YES];
393                 if (objectRequests) {
394                     for (ASIPithosObjectRequest *objectRequest in objectRequests) {
395                         [names addObject:[objectRequest.userInfo valueForKey:@"fileName"]];
396                         // XXX set delegates and queue
397                         [objectRequest setCompletionBlock:^{
398                             NSLog(@"dl completed: %@", [objectRequest url]);
399                         }];
400                         [objectRequest setFailedBlock:^{
401                             NSLog(@"dl failed: %@, error: %@", [objectRequest url], [objectRequest error]);
402                         }];
403                         [objectRequest startAsynchronous];
404                     }
405                 }
406             }
407         } else {
408             ASIPithosObjectRequest *objectRequest = [PithosFileUtilities objectDataRequestWithContainerName:node.pithosContainer.name 
409                                                                                                  objectName:node.pithosObject.name 
410                                                                                                 toDirectory:[dropDestination path] 
411                                                                                               checkIfExists:YES];
412             if (objectRequest) {
413                 [names addObject:[objectRequest.userInfo valueForKey:@"fileName"]];
414                 // XXX set delegates and queue
415                 [objectRequest setCompletionBlock:^{
416                     NSLog(@"dl completed: %@", [objectRequest url]);
417                 }];
418                 [objectRequest setFailedBlock:^{
419                     NSLog(@"dl failed: %@, error: %@", [objectRequest url], [objectRequest error]);
420                 }];
421                 [objectRequest startAsynchronous];
422             }
423         }
424     }
425     return names;
426 }
427
428 #pragma mark -
429 #pragma mark NSSplitViewDelegate
430
431 - (CGFloat)splitView:(NSSplitView *)splitView constrainMinCoordinate:(CGFloat)proposedMinimumPosition ofSubviewAt:(NSInteger)dividerIndex {
432     return 120;
433 }
434
435 - (CGFloat)splitView:(NSSplitView *)splitView constrainMaxCoordinate:(CGFloat)proposedMaximumPosition ofSubviewAt:(NSInteger)dividerIndex {
436     return 220;
437 }
438
439 #pragma mark -
440 #pragma mark NSOutlineViewDelegate
441
442 - (BOOL)outlineView:outlineView shouldSelectItem:(id)item {
443     return ([[item representedObject] isLeaf]);
444 }
445
446 - (BOOL)outlineView:(NSOutlineView *)outlineView isGroupItem:(id)item {
447         return (![[item representedObject] isLeaf]);
448 }
449
450 - (void)outlineViewSelectionDidChange:(NSNotification *)notification {
451     PithosNode *node = [[[outlineView itemAtRow:[outlineView selectedRow]] representedObject] representedObject];
452     if (node) {
453         rootNode = node;
454         [browser loadColumnZero];
455     }
456 }
457
458 #pragma mark -
459 #pragma mark NSMenuDelegate
460
461 - (void)menuNeedsUpdate:(NSMenu *)menu {
462     NSInteger column = [browser clickedColumn];
463     NSInteger row = [browser clickedRow];
464     [menu removeAllItems];
465     if ((column == -1) || (row == -1)) {
466         // General context menu has 0
467     } else {
468         // PithosNode menu has 1 items
469         // Get Info
470         NSMenuItem *menuItem = [[NSMenuItem alloc] initWithTitle:@"Get Info" action:@selector(getInfo:) keyEquivalent:@""];
471         [menuItem setRepresentedObject:[browser itemAtRow:row inColumn:column]];
472         [menu addItem:menuItem];
473     }
474 }
475
476 #pragma mark -
477 #pragma mark Menu Actions
478
479 - (void)getInfo:(NSMenuItem *)sender {
480     [(PithosNode *)[sender representedObject] showPithosNodeInfo:sender];
481 }
482
483 @end