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