Make browser and preferences window load lazily.
[pithos-macos] / pithos-macos / pithos_macosAppDelegate.m
1 //
2 //  pithos_macosAppDelegate.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 "pithos_macosAppDelegate.h"
39 #import "PithosAccount.h"
40 #import "PithosBrowserController.h"
41 #import "PithosPreferencesController.h"
42 #import "PithosSyncDaemon.h"
43 #import "ASIPithosRequest.h"
44 #import "ASIPithos.h"
45 #import "ASIDownloadCache.h"
46 #import "LastCompletedSyncTransformer.h"
47
48 @implementation pithos_macosAppDelegate
49 @synthesize pithosBrowserController, pithosPreferencesController, alwaysNo, activated, 
50 currentPithosAccount, pithosAccounts, pithosAccountsDictionary, syncPithosAccount, activityFacilityTimeInterval;
51
52 - (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
53     [[NSAppleEventManager sharedAppleEventManager] setEventHandler:self 
54                                                        andSelector:@selector(handleAppleEvent:withReplyEvent:) 
55                                                      forEventClass:kInternetEventClass 
56                                                         andEventID:kAEGetURL];
57     
58     userDefaults = [[NSUserDefaults standardUserDefaults] retain];
59     
60     syncTimeInterval = [userDefaults doubleForKey:@"syncTimeInterval"];
61     if (syncTimeInterval <= 0.0) {
62         syncTimeInterval = 180.0;
63         [userDefaults setDouble:syncTimeInterval forKey:@"syncTimeInterval"];
64         [userDefaults synchronize];
65     }
66
67     activityFacilityTimeInterval = [userDefaults doubleForKey:@"activityFacilityTimeInterval"];
68     if (activityFacilityTimeInterval <= 0.0) {
69         activityFacilityTimeInterval = 0.05;
70         [userDefaults setDouble:activityFacilityTimeInterval forKey:@"activityFacilityTimeInterval"];
71         [userDefaults synchronize];
72     }
73
74     NSData *tmpData = [userDefaults objectForKey:@"pithosAccounts"];
75     NSArray *tmpArray;
76     if (tmpData && (tmpArray = [NSKeyedUnarchiver unarchiveObjectWithData:tmpData]))
77         self.pithosAccounts = [NSMutableArray arrayWithArray:tmpArray];
78     else
79         self.pithosAccounts = [NSMutableArray array];
80     
81     if (![pithosAccounts count]) {
82         [pithosAccounts addObject:[PithosAccount pithosAccount]];
83         self.pithosAccounts = self.pithosAccounts;
84     } else {
85         self.activated = YES;
86     }
87     
88     pithosAccountsDictionary = [[NSMutableDictionary alloc] initWithCapacity:[pithosAccounts count]];
89     for (PithosAccount *pithosAccount in pithosAccounts) {
90         [pithosAccountsDictionary setObject:pithosAccount forKey:pithosAccount.name];
91         if (!currentPithosAccount && pithosAccount.active)
92             currentPithosAccount = [pithosAccount retain];
93     }
94     if (!currentPithosAccount)
95         self.currentPithosAccount = [pithosAccounts objectAtIndex:0];
96     
97     if (currentPithosAccount.active) {
98         [self savePithosAccounts:self];
99         [self showPithosBrowser:self];
100         self.pithosBrowserController.pithos = currentPithosAccount.pithos;
101     } else {
102         // XXX maybe call specifically to go to new account tab
103         [self showPithosPreferences:self];
104     }
105
106     syncTimer = [[NSTimer scheduledTimerWithTimeInterval:syncTimeInterval 
107                                                   target:self 
108                                                 selector:@selector(sync) 
109                                                 userInfo:nil 
110                                                  repeats:YES] retain];
111     [syncTimer fire];
112 }
113
114 // Based on: http://cocoatutorial.grapewave.com/2010/01/creating-a-status-bar-application/
115 // and: http://www.cocoadev.com/index.pl?ThumbnailImages
116 - (void)awakeFromNib {
117     NSImage *sourceImage = [NSImage imageNamed:@"pithos-large.png"];
118     
119     NSImage *smallImage = [[[NSImage alloc] initWithSize:NSMakeSize(18, 18)] autorelease];
120     [smallImage lockFocus];
121     [[NSGraphicsContext currentContext] setImageInterpolation:NSImageInterpolationHigh];
122     [sourceImage setSize:NSMakeSize(18, 18)];
123     [sourceImage compositeToPoint:NSZeroPoint operation:NSCompositeCopy];
124     [smallImage unlockFocus];
125     
126     statusItem = [[[NSStatusBar systemStatusBar] statusItemWithLength:NSSquareStatusItemLength] retain];
127     [statusItem setMenu:statusMenu];
128     [statusItem setImage:sourceImage];
129     [statusItem setHighlightMode:YES];
130     
131     self.alwaysNo = NO;
132 }
133
134 - (void)handleAppleEvent:(NSAppleEventDescriptor *)event withReplyEvent: (NSAppleEventDescriptor *)replyEvent {
135     NSURL *url = [NSURL URLWithString:[[event paramDescriptorForKeyword:keyDirectObject] stringValue]];
136     NSString *host = [url host];
137         NSString *query = [url query];
138     PithosAccount *pithosAccount = [pithosAccountsDictionary objectForKey:[url lastPathComponent]];
139     NSProcessInfo *processInfo = [NSProcessInfo processInfo];
140     if ([host isEqualToString:[NSString stringWithFormat:@"%@_%d", [processInfo processName], [processInfo processIdentifier]]] && 
141         pithosAccount && query) {
142         // user=
143         NSString *authUser;
144         NSRange userRange = [query rangeOfString:@"user=" options:NSCaseInsensitiveSearch];
145         if (userRange.length == 0)
146             // XXX maybe show an error message?
147             return;
148         NSUInteger authUserStartLocation = userRange.location + userRange.length;
149         NSRange userEndRange = [query rangeOfString:@"&" options:NSCaseInsensitiveSearch 
150                                               range:NSMakeRange(authUserStartLocation, [query length] - authUserStartLocation)];
151         if (userEndRange.length) {
152             authUser = [[query substringWithRange:NSMakeRange(authUserStartLocation, userEndRange.location - authUserStartLocation)]
153                         stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
154         } else {
155             authUser = [[query substringFromIndex:authUserStartLocation]
156                         stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
157         }
158         // token=
159         NSString *authToken;
160         NSRange tokenRange = [query rangeOfString:@"token=" options:NSCaseInsensitiveSearch];
161         if (tokenRange.length == 0)
162             // XXX maybe show an error message?
163             return;
164         NSUInteger authTokenStartLocation = tokenRange.location + tokenRange.length;
165         NSRange tokenEndRange = [query rangeOfString:@"&" options:NSCaseInsensitiveSearch 
166                                               range:NSMakeRange(authTokenStartLocation, [query length] - authTokenStartLocation)];
167         if (tokenEndRange.length) {
168             authToken = [[query substringWithRange:NSMakeRange(authTokenStartLocation, tokenEndRange.location - authTokenStartLocation)]
169                          stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
170         } else {
171             authToken = [[query substringFromIndex:authTokenStartLocation]
172                          stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
173         }
174         
175         NSLog(@"query authUser: '%@', authToken: '%@'", authUser, authToken);
176         if ([authUser length] && [authToken length]) {
177             [pithosAccount authenticateWithServerURL:nil authUser:authUser authToken:authToken];
178             [self savePithosAccounts:self];
179             if (self.pithosPreferencesController && [self.pithosPreferencesController.selectedPithosAccount isEqualTo:pithosAccount]) {
180                 self.pithosPreferencesController.authUser = pithosAccount.authUser;
181                 self.pithosPreferencesController.authToken = pithosAccount.authToken;
182             }
183             self.activated = YES;
184             if ([pithosAccount isEqualTo:currentPithosAccount]) {
185                 [self showPithosBrowser:self];
186                 self.pithosBrowserController.pithos = pithosAccount.pithos;
187             }
188         }
189         // XXX else maybe show an error message?
190     }
191     // XXX else maybe show an error message?
192 }
193
194 - (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender {
195     [self savePithosAccounts:self];
196     if ([self.pithosBrowserController operationsPending]) {
197         NSAlert *alert = [[[NSAlert alloc] init] autorelease];
198         [alert setMessageText:@"Pending Operations"];
199         [alert setInformativeText:@"There are pending operations in the browser, do you want to quit and cancel them?"];
200         [alert addButtonWithTitle:@"OK"];
201         [alert addButtonWithTitle:@"Cancel"];
202         NSInteger choice = [alert runModal];
203         if (choice == NSAlertSecondButtonReturn) 
204             return NSTerminateCancel;
205     }
206     return NSTerminateNow;
207 }
208
209 #pragma mark -
210 #pragma mark Properties
211
212 - (PithosBrowserController *)pithosBrowserController {
213     if (!pithosBrowserController) {
214         pithosBrowserController = [[PithosBrowserController alloc] init];
215     }
216     return pithosBrowserController;
217 }
218
219 - (PithosPreferencesController *)pithosPreferencesController {
220     if (!pithosPreferencesController) {
221         pithosPreferencesController = [[PithosPreferencesController alloc] init];
222     }
223     return pithosPreferencesController;
224 }
225
226 #pragma mark -
227 #pragma mark NSMenuDelegate
228
229 - (void)menuNeedsUpdate:(NSMenu *)menu {
230     NSMenuItem *menuItem;
231     [menu removeAllItems];
232     if ([menu isEqualTo:accountsMenu]) {
233         [menu setAutoenablesItems:NO];
234         for (PithosAccount *pithosAccount in pithosAccounts) {
235             menuItem = [[[NSMenuItem alloc] initWithTitle:pithosAccount.name 
236                                                    action:@selector(menuChangePithosAccount:) 
237                                             keyEquivalent:@""] autorelease];
238             [menuItem setRepresentedObject:pithosAccount];
239             [menuItem setEnabled:pithosAccount.active];
240             [menuItem setState:((pithosAccount.active && [currentPithosAccount isEqualTo:pithosAccount]) ? NSOnState : NSOffState)];
241             [menu addItem:menuItem];
242         }
243     } else if ([menu isEqualTo:lastSyncMenu]) {
244         NSString *menuItemTitle;
245         [menu setAutoenablesItems:NO];
246         for (PithosAccount *pithosAccount in pithosAccounts) {
247             menuItemTitle = [NSString stringWithFormat:@"%@: %@", 
248                              pithosAccount.name, 
249                              [[[[LastCompletedSyncTransformer alloc] init] autorelease] transformedValue:pithosAccount.syncLastCompleted]];
250             if ([pithosAccount isEqualTo:syncPithosAccount] && [pithosAccount.syncDaemon isSyncing])
251                 menuItemTitle = [menuItemTitle stringByAppendingString:@" (syncing)"];
252 //            menuItem = [[[NSMenuItem alloc] initWithTitle:menuItemTitle 
253 //                                                   action:@selector(menuChangeSyncActive:) 
254 //                                            keyEquivalent:@""] autorelease];
255 //            [menuItem setRepresentedObject:pithosAccount];
256 //            [menuItem setEnabled:pithosAccount.active];
257 //            [menuItem setState:((pithosAccount.active && pithosAccount.syncActive) ? NSOnState : NSOffState)];
258             menuItem = [[[NSMenuItem alloc] initWithTitle:menuItemTitle action:nil keyEquivalent:@""] autorelease];
259             [menuItem setEnabled:NO];
260             [menuItem setState:NO];
261             [menu addItem:menuItem];
262         }
263         [menu addItem:[NSMenuItem separatorItem]];
264         [menu addItem:[[[NSMenuItem alloc] initWithTitle:@"Next Sync" 
265                                                   action:@selector(sync) 
266                                            keyEquivalent:@""] autorelease]];
267     }
268 }
269
270 #pragma mark -
271 #pragma mark Actions
272
273 - (IBAction)showPithosBrowser:(id)sender {
274     if (!activated)
275         return;
276     [self.pithosBrowserController showWindow:sender];
277     [[self.pithosBrowserController window] makeKeyAndOrderFront:sender];
278     [NSApp activateIgnoringOtherApps:YES];
279 }
280
281 - (IBAction)showPithosPreferences:(id)sender {
282     [self.pithosPreferencesController showWindow:sender];
283     [[self.pithosPreferencesController window] makeKeyAndOrderFront:sender];
284     [NSApp activateIgnoringOtherApps:YES];
285 }
286
287 - (void)sync {
288     if (!activated || ![pithosAccounts count])
289         return;
290     NSUInteger syncIndex;
291     BOOL syncPithosAccountFound = [pithosAccounts containsObject:syncPithosAccount];
292     if (syncPithosAccountFound)
293          syncIndex = [pithosAccounts indexOfObject:syncPithosAccount];
294     
295     PithosAccount *singleSyncPithosAccount = nil;
296     for (PithosAccount *pithosAccount in pithosAccounts) {
297         if (!singleSyncPithosAccount && pithosAccount.active && pithosAccount.syncActive && pithosAccount.syncDaemon) {
298             singleSyncPithosAccount = pithosAccount;
299         } else if (singleSyncPithosAccount && pithosAccount.active && pithosAccount.syncActive && pithosAccount.syncDaemon) {
300             singleSyncPithosAccount = nil;
301             break;
302         }
303     }
304     
305     if (syncPithosAccount && syncPithosAccount.active && syncPithosAccount.syncActive && syncPithosAccount.syncDaemon) {
306         // An active syncDaemon was previously syncing
307         if (singleSyncPithosAccount && [singleSyncPithosAccount isEqualTo:syncPithosAccount]) {
308             // It's the only one, sync again
309             [syncPithosAccount.syncDaemon startDaemon];
310             [syncPithosAccount.syncDaemon sync];
311             return;
312         } else if ([syncPithosAccount.syncDaemon isSyncing]) {
313             // It's still syncing, mark it as late and return
314             [syncPithosAccount.syncDaemon syncLate];
315             return;
316         }
317     }
318     PithosAccount *newSyncPithosAccount = nil;
319     if (syncPithosAccountFound) {
320         for (PithosAccount *pithosAccount in [pithosAccounts objectsAtIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(syncIndex + 1, [pithosAccounts count] - syncIndex - 1)]]) {
321             if (pithosAccount.active && pithosAccount.syncActive && pithosAccount.syncDaemon) {
322                 newSyncPithosAccount = pithosAccount;
323                 break;
324             }
325         }
326         if (!newSyncPithosAccount) {
327             for (PithosAccount *pithosAccount in [pithosAccounts objectsAtIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, syncIndex)]]) {
328                 if (pithosAccount.active && pithosAccount.syncActive && pithosAccount.syncDaemon) {
329                     newSyncPithosAccount = pithosAccount;
330                     break;
331                 }
332             }
333         }
334     } else {
335         for (PithosAccount *pithosAccount in pithosAccounts) {
336             if (pithosAccount.active && pithosAccount.syncActive && pithosAccount.syncDaemon) {
337                 newSyncPithosAccount = pithosAccount;
338                 break;
339             }
340         }
341     }        
342     if (newSyncPithosAccount) {
343         // A different syncDaemon is found, sync it
344         self.syncPithosAccount = newSyncPithosAccount;
345         [syncPithosAccount.syncDaemon startDaemon];
346         [syncPithosAccount.syncDaemon sync];
347     } else if (syncPithosAccountFound && syncPithosAccount && syncPithosAccount.active && syncPithosAccount.syncActive && syncPithosAccount.syncDaemon) {
348         [syncPithosAccount.syncDaemon startDaemon];
349         [syncPithosAccount.syncDaemon sync];
350     } else {
351         self.syncPithosAccount = nil;
352     }
353 }
354
355 - (void)savePithosAccounts:(id)sender {
356     [userDefaults setObject:[NSKeyedArchiver archivedDataWithRootObject:pithosAccounts] forKey:@"pithosAccounts"];
357     [userDefaults synchronize];
358 }
359
360 - (void)removedPithosAccount:(PithosAccount *)removedPithosAccount {
361     if ([self.currentPithosAccount isEqualTo:removedPithosAccount]) {
362         for (PithosAccount *pithosAccount in pithosAccounts) {
363             if (pithosAccount.active) {
364                 self.currentPithosAccount = pithosAccount;
365                 self.pithosBrowserController.pithos = currentPithosAccount.pithos;
366                 break;
367             }
368         }
369         if ([self.currentPithosAccount isEqualTo:removedPithosAccount]) {
370             self.activated = NO;
371             [self.pithosBrowserController.window close];
372             [self.pithosBrowserController resetBrowser];
373             self.currentPithosAccount = [pithosAccounts objectAtIndex:0];
374         }
375     }
376     if ([self.syncPithosAccount isEqualTo:removedPithosAccount])
377         self.syncPithosAccount = nil;
378 }
379
380 #pragma mark -
381 #pragma mark Menu Actions
382
383 - (void)menuChangePithosAccount:(NSMenuItem *)sender {
384     PithosAccount *pithosAccount = (PithosAccount *)[sender representedObject];
385     if (!pithosAccount.active)
386         return;
387     if (![currentPithosAccount isEqualTo:pithosAccount] && [pithosAccounts containsObject:pithosAccount]) {
388         if ([self.pithosBrowserController operationsPending]) {
389             NSAlert *alert = [[[NSAlert alloc] init] autorelease];
390             [alert setMessageText:@"Pending Operations"];
391             [alert setInformativeText:@"There are pending operations in the browser, do you want to change accounts and cancel them?"];
392             [alert addButtonWithTitle:@"OK"];
393             [alert addButtonWithTitle:@"Cancel"];
394             NSInteger choice = [alert runModal];
395             if (choice == NSAlertSecondButtonReturn) 
396                 return;
397         }
398         self.currentPithosAccount = pithosAccount;
399         [self showPithosBrowser:self];
400         self.pithosBrowserController.pithos = currentPithosAccount.pithos;
401     }
402 }
403
404 //- (void)menuChangeSyncActive:(NSMenuItem *)sender {
405 //    PithosAccount *pithosAccount = (PithosAccount *)[sender representedObject];
406 //    if (!pithosAccount.active)
407 //        return;
408 //    pithosAccount.syncActive = !pithosAccount.syncActive;
409 //    if (self.pithosPreferencesController && [self.pithosPreferencesController.selectedPithosAccount isEqualTo:pithosAccount])
410 //        self.pithosPreferencesController.syncActive = pithosAccount.syncActive;
411 //    [self savePithosAccounts:self];
412 //}
413
414 @end