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/app/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(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. 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