// Copyright (c) 2009 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/cocoa/about_window_controller.h" #include "app/l10n_util_mac.h" #include "app/resource_bundle.h" #include "base/logging.h" #include "base/mac_util.h" #include "base/string_util.h" #include "base/sys_string_conversions.h" #include "chrome/browser/browser_list.h" #import "chrome/browser/cocoa/background_tile_view.h" #import "chrome/browser/cocoa/keystone_glue.h" #include "chrome/browser/cocoa/restart_browser.h" #include "chrome/common/platform_util.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 = @""; string16 modifier = platform_util::GetVersionStringModifier(); if (modifier.length()) versionModifier = [NSString stringWithFormat:@" %@", base::SysUTF16ToNSString(modifier)]; // The format string is not localized, but this is how the displayed version // is built on Windows too. NSString* svnRevision = [bundle objectForInfoDictionaryKey:@"SVNRevision"]; NSString* version = [NSString stringWithFormat:@"%@%@ (%@)", chromeVersion, versionModifier, svnRevision]; [version_ setStringValue:version]; // Put the two images into the UI. ResourceBundle& rb = ResourceBundle::GetSharedInstance(); NSImage* backgroundImage = rb.GetNSImageNamed(IDR_ABOUT_BACKGROUND_COLOR); DCHECK(backgroundImage); [backgroundView_ setTileImage:backgroundImage]; NSImage* logoImage = rb.GetNSImageNamed(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 = ![promotionBlock_ isHidden]; if (promotion == oldPromotion && update == oldUpdate) { return; } NSRect updateFrame = [updateBlock_ frame]; NSRect promotionFrame = [promotionBlock_ frame]; // This routine assumes no space between the update and promotion blocks. DCHECK_EQ(NSMinY(updateFrame), NSMaxY(promotionFrame)); bool promotionOnly = !update && promotion; bool oldPromotionOnly = !oldUpdate && oldPromotion; if (promotionOnly != oldPromotionOnly) { // The window is transitioning from having a promotion block and no update // block to any other state, or the other way around. Move the promotion // frame up so that its top edge is in the same position as the update // frame's top edge, or move it back down to its original location. promotionFrame.origin.y += (promotionOnly ? 1.0 : -1.0) * NSHeight(updateFrame); } CGFloat delta = 0.0; if (update != oldUpdate) { [updateBlock_ setHidden:!update]; delta += (update ? 1.0 : -1.0) * NSHeight(updateFrame); } if (promotion != oldPromotion) { [promotionBlock_ setHidden:!promotion]; delta += (promotion ? 1.0 : -1.0) * NSHeight(promotionFrame); } NSRect legalFrame = [legalBlock_ frame]; bool updateOrPromotion = update || promotion; bool oldUpdateOrPromotion = oldUpdate || oldPromotion; if (updateOrPromotion != oldUpdateOrPromotion) { // The window is transitioning from having an update or promotion block or // both to not having either, or the other way around. Adjust delta to // account for the space between the legal block and the update or // promotion block. When the update and promotion blocks are not visible, // this extra spacing is not used. delta += (updateOrPromotion ? 1.0 : -1.0) * (NSMinY(legalFrame) - NSMaxY(updateFrame)); } // The promotion frame may have changed even if delta is 0, so always reset // its frame. promotionFrame.origin.y += delta; [promotionBlock_ setFrame:promotionFrame]; 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.GetNSImageNamed(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( [[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, 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, IntToString16(status)); break; case kAutoupdateRegisterFailed: imageID = IDR_UPDATE_FAIL; message = l10n_util::GetNSStringFWithFixup(IDS_UPGRADE_ERROR, IntToString16(status)); enablePromoteButton = false; break; case kAutoupdateCheckFailed: imageID = IDR_UPDATE_FAIL; message = l10n_util::GetNSStringFWithFixup(IDS_UPGRADE_ERROR, IntToString16(status)); break; case kAutoupdateInstallFailed: recentShownUserActionFailedStatus = YES; imageID = IDR_UPDATE_FAIL; message = l10n_util::GetNSStringFWithFixup(IDS_UPGRADE_ERROR, IntToString16(status)); // Allow another chance. enableUpdateButton = true; break; case kAutoupdatePromoteFailed: recentShownUserActionFailedStatus = YES; imageID = IDR_UPDATE_FAIL; message = l10n_util::GetNSStringFWithFixup(IDS_UPGRADE_ERROR, 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_); if (browser) { browser->OpenURL(GURL([link UTF8String]), GURL(), NEW_WINDOW, PageTransition::LINK); } 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 = @"about:credits"; // 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 = @"about:terms"; // Following Window. 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 url_offsets; std::wstring w_about_terms = l10n_util::GetStringF(IDS_ABOUT_TERMS_OF_SERVICE, std::wstring(), std::wstring(), &url_offsets); DCHECK_EQ(url_offsets.size(), 1U); NSString* about_terms = base::SysWideToNSString(w_about_terms); 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