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
30namespace {
31
32void 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
42void 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:.
130static 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