Add UI for selective sync folders. Call next sync when an account has sync preference...
[pithos-macos] / pithos-macos / PithosPreferencesController.m
index 4cbf3ac..f80a0a4 100644 (file)
 #import "PithosPreferencesController.h"
 #import "PithosBrowserController.h"
 #import "PithosAccountNode.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 selectedPithosAccount;
 @synthesize accountsArrayController;
 @synthesize accountRemoveEnable;
 @synthesize serverURL, authUser, authToken, manual, loginEnable, loginCancelEnable;
+@synthesize syncActive, syncDirectoryPath, syncContainersDictionary, syncApplyEnable, syncCancelEnable, 
+            syncContainersOutlineView, syncContainersRootFilesNodes;
 @synthesize groupsDictionaryController, selectedGroupMembersDictionaryController;
-@synthesize syncActive, syncDirectoryPath, syncApplyEnable, syncCancelEnable;
 
 #pragma mark -
 #pragma mark Object Lifecycle
@@ -65,7 +89,9 @@
     
 //     // Select the first tab when the window is loaded for the first time.
 //     [[window valueForKeyPath:@"toolbar"] setSelectedItemIdentifier:@"0"];
-
+    
+    [[[syncContainersOutlineView tableColumns] objectAtIndex:1] setDataCell:[[[PithosPreferencesSyncOutlineViewCell alloc] init] autorelease]];
+    
     [groupsDictionaryController setInitialKey:@"group"];
     [groupsDictionaryController setInitialValue:@"user"];
     [selectedGroupMembersDictionaryController setInitialKey:@"user"];
     
     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];
 }
 
 - (BOOL)windowShouldClose:(id)sender {
     }
 }
 
+- (void)selectedPithosAccountNodeChildrenUpdated:(NSNotification *)notification {
+    [syncContainersOutlineView reloadData];
+    [syncContainersOutlineView expandItem:nil expandChildren:YES];
+}
+
 #pragma mark -
 #pragma Update
 
                             ((selectedPithosAccount.syncActive != syncActive) || 
                              (![selectedPithosAccount.syncDirectoryPath isEqualToString:syncDirectoryPath] && 
                               (![[NSFileManager defaultManager] fileExistsAtPath:syncDirectoryPath isDirectory:&isDirectory] || 
-                               isDirectory))));
+                               isDirectory)) ||
+                             ![selectedPithosAccount.syncContainersDictionary isEqualToDictionary:syncContainersDictionary]));
     self.syncCancelEnable = (selectedPithosAccount.active && 
                              ((selectedPithosAccount.syncActive != syncActive) || 
-                              ![selectedPithosAccount.syncDirectoryPath isEqualToString:syncDirectoryPath]));
+                              ![selectedPithosAccount.syncDirectoryPath isEqualToString:syncDirectoryPath] ||
+                              ![selectedPithosAccount.syncContainersDictionary isEqualToDictionary:syncContainersDictionary]));
 }
 
 #pragma mark -
 
 - (void)setSelectedPithosAccount:(PithosAccount *)aSelectedPithosAccount {
     if (aSelectedPithosAccount && ![aSelectedPithosAccount isEqualTo:selectedPithosAccount]) {
+        selectedPithosAccount.accountNode.childrenUpdatedNotificationName = nil;
         [selectedPithosAccount release];
         selectedPithosAccount = [aSelectedPithosAccount retain];
+        selectedPithosAccount.accountNode.childrenUpdatedNotificationName = [NSString stringWithString:@"SelectedPithosAccountNodeChildrenUpdated"];
         
         [self updateAccounts];
         [self loginCancel:self];
     manual = aManual;
     [self updateLogin];
     if (!manual) {
-        self.authUser = [[selectedPithosAccount.authUser copy] autorelease];
-        self.authToken = [[selectedPithosAccount.authToken copy] autorelease];
+        self.authUser = selectedPithosAccount.authUser;
+        self.authToken = selectedPithosAccount.authToken;
     }
 }
 
     [self updateSync];
 }
 
+- (void)setSyncContainersDictionary:(NSMutableDictionary *)aSyncContainersDictionary {
+    [syncContainersDictionary release];
+    syncContainersDictionary = [[NSMutableDictionary alloc] initWithCapacity:[aSyncContainersDictionary count]];
+    for (NSString *containerName in aSyncContainersDictionary) {
+        [syncContainersDictionary setObject:[NSMutableArray arrayWithArray:[aSyncContainersDictionary objectForKey:containerName]] 
+                                                                    forKey:containerName];
+    }
+    [self updateSync];
+}
+
 #pragma mark -
 #pragma Actions
 
 }
 
 - (IBAction)loginCancel:(id)server {
-    self.serverURL = [[selectedPithosAccount.serverURL copy] autorelease];
-    self.authUser = [[selectedPithosAccount.authUser copy] autorelease];
-    self.authToken = [[selectedPithosAccount.authToken copy] autorelease];
+    self.serverURL = selectedPithosAccount.serverURL;
+    self.authUser = selectedPithosAccount.authUser;
+    self.authToken = selectedPithosAccount.authToken;
 }
 
 #pragma Sync Actions
 
 - (IBAction)syncApply:(id)sender {
-    [selectedPithosAccount updateSyncWithSyncActive:syncActive syncDirectoryPath:syncDirectoryPath];
+    [selectedPithosAccount updateSyncWithSyncActive:syncActive 
+                                  syncDirectoryPath:syncDirectoryPath 
+                           syncContainersDictionary:syncContainersDictionary];
     [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 copy] autorelease];
+    self.syncDirectoryPath = selectedPithosAccount.syncDirectoryPath;
+    self.syncContainersDictionary = selectedPithosAccount.syncContainersDictionary;
+    self.syncContainersRootFilesNodes = [NSMutableDictionary dictionary];
+    [self selectedPithosAccountNodeChildrenUpdated:nil];
+}
+
+- (IBAction)syncRefresh:(id)sender {
+    selectedPithosAccount.accountNode.forcedRefresh = YES;
+    [selectedPithosAccount.accountNode invalidateChildrenRecursive];
+    if (selectedPithosAccount.accountNode.children) {
+    }
 }
 
 #pragma mark Groups Actions
         [selectedPithosAccount.accountNode refreshInfo];
 }
 
+#pragma mark -
+#pragma mark NSOutlineViewDataSource
+
+- (NSInteger)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(id)item {
+    if (outlineView == syncContainersOutlineView) {
+        if (item == nil)
+            return selectedPithosAccount.accountNode.children.count;
+        if ([item class] == [PithosContainerNode class]) {
+            // We add 1 for the root files node
+            NSInteger subdirCount = 1;
+            for (PithosNode *node in [(PithosContainerNode *)item children]) {
+                if ([node class] == [PithosSubdirNode class])
+                    subdirCount++;
+            }
+            return subdirCount;
+        }
+    }
+    return 0;
+}
+
+- (id)outlineView:(NSOutlineView *)outlineView child:(NSInteger)index ofItem:(id)item {
+    if (outlineView == syncContainersOutlineView) {
+        if (item == nil)
+            return [selectedPithosAccount.accountNode.children objectAtIndex:index];
+        if ([item class] == [PithosContainerNode class]) {
+            PithosContainerNode *containerNode = (PithosContainerNode *)item;
+            NSInteger currentSubdirIndex = -1;
+            for (PithosNode *node in containerNode.children) {
+                if ([node class] == [PithosSubdirNode class]) {
+                    currentSubdirIndex++;
+                    if (currentSubdirIndex == index)
+                        return node;
+                }
+            }
+            if (++currentSubdirIndex == index) {
+                PithosEmptyNode *rootFilesNode = [syncContainersRootFilesNodes objectForKey:containerNode.displayName];
+                if (!rootFilesNode) {
+                    rootFilesNode = [[[PithosEmptyNode alloc] initWithDisplayName:@"<root files>" 
+                                                                             icon:[[NSWorkspace sharedWorkspace] iconForFileType:@""]] 
+                                     autorelease];
+                    rootFilesNode.parent = containerNode;
+                    [syncContainersRootFilesNodes setObject:rootFilesNode forKey:containerNode.displayName];
+                }
+                return rootFilesNode;
+            }
+        }
+    }
+    return nil;
+}
+
+- (BOOL)outlineView:(NSOutlineView *)outlineView isItemExpandable:(id)item {
+    if (outlineView == syncContainersOutlineView) {
+        if ([item class] == [PithosContainerNode class])
+            return YES;
+    }
+    return NO;
+}
+
+- (id)outlineView:(NSOutlineView *)outlineView objectValueForTableColumn:(NSTableColumn *)tableColumn byItem:(id)item {
+    if (outlineView == syncContainersOutlineView) {
+        if ([[tableColumn identifier] isEqualToString:@"sync"]) {
+            if ([item class] == [PithosContainerNode class]) {
+                // A container is 
+                // off if not in dictionary
+                // mixed if in dictionary with exclusions
+                // on if in dictionary without exclusions
+                PithosContainerNode *node = (PithosContainerNode *)item;
+                NSMutableArray *containerExcludedDirectories = [syncContainersDictionary 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 files are off if parent container not in dictionary or if excluded
+                // displayName should be localized and lowercased
+                PithosSubdirNode *node = (PithosSubdirNode *)item;
+                NSMutableArray *containerExcludedDirectories = [syncContainersDictionary objectForKey:node.parent.displayName];
+                NSString *directoryName = [[node.displayName lowercaseString] stringByReplacingOccurrencesOfString:@"/" withString:@":"];
+                if (!containerExcludedDirectories || [containerExcludedDirectories containsObject:directoryName])
+                    return [NSNumber numberWithUnsignedInteger:NSOffState];
+                else
+                    return [NSNumber numberWithUnsignedInteger:NSOnState];
+            } else if ([item class] == [PithosEmptyNode class]) {
+                // Root files are off if parent container not in dictionary or if excluded
+                PithosEmptyNode *node = (PithosEmptyNode *)item;
+                NSMutableArray *containerExcludedDirectories = [syncContainersDictionary 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 == syncContainersOutlineView) {
+        if ([[tableColumn identifier] isEqualToString:@"sync"]) {
+            NSCellStateValue newState = [object unsignedIntegerValue];
+            if ([item class] == [PithosContainerNode class]) {
+                // If new state is
+                // mixed/on include container with no excluded directories
+                // off exclude container
+                PithosContainerNode *node = (PithosContainerNode *)item;
+                if ((newState == NSOnState) || (newState == NSMixedState)) {
+                    [syncContainersDictionary setObject:[NSMutableArray array] forKey:node.displayName];
+                } else {
+                    [syncContainersDictionary removeObjectForKey:node.displayName];
+                }
+                [outlineView reloadItem:item reloadChildren:YES];
+            } else if ([item class] == [PithosSubdirNode class]) {
+                // If new state is
+                // mixed/on include directory (if container not included, include and exclude all others)
+                // off exclude directory
+                PithosSubdirNode *node = (PithosSubdirNode *)item;
+                NSMutableArray *containerExcludedDirectories = [syncContainersDictionary objectForKey:node.parent.displayName];
+                NSString *directoryName = [[node.displayName lowercaseString] stringByReplacingOccurrencesOfString:@"/" withString:@":"];
+                if ((newState == NSOnState) || (newState == NSMixedState)) {
+                    if (containerExcludedDirectories) {
+                        [containerExcludedDirectories removeObject:directoryName];
+                    } else {
+                        NSMutableArray *newContainerExcludeDirectories = [NSMutableArray arrayWithObject:@""];
+                        for (PithosNode *siblingNode in node.parent.children) {
+                            if ([siblingNode class] == [PithosSubdirNode class]) {
+                                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 (![containerExcludedDirectories containsObject:directoryName]) {
+                    [containerExcludedDirectories addObject:directoryName];
+                }
+                [outlineView reloadItem:[outlineView parentForItem:item]];
+            } 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;
+                NSMutableArray *containerExcludedDirectories = [syncContainersDictionary objectForKey:node.parent.displayName];
+                if ((newState == NSOnState) || (newState == NSMixedState)) {
+                    if (containerExcludedDirectories) {
+                        [containerExcludedDirectories removeObject:@""];
+                    } else {
+                        NSMutableArray *newContainerExcludeDirectories = [NSMutableArray array];
+                        for (PithosNode *siblingNode in node.parent.children) {
+                            if ([siblingNode class] == [PithosSubdirNode class]) {
+                                NSString *siblingDirectoryName = [[siblingNode.displayName lowercaseString] stringByReplacingOccurrencesOfString:@"/" withString:@":"];
+                                if (![newContainerExcludeDirectories containsObject:siblingDirectoryName])
+                                    [newContainerExcludeDirectories addObject:siblingDirectoryName];
+                            }
+                        }
+                        [syncContainersDictionary setObject:newContainerExcludeDirectories forKey:node.parent.displayName];
+                    }
+                } else if (![containerExcludedDirectories containsObject:@""]) {
+                    [containerExcludedDirectories addObject:@""];
+                }
+                [outlineView reloadItem:[outlineView parentForItem:item]];
+            }
+            [self updateSync];
+        }
+    }
+}
+
 @end