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