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