diff options
Diffstat (limited to 'chrome/browser/ui/cocoa/about_window_controller.mm')
-rw-r--r-- | chrome/browser/ui/cocoa/about_window_controller.mm | 761 |
1 files changed, 761 insertions, 0 deletions
diff --git a/chrome/browser/ui/cocoa/about_window_controller.mm b/chrome/browser/ui/cocoa/about_window_controller.mm new file mode 100644 index 0000000..f6da6ab --- /dev/null +++ b/chrome/browser/ui/cocoa/about_window_controller.mm @@ -0,0 +1,761 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "chrome/browser/ui/cocoa/about_window_controller.h" + +#include "app/l10n_util.h" +#include "app/l10n_util_mac.h" +#include "app/resource_bundle.h" +#include "base/logging.h" +#include "base/mac_util.h" +#include "base/string_number_conversions.h" +#include "base/string_util.h" +#include "base/sys_string_conversions.h" +#include "chrome/browser/browser_list.h" +#include "chrome/browser/browser_window.h" +#include "chrome/browser/platform_util.h" +#import "chrome/browser/ui/cocoa/background_tile_view.h" +#import "chrome/browser/ui/cocoa/keystone_glue.h" +#include "chrome/browser/ui/cocoa/restart_browser.h" +#include "chrome/common/url_constants.h" +#include "grit/chromium_strings.h" +#include "grit/generated_resources.h" +#include "grit/theme_resources.h" +#include "grit/locale_settings.h" +#include "third_party/GTM/AppKit/GTMUILocalizerAndLayoutTweaker.h" + +namespace { + +void AttributedStringAppendString(NSMutableAttributedString* attr_str, + NSString* str) { + // You might think doing [[attr_str mutableString] appendString:str] would + // work, but it causes any trailing style to get extened, meaning as we + // append links, they grow to include the new text, not what we want. + NSAttributedString* new_attr_str = + [[[NSAttributedString alloc] initWithString:str] autorelease]; + [attr_str appendAttributedString:new_attr_str]; +} + +void AttributedStringAppendHyperlink(NSMutableAttributedString* attr_str, + NSString* text, NSString* url_str) { + // Figure out the range of the text we're adding and add the text. + NSRange range = NSMakeRange([attr_str length], [text length]); + AttributedStringAppendString(attr_str, text); + + // Add the link + [attr_str addAttribute:NSLinkAttributeName value:url_str range:range]; + + // Blue and underlined + [attr_str addAttribute:NSForegroundColorAttributeName + value:[NSColor blueColor] + range:range]; + [attr_str addAttribute:NSUnderlineStyleAttributeName + value:[NSNumber numberWithInt:NSSingleUnderlineStyle] + range:range]; + [attr_str addAttribute:NSCursorAttributeName + value:[NSCursor pointingHandCursor] + range:range]; +} + +} // namespace + +@interface AboutWindowController(Private) + +// Launches a check for available updates. +- (void)checkForUpdate; + +// Turns the update and promotion blocks on and off as needed based on whether +// updates are possible and promotion is desired or required. +- (void)adjustUpdateUIVisibility; + +// Maintains the update and promotion block visibility and window sizing. +// This uses bool instead of BOOL for the convenience of the internal +// implementation. +- (void)setAllowsUpdate:(bool)update allowsPromotion:(bool)promotion; + +// Notification callback, called with the status of asynchronous +// -checkForUpdate and -updateNow: operations. +- (void)updateStatus:(NSNotification*)notification; + +// These methods maintain the image (or throbber) and text displayed regarding +// update status. -setUpdateThrobberMessage: starts a progress throbber and +// sets the text. -setUpdateImage:message: displays an image and sets the +// text. +- (void)setUpdateThrobberMessage:(NSString*)message; +- (void)setUpdateImage:(int)imageID message:(NSString*)message; + +@end // @interface AboutWindowController(Private) + +@implementation AboutLegalTextView + +// Never draw the insertion point (otherwise, it shows up without any user +// action if full keyboard accessibility is enabled). +- (BOOL)shouldDrawInsertionPoint { + return NO; +} + +@end + +@implementation AboutWindowController + +- (id)initWithProfile:(Profile*)profile { + NSString* nibPath = [mac_util::MainAppBundle() pathForResource:@"About" + ofType:@"nib"]; + if ((self = [super initWithWindowNibPath:nibPath owner:self])) { + profile_ = profile; + NSNotificationCenter* center = [NSNotificationCenter defaultCenter]; + [center addObserver:self + selector:@selector(updateStatus:) + name:kAutoupdateStatusNotification + object:nil]; + } + return self; +} + +- (void)dealloc { + [[NSNotificationCenter defaultCenter] removeObserver:self]; + [super dealloc]; +} + +// YES when an About box is currently showing the kAutoupdateInstallFailed +// status, or if no About box is visible, if the most recent About box to be +// closed was closed while showing this status. When an About box opens, if +// the recent status is kAutoupdateInstallFailed or kAutoupdatePromoteFailed +// and recentShownUserActionFailedStatus is NO, the failure needs to be shown +// instead of launching a new update check. recentShownInstallFailedStatus is +// maintained by -updateStatus:. +static BOOL recentShownUserActionFailedStatus = NO; + +- (void)awakeFromNib { + NSBundle* bundle = mac_util::MainAppBundle(); + NSString* chromeVersion = + [bundle objectForInfoDictionaryKey:@"CFBundleShortVersionString"]; + + NSString* versionModifier = @""; + NSString* svnRevision = @""; + std::string modifier = platform_util::GetVersionStringModifier(); + if (!modifier.empty()) + versionModifier = [NSString stringWithFormat:@" %@", + base::SysUTF8ToNSString(modifier)]; + +#if !defined(GOOGLE_CHROME_BUILD) + svnRevision = [NSString stringWithFormat:@" (%@)", + [bundle objectForInfoDictionaryKey:@"SVNRevision"]]; +#endif + // The format string is not localized, but this is how the displayed version + // is built on Windows too. + NSString* version = + [NSString stringWithFormat:@"%@%@%@", + chromeVersion, svnRevision, versionModifier]; + + [version_ setStringValue:version]; + + // Put the two images into the UI. + ResourceBundle& rb = ResourceBundle::GetSharedInstance(); + NSImage* backgroundImage = rb.GetNativeImageNamed(IDR_ABOUT_BACKGROUND_COLOR); + DCHECK(backgroundImage); + [backgroundView_ setTileImage:backgroundImage]; + NSImage* logoImage = rb.GetNativeImageNamed(IDR_ABOUT_BACKGROUND); + DCHECK(logoImage); + [logoView_ setImage:logoImage]; + + [[legalText_ textStorage] setAttributedString:[[self class] legalTextBlock]]; + + // Resize our text view now so that the |updateShift| below is set + // correctly. The About box has its controls manually positioned, so we need + // to calculate how much larger (or smaller) our text box is and store that + // difference in |legalShift|. We do something similar with |updateShift| + // below, which is either 0, or the amount of space to offset the window size + // because the view that contains the update button has been removed because + // this build doesn't have Keystone. + NSRect oldLegalRect = [legalBlock_ frame]; + [legalText_ sizeToFit]; + NSRect newRect = oldLegalRect; + newRect.size.height = [legalText_ frame].size.height; + [legalBlock_ setFrame:newRect]; + CGFloat legalShift = newRect.size.height - oldLegalRect.size.height; + + NSRect backgroundFrame = [backgroundView_ frame]; + backgroundFrame.origin.y += legalShift; + [backgroundView_ setFrame:backgroundFrame]; + + NSSize windowDelta = NSMakeSize(0.0, legalShift); + [GTMUILocalizerAndLayoutTweaker + resizeWindowWithoutAutoResizingSubViews:[self window] + delta:windowDelta]; + + windowHeight_ = [[self window] frame].size.height; + + [self adjustUpdateUIVisibility]; + + // Don't do anything update-related if adjustUpdateUIVisibility decided that + // updates aren't possible. + if (![updateBlock_ isHidden]) { + KeystoneGlue* keystoneGlue = [KeystoneGlue defaultKeystoneGlue]; + AutoupdateStatus recentStatus = [keystoneGlue recentStatus]; + if ([keystoneGlue asyncOperationPending] || + recentStatus == kAutoupdateRegisterFailed || + ((recentStatus == kAutoupdateInstallFailed || + recentStatus == kAutoupdatePromoteFailed) && + !recentShownUserActionFailedStatus)) { + // If an asynchronous update operation is currently pending, such as a + // check for updates or an update installation attempt, set the status + // up correspondingly without launching a new update check. + // + // If registration failed, no other operations make sense, so just go + // straight to the error. + // + // If a previous update or promotion attempt was unsuccessful but no + // About box was around to report the error, show it now, and allow + // another chance to perform the action. + [self updateStatus:[keystoneGlue recentNotification]]; + } else { + // Launch a new update check, even if one was already completed, because + // a new update may be available or a new update may have been installed + // in the background since the last time an About box was displayed. + [self checkForUpdate]; + } + } + + [[self window] center]; +} + +- (void)windowWillClose:(NSNotification*)notification { + [self autorelease]; +} + +- (void)adjustUpdateUIVisibility { + bool allowUpdate; + bool allowPromotion; + + KeystoneGlue* keystoneGlue = [KeystoneGlue defaultKeystoneGlue]; + if (keystoneGlue && ![keystoneGlue isOnReadOnlyFilesystem]) { + AutoupdateStatus recentStatus = [keystoneGlue recentStatus]; + if (recentStatus == kAutoupdateRegistering || + recentStatus == kAutoupdateRegisterFailed || + recentStatus == kAutoupdatePromoted) { + // Show the update block while registering so that there's a progress + // spinner, and if registration failed so that there's an error message. + // Show it following a promotion because updates should be possible + // after promotion successfully completes. + allowUpdate = true; + + // Promotion isn't possible at this point. + allowPromotion = false; + } else if (recentStatus == kAutoupdatePromoteFailed) { + // TODO(mark): Add kAutoupdatePromoting to this block. KSRegistration + // currently handles the promotion synchronously, meaning that the main + // thread's loop doesn't spin, meaning that animations and other updates + // to the window won't occur until KSRegistration is done with + // promotion. This looks laggy and bad and probably qualifies as + // "jank." For now, there just won't be any visual feedback while + // promotion is in progress, but it should complete (or fail) very + // quickly. http://b/2290009. + // + // Also see the TODO for kAutoupdatePromoting in -updateStatus:version:. + // + // Show the update block so that there's some visual feedback that + // promotion is under way or that it's failed. Show the promotion block + // because the user either just clicked that button or because the user + // should be able to click it again. + allowUpdate = true; + allowPromotion = true; + } else { + // Show the update block only if a promotion is not absolutely required. + allowUpdate = ![keystoneGlue needsPromotion]; + + // Show the promotion block if promotion is a possibility. + allowPromotion = [keystoneGlue wantsPromotion]; + } + } else { + // There is no glue, or the application is on a read-only filesystem. + // Updates and promotions are impossible. + allowUpdate = false; + allowPromotion = false; + } + + [self setAllowsUpdate:allowUpdate allowsPromotion:allowPromotion]; +} + +- (void)setAllowsUpdate:(bool)update allowsPromotion:(bool)promotion { + bool oldUpdate = ![updateBlock_ isHidden]; + bool oldPromotion = ![promoteButton_ isHidden]; + + if (promotion == oldPromotion && update == oldUpdate) { + return; + } + + NSRect updateFrame = [updateBlock_ frame]; + CGFloat delta = 0.0; + + if (update != oldUpdate) { + [updateBlock_ setHidden:!update]; + delta += (update ? 1.0 : -1.0) * NSHeight(updateFrame); + } + + if (promotion != oldPromotion) { + [promoteButton_ setHidden:!promotion]; + } + + NSRect legalFrame = [legalBlock_ frame]; + + if (delta) { + updateFrame.origin.y += delta; + [updateBlock_ setFrame:updateFrame]; + + legalFrame.origin.y += delta; + [legalBlock_ setFrame:legalFrame]; + + NSRect backgroundFrame = [backgroundView_ frame]; + backgroundFrame.origin.y += delta; + [backgroundView_ setFrame:backgroundFrame]; + + // GTMUILocalizerAndLayoutTweaker resizes the window without any + // opportunity for animation. In order to animate, disable window + // updates, save the current frame, let GTMUILocalizerAndLayoutTweaker do + // its thing, save the desired frame, restore the original frame, and then + // animate. + NSWindow* window = [self window]; + [window disableScreenUpdatesUntilFlush]; + + NSRect oldFrame = [window frame]; + + // GTMUILocalizerAndLayoutTweaker applies its delta to the window's + // current size (like oldFrame.size), but oldFrame isn't trustworthy if + // an animation is in progress. Set the window's frame to + // intermediateFrame, which is a frame of the size that an existing + // animation is animating to, so that GTM can apply the delta to the right + // size. + NSRect intermediateFrame = oldFrame; + intermediateFrame.origin.y -= intermediateFrame.size.height - windowHeight_; + intermediateFrame.size.height = windowHeight_; + [window setFrame:intermediateFrame display:NO]; + + NSSize windowDelta = NSMakeSize(0.0, delta); + [GTMUILocalizerAndLayoutTweaker + resizeWindowWithoutAutoResizingSubViews:window + delta:windowDelta]; + [window setFrameTopLeftPoint:NSMakePoint(NSMinX(intermediateFrame), + NSMaxY(intermediateFrame))]; + NSRect newFrame = [window frame]; + + windowHeight_ += delta; + + if (![[self window] isVisible]) { + // Don't animate if the window isn't on screen yet. + [window setFrame:newFrame display:NO]; + } else { + [window setFrame:oldFrame display:NO]; + [window setFrame:newFrame display:YES animate:YES]; + } + } +} + +- (void)setUpdateThrobberMessage:(NSString*)message { + [updateStatusIndicator_ setHidden:YES]; + + [spinner_ setHidden:NO]; + [spinner_ startAnimation:self]; + + [updateText_ setStringValue:message]; +} + +- (void)setUpdateImage:(int)imageID message:(NSString*)message { + [spinner_ stopAnimation:self]; + [spinner_ setHidden:YES]; + + ResourceBundle& rb = ResourceBundle::GetSharedInstance(); + NSImage* statusImage = rb.GetNativeImageNamed(imageID); + DCHECK(statusImage); + [updateStatusIndicator_ setImage:statusImage]; + [updateStatusIndicator_ setHidden:NO]; + + [updateText_ setStringValue:message]; +} + +- (void)checkForUpdate { + [[KeystoneGlue defaultKeystoneGlue] checkForUpdate]; + + // Immediately, kAutoupdateStatusNotification will be posted, and + // -updateStatus: will be called with status kAutoupdateChecking. + // + // Upon completion, kAutoupdateStatusNotification will be posted, and + // -updateStatus: will be called with a status indicating the result of the + // check. +} + +- (IBAction)updateNow:(id)sender { + [[KeystoneGlue defaultKeystoneGlue] installUpdate]; + + // Immediately, kAutoupdateStatusNotification will be posted, and + // -updateStatus: will be called with status kAutoupdateInstalling. + // + // Upon completion, kAutoupdateStatusNotification will be posted, and + // -updateStatus: will be called with a status indicating the result of the + // installation attempt. +} + +- (IBAction)promoteUpdater:(id)sender { + [[KeystoneGlue defaultKeystoneGlue] promoteTicket]; + + // Immediately, kAutoupdateStatusNotification will be posted, and + // -updateStatus: will be called with status kAutoupdatePromoting. + // + // Upon completion, kAutoupdateStatusNotification will be posted, and + // -updateStatus: will be called with a status indicating a result of the + // installation attempt. + // + // If the promotion was successful, KeystoneGlue will re-register the ticket + // and -updateStatus: will be called again indicating first that + // registration is in progress and subsequently that it has completed. +} + +- (void)updateStatus:(NSNotification*)notification { + recentShownUserActionFailedStatus = NO; + + NSDictionary* dictionary = [notification userInfo]; + AutoupdateStatus status = static_cast<AutoupdateStatus>( + [[dictionary objectForKey:kAutoupdateStatusStatus] intValue]); + + // Don't assume |version| is a real string. It may be nil. + NSString* version = [dictionary objectForKey:kAutoupdateStatusVersion]; + + bool updateMessage = true; + bool throbber = false; + int imageID = 0; + NSString* message; + bool enableUpdateButton = false; + bool enablePromoteButton = true; + + switch (status) { + case kAutoupdateRegistering: + // When registering, use the "checking" message. The check will be + // launched if appropriate immediately after registration. + throbber = true; + message = l10n_util::GetNSStringWithFixup(IDS_UPGRADE_CHECK_STARTED); + enablePromoteButton = false; + + break; + + case kAutoupdateRegistered: + // Once registered, the ability to update and promote is known. + [self adjustUpdateUIVisibility]; + + if (![updateBlock_ isHidden]) { + // If registration completes while the window is visible, go straight + // into an update check. Return immediately, this routine will be + // re-entered shortly with kAutoupdateChecking. + [self checkForUpdate]; + return; + } + + // Nothing actually failed, but updates aren't possible. The throbber + // and message are hidden, but they'll be reset to these dummy values + // just to get the throbber to stop spinning if it's running. + imageID = IDR_UPDATE_FAIL; + message = l10n_util::GetNSStringFWithFixup(IDS_UPGRADE_ERROR, + base::IntToString16(status)); + + break; + + case kAutoupdateChecking: + throbber = true; + message = l10n_util::GetNSStringWithFixup(IDS_UPGRADE_CHECK_STARTED); + enablePromoteButton = false; + + break; + + case kAutoupdateCurrent: + imageID = IDR_UPDATE_UPTODATE; + message = l10n_util::GetNSStringFWithFixup( + IDS_UPGRADE_ALREADY_UP_TO_DATE, + l10n_util::GetStringUTF16(IDS_PRODUCT_NAME), + base::SysNSStringToUTF16(version)); + + break; + + case kAutoupdateAvailable: + imageID = IDR_UPDATE_AVAILABLE; + message = l10n_util::GetNSStringFWithFixup( + IDS_UPGRADE_AVAILABLE, l10n_util::GetStringUTF16(IDS_PRODUCT_NAME)); + enableUpdateButton = true; + + break; + + case kAutoupdateInstalling: + throbber = true; + message = l10n_util::GetNSStringWithFixup(IDS_UPGRADE_STARTED); + enablePromoteButton = false; + + break; + + case kAutoupdateInstalled: + { + imageID = IDR_UPDATE_UPTODATE; + string16 productName = l10n_util::GetStringUTF16(IDS_PRODUCT_NAME); + if (version) { + message = l10n_util::GetNSStringFWithFixup( + IDS_UPGRADE_SUCCESSFUL, + productName, + base::SysNSStringToUTF16(version)); + } else { + message = l10n_util::GetNSStringFWithFixup( + IDS_UPGRADE_SUCCESSFUL_NOVERSION, productName); + } + + // TODO(mark): Turn the button in the dialog into a restart button + // instead of springing this sheet or dialog. + NSWindow* window = [self window]; + NSWindow* restartDialogParent = [window isVisible] ? window : nil; + restart_browser::RequestRestart(restartDialogParent); + } + + break; + + case kAutoupdatePromoting: +#if 1 + // TODO(mark): See the TODO in -adjustUpdateUIVisibility for an + // explanation of why nothing can be done here at the moment. When + // KSRegistration handles promotion asynchronously, this dummy block can + // be replaced with the #else block. For now, just leave the messaging + // alone. http://b/2290009. + updateMessage = false; +#else + // The visibility may be changing. + [self adjustUpdateUIVisibility]; + + // This is not a terminal state, and kAutoupdatePromoted or + // kAutoupdatePromoteFailed will follow. Use the throbber and + // "checking" message so that it looks like something's happening. + throbber = true; + message = l10n_util::GetNSStringWithFixup(IDS_UPGRADE_CHECK_STARTED); +#endif + + enablePromoteButton = false; + + break; + + case kAutoupdatePromoted: + // The visibility may be changing. + [self adjustUpdateUIVisibility]; + + if (![updateBlock_ isHidden]) { + // If promotion completes while the window is visible, go straight + // into an update check. Return immediately, this routine will be + // re-entered shortly with kAutoupdateChecking. + [self checkForUpdate]; + return; + } + + // Nothing actually failed, but updates aren't possible. The throbber + // and message are hidden, but they'll be reset to these dummy values + // just to get the throbber to stop spinning if it's running. + imageID = IDR_UPDATE_FAIL; + message = l10n_util::GetNSStringFWithFixup(IDS_UPGRADE_ERROR, + base::IntToString16(status)); + + break; + + case kAutoupdateRegisterFailed: + imageID = IDR_UPDATE_FAIL; + message = l10n_util::GetNSStringFWithFixup(IDS_UPGRADE_ERROR, + base::IntToString16(status)); + enablePromoteButton = false; + + break; + + case kAutoupdateCheckFailed: + imageID = IDR_UPDATE_FAIL; + message = l10n_util::GetNSStringFWithFixup(IDS_UPGRADE_ERROR, + base::IntToString16(status)); + + break; + + case kAutoupdateInstallFailed: + recentShownUserActionFailedStatus = YES; + + imageID = IDR_UPDATE_FAIL; + message = l10n_util::GetNSStringFWithFixup(IDS_UPGRADE_ERROR, + base::IntToString16(status)); + + // Allow another chance. + enableUpdateButton = true; + + break; + + case kAutoupdatePromoteFailed: + recentShownUserActionFailedStatus = YES; + + imageID = IDR_UPDATE_FAIL; + message = l10n_util::GetNSStringFWithFixup(IDS_UPGRADE_ERROR, + base::IntToString16(status)); + + break; + + default: + NOTREACHED(); + + return; + } + + if (updateMessage) { + if (throbber) { + [self setUpdateThrobberMessage:message]; + } else { + DCHECK_NE(imageID, 0); + [self setUpdateImage:imageID message:message]; + } + } + + // Note that these buttons may be hidden depending on what + // -adjustUpdateUIVisibility did. Their enabled/disabled status doesn't + // necessarily have anything to do with their visibility. + [updateNowButton_ setEnabled:enableUpdateButton]; + [promoteButton_ setEnabled:enablePromoteButton]; +} + +- (BOOL)textView:(NSTextView *)aTextView + clickedOnLink:(id)link + atIndex:(NSUInteger)charIndex { + // We always create a new window, so there's no need to try to re-use + // an existing one just to pass in the NEW_WINDOW disposition. + Browser* browser = Browser::Create(profile_); + browser->OpenURL(GURL([link UTF8String]), GURL(), NEW_FOREGROUND_TAB, + PageTransition::LINK); + browser->window()->Show(); + return YES; +} + +- (NSTextView*)legalText { + return legalText_; +} + +- (NSButton*)updateButton { + return updateNowButton_; +} + +- (NSTextField*)updateText { + return updateText_; +} + ++ (NSAttributedString*)legalTextBlock { + // Windows builds this up in a very complex way, we're just trying to model + // it the best we can to get all the information in (they actually do it + // but created Labels and Links that they carefully place to make it appear + // to be a paragraph of text). + // src/chrome/browser/views/about_chrome_view.cc AboutChromeView::Init() + + NSMutableAttributedString* legal_block = + [[[NSMutableAttributedString alloc] init] autorelease]; + [legal_block beginEditing]; + + NSString* copyright = + l10n_util::GetNSStringWithFixup(IDS_ABOUT_VERSION_COPYRIGHT); + AttributedStringAppendString(legal_block, copyright); + + // These are the markers directly in IDS_ABOUT_VERSION_LICENSE + NSString* kBeginLinkChr = @"BEGIN_LINK_CHR"; + NSString* kBeginLinkOss = @"BEGIN_LINK_OSS"; + NSString* kEndLinkChr = @"END_LINK_CHR"; + NSString* kEndLinkOss = @"END_LINK_OSS"; + // The CHR link should go to here + NSString* kChromiumProject = l10n_util::GetNSString(IDS_CHROMIUM_PROJECT_URL); + // The OSS link should go to here + NSString* kAcknowledgements = + [NSString stringWithUTF8String:chrome::kAboutCreditsURL]; + + // Now fetch the license string and deal with the markers + + NSString* license = + l10n_util::GetNSStringWithFixup(IDS_ABOUT_VERSION_LICENSE); + + NSRange begin_chr = [license rangeOfString:kBeginLinkChr]; + NSRange begin_oss = [license rangeOfString:kBeginLinkOss]; + NSRange end_chr = [license rangeOfString:kEndLinkChr]; + NSRange end_oss = [license rangeOfString:kEndLinkOss]; + DCHECK_NE(begin_chr.location, NSNotFound); + DCHECK_NE(begin_oss.location, NSNotFound); + DCHECK_NE(end_chr.location, NSNotFound); + DCHECK_NE(end_oss.location, NSNotFound); + + // We don't know which link will come first, so we have to deal with things + // like this: + // [text][begin][text][end][text][start][text][end][text] + + bool chromium_link_first = begin_chr.location < begin_oss.location; + + NSRange* begin1 = &begin_chr; + NSRange* begin2 = &begin_oss; + NSRange* end1 = &end_chr; + NSRange* end2 = &end_oss; + NSString* link1 = kChromiumProject; + NSString* link2 = kAcknowledgements; + if (!chromium_link_first) { + // OSS came first, switch! + begin2 = &begin_chr; + begin1 = &begin_oss; + end2 = &end_chr; + end1 = &end_oss; + link2 = kChromiumProject; + link1 = kAcknowledgements; + } + + NSString *sub_str; + + AttributedStringAppendString(legal_block, @"\n"); + sub_str = [license substringWithRange:NSMakeRange(0, begin1->location)]; + AttributedStringAppendString(legal_block, sub_str); + sub_str = [license substringWithRange:NSMakeRange(NSMaxRange(*begin1), + end1->location - + NSMaxRange(*begin1))]; + AttributedStringAppendHyperlink(legal_block, sub_str, link1); + sub_str = [license substringWithRange:NSMakeRange(NSMaxRange(*end1), + begin2->location - + NSMaxRange(*end1))]; + AttributedStringAppendString(legal_block, sub_str); + sub_str = [license substringWithRange:NSMakeRange(NSMaxRange(*begin2), + end2->location - + NSMaxRange(*begin2))]; + AttributedStringAppendHyperlink(legal_block, sub_str, link2); + sub_str = [license substringWithRange:NSMakeRange(NSMaxRange(*end2), + [license length] - + NSMaxRange(*end2))]; + AttributedStringAppendString(legal_block, sub_str); + +#if defined(GOOGLE_CHROME_BUILD) + // Terms of service is only valid for Google Chrome + + // The url within terms should point here: + NSString* kTOS = [NSString stringWithUTF8String:chrome::kAboutTermsURL]; + // Following Windows. There is one marker in the string for where the terms + // link goes, but the text of the link comes from a second string resources. + std::vector<size_t> url_offsets; + NSString* about_terms = l10n_util::GetNSStringF(IDS_ABOUT_TERMS_OF_SERVICE, + string16(), + string16(), + &url_offsets); + DCHECK_EQ(url_offsets.size(), 1U); + NSString* terms_link_text = + l10n_util::GetNSStringWithFixup(IDS_TERMS_OF_SERVICE); + + AttributedStringAppendString(legal_block, @"\n\n"); + sub_str = [about_terms substringToIndex:url_offsets[0]]; + AttributedStringAppendString(legal_block, sub_str); + AttributedStringAppendHyperlink(legal_block, terms_link_text, kTOS); + sub_str = [about_terms substringFromIndex:url_offsets[0]]; + AttributedStringAppendString(legal_block, sub_str); +#endif // GOOGLE_CHROME_BUILD + + // We need to explicitly select Lucida Grande because once we click on + // the NSTextView, it changes to Helvetica 12 otherwise. + NSRange string_range = NSMakeRange(0, [legal_block length]); + [legal_block addAttribute:NSFontAttributeName + value:[NSFont labelFontOfSize:11] + range:string_range]; + + [legal_block endEditing]; + return legal_block; +} + +@end // @implementation AboutWindowController |