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