Convert to Objective-C ARC
[pithos-macos] / pithos-macos / PithosPreferencesController.m
index 23d1e95..c978c02 100644 (file)
@@ -2,7 +2,7 @@
 //  PithosPreferencesController.m
 //  pithos-macos
 //
-// Copyright 2011 GRNET S.A. All rights reserved.
+// Copyright 2011-2012 GRNET S.A. All rights reserved.
 //
 // Redistribution and use in source and binary forms, with or
 // without modification, are permitted provided that the following
 // or implied, of GRNET S.A.
 
 #import "PithosPreferencesController.h"
+#import "PithosBrowserController.h"
+#import "PithosAccountNode.h"
+#import "PithosSharingAccountsNode.h"
+#import "PithosContainerNode.h"
+#import "PithosSubdirNode.h"
+#import "PithosObjectNode.h"
+#import "PithosEmptyNode.h"
+#import "PithosAccount.h"
 #import "pithos_macosAppDelegate.h"
 
+#import "ImageAndTextCell.h"
+@interface PithosPreferencesSyncOutlineViewCell : ImageAndTextCell {}
+@end
+
+@implementation PithosPreferencesSyncOutlineViewCell
+
+- (void)setObjectValue:(id)object {
+    if ([object isKindOfClass:[PithosNode class]]) {
+        PithosNode *node = (PithosNode *)object;
+        [self setStringValue:node.displayName];
+        [self setImage:node.icon];
+        [self setEditable:NO];
+    } else {
+        [super setObjectValue:object];
+    }
+}
+
+@end
+
 @implementation PithosPreferencesController
-@synthesize userDefaultsController;
-@synthesize authenticationUserTextField, authenticationTokenTextField, authenticationRenewCheckBox;
+@synthesize selectedPithosAccount;
+@synthesize accountsArrayController;
+@synthesize accountRemoveEnable;
+@synthesize serverURL, authUser, authToken, manual, loginEnable, loginCancelEnable;
+@synthesize syncActive, syncSkipHidden, syncDirectoryPath, syncAccountsDictionary, syncApplyEnable, syncCancelEnable, 
+            syncAccountsOutlineView, syncAccountsRootFilesNodes;
+@synthesize groupsDictionaryController, selectedGroupMembersDictionaryController;
+
+#pragma mark -
+#pragma mark Object Lifecycle
 
 - (id)init {
     return [super initWithWindowNibName:@"PithosPreferencesController"];
 }
 
-- (void)dealloc {
-    [authenticationRenewCheckBox release];
-    [authenticationTokenTextField release];
-    [authenticationUserTextField release];
-    [userDefaultsController release];
-    [super dealloc];
-}
-
 - (void)windowDidLoad {
     [super windowDidLoad];
     
     [window setHidesOnDeactivate:NO];
     [window setExcludedFromWindowsMenu:YES];
     
-       // Select the first tab when the window is loaded for the first time.
-       [[window valueForKeyPath:@"toolbar"] setSelectedItemIdentifier:@"0"];
+//     // Select the first tab when the window is loaded for the first time.
+//     [[window valueForKeyPath:@"toolbar"] setSelectedItemIdentifier:@"0"];
     
-//    // Monitor changes.
-//    [userDefaultsController addObserver:self
-//                             forKeyPath:@"values.authUser"
-//                                options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld)
-//                                context:NULL];
-//    [userDefaultsController addObserver:self
-//                             forKeyPath:@"values.authToken"
-//                                options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld)
-//                                context:NULL];
+    [[[syncAccountsOutlineView tableColumns] objectAtIndex:1] setDataCell:[[PithosPreferencesSyncOutlineViewCell alloc] init]];
+    syncAccountsMyAccountNode = [[PithosEmptyNode alloc] initWithDisplayName:@"<my account>" 
+                                                                        icon:[[NSWorkspace sharedWorkspace] iconForFileType:NSFileTypeForHFSTypeCode(kUserIcon)]];
     
+    [groupsDictionaryController setInitialKey:@"group"];
+    [groupsDictionaryController setInitialValue:@"user"];
+    [selectedGroupMembersDictionaryController setInitialKey:@"user"];
+    [selectedGroupMembersDictionaryController setInitialValue:@""];
+
     [window setDelegate:self];
+    
+    self.selectedPithosAccount = [[accountsArrayController selectedObjects] objectAtIndex:0];
+    [accountsArrayController addObserver:self forKeyPath:@"selection" options:NSKeyValueObservingOptionNew context:NULL];
+    [[NSNotificationCenter defaultCenter] addObserver:self 
+                                             selector:@selector(selectedPithosAccountNodeChildrenUpdated:) 
+                                                 name:@"SelectedPithosAccountNodeChildrenUpdated" 
+                                               object:nil];
 }
 
-// We do this to catch the case where the user enters a value into one of
-// the text fields but closes the window without hitting enter or tab.
-// TODO: Does not seem to work.
-- (BOOL)windowShouldClose:(NSWindow *)window {
-    return [window makeFirstResponder:nil];
+- (BOOL)windowShouldClose:(id)sender {
+    return [(pithos_macosAppDelegate *)[[NSApplication sharedApplication] delegate] activated];
 }
 
-- (IBAction)toolbarItemSelected:(id)sender {
-}
+//- (void)windowWillClose:(NSNotification *)notification {
+//}
 
-//- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
-//    NSLog(@"observeValueForKeyPath: %@ %@", keyPath, [userDefaultsController valueForKeyPath:keyPath]);
+//- (IBAction)toolbarItemSelected:(id)sender {
 //}
 
 #pragma mark -
-#pragma Authentication
-
-- (IBAction)authenticationLogin:(id)sender {
-    // XXX hardcoded for now
-    NSProcessInfo *processInfo = [NSProcessInfo processInfo];
-    NSString *loginURL = [NSString stringWithFormat:@"https://pithos.dev.grnet.gr/login?next=pithos://%@_%d",
-                          [processInfo processName], [processInfo processIdentifier]];
-    if ([authenticationRenewCheckBox state] == NSOnState)
-        loginURL = [loginURL stringByAppendingString:@"&renew="];
-    NSLog(@"loginURL: %@", loginURL);
-    [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:loginURL]];
-    // XXX Should we wait for results or do something else?
-    // XXX check the case where this happens for the first time
-}
-
-- (IBAction)authenticationManual:(id)sender {
-    [userDefaultsController save:sender];
-    // Because of delayed saves of the userDefaultsController, we use the TextField values directly, instead of 
-    //NSString *authUser = [[userDefaultsController values] valueForKey:@"authUser"];
-    //NSString *authToken = [[userDefaultsController values] valueForKey:@"authToken"];
-    [(pithos_macosAppDelegate *)[[NSApplication sharedApplication] delegate] authenticateWithAuthUser:[authenticationUserTextField stringValue] authToken:[authenticationTokenTextField stringValue]];
+#pragma Observers
+
+- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
+    if ([object isEqualTo:accountsArrayController] && 
+        [keyPath isEqualToString:@"selection"] && 
+        [[accountsArrayController selectedObjects] count]) {
+        self.selectedPithosAccount = [[accountsArrayController selectedObjects] objectAtIndex:0];
+    }
+}
+
+- (void)selectedPithosAccountNodeChildrenUpdated:(NSNotification *)notification {
+    [syncAccountsOutlineView reloadData];
+//    [syncAccountsOutlineView expandItem:nil expandChildren:YES];
+}
+
+#pragma mark -
+#pragma Update
+
+- (void)updateAccounts {
+    pithos_macosAppDelegate *delegate = (pithos_macosAppDelegate *)[[NSApplication sharedApplication] delegate];
+    self.accountRemoveEnable = (delegate.activated && ([delegate.pithosAccounts count] > 1));
+}
+
+- (void)updateLogin {
+    self.loginEnable = ([selectedPithosAccount urlIsValid:serverURL] && (!manual || ([authUser length] && [authToken length])));
+    self.loginCancelEnable = (![selectedPithosAccount.serverURL isEqualToString:serverURL] || 
+                              (selectedPithosAccount.authUser && ![selectedPithosAccount.authUser isEqualToString:authUser]) || 
+                              (selectedPithosAccount.authToken && ![selectedPithosAccount.authToken isEqualToString:authToken]));
+}
+
+- (void)updateSync {
+    BOOL isDirectory;
+    self.syncApplyEnable = (selectedPithosAccount.active && 
+                            ((selectedPithosAccount.syncActive != syncActive) || 
+                             (selectedPithosAccount.syncSkipHidden != syncSkipHidden) || 
+                             (![selectedPithosAccount.syncDirectoryPath isEqualToString:syncDirectoryPath] && 
+                              (![[NSFileManager defaultManager] fileExistsAtPath:syncDirectoryPath isDirectory:&isDirectory] || 
+                               isDirectory)) ||
+                             ![selectedPithosAccount.syncAccountsDictionary isEqualToDictionary:syncAccountsDictionary]));
+    self.syncCancelEnable = (selectedPithosAccount.active && 
+                             ((selectedPithosAccount.syncActive != syncActive) || 
+                              (selectedPithosAccount.syncSkipHidden != syncSkipHidden) || 
+                              ![selectedPithosAccount.syncDirectoryPath isEqualToString:syncDirectoryPath] ||
+                              ![selectedPithosAccount.syncAccountsDictionary isEqualToDictionary:syncAccountsDictionary]));
+}
+
+#pragma mark -
+#pragma Properties
+
+- (void)setSelectedPithosAccount:(PithosAccount *)aSelectedPithosAccount {
+    if (aSelectedPithosAccount && ![aSelectedPithosAccount isEqualTo:selectedPithosAccount]) {
+        selectedPithosAccount.accountNode.childrenUpdatedNotificationName = nil;
+        selectedPithosAccount.sharingAccountsNode.childrenUpdatedNotificationName = nil;
+        selectedPithosAccount = aSelectedPithosAccount;
+        selectedPithosAccount.accountNode.childrenUpdatedNotificationName = @"SelectedPithosAccountNodeChildrenUpdated";
+        selectedPithosAccount.sharingAccountsNode.childrenUpdatedNotificationName = @"SelectedPithosAccountNodeChildrenUpdated";
+        
+        [self updateAccounts];
+        [self loginCancel:self];
+        [self syncCancel:self];
+        [self groupsRevert:self];
+    }
+}
+
+#pragma Login Properties
+
+- (void)setServerURL:(NSString *)aServerURL {
+    serverURL = [aServerURL copy];
+    [self updateLogin];
+}
+
+- (void)setAuthUser:(NSString *)anAuthUser {
+    authUser = [anAuthUser copy];
+    [self updateLogin];
+}
+
+- (void)setAuthToken:(NSString *)anAuthToken {
+    authToken = [anAuthToken copy];
+    [self updateLogin];
+}
+
+- (void)setManual:(BOOL)aManual {
+    manual = aManual;
+    [self updateLogin];
+    if (!manual) {
+        self.authUser = selectedPithosAccount.authUser;
+        self.authToken = selectedPithosAccount.authToken;
+    }
+}
+
+#pragma Sync Properties
+
+- (void)setSyncActive:(BOOL)aSyncActive {
+    syncActive = aSyncActive;
+    [self updateSync];
+}
+
+- (void)setSyncSkipHidden:(BOOL)aSyncSkipHidden {
+    syncSkipHidden = aSyncSkipHidden;
+    [self updateSync];
+    [self selectedPithosAccountNodeChildrenUpdated:nil];
+}
+
+- (void)setSyncDirectoryPath:(NSString *)aSyncDirectoryPath {
+    syncDirectoryPath = [aSyncDirectoryPath copy];
+    [self updateSync];
+}
+
+- (void)setSyncAccountsDictionary:(NSMutableDictionary *)aSyncAccountsDictionary {
+    syncAccountsDictionary = [[NSMutableDictionary alloc] initWithCapacity:[aSyncAccountsDictionary count]];
+    for (NSString *accountName in aSyncAccountsDictionary) {
+        NSDictionary *aSyncContainersDictionary = [aSyncAccountsDictionary objectForKey:accountName];
+        NSMutableDictionary *syncContainersDictionary = [NSMutableDictionary dictionary];
+        for (NSString *containerName in aSyncContainersDictionary) {
+            if (![accountName isEqualToString:@""] || ![[containerName lowercaseString] isEqualToString:@"shared to me"])
+                [syncContainersDictionary setObject:[NSMutableSet setWithSet:[aSyncContainersDictionary objectForKey:containerName]] 
+                                             forKey:containerName];
+        }
+        if ([syncContainersDictionary count])
+            [syncAccountsDictionary setObject:syncContainersDictionary forKey:accountName];
+    }
+    [self updateSync];
+}
+
+#pragma mark -
+#pragma Actions
+
+- (IBAction)addAccount:(id)sender {
+    [accountsArrayController addObject:[PithosAccount pithosAccount]];
+    [self updateAccounts];
+    pithos_macosAppDelegate *delegate = (pithos_macosAppDelegate *)[[NSApplication sharedApplication] delegate];
+    [delegate.pithosAccountsDictionary setObject:selectedPithosAccount forKey:selectedPithosAccount.name];
+    [delegate savePithosAccounts:self];
+}
+
+- (IBAction)removeAccount:(id)sender {
+    [self updateAccounts];
+    if (!accountRemoveEnable)
+        return;
+    PithosAccount *removedPithosAccount = selectedPithosAccount;
+    pithos_macosAppDelegate *delegate = (pithos_macosAppDelegate *)[[NSApplication sharedApplication] delegate];
+    if ([delegate.currentPithosAccount isEqualTo:removedPithosAccount] && [delegate.pithosBrowserController operationsPending]) {
+        NSAlert *alert = [[NSAlert alloc] init];
+        [alert setMessageText:@"Operations Pending"];
+        [alert setInformativeText:@"There are pending operations in the browser, do you want to remove the account and cancel them?"];
+        [alert addButtonWithTitle:@"OK"];
+        [alert addButtonWithTitle:@"Cancel"];
+        NSInteger choice = [alert runModal];
+        if (choice == NSAlertSecondButtonReturn) {
+            return;
+        }
+    }
+    [accountsArrayController removeObject:selectedPithosAccount];
+    [delegate.pithosAccountsDictionary removeObjectForKey:removedPithosAccount.name];
+    [delegate removedPithosAccount:removedPithosAccount];
+    [delegate savePithosAccounts:self];
+    [self updateAccounts];
+}
+
+#pragma Login Actions
+
+- (IBAction)login:(id)sender {
+    self.syncAccountsRootFilesNodes = [NSMutableDictionary dictionary];
+    if (!manual) {
+        [selectedPithosAccount loginWithServerURL:serverURL];
+    } else {
+        [selectedPithosAccount authenticateWithServerURL:serverURL authUser:authUser authToken:authToken];
+        self.manual = NO;
+        pithos_macosAppDelegate *delegate = (pithos_macosAppDelegate *)[[NSApplication sharedApplication] delegate];
+        [delegate savePithosAccounts:self];
+        if (!delegate.activated) {
+            delegate.activated = YES;
+            [delegate showPithosBrowser:self];
+        }
+        if ([selectedPithosAccount isEqualTo:delegate.currentPithosAccount])
+            delegate.pithosBrowserController.pithos = selectedPithosAccount.pithos;
+    }
+}
+
+- (IBAction)loginCancel:(id)server {
+    self.serverURL = selectedPithosAccount.serverURL;
+    self.authUser = selectedPithosAccount.authUser;
+    self.authToken = selectedPithosAccount.authToken;
+    self.manual = NO;
+}
+
+#pragma Sync Actions
+
+- (IBAction)syncApply:(id)sender {
+    [selectedPithosAccount updateSyncWithSyncActive:syncActive 
+                                  syncDirectoryPath:syncDirectoryPath 
+                           syncAccountsDictionary:syncAccountsDictionary 
+                                     syncSkipHidden:syncSkipHidden];
+    [self updateSync];
+    pithos_macosAppDelegate *delegate = (pithos_macosAppDelegate *)[[NSApplication sharedApplication] delegate];
+    [delegate savePithosAccounts:self];
+    [delegate sync];
+}
+
+- (IBAction)syncCancel:(id)sender {
+    self.syncActive = selectedPithosAccount.syncActive;
+    self.syncDirectoryPath = selectedPithosAccount.syncDirectoryPath;
+    self.syncAccountsDictionary = selectedPithosAccount.syncAccountsDictionary;
+    self.syncAccountsRootFilesNodes = [NSMutableDictionary dictionary];
+    self.syncSkipHidden = selectedPithosAccount.syncSkipHidden;
+}
+
+- (IBAction)syncRefresh:(id)sender {
+    selectedPithosAccount.accountNode.forcedRefresh = YES;
+    [selectedPithosAccount.accountNode invalidateChildrenRecursive];
+    selectedPithosAccount.sharingAccountsNode.forcedRefresh = YES;
+    [selectedPithosAccount.sharingAccountsNode invalidateChildrenRecursive];
+    if (selectedPithosAccount.accountNode.children && selectedPithosAccount.sharingAccountsNode.children) {
+    }
+}
+
+#pragma mark Groups Actions
+
+- (IBAction)groupsApply:(id)sender {
+    [[self window] makeFirstResponder:nil];
+    if (selectedPithosAccount.active)
+        [selectedPithosAccount.accountNode applyInfo];
+}
+
+- (IBAction)groupsRevert:(id)sender {
+    if (selectedPithosAccount.active && selectedPithosAccount.accountNode)
+        [selectedPithosAccount.accountNode refreshInfo];
+}
+
+#pragma mark -
+#pragma mark NSOutlineViewDataSource
+
+// <my account> [PithosEmptyNode]
+// - <container>+ [PithosContainerNode]
+// -- <subdir>+ [PithosSubdirNode]
+// -- <root files> [PithosEmptyNode]
+// <sharing account>+ [PithosSharingAccountNode]
+// - <container>+ [PithosContainerNode]
+// -- <subdir>+ [PithosSubdirNode]
+// -- <root files> [PithosEmptyNode]
+
+- (NSInteger)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(id)item {
+    if (!selectedPithosAccount.active)
+        return 0;
+    if (outlineView == syncAccountsOutlineView) {
+        if (item == nil) {
+            // root: <my account> + #<sharing account>
+            NSInteger accountsCount = 0;
+            if ([selectedPithosAccount.accountNode.children count])
+                accountsCount = 1;
+            if (selectedPithosAccount.sharingAccountsNode.children)
+                accountsCount += selectedPithosAccount.sharingAccountsNode.children.count;
+            return accountsCount;
+        } else if (item == syncAccountsMyAccountNode) {
+            // root/<my account>: #<container>
+            if (selectedPithosAccount.accountNode.children) {
+                NSInteger containersCount = 0;
+                for (PithosContainerNode *node in selectedPithosAccount.accountNode.children) {
+                    if (![[node.displayName lowercaseString] isEqualToString:@"shared to me"])
+                        containersCount++;
+                }
+                return containersCount;
+            }
+        } else if ([item class] == [PithosAccountNode class]) {
+            // root/<sharing account>: #<container>
+            PithosAccountNode *accountNode = (PithosAccountNode *)item;
+            if (accountNode.children)
+                return accountNode.children.count;
+        } else if ([item class] == [PithosContainerNode class]) {
+            // root/{<my account>, <sharing account>}/<container>: #<subdir> + <root files>
+            PithosContainerNode *containerNode = (PithosContainerNode *)item;
+            if (containerNode.children) {
+                // We add 1 for the root files node
+                NSInteger subdirCount = 1;
+                for (PithosNode *node in containerNode.children) {
+                    if (([node class] == [PithosSubdirNode class]) && (!syncSkipHidden || ![node.displayName hasPrefix:@"."]))
+                        subdirCount++;
+                }
+                return subdirCount;
+            }
+        }
+    }
+    return 0;
+}
+
+- (id)outlineView:(NSOutlineView *)outlineView child:(NSInteger)index ofItem:(id)item {
+    if (outlineView == syncAccountsOutlineView) {
+        if (item == nil) {
+            // root: [ <my account>, <sharing account>+ ]
+            if ([selectedPithosAccount.accountNode.children count]) {
+                if (index == 0)
+                    return syncAccountsMyAccountNode;
+                else
+                    return [selectedPithosAccount.sharingAccountsNode.children objectAtIndex:(index - 1)];
+            } else {
+                return [selectedPithosAccount.sharingAccountsNode.children objectAtIndex:index];
+            }
+        } else if (item == syncAccountsMyAccountNode) {
+            // root/<my account>: [ <container>+ ]
+            NSInteger currentContainerIndex = -1;
+            for (PithosContainerNode *node in selectedPithosAccount.accountNode.children) {
+                if (![[node.displayName lowercaseString] isEqualToString:@"shared to me"]) {
+                    currentContainerIndex++;
+                    if (currentContainerIndex == index)
+                        return node;
+                }
+            }
+        } else if ([item class] == [PithosAccountNode class]) {
+            // root/<sharing account>: [ <container>+ ]
+            return [((PithosAccountNode *)item).children objectAtIndex:index];
+        } else if ([item class] == [PithosContainerNode class]) {
+            // root/{<my account>, <sharing account>}/<container>: [ <subdir>+, <root files> ]
+            PithosContainerNode *containerNode = (PithosContainerNode *)item;
+            NSInteger currentSubdirIndex = -1;
+            for (PithosNode *node in containerNode.children) {
+                if (([node class] == [PithosSubdirNode class]) && (!syncSkipHidden || ![node.displayName hasPrefix:@"."])) {
+                    currentSubdirIndex++;
+                    if (currentSubdirIndex == index)
+                        return node;
+                }
+            }
+            if (++currentSubdirIndex == index) {
+                NSString *accountName = containerNode.sharingAccount;
+                if (!accountName)
+                    accountName = @"";
+                PithosEmptyNode *rootFilesNode = [[syncAccountsRootFilesNodes objectForKey:accountName] 
+                                                  objectForKey:containerNode.displayName];
+                if (!rootFilesNode) {
+                    if (![syncAccountsRootFilesNodes objectForKey:accountName])
+                        [syncAccountsRootFilesNodes setObject:[NSMutableDictionary dictionary] forKey:accountName];
+                    rootFilesNode = [[PithosEmptyNode alloc] initWithDisplayName:@"<root files>" 
+                                                                             icon:[[NSWorkspace sharedWorkspace] iconForFileType:@""]];
+                    rootFilesNode.parent = containerNode;
+                    [[syncAccountsRootFilesNodes objectForKey:accountName] setObject:rootFilesNode forKey:containerNode.displayName];
+                }
+                return rootFilesNode;
+            }
+        }
+    }
+    return nil;
+}
+
+- (BOOL)outlineView:(NSOutlineView *)outlineView isItemExpandable:(id)item {
+    if (outlineView == syncAccountsOutlineView) {
+        if ((item == syncAccountsMyAccountNode) || 
+            ([item class] == [PithosAccountNode class]) || 
+            ([item class] == [PithosContainerNode class]))
+            return YES;
+    }
+    return NO;
+}
+
+- (id)outlineView:(NSOutlineView *)outlineView objectValueForTableColumn:(NSTableColumn *)tableColumn byItem:(id)item {
+    if (outlineView == syncAccountsOutlineView) {
+        if ([[tableColumn identifier] isEqualToString:@"sync"]) {
+            if (item == syncAccountsMyAccountNode) {
+                // root/<my account>
+                // My account is 
+                // off if not in dictionary
+                // mixed if in dictionary with exclusions
+                // on if in dictionary without exclusions
+                NSMutableDictionary *syncContainersDictionary = [syncAccountsDictionary objectForKey:@""];
+                if (syncContainersDictionary) {
+                    for (PithosContainerNode *node in selectedPithosAccount.accountNode.children) {
+                        if (![[node.displayName lowercaseString] isEqualToString:@"shared to me"]) {
+                            NSMutableSet *containerExcludedDirectories = [syncContainersDictionary objectForKey:node.displayName];
+                            if (!containerExcludedDirectories || [containerExcludedDirectories count])
+                                return [NSNumber numberWithUnsignedInteger:NSMixedState];
+                        }
+                    }
+                    return [NSNumber numberWithUnsignedInteger:NSOnState];
+                }
+                return [NSNumber numberWithUnsignedInteger:NSOffState];
+            } else if ([item class] == [PithosAccountNode class]) {
+                // root/<sharing account>
+                // A sharing account is 
+                // off if not in dictionary
+                // mixed if in dictionary with exclusions
+                // on if in dictionary without exclusions
+                PithosAccountNode *accountNode = (PithosAccountNode *)item;
+                NSMutableDictionary *syncContainersDictionary = [syncAccountsDictionary objectForKey:accountNode.displayName];
+                if (syncContainersDictionary) {
+                    for (PithosContainerNode *node in accountNode.children) {
+                        NSMutableSet *containerExcludedDirectories = [syncContainersDictionary objectForKey:node.displayName];
+                        if (!containerExcludedDirectories || [containerExcludedDirectories count])
+                            return [NSNumber numberWithUnsignedInteger:NSMixedState];
+                    }
+                    return [NSNumber numberWithUnsignedInteger:NSOnState];
+                }
+                return [NSNumber numberWithUnsignedInteger:NSOffState];
+            } else if ([item class] == [PithosContainerNode class]) {
+                // root/{<my account>, <sharing account>}/<container>
+                // A container is 
+                // off if not in dictionary
+                // mixed if in dictionary with exclusions
+                // on if in dictionary without exclusions
+                PithosContainerNode *node = (PithosContainerNode *)item;
+                NSString *accountName = node.sharingAccount;
+                if (!accountName)
+                    accountName = @"";
+                NSMutableSet *containerExcludedDirectories = [[syncAccountsDictionary objectForKey:accountName] 
+                                                              objectForKey:node.displayName];
+                if (containerExcludedDirectories) {
+                    if ([containerExcludedDirectories count])
+                        return [NSNumber numberWithUnsignedInteger:NSMixedState];
+                    else
+                        return [NSNumber numberWithUnsignedInteger:NSOnState];
+                }
+                return [NSNumber numberWithUnsignedInteger:NSOffState];
+            } else if ([item class] == [PithosSubdirNode class]) {
+                // root/{<my account>, <sharing account>}/<container>/<subdir>
+                // Directory is off if parent container not in dictionary or if excluded
+                // displayName should be localized and lowercased
+                PithosSubdirNode *node = (PithosSubdirNode *)item;
+                NSString *accountName = node.sharingAccount;
+                if (!accountName)
+                    accountName = @"";
+                NSMutableSet *containerExcludedDirectories = [[syncAccountsDictionary objectForKey:accountName] 
+                                                              objectForKey:node.parent.displayName];
+                if (!containerExcludedDirectories || 
+                    [containerExcludedDirectories 
+                     containsObject:[[node.displayName lowercaseString] stringByReplacingOccurrencesOfString:@"/" withString:@":"]])
+                    return [NSNumber numberWithUnsignedInteger:NSOffState];
+                else
+                    return [NSNumber numberWithUnsignedInteger:NSOnState];
+            } else if ([item class] == [PithosEmptyNode class]) {
+                // root/{<my account>, <sharing account>}/<container>/<root files>
+                // Root files is off if parent container not in dictionary or if excluded
+                PithosEmptyNode *node = (PithosEmptyNode *)item;
+                NSString *accountName = node.parent.sharingAccount;
+                if (!accountName)
+                    accountName = @"";
+                NSMutableSet *containerExcludedDirectories = [[syncAccountsDictionary objectForKey:accountName] 
+                                                                objectForKey:node.parent.displayName];
+                if (!containerExcludedDirectories || [containerExcludedDirectories containsObject:@""])
+                    return [NSNumber numberWithUnsignedInteger:NSOffState];
+                else
+                    return [NSNumber numberWithUnsignedInteger:NSOnState];
+            }
+            return [NSNumber numberWithUnsignedInteger:NSOffState];
+        } else if ([[tableColumn identifier] isEqualToString:@"path"]) {
+            return (PithosNode *)item;
+        }
+    }
+    return nil;
+}
+
+- (void)outlineView:(NSOutlineView *)outlineView setObjectValue:(id)object forTableColumn:(NSTableColumn *)tableColumn byItem:(id)item {
+    if (outlineView == syncAccountsOutlineView) {
+        if ([[tableColumn identifier] isEqualToString:@"sync"]) {
+            NSCellStateValue newState = [object unsignedIntegerValue];
+            if (item == syncAccountsMyAccountNode) {
+                // root/<my account>
+                // If new state is
+                // mixed/on include my account with no exclusions
+                // off exclude my account
+                if ((newState == NSOnState) || (newState == NSMixedState)) {
+                    NSMutableDictionary *syncContainersDictionary = [NSMutableDictionary dictionary];
+                    for (PithosContainerNode *node in selectedPithosAccount.accountNode.children) {
+                        if (![[node.displayName lowercaseString] isEqualToString:@"shared to me"])
+                            [syncContainersDictionary setObject:[NSMutableSet set] forKey:node.displayName];
+                    }
+                    [syncAccountsDictionary setObject:syncContainersDictionary forKey:@""];
+                } else {
+                    [syncAccountsDictionary removeObjectForKey:@""];
+                }
+                [outlineView reloadItem:item reloadChildren:YES];
+            } else if ([item class] == [PithosAccountNode class]) {
+                // root/<sharing account>
+                // If new state is
+                // mixed/on include sharing account with no exclusions
+                // off exclude sharing account
+                PithosAccountNode *accountNode = (PithosAccountNode *)item;
+                if ((newState == NSOnState) || (newState == NSMixedState)) {
+                    NSMutableDictionary *syncContainersDictionary = [NSMutableDictionary dictionary];
+                    for (PithosContainerNode *node in accountNode.children) {
+                        [syncContainersDictionary setObject:[NSMutableSet set] forKey:node.displayName];
+                    }
+                    [syncAccountsDictionary setObject:syncContainersDictionary forKey:accountNode.displayName];
+                } else {
+                    [syncAccountsDictionary removeObjectForKey:accountNode.displayName];
+                }
+                [outlineView reloadItem:item reloadChildren:YES];
+            } else if ([item class] == [PithosContainerNode class]) {
+                // root/{<my account>, <sharing account>}/<container>
+                // If new state is
+                // mixed/on include container with no excluded directories
+                // off exclude container
+                PithosContainerNode *node = (PithosContainerNode *)item;
+                NSString *accountName = node.sharingAccount;
+                PithosNode *accountNode = node.parent;
+                if (!accountName) {
+                    accountName = @"";
+                    accountNode = syncAccountsMyAccountNode;
+                }
+                NSMutableDictionary *syncContainersDictionary = [syncAccountsDictionary objectForKey:accountName];
+                if ((newState == NSOnState) || (newState == NSMixedState)) {
+                    if (!syncContainersDictionary) {
+                        syncContainersDictionary = [NSMutableDictionary dictionary];
+                        [syncAccountsDictionary setObject:syncContainersDictionary forKey:accountName];
+                    }
+                    [syncContainersDictionary setObject:[NSMutableSet set] forKey:node.displayName];
+                } else if (syncContainersDictionary) {
+                    [syncContainersDictionary removeObjectForKey:node.displayName];
+                    if (![syncContainersDictionary count])
+                        [syncAccountsDictionary removeObjectForKey:accountName];
+                }
+                [outlineView reloadItem:accountNode reloadChildren:YES];
+            } else if ([item class] == [PithosSubdirNode class]) {
+                // root/{<my account>, <sharing account>}/<container>/<subdir>
+                // If new state is
+                // mixed/on include directory (if container not included, include and exclude all others)
+                // off exclude directory
+                PithosSubdirNode *node = (PithosSubdirNode *)item;
+                NSString *accountName = node.sharingAccount;
+                PithosNode *accountNode = node.parent.parent;
+                if (!accountName) {
+                    accountName = @"";
+                    accountNode = syncAccountsMyAccountNode;
+                }
+                NSMutableDictionary *syncContainersDictionary = [syncAccountsDictionary objectForKey:accountName];
+                NSMutableSet *containerExcludedDirectories = [syncContainersDictionary objectForKey:node.parent.displayName];
+                NSString *directoryName = [[node.displayName lowercaseString] stringByReplacingOccurrencesOfString:@"/" withString:@":"];
+                if ((newState == NSOnState) || (newState == NSMixedState)) {
+                    if (containerExcludedDirectories) {
+                        [containerExcludedDirectories removeObject:directoryName];
+                    } else {
+                        if (!syncContainersDictionary) {
+                            syncContainersDictionary = [NSMutableDictionary dictionary];
+                            [syncAccountsDictionary setObject:syncContainersDictionary forKey:accountName];
+                        }
+                        NSMutableSet *newContainerExcludeDirectories = [NSMutableSet setWithObject:@""];
+                        for (PithosNode *siblingNode in node.parent.children) {
+                            if (([siblingNode class] == [PithosSubdirNode class]) && 
+                                (!syncSkipHidden || ![siblingNode.displayName hasPrefix:@"."])) {
+                                NSString *siblingDirectoryName = [[siblingNode.displayName lowercaseString] 
+                                                                  stringByReplacingOccurrencesOfString:@"/" withString:@":"];
+                                if (![siblingDirectoryName isEqualToString:directoryName] && 
+                                    ![newContainerExcludeDirectories containsObject:siblingDirectoryName])
+                                    [newContainerExcludeDirectories addObject:siblingDirectoryName];
+                            }
+                        }
+                        [syncContainersDictionary setObject:newContainerExcludeDirectories forKey:node.parent.displayName];
+                    }
+                } else if (syncContainersDictionary && 
+                           containerExcludedDirectories && 
+                           ![containerExcludedDirectories containsObject:directoryName]) {
+                    [containerExcludedDirectories addObject:directoryName];
+                }
+                [outlineView reloadItem:accountNode reloadChildren:YES];
+            } else if ([item class] == [PithosEmptyNode class]) {
+                // If new state is
+                // mixed/on include root files (if container not included, include and exclude all others)
+                // off exclude root files
+                PithosEmptyNode *node = (PithosEmptyNode *)item;
+                NSString *accountName = node.parent.sharingAccount;
+                PithosNode *accountNode = node.parent.parent;
+                if (!accountName) {
+                    accountName = @"";
+                    accountNode = syncAccountsMyAccountNode;
+                }
+                NSMutableDictionary *syncContainersDictionary = [syncAccountsDictionary objectForKey:accountName];
+                NSMutableSet *containerExcludedDirectories = [syncContainersDictionary objectForKey:node.parent.displayName];
+                if ((newState == NSOnState) || (newState == NSMixedState)) {
+                    if (containerExcludedDirectories) {
+                        [containerExcludedDirectories removeObject:@""];
+                    } else {
+                        if (!syncContainersDictionary) {
+                            syncContainersDictionary = [NSMutableDictionary dictionary];
+                            [syncAccountsDictionary setObject:syncContainersDictionary forKey:accountName];
+                        }
+                        NSMutableSet *newContainerExcludeDirectories = [NSMutableSet set];
+                        for (PithosNode *siblingNode in node.parent.children) {
+                            if (([siblingNode class] == [PithosSubdirNode class]) && 
+                                (!syncSkipHidden || ![siblingNode.displayName hasPrefix:@"."])) {
+                                NSString *siblingDirectoryName = [[siblingNode.displayName lowercaseString] 
+                                                                  stringByReplacingOccurrencesOfString:@"/" withString:@":"];
+                                if (![newContainerExcludeDirectories containsObject:siblingDirectoryName])
+                                    [newContainerExcludeDirectories addObject:siblingDirectoryName];
+                            }
+                        }
+                        [syncContainersDictionary setObject:newContainerExcludeDirectories forKey:node.parent.displayName];
+                    }
+                } else if (syncContainersDictionary && 
+                           containerExcludedDirectories && 
+                           ![containerExcludedDirectories containsObject:@""]) {
+                    [containerExcludedDirectories addObject:@""];
+                }
+                [outlineView reloadItem:accountNode reloadChildren:YES];
+            }
+            [self updateSync];
+        }
+    }
 }
 
 @end