Add preference to skip hidden files during sync.
[pithos-macos] / pithos-macos / PithosPreferencesController.m
1 //
2 //  PithosPreferencesController.m
3 //  pithos-macos
4 //
5 // Copyright 2011-2012 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 "PithosPreferencesController.h"
39 #import "PithosBrowserController.h"
40 #import "PithosAccountNode.h"
41 #import "PithosSharingAccountsNode.h"
42 #import "PithosContainerNode.h"
43 #import "PithosSubdirNode.h"
44 #import "PithosObjectNode.h"
45 #import "PithosEmptyNode.h"
46 #import "PithosAccount.h"
47 #import "pithos_macosAppDelegate.h"
48
49 #import "ImageAndTextCell.h"
50 @interface PithosPreferencesSyncOutlineViewCell : ImageAndTextCell {}
51 @end
52
53 @implementation PithosPreferencesSyncOutlineViewCell
54
55 - (void)setObjectValue:(id)object {
56     if ([object isKindOfClass:[PithosNode class]]) {
57         PithosNode *node = (PithosNode *)object;
58         [self setStringValue:node.displayName];
59         [self setImage:node.icon];
60         [self setEditable:NO];
61     } else {
62         [super setObjectValue:object];
63     }
64 }
65
66 @end
67
68 @implementation PithosPreferencesController
69 @synthesize selectedPithosAccount;
70 @synthesize accountsArrayController;
71 @synthesize accountRemoveEnable;
72 @synthesize serverURL, authUser, authToken, manual, loginEnable, loginCancelEnable;
73 @synthesize syncActive, syncSkipHidden, syncDirectoryPath, syncAccountsDictionary, syncApplyEnable, syncCancelEnable, 
74             syncAccountsOutlineView, syncAccountsRootFilesNodes;
75 @synthesize groupsDictionaryController, selectedGroupMembersDictionaryController;
76
77 #pragma mark -
78 #pragma mark Object Lifecycle
79
80 - (id)init {
81     return [super initWithWindowNibName:@"PithosPreferencesController"];
82 }
83
84 - (void)windowDidLoad {
85     [super windowDidLoad];
86     
87     NSWindow *window = [self window];
88     [window setHidesOnDeactivate:NO];
89     [window setExcludedFromWindowsMenu:YES];
90     
91 //      // Select the first tab when the window is loaded for the first time.
92 //      [[window valueForKeyPath:@"toolbar"] setSelectedItemIdentifier:@"0"];
93     
94     [[[syncAccountsOutlineView tableColumns] objectAtIndex:1] setDataCell:[[[PithosPreferencesSyncOutlineViewCell alloc] init] autorelease]];
95     syncAccountsMyAccountNode = [[PithosEmptyNode alloc] initWithDisplayName:@"<my account>" 
96                                                                         icon:[[NSWorkspace sharedWorkspace] iconForFileType:NSFileTypeForHFSTypeCode(kUserIcon)]];
97     
98     [groupsDictionaryController setInitialKey:@"group"];
99     [groupsDictionaryController setInitialValue:@"user"];
100     [selectedGroupMembersDictionaryController setInitialKey:@"user"];
101     [selectedGroupMembersDictionaryController setInitialValue:@""];
102
103     [window setDelegate:self];
104     
105     self.selectedPithosAccount = [[accountsArrayController selectedObjects] objectAtIndex:0];
106     [accountsArrayController addObserver:self forKeyPath:@"selection" options:NSKeyValueObservingOptionNew context:NULL];
107     [[NSNotificationCenter defaultCenter] addObserver:self 
108                                              selector:@selector(selectedPithosAccountNodeChildrenUpdated:) 
109                                                  name:@"SelectedPithosAccountNodeChildrenUpdated" 
110                                                object:nil];
111 }
112
113 - (BOOL)windowShouldClose:(id)sender {
114     return [(pithos_macosAppDelegate *)[[NSApplication sharedApplication] delegate] activated];
115 }
116
117 //- (void)windowWillClose:(NSNotification *)notification {
118 //}
119
120 //- (IBAction)toolbarItemSelected:(id)sender {
121 //}
122
123 #pragma mark -
124 #pragma Observers
125
126 - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
127     if ([object isEqualTo:accountsArrayController] && 
128         [keyPath isEqualToString:@"selection"] && 
129         [[accountsArrayController selectedObjects] count]) {
130         self.selectedPithosAccount = [[accountsArrayController selectedObjects] objectAtIndex:0];
131     }
132 }
133
134 - (void)selectedPithosAccountNodeChildrenUpdated:(NSNotification *)notification {
135     [syncAccountsOutlineView reloadData];
136 //    [syncAccountsOutlineView expandItem:nil expandChildren:YES];
137 }
138
139 #pragma mark -
140 #pragma Update
141
142 - (void)updateAccounts {
143     pithos_macosAppDelegate *delegate = (pithos_macosAppDelegate *)[[NSApplication sharedApplication] delegate];
144     self.accountRemoveEnable = (delegate.activated && ([delegate.pithosAccounts count] > 1));
145 }
146
147 - (void)updateLogin {
148     self.loginEnable = ([selectedPithosAccount urlIsValid:serverURL] && (!manual || ([authUser length] && [authToken length])));
149     self.loginCancelEnable = (![selectedPithosAccount.serverURL isEqualToString:serverURL] || 
150                               (selectedPithosAccount.authUser && ![selectedPithosAccount.authUser isEqualToString:authUser]) || 
151                               (selectedPithosAccount.authToken && ![selectedPithosAccount.authToken isEqualToString:authToken]));
152 }
153
154 - (void)updateSync {
155     BOOL isDirectory;
156     self.syncApplyEnable = (selectedPithosAccount.active && 
157                             ((selectedPithosAccount.syncActive != syncActive) || 
158                              (selectedPithosAccount.syncSkipHidden != syncSkipHidden) || 
159                              (![selectedPithosAccount.syncDirectoryPath isEqualToString:syncDirectoryPath] && 
160                               (![[NSFileManager defaultManager] fileExistsAtPath:syncDirectoryPath isDirectory:&isDirectory] || 
161                                isDirectory)) ||
162                              ![selectedPithosAccount.syncAccountsDictionary isEqualToDictionary:syncAccountsDictionary]));
163     self.syncCancelEnable = (selectedPithosAccount.active && 
164                              ((selectedPithosAccount.syncActive != syncActive) || 
165                               (selectedPithosAccount.syncSkipHidden != syncSkipHidden) || 
166                               ![selectedPithosAccount.syncDirectoryPath isEqualToString:syncDirectoryPath] ||
167                               ![selectedPithosAccount.syncAccountsDictionary isEqualToDictionary:syncAccountsDictionary]));
168 }
169
170 #pragma mark -
171 #pragma Properties
172
173 - (void)setSelectedPithosAccount:(PithosAccount *)aSelectedPithosAccount {
174     if (aSelectedPithosAccount && ![aSelectedPithosAccount isEqualTo:selectedPithosAccount]) {
175         selectedPithosAccount.accountNode.childrenUpdatedNotificationName = nil;
176         selectedPithosAccount.sharingAccountsNode.childrenUpdatedNotificationName = nil;
177         [selectedPithosAccount release];
178         selectedPithosAccount = [aSelectedPithosAccount retain];
179         selectedPithosAccount.accountNode.childrenUpdatedNotificationName = @"SelectedPithosAccountNodeChildrenUpdated";
180         selectedPithosAccount.sharingAccountsNode.childrenUpdatedNotificationName = @"SelectedPithosAccountNodeChildrenUpdated";
181         
182         [self updateAccounts];
183         [self loginCancel:self];
184         [self syncCancel:self];
185         [self groupsRevert:self];
186     }
187 }
188
189 #pragma Login Properties
190
191 - (void)setServerURL:(NSString *)aServerURL {
192     [serverURL release];
193     serverURL = [aServerURL copy];
194     [self updateLogin];
195 }
196
197 - (void)setAuthUser:(NSString *)anAuthUser {
198     [authUser release];
199     authUser = [anAuthUser copy];
200     [self updateLogin];
201 }
202
203 - (void)setAuthToken:(NSString *)anAuthToken {
204     [authToken release];
205     authToken = [anAuthToken copy];
206     [self updateLogin];
207 }
208
209 - (void)setManual:(BOOL)aManual {
210     manual = aManual;
211     [self updateLogin];
212     if (!manual) {
213         self.authUser = selectedPithosAccount.authUser;
214         self.authToken = selectedPithosAccount.authToken;
215     }
216 }
217
218 #pragma Sync Properties
219
220 - (void)setSyncActive:(BOOL)aSyncActive {
221     syncActive = aSyncActive;
222     [self updateSync];
223 }
224
225 - (void)setSyncSkipHidden:(BOOL)aSyncSkipHidden {
226     syncSkipHidden = aSyncSkipHidden;
227     [self updateSync];
228     [self selectedPithosAccountNodeChildrenUpdated:nil];
229 }
230
231 - (void)setSyncDirectoryPath:(NSString *)aSyncDirectoryPath {
232     [syncDirectoryPath release];
233     syncDirectoryPath = [aSyncDirectoryPath copy];
234     [self updateSync];
235 }
236
237 - (void)setSyncAccountsDictionary:(NSMutableDictionary *)aSyncAccountsDictionary {
238     [syncAccountsDictionary release];
239     syncAccountsDictionary = [[NSMutableDictionary alloc] initWithCapacity:[aSyncAccountsDictionary count]];
240     for (NSString *accountName in aSyncAccountsDictionary) {
241         NSDictionary *aSyncContainersDictionary = [aSyncAccountsDictionary objectForKey:accountName];
242         NSMutableDictionary *syncContainersDictionary = [NSMutableDictionary dictionary];
243         for (NSString *containerName in aSyncContainersDictionary) {
244             if (![accountName isEqualToString:@""] || ![[containerName lowercaseString] isEqualToString:@"shared to me"])
245                 [syncContainersDictionary setObject:[NSMutableSet setWithSet:[aSyncContainersDictionary objectForKey:containerName]] 
246                                              forKey:containerName];
247         }
248         if ([syncContainersDictionary count])
249             [syncAccountsDictionary setObject:syncContainersDictionary forKey:accountName];
250     }
251     [self updateSync];
252 }
253
254 #pragma mark -
255 #pragma Actions
256
257 - (IBAction)addAccount:(id)sender {
258     [accountsArrayController addObject:[PithosAccount pithosAccount]];
259     [self updateAccounts];
260     pithos_macosAppDelegate *delegate = (pithos_macosAppDelegate *)[[NSApplication sharedApplication] delegate];
261     [delegate.pithosAccountsDictionary setObject:selectedPithosAccount forKey:selectedPithosAccount.name];
262     [delegate savePithosAccounts:self];
263 }
264
265 - (IBAction)removeAccount:(id)sender {
266     [self updateAccounts];
267     if (!accountRemoveEnable)
268         return;
269     PithosAccount *removedPithosAccount = [selectedPithosAccount retain];
270     pithos_macosAppDelegate *delegate = (pithos_macosAppDelegate *)[[NSApplication sharedApplication] delegate];
271     if ([delegate.currentPithosAccount isEqualTo:removedPithosAccount] && [delegate.pithosBrowserController operationsPending]) {
272         NSAlert *alert = [[[NSAlert alloc] init] autorelease];
273         [alert setMessageText:@"Operations Pending"];
274         [alert setInformativeText:@"There are pending operations in the browser, do you want to remove the account and cancel them?"];
275         [alert addButtonWithTitle:@"OK"];
276         [alert addButtonWithTitle:@"Cancel"];
277         NSInteger choice = [alert runModal];
278         if (choice == NSAlertSecondButtonReturn) {
279             [removedPithosAccount release];
280             return;
281         }
282     }
283     [accountsArrayController removeObject:selectedPithosAccount];
284     [delegate.pithosAccountsDictionary removeObjectForKey:removedPithosAccount.name];
285     [delegate removedPithosAccount:removedPithosAccount];
286     [delegate savePithosAccounts:self];
287     [removedPithosAccount release];
288     [self updateAccounts];
289 }
290
291 #pragma Login Actions
292
293 - (IBAction)login:(id)sender {
294     self.syncAccountsRootFilesNodes = [NSMutableDictionary dictionary];
295     if (!manual) {
296         [selectedPithosAccount loginWithServerURL:serverURL];
297     } else {
298         [selectedPithosAccount authenticateWithServerURL:serverURL authUser:authUser authToken:authToken];
299         self.manual = NO;
300         pithos_macosAppDelegate *delegate = (pithos_macosAppDelegate *)[[NSApplication sharedApplication] delegate];
301         [delegate savePithosAccounts:self];
302         if (!delegate.activated) {
303             delegate.activated = YES;
304             [delegate showPithosBrowser:self];
305         }
306         if ([selectedPithosAccount isEqualTo:delegate.currentPithosAccount])
307             delegate.pithosBrowserController.pithos = selectedPithosAccount.pithos;
308     }
309 }
310
311 - (IBAction)loginCancel:(id)server {
312     self.serverURL = selectedPithosAccount.serverURL;
313     self.authUser = selectedPithosAccount.authUser;
314     self.authToken = selectedPithosAccount.authToken;
315     self.manual = NO;
316 }
317
318 #pragma Sync Actions
319
320 - (IBAction)syncApply:(id)sender {
321     [selectedPithosAccount updateSyncWithSyncActive:syncActive 
322                                   syncDirectoryPath:syncDirectoryPath 
323                            syncAccountsDictionary:syncAccountsDictionary 
324                                      syncSkipHidden:syncSkipHidden];
325     [self updateSync];
326     pithos_macosAppDelegate *delegate = (pithos_macosAppDelegate *)[[NSApplication sharedApplication] delegate];
327     [delegate savePithosAccounts:self];
328     [delegate sync];
329 }
330
331 - (IBAction)syncCancel:(id)sender {
332     self.syncActive = selectedPithosAccount.syncActive;
333     self.syncDirectoryPath = selectedPithosAccount.syncDirectoryPath;
334     self.syncAccountsDictionary = selectedPithosAccount.syncAccountsDictionary;
335     self.syncAccountsRootFilesNodes = [NSMutableDictionary dictionary];
336     self.syncSkipHidden = selectedPithosAccount.syncSkipHidden;
337 }
338
339 - (IBAction)syncRefresh:(id)sender {
340     selectedPithosAccount.accountNode.forcedRefresh = YES;
341     [selectedPithosAccount.accountNode invalidateChildrenRecursive];
342     selectedPithosAccount.sharingAccountsNode.forcedRefresh = YES;
343     [selectedPithosAccount.sharingAccountsNode invalidateChildrenRecursive];
344     if (selectedPithosAccount.accountNode.children && selectedPithosAccount.sharingAccountsNode.children) {
345     }
346 }
347
348 #pragma mark Groups Actions
349
350 - (IBAction)groupsApply:(id)sender {
351     [[self window] makeFirstResponder:nil];
352     if (selectedPithosAccount.active)
353         [selectedPithosAccount.accountNode applyInfo];
354 }
355
356 - (IBAction)groupsRevert:(id)sender {
357     if (selectedPithosAccount.active && selectedPithosAccount.accountNode)
358         [selectedPithosAccount.accountNode refreshInfo];
359 }
360
361 #pragma mark -
362 #pragma mark NSOutlineViewDataSource
363
364 // <my account> [PithosEmptyNode]
365 // - <container>+ [PithosContainerNode]
366 // -- <subdir>+ [PithosSubdirNode]
367 // -- <root files> [PithosEmptyNode]
368 // <sharing account>+ [PithosSharingAccountNode]
369 // - <container>+ [PithosContainerNode]
370 // -- <subdir>+ [PithosSubdirNode]
371 // -- <root files> [PithosEmptyNode]
372
373 - (NSInteger)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(id)item {
374     if (!selectedPithosAccount.active)
375         return 0;
376     if (outlineView == syncAccountsOutlineView) {
377         if (item == nil) {
378             // root: <my account> + #<sharing account>
379             NSInteger accountsCount = 0;
380             if ([selectedPithosAccount.accountNode.children count])
381                 accountsCount = 1;
382             if (selectedPithosAccount.sharingAccountsNode.children)
383                 accountsCount += selectedPithosAccount.sharingAccountsNode.children.count;
384             return accountsCount;
385         } else if (item == syncAccountsMyAccountNode) {
386             // root/<my account>: #<container>
387             if (selectedPithosAccount.accountNode.children) {
388                 NSInteger containersCount = 0;
389                 for (PithosContainerNode *node in selectedPithosAccount.accountNode.children) {
390                     if (![[node.displayName lowercaseString] isEqualToString:@"shared to me"])
391                         containersCount++;
392                 }
393                 return containersCount;
394             }
395         } else if ([item class] == [PithosAccountNode class]) {
396             // root/<sharing account>: #<container>
397             PithosAccountNode *accountNode = (PithosAccountNode *)item;
398             if (accountNode.children)
399                 return accountNode.children.count;
400         } else if ([item class] == [PithosContainerNode class]) {
401             // root/{<my account>, <sharing account>}/<container>: #<subdir> + <root files>
402             PithosContainerNode *containerNode = (PithosContainerNode *)item;
403             if (containerNode.children) {
404                 // We add 1 for the root files node
405                 NSInteger subdirCount = 1;
406                 for (PithosNode *node in containerNode.children) {
407                     if (([node class] == [PithosSubdirNode class]) && (!syncSkipHidden || ![node.displayName hasPrefix:@"."]))
408                         subdirCount++;
409                 }
410                 return subdirCount;
411             }
412         }
413     }
414     return 0;
415 }
416
417 - (id)outlineView:(NSOutlineView *)outlineView child:(NSInteger)index ofItem:(id)item {
418     if (outlineView == syncAccountsOutlineView) {
419         if (item == nil) {
420             // root: [ <my account>, <sharing account>+ ]
421             if ([selectedPithosAccount.accountNode.children count]) {
422                 if (index == 0)
423                     return syncAccountsMyAccountNode;
424                 else
425                     return [selectedPithosAccount.sharingAccountsNode.children objectAtIndex:(index - 1)];
426             } else {
427                 return [selectedPithosAccount.sharingAccountsNode.children objectAtIndex:index];
428             }
429         } else if (item == syncAccountsMyAccountNode) {
430             // root/<my account>: [ <container>+ ]
431             NSInteger currentContainerIndex = -1;
432             for (PithosContainerNode *node in selectedPithosAccount.accountNode.children) {
433                 if (![[node.displayName lowercaseString] isEqualToString:@"shared to me"]) {
434                     currentContainerIndex++;
435                     if (currentContainerIndex == index)
436                         return node;
437                 }
438             }
439         } else if ([item class] == [PithosAccountNode class]) {
440             // root/<sharing account>: [ <container>+ ]
441             return [((PithosAccountNode *)item).children objectAtIndex:index];
442         } else if ([item class] == [PithosContainerNode class]) {
443             // root/{<my account>, <sharing account>}/<container>: [ <subdir>+, <root files> ]
444             PithosContainerNode *containerNode = (PithosContainerNode *)item;
445             NSInteger currentSubdirIndex = -1;
446             for (PithosNode *node in containerNode.children) {
447                 if (([node class] == [PithosSubdirNode class]) && (!syncSkipHidden || ![node.displayName hasPrefix:@"."])) {
448                     currentSubdirIndex++;
449                     if (currentSubdirIndex == index)
450                         return node;
451                 }
452             }
453             if (++currentSubdirIndex == index) {
454                 NSString *accountName = containerNode.sharingAccount;
455                 if (!accountName)
456                     accountName = @"";
457                 PithosEmptyNode *rootFilesNode = [[syncAccountsRootFilesNodes objectForKey:accountName] 
458                                                   objectForKey:containerNode.displayName];
459                 if (!rootFilesNode) {
460                     if (![syncAccountsRootFilesNodes objectForKey:accountName])
461                         [syncAccountsRootFilesNodes setObject:[NSMutableDictionary dictionary] forKey:accountName];
462                     rootFilesNode = [[[PithosEmptyNode alloc] initWithDisplayName:@"<root files>" 
463                                                                              icon:[[NSWorkspace sharedWorkspace] iconForFileType:@""]] 
464                                      autorelease];
465                     rootFilesNode.parent = containerNode;
466                     [[syncAccountsRootFilesNodes objectForKey:accountName] setObject:rootFilesNode forKey:containerNode.displayName];
467                 }
468                 return rootFilesNode;
469             }
470         }
471     }
472     return nil;
473 }
474
475 - (BOOL)outlineView:(NSOutlineView *)outlineView isItemExpandable:(id)item {
476     if (outlineView == syncAccountsOutlineView) {
477         if ((item == syncAccountsMyAccountNode) || 
478             ([item class] == [PithosAccountNode class]) || 
479             ([item class] == [PithosContainerNode class]))
480             return YES;
481     }
482     return NO;
483 }
484
485 - (id)outlineView:(NSOutlineView *)outlineView objectValueForTableColumn:(NSTableColumn *)tableColumn byItem:(id)item {
486     if (outlineView == syncAccountsOutlineView) {
487         if ([[tableColumn identifier] isEqualToString:@"sync"]) {
488             if (item == syncAccountsMyAccountNode) {
489                 // root/<my account>
490                 // My account is 
491                 // off if not in dictionary
492                 // mixed if in dictionary with exclusions
493                 // on if in dictionary without exclusions
494                 NSMutableDictionary *syncContainersDictionary = [syncAccountsDictionary objectForKey:@""];
495                 if (syncContainersDictionary) {
496                     for (PithosContainerNode *node in selectedPithosAccount.accountNode.children) {
497                         if (![[node.displayName lowercaseString] isEqualToString:@"shared to me"]) {
498                             NSMutableSet *containerExcludedDirectories = [syncContainersDictionary objectForKey:node.displayName];
499                             if (!containerExcludedDirectories || [containerExcludedDirectories count])
500                                 return [NSNumber numberWithUnsignedInteger:NSMixedState];
501                         }
502                     }
503                     return [NSNumber numberWithUnsignedInteger:NSOnState];
504                 }
505                 return [NSNumber numberWithUnsignedInteger:NSOffState];
506             } else if ([item class] == [PithosAccountNode class]) {
507                 // root/<sharing account>
508                 // A sharing account is 
509                 // off if not in dictionary
510                 // mixed if in dictionary with exclusions
511                 // on if in dictionary without exclusions
512                 PithosAccountNode *accountNode = (PithosAccountNode *)item;
513                 NSMutableDictionary *syncContainersDictionary = [syncAccountsDictionary objectForKey:accountNode.displayName];
514                 if (syncContainersDictionary) {
515                     for (PithosContainerNode *node in accountNode.children) {
516                         NSMutableSet *containerExcludedDirectories = [syncContainersDictionary objectForKey:node.displayName];
517                         if (!containerExcludedDirectories || [containerExcludedDirectories count])
518                             return [NSNumber numberWithUnsignedInteger:NSMixedState];
519                     }
520                     return [NSNumber numberWithUnsignedInteger:NSOnState];
521                 }
522                 return [NSNumber numberWithUnsignedInteger:NSOffState];
523             } else if ([item class] == [PithosContainerNode class]) {
524                 // root/{<my account>, <sharing account>}/<container>
525                 // A container is 
526                 // off if not in dictionary
527                 // mixed if in dictionary with exclusions
528                 // on if in dictionary without exclusions
529                 PithosContainerNode *node = (PithosContainerNode *)item;
530                 NSString *accountName = node.sharingAccount;
531                 if (!accountName)
532                     accountName = @"";
533                 NSMutableSet *containerExcludedDirectories = [[syncAccountsDictionary objectForKey:accountName] 
534                                                               objectForKey:node.displayName];
535                 if (containerExcludedDirectories) {
536                     if ([containerExcludedDirectories count])
537                         return [NSNumber numberWithUnsignedInteger:NSMixedState];
538                     else
539                         return [NSNumber numberWithUnsignedInteger:NSOnState];
540                 }
541                 return [NSNumber numberWithUnsignedInteger:NSOffState];
542             } else if ([item class] == [PithosSubdirNode class]) {
543                 // root/{<my account>, <sharing account>}/<container>/<subdir>
544                 // Directory is off if parent container not in dictionary or if excluded
545                 // displayName should be localized and lowercased
546                 PithosSubdirNode *node = (PithosSubdirNode *)item;
547                 NSString *accountName = node.sharingAccount;
548                 if (!accountName)
549                     accountName = @"";
550                 NSMutableSet *containerExcludedDirectories = [[syncAccountsDictionary objectForKey:accountName] 
551                                                               objectForKey:node.parent.displayName];
552                 if (!containerExcludedDirectories || 
553                     [containerExcludedDirectories 
554                      containsObject:[[node.displayName lowercaseString] stringByReplacingOccurrencesOfString:@"/" withString:@":"]])
555                     return [NSNumber numberWithUnsignedInteger:NSOffState];
556                 else
557                     return [NSNumber numberWithUnsignedInteger:NSOnState];
558             } else if ([item class] == [PithosEmptyNode class]) {
559                 // root/{<my account>, <sharing account>}/<container>/<root files>
560                 // Root files is off if parent container not in dictionary or if excluded
561                 PithosEmptyNode *node = (PithosEmptyNode *)item;
562                 NSString *accountName = node.parent.sharingAccount;
563                 if (!accountName)
564                     accountName = @"";
565                 NSMutableSet *containerExcludedDirectories = [[syncAccountsDictionary objectForKey:accountName] 
566                                                                 objectForKey:node.parent.displayName];
567                 if (!containerExcludedDirectories || [containerExcludedDirectories containsObject:@""])
568                     return [NSNumber numberWithUnsignedInteger:NSOffState];
569                 else
570                     return [NSNumber numberWithUnsignedInteger:NSOnState];
571             }
572             return [NSNumber numberWithUnsignedInteger:NSOffState];
573         } else if ([[tableColumn identifier] isEqualToString:@"path"]) {
574             return (PithosNode *)item;
575         }
576     }
577     return nil;
578 }
579
580 - (void)outlineView:(NSOutlineView *)outlineView setObjectValue:(id)object forTableColumn:(NSTableColumn *)tableColumn byItem:(id)item {
581     if (outlineView == syncAccountsOutlineView) {
582         if ([[tableColumn identifier] isEqualToString:@"sync"]) {
583             NSCellStateValue newState = [object unsignedIntegerValue];
584             if (item == syncAccountsMyAccountNode) {
585                 // root/<my account>
586                 // If new state is
587                 // mixed/on include my account with no exclusions
588                 // off exclude my account
589                 if ((newState == NSOnState) || (newState == NSMixedState)) {
590                     NSMutableDictionary *syncContainersDictionary = [NSMutableDictionary dictionary];
591                     for (PithosContainerNode *node in selectedPithosAccount.accountNode.children) {
592                         if (![[node.displayName lowercaseString] isEqualToString:@"shared to me"])
593                             [syncContainersDictionary setObject:[NSMutableSet set] forKey:node.displayName];
594                     }
595                     [syncAccountsDictionary setObject:syncContainersDictionary forKey:@""];
596                 } else {
597                     [syncAccountsDictionary removeObjectForKey:@""];
598                 }
599                 [outlineView reloadItem:item reloadChildren:YES];
600             } else if ([item class] == [PithosAccountNode class]) {
601                 // root/<sharing account>
602                 // If new state is
603                 // mixed/on include sharing account with no exclusions
604                 // off exclude sharing account
605                 PithosAccountNode *accountNode = (PithosAccountNode *)item;
606                 if ((newState == NSOnState) || (newState == NSMixedState)) {
607                     NSMutableDictionary *syncContainersDictionary = [NSMutableDictionary dictionary];
608                     for (PithosContainerNode *node in accountNode.children) {
609                         [syncContainersDictionary setObject:[NSMutableSet set] forKey:node.displayName];
610                     }
611                     [syncAccountsDictionary setObject:syncContainersDictionary forKey:accountNode.displayName];
612                 } else {
613                     [syncAccountsDictionary removeObjectForKey:accountNode.displayName];
614                 }
615                 [outlineView reloadItem:item reloadChildren:YES];
616             } else if ([item class] == [PithosContainerNode class]) {
617                 // root/{<my account>, <sharing account>}/<container>
618                 // If new state is
619                 // mixed/on include container with no excluded directories
620                 // off exclude container
621                 PithosContainerNode *node = (PithosContainerNode *)item;
622                 NSString *accountName = node.sharingAccount;
623                 PithosNode *accountNode = node.parent;
624                 if (!accountName) {
625                     accountName = @"";
626                     accountNode = syncAccountsMyAccountNode;
627                 }
628                 NSMutableDictionary *syncContainersDictionary = [syncAccountsDictionary objectForKey:accountName];
629                 if ((newState == NSOnState) || (newState == NSMixedState)) {
630                     if (!syncContainersDictionary) {
631                         syncContainersDictionary = [NSMutableDictionary dictionary];
632                         [syncAccountsDictionary setObject:syncContainersDictionary forKey:accountName];
633                     }
634                     [syncContainersDictionary setObject:[NSMutableSet set] forKey:node.displayName];
635                 } else if (syncContainersDictionary) {
636                     [syncContainersDictionary removeObjectForKey:node.displayName];
637                     if (![syncContainersDictionary count])
638                         [syncAccountsDictionary removeObjectForKey:accountName];
639                 }
640                 [outlineView reloadItem:accountNode reloadChildren:YES];
641             } else if ([item class] == [PithosSubdirNode class]) {
642                 // root/{<my account>, <sharing account>}/<container>/<subdir>
643                 // If new state is
644                 // mixed/on include directory (if container not included, include and exclude all others)
645                 // off exclude directory
646                 PithosSubdirNode *node = (PithosSubdirNode *)item;
647                 NSString *accountName = node.sharingAccount;
648                 PithosNode *accountNode = node.parent.parent;
649                 if (!accountName) {
650                     accountName = @"";
651                     accountNode = syncAccountsMyAccountNode;
652                 }
653                 NSMutableDictionary *syncContainersDictionary = [syncAccountsDictionary objectForKey:accountName];
654                 NSMutableSet *containerExcludedDirectories = [syncContainersDictionary objectForKey:node.parent.displayName];
655                 NSString *directoryName = [[node.displayName lowercaseString] stringByReplacingOccurrencesOfString:@"/" withString:@":"];
656                 if ((newState == NSOnState) || (newState == NSMixedState)) {
657                     if (containerExcludedDirectories) {
658                         [containerExcludedDirectories removeObject:directoryName];
659                     } else {
660                         if (!syncContainersDictionary) {
661                             syncContainersDictionary = [NSMutableDictionary dictionary];
662                             [syncAccountsDictionary setObject:syncContainersDictionary forKey:accountName];
663                         }
664                         NSMutableSet *newContainerExcludeDirectories = [NSMutableSet setWithObject:@""];
665                         for (PithosNode *siblingNode in node.parent.children) {
666                             if (([siblingNode class] == [PithosSubdirNode class]) && 
667                                 (!syncSkipHidden || ![siblingNode.displayName hasPrefix:@"."])) {
668                                 NSString *siblingDirectoryName = [[siblingNode.displayName lowercaseString] 
669                                                                   stringByReplacingOccurrencesOfString:@"/" withString:@":"];
670                                 if (![siblingDirectoryName isEqualToString:directoryName] && 
671                                     ![newContainerExcludeDirectories containsObject:siblingDirectoryName])
672                                     [newContainerExcludeDirectories addObject:siblingDirectoryName];
673                             }
674                         }
675                         [syncContainersDictionary setObject:newContainerExcludeDirectories forKey:node.parent.displayName];
676                     }
677                 } else if (syncContainersDictionary && 
678                            containerExcludedDirectories && 
679                            ![containerExcludedDirectories containsObject:directoryName]) {
680                     [containerExcludedDirectories addObject:directoryName];
681                 }
682                 [outlineView reloadItem:accountNode reloadChildren:YES];
683             } else if ([item class] == [PithosEmptyNode class]) {
684                 // If new state is
685                 // mixed/on include root files (if container not included, include and exclude all others)
686                 // off exclude root files
687                 PithosEmptyNode *node = (PithosEmptyNode *)item;
688                 NSString *accountName = node.parent.sharingAccount;
689                 PithosNode *accountNode = node.parent.parent;
690                 if (!accountName) {
691                     accountName = @"";
692                     accountNode = syncAccountsMyAccountNode;
693                 }
694                 NSMutableDictionary *syncContainersDictionary = [syncAccountsDictionary objectForKey:accountName];
695                 NSMutableSet *containerExcludedDirectories = [syncContainersDictionary objectForKey:node.parent.displayName];
696                 if ((newState == NSOnState) || (newState == NSMixedState)) {
697                     if (containerExcludedDirectories) {
698                         [containerExcludedDirectories removeObject:@""];
699                     } else {
700                         if (!syncContainersDictionary) {
701                             syncContainersDictionary = [NSMutableDictionary dictionary];
702                             [syncAccountsDictionary setObject:syncContainersDictionary forKey:accountName];
703                         }
704                         NSMutableSet *newContainerExcludeDirectories = [NSMutableSet set];
705                         for (PithosNode *siblingNode in node.parent.children) {
706                             if (([siblingNode class] == [PithosSubdirNode class]) && 
707                                 (!syncSkipHidden || ![siblingNode.displayName hasPrefix:@"."])) {
708                                 NSString *siblingDirectoryName = [[siblingNode.displayName lowercaseString] 
709                                                                   stringByReplacingOccurrencesOfString:@"/" withString:@":"];
710                                 if (![newContainerExcludeDirectories containsObject:siblingDirectoryName])
711                                     [newContainerExcludeDirectories addObject:siblingDirectoryName];
712                             }
713                         }
714                         [syncContainersDictionary setObject:newContainerExcludeDirectories forKey:node.parent.displayName];
715                     }
716                 } else if (syncContainersDictionary && 
717                            containerExcludedDirectories && 
718                            ![containerExcludedDirectories containsObject:@""]) {
719                     [containerExcludedDirectories addObject:@""];
720                 }
721                 [outlineView reloadItem:accountNode reloadChildren:YES];
722             }
723             [self updateSync];
724         }
725     }
726 }
727
728 @end