1 // Copyright (c) 2011 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 #import "chrome/browser/ui/cocoa/about_window_controller.h" 6 7 #include "base/logging.h" 8 #include "base/mac/mac_util.h" 9 #include "base/string_number_conversions.h" 10 #include "base/string_util.h" 11 #include "base/sys_string_conversions.h" 12 #import "chrome/browser/cocoa/keystone_glue.h" 13 #include "chrome/browser/google/google_util.h" 14 #include "chrome/browser/platform_util.h" 15 #include "chrome/browser/ui/browser_list.h" 16 #include "chrome/browser/ui/browser_window.h" 17 #import "chrome/browser/ui/cocoa/background_tile_view.h" 18 #include "chrome/browser/ui/cocoa/restart_browser.h" 19 #include "chrome/common/url_constants.h" 20 #include "grit/chromium_strings.h" 21 #include "grit/generated_resources.h" 22 #include "grit/locale_settings.h" 23 #include "grit/theme_resources.h" 24 #include "third_party/GTM/AppKit/GTMUILocalizerAndLayoutTweaker.h" 25 #include "ui/base/l10n/l10n_util.h" 26 #include "ui/base/l10n/l10n_util_mac.h" 27 #include "ui/base/resource/resource_bundle.h" 28 #include "ui/gfx/image.h" 29 30 namespace { 31 32 void AttributedStringAppendString(NSMutableAttributedString* attr_str, 33 NSString* str) { 34 // You might think doing [[attr_str mutableString] appendString:str] would 35 // work, but it causes any trailing style to get extened, meaning as we 36 // append links, they grow to include the new text, not what we want. 37 NSAttributedString* new_attr_str = 38 [[[NSAttributedString alloc] initWithString:str] autorelease]; 39 [attr_str appendAttributedString:new_attr_str]; 40 } 41 42 void AttributedStringAppendHyperlink(NSMutableAttributedString* attr_str, 43 NSString* text, NSString* url_str) { 44 // Figure out the range of the text we're adding and add the text. 45 NSRange range = NSMakeRange([attr_str length], [text length]); 46 AttributedStringAppendString(attr_str, text); 47 48 // Add the link 49 [attr_str addAttribute:NSLinkAttributeName value:url_str range:range]; 50 51 // Blue and underlined 52 [attr_str addAttribute:NSForegroundColorAttributeName 53 value:[NSColor blueColor] 54 range:range]; 55 [attr_str addAttribute:NSUnderlineStyleAttributeName 56 value:[NSNumber numberWithInt:NSSingleUnderlineStyle] 57 range:range]; 58 [attr_str addAttribute:NSCursorAttributeName 59 value:[NSCursor pointingHandCursor] 60 range:range]; 61 } 62 63 } // namespace 64 65 @interface AboutWindowController(Private) 66 67 // Launches a check for available updates. 68 - (void)checkForUpdate; 69 70 // Turns the update and promotion blocks on and off as needed based on whether 71 // updates are possible and promotion is desired or required. 72 - (void)adjustUpdateUIVisibility; 73 74 // Maintains the update and promotion block visibility and window sizing. 75 // This uses bool instead of BOOL for the convenience of the internal 76 // implementation. 77 - (void)setAllowsUpdate:(bool)update allowsPromotion:(bool)promotion; 78 79 // Notification callback, called with the status of asynchronous 80 // -checkForUpdate and -updateNow: operations. 81 - (void)updateStatus:(NSNotification*)notification; 82 83 // These methods maintain the image (or throbber) and text displayed regarding 84 // update status. -setUpdateThrobberMessage: starts a progress throbber and 85 // sets the text. -setUpdateImage:message: displays an image and sets the 86 // text. 87 - (void)setUpdateThrobberMessage:(NSString*)message; 88 - (void)setUpdateImage:(int)imageID message:(NSString*)message; 89 90 @end // @interface AboutWindowController(Private) 91 92 @implementation AboutLegalTextView 93 94 // Never draw the insertion point (otherwise, it shows up without any user 95 // action if full keyboard accessibility is enabled). 96 - (BOOL)shouldDrawInsertionPoint { 97 return NO; 98 } 99 100 @end 101 102 @implementation AboutWindowController 103 104 - (id)initWithProfile:(Profile*)profile { 105 NSString* nibPath = [base::mac::MainAppBundle() pathForResource:@"About" 106 ofType:@"nib"]; 107 if ((self = [super initWithWindowNibPath:nibPath owner:self])) { 108 profile_ = profile; 109 NSNotificationCenter* center = [NSNotificationCenter defaultCenter]; 110 [center addObserver:self 111 selector:@selector(updateStatus:) 112 name:kAutoupdateStatusNotification 113 object:nil]; 114 } 115 return self; 116 } 117 118 - (void)dealloc { 119 [[NSNotificationCenter defaultCenter] removeObserver:self]; 120 [super dealloc]; 121 } 122 123 // YES when an About box is currently showing the kAutoupdateInstallFailed 124 // status, or if no About box is visible, if the most recent About box to be 125 // closed was closed while showing this status. When an About box opens, if 126 // the recent status is kAutoupdateInstallFailed or kAutoupdatePromoteFailed 127 // and recentShownUserActionFailedStatus is NO, the failure needs to be shown 128 // instead of launching a new update check. recentShownInstallFailedStatus is 129 // maintained by -updateStatus:. 130 static BOOL recentShownUserActionFailedStatus = NO; 131 132 - (void)awakeFromNib { 133 NSBundle* bundle = base::mac::MainAppBundle(); 134 NSString* chromeVersion = 135 [bundle objectForInfoDictionaryKey:@"CFBundleShortVersionString"]; 136 137 NSString* versionModifier = @""; 138 NSString* svnRevision = @""; 139 std::string modifier = platform_util::GetVersionStringModifier(); 140 if (!modifier.empty()) 141 versionModifier = [NSString stringWithFormat:@" %@", 142 base::SysUTF8ToNSString(modifier)]; 143 144 #if !defined(GOOGLE_CHROME_BUILD) 145 svnRevision = [NSString stringWithFormat:@" (%@)", 146 [bundle objectForInfoDictionaryKey:@"SVNRevision"]]; 147 #endif 148 // The format string is not localized, but this is how the displayed version 149 // is built on Windows too. 150 NSString* version = 151 [NSString stringWithFormat:@"%@%@%@", 152 chromeVersion, svnRevision, versionModifier]; 153 154 [version_ setStringValue:version]; 155 156 // Put the two images into the UI. 157 ResourceBundle& rb = ResourceBundle::GetSharedInstance(); 158 NSImage* backgroundImage = rb.GetNativeImageNamed(IDR_ABOUT_BACKGROUND_COLOR); 159 DCHECK(backgroundImage); 160 [backgroundView_ setTileImage:backgroundImage]; 161 NSImage* logoImage = rb.GetNativeImageNamed(IDR_ABOUT_BACKGROUND); 162 DCHECK(logoImage); 163 [logoView_ setImage:logoImage]; 164 165 [[legalText_ textStorage] setAttributedString:[[self class] legalTextBlock]]; 166 167 // Resize our text view now so that the |updateShift| below is set 168 // correctly. The About box has its controls manually positioned, so we need 169 // to calculate how much larger (or smaller) our text box is and store that 170 // difference in |legalShift|. We do something similar with |updateShift| 171 // below, which is either 0, or the amount of space to offset the window size 172 // because the view that contains the update button has been removed because 173 // this build doesn't have Keystone. 174 NSRect oldLegalRect = [legalBlock_ frame]; 175 [legalText_ sizeToFit]; 176 NSRect newRect = oldLegalRect; 177 newRect.size.height = [legalText_ frame].size.height; 178 [legalBlock_ setFrame:newRect]; 179 CGFloat legalShift = newRect.size.height - oldLegalRect.size.height; 180 181 NSRect backgroundFrame = [backgroundView_ frame]; 182 backgroundFrame.origin.y += legalShift; 183 [backgroundView_ setFrame:backgroundFrame]; 184 185 NSSize windowDelta = NSMakeSize(0.0, legalShift); 186 [GTMUILocalizerAndLayoutTweaker 187 resizeWindowWithoutAutoResizingSubViews:[self window] 188 delta:windowDelta]; 189 190 windowHeight_ = [[self window] frame].size.height; 191 192 [self adjustUpdateUIVisibility]; 193 194 // Don't do anything update-related if adjustUpdateUIVisibility decided that 195 // updates aren't possible. 196 if (![updateBlock_ isHidden]) { 197 KeystoneGlue* keystoneGlue = [KeystoneGlue defaultKeystoneGlue]; 198 AutoupdateStatus recentStatus = [keystoneGlue recentStatus]; 199 if ([keystoneGlue asyncOperationPending] || 200 recentStatus == kAutoupdateRegisterFailed || 201 ((recentStatus == kAutoupdateInstallFailed || 202 recentStatus == kAutoupdatePromoteFailed) && 203 !recentShownUserActionFailedStatus)) { 204 // If an asynchronous update operation is currently pending, such as a 205 // check for updates or an update installation attempt, set the status 206 // up correspondingly without launching a new update check. 207 // 208 // If registration failed, no other operations make sense, so just go 209 // straight to the error. 210 // 211 // If a previous update or promotion attempt was unsuccessful but no 212 // About box was around to report the error, show it now, and allow 213 // another chance to perform the action. 214 [self updateStatus:[keystoneGlue recentNotification]]; 215 } else { 216 // Launch a new update check, even if one was already completed, because 217 // a new update may be available or a new update may have been installed 218 // in the background since the last time an About box was displayed. 219 [self checkForUpdate]; 220 } 221 } 222 223 [[self window] center]; 224 } 225 226 - (void)windowWillClose:(NSNotification*)notification { 227 [self autorelease]; 228 } 229 230 - (void)adjustUpdateUIVisibility { 231 bool allowUpdate; 232 bool allowPromotion; 233 234 KeystoneGlue* keystoneGlue = [KeystoneGlue defaultKeystoneGlue]; 235 if (keystoneGlue && ![keystoneGlue isOnReadOnlyFilesystem]) { 236 AutoupdateStatus recentStatus = [keystoneGlue recentStatus]; 237 if (recentStatus == kAutoupdateRegistering || 238 recentStatus == kAutoupdateRegisterFailed || 239 recentStatus == kAutoupdatePromoted) { 240 // Show the update block while registering so that there's a progress 241 // spinner, and if registration failed so that there's an error message. 242 // Show it following a promotion because updates should be possible 243 // after promotion successfully completes. 244 allowUpdate = true; 245 246 // Promotion isn't possible at this point. 247 allowPromotion = false; 248 } else if (recentStatus == kAutoupdatePromoteFailed) { 249 // TODO(mark): Add kAutoupdatePromoting to this block. KSRegistration 250 // currently handles the promotion synchronously, meaning that the main 251 // thread's loop doesn't spin, meaning that animations and other updates 252 // to the window won't occur until KSRegistration is done with 253 // promotion. This looks laggy and bad and probably qualifies as 254 // "jank." For now, there just won't be any visual feedback while 255 // promotion is in progress, but it should complete (or fail) very 256 // quickly. http://b/2290009. 257 // 258 // Also see the TODO for kAutoupdatePromoting in -updateStatus:version:. 259 // 260 // Show the update block so that there's some visual feedback that 261 // promotion is under way or that it's failed. Show the promotion block 262 // because the user either just clicked that button or because the user 263 // should be able to click it again. 264 allowUpdate = true; 265 allowPromotion = true; 266 } else { 267 // Show the update block only if a promotion is not absolutely required. 268 allowUpdate = ![keystoneGlue needsPromotion]; 269 270 // Show the promotion block if promotion is a possibility. 271 allowPromotion = [keystoneGlue wantsPromotion]; 272 } 273 } else { 274 // There is no glue, or the application is on a read-only filesystem. 275 // Updates and promotions are impossible. 276 allowUpdate = false; 277 allowPromotion = false; 278 } 279 280 [self setAllowsUpdate:allowUpdate allowsPromotion:allowPromotion]; 281 } 282 283 - (void)setAllowsUpdate:(bool)update allowsPromotion:(bool)promotion { 284 bool oldUpdate = ![updateBlock_ isHidden]; 285 bool oldPromotion = ![promoteButton_ isHidden]; 286 287 if (promotion == oldPromotion && update == oldUpdate) { 288 return; 289 } 290 291 NSRect updateFrame = [updateBlock_ frame]; 292 CGFloat delta = 0.0; 293 294 if (update != oldUpdate) { 295 [updateBlock_ setHidden:!update]; 296 delta += (update ? 1.0 : -1.0) * NSHeight(updateFrame); 297 } 298 299 if (promotion != oldPromotion) { 300 [promoteButton_ setHidden:!promotion]; 301 } 302 303 NSRect legalFrame = [legalBlock_ frame]; 304 305 if (delta) { 306 updateFrame.origin.y += delta; 307 [updateBlock_ setFrame:updateFrame]; 308 309 legalFrame.origin.y += delta; 310 [legalBlock_ setFrame:legalFrame]; 311 312 NSRect backgroundFrame = [backgroundView_ frame]; 313 backgroundFrame.origin.y += delta; 314 [backgroundView_ setFrame:backgroundFrame]; 315 316 // GTMUILocalizerAndLayoutTweaker resizes the window without any 317 // opportunity for animation. In order to animate, disable window 318 // updates, save the current frame, let GTMUILocalizerAndLayoutTweaker do 319 // its thing, save the desired frame, restore the original frame, and then 320 // animate. 321 NSWindow* window = [self window]; 322 [window disableScreenUpdatesUntilFlush]; 323 324 NSRect oldFrame = [window frame]; 325 326 // GTMUILocalizerAndLayoutTweaker applies its delta to the window's 327 // current size (like oldFrame.size), but oldFrame isn't trustworthy if 328 // an animation is in progress. Set the window's frame to 329 // intermediateFrame, which is a frame of the size that an existing 330 // animation is animating to, so that GTM can apply the delta to the right 331 // size. 332 NSRect intermediateFrame = oldFrame; 333 intermediateFrame.origin.y -= intermediateFrame.size.height - windowHeight_; 334 intermediateFrame.size.height = windowHeight_; 335 [window setFrame:intermediateFrame display:NO]; 336 337 NSSize windowDelta = NSMakeSize(0.0, delta); 338 [GTMUILocalizerAndLayoutTweaker 339 resizeWindowWithoutAutoResizingSubViews:window 340 delta:windowDelta]; 341 [window setFrameTopLeftPoint:NSMakePoint(NSMinX(intermediateFrame), 342 NSMaxY(intermediateFrame))]; 343 NSRect newFrame = [window frame]; 344 345 windowHeight_ += delta; 346 347 if (![[self window] isVisible]) { 348 // Don't animate if the window isn't on screen yet. 349 [window setFrame:newFrame display:NO]; 350 } else { 351 [window setFrame:oldFrame display:NO]; 352 [window setFrame:newFrame display:YES animate:YES]; 353 } 354 } 355 } 356 357 - (void)setUpdateThrobberMessage:(NSString*)message { 358 [updateStatusIndicator_ setHidden:YES]; 359 360 [spinner_ setHidden:NO]; 361 [spinner_ startAnimation:self]; 362 363 [updateText_ setStringValue:message]; 364 } 365 366 - (void)setUpdateImage:(int)imageID message:(NSString*)message { 367 [spinner_ stopAnimation:self]; 368 [spinner_ setHidden:YES]; 369 370 ResourceBundle& rb = ResourceBundle::GetSharedInstance(); 371 NSImage* statusImage = rb.GetNativeImageNamed(imageID); 372 DCHECK(statusImage); 373 [updateStatusIndicator_ setImage:statusImage]; 374 [updateStatusIndicator_ setHidden:NO]; 375 376 [updateText_ setStringValue:message]; 377 } 378 379 - (void)checkForUpdate { 380 [[KeystoneGlue defaultKeystoneGlue] checkForUpdate]; 381 382 // Immediately, kAutoupdateStatusNotification will be posted, and 383 // -updateStatus: will be called with status kAutoupdateChecking. 384 // 385 // Upon completion, kAutoupdateStatusNotification will be posted, and 386 // -updateStatus: will be called with a status indicating the result of the 387 // check. 388 } 389 390 - (IBAction)updateNow:(id)sender { 391 [[KeystoneGlue defaultKeystoneGlue] installUpdate]; 392 393 // Immediately, kAutoupdateStatusNotification will be posted, and 394 // -updateStatus: will be called with status kAutoupdateInstalling. 395 // 396 // Upon completion, kAutoupdateStatusNotification will be posted, and 397 // -updateStatus: will be called with a status indicating the result of the 398 // installation attempt. 399 } 400 401 - (IBAction)promoteUpdater:(id)sender { 402 [[KeystoneGlue defaultKeystoneGlue] promoteTicket]; 403 404 // Immediately, kAutoupdateStatusNotification will be posted, and 405 // -updateStatus: will be called with status kAutoupdatePromoting. 406 // 407 // Upon completion, kAutoupdateStatusNotification will be posted, and 408 // -updateStatus: will be called with a status indicating a result of the 409 // installation attempt. 410 // 411 // If the promotion was successful, KeystoneGlue will re-register the ticket 412 // and -updateStatus: will be called again indicating first that 413 // registration is in progress and subsequently that it has completed. 414 } 415 416 - (void)updateStatus:(NSNotification*)notification { 417 recentShownUserActionFailedStatus = NO; 418 419 NSDictionary* dictionary = [notification userInfo]; 420 AutoupdateStatus status = static_cast<AutoupdateStatus>( 421 [[dictionary objectForKey:kAutoupdateStatusStatus] intValue]); 422 423 // Don't assume |version| is a real string. It may be nil. 424 NSString* version = [dictionary objectForKey:kAutoupdateStatusVersion]; 425 426 bool updateMessage = true; 427 bool throbber = false; 428 int imageID = 0; 429 NSString* message; 430 bool enableUpdateButton = false; 431 bool enablePromoteButton = true; 432 433 switch (status) { 434 case kAutoupdateRegistering: 435 // When registering, use the "checking" message. The check will be 436 // launched if appropriate immediately after registration. 437 throbber = true; 438 message = l10n_util::GetNSStringWithFixup(IDS_UPGRADE_CHECK_STARTED); 439 enablePromoteButton = false; 440 441 break; 442 443 case kAutoupdateRegistered: 444 // Once registered, the ability to update and promote is known. 445 [self adjustUpdateUIVisibility]; 446 447 if (![updateBlock_ isHidden]) { 448 // If registration completes while the window is visible, go straight 449 // into an update check. Return immediately, this routine will be 450 // re-entered shortly with kAutoupdateChecking. 451 [self checkForUpdate]; 452 return; 453 } 454 455 // Nothing actually failed, but updates aren't possible. The throbber 456 // and message are hidden, but they'll be reset to these dummy values 457 // just to get the throbber to stop spinning if it's running. 458 imageID = IDR_UPDATE_FAIL; 459 message = l10n_util::GetNSStringFWithFixup(IDS_UPGRADE_ERROR, 460 base::IntToString16(status)); 461 462 break; 463 464 case kAutoupdateChecking: 465 throbber = true; 466 message = l10n_util::GetNSStringWithFixup(IDS_UPGRADE_CHECK_STARTED); 467 enablePromoteButton = false; 468 469 break; 470 471 case kAutoupdateCurrent: 472 imageID = IDR_UPDATE_UPTODATE; 473 message = l10n_util::GetNSStringFWithFixup( 474 IDS_UPGRADE_ALREADY_UP_TO_DATE, 475 l10n_util::GetStringUTF16(IDS_PRODUCT_NAME), 476 base::SysNSStringToUTF16(version)); 477 478 break; 479 480 case kAutoupdateAvailable: 481 imageID = IDR_UPDATE_AVAILABLE; 482 message = l10n_util::GetNSStringFWithFixup( 483 IDS_UPGRADE_AVAILABLE, l10n_util::GetStringUTF16(IDS_PRODUCT_NAME)); 484 enableUpdateButton = true; 485 486 break; 487 488 case kAutoupdateInstalling: 489 throbber = true; 490 message = l10n_util::GetNSStringWithFixup(IDS_UPGRADE_STARTED); 491 enablePromoteButton = false; 492 493 break; 494 495 case kAutoupdateInstalled: 496 { 497 imageID = IDR_UPDATE_UPTODATE; 498 string16 productName = l10n_util::GetStringUTF16(IDS_PRODUCT_NAME); 499 if (version) { 500 message = l10n_util::GetNSStringFWithFixup( 501 IDS_UPGRADE_SUCCESSFUL, 502 productName, 503 base::SysNSStringToUTF16(version)); 504 } else { 505 message = l10n_util::GetNSStringFWithFixup( 506 IDS_UPGRADE_SUCCESSFUL_NOVERSION, productName); 507 } 508 509 // TODO(mark): Turn the button in the dialog into a restart button 510 // instead of springing this sheet or dialog. 511 NSWindow* window = [self window]; 512 NSWindow* restartDialogParent = [window isVisible] ? window : nil; 513 restart_browser::RequestRestart(restartDialogParent); 514 } 515 516 break; 517 518 case kAutoupdatePromoting: 519 #if 1 520 // TODO(mark): See the TODO in -adjustUpdateUIVisibility for an 521 // explanation of why nothing can be done here at the moment. When 522 // KSRegistration handles promotion asynchronously, this dummy block can 523 // be replaced with the #else block. For now, just leave the messaging 524 // alone. http://b/2290009. 525 updateMessage = false; 526 #else 527 // The visibility may be changing. 528 [self adjustUpdateUIVisibility]; 529 530 // This is not a terminal state, and kAutoupdatePromoted or 531 // kAutoupdatePromoteFailed will follow. Use the throbber and 532 // "checking" message so that it looks like something's happening. 533 throbber = true; 534 message = l10n_util::GetNSStringWithFixup(IDS_UPGRADE_CHECK_STARTED); 535 #endif 536 537 enablePromoteButton = false; 538 539 break; 540 541 case kAutoupdatePromoted: 542 // The visibility may be changing. 543 [self adjustUpdateUIVisibility]; 544 545 if (![updateBlock_ isHidden]) { 546 // If promotion completes while the window is visible, go straight 547 // into an update check. Return immediately, this routine will be 548 // re-entered shortly with kAutoupdateChecking. 549 [self checkForUpdate]; 550 return; 551 } 552 553 // Nothing actually failed, but updates aren't possible. The throbber 554 // and message are hidden, but they'll be reset to these dummy values 555 // just to get the throbber to stop spinning if it's running. 556 imageID = IDR_UPDATE_FAIL; 557 message = l10n_util::GetNSStringFWithFixup(IDS_UPGRADE_ERROR, 558 base::IntToString16(status)); 559 560 break; 561 562 case kAutoupdateRegisterFailed: 563 imageID = IDR_UPDATE_FAIL; 564 message = l10n_util::GetNSStringFWithFixup(IDS_UPGRADE_ERROR, 565 base::IntToString16(status)); 566 enablePromoteButton = false; 567 568 break; 569 570 case kAutoupdateCheckFailed: 571 imageID = IDR_UPDATE_FAIL; 572 message = l10n_util::GetNSStringFWithFixup(IDS_UPGRADE_ERROR, 573 base::IntToString16(status)); 574 575 break; 576 577 case kAutoupdateInstallFailed: 578 recentShownUserActionFailedStatus = YES; 579 580 imageID = IDR_UPDATE_FAIL; 581 message = l10n_util::GetNSStringFWithFixup(IDS_UPGRADE_ERROR, 582 base::IntToString16(status)); 583 584 // Allow another chance. 585 enableUpdateButton = true; 586 587 break; 588 589 case kAutoupdatePromoteFailed: 590 recentShownUserActionFailedStatus = YES; 591 592 imageID = IDR_UPDATE_FAIL; 593 message = l10n_util::GetNSStringFWithFixup(IDS_UPGRADE_ERROR, 594 base::IntToString16(status)); 595 596 break; 597 598 default: 599 NOTREACHED(); 600 601 return; 602 } 603 604 if (updateMessage) { 605 if (throbber) { 606 [self setUpdateThrobberMessage:message]; 607 } else { 608 DCHECK_NE(imageID, 0); 609 [self setUpdateImage:imageID message:message]; 610 } 611 } 612 613 // Note that these buttons may be hidden depending on what 614 // -adjustUpdateUIVisibility did. Their enabled/disabled status doesn't 615 // necessarily have anything to do with their visibility. 616 [updateNowButton_ setEnabled:enableUpdateButton]; 617 [promoteButton_ setEnabled:enablePromoteButton]; 618 } 619 620 - (BOOL)textView:(NSTextView *)aTextView 621 clickedOnLink:(id)link 622 atIndex:(NSUInteger)charIndex { 623 // We always create a new window, so there's no need to try to re-use 624 // an existing one just to pass in the NEW_WINDOW disposition. 625 Browser* browser = Browser::Create(profile_); 626 browser->OpenURL(GURL([link UTF8String]), GURL(), NEW_FOREGROUND_TAB, 627 PageTransition::LINK); 628 browser->window()->Show(); 629 return YES; 630 } 631 632 - (NSTextView*)legalText { 633 return legalText_; 634 } 635 636 - (NSButton*)updateButton { 637 return updateNowButton_; 638 } 639 640 - (NSTextField*)updateText { 641 return updateText_; 642 } 643 644 + (NSAttributedString*)legalTextBlock { 645 // Windows builds this up in a very complex way, we're just trying to model 646 // it the best we can to get all the information in (they actually do it 647 // but created Labels and Links that they carefully place to make it appear 648 // to be a paragraph of text). 649 // src/chrome/browser/ui/views/about_chrome_view.cc AboutChromeView::Init() 650 651 NSMutableAttributedString* legal_block = 652 [[[NSMutableAttributedString alloc] init] autorelease]; 653 [legal_block beginEditing]; 654 655 NSString* copyright = 656 l10n_util::GetNSStringWithFixup(IDS_ABOUT_VERSION_COPYRIGHT); 657 AttributedStringAppendString(legal_block, copyright); 658 659 // These are the markers directly in IDS_ABOUT_VERSION_LICENSE 660 NSString* kBeginLinkChr = @"BEGIN_LINK_CHR"; 661 NSString* kBeginLinkOss = @"BEGIN_LINK_OSS"; 662 NSString* kEndLinkChr = @"END_LINK_CHR"; 663 NSString* kEndLinkOss = @"END_LINK_OSS"; 664 // The CHR link should go to here 665 GURL url = google_util::AppendGoogleLocaleParam( 666 GURL(chrome::kChromiumProjectURL)); 667 NSString* kChromiumProject = base::SysUTF8ToNSString(url.spec()); 668 // The OSS link should go to here 669 NSString* kAcknowledgements = 670 [NSString stringWithUTF8String:chrome::kAboutCreditsURL]; 671 672 // Now fetch the license string and deal with the markers 673 674 NSString* license = 675 l10n_util::GetNSStringWithFixup(IDS_ABOUT_VERSION_LICENSE); 676 677 NSRange begin_chr = [license rangeOfString:kBeginLinkChr]; 678 NSRange begin_oss = [license rangeOfString:kBeginLinkOss]; 679 NSRange end_chr = [license rangeOfString:kEndLinkChr]; 680 NSRange end_oss = [license rangeOfString:kEndLinkOss]; 681 DCHECK_NE(begin_chr.location, NSNotFound); 682 DCHECK_NE(begin_oss.location, NSNotFound); 683 DCHECK_NE(end_chr.location, NSNotFound); 684 DCHECK_NE(end_oss.location, NSNotFound); 685 686 // We don't know which link will come first, so we have to deal with things 687 // like this: 688 // [text][begin][text][end][text][start][text][end][text] 689 690 bool chromium_link_first = begin_chr.location < begin_oss.location; 691 692 NSRange* begin1 = &begin_chr; 693 NSRange* begin2 = &begin_oss; 694 NSRange* end1 = &end_chr; 695 NSRange* end2 = &end_oss; 696 NSString* link1 = kChromiumProject; 697 NSString* link2 = kAcknowledgements; 698 if (!chromium_link_first) { 699 // OSS came first, switch! 700 begin2 = &begin_chr; 701 begin1 = &begin_oss; 702 end2 = &end_chr; 703 end1 = &end_oss; 704 link2 = kChromiumProject; 705 link1 = kAcknowledgements; 706 } 707 708 NSString *sub_str; 709 710 AttributedStringAppendString(legal_block, @"\n"); 711 sub_str = [license substringWithRange:NSMakeRange(0, begin1->location)]; 712 AttributedStringAppendString(legal_block, sub_str); 713 sub_str = [license substringWithRange:NSMakeRange(NSMaxRange(*begin1), 714 end1->location - 715 NSMaxRange(*begin1))]; 716 AttributedStringAppendHyperlink(legal_block, sub_str, link1); 717 sub_str = [license substringWithRange:NSMakeRange(NSMaxRange(*end1), 718 begin2->location - 719 NSMaxRange(*end1))]; 720 AttributedStringAppendString(legal_block, sub_str); 721 sub_str = [license substringWithRange:NSMakeRange(NSMaxRange(*begin2), 722 end2->location - 723 NSMaxRange(*begin2))]; 724 AttributedStringAppendHyperlink(legal_block, sub_str, link2); 725 sub_str = [license substringWithRange:NSMakeRange(NSMaxRange(*end2), 726 [license length] - 727 NSMaxRange(*end2))]; 728 AttributedStringAppendString(legal_block, sub_str); 729 730 #if defined(GOOGLE_CHROME_BUILD) 731 // Terms of service is only valid for Google Chrome 732 733 // The url within terms should point here: 734 NSString* kTOS = [NSString stringWithUTF8String:chrome::kAboutTermsURL]; 735 // Following Windows. There is one marker in the string for where the terms 736 // link goes, but the text of the link comes from a second string resources. 737 std::vector<size_t> url_offsets; 738 NSString* about_terms = l10n_util::GetNSStringF(IDS_ABOUT_TERMS_OF_SERVICE, 739 string16(), 740 string16(), 741 &url_offsets); 742 DCHECK_EQ(url_offsets.size(), 1U); 743 NSString* terms_link_text = 744 l10n_util::GetNSStringWithFixup(IDS_TERMS_OF_SERVICE); 745 746 AttributedStringAppendString(legal_block, @"\n\n"); 747 sub_str = [about_terms substringToIndex:url_offsets[0]]; 748 AttributedStringAppendString(legal_block, sub_str); 749 AttributedStringAppendHyperlink(legal_block, terms_link_text, kTOS); 750 sub_str = [about_terms substringFromIndex:url_offsets[0]]; 751 AttributedStringAppendString(legal_block, sub_str); 752 #endif // GOOGLE_CHROME_BUILD 753 754 // We need to explicitly select Lucida Grande because once we click on 755 // the NSTextView, it changes to Helvetica 12 otherwise. 756 NSRange string_range = NSMakeRange(0, [legal_block length]); 757 [legal_block addAttribute:NSFontAttributeName 758 value:[NSFont labelFontOfSize:11] 759 range:string_range]; 760 761 [legal_block endEditing]; 762 return legal_block; 763 } 764 765 @end // @implementation AboutWindowController 766