unison

Fork of Unison, a bi-directional file synchronization tool
git clone git://git.laack.co/unison.git
Log | Files | Refs | README | LICENSE

MyController.m (43955B)


      1 /* Copyright (c) 2003, 2016, see file COPYING for details. */
      2 
      3 #import "MyController.h"
      4 
      5 /* The following two define are a workaround for an incompatibility between
      6 Ocaml 3.11.2 (and older) and the Mac OS X header files */
      7 #define uint64 uint64_caml
      8 #define int64 int64_caml
      9 
     10 #define CAML_NAME_SPACE
     11 #include <caml/callback.h>
     12 #include <caml/alloc.h>
     13 #include <caml/mlvalues.h>
     14 #include <caml/memory.h>
     15 
     16 @interface NSString (_UnisonUtil)
     17 - (NSString *)trim;
     18 @end
     19 
     20 @implementation MyController
     21 
     22 static MyController *me; // needed by reloadTable and displayStatus, below
     23 
     24 // BCP (11/09): Added per Onne Gorter:
     25 // if user closes main window, terminate app, instead of keeping an empty app around with no window
     26 - (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)theApplication {
     27   return YES;
     28 }
     29 
     30 - (id)init
     31 {
     32   if (([super init])) {
     33 
     34     /* Initialize locals */
     35     me = self;
     36     doneFirstDiff = NO;
     37 
     38     NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
     39     NSDictionary *appDefaults = [NSDictionary dictionaryWithObjectsAndKeys:
     40                                  /* By default, invite user to install cltool */
     41                                  @"YES",  @"CheckCltool",
     42                                  @"NO", @"openProfileAtStartup",
     43                                  @"",   @"profileToOpen",
     44                                  @"NO", @"deleteLogOnExit",
     45                                  @"",   @"detailsFont",
     46                                  @"",   @"diffFont",
     47                                  nil];
     48 
     49     [defaults registerDefaults:appDefaults];
     50     fontChangeTarget = nil;
     51   }
     52 
     53   return self;
     54 }
     55 
     56 - (void) applicationWillTerminate:(NSNotification *)aNotification {
     57   NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
     58   [defaults setObject:[NSArchiver archivedDataWithRootObject:[detailsTextView font]] forKey:@"detailsFont"];
     59   [defaults setObject:[NSArchiver archivedDataWithRootObject:[diffView font]] forKey:@"diffFont"];
     60   [defaults synchronize];
     61 }
     62 
     63 - (void)awakeFromNib
     64 {
     65   [splitView setAutosaveName:@"splitView"];
     66 
     67   NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
     68   NSFont *defaultFont = [NSFont fontWithName:@"Monaco" size:11];
     69   NSData *detailsFontData = [defaults dataForKey:@"detailsFont"];
     70   if (detailsFontData) {
     71     NSFont *tmpFont = (NSFont*) [NSUnarchiver unarchiveObjectWithData:detailsFontData];
     72     if (tmpFont)
     73       [detailsTextView setFont:tmpFont];
     74     else
     75       [detailsTextView setFont:defaultFont];
     76   } else
     77     [detailsTextView setFont:defaultFont];
     78   [detailsTextView.cell setBackgroundStyle:NSBackgroundStyleRaised];
     79   [detailsTextView.cell setBackgroundStyle:NSBackgroundStyleRaised];
     80 
     81   NSColor *startColor = [NSColor colorWithCalibratedRed:0.613 green:0.665 blue:0.715 alpha:1.000];
     82   NSColor *endColor = [NSColor colorWithCalibratedRed:0.439 green:0.496 blue:0.548 alpha:1.000];
     83 
     84   [detailsTextViewGradient setStartingColor:startColor];
     85   [detailsTextViewGradient setEndingColor:endColor];
     86   [detailsTextViewGradient setAngle:270];
     87   [connectingViewGradient setStartingColor:startColor];
     88   [connectingViewGradient setEndingColor:endColor];
     89   [connectingViewGradient setAngle:270];
     90 
     91   NSData *diffFontData = [defaults dataForKey:@"diffFont"];
     92   if (diffFontData) {
     93     NSFont *tmpFont = (NSFont*) [NSUnarchiver unarchiveObjectWithData:diffFontData];
     94     if (tmpFont)
     95       [diffView setFont:tmpFont];
     96     else
     97       [diffView setFont:defaultFont];
     98   } else
     99     [diffView setFont:defaultFont];
    100 
    101   blankView = [[NSView alloc] init];
    102 
    103   /* Double clicking in the profile list will open the profile */
    104   [[profileController tableView] setTarget:self];
    105   [[profileController tableView] setDoubleAction:@selector(openButton:)];
    106 
    107         [tableView setAutoresizesOutlineColumn:NO];
    108 
    109         // use combo-cell for path
    110   [[tableView tableColumnWithIdentifier:@"path"] setDataCell:[[[ImageAndTextCell alloc] init] autorelease]];
    111 
    112         // Custom progress cell
    113         ProgressCell *progressCell = [[[ProgressCell alloc] init] autorelease];
    114         [[tableView tableColumnWithIdentifier:@"percentTransferred"] setDataCell:progressCell];
    115 
    116   /* Set up the version string in the about box.  We use a custom
    117    about box just because PRCS doesn't seem capable of getting the
    118    version into the InfoPlist.strings file; otherwise we'd use the
    119    standard about box. */
    120   [versionText setStringValue:ocamlCall("S", "unisonGetVersion")];
    121 
    122   /* Command-line processing */
    123         OCamlValue *clprofile = (id)ocamlCall("@", "unisonInit0");
    124 
    125         BOOL areRootsSet = (long)ocamlCall("i", "areRootsSet") ? YES : NO;
    126         /*
    127         if (areRootsSet) {
    128                 NSLog(@"Roots are on the command line");
    129         }
    130         else {
    131                 NSLog(@"Roots are not set on the command line");
    132         }
    133         */
    134 
    135   /* Add toolbar */
    136   toolbar = [[[UnisonToolbar alloc]
    137               initWithIdentifier: @"unisonToolbar" :self :tableView] autorelease];
    138   [mainWindow setToolbar: toolbar];
    139         [toolbar takeTableModeView:tableModeSelector];
    140         [self initTableMode];
    141 
    142 
    143   /* Set up the first window the user will see */
    144   if (clprofile) {
    145     /* A profile name was given on the command line */
    146                 NSString *profileName = [clprofile getField:0 withType:'S'];
    147     [self profileSelected:profileName];
    148 
    149     /* If invoked from terminal we need to bring the app to the front */
    150     [NSApp activateIgnoringOtherApps:YES];
    151 
    152     /* Start the connection */
    153     [self connect:profileName];
    154   }
    155   else if (areRootsSet) {
    156         /* If invoked from terminal we need to bring the app to the front */
    157         [NSApp activateIgnoringOtherApps:YES];
    158         /* Start the connection with the empty profile name, indicating roots only */
    159         [self connect:@""];
    160   }
    161   else {
    162     /* If invoked from terminal we need to bring the app to the front */
    163     [NSApp activateIgnoringOtherApps:YES];
    164     if ([[NSUserDefaults standardUserDefaults] boolForKey:@"openProfileAtStartup"]) {
    165       NSString *profileToOpen = [[NSUserDefaults standardUserDefaults]
    166                                  stringForKey:@"profileToOpen"];
    167       if ([[profileToOpen trim] compare:@""] != NSOrderedSame &&
    168           [[profileController getProfiles] indexOfObject:profileToOpen] != NSNotFound) {
    169         [self profileSelected:profileToOpen];
    170         [self connect:profileToOpen];
    171       } else {
    172         /* Bring up the dialog to choose a profile */
    173         [self chooseProfiles];
    174       }
    175     } else {
    176       /* Bring up the dialog to choose a profile */
    177       [self chooseProfiles];
    178     }
    179   }
    180 
    181   [mainWindow display];
    182   [mainWindow makeKeyAndOrderFront:nil];
    183 
    184   /* unless user has clicked Don't ask me again, ask about cltool */
    185   if ( ([[NSUserDefaults standardUserDefaults] boolForKey:@"CheckCltool"]) &&
    186           (![[NSFileManager defaultManager]
    187               /* BCP 6/2016: Changed from /usr/bin/unison for El Capitan, per
    188                  suggestion from Alan Shutko */
    189                  fileExistsAtPath:@"/usr/local/bin/unison"]) )
    190           [self raiseCltoolWindow:nil];
    191 }
    192 
    193 - (IBAction) checkOpenProfileChanged:(id)sender {
    194   [profileBox setEnabled:[checkOpenProfile state]];
    195   if ([profileBox isEnabled] && [profileBox indexOfSelectedItem] < 0) {
    196     [profileBox selectItemAtIndex:0];
    197     [[NSUserDefaults standardUserDefaults] setObject:[profileBox itemObjectValueAtIndex:0] forKey:@"profileToOpen"];
    198   }
    199 }
    200 
    201 - (IBAction) chooseFont:(id)sender {
    202   [[NSFontPanel sharedFontPanel] makeKeyAndOrderFront:self];
    203   [[NSFontManager sharedFontManager] setDelegate:self];
    204   fontChangeTarget = sender;
    205 }
    206 
    207 - (void) changeFont:(id)sender {
    208   NSFont *newFont = [sender convertFont:[detailsTextView font]];
    209   if (fontChangeTarget == chooseDetailsFont)
    210     [detailsTextView setFont:newFont];
    211   else if (fontChangeTarget == chooseDiffFont)
    212     [diffView setFont:newFont];
    213   [self updateFontDisplay];
    214 }
    215 
    216 - (void) updateFontDisplay {
    217   NSFont *detailsFont = [detailsTextView font];
    218   NSFont *diffFont = [diffView font];
    219   [detailsFontLabel setStringValue:[NSString stringWithFormat:@"%@ : %ld", [detailsFont displayName], (long) [detailsFont pointSize]]];
    220   [diffFontLabel setStringValue:[NSString stringWithFormat:@"%@ : %ld", [diffFont displayName], (long) [diffFont pointSize]]];
    221 }
    222 
    223 - (void)chooseProfiles
    224 {
    225     [mainWindow setContentView:blankView];
    226     [self resizeWindowToSize:[chooseProfileView frame].size];
    227     [mainWindow setContentMinSize:
    228         NSMakeSize(NSWidth([[mainWindow contentView] frame]),150)];
    229     [mainWindow setContentMaxSize:NSMakeSize(FLT_MAX, FLT_MAX)];
    230     [mainWindow setContentView:chooseProfileView];
    231     [toolbar setView:@"chooseProfileView"];
    232     [mainWindow setTitle:@"Unison"];
    233 
    234     // profiles get keyboard input
    235     [mainWindow makeFirstResponder:[profileController tableView]];
    236     [chooseProfileView display];
    237 }
    238 
    239 - (IBAction)createButton:(id)sender
    240 {
    241     [preferencesController reset];
    242     [mainWindow setContentView:blankView];
    243     [self resizeWindowToSize:[preferencesView frame].size];
    244     [mainWindow setContentMinSize:
    245         NSMakeSize(400,NSHeight([[mainWindow contentView] frame]))];
    246     [mainWindow setContentMaxSize:
    247         NSMakeSize(FLT_MAX,NSHeight([[mainWindow contentView] frame]))];
    248     [mainWindow setContentView:preferencesView];
    249     [toolbar setView:@"preferencesView"];
    250 }
    251 
    252 - (IBAction)saveProfileButton:(id)sender
    253 {
    254     if ([preferencesController validatePrefs]) {
    255         // so the list contains the new profile
    256         [profileController initProfiles];
    257         [self chooseProfiles];
    258     }
    259 }
    260 
    261 - (IBAction)cancelProfileButton:(id)sender
    262 {
    263     [self chooseProfiles];
    264 }
    265 
    266 /* Only valid once a profile has been selected */
    267 - (NSString *)profile {
    268     return myProfile;
    269 }
    270 
    271 - (void)profileSelected:(NSString *)aProfile
    272 {
    273     [aProfile retain];
    274     [myProfile release];
    275     myProfile = aProfile;
    276     [mainWindow setTitle: [NSString stringWithFormat:@"Unison: %@", myProfile]];
    277 }
    278 
    279 - (IBAction)showPreferences:(id)sender {
    280   [profileBox removeAllItems];
    281   [profileBox addItemsWithObjectValues:[profileController getProfiles]];
    282   NSUInteger index = [[profileController getProfiles] indexOfObject:
    283                       [[NSUserDefaults standardUserDefaults]
    284                        stringForKey:@"profileToOpen"]];
    285   if (index == NSNotFound) {
    286     [checkOpenProfile setState:NSOffState];
    287     [profileBox setStringValue:@""];
    288   } else
    289     [profileBox selectItemAtIndex:index];
    290 
    291   [profileBox setEnabled:[checkOpenProfile state]];
    292   if ([profileBox isEnabled] && [profileBox indexOfSelectedItem] < 0)
    293     [profileBox selectItemAtIndex:0];
    294 
    295   [self updateFontDisplay];
    296 
    297   [self raiseWindow:preferencesWindow];
    298 }
    299 
    300 - (IBAction)restartButton:(id)sender
    301 {
    302     [tableView setEditable:NO];
    303     [self chooseProfiles];
    304 }
    305 
    306 - (IBAction)rescan:(id)sender
    307 {
    308     /* There is a delay between turning off the button and it
    309        actually being disabled. Make sure we don't respond. */
    310     if ([self validateItem:@selector(rescan:)]) {
    311         waitingForPassword = NO;
    312         [self afterOpen];
    313     }
    314 }
    315 
    316 - (IBAction)openButton:(id)sender
    317 {
    318     NSString *profile = [profileController selected];
    319     if (profile) {
    320         [self profileSelected:profile];
    321         [self connect:profile];
    322     }
    323     return;
    324 }
    325 
    326 - (void)updateToolbar
    327 {
    328   [toolbar validateVisibleItems];
    329         [tableModeSelector setEnabled:((syncable && !duringSync) || afterSync)];
    330 
    331         // Why?
    332     [updatesView setNeedsDisplay:YES];
    333 }
    334 
    335 - (void)updateTableViewWithReset:(BOOL)shouldResetSelection
    336 {
    337         [tableView reloadData];
    338         if (shouldResetSelection) {
    339                 [tableView selectRowIndexes:[NSIndexSet indexSetWithIndex:0] byExtendingSelection:NO];
    340                 shouldResetSelection = NO;
    341         }
    342         [updatesView setNeedsDisplay:YES];
    343 }
    344 
    345 - (void)updateProgressBar:(NSNumber *)newProgress
    346 {
    347         // NSLog(@"Updating progress bar: %i - %i", (int)[newProgress doubleValue], (int)[progressBar doubleValue]);
    348         [progressBar incrementBy:([newProgress doubleValue] - [progressBar doubleValue])];
    349 }
    350 
    351 - (void)updateTableViewSelection
    352 {
    353     NSInteger n = [tableView numberOfSelectedRows];
    354     if (n == 1) [self displayDetails:[tableView itemAtRow:[tableView selectedRow]]];
    355     else [self clearDetails];
    356 }
    357 
    358 - (void)outlineViewSelectionDidChange:(NSNotification *)note
    359 {
    360         [self updateTableViewSelection];
    361 }
    362 
    363 - (void)connect:(NSString *)profileName
    364 {
    365   // contact server, propagate prefs
    366   /* NSLog(@"Connecting to %@...", profileName); */
    367 
    368   // Switch to ConnectingView
    369   [mainWindow setContentView:blankView];
    370   [self resizeWindowToSize:[updatesView frame].size];
    371   [mainWindow setContentMinSize:NSMakeSize(150,150)];
    372   [mainWindow setContentMaxSize:NSMakeSize(FLT_MAX, FLT_MAX)];
    373   [mainWindow setContentView:ConnectingView];
    374   [toolbar setView:@"connectingView"];
    375 
    376   // Update (almost) immediately
    377   [ConnectingView display];
    378   [connectingAnimation startAnimation:self];
    379 
    380   syncable = NO;
    381   afterSync = NO;
    382 
    383         [self updateToolbar];
    384 
    385         // will spawn thread on OCaml side and callback when complete
    386   (void)ocamlCall("xS", "unisonInit1", profileName);
    387 }
    388 
    389 CAMLprim value unisonInit1Complete(value v)
    390 {
    391   id pool = [[NSAutoreleasePool alloc] init];
    392   if (v == Val_unit) {
    393     /* NSLog(@"Connected."); */
    394     [me->connectingAnimation stopAnimation:me];
    395                 [me->preconn release];
    396                 me->preconn = NULL;
    397     [me performSelectorOnMainThread:@selector(afterOpen:) withObject:nil waitUntilDone:FALSE];
    398   } else {
    399     // prompting required
    400                 me->preconn = [[OCamlValue alloc] initWithValue:Field(v,0)]; // value of Some
    401                 [me performSelectorOnMainThread:@selector(unisonInit1Complete:) withObject:nil waitUntilDone:FALSE];
    402         }
    403   [pool release];
    404   return Val_unit;
    405 }
    406 
    407 - (void)unisonInit1Complete:(id)ignore
    408 {
    409         @try {
    410                 OCamlValue *prompt = ocamlCall("@@", "openConnectionPrompt", preconn);
    411                 if (!prompt) {
    412                         // turns out, no prompt needed, but must finish opening connection
    413                         ocamlCall("x@", "openConnectionEnd", preconn);
    414                         // NSLog(@"Connected.");
    415                         waitingForPassword = NO;
    416                         [self afterOpen];
    417                         return;
    418                 }
    419                 waitingForPassword = YES;
    420 
    421                 [self raisePasswordWindow:[prompt getField:0 withType:'S']];
    422         } @catch (NSException *ex) {
    423             NSRunAlertPanel(@"Connection Error", @"%@", @"OK", nil, nil, [ex description]);
    424                 [self chooseProfiles];
    425                 return;
    426         }
    427 
    428         // NSLog(@"Connected.");
    429 }
    430 
    431 - (void)raisePasswordWindow:(NSString *)prompt
    432 {
    433     // FIX: some prompts don't ask for password, need to look at it
    434     /* NSLog(@"Got the prompt: '%@'",prompt); */
    435     if ((long)ocamlCall("iS", "unisonPasswordMsg", prompt)) {
    436         [passwordPrompt setStringValue:@"Please enter your password"];
    437         [NSApp beginSheet:passwordWindow
    438             modalForWindow:mainWindow
    439             modalDelegate:nil
    440             didEndSelector:nil
    441             contextInfo:nil];
    442         return;
    443     }
    444     if ((long)ocamlCall("iS", "unisonPassphraseMsg", prompt)) {
    445         [passwordPrompt setStringValue:@"Please enter your passphrase"];
    446         [NSApp beginSheet:passwordWindow
    447             modalForWindow:mainWindow
    448             modalDelegate:nil
    449             didEndSelector:nil
    450             contextInfo:nil];
    451         return;
    452     }
    453     if ((long)ocamlCall("iS", "unisonAuthenticityMsg", prompt)) {
    454         NSInteger i = NSRunAlertPanel(@"New host",@"%@",@"Yes",@"No",nil,prompt);
    455         if (i == NSAlertDefaultReturn) {
    456                         ocamlCall("x@s", "openConnectionReply", preconn, "yes");
    457                         prompt = ocamlCall("S@", "openConnectionPrompt", preconn);
    458             if (!prompt) {
    459                 // all done with prompts, finish opening connection
    460                                 ocamlCall("x@", "openConnectionEnd", preconn);
    461                 waitingForPassword = NO;
    462                 [self afterOpen];
    463                 return;
    464             }
    465             else {
    466                                 [self raisePasswordWindow:[NSString
    467                     stringWithUTF8String:String_val(Field(prompt,0))]];
    468                 return;
    469             }
    470         }
    471         if (i == NSAlertAlternateReturn) {
    472                         ocamlCall("x@", "openConnectionCancel", preconn);
    473             return;
    474         }
    475         else {
    476             NSLog(@"Unrecognized response '%ld' from NSRunAlertPanel",(long)i);
    477                         ocamlCall("x@", "openConnectionCancel", preconn);
    478             return;
    479         }
    480     }
    481     /* Unison uimac versions <= 2.51.5 always produce an NSLog message
    482      * "Calling nonGuiStartup". Previously, this was just hidden from the
    483      * user. Starting Unison uimac version 2.51.5, error messages are
    484      * displayed to the user. Since this is not an error message, and it
    485      * is a known message to ignore, silently drop it here */
    486     if (NSEqualRanges([prompt rangeOfString:@"Calling nonGuiStartup"],
    487                       NSMakeRange(NSNotFound, 0))) {
    488         NSLog(@"Unrecognized message from ssh: '%@'",prompt);
    489         NSInteger i = NSRunAlertPanel(@"Connection Error", @"Unrecognized message from ssh: '%@'",
    490                           @"Continue", @"Cancel", nil, prompt);
    491 	if (i == NSAlertAlternateReturn) {
    492             ocamlCall("x@", "openConnectionCancel", preconn);
    493             [self chooseProfiles];
    494             return;
    495 	}
    496     }
    497     /* Unrecognized message from ssh does not immediately mean connection
    498      * failure. Continue. */
    499     prompt = ocamlCall("S@", "openConnectionPrompt", preconn);
    500     if (!prompt) {
    501         // all done with prompts, finish opening connection
    502         ocamlCall("x@", "openConnectionEnd", preconn);
    503         waitingForPassword = NO;
    504         [self afterOpen];
    505         return;
    506     } else {
    507         [self raisePasswordWindow:[NSString
    508             stringWithUTF8String:String_val(Field(prompt, 0))]];
    509         return;
    510     }
    511 }
    512 
    513 // The password window will invoke this when Enter occurs, b/c we
    514 // are the delegate.
    515 - (void)controlTextDidEndEditing:(NSNotification *)notification
    516 {
    517     NSNumber *reason = [[notification userInfo] objectForKey:@"NSTextMovement"];
    518     int code = [reason intValue];
    519     if (code == NSReturnTextMovement)
    520         [self endPasswordWindow:self];
    521 }
    522 // Or, the Continue button will invoke this when clicked
    523 - (IBAction)endPasswordWindow:(id)sender
    524 {
    525     [passwordWindow orderOut:self];
    526     [NSApp endSheet:passwordWindow];
    527     if ([sender isEqualTo:passwordCancelButton]) {
    528                 ocamlCall("x@", "openConnectionCancel", preconn);
    529         [self chooseProfiles];
    530         return;
    531     }
    532     NSString *password = [passwordText stringValue];
    533         ocamlCall("x@S", "openConnectionReply", preconn, password);
    534 
    535     OCamlValue *prompt = ocamlCall("@@", "openConnectionPrompt", preconn);
    536     if (!prompt) {
    537         // all done with prompts, finish opening connection
    538                 ocamlCall("x@", "openConnectionEnd", preconn);
    539         waitingForPassword = NO;
    540         [self afterOpen];
    541     }
    542     else {
    543                 [self raisePasswordWindow:[prompt getField:0 withType:'S']];
    544     }
    545 }
    546 
    547 - (void)afterOpen:(id)ignore
    548 {
    549         [self afterOpen];
    550 }
    551 
    552 - (void)afterOpen
    553 {
    554     if (waitingForPassword) return;
    555     // move to updates window after clearing it
    556         [self updateReconItems:nil];
    557         [progressBar setDoubleValue:0.0];
    558         [progressBar stopAnimation:self];
    559     // [self clearDetails];
    560     [mainWindow setContentView:blankView];
    561     [self resizeWindowToSize:[updatesView frame].size];
    562     [mainWindow setContentMinSize:
    563         NSMakeSize(NSWidth([[mainWindow contentView] frame]),200)];
    564     [mainWindow setContentMaxSize:NSMakeSize(FLT_MAX, FLT_MAX)];
    565     [mainWindow setContentView:updatesView];
    566     [toolbar setView:@"updatesView"];
    567 
    568     syncable = NO;
    569     afterSync = NO;
    570 
    571     [tableView deselectAll:self];
    572         [self updateToolbar];
    573         [self updateProgressBar:[NSNumber numberWithDouble:0.0]];
    574 
    575     // this should depend on the number of reconitems, and is now done
    576     // in updateReconItems:
    577     // reconItems table gets keyboard input
    578     //[mainWindow makeFirstResponder:tableView];
    579     [tableView scrollRowToVisible:0];
    580 
    581         [preconn release];
    582     preconn = nil; // so old preconn can be garbage collected
    583         // This will run in another thread spawned in OCaml and will return immediately
    584         // We'll get a call back to unisonInit2Complete() when it is complete
    585         ocamlCall("x", "unisonInit2");
    586 }
    587 
    588 - (void)doSync
    589 {
    590     [tableView setEditable:NO];
    591     syncable = NO;
    592     duringSync = YES;
    593 
    594         [self updateToolbar];
    595 
    596         // This will run in another thread spawned in OCaml and will return immediately
    597         // We'll get a call back to syncComplete() when it is complete
    598         ocamlCall("x", "unisonSynchronize");
    599 }
    600 
    601 - (IBAction)syncButton:(id)sender
    602 {
    603         [self doSync];
    604 }
    605 
    606 
    607 - (void)afterUpdate:(id)retainedReconItems
    608 {
    609         // NSLog(@"In afterUpdate:...");
    610     [self updateReconItems:retainedReconItems];
    611         [retainedReconItems release];
    612 
    613     [notificationController updateFinishedFor:[self profile]];
    614 
    615     // label the left and right columns with the roots
    616         NSString *leftHost = [(NSString *)ocamlCall("S", "unisonFirstRootString") trim];
    617         NSString *rightHost = [(NSString *)ocamlCall("S", "unisonSecondRootString") trim];
    618         /*
    619     [[[tableView tableColumnWithIdentifier:@"left"] headerCell] setObjectValue:lefthost];
    620     [[[tableView tableColumnWithIdentifier:@"right"] headerCell] setObjectValue:rightHost];
    621     */
    622     [mainWindow setTitle: [NSString stringWithFormat:@"Unison: %@ (%@ <-> %@)",
    623                         [self profile], leftHost, rightHost]];
    624 
    625         // initial sort
    626         [tableView setSortDescriptors:[NSArray arrayWithObjects:
    627                 [[tableView tableColumnWithIdentifier:@"fileSizeString"] sortDescriptorPrototype],
    628                 [[tableView tableColumnWithIdentifier:@"path"] sortDescriptorPrototype],
    629                 nil]];
    630 
    631         [self updateTableViewWithReset:([reconItems count] > 0)];
    632         [self updateToolbar];
    633         isBatchSet = (long)ocamlCall("i", "isBatchSet") ? YES : NO;
    634         /*
    635         if (isBatchSet) {
    636                 NSLog(@"batch set on the command line");
    637         }
    638         else {
    639                 NSLog(@"batch not set on the command line");
    640         }
    641         */
    642 
    643         if (isBatchSet) {
    644                 [self doSync];
    645         }
    646 }
    647 
    648 CAMLprim value unisonInit2Complete(value v)
    649 {
    650   id pool = [[NSAutoreleasePool alloc] init];
    651   [me performSelectorOnMainThread:@selector(afterUpdate:) withObject:[[OCamlValue alloc] initWithValue:v] waitUntilDone:FALSE];
    652   [pool release];
    653   return Val_unit;
    654 }
    655 
    656 - (void)afterSync:(id)ignore
    657 {
    658     [notificationController syncFinishedFor:[self profile]];
    659     duringSync = NO;
    660     afterSync = YES;
    661     [self updateToolbar];
    662 
    663     int i;
    664     for (i = 0; i < [reconItems count]; i++) {
    665         [[reconItems objectAtIndex:i] resetProgress];
    666     }
    667 
    668         [self updateTableViewSelection];
    669 
    670         [self updateTableViewWithReset:FALSE];
    671 }
    672 
    673 - (void)alertDidEnd:(NSAlert *)alert returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo
    674 {
    675     [_timer invalidate];
    676 
    677     switch (returnCode) {
    678         case NSAlertAlternateReturn:
    679             return;
    680             break;
    681 
    682         default:
    683             [[NSApplication sharedApplication] performSelector: @selector(terminate:) withObject: nil afterDelay: 0.0];
    684             break;
    685     }
    686 }
    687 
    688 - (void)updateCountdown
    689 {
    690     if (_secondsRemaining == 0) {
    691         [_timer invalidate];
    692         [[_timeoutAlert window] orderOut: nil];
    693         [self alertDidEnd: _timeoutAlert returnCode: NSAlertDefaultReturn contextInfo: nil];
    694     } else {
    695         [_timeoutAlert setMessageText: [NSString stringWithFormat: @"Unison will quit in %lu seconds", _secondsRemaining]];
    696         _secondsRemaining--;
    697     }
    698 }
    699 
    700 
    701 - (void)quitIfBatch:(id)ignore
    702 {
    703         if (isBatchSet) {
    704           // NSLog(@"Automatically quitting because of -batch");
    705                 _timeoutAlert = [NSAlert alertWithMessageText: @"" defaultButton: @"Quit" alternateButton: @"Cancel" otherButton: nil informativeTextWithFormat: @""];
    706 
    707                 _secondsRemaining = 10;
    708 
    709                 _timer = [NSTimer scheduledTimerWithTimeInterval: 1 target: self selector: @selector(updateCountdown) userInfo: nil repeats: YES];
    710 
    711                 [_timeoutAlert beginSheetModalForWindow: mainWindow modalDelegate: self didEndSelector: @selector(alertDidEnd:returnCode:contextInfo:) contextInfo: NULL];
    712         }
    713 }
    714 
    715 // TODO: (BCP, 3/2012) Note that the string literal "~/unison.log" here is wrong --
    716 // this is a user-settable preference (in ubase/trace.ml) and we should ask for its value.
    717 CAMLprim value syncComplete()
    718 {
    719   id pool = [[NSAutoreleasePool alloc] init];
    720   [me performSelectorOnMainThread:@selector(afterSync:) withObject:nil waitUntilDone:FALSE];
    721   if ([[NSUserDefaults standardUserDefaults] boolForKey:@"deleteLogOnExit"])
    722     [[NSFileManager defaultManager] removeItemAtPath:[@"~/unison.log" stringByExpandingTildeInPath] error:nil];
    723   [pool release];
    724 
    725   [me performSelectorOnMainThread:@selector(quitIfBatch:) withObject:nil waitUntilDone:FALSE];
    726 
    727   return Val_unit;
    728 }
    729 
    730 // A function called from ocaml
    731 - (void)reloadTable:(NSNumber *)i
    732 {
    733         // NSLog(@"*** ReloadTable: %i", [i intValue]);
    734 
    735     [[reconItems objectAtIndex:[i intValue]] resetProgress];
    736         [self updateTableViewWithReset:FALSE];
    737 }
    738 
    739 CAMLprim value reloadTable(value row)
    740 {
    741   id pool = [[NSAutoreleasePool alloc] init];
    742         // NSLog(@"OCaml says... ReloadTable: %i", Int_val(row));
    743         NSNumber *num = [[NSNumber alloc] initWithInt:Int_val(row)];
    744   [me performSelectorOnMainThread:@selector(reloadTable:) withObject:num waitUntilDone:FALSE];
    745         [num release];
    746   [pool release];
    747   return Val_unit;
    748 }
    749 
    750 - (NSUInteger)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(id)item {
    751         if (item == nil) item = rootItem;
    752         return [[item children] count];
    753 }
    754 
    755 - (BOOL)outlineView:(NSOutlineView *)outlineView isItemExpandable:(id)item {
    756     return [item isKindOfClass:[ParentReconItem class]];
    757 }
    758 
    759 - (id)outlineView:(NSOutlineView *)outlineView child:(int)index ofItem:(id)item {
    760         if (item == nil) item = rootItem;
    761         return [[item children] objectAtIndex:index];
    762 }
    763 
    764 - (id)outlineView:(NSOutlineView *)outlineView objectValueForTableColumn:(NSTableColumn *)tableColumn byItem:(id)item {
    765     NSString *identifier = [tableColumn identifier];
    766         if (item == nil) item = rootItem;
    767 
    768         if ([identifier isEqualToString:@"percentTransferred"] && (!duringSync && !afterSync)) return nil;
    769 
    770         return [item valueForKey:identifier];
    771 }
    772 
    773 static NSDictionary *_SmallGreyAttributes = nil;
    774 
    775 - (void)outlineView:(NSOutlineView *)outlineView willDisplayCell:(NSCell *)cell forTableColumn:(NSTableColumn *)tableColumn item:(id)item {
    776         NSString *identifier = [tableColumn identifier];
    777     if ([identifier isEqualToString:@"path"]) {
    778                 // The file icon
    779                 [(ImageAndTextCell*)cell setImage:[item fileIcon]];
    780 
    781                 // For parents, format the file count into the text
    782                 long fileCount = [item fileCount];
    783                 if (fileCount > 1) {
    784                         NSString *countString = [NSString stringWithFormat:@"  (%ld files)", fileCount];
    785                         NSString *fullString = [(NSString *)[cell objectValue] stringByAppendingString:countString];
    786                         NSMutableAttributedString *as = [[NSMutableAttributedString alloc] initWithString:fullString];
    787 
    788                         if (!_SmallGreyAttributes) {
    789                                 NSColor *txtColor = [NSColor grayColor];
    790                                 NSFont *txtFont = [NSFont systemFontOfSize:9.0];
    791                                 _SmallGreyAttributes = [[NSDictionary dictionaryWithObjectsAndKeys:txtFont,
    792                                         NSFontAttributeName, txtColor, NSForegroundColorAttributeName,  nil] retain];
    793                         }
    794                         [as setAttributes:_SmallGreyAttributes range:NSMakeRange([fullString length] - [countString length], [countString length])];
    795                         [cell setAttributedStringValue:as];
    796                         [as release];
    797                 }
    798     } else if ([identifier isEqualToString:@"percentTransferred"]) {
    799                 [(ProgressCell*)cell setIcon:[item direction]];
    800                 [(ProgressCell*)cell setStatusString:[item progressString]];
    801                 [(ProgressCell*)cell setIsActive:[item isKindOfClass:[LeafReconItem class]]];
    802     }
    803 }
    804 
    805 - (void)outlineView:(NSOutlineView *)outlineView
    806       sortDescriptorsDidChange:(NSArray *)oldDescriptors {
    807         NSArray *originalSelection = [outlineView selectedObjects];
    808 
    809         // do we want to catch case of object changes to allow resort in same direction for progress / direction?
    810         // Could check if our objects change and if the first item at the head of new and old were the same
    811         [rootItem sortUsingDescriptors:[outlineView sortDescriptors]];
    812         [outlineView reloadData];
    813         [outlineView setSelectedObjects:originalSelection];
    814 }
    815 
    816 // Delegate methods
    817 
    818 - (BOOL)outlineView:(NSOutlineView *)outlineView shouldEditTableColumn:(NSTableColumn *)tableColumn item:(id)item {
    819     return NO;
    820 }
    821 
    822 - (NSMutableArray *)reconItems // used in ReconTableView only
    823 {
    824     return reconItems;
    825 }
    826 
    827 - (NSInteger)tableMode
    828 {
    829         return [tableModeSelector selectedSegment];
    830 }
    831 
    832 - (IBAction)tableModeChanged:(id)sender
    833 {
    834         [[NSUserDefaults standardUserDefaults] setInteger:[self tableMode]+1 forKey:@"TableLayout"];
    835         [self updateForChangedItems];
    836 }
    837 
    838 - (void)initTableMode
    839 {
    840         long mode = [[NSUserDefaults standardUserDefaults] integerForKey:@"TableLayout"] - 1;
    841         if (mode == -1) mode = 1;
    842         [tableModeSelector setSelectedSegment:mode];
    843 }
    844 
    845 - (void)updateReconItems:(OCamlValue *)caml_reconItems
    846 {
    847     [reconItems release];
    848     reconItems = [[NSMutableArray alloc] init];
    849         long i, n =[caml_reconItems count];
    850     for (i=0; i<n; i++) {
    851                 LeafReconItem *item = [[LeafReconItem alloc] initWithRiAndIndex:(id)[caml_reconItems getField:i withType:'@'] index:i];
    852         [reconItems addObject:item];
    853                 [item release];
    854     }
    855         [self updateForChangedItems];
    856 }
    857 
    858 - (void)expandConflictedParent:(ParentReconItem *)parent
    859 {
    860         if ([parent hasConflictedChildren]) {
    861                 // NSLog(@"Expanding conflictedParent: %@", [parent fullPath]);
    862                 [tableView expandItem:parent expandChildren:NO];
    863                 NSArray *children = [parent children];
    864                 NSUInteger i = 0, count = [children count];
    865                 for (;i < count; i++) {
    866                         id child = [children objectAtIndex:i];
    867                         if ([child isKindOfClass:[ParentReconItem class]]) [self expandConflictedParent:child];
    868                 }
    869         }
    870 }
    871 
    872 - (void)updateForChangedItems
    873 {
    874         NSInteger tableMode = [self tableMode];
    875 
    876         [rootItem release];
    877         ParentReconItem *root = rootItem = [[ParentReconItem alloc] init];
    878 
    879         if (tableMode != 0 && [reconItems count]) {
    880                 // Special roll-up root item for outline displays
    881                 root = [[ParentReconItem alloc] init];
    882                 [rootItem addChild:root nested:NO];
    883                 [root setPath:@"All Changes..."];
    884                 [root setFullPath:@""];
    885                 [root release];
    886         }
    887 
    888     NSUInteger j = 0, n =[reconItems count];
    889     for (; j<n; j++) {
    890                 [root addChild:[reconItems objectAtIndex:j] nested:(tableMode != 0)];
    891     }
    892 
    893         if (tableMode == 1) [root collapseParentsWithSingleChildren:YES];
    894 
    895         [tableView reloadData];
    896 
    897         if (NO) {
    898                 // Pre-expand entire tree
    899                 int i = [[rootItem children] count];
    900                 while (i--) {
    901                         [tableView expandItem:[[rootItem children] objectAtIndex:i] expandChildren:YES];
    902                 }
    903         } else if (tableMode != 0) {
    904                 // Always open root node
    905                 [tableView expandItem:rootItem expandChildren:NO];
    906 
    907                 // then smart expand to reveal conflicts / changes in direction
    908                 [self expandConflictedParent:root];
    909 
    910                 // then open more levels if we can do so without causing scrolling
    911                 [tableView expandChildrenIfSpace];
    912         }
    913 
    914     // Make sure details get updated (or cleared)
    915         [self updateTableViewSelection];
    916 
    917     // Only enable sync if there are reconitems
    918     if ([reconItems count]>0) {
    919         [tableView setEditable:YES];
    920 
    921         // reconItems table gets keyboard input
    922         [mainWindow makeFirstResponder:tableView];
    923 
    924         syncable = YES;
    925     }
    926     else {
    927         [tableView setEditable:NO];
    928         afterSync = YES; // rescan should be enabled
    929 
    930         // reconItems table no longer gets keyboard input
    931         [mainWindow makeFirstResponder:nil];
    932     }
    933         [self updateToolbar];
    934 }
    935 
    936 - (id)updateForIgnore:(id)item
    937 {
    938     long j = (long)ocamlCall("ii", "unisonUpdateForIgnore", [reconItems indexOfObjectIdenticalTo:item]);
    939     // NSLog(@"Updating for ignore...");
    940     [self updateReconItems:(OCamlValue *)ocamlCall("@", "unisonState")];
    941     return [reconItems objectAtIndex:j];
    942 }
    943 
    944 // A function called from ocaml
    945 CAMLprim value displayStatus(value s)
    946 {
    947   id pool = [[NSAutoreleasePool alloc] init];
    948         NSString *str = [[NSString alloc] initWithUTF8String:String_val(s)];
    949     // NSLog(@"displayStatus: %@", str);
    950     [me performSelectorOnMainThread:@selector(statusTextSet:) withObject:str waitUntilDone:FALSE];
    951         [str release];
    952   [pool release];
    953   return Val_unit;
    954 }
    955 
    956 - (void)statusTextSet:(NSString *)s {
    957     /* filter out strings with # reconitems, and empty strings */
    958     if (!NSEqualRanges([s rangeOfString:@"reconitems"],
    959          NSMakeRange(NSNotFound,0))) return;
    960     [statusText setStringValue:s];
    961 }
    962 
    963 // Called from ocaml to display progress bar
    964 CAMLprim value displayGlobalProgress(value p)
    965 {
    966   id pool = [[NSAutoreleasePool alloc] init];
    967         NSNumber *num = [[NSNumber alloc] initWithDouble:Double_val(p)];
    968   [me performSelectorOnMainThread:@selector(updateProgressBar:)
    969                 withObject:num waitUntilDone:FALSE];
    970         [num release];
    971   [pool release];
    972   return Val_unit;
    973 }
    974 
    975 // Called from ocaml to display diff
    976 CAMLprim value displayDiff(value s, value s2)
    977 {
    978   id pool = [[NSAutoreleasePool alloc] init];
    979   [me performSelectorOnMainThread:@selector(diffViewTextSet:)
    980                                                 withObject:[NSArray arrayWithObjects:[NSString stringWithUTF8String:String_val(s)],
    981                                                                                         [NSString stringWithUTF8String:String_val(s2)], nil]
    982                                                 waitUntilDone:FALSE];
    983   [pool release];
    984   return Val_unit;
    985 }
    986 
    987 // Called from ocaml to display diff error messages
    988 CAMLprim value displayDiffErr(value s)
    989 {
    990   id pool = [[NSAutoreleasePool alloc] init];
    991   NSString * str = [NSString stringWithUTF8String:String_val(s)];
    992   str = [[str componentsSeparatedByString:@"\n"] componentsJoinedByString:@" "];
    993         [me->statusText performSelectorOnMainThread:@selector(setStringValue:)
    994                                 withObject:str waitUntilDone:FALSE];
    995   [pool release];
    996   return Val_unit;
    997 }
    998 
    999 - (void)diffViewTextSet:(NSArray *)args
   1000 {
   1001         [self diffViewTextSet:[args objectAtIndex:0] bodyText:[args objectAtIndex:1]];
   1002 }
   1003 
   1004 - (void)diffViewTextSet:(NSString *)title bodyText:(NSString *)body {
   1005    if ([body length]==0) return;
   1006    [diffWindow setTitle:title];
   1007    //[diffView setFont:diffFont];
   1008    [diffView setString:body];
   1009    if (!doneFirstDiff) {
   1010        /* On first open, position the diff window to the right of
   1011        the main window, but without going off the mainwindow's screen */
   1012        float screenOriginX = [[mainWindow screen] visibleFrame].origin.x;
   1013        float screenWidth = [[mainWindow screen] visibleFrame].size.width;
   1014        float mainOriginX = [mainWindow frame].origin.x;
   1015        float mainOriginY = [mainWindow frame].origin.y;
   1016        float mainWidth = [mainWindow frame].size.width;
   1017        float mainHeight = [mainWindow frame].size.height;
   1018        float diffWidth = [diffWindow frame].size.width;
   1019 
   1020        float diffX = mainOriginX+mainWidth;
   1021        float maxX = screenOriginX+screenWidth-diffWidth;
   1022        if (diffX > maxX) diffX = maxX;
   1023        float diffY = mainOriginY + mainHeight;
   1024 
   1025        NSPoint diffOrigin = NSMakePoint(diffX,diffY);
   1026        [diffWindow cascadeTopLeftFromPoint:diffOrigin];
   1027 
   1028        doneFirstDiff = YES;
   1029    }
   1030    [diffWindow orderFront:nil];
   1031 }
   1032 
   1033 - (void)displayDetails:(ReconItem *)item
   1034 {
   1035         //[detailsTextView setFont:diffFont];
   1036         NSString *text = [item details];
   1037         if (!text) text = @"";
   1038         [detailsTextView setStringValue:text];
   1039 }
   1040 
   1041 - (void)clearDetails
   1042 {
   1043     [detailsTextView setStringValue:@""];
   1044 }
   1045 
   1046 - (IBAction)raiseCltoolWindow:(id)sender
   1047 {
   1048   [cltoolPref setState:[[NSUserDefaults standardUserDefaults] boolForKey:@"CheckCltool"] ? NSOffState : NSOnState];
   1049   [self raiseWindow: cltoolWindow];
   1050 }
   1051 
   1052 - (IBAction)cltoolYesButton:(id)sender;
   1053 {
   1054   [[NSUserDefaults standardUserDefaults] setBool:([cltoolPref state] != NSOnState) forKey:@"CheckCltool"];
   1055   [self installCommandLineTool:self];
   1056   [cltoolWindow close];
   1057 }
   1058 
   1059 - (IBAction)cltoolNoButton:(id)sender;
   1060 {
   1061   [[NSUserDefaults standardUserDefaults] setBool:([cltoolPref state] != NSOnState) forKey:@"CheckCltool"];
   1062   [cltoolWindow close];
   1063 }
   1064 
   1065 - (IBAction)raiseAboutWindow:(id)sender
   1066 {
   1067     [self raiseWindow: aboutWindow];
   1068 }
   1069 
   1070 - (void)raiseWindow:(NSWindow *)theWindow
   1071 {
   1072     NSRect screenFrame = [[mainWindow screen] visibleFrame];
   1073     NSRect mainWindowFrame = [mainWindow frame];
   1074     NSRect theWindowFrame = [theWindow frame];
   1075 
   1076     float winX = mainWindowFrame.origin.x +
   1077         (mainWindowFrame.size.width - theWindowFrame.size.width)/2;
   1078     float winY = mainWindowFrame.origin.y +
   1079         (mainWindowFrame.size.height + theWindowFrame.size.height)/2;
   1080 
   1081     if (winX<screenFrame.origin.x) winX=screenFrame.origin.x;
   1082     float maxX = screenFrame.origin.x+screenFrame.size.width-
   1083         theWindowFrame.size.width;
   1084     if (winX>maxX) winX=maxX;
   1085     float minY = screenFrame.origin.y+theWindowFrame.size.height;
   1086     if (winY<minY) winY=minY;
   1087     float maxY = screenFrame.origin.y+screenFrame.size.height;
   1088     if (winY>maxY) winY=maxY;
   1089 
   1090     [theWindow cascadeTopLeftFromPoint:
   1091         NSMakePoint(winX,winY)];
   1092 
   1093     [theWindow makeKeyAndOrderFront:nil];
   1094 }
   1095 
   1096 - (IBAction)onlineHelp:(id)sender
   1097 {
   1098     [[NSWorkspace sharedWorkspace]
   1099         openURL:[NSURL URLWithString:@"http://www.cis.upenn.edu/~bcpierce/unison/docs.html"]];
   1100 }
   1101 
   1102 /* from http://developer.apple.com/documentation/Security/Conceptual/authorization_concepts/index.html */
   1103 #include <Security/Authorization.h>
   1104 #include <Security/AuthorizationTags.h>
   1105 - (IBAction)installCommandLineTool:(id)sender
   1106 {
   1107   /* Install the command-line tool in /usr/bin/unison.
   1108      Requires root privilege, so we ask for it and
   1109      pass the task off to /bin/sh. */
   1110 
   1111   OSStatus myStatus;
   1112 
   1113   AuthorizationFlags myFlags = kAuthorizationFlagDefaults;
   1114   AuthorizationRef myAuthorizationRef;
   1115   myStatus = AuthorizationCreate(NULL, kAuthorizationEmptyEnvironment,
   1116                                  myFlags, &myAuthorizationRef);
   1117   if (myStatus != errAuthorizationSuccess) return;
   1118 
   1119   {
   1120     AuthorizationItem myItems = {kAuthorizationRightExecute, 0,
   1121                                  NULL, 0};
   1122     AuthorizationRights myRights = {1, &myItems};
   1123     myFlags = kAuthorizationFlagDefaults |
   1124       kAuthorizationFlagInteractionAllowed |
   1125       kAuthorizationFlagPreAuthorize |
   1126       kAuthorizationFlagExtendRights;
   1127     myStatus =
   1128       AuthorizationCopyRights(myAuthorizationRef,&myRights,NULL,myFlags,NULL);
   1129   }
   1130   if (myStatus == errAuthorizationSuccess) {
   1131     NSBundle *bundle = [NSBundle mainBundle];
   1132     NSString *bundle_path = [bundle bundlePath];
   1133     NSString *exec_path =
   1134       [bundle_path stringByAppendingString:@"/Contents/MacOS/cltool"];
   1135     // Not sure why but this doesn't work:
   1136     // [bundle pathForResource:@"cltool" ofType:nil];
   1137 
   1138     if (exec_path == nil) return;
   1139     char *args[] = { "-f", (char *)[exec_path UTF8String],
   1140                      "/usr/local/bin/unison", NULL };
   1141 
   1142     myFlags = kAuthorizationFlagDefaults;
   1143     myStatus = AuthorizationExecuteWithPrivileges
   1144       (myAuthorizationRef, "/bin/cp", myFlags, args,
   1145        NULL);
   1146   }
   1147   AuthorizationFree (myAuthorizationRef, kAuthorizationFlagDefaults);
   1148 
   1149   /*
   1150   if (myStatus == errAuthorizationCanceled)
   1151     NSLog(@"The attempt was canceled\n");
   1152   else if (myStatus)
   1153       NSLog(@"There was an authorization error: %ld\n", myStatus);
   1154   */
   1155 }
   1156 
   1157 - (BOOL)validateItem:(SEL) action
   1158 {
   1159     if (action == @selector(syncButton:)) return syncable;
   1160     // FIXME Restarting during sync is disabled because it causes UI corruption
   1161     else if (action == @selector(restartButton:)) return !duringSync;
   1162     else if (action == @selector(rescan:)) return ((syncable && !duringSync) || afterSync);
   1163     else return YES;
   1164 }
   1165 
   1166 - (BOOL)validateMenuItem:(NSMenuItem *)menuItem
   1167 {
   1168     return [self validateItem:[menuItem action]];
   1169 }
   1170 
   1171 - (BOOL)validateToolbarItem:(NSToolbarItem *)toolbarItem
   1172 {
   1173     return [self validateItem:[toolbarItem action]];
   1174 }
   1175 
   1176 - (void)resizeWindowToSize:(NSSize)newSize
   1177 {
   1178     NSRect aFrame;
   1179 
   1180     float newHeight = newSize.height+[self toolbarHeightForWindow:mainWindow];
   1181     float newWidth = newSize.width;
   1182 
   1183     aFrame = [NSWindow contentRectForFrameRect:[mainWindow frame]
   1184                        styleMask:[mainWindow styleMask]];
   1185 
   1186     aFrame.origin.y += aFrame.size.height;
   1187     aFrame.origin.y -= newHeight;
   1188     aFrame.size.height = newHeight;
   1189     aFrame.size.width = newWidth;
   1190 
   1191     aFrame = [NSWindow frameRectForContentRect:aFrame
   1192                        styleMask:[mainWindow styleMask]];
   1193 
   1194     [mainWindow setFrame:aFrame display:YES animate:YES];
   1195 }
   1196 
   1197 - (float)toolbarHeightForWindow:(NSWindow *)window
   1198 {
   1199     NSToolbar *aToolbar;
   1200     float toolbarHeight = 0.0;
   1201     NSRect windowFrame;
   1202 
   1203     aToolbar = [window toolbar];
   1204     if(aToolbar && [aToolbar isVisible])
   1205     {
   1206         windowFrame = [NSWindow contentRectForFrameRect:[window frame]
   1207             styleMask:[window styleMask]];
   1208         toolbarHeight = NSHeight(windowFrame)
   1209             - NSHeight([[window contentView] frame]);
   1210     }
   1211     return toolbarHeight;
   1212 }
   1213 
   1214 CAMLprim value fatalError(value s)
   1215 {
   1216         NSString *str = [[NSString alloc] initWithUTF8String:String_val(s)];
   1217 
   1218         [me performSelectorOnMainThread:@selector(fatalError:) withObject:str waitUntilDone:FALSE];
   1219         [str release];
   1220     return Val_unit;
   1221 }
   1222 
   1223 - (void)fatalError:(NSString *)msg {
   1224         NSRunAlertPanel(@"Fatal error", @"%@", @"Exit", nil, nil, msg);
   1225         exit(1);
   1226 }
   1227 
   1228 /* Returns true if we need to exit, false if we proceed */
   1229 
   1230 CAMLprim value warnPanel(value s)
   1231 {
   1232         NSString *str = [[NSString alloc] initWithUTF8String:String_val(s)];
   1233 
   1234   [me performSelectorOnMainThread:@selector(warnPanel:) withObject:str waitUntilDone:TRUE];
   1235         [str release];
   1236   if (me -> shouldExitAfterWarning) {
   1237     return Val_true;
   1238   } else {
   1239     return Val_false;
   1240   }
   1241 }
   1242 
   1243 - (void)warnPanel:(NSString *)msg {
   1244   NSInteger warnVal = NSRunAlertPanel(@"Warning", @"%@", @"Proceed", @"Exit", nil, msg);
   1245   NSLog(@"Warning Panel Returned %ld",(long)warnVal);
   1246   if (warnVal == NSAlertAlternateReturn) {
   1247     shouldExitAfterWarning = YES;
   1248   } else {
   1249     shouldExitAfterWarning = FALSE;
   1250   }
   1251 }
   1252 
   1253 @end
   1254 
   1255 @implementation NSString (_UnisonUtil)
   1256 - (NSString *)trim
   1257 {
   1258         NSCharacterSet *ws = [NSCharacterSet whitespaceCharacterSet];
   1259         NSUInteger len = [self length], i = len;
   1260         while (i && [ws characterIsMember:[self characterAtIndex:i-1]]) i--;
   1261         return (i == len) ? self : [self substringToIndex:i];
   1262 }
   1263 @end