Home | History | Annotate | Download | only in cocoa
      1 // Copyright (c) 2012 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/first_run_dialog.h"
      6 
      7 #include "base/bind.h"
      8 #include "base/mac/bundle_locations.h"
      9 #include "base/mac/mac_util.h"
     10 #import "base/mac/scoped_nsobject.h"
     11 #include "base/memory/ref_counted.h"
     12 #include "base/message_loop/message_loop.h"
     13 #include "base/strings/sys_string_conversions.h"
     14 #include "chrome/browser/first_run/first_run.h"
     15 #include "chrome/browser/first_run/first_run_dialog.h"
     16 #include "chrome/browser/profiles/profile.h"
     17 #include "chrome/browser/search_engines/template_url_service_factory.h"
     18 #include "chrome/browser/shell_integration.h"
     19 #include "chrome/common/chrome_version_info.h"
     20 #include "chrome/common/url_constants.h"
     21 #include "components/search_engines/template_url_service.h"
     22 #import "third_party/google_toolbox_for_mac/src/AppKit/GTMUILocalizerAndLayoutTweaker.h"
     23 #include "ui/base/l10n/l10n_util_mac.h"
     24 #include "url/gurl.h"
     25 
     26 #if defined(GOOGLE_CHROME_BUILD)
     27 #include "base/prefs/pref_service.h"
     28 #include "chrome/browser/browser_process.h"
     29 #include "chrome/common/pref_names.h"
     30 #include "chrome/installer/util/google_update_settings.h"
     31 #import "components/crash/app/breakpad_mac.h"
     32 #endif
     33 
     34 @interface FirstRunDialogController (PrivateMethods)
     35 // Show the dialog.
     36 - (void)show;
     37 @end
     38 
     39 namespace {
     40 
     41 // Compare function for -[NSArray sortedArrayUsingFunction:context:] that
     42 // sorts the views in Y order bottom up.
     43 NSInteger CompareFrameY(id view1, id view2, void* context) {
     44   CGFloat y1 = NSMinY([view1 frame]);
     45   CGFloat y2 = NSMinY([view2 frame]);
     46   if (y1 < y2)
     47     return NSOrderedAscending;
     48   else if (y1 > y2)
     49     return NSOrderedDescending;
     50   else
     51     return NSOrderedSame;
     52 }
     53 
     54 class FirstRunShowBridge : public base::RefCounted<FirstRunShowBridge> {
     55  public:
     56   FirstRunShowBridge(FirstRunDialogController* controller);
     57 
     58   void ShowDialog();
     59 
     60  private:
     61   friend class base::RefCounted<FirstRunShowBridge>;
     62 
     63   ~FirstRunShowBridge();
     64 
     65   FirstRunDialogController* controller_;
     66 };
     67 
     68 FirstRunShowBridge::FirstRunShowBridge(
     69     FirstRunDialogController* controller) : controller_(controller) {
     70 }
     71 
     72 void FirstRunShowBridge::ShowDialog() {
     73   [controller_ show];
     74   base::MessageLoop::current()->QuitNow();
     75 }
     76 
     77 FirstRunShowBridge::~FirstRunShowBridge() {}
     78 
     79 // Show the first run UI.
     80 // Returns true if the first run dialog was shown.
     81 bool ShowFirstRun(Profile* profile) {
     82   bool dialog_shown = false;
     83 #if defined(GOOGLE_CHROME_BUILD)
     84   // The purpose of the dialog is to ask the user to enable stats and crash
     85   // reporting. This setting may be controlled through configuration management
     86   // in enterprise scenarios. If that is the case, skip the dialog entirely, as
     87   // it's not worth bothering the user for only the default browser question
     88   // (which is likely to be forced in enterprise deployments anyway).
     89   const PrefService::Preference* metrics_reporting_pref =
     90       g_browser_process->local_state()->FindPreference(
     91           prefs::kMetricsReportingEnabled);
     92   if (!metrics_reporting_pref || !metrics_reporting_pref->IsManaged()) {
     93     base::scoped_nsobject<FirstRunDialogController> dialog(
     94         [[FirstRunDialogController alloc] init]);
     95 
     96     [dialog.get() showWindow:nil];
     97     dialog_shown = true;
     98 
     99     // If the dialog asked the user to opt-in for stats and crash reporting,
    100     // record the decision and enable the crash reporter if appropriate.
    101     bool stats_enabled = [dialog.get() statsEnabled];
    102     GoogleUpdateSettings::SetCollectStatsConsent(stats_enabled);
    103 
    104     // Breakpad is normally enabled very early in the startup process.  However,
    105     // on the first run it may not have been enabled due to the missing opt-in
    106     // from the user.  If the user agreed now, enable breakpad if necessary.
    107     if (!breakpad::IsCrashReporterEnabled() && stats_enabled) {
    108       breakpad::InitCrashReporter(std::string());
    109       breakpad::InitCrashProcessInfo(std::string());
    110     }
    111 
    112     // If selected set as default browser.
    113     BOOL make_default_browser = [dialog.get() makeDefaultBrowser];
    114     if (make_default_browser) {
    115       bool success = ShellIntegration::SetAsDefaultBrowser();
    116       DCHECK(success);
    117     }
    118   }
    119 #else  // GOOGLE_CHROME_BUILD
    120   // We don't show the dialog in Chromium.
    121 #endif  // GOOGLE_CHROME_BUILD
    122 
    123   // Set preference to show first run bubble and welcome page.
    124   // Only display the bubble if there is a default search provider.
    125   TemplateURLService* search_engines_model =
    126       TemplateURLServiceFactory::GetForProfile(profile);
    127   if (search_engines_model &&
    128       search_engines_model->GetDefaultSearchProvider()) {
    129     first_run::SetShowFirstRunBubblePref(first_run::FIRST_RUN_BUBBLE_SHOW);
    130   }
    131   first_run::SetShouldShowWelcomePage();
    132 
    133   return dialog_shown;
    134 }
    135 
    136 // True when the stats checkbox should be checked by default. This is only
    137 // the case when the canary is running.
    138 bool StatsCheckboxDefault() {
    139   return chrome::VersionInfo::GetChannel() ==
    140       chrome::VersionInfo::CHANNEL_CANARY;
    141 }
    142 
    143 }  // namespace
    144 
    145 namespace first_run {
    146 
    147 bool ShowFirstRunDialog(Profile* profile) {
    148   return ShowFirstRun(profile);
    149 }
    150 
    151 }  // namespace first_run
    152 
    153 @implementation FirstRunDialogController
    154 
    155 @synthesize statsEnabled = statsEnabled_;
    156 @synthesize makeDefaultBrowser = makeDefaultBrowser_;
    157 
    158 - (id)init {
    159   NSString* nibpath =
    160       [base::mac::FrameworkBundle() pathForResource:@"FirstRunDialog"
    161                                              ofType:@"nib"];
    162   if ((self = [super initWithWindowNibPath:nibpath owner:self])) {
    163     // Bound to the dialog checkboxes.
    164     makeDefaultBrowser_ = ShellIntegration::CanSetAsDefaultBrowser() !=
    165         ShellIntegration::SET_DEFAULT_NOT_ALLOWED;
    166     statsEnabled_ = StatsCheckboxDefault();
    167   }
    168   return self;
    169 }
    170 
    171 - (void)dealloc {
    172   [super dealloc];
    173 }
    174 
    175 - (IBAction)showWindow:(id)sender {
    176   // The main MessageLoop has not yet run, but has been spun. If we call
    177   // -[NSApplication runModalForWindow:] we will hang <http://crbug.com/54248>.
    178   // Therefore the main MessageLoop is run so things work.
    179 
    180   scoped_refptr<FirstRunShowBridge> bridge(new FirstRunShowBridge(self));
    181   base::MessageLoop::current()->PostTask(FROM_HERE,
    182       base::Bind(&FirstRunShowBridge::ShowDialog, bridge.get()));
    183   base::MessageLoop::current()->Run();
    184 }
    185 
    186 - (void)show {
    187   NSWindow* win = [self window];
    188 
    189   if (!ShellIntegration::CanSetAsDefaultBrowser()) {
    190     [setAsDefaultCheckbox_ setHidden:YES];
    191   }
    192 
    193   // Only support the sizing the window once.
    194   DCHECK(!beenSized_) << "ShowWindow was called twice?";
    195   if (!beenSized_) {
    196     beenSized_ = YES;
    197     DCHECK_GT([objectsToSize_ count], 0U);
    198 
    199     // Size everything to fit, collecting the widest growth needed (XIB provides
    200     // the min size, i.e.-never shrink, just grow).
    201     CGFloat largestWidthChange = 0.0;
    202     for (NSView* view in objectsToSize_) {
    203       DCHECK_NE(statsCheckbox_, view) << "Stats checkbox shouldn't be in list";
    204       if (![view isHidden]) {
    205         NSSize delta = [GTMUILocalizerAndLayoutTweaker sizeToFitView:view];
    206         DCHECK_EQ(delta.height, 0.0)
    207             << "Didn't expect anything to change heights";
    208         if (largestWidthChange < delta.width)
    209           largestWidthChange = delta.width;
    210       }
    211     }
    212 
    213     // Make the window wide enough to fit everything.
    214     if (largestWidthChange > 0.0) {
    215       NSView* contentView = [win contentView];
    216       NSRect windowFrame = [contentView convertRect:[win frame] fromView:nil];
    217       windowFrame.size.width += largestWidthChange;
    218       windowFrame = [contentView convertRect:windowFrame toView:nil];
    219       [win setFrame:windowFrame display:NO];
    220     }
    221 
    222     // The stats checkbox gets some really long text, so it gets word wrapped
    223     // and then sized.
    224     DCHECK(statsCheckbox_);
    225     CGFloat statsCheckboxHeightChange = 0.0;
    226     [GTMUILocalizerAndLayoutTweaker wrapButtonTitleForWidth:statsCheckbox_];
    227     statsCheckboxHeightChange =
    228         [GTMUILocalizerAndLayoutTweaker sizeToFitView:statsCheckbox_].height;
    229 
    230     // Walk bottom up shuffling for all the hidden views.
    231     NSArray* subViews =
    232         [[[win contentView] subviews] sortedArrayUsingFunction:CompareFrameY
    233                                                        context:NULL];
    234     CGFloat moveDown = 0.0;
    235     NSUInteger numSubViews = [subViews count];
    236     for (NSUInteger idx = 0 ; idx < numSubViews ; ++idx) {
    237       NSView* view = [subViews objectAtIndex:idx];
    238 
    239       // If the view is hidden, collect the amount to move everything above it
    240       // down, if it's not hidden, apply any shift down.
    241       if ([view isHidden]) {
    242         DCHECK_GT((numSubViews - 1), idx)
    243             << "Don't support top view being hidden";
    244         NSView* nextView = [subViews objectAtIndex:(idx + 1)];
    245         CGFloat viewBottom = [view frame].origin.y;
    246         CGFloat nextViewBottom = [nextView frame].origin.y;
    247         moveDown += nextViewBottom - viewBottom;
    248       } else {
    249         if (moveDown != 0.0) {
    250           NSPoint origin = [view frame].origin;
    251           origin.y -= moveDown;
    252           [view setFrameOrigin:origin];
    253         }
    254       }
    255       // Special case, if this is the stats checkbox, everything above it needs
    256       // to get moved up by the amount it changed height.
    257       if (view == statsCheckbox_) {
    258         moveDown -= statsCheckboxHeightChange;
    259       }
    260     }
    261 
    262     // Resize the window for any height change from hidden views, etc.
    263     if (moveDown != 0.0) {
    264       NSView* contentView = [win contentView];
    265       [contentView setAutoresizesSubviews:NO];
    266       NSRect windowFrame = [contentView convertRect:[win frame] fromView:nil];
    267       windowFrame.size.height -= moveDown;
    268       windowFrame = [contentView convertRect:windowFrame toView:nil];
    269       [win setFrame:windowFrame display:NO];
    270       [contentView setAutoresizesSubviews:YES];
    271     }
    272 
    273   }
    274 
    275   // Neat weirdness in the below code - the Application menu stays enabled
    276   // while the window is open but selecting items from it (e.g. Quit) has
    277   // no effect.  I'm guessing that this is an artifact of us being a
    278   // background-only application at this stage and displaying a modal
    279   // window.
    280 
    281   // Display dialog.
    282   [win center];
    283   [NSApp runModalForWindow:win];
    284 }
    285 
    286 - (IBAction)ok:(id)sender {
    287   [[self window] close];
    288   [NSApp stopModal];
    289 }
    290 
    291 - (IBAction)learnMore:(id)sender {
    292   NSString* urlStr = base::SysUTF8ToNSString(chrome::kLearnMoreReportingURL);
    293   NSURL* learnMoreUrl = [NSURL URLWithString:urlStr];
    294   [[NSWorkspace sharedWorkspace] openURL:learnMoreUrl];
    295 }
    296 
    297 @end
    298