first_run_dialog.mm revision 4e180b6a0b4720a9b8e9e959a882386f690f08ff
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/GTM/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/breakpad_mac.h"
33#endif
34
35@interface FirstRunDialogController (PrivateMethods)
36// Show the dialog.
37- (void)show;
38@end
39
40namespace {
41
42// Compare function for -[NSArray sortedArrayUsingFunction:context:] that
43// sorts the views in Y order bottom up.
44NSInteger 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
55class 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
69FirstRunShowBridge::FirstRunShowBridge(
70    FirstRunDialogController* controller) : controller_(controller) {
71}
72
73void FirstRunShowBridge::ShowDialog() {
74  [controller_ show];
75  base::MessageLoop::current()->QuitNow();
76}
77
78FirstRunShowBridge::~FirstRunShowBridge() {}
79
80// Show the first run UI.
81// Returns true if the first run dialog was shown.
82bool 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();
110      breakpad::InitCrashProcessInfo();
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.
139bool StatsCheckboxDefault() {
140  return chrome::VersionInfo::GetChannel() ==
141      chrome::VersionInfo::CHANNEL_CANARY;
142}
143
144}  // namespace
145
146namespace first_run {
147
148bool 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