Home | History | Annotate | Download | only in cocoa
      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