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