blob: f83c9a1ae8465b5dd51bab935317a87b46119de2 [file] [log] [blame]
// 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/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/google/google_util.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 = [base::mac::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 = base::mac::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
GURL url = google_util::AppendGoogleLocaleParam(
GURL(chrome::kChromiumProjectURL));
NSString* kChromiumProject = base::SysUTF8ToNSString(url.spec());
// 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