diff options
Diffstat (limited to 'chrome/browser/ui/cocoa/preferences_window_controller.mm')
-rw-r--r-- | chrome/browser/ui/cocoa/preferences_window_controller.mm | 2184 |
1 files changed, 2184 insertions, 0 deletions
diff --git a/chrome/browser/ui/cocoa/preferences_window_controller.mm b/chrome/browser/ui/cocoa/preferences_window_controller.mm new file mode 100644 index 0000000..de8459a --- /dev/null +++ b/chrome/browser/ui/cocoa/preferences_window_controller.mm @@ -0,0 +1,2184 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "chrome/browser/ui/cocoa/preferences_window_controller.h" + +#include <algorithm> + +#include "app/l10n_util.h" +#include "app/l10n_util_mac.h" +#include "app/resource_bundle.h" +#include "base/logging.h" +#include "base/mac_util.h" +#include "base/mac/scoped_aedesc.h" +#include "base/string16.h" +#include "base/string_util.h" +#include "base/sys_string_conversions.h" +#include "chrome/browser/autofill/autofill_dialog.h" +#include "chrome/browser/autofill/autofill_type.h" +#include "chrome/browser/autofill/personal_data_manager.h" +#include "chrome/browser/browser_process.h" +#include "chrome/browser/download/download_manager.h" +#include "chrome/browser/download/download_prefs.h" +#include "chrome/browser/extensions/extensions_service.h" +#include "chrome/browser/google/google_util.h" +#include "chrome/browser/instant/instant_confirm_dialog.h" +#include "chrome/browser/instant/instant_controller.h" +#include "chrome/browser/metrics/metrics_service.h" +#include "chrome/browser/metrics/user_metrics.h" +#include "chrome/browser/net/url_fixer_upper.h" +#include "chrome/browser/options_util.h" +#include "chrome/browser/options_window.h" +#include "chrome/browser/policy/managed_prefs_banner_base.h" +#include "chrome/browser/prefs/pref_service.h" +#include "chrome/browser/prefs/session_startup_pref.h" +#include "chrome/browser/profile.h" +#include "chrome/browser/safe_browsing/safe_browsing_service.h" +#include "chrome/browser/shell_integration.h" +#include "chrome/browser/show_options_url.h" +#include "chrome/browser/sync/profile_sync_service.h" +#include "chrome/browser/sync/sync_ui_util.h" +#include "chrome/browser/tab_contents/tab_contents.h" +#include "chrome/browser/ui/browser.h" +#include "chrome/browser/ui/browser_list.h" +#import "chrome/browser/ui/cocoa/clear_browsing_data_controller.h" +#import "chrome/browser/ui/cocoa/content_settings_dialog_controller.h" +#import "chrome/browser/ui/cocoa/custom_home_pages_model.h" +#import "chrome/browser/ui/cocoa/font_language_settings_controller.h" +#import "chrome/browser/ui/cocoa/import_settings_dialog.h" +#import "chrome/browser/ui/cocoa/keyword_editor_cocoa_controller.h" +#import "chrome/browser/ui/cocoa/l10n_util.h" +#import "chrome/browser/ui/cocoa/search_engine_list_model.h" +#import "chrome/browser/ui/cocoa/vertical_gradient_view.h" +#import "chrome/browser/ui/cocoa/window_size_autosaver.h" +#include "chrome/common/chrome_switches.h" +#include "chrome/common/notification_details.h" +#include "chrome/common/notification_observer.h" +#include "chrome/common/notification_type.h" +#include "chrome/common/pref_names.h" +#include "chrome/common/url_constants.h" +#include "chrome/installer/util/google_update_settings.h" +#include "grit/chromium_strings.h" +#include "grit/generated_resources.h" +#include "grit/locale_settings.h" +#include "grit/theme_resources.h" +#import "third_party/GTM/AppKit/GTMNSAnimation+Duration.h" +#import "third_party/GTM/AppKit/GTMUILocalizerAndLayoutTweaker.h" + +namespace { + +// Colors for the managed preferences warning banner. +static const double kBannerGradientColorTop[3] = + {255.0 / 255.0, 242.0 / 255.0, 183.0 / 255.0}; +static const double kBannerGradientColorBottom[3] = + {250.0 / 255.0, 230.0 / 255.0, 145.0 / 255.0}; +static const double kBannerStrokeColor = 135.0 / 255.0; + +// Tag id for retrieval via viewWithTag in NSView (from IB). +static const uint32 kBasicsStartupPageTableTag = 1000; + +bool IsNewTabUIURLString(const GURL& url) { + return url == GURL(chrome::kChromeUINewTabURL); +} + +// Helper that sizes two buttons to fit in a row keeping their spacing, returns +// the total horizontal size change. +CGFloat SizeToFitButtonPair(NSButton* leftButton, NSButton* rightButton) { + CGFloat widthShift = 0.0; + + NSSize delta = [GTMUILocalizerAndLayoutTweaker sizeToFitView:leftButton]; + DCHECK_EQ(delta.height, 0.0) << "Height changes unsupported"; + widthShift += delta.width; + + if (widthShift != 0.0) { + NSPoint origin = [rightButton frame].origin; + origin.x += widthShift; + [rightButton setFrameOrigin:origin]; + } + delta = [GTMUILocalizerAndLayoutTweaker sizeToFitView:rightButton]; + DCHECK_EQ(delta.height, 0.0) << "Height changes unsupported"; + widthShift += delta.width; + + return widthShift; +} + +// The different behaviors for the "pref group" auto sizing. +enum AutoSizeGroupBehavior { + kAutoSizeGroupBehaviorVerticalToFit, + kAutoSizeGroupBehaviorVerticalFirstToFit, + kAutoSizeGroupBehaviorHorizontalToFit, + kAutoSizeGroupBehaviorHorizontalFirstGrows, + kAutoSizeGroupBehaviorFirstTwoAsRowVerticalToFit +}; + +// Helper to tweak the layout of the "pref groups" and also ripple any height +// changes from one group to the next groups' origins. +// |views| is an ordered list of views with first being the label for the +// group and the rest being top down or left to right ordering of the views. +// The label is assumed to already be the same height as all the views it is +// next too. +CGFloat AutoSizeGroup(NSArray* views, AutoSizeGroupBehavior behavior, + CGFloat verticalShift) { + DCHECK_GE([views count], 2U) << "Should be at least a label and a control"; + NSTextField* label = [views objectAtIndex:0]; + DCHECK([label isKindOfClass:[NSTextField class]]) + << "First view should be the label for the group"; + + // Auto size the label to see if we need more vertical space for its localized + // string. + CGFloat labelHeightChange = + [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:label]; + + CGFloat localVerticalShift = 0.0; + switch (behavior) { + case kAutoSizeGroupBehaviorVerticalToFit: { + // Walk bottom up doing the sizing and moves. + for (NSUInteger index = [views count] - 1; index > 0; --index) { + NSView* view = [views objectAtIndex:index]; + NSSize delta = cocoa_l10n_util::WrapOrSizeToFit(view); + DCHECK_GE(delta.height, 0.0) << "Should NOT shrink in height"; + if (localVerticalShift) { + NSPoint origin = [view frame].origin; + origin.y += localVerticalShift; + [view setFrameOrigin:origin]; + } + localVerticalShift += delta.height; + } + break; + } + case kAutoSizeGroupBehaviorVerticalFirstToFit: { + // Just size the top one. + NSView* view = [views objectAtIndex:1]; + NSSize delta = cocoa_l10n_util::WrapOrSizeToFit(view); + DCHECK_GE(delta.height, 0.0) << "Should NOT shrink in height"; + localVerticalShift += delta.height; + break; + } + case kAutoSizeGroupBehaviorHorizontalToFit: { + // Walk left to right doing the sizing and moves. + // NOTE: Don't worry about vertical, assume it always fits. + CGFloat horizontalShift = 0.0; + NSUInteger count = [views count]; + for (NSUInteger index = 1; index < count; ++index) { + NSView* view = [views objectAtIndex:index]; + NSSize delta = cocoa_l10n_util::WrapOrSizeToFit(view); + DCHECK_GE(delta.height, 0.0) << "Should NOT shrink in height"; + if (horizontalShift) { + NSPoint origin = [view frame].origin; + origin.x += horizontalShift; + [view setFrameOrigin:origin]; + } + horizontalShift += delta.width; + } + break; + } + case kAutoSizeGroupBehaviorHorizontalFirstGrows: { + // Walk right to left doing the sizing and moves, then apply the space + // collected into the first. + // NOTE: Don't worry about vertical, assume it always all fits. + CGFloat horizontalShift = 0.0; + for (NSUInteger index = [views count] - 1; index > 1; --index) { + NSView* view = [views objectAtIndex:index]; + NSSize delta = cocoa_l10n_util::WrapOrSizeToFit(view); + DCHECK_GE(delta.height, 0.0) << "Should NOT shrink in height"; + horizontalShift -= delta.width; + NSPoint origin = [view frame].origin; + origin.x += horizontalShift; + [view setFrameOrigin:origin]; + } + if (horizontalShift) { + NSView* view = [views objectAtIndex:1]; + NSSize delta = NSMakeSize(horizontalShift, 0.0); + [GTMUILocalizerAndLayoutTweaker + resizeViewWithoutAutoResizingSubViews:view + delta:delta]; + } + break; + } + case kAutoSizeGroupBehaviorFirstTwoAsRowVerticalToFit: { + // Start out like kAutoSizeGroupBehaviorVerticalToFit but don't do + // the first two. Then handle the two as a row, but apply any + // vertical shift. + // All but the first two (in the row); walk bottom up. + for (NSUInteger index = [views count] - 1; index > 2; --index) { + NSView* view = [views objectAtIndex:index]; + NSSize delta = cocoa_l10n_util::WrapOrSizeToFit(view); + DCHECK_GE(delta.height, 0.0) << "Should NOT shrink in height"; + if (localVerticalShift) { + NSPoint origin = [view frame].origin; + origin.y += localVerticalShift; + [view setFrameOrigin:origin]; + } + localVerticalShift += delta.height; + } + // Deal with the two for the horizontal row. Size the second one. + CGFloat horizontalShift = 0.0; + NSView* view = [views objectAtIndex:2]; + NSSize delta = cocoa_l10n_util::WrapOrSizeToFit(view); + DCHECK_GE(delta.height, 0.0) << "Should NOT shrink in height"; + horizontalShift -= delta.width; + NSPoint origin = [view frame].origin; + origin.x += horizontalShift; + if (localVerticalShift) { + origin.y += localVerticalShift; + } + [view setFrameOrigin:origin]; + // Now expand the first item in the row to consume the space opened up. + view = [views objectAtIndex:1]; + if (horizontalShift) { + NSSize delta = NSMakeSize(horizontalShift, 0.0); + [GTMUILocalizerAndLayoutTweaker + resizeViewWithoutAutoResizingSubViews:view + delta:delta]; + } + // And move it up by any amount needed from the previous items. + if (localVerticalShift) { + NSPoint origin = [view frame].origin; + origin.y += localVerticalShift; + [view setFrameOrigin:origin]; + } + break; + } + default: + NOTREACHED(); + break; + } + + // If the label grew more then the views, the other views get an extra shift. + // Otherwise, move the label to its top is aligned with the other views. + CGFloat nonLabelShift = 0.0; + if (labelHeightChange > localVerticalShift) { + // Since the lable is taller, centering the other views looks best, just + // shift the views by 1/2 of the size difference. + nonLabelShift = (labelHeightChange - localVerticalShift) / 2.0; + } else { + NSPoint origin = [label frame].origin; + origin.y += localVerticalShift - labelHeightChange; + [label setFrameOrigin:origin]; + } + + // Apply the input shift requested along with any the shift from label being + // taller then the rest of the group. + for (NSView* view in views) { + NSPoint origin = [view frame].origin; + origin.y += verticalShift; + if (view != label) { + origin.y += nonLabelShift; + } + [view setFrameOrigin:origin]; + } + + // Return how much the group grew. + return localVerticalShift + nonLabelShift; +} + +// Helper to remove a view and move everything above it down to take over the +// space. +void RemoveViewFromView(NSView* view, NSView* toRemove) { + // Sort bottom up so we can spin over what is above it. + NSArray* views = + [[view subviews] sortedArrayUsingFunction:cocoa_l10n_util::CompareFrameY + context:NULL]; + + // Find where |toRemove| was. + NSUInteger index = [views indexOfObject:toRemove]; + DCHECK_NE(index, NSNotFound); + NSUInteger count = [views count]; + CGFloat shrinkHeight = 0; + if (index < (count - 1)) { + // If we're not the topmost control, the amount to shift is the bottom of + // |toRemove| to the bottom of the view above it. + CGFloat shiftDown = + NSMinY([[views objectAtIndex:index + 1] frame]) - + NSMinY([toRemove frame]); + + // Now cycle over the views above it moving them down. + for (++index; index < count; ++index) { + NSView* view = [views objectAtIndex:index]; + NSPoint origin = [view frame].origin; + origin.y -= shiftDown; + [view setFrameOrigin:origin]; + } + + shrinkHeight = shiftDown; + } else if (index > 0) { + // If we're the topmost control, there's nothing to shift but we want to + // shrink until the top edge of the second-topmost control, unless it is + // actually higher than the topmost control (since we're sorting by the + // bottom edge). + shrinkHeight = std::max(0.f, + NSMaxY([toRemove frame]) - + NSMaxY([[views objectAtIndex:index - 1] frame])); + } + // If we only have one control, don't do any resizing (for now). + + // Remove |toRemove|. + [toRemove removeFromSuperview]; + + [GTMUILocalizerAndLayoutTweaker + resizeViewWithoutAutoResizingSubViews:view + delta:NSMakeSize(0, -shrinkHeight)]; +} + +// Simply removes all the views in |toRemove|. +void RemoveGroupFromView(NSView* view, NSArray* toRemove) { + for (NSView* viewToRemove in toRemove) { + RemoveViewFromView(view, viewToRemove); + } +} + +// Helper to tweak the layout of the "Under the Hood" content by autosizing all +// the views and moving things up vertically. Special case the two controls for +// download location as they are horizontal, and should fill the row. Special +// case "Content Settings" and "Clear browsing data" as they are horizontal as +// well. +CGFloat AutoSizeUnderTheHoodContent(NSView* view, + NSPathControl* downloadLocationControl, + NSButton* downloadLocationButton) { + CGFloat verticalShift = 0.0; + + // Loop bottom up through the views sizing and shifting. + NSArray* views = + [[view subviews] sortedArrayUsingFunction:cocoa_l10n_util::CompareFrameY + context:NULL]; + for (NSView* view in views) { + NSSize delta = cocoa_l10n_util::WrapOrSizeToFit(view); + DCHECK_GE(delta.height, 0.0) << "Should NOT shrink in height"; + if (verticalShift) { + NSPoint origin = [view frame].origin; + origin.y += verticalShift; + [view setFrameOrigin:origin]; + } + verticalShift += delta.height; + + // The Download Location controls go in a row with the button aligned to the + // right edge and the path control using all the rest of the space. + if (view == downloadLocationButton) { + NSPoint origin = [downloadLocationButton frame].origin; + origin.x -= delta.width; + [downloadLocationButton setFrameOrigin:origin]; + NSSize controlSize = [downloadLocationControl frame].size; + controlSize.width -= delta.width; + [downloadLocationControl setFrameSize:controlSize]; + } + } + + return verticalShift; +} + +} // namespace + +//------------------------------------------------------------------------- + +@interface PreferencesWindowController(Private) +// Callback when preferences are changed. |prefName| is the name of the +// pref that has changed. +- (void)prefChanged:(std::string*)prefName; +// Callback when sync state has changed. syncService_ needs to be +// queried to find out what happened. +- (void)syncStateChanged; +// Record the user performed a certain action and save the preferences. +- (void)recordUserAction:(const UserMetricsAction&) action; +- (void)registerPrefObservers; +- (void)configureInstant; + +// KVC setter methods. +- (void)setNewTabPageIsHomePageIndex:(NSInteger)val; +- (void)setHomepageURL:(NSString*)urlString; +- (void)setRestoreOnStartupIndex:(NSInteger)type; +- (void)setShowHomeButton:(BOOL)value; +- (void)setPasswordManagerEnabledIndex:(NSInteger)value; +- (void)setIsUsingDefaultTheme:(BOOL)value; +- (void)setShowAlternateErrorPages:(BOOL)value; +- (void)setUseSuggest:(BOOL)value; +- (void)setDnsPrefetch:(BOOL)value; +- (void)setSafeBrowsing:(BOOL)value; +- (void)setMetricsReporting:(BOOL)value; +- (void)setAskForSaveLocation:(BOOL)value; +- (void)setFileHandlerUIEnabled:(BOOL)value; +- (void)setTranslateEnabled:(BOOL)value; +- (void)setTabsToLinks:(BOOL)value; +- (void)displayPreferenceViewForPage:(OptionsPage)page + animate:(BOOL)animate; +- (void)resetSubViews; +- (void)initBannerStateForPage:(OptionsPage)page; + +// KVC getter methods. +- (BOOL)fileHandlerUIEnabled; +@end + +namespace PreferencesWindowControllerInternal { + +// A C++ class registered for changes in preferences. Bridges the +// notification back to the PWC. +class PrefObserverBridge : public NotificationObserver, + public ProfileSyncServiceObserver { + public: + PrefObserverBridge(PreferencesWindowController* controller) + : controller_(controller) {} + + virtual ~PrefObserverBridge() {} + + // Overridden from NotificationObserver: + virtual void Observe(NotificationType type, + const NotificationSource& source, + const NotificationDetails& details) { + if (type == NotificationType::PREF_CHANGED) + [controller_ prefChanged:Details<std::string>(details).ptr()]; + } + + // Overridden from ProfileSyncServiceObserver. + virtual void OnStateChanged() { + [controller_ syncStateChanged]; + } + + private: + PreferencesWindowController* controller_; // weak, owns us +}; + +// Tracks state for a managed prefs banner and triggers UI updates through the +// PreferencesWindowController as appropriate. +class ManagedPrefsBannerState : public policy::ManagedPrefsBannerBase { + public: + virtual ~ManagedPrefsBannerState() { } + + explicit ManagedPrefsBannerState(PreferencesWindowController* controller, + OptionsPage page, + PrefService* local_state, + PrefService* prefs) + : policy::ManagedPrefsBannerBase(local_state, prefs, page), + controller_(controller), + page_(page) { } + + BOOL IsVisible() { + return DetermineVisibility(); + } + + protected: + // Overridden from ManagedPrefsBannerBase. + virtual void OnUpdateVisibility() { + [controller_ switchToPage:page_ animate:YES]; + } + + private: + PreferencesWindowController* controller_; // weak, owns us + OptionsPage page_; // current options page +}; + +} // namespace PreferencesWindowControllerInternal + +@implementation PreferencesWindowController + +@synthesize restoreButtonsEnabled = restoreButtonsEnabled_; +@synthesize restoreURLsEnabled = restoreURLsEnabled_; +@synthesize showHomeButtonEnabled = showHomeButtonEnabled_; +@synthesize defaultSearchEngineEnabled = defaultSearchEngineEnabled_; +@synthesize passwordManagerChoiceEnabled = passwordManagerChoiceEnabled_; +@synthesize passwordManagerButtonEnabled = passwordManagerButtonEnabled_; +@synthesize autoFillSettingsButtonEnabled = autoFillSettingsButtonEnabled_; +@synthesize showAlternateErrorPagesEnabled = showAlternateErrorPagesEnabled_; +@synthesize useSuggestEnabled = useSuggestEnabled_; +@synthesize dnsPrefetchEnabled = dnsPrefetchEnabled_; +@synthesize safeBrowsingEnabled = safeBrowsingEnabled_; +@synthesize metricsReportingEnabled = metricsReportingEnabled_; +@synthesize proxiesConfigureButtonEnabled = proxiesConfigureButtonEnabled_; + +- (id)initWithProfile:(Profile*)profile initialPage:(OptionsPage)initialPage { + DCHECK(profile); + // Use initWithWindowNibPath:: instead of initWithWindowNibName: so we + // can override it in a unit test. + NSString* nibPath = [mac_util::MainAppBundle() + pathForResource:@"Preferences" + ofType:@"nib"]; + if ((self = [super initWithWindowNibPath:nibPath owner:self])) { + profile_ = profile->GetOriginalProfile(); + initialPage_ = initialPage; + prefs_ = profile->GetPrefs(); + DCHECK(prefs_); + observer_.reset( + new PreferencesWindowControllerInternal::PrefObserverBridge(self)); + + // Set up the model for the custom home page table. The KVO observation + // tells us when the number of items in the array changes. The normal + // observation tells us when one of the URLs of an item changes. + customPagesSource_.reset([[CustomHomePagesModel alloc] + initWithProfile:profile_]); + const SessionStartupPref startupPref = + SessionStartupPref::GetStartupPref(prefs_); + [customPagesSource_ setURLs:startupPref.urls]; + + // Set up the model for the default search popup. Register for notifications + // about when the model changes so we can update the selection in the view. + searchEngineModel_.reset( + [[SearchEngineListModel alloc] + initWithModel:profile->GetTemplateURLModel()]); + [[NSNotificationCenter defaultCenter] + addObserver:self + selector:@selector(searchEngineModelChanged:) + name:kSearchEngineListModelChangedNotification + object:searchEngineModel_.get()]; + + // This needs to be done before awakeFromNib: because the bindings set up + // in the nib rely on it. + [self registerPrefObservers]; + + // Use one animation so we can stop it if the user clicks quickly, and + // start the new animation. + animation_.reset([[NSViewAnimation alloc] init]); + // Make this the delegate so it can remove the old view at the end of the + // animation (once it is faded out). + [animation_ setDelegate:self]; + [animation_ setAnimationBlockingMode:NSAnimationNonblocking]; + + // TODO(akalin): handle incognito profiles? The windows version of this + // (in chrome/browser/views/options/content_page_view.cc) just does what + // we do below. + syncService_ = profile_->GetProfileSyncService(); + + // TODO(akalin): This color is taken from kSyncLabelErrorBgColor in + // content_page_view.cc. Either decomp that color out into a + // function/variable that is referenced by both this file and + // content_page_view.cc, or maybe pick a more suitable color. + syncErrorBackgroundColor_.reset( + [[NSColor colorWithDeviceRed:0xff/255.0 + green:0x9a/255.0 + blue:0x9a/255.0 + alpha:1.0] retain]); + + // Disable the |autoFillSettingsButton_| if we have no + // |personalDataManager|. + PersonalDataManager* personalDataManager = + profile_->GetPersonalDataManager(); + [autoFillSettingsButton_ setHidden:(personalDataManager == NULL)]; + bool autofill_disabled_by_policy = + autoFillEnabled_.IsManaged() && !autoFillEnabled_.GetValue(); + [self setAutoFillSettingsButtonEnabled:!autofill_disabled_by_policy]; + [self setPasswordManagerChoiceEnabled:!askSavePasswords_.IsManaged()]; + [self setPasswordManagerButtonEnabled: + !askSavePasswords_.IsManaged() || askSavePasswords_.GetValue()]; + + // Initialize the enabled state of the elements on the general tab. + [self setShowHomeButtonEnabled:!showHomeButton_.IsManaged()]; + [self setEnabledStateOfRestoreOnStartup]; + [self setDefaultSearchEngineEnabled:![searchEngineModel_ isDefaultManaged]]; + + // Initialize UI state for the advanced page. + [self setShowAlternateErrorPagesEnabled:!alternateErrorPages_.IsManaged()]; + [self setUseSuggestEnabled:!useSuggest_.IsManaged()]; + [self setDnsPrefetchEnabled:!dnsPrefetch_.IsManaged()]; + [self setSafeBrowsingEnabled:!safeBrowsing_.IsManaged()]; + [self setMetricsReportingEnabled:!metricsReporting_.IsManaged()]; + proxyPrefs_.reset( + PrefSetObserver::CreateProxyPrefSetObserver(prefs_, observer_.get())); + [self setProxiesConfigureButtonEnabled:!proxyPrefs_->IsManaged()]; + } + return self; +} + +- (void)awakeFromNib { + + // Validate some assumptions in debug builds. + + // "Basics", "Personal Stuff", and "Under the Hood" views should be the same + // width. They should be the same width so they are laid out to look as good + // as possible at that width with controls just having to wrap if their text + // is too long. + DCHECK_EQ(NSWidth([basicsView_ frame]), NSWidth([personalStuffView_ frame])) + << "Basics and Personal Stuff should be the same widths"; + DCHECK_EQ(NSWidth([basicsView_ frame]), NSWidth([underTheHoodView_ frame])) + << "Basics and Under the Hood should be the same widths"; + // "Under the Hood" content should always be skinnier than the scroller it + // goes into (we resize it). + DCHECK_LE(NSWidth([underTheHoodContentView_ frame]), + [underTheHoodScroller_ contentSize].width) + << "The Under the Hood content should be narrower than the content " + "of the scroller it goes into"; + +#if !defined(GOOGLE_CHROME_BUILD) + // "Enable logging" (breakpad and stats) is only in Google Chrome builds, + // remove the checkbox and slide everything above it down. + RemoveViewFromView(underTheHoodContentView_, enableLoggingCheckbox_); +#endif // !defined(GOOGLE_CHROME_BUILD) + + // There are four problem children within the groups: + // Basics - Default Browser + // Personal Stuff - Sync + // Personal Stuff - Themes + // Personal Stuff - Browser Data + // These four have buttons that with some localizations are wider then the + // view. So the four get manually laid out before doing the general work so + // the views/window can be made wide enough to fit them. The layout in the + // general pass is a noop for these buttons (since they are already sized). + + // Size the default browser button. + const NSUInteger kDefaultBrowserGroupCount = 3; + const NSUInteger kDefaultBrowserButtonIndex = 1; + DCHECK_EQ([basicsGroupDefaultBrowser_ count], kDefaultBrowserGroupCount) + << "Expected only two items in Default Browser group"; + NSButton* defaultBrowserButton = + [basicsGroupDefaultBrowser_ objectAtIndex:kDefaultBrowserButtonIndex]; + NSSize defaultBrowserChange = + [GTMUILocalizerAndLayoutTweaker sizeToFitView:defaultBrowserButton]; + DCHECK_EQ(defaultBrowserChange.height, 0.0) + << "Button should have been right height in nib"; + + [self configureInstant]; + + // Size the sync row. + CGFloat syncRowChange = SizeToFitButtonPair(syncButton_, + syncCustomizeButton_); + + // Size the themes row. + const NSUInteger kThemeGroupCount = 3; + const NSUInteger kThemeResetButtonIndex = 1; + const NSUInteger kThemeThemesButtonIndex = 2; + DCHECK_EQ([personalStuffGroupThemes_ count], kThemeGroupCount) + << "Expected only two items in Themes group"; + CGFloat themeRowChange = SizeToFitButtonPair( + [personalStuffGroupThemes_ objectAtIndex:kThemeResetButtonIndex], + [personalStuffGroupThemes_ objectAtIndex:kThemeThemesButtonIndex]); + + // Size the Privacy and Clear buttons that make a row in Under the Hood. + CGFloat privacyRowChange = SizeToFitButtonPair(contentSettingsButton_, + clearDataButton_); + // Under the Hood view is narrower (then the other panes) in the nib, subtract + // out the amount it was already going to grow to match the other panes when + // calculating how much the row needs things to grow. + privacyRowChange -= + ([underTheHoodScroller_ contentSize].width - + NSWidth([underTheHoodContentView_ frame])); + + // Find the most any row changed in size. + CGFloat maxWidthChange = std::max(defaultBrowserChange.width, syncRowChange); + maxWidthChange = std::max(maxWidthChange, themeRowChange); + maxWidthChange = std::max(maxWidthChange, privacyRowChange); + + // If any grew wider, make the views wider. If they all shrank, they fit the + // existing view widths, so no change is needed//. + if (maxWidthChange > 0.0) { + NSSize viewSize = [basicsView_ frame].size; + viewSize.width += maxWidthChange; + [basicsView_ setFrameSize:viewSize]; + viewSize = [personalStuffView_ frame].size; + viewSize.width += maxWidthChange; + [personalStuffView_ setFrameSize:viewSize]; + } + + // Now that we have the width needed for Basics and Personal Stuff, lay out + // those pages bottom up making sure the strings fit and moving things up as + // needed. + + CGFloat newWidth = NSWidth([basicsView_ frame]); + CGFloat verticalShift = 0.0; + verticalShift += AutoSizeGroup(basicsGroupDefaultBrowser_, + kAutoSizeGroupBehaviorVerticalFirstToFit, + verticalShift); + // TODO(rsesek/rohitrao): This is ugly, when the instant experiement is no + // longer displayed, please remove this code, the NSTextField and IBOutlet + // needed. + DCHECK(instantExperiment_ != nil); + if (verticalShift) { + // If the default browser moved things up, move the experiment field up + // also, it is not in the SearchEngine group due to its position on screen. + NSPoint origin = [instantExperiment_ frame].origin; + origin.y += verticalShift; + [instantExperiment_ setFrameOrigin:origin]; + } + // End TODO + verticalShift += AutoSizeGroup(basicsGroupSearchEngine_, + kAutoSizeGroupBehaviorFirstTwoAsRowVerticalToFit, + verticalShift); + verticalShift += AutoSizeGroup(basicsGroupToolbar_, + kAutoSizeGroupBehaviorVerticalToFit, + verticalShift); + verticalShift += AutoSizeGroup(basicsGroupHomePage_, + kAutoSizeGroupBehaviorVerticalToFit, + verticalShift); + verticalShift += AutoSizeGroup(basicsGroupStartup_, + kAutoSizeGroupBehaviorVerticalFirstToFit, + verticalShift); + [GTMUILocalizerAndLayoutTweaker + resizeViewWithoutAutoResizingSubViews:basicsView_ + delta:NSMakeSize(0.0, verticalShift)]; + + verticalShift = 0.0; + verticalShift += AutoSizeGroup(personalStuffGroupThemes_, + kAutoSizeGroupBehaviorHorizontalToFit, + verticalShift); + verticalShift += AutoSizeGroup(personalStuffGroupBrowserData_, + kAutoSizeGroupBehaviorVerticalToFit, + verticalShift); + verticalShift += AutoSizeGroup(personalStuffGroupAutofill_, + kAutoSizeGroupBehaviorVerticalToFit, + verticalShift); + verticalShift += AutoSizeGroup(personalStuffGroupPasswords_, + kAutoSizeGroupBehaviorVerticalToFit, + verticalShift); + // TODO(akalin): Here we rely on the initial contents of the sync + // group's text field/link field to be large enough to hold all + // possible messages so that we don't have to re-layout when sync + // state changes. This isn't perfect, since e.g. some sync messages + // use the user's e-mail address (which may be really long), and the + // link field is usually not shown (leaving a big empty space). + // Rethink sync preferences UI for Mac. + verticalShift += AutoSizeGroup(personalStuffGroupSync_, + kAutoSizeGroupBehaviorVerticalToFit, + verticalShift); + [GTMUILocalizerAndLayoutTweaker + resizeViewWithoutAutoResizingSubViews:personalStuffView_ + delta:NSMakeSize(0.0, verticalShift)]; + + if (syncService_) { + syncService_->AddObserver(observer_.get()); + // Update the controls according to the initial state. + [self syncStateChanged]; + } else { + // If sync is disabled we don't want to show the sync controls at all. + RemoveGroupFromView(personalStuffView_, personalStuffGroupSync_); + } + + // Make the window as wide as the views. + NSWindow* prefsWindow = [self window]; + NSView* prefsContentView = [prefsWindow contentView]; + NSRect frame = [prefsContentView convertRect:[prefsWindow frame] + fromView:nil]; + frame.size.width = newWidth; + frame = [prefsContentView convertRect:frame toView:nil]; + [prefsWindow setFrame:frame display:NO]; + + // The Under the Hood prefs is a scroller, it shouldn't get any border, so it + // gets resized to be as wide as the window ended up. + NSSize underTheHoodSize = [underTheHoodView_ frame].size; + underTheHoodSize.width = newWidth; + [underTheHoodView_ setFrameSize:underTheHoodSize]; + + // Widen the Under the Hood content so things can rewrap to the full width. + NSSize underTheHoodContentSize = [underTheHoodContentView_ frame].size; + underTheHoodContentSize.width = [underTheHoodScroller_ contentSize].width; + [underTheHoodContentView_ setFrameSize:underTheHoodContentSize]; + + // Now that Under the Hood is the right width, auto-size to the new width to + // get the final height. + verticalShift = AutoSizeUnderTheHoodContent(underTheHoodContentView_, + downloadLocationControl_, + downloadLocationButton_); + [GTMUILocalizerAndLayoutTweaker + resizeViewWithoutAutoResizingSubViews:underTheHoodContentView_ + delta:NSMakeSize(0.0, verticalShift)]; + underTheHoodContentSize = [underTheHoodContentView_ frame].size; + + // Put the Under the Hood content view into the scroller and scroll it to the + // top. + [underTheHoodScroller_ setDocumentView:underTheHoodContentView_]; + [underTheHoodContentView_ scrollPoint: + NSMakePoint(0, underTheHoodContentSize.height)]; + + ResourceBundle& rb = ResourceBundle::GetSharedInstance(); + NSImage* alertIcon = rb.GetNativeImageNamed(IDR_WARNING); + DCHECK(alertIcon); + [managedPrefsBannerWarningImage_ setImage:alertIcon]; + + [self initBannerStateForPage:initialPage_]; + [self switchToPage:initialPage_ animate:NO]; + + // Save/restore position based on prefs. + if (g_browser_process && g_browser_process->local_state()) { + sizeSaver_.reset([[WindowSizeAutosaver alloc] + initWithWindow:[self window] + prefService:g_browser_process->local_state() + path:prefs::kPreferencesWindowPlacement]); + } + + // Initialize the banner gradient and stroke color. + NSColor* bannerStartingColor = + [NSColor colorWithCalibratedRed:kBannerGradientColorTop[0] + green:kBannerGradientColorTop[1] + blue:kBannerGradientColorTop[2] + alpha:1.0]; + NSColor* bannerEndingColor = + [NSColor colorWithCalibratedRed:kBannerGradientColorBottom[0] + green:kBannerGradientColorBottom[1] + blue:kBannerGradientColorBottom[2] + alpha:1.0]; + scoped_nsobject<NSGradient> bannerGradient( + [[NSGradient alloc] initWithStartingColor:bannerStartingColor + endingColor:bannerEndingColor]); + [managedPrefsBannerView_ setGradient:bannerGradient]; + + NSColor* bannerStrokeColor = + [NSColor colorWithCalibratedWhite:kBannerStrokeColor + alpha:1.0]; + [managedPrefsBannerView_ setStrokeColor:bannerStrokeColor]; + + // Set accessibility related attributes. + NSTableView* tableView = [basicsView_ viewWithTag:kBasicsStartupPageTableTag]; + NSString* description = + l10n_util::GetNSStringWithFixup(IDS_OPTIONS_STARTUP_SHOW_PAGES); + [tableView accessibilitySetOverrideValue:description + forAttribute:NSAccessibilityDescriptionAttribute]; +} + +- (void)dealloc { + if (syncService_) { + syncService_->RemoveObserver(observer_.get()); + } + [[NSNotificationCenter defaultCenter] removeObserver:self]; + [animation_ setDelegate:nil]; + [animation_ stopAnimation]; + [super dealloc]; +} + +// Xcode 3.1.x version of Interface Builder doesn't do a lot for editing +// toolbars in XIB. So the toolbar's delegate is set to the controller so it +// can tell the toolbar what items are selectable. +- (NSArray*)toolbarSelectableItemIdentifiers:(NSToolbar*)toolbar { + DCHECK(toolbar == toolbar_); + return [[toolbar_ items] valueForKey:@"itemIdentifier"]; +} + +// Register our interest in the preferences we're displaying so if anything +// else in the UI changes them we will be updated. +- (void)registerPrefObservers { + if (!prefs_) return; + + // Basics panel + registrar_.Init(prefs_); + registrar_.Add(prefs::kURLsToRestoreOnStartup, observer_.get()); + restoreOnStartup_.Init(prefs::kRestoreOnStartup, prefs_, observer_.get()); + newTabPageIsHomePage_.Init(prefs::kHomePageIsNewTabPage, + prefs_, observer_.get()); + homepage_.Init(prefs::kHomePage, prefs_, observer_.get()); + showHomeButton_.Init(prefs::kShowHomeButton, prefs_, observer_.get()); + instantEnabled_.Init(prefs::kInstantEnabled, prefs_, observer_.get()); + + // Personal Stuff panel + askSavePasswords_.Init(prefs::kPasswordManagerEnabled, + prefs_, observer_.get()); + autoFillEnabled_.Init(prefs::kAutoFillEnabled, prefs_, observer_.get()); + currentTheme_.Init(prefs::kCurrentThemeID, prefs_, observer_.get()); + + // Under the hood panel + alternateErrorPages_.Init(prefs::kAlternateErrorPagesEnabled, + prefs_, observer_.get()); + useSuggest_.Init(prefs::kSearchSuggestEnabled, prefs_, observer_.get()); + dnsPrefetch_.Init(prefs::kDnsPrefetchingEnabled, prefs_, observer_.get()); + safeBrowsing_.Init(prefs::kSafeBrowsingEnabled, prefs_, observer_.get()); + autoOpenFiles_.Init( + prefs::kDownloadExtensionsToOpen, prefs_, observer_.get()); + translateEnabled_.Init(prefs::kEnableTranslate, prefs_, observer_.get()); + tabsToLinks_.Init(prefs::kWebkitTabsToLinks, prefs_, observer_.get()); + + // During unit tests, there is no local state object, so we fall back to + // the prefs object (where we've explicitly registered this pref so we + // know it's there). + PrefService* local = g_browser_process->local_state(); + if (!local) + local = prefs_; + metricsReporting_.Init(prefs::kMetricsReportingEnabled, + local, observer_.get()); + defaultDownloadLocation_.Init(prefs::kDownloadDefaultDirectory, prefs_, + observer_.get()); + askForSaveLocation_.Init(prefs::kPromptForDownload, prefs_, observer_.get()); + + // We don't need to observe changes in this value. + lastSelectedPage_.Init(prefs::kOptionsWindowLastTabIndex, local, NULL); +} + +// Called when the window wants to be closed. +- (BOOL)windowShouldClose:(id)sender { + // Stop any animation and clear the delegate to avoid stale pointers. + [animation_ setDelegate:nil]; + [animation_ stopAnimation]; + + return YES; +} + +// Called when the user hits the escape key. Closes the window. +- (void)cancel:(id)sender { + [[self window] performClose:self]; +} + +// Record the user performed a certain action and save the preferences. +- (void)recordUserAction:(const UserMetricsAction &)action { + UserMetrics::RecordAction(action, profile_); + if (prefs_) + prefs_->ScheduleSavePersistentPrefs(); +} + +// Returns the set of keys that |key| depends on for its value so it can be +// re-computed when any of those change as well. ++ (NSSet*)keyPathsForValuesAffectingValueForKey:(NSString*)key { + NSSet* paths = [super keyPathsForValuesAffectingValueForKey:key]; + if ([key isEqualToString:@"isHomepageURLEnabled"]) { + paths = [paths setByAddingObject:@"newTabPageIsHomePageIndex"]; + paths = [paths setByAddingObject:@"homepageURL"]; + } else if ([key isEqualToString:@"restoreURLsEnabled"]) { + paths = [paths setByAddingObject:@"restoreOnStartupIndex"]; + } else if ([key isEqualToString:@"isHomepageChoiceEnabled"]) { + paths = [paths setByAddingObject:@"newTabPageIsHomePageIndex"]; + paths = [paths setByAddingObject:@"homepageURL"]; + } else if ([key isEqualToString:@"newTabPageIsHomePageIndex"]) { + paths = [paths setByAddingObject:@"homepageURL"]; + } else if ([key isEqualToString:@"hompageURL"]) { + paths = [paths setByAddingObject:@"newTabPageIsHomePageIndex"]; + } else if ([key isEqualToString:@"isDefaultBrowser"]) { + paths = [paths setByAddingObject:@"defaultBrowser"]; + } else if ([key isEqualToString:@"defaultBrowserTextColor"]) { + paths = [paths setByAddingObject:@"defaultBrowser"]; + } else if ([key isEqualToString:@"defaultBrowserText"]) { + paths = [paths setByAddingObject:@"defaultBrowser"]; + } + return paths; +} + +// Launch the Keychain Access app. +- (void)launchKeychainAccess { + NSString* const kKeychainBundleId = @"com.apple.keychainaccess"; + [[NSWorkspace sharedWorkspace] + launchAppWithBundleIdentifier:kKeychainBundleId + options:0L + additionalEventParamDescriptor:nil + launchIdentifier:nil]; +} + +//------------------------------------------------------------------------- +// Basics panel + +// Sets the home page preferences for kNewTabPageIsHomePage and kHomePage. If a +// blank or null-host URL is passed in we revert to using NewTab page +// as the Home page. Note: using SetValue() causes the observers not to fire, +// which is actually a good thing as we could end up in a state where setting +// the homepage to an empty url would automatically reset the prefs back to +// using the NTP, so we'd be never be able to change it. +- (void)setHomepage:(const GURL&)homepage { + if (IsNewTabUIURLString(homepage)) { + newTabPageIsHomePage_.SetValueIfNotManaged(true); + homepage_.SetValueIfNotManaged(std::string()); + } else if (!homepage.is_valid()) { + newTabPageIsHomePage_.SetValueIfNotManaged(true); + if (!homepage.has_host()) + homepage_.SetValueIfNotManaged(std::string()); + } else { + homepage_.SetValueIfNotManaged(homepage.spec()); + } +} + +// Callback when preferences are changed by someone modifying the prefs backend +// externally. |prefName| is the name of the pref that has changed. Unlike on +// Windows, we don't need to use this method for initializing, that's handled by +// Cocoa Bindings. +// Handles prefs for the "Basics" panel. +- (void)basicsPrefChanged:(std::string*)prefName { + if (*prefName == prefs::kRestoreOnStartup) { + const SessionStartupPref startupPref = + SessionStartupPref::GetStartupPref(prefs_); + [self setRestoreOnStartupIndex:startupPref.type]; + [self setEnabledStateOfRestoreOnStartup]; + } else if (*prefName == prefs::kURLsToRestoreOnStartup) { + [customPagesSource_ reloadURLs]; + [self setEnabledStateOfRestoreOnStartup]; + } else if (*prefName == prefs::kHomePageIsNewTabPage) { + NSInteger useNewTabPage = newTabPageIsHomePage_.GetValue() ? 0 : 1; + [self setNewTabPageIsHomePageIndex:useNewTabPage]; + } else if (*prefName == prefs::kHomePage) { + NSString* value = base::SysUTF8ToNSString(homepage_.GetValue()); + [self setHomepageURL:value]; + } else if (*prefName == prefs::kShowHomeButton) { + [self setShowHomeButton:showHomeButton_.GetValue() ? YES : NO]; + [self setShowHomeButtonEnabled:!showHomeButton_.IsManaged()]; + } else if (*prefName == prefs::kInstantEnabled) { + [self configureInstant]; + } +} + +// Returns the index of the selected cell in the "on startup" matrix based +// on the "restore on startup" pref. The ordering of the cells is in the +// same order as the pref. +- (NSInteger)restoreOnStartupIndex { + const SessionStartupPref pref = SessionStartupPref::GetStartupPref(prefs_); + return pref.type; +} + +// A helper function that takes the startup session type, grabs the URLs to +// restore, and saves it all in prefs. +- (void)saveSessionStartupWithType:(SessionStartupPref::Type)type { + SessionStartupPref pref; + pref.type = type; + pref.urls = [customPagesSource_.get() URLs]; + SessionStartupPref::SetStartupPref(prefs_, pref); +} + +// Sets the pref based on the index of the selected cell in the matrix and +// marks the appropriate user metric. +- (void)setRestoreOnStartupIndex:(NSInteger)type { + SessionStartupPref::Type startupType = + static_cast<SessionStartupPref::Type>(type); + switch (startupType) { + case SessionStartupPref::DEFAULT: + [self recordUserAction:UserMetricsAction("Options_Startup_Homepage")]; + break; + case SessionStartupPref::LAST: + [self recordUserAction:UserMetricsAction("Options_Startup_LastSession")]; + break; + case SessionStartupPref::URLS: + [self recordUserAction:UserMetricsAction("Options_Startup_Custom")]; + break; + default: + NOTREACHED(); + } + [self saveSessionStartupWithType:startupType]; +} + +// Enables or disables the restoreOnStartup elements +- (void) setEnabledStateOfRestoreOnStartup { + const SessionStartupPref startupPref = + SessionStartupPref::GetStartupPref(prefs_); + [self setRestoreButtonsEnabled:!SessionStartupPref::TypeIsManaged(prefs_)]; + [self setRestoreURLsEnabled:!SessionStartupPref::URLsAreManaged(prefs_) && + [self restoreOnStartupIndex] == SessionStartupPref::URLS]; +} + +// Getter for the |customPagesSource| property for bindings. +- (CustomHomePagesModel*)customPagesSource { + return customPagesSource_.get(); +} + +// Called when the selection in the table changes. If a flag is set indicating +// that we're waiting for a special select message, edit the cell. Otherwise +// just ignore it, we don't normally care. +- (void)tableViewSelectionDidChange:(NSNotification*)aNotification { + if (pendingSelectForEdit_) { + NSTableView* table = [aNotification object]; + NSUInteger selectedRow = [table selectedRow]; + [table editColumn:0 row:selectedRow withEvent:nil select:YES]; + pendingSelectForEdit_ = NO; + } +} + +// Called when the user hits the (+) button for adding a new homepage to the +// list. This will also attempt to make the new item editable so the user can +// just start typing. +- (IBAction)addHomepage:(id)sender { + [customPagesArrayController_ add:sender]; + + // When the new item is added to the model, the array controller will select + // it. We'll watch for that notification (because we are the table view's + // delegate) and then make the cell editable. Note that this can't be + // accomplished simply by subclassing the array controller's add method (I + // did try). The update of the table is asynchronous with the controller + // updating the model. + pendingSelectForEdit_ = YES; +} + +// Called when the user hits the (-) button for removing the selected items in +// the homepage table. The controller does all the work. +- (IBAction)removeSelectedHomepages:(id)sender { + [customPagesArrayController_ remove:sender]; +} + +// Add all entries for all open browsers with our profile. +- (IBAction)useCurrentPagesAsHomepage:(id)sender { + std::vector<GURL> urls; + for (BrowserList::const_iterator browserIter = BrowserList::begin(); + browserIter != BrowserList::end(); ++browserIter) { + Browser* browser = *browserIter; + if (browser->profile() != profile_) + continue; // Only want entries for open profile. + + for (int tabIndex = 0; tabIndex < browser->tab_count(); ++tabIndex) { + TabContents* tab = browser->GetTabContentsAt(tabIndex); + if (tab->ShouldDisplayURL()) { + const GURL url = browser->GetTabContentsAt(tabIndex)->GetURL(); + if (!url.is_empty()) + urls.push_back(url); + } + } + } + [customPagesSource_ setURLs:urls]; +} + +enum { kHomepageNewTabPage, kHomepageURL }; + +// Here's a table describing the desired characteristics of the homepage choice +// radio value, it's enabled state and the URL field enabled state. They depend +// on the values of the managed bits for homepage (m_hp) and +// homepageIsNewTabPage (m_ntp) preferences, as well as the value of the +// homepageIsNewTabPage preference (ntp) and whether the homepage preference +// is equal to the new tab page URL (hpisntp). +// +// m_hp m_ntp ntp hpisntp | choice value | choice enabled | URL field enabled +// -------------------------------------------------------------------------- +// 0 0 0 0 | homepage | 1 | 1 +// 0 0 0 1 | new tab page | 1 | 0 +// 0 0 1 0 | new tab page | 1 | 0 +// 0 0 1 1 | new tab page | 1 | 0 +// 0 1 0 0 | homepage | 0 | 1 +// 0 1 0 1 | homepage | 0 | 1 +// 0 1 1 0 | new tab page | 0 | 0 +// 0 1 1 1 | new tab page | 0 | 0 +// 1 0 0 0 | homepage | 1 | 0 +// 1 0 0 1 | new tab page | 0 | 0 +// 1 0 1 0 | new tab page | 1 | 0 +// 1 0 1 1 | new tab page | 0 | 0 +// 1 1 0 0 | homepage | 0 | 0 +// 1 1 0 1 | new tab page | 0 | 0 +// 1 1 1 0 | new tab page | 0 | 0 +// 1 1 1 1 | new tab page | 0 | 0 +// +// thus, we have: +// +// choice value is new tab page === ntp || (hpisntp && (m_hp || !m_ntp)) +// choice enabled === !m_ntp && !(m_hp && hpisntp) +// URL field enabled === !ntp && !mhp && !(hpisntp && !m_ntp) +// +// which also make sense if you think about them. + +// Checks whether the homepage URL refers to the new tab page. +- (BOOL)isHomepageNewTabUIURL { + return IsNewTabUIURLString(GURL(homepage_.GetValue().c_str())); +} + +// Returns the index of the selected cell in the "home page" marix based on +// the "new tab is home page" pref. Sadly, the ordering is reversed from the +// pref value. +- (NSInteger)newTabPageIsHomePageIndex { + return newTabPageIsHomePage_.GetValue() || + ([self isHomepageNewTabUIURL] && + (homepage_.IsManaged() || !newTabPageIsHomePage_.IsManaged())) ? + kHomepageNewTabPage : kHomepageURL; +} + +// Sets the pref based on the given index into the matrix and marks the +// appropriate user metric. +- (void)setNewTabPageIsHomePageIndex:(NSInteger)index { + bool useNewTabPage = index == kHomepageNewTabPage ? true : false; + if (useNewTabPage) { + [self recordUserAction:UserMetricsAction("Options_Homepage_UseNewTab")]; + } else { + [self recordUserAction:UserMetricsAction("Options_Homepage_UseURL")]; + if ([self isHomepageNewTabUIURL]) + homepage_.SetValueIfNotManaged(std::string()); + } + newTabPageIsHomePage_.SetValueIfNotManaged(useNewTabPage); +} + +// Check whether the new tab and URL homepage radios should be enabled, i.e. if +// the corresponding preference is not managed through configuration policy. +- (BOOL)isHomepageChoiceEnabled { + return !newTabPageIsHomePage_.IsManaged() && + !(homepage_.IsManaged() && [self isHomepageNewTabUIURL]); +} + +// Returns whether or not the homepage URL text field should be enabled +// based on if the new tab page is the home page. +- (BOOL)isHomepageURLEnabled { + return !newTabPageIsHomePage_.GetValue() && !homepage_.IsManaged() && + !([self isHomepageNewTabUIURL] && !newTabPageIsHomePage_.IsManaged()); +} + +// Returns the homepage URL. +- (NSString*)homepageURL { + NSString* value = base::SysUTF8ToNSString(homepage_.GetValue()); + return [self isHomepageNewTabUIURL] ? nil : value; +} + +// Sets the homepage URL to |urlString| with some fixing up. +- (void)setHomepageURL:(NSString*)urlString { + // If the text field contains a valid URL, sync it to prefs. We run it + // through the fixer upper to allow input like "google.com" to be converted + // to something valid ("http://google.com"). + std::string unfixedURL = urlString ? base::SysNSStringToUTF8(urlString) : + chrome::kChromeUINewTabURL; + [self setHomepage:URLFixerUpper::FixupURL(unfixedURL, std::string())]; +} + +// Returns whether the home button should be checked based on the preference. +- (BOOL)showHomeButton { + return showHomeButton_.GetValue() ? YES : NO; +} + +// Sets the backend pref for whether or not the home button should be displayed +// based on |value|. +- (void)setShowHomeButton:(BOOL)value { + if (value) + [self recordUserAction:UserMetricsAction( + "Options_Homepage_ShowHomeButton")]; + else + [self recordUserAction:UserMetricsAction( + "Options_Homepage_HideHomeButton")]; + showHomeButton_.SetValueIfNotManaged(value ? true : false); +} + +// Getter for the |searchEngineModel| property for bindings. +- (id)searchEngineModel { + return searchEngineModel_.get(); +} + +// Bindings for the search engine popup. We not binding directly to the model +// in order to siphon off the setter so we can record the metric. If we're +// doing it with one, might as well do it with both. +- (NSUInteger)searchEngineSelectedIndex { + return [searchEngineModel_ defaultIndex]; +} + +- (void)setSearchEngineSelectedIndex:(NSUInteger)index { + [self recordUserAction:UserMetricsAction("Options_SearchEngineChanged")]; + [searchEngineModel_ setDefaultIndex:index]; +} + +// Called when the search engine model changes. Update the selection in the +// popup by tickling the bindings with the new value. +- (void)searchEngineModelChanged:(NSNotification*)notify { + [self setSearchEngineSelectedIndex:[self searchEngineSelectedIndex]]; + [self setDefaultSearchEngineEnabled:![searchEngineModel_ isDefaultManaged]]; + +} + +- (IBAction)manageSearchEngines:(id)sender { + [KeywordEditorCocoaController showKeywordEditor:profile_]; +} + +- (IBAction)toggleInstant:(id)sender { + if (instantEnabled_.GetValue()) { + InstantController::Disable(profile_); + } else { + [instantCheckbox_ setState:NSOffState]; + browser::ShowInstantConfirmDialogIfNecessary([self window], profile_); + } +} + +// Sets the state of the Instant checkbox and adds the type information to the +// label. +- (void)configureInstant { + bool enabled = instantEnabled_.GetValue(); + NSInteger state = enabled ? NSOnState : NSOffState; + [instantCheckbox_ setState:state]; + + [instantExperiment_ setStringValue:@""]; +} + +- (IBAction)learnMoreAboutInstant:(id)sender { + browser::ShowOptionsURL(profile_, GURL(browser::kInstantLearnMoreURL)); +} + +// Called when the user clicks the button to make Chromium the default +// browser. Registers http and https. +- (IBAction)makeDefaultBrowser:(id)sender { + [self willChangeValueForKey:@"defaultBrowser"]; + + ShellIntegration::SetAsDefaultBrowser(); + [self recordUserAction:UserMetricsAction("Options_SetAsDefaultBrowser")]; + // If the user made Chrome the default browser, then he/she arguably wants + // to be notified when that changes. + prefs_->SetBoolean(prefs::kCheckDefaultBrowser, true); + + // Tickle KVO so that the UI updates. + [self didChangeValueForKey:@"defaultBrowser"]; +} + +// Returns the Chromium default browser state. +- (ShellIntegration::DefaultBrowserState)isDefaultBrowser { + return ShellIntegration::IsDefaultBrowser(); +} + +// Returns the text color of the "chromium is your default browser" text (green +// for yes, red for no). +- (NSColor*)defaultBrowserTextColor { + ShellIntegration::DefaultBrowserState state = [self isDefaultBrowser]; + return (state == ShellIntegration::IS_DEFAULT_BROWSER) ? + [NSColor colorWithCalibratedRed:0.0 green:135.0/255.0 blue:0 alpha:1.0] : + [NSColor colorWithCalibratedRed:135.0/255.0 green:0 blue:0 alpha:1.0]; +} + +// Returns the text for the "chromium is your default browser" string dependent +// on if Chromium actually is or not. +- (NSString*)defaultBrowserText { + ShellIntegration::DefaultBrowserState state = [self isDefaultBrowser]; + int stringId; + if (state == ShellIntegration::IS_DEFAULT_BROWSER) + stringId = IDS_OPTIONS_DEFAULTBROWSER_DEFAULT; + else if (state == ShellIntegration::NOT_DEFAULT_BROWSER) + stringId = IDS_OPTIONS_DEFAULTBROWSER_NOTDEFAULT; + else + stringId = IDS_OPTIONS_DEFAULTBROWSER_UNKNOWN; + string16 text = + l10n_util::GetStringFUTF16(stringId, + l10n_util::GetStringUTF16(IDS_PRODUCT_NAME)); + return base::SysUTF16ToNSString(text); +} + +//------------------------------------------------------------------------- +// User Data panel + +// Since passwords and forms are radio groups, 'enabled' is index 0 and +// 'disabled' is index 1. Yay. +const int kEnabledIndex = 0; +const int kDisabledIndex = 1; + +// Callback when preferences are changed. |prefName| is the name of the pref +// that has changed. Unlike on Windows, we don't need to use this method for +// initializing, that's handled by Cocoa Bindings. +// Handles prefs for the "Personal Stuff" panel. +- (void)userDataPrefChanged:(std::string*)prefName { + if (*prefName == prefs::kPasswordManagerEnabled) { + [self setPasswordManagerEnabledIndex:askSavePasswords_.GetValue() ? + kEnabledIndex : kDisabledIndex]; + [self setPasswordManagerChoiceEnabled:!askSavePasswords_.IsManaged()]; + [self setPasswordManagerButtonEnabled: + !askSavePasswords_.IsManaged() || askSavePasswords_.GetValue()]; + } + if (*prefName == prefs::kAutoFillEnabled) { + bool autofill_disabled_by_policy = + autoFillEnabled_.IsManaged() && !autoFillEnabled_.GetValue(); + [self setAutoFillSettingsButtonEnabled:!autofill_disabled_by_policy]; + } + if (*prefName == prefs::kCurrentThemeID) { + [self setIsUsingDefaultTheme:currentTheme_.GetValue().length() == 0]; + } +} + +// Called to launch the Keychain Access app to show the user's stored +// passwords. +- (IBAction)showSavedPasswords:(id)sender { + [self recordUserAction:UserMetricsAction("Options_ShowPasswordsExceptions")]; + [self launchKeychainAccess]; +} + +// Called to show the Auto Fill Settings dialog. +- (IBAction)showAutoFillSettings:(id)sender { + [self recordUserAction:UserMetricsAction("Options_ShowAutoFillSettings")]; + + PersonalDataManager* personalDataManager = profile_->GetPersonalDataManager(); + if (!personalDataManager) { + // Should not reach here because button is disabled when + // |personalDataManager| is NULL. + NOTREACHED(); + return; + } + + ShowAutoFillDialog(NULL, personalDataManager, profile_); +} + +// Called to import data from other browsers (Safari, Firefox, etc). +- (IBAction)importData:(id)sender { + UserMetrics::RecordAction(UserMetricsAction("Import_ShowDlg"), profile_); + [ImportSettingsDialogController showImportSettingsDialogForProfile:profile_]; +} + +- (IBAction)resetThemeToDefault:(id)sender { + [self recordUserAction:UserMetricsAction("Options_ThemesReset")]; + profile_->ClearTheme(); +} + +- (IBAction)themesGallery:(id)sender { + [self recordUserAction:UserMetricsAction("Options_ThemesGallery")]; + Browser* browser = BrowserList::GetLastActive(); + + if (!browser || !browser->GetSelectedTabContents()) + browser = Browser::Create(profile_); + browser->OpenThemeGalleryTabAndActivate(); +} + +// Called when the "stop syncing" confirmation dialog started by +// doSyncAction is finished. Stop syncing only If the user clicked +// OK. +- (void)stopSyncAlertDidEnd:(NSAlert*)alert + returnCode:(int)returnCode + contextInfo:(void*)contextInfo { + DCHECK(syncService_ && !syncService_->IsManaged()); + if (returnCode == NSAlertFirstButtonReturn) { + syncService_->DisableForUser(); + ProfileSyncService::SyncEvent(ProfileSyncService::STOP_FROM_OPTIONS); + } +} + +// Called when the user clicks the multi-purpose sync button in the +// "Personal Stuff" pane. +- (IBAction)doSyncAction:(id)sender { + DCHECK(syncService_ && !syncService_->IsManaged()); + if (syncService_->HasSyncSetupCompleted()) { + // If sync setup has completed that means the sync button was a + // "stop syncing" button. Bring up a confirmation dialog before + // actually stopping syncing (see stopSyncAlertDidEnd). + scoped_nsobject<NSAlert> alert([[NSAlert alloc] init]); + [alert addButtonWithTitle:l10n_util::GetNSStringWithFixup( + IDS_SYNC_STOP_SYNCING_CONFIRM_BUTTON_LABEL)]; + [alert addButtonWithTitle:l10n_util::GetNSStringWithFixup( + IDS_CANCEL)]; + [alert setMessageText:l10n_util::GetNSStringWithFixup( + IDS_SYNC_STOP_SYNCING_DIALOG_TITLE)]; + [alert setInformativeText:l10n_util::GetNSStringFWithFixup( + IDS_SYNC_STOP_SYNCING_EXPLANATION_LABEL, + l10n_util::GetStringUTF16(IDS_PRODUCT_NAME))]; + [alert setAlertStyle:NSWarningAlertStyle]; + const SEL kEndSelector = + @selector(stopSyncAlertDidEnd:returnCode:contextInfo:); + [alert beginSheetModalForWindow:[self window] + modalDelegate:self + didEndSelector:kEndSelector + contextInfo:NULL]; + } else { + // Otherwise, the sync button was a "sync my bookmarks" button. + // Kick off the sync setup process. + syncService_->ShowLoginDialog(NULL); + ProfileSyncService::SyncEvent(ProfileSyncService::START_FROM_OPTIONS); + } +} + +// Called when the user clicks on the link to the privacy dashboard. +- (IBAction)showPrivacyDashboard:(id)sender { + Browser* browser = BrowserList::GetLastActive(); + + if (!browser || !browser->GetSelectedTabContents()) + browser = Browser::Create(profile_); + browser->OpenPrivacyDashboardTabAndActivate(); +} + +// Called when the user clicks the "Customize Sync" button in the +// "Personal Stuff" pane. Spawns a dialog-modal sheet that cleans +// itself up on close. +- (IBAction)doSyncCustomize:(id)sender { + syncService_->ShowConfigure(NULL); +} + +- (IBAction)doSyncReauthentication:(id)sender { + DCHECK(syncService_ && !syncService_->IsManaged()); + syncService_->ShowLoginDialog(NULL); +} + +- (void)setPasswordManagerEnabledIndex:(NSInteger)value { + if (value == kEnabledIndex) + [self recordUserAction:UserMetricsAction( + "Options_PasswordManager_Enable")]; + else + [self recordUserAction:UserMetricsAction( + "Options_PasswordManager_Disable")]; + askSavePasswords_.SetValueIfNotManaged(value == kEnabledIndex ? true : false); +} + +- (NSInteger)passwordManagerEnabledIndex { + return askSavePasswords_.GetValue() ? kEnabledIndex : kDisabledIndex; +} + +- (void)setIsUsingDefaultTheme:(BOOL)value { + if (value) + [self recordUserAction:UserMetricsAction( + "Options_IsUsingDefaultTheme_Enable")]; + else + [self recordUserAction:UserMetricsAction( + "Options_IsUsingDefaultTheme_Disable")]; +} + +- (BOOL)isUsingDefaultTheme { + return currentTheme_.GetValue().length() == 0; +} + +//------------------------------------------------------------------------- +// Under the hood panel + +// Callback when preferences are changed. |prefName| is the name of the pref +// that has changed. Unlike on Windows, we don't need to use this method for +// initializing, that's handled by Cocoa Bindings. +// Handles prefs for the "Under the hood" panel. +- (void)underHoodPrefChanged:(std::string*)prefName { + if (*prefName == prefs::kAlternateErrorPagesEnabled) { + [self setShowAlternateErrorPages: + alternateErrorPages_.GetValue() ? YES : NO]; + [self setShowAlternateErrorPagesEnabled:!alternateErrorPages_.IsManaged()]; + } + else if (*prefName == prefs::kSearchSuggestEnabled) { + [self setUseSuggest:useSuggest_.GetValue() ? YES : NO]; + [self setUseSuggestEnabled:!useSuggest_.IsManaged()]; + } + else if (*prefName == prefs::kDnsPrefetchingEnabled) { + [self setDnsPrefetch:dnsPrefetch_.GetValue() ? YES : NO]; + [self setDnsPrefetchEnabled:!dnsPrefetch_.IsManaged()]; + } + else if (*prefName == prefs::kSafeBrowsingEnabled) { + [self setSafeBrowsing:safeBrowsing_.GetValue() ? YES : NO]; + [self setSafeBrowsingEnabled:!safeBrowsing_.IsManaged()]; + } + else if (*prefName == prefs::kMetricsReportingEnabled) { + [self setMetricsReporting:metricsReporting_.GetValue() ? YES : NO]; + [self setMetricsReportingEnabled:!metricsReporting_.IsManaged()]; + } + else if (*prefName == prefs::kDownloadDefaultDirectory) { + // Poke KVO. + [self willChangeValueForKey:@"defaultDownloadLocation"]; + [self didChangeValueForKey:@"defaultDownloadLocation"]; + } + else if (*prefName == prefs::kPromptForDownload) { + [self setAskForSaveLocation:askForSaveLocation_.GetValue() ? YES : NO]; + } + else if (*prefName == prefs::kEnableTranslate) { + [self setTranslateEnabled:translateEnabled_.GetValue() ? YES : NO]; + } + else if (*prefName == prefs::kWebkitTabsToLinks) { + [self setTabsToLinks:tabsToLinks_.GetValue() ? YES : NO]; + } + else if (*prefName == prefs::kDownloadExtensionsToOpen) { + // Poke KVC. + [self setFileHandlerUIEnabled:[self fileHandlerUIEnabled]]; + } + else if (proxyPrefs_->IsObserved(*prefName)) { + [self setProxiesConfigureButtonEnabled:!proxyPrefs_->IsManaged()]; + } +} + +// Set the new download path and notify the UI via KVO. +- (void)downloadPathPanelDidEnd:(NSOpenPanel*)panel + code:(NSInteger)returnCode + context:(void*)context { + if (returnCode == NSOKButton) { + [self recordUserAction:UserMetricsAction("Options_SetDownloadDirectory")]; + NSURL* path = [[panel URLs] lastObject]; // We only allow 1 item. + [self willChangeValueForKey:@"defaultDownloadLocation"]; + defaultDownloadLocation_.SetValue(base::SysNSStringToUTF8([path path])); + [self didChangeValueForKey:@"defaultDownloadLocation"]; + } +} + +// Bring up an open panel to allow the user to set a new downloads location. +- (void)browseDownloadLocation:(id)sender { + NSOpenPanel* panel = [NSOpenPanel openPanel]; + [panel setAllowsMultipleSelection:NO]; + [panel setCanChooseFiles:NO]; + [panel setCanChooseDirectories:YES]; + NSString* path = base::SysUTF8ToNSString(defaultDownloadLocation_.GetValue()); + [panel beginSheetForDirectory:path + file:nil + types:nil + modalForWindow:[self window] + modalDelegate:self + didEndSelector:@selector(downloadPathPanelDidEnd:code:context:) + contextInfo:NULL]; +} + +// Called to clear user's browsing data. This puts up an application-modal +// dialog to guide the user through clearing the data. +- (IBAction)clearData:(id)sender { + [ClearBrowsingDataController + showClearBrowsingDialogForProfile:profile_]; +} + +// Opens the "Content Settings" dialog. +- (IBAction)showContentSettings:(id)sender { + [ContentSettingsDialogController + showContentSettingsForType:CONTENT_SETTINGS_TYPE_DEFAULT + profile:profile_]; +} + +- (IBAction)privacyLearnMore:(id)sender { + GURL url = google_util::AppendGoogleLocaleParam( + GURL(chrome::kPrivacyLearnMoreURL)); + // We open a new browser window so the Options dialog doesn't get lost + // behind other windows. + browser::ShowOptionsURL(profile_, url); +} + +- (IBAction)resetAutoOpenFiles:(id)sender { + profile_->GetDownloadManager()->download_prefs()->ResetAutoOpen(); + [self recordUserAction:UserMetricsAction("Options_ResetAutoOpenFiles")]; +} + +- (IBAction)openProxyPreferences:(id)sender { + NSArray* itemsToOpen = [NSArray arrayWithObject:[NSURL fileURLWithPath: + @"/System/Library/PreferencePanes/Network.prefPane"]]; + + const char* proxyPrefCommand = "Proxies"; + base::mac::ScopedAEDesc<> openParams; + OSStatus status = AECreateDesc('ptru', + proxyPrefCommand, + strlen(proxyPrefCommand), + openParams.OutPointer()); + LOG_IF(ERROR, status != noErr) << "Failed to create open params: " << status; + + LSLaunchURLSpec launchSpec = { 0 }; + launchSpec.itemURLs = (CFArrayRef)itemsToOpen; + launchSpec.passThruParams = openParams; + launchSpec.launchFlags = kLSLaunchAsync | kLSLaunchDontAddToRecents; + LSOpenFromURLSpec(&launchSpec, NULL); +} + +// Returns whether the alternate error page checkbox should be checked based +// on the preference. +- (BOOL)showAlternateErrorPages { + return alternateErrorPages_.GetValue() ? YES : NO; +} + +// Sets the backend pref for whether or not the alternate error page checkbox +// should be displayed based on |value|. +- (void)setShowAlternateErrorPages:(BOOL)value { + if (value) + [self recordUserAction:UserMetricsAction( + "Options_LinkDoctorCheckbox_Enable")]; + else + [self recordUserAction:UserMetricsAction( + "Options_LinkDoctorCheckbox_Disable")]; + alternateErrorPages_.SetValueIfNotManaged(value ? true : false); +} + +// Returns whether the suggest checkbox should be checked based on the +// preference. +- (BOOL)useSuggest { + return useSuggest_.GetValue() ? YES : NO; +} + +// Sets the backend pref for whether or not the suggest checkbox should be +// displayed based on |value|. +- (void)setUseSuggest:(BOOL)value { + if (value) + [self recordUserAction:UserMetricsAction( + "Options_UseSuggestCheckbox_Enable")]; + else + [self recordUserAction:UserMetricsAction( + "Options_UseSuggestCheckbox_Disable")]; + useSuggest_.SetValueIfNotManaged(value ? true : false); +} + +// Returns whether the DNS prefetch checkbox should be checked based on the +// preference. +- (BOOL)dnsPrefetch { + return dnsPrefetch_.GetValue() ? YES : NO; +} + +// Sets the backend pref for whether or not the DNS prefetch checkbox should be +// displayed based on |value|. +- (void)setDnsPrefetch:(BOOL)value { + if (value) + [self recordUserAction:UserMetricsAction( + "Options_DnsPrefetchCheckbox_Enable")]; + else + [self recordUserAction:UserMetricsAction( + "Options_DnsPrefetchCheckbox_Disable")]; + dnsPrefetch_.SetValueIfNotManaged(value ? true : false); +} + +// Returns whether the safe browsing checkbox should be checked based on the +// preference. +- (BOOL)safeBrowsing { + return safeBrowsing_.GetValue() ? YES : NO; +} + +// Sets the backend pref for whether or not the safe browsing checkbox should be +// displayed based on |value|. +- (void)setSafeBrowsing:(BOOL)value { + if (value) + [self recordUserAction:UserMetricsAction( + "Options_SafeBrowsingCheckbox_Enable")]; + else + [self recordUserAction:UserMetricsAction( + "Options_SafeBrowsingCheckbox_Disable")]; + safeBrowsing_.SetValueIfNotManaged(value ? true : false); + SafeBrowsingService* safeBrowsingService = + g_browser_process->resource_dispatcher_host()->safe_browsing_service(); + MessageLoop::current()->PostTask( + FROM_HERE, + NewRunnableMethod(safeBrowsingService, + &SafeBrowsingService::OnEnable, + safeBrowsing_.GetValue())); +} + +// Returns whether the metrics reporting checkbox should be checked based on the +// preference. +- (BOOL)metricsReporting { + return metricsReporting_.GetValue() ? YES : NO; +} + +// Sets the backend pref for whether or not the metrics reporting checkbox +// should be displayed based on |value|. +- (void)setMetricsReporting:(BOOL)value { + if (value) + [self recordUserAction:UserMetricsAction( + "Options_MetricsReportingCheckbox_Enable")]; + else + [self recordUserAction:UserMetricsAction( + "Options_MetricsReportingCheckbox_Disable")]; + + // TODO(pinkerton): windows shows a dialog here telling the user they need to + // restart for this to take effect. http://crbug.com/34653 + metricsReporting_.SetValueIfNotManaged(value ? true : false); + + bool enabled = metricsReporting_.GetValue(); + GoogleUpdateSettings::SetCollectStatsConsent(enabled); + bool update_pref = GoogleUpdateSettings::GetCollectStatsConsent(); + if (enabled != update_pref) { + DVLOG(1) << "GENERAL SECTION: Unable to set crash report status to " + << enabled; + } + // Only change the pref if GoogleUpdateSettings::GetCollectStatsConsent + // succeeds. + enabled = update_pref; + + MetricsService* metrics = g_browser_process->metrics_service(); + DCHECK(metrics); + if (metrics) { + metrics->SetUserPermitsUpload(enabled); + if (enabled) + metrics->Start(); + else + metrics->Stop(); + } +} + +- (NSURL*)defaultDownloadLocation { + NSString* pathString = + base::SysUTF8ToNSString(defaultDownloadLocation_.GetValue()); + return [NSURL fileURLWithPath:pathString]; +} + +- (BOOL)askForSaveLocation { + return askForSaveLocation_.GetValue(); +} + +- (void)setAskForSaveLocation:(BOOL)value { + if (value) { + [self recordUserAction:UserMetricsAction( + "Options_AskForSaveLocation_Enable")]; + } else { + [self recordUserAction:UserMetricsAction( + "Options_AskForSaveLocation_Disable")]; + } + askForSaveLocation_.SetValue(value); +} + +- (BOOL)fileHandlerUIEnabled { + if (!profile_->GetDownloadManager()) // Not set in unit tests. + return NO; + return profile_->GetDownloadManager()->download_prefs()->IsAutoOpenUsed(); +} + +- (void)setFileHandlerUIEnabled:(BOOL)value { + [resetFileHandlersButton_ setEnabled:value]; +} + +- (BOOL)translateEnabled { + return translateEnabled_.GetValue(); +} + +- (void)setTranslateEnabled:(BOOL)value { + if (value) { + [self recordUserAction:UserMetricsAction("Options_Translate_Enable")]; + } else { + [self recordUserAction:UserMetricsAction("Options_Translate_Disable")]; + } + translateEnabled_.SetValue(value); +} + +- (BOOL)tabsToLinks { + return tabsToLinks_.GetValue(); +} + +- (void)setTabsToLinks:(BOOL)value { + if (value) { + [self recordUserAction:UserMetricsAction("Options_TabsToLinks_Enable")]; + } else { + [self recordUserAction:UserMetricsAction("Options_TabsToLinks_Disable")]; + } + tabsToLinks_.SetValue(value); +} + +- (void)fontAndLanguageEndSheet:(NSWindow*)sheet + returnCode:(NSInteger)returnCode + contextInfo:(void*)context { + [sheet close]; + [sheet orderOut:self]; + fontLanguageSettings_ = nil; +} + +- (IBAction)changeFontAndLanguageSettings:(id)sender { + // Intentionally leak the controller as it will clean itself up when the + // sheet closes. + fontLanguageSettings_ = + [[FontLanguageSettingsController alloc] initWithProfile:profile_]; + [NSApp beginSheet:[fontLanguageSettings_ window] + modalForWindow:[self window] + modalDelegate:self + didEndSelector:@selector(fontAndLanguageEndSheet:returnCode:contextInfo:) + contextInfo:nil]; +} + +// Called to launch the Keychain Access app to show the user's stored +// certificates. Note there's no way to script the app to auto-select the +// certificates. +- (IBAction)showCertificates:(id)sender { + [self recordUserAction:UserMetricsAction("Options_ManagerCerts")]; + [self launchKeychainAccess]; +} + +- (IBAction)resetToDefaults:(id)sender { + // The alert will clean itself up in the did-end selector. + NSAlert* alert = [[NSAlert alloc] init]; + [alert setMessageText:l10n_util::GetNSString(IDS_OPTIONS_RESET_MESSAGE)]; + NSButton* resetButton = [alert addButtonWithTitle: + l10n_util::GetNSString(IDS_OPTIONS_RESET_OKLABEL)]; + [resetButton setKeyEquivalent:@""]; + NSButton* cancelButton = [alert addButtonWithTitle: + l10n_util::GetNSString(IDS_OPTIONS_RESET_CANCELLABEL)]; + [cancelButton setKeyEquivalent:@"\r"]; + + [alert beginSheetModalForWindow:[self window] + modalDelegate:self + didEndSelector:@selector(resetToDefaults:returned:context:) + contextInfo:nil]; +} + +- (void)resetToDefaults:(NSAlert*)alert + returned:(NSInteger)code + context:(void*)context { + if (code == NSAlertFirstButtonReturn) { + OptionsUtil::ResetToDefaults(profile_); + } + [alert autorelease]; +} + +//------------------------------------------------------------------------- + +// Callback when preferences are changed. |prefName| is the name of the +// pref that has changed and should not be NULL. +- (void)prefChanged:(std::string*)prefName { + DCHECK(prefName); + if (!prefName) return; + [self basicsPrefChanged:prefName]; + [self userDataPrefChanged:prefName]; + [self underHoodPrefChanged:prefName]; +} + +// Callback when sync service state has changed. +// +// TODO(akalin): Decomp this out since a lot of it is copied from the +// Windows version. +// TODO(akalin): Change the background of the status label/link on error. +- (void)syncStateChanged { + DCHECK(syncService_); + + string16 statusLabel, linkLabel; + sync_ui_util::MessageType status = + sync_ui_util::GetStatusLabels(syncService_, &statusLabel, &linkLabel); + bool managed = syncService_->IsManaged(); + + [syncButton_ setEnabled:!syncService_->WizardIsVisible()]; + NSString* buttonLabel; + if (syncService_->HasSyncSetupCompleted()) { + buttonLabel = l10n_util::GetNSStringWithFixup( + IDS_SYNC_STOP_SYNCING_BUTTON_LABEL); + [syncCustomizeButton_ setHidden:false]; + } else if (syncService_->SetupInProgress()) { + buttonLabel = l10n_util::GetNSStringWithFixup( + IDS_SYNC_NTP_SETUP_IN_PROGRESS); + [syncCustomizeButton_ setHidden:true]; + } else { + buttonLabel = l10n_util::GetNSStringWithFixup( + IDS_SYNC_START_SYNC_BUTTON_LABEL); + [syncCustomizeButton_ setHidden:true]; + } + [syncCustomizeButton_ setEnabled:!managed]; + [syncButton_ setTitle:buttonLabel]; + [syncButton_ setEnabled:!managed]; + + [syncStatus_ setStringValue:base::SysUTF16ToNSString(statusLabel)]; + [syncLink_ setHidden:linkLabel.empty()]; + [syncLink_ setTitle:base::SysUTF16ToNSString(linkLabel)]; + [syncLink_ setEnabled:!managed]; + + NSButtonCell* syncLinkCell = static_cast<NSButtonCell*>([syncLink_ cell]); + if (!syncStatusNoErrorBackgroundColor_) { + DCHECK(!syncLinkNoErrorBackgroundColor_); + // We assume that the sync controls start off in a non-error + // state. + syncStatusNoErrorBackgroundColor_.reset( + [[syncStatus_ backgroundColor] retain]); + syncLinkNoErrorBackgroundColor_.reset( + [[syncLinkCell backgroundColor] retain]); + } + if (status == sync_ui_util::SYNC_ERROR) { + [syncStatus_ setBackgroundColor:syncErrorBackgroundColor_]; + [syncLinkCell setBackgroundColor:syncErrorBackgroundColor_]; + } else { + [syncStatus_ setBackgroundColor:syncStatusNoErrorBackgroundColor_]; + [syncLinkCell setBackgroundColor:syncLinkNoErrorBackgroundColor_]; + } +} + +// Show the preferences window. +- (IBAction)showPreferences:(id)sender { + [self showWindow:sender]; +} + +- (IBAction)toolbarButtonSelected:(id)sender { + DCHECK([sender isKindOfClass:[NSToolbarItem class]]); + OptionsPage page = [self getPageForToolbarItem:sender]; + [self displayPreferenceViewForPage:page animate:YES]; +} + +// Helper to update the window to display a preferences view for a page. +- (void)displayPreferenceViewForPage:(OptionsPage)page + animate:(BOOL)animate { + NSWindow* prefsWindow = [self window]; + + // Needs to go *after* the call to [self window], which triggers + // awakeFromNib if necessary. + NSView* prefsView = [self getPrefsViewForPage:page]; + NSView* contentView = [prefsWindow contentView]; + + // Make sure we aren't being told to display the same thing again. + if (currentPrefsView_ == prefsView && + managedPrefsBannerVisible_ == bannerState_->IsVisible()) { + return; + } + + // Remember new options page as current page. + if (page != OPTIONS_PAGE_DEFAULT) + lastSelectedPage_.SetValue(page); + + // Stop any running animation, and reset the subviews to the new state. We + // re-add any views we need for animation later. + [animation_ stopAnimation]; + NSView* oldPrefsView = currentPrefsView_; + currentPrefsView_ = prefsView; + [self resetSubViews]; + + // Update the banner state. + [self initBannerStateForPage:page]; + BOOL showBanner = bannerState_->IsVisible(); + + // Update the window title. + NSToolbarItem* toolbarItem = [self getToolbarItemForPage:page]; + [prefsWindow setTitle:[toolbarItem label]]; + + // Calculate new frames for the subviews. + NSRect prefsViewFrame = [prefsView frame]; + NSRect contentViewFrame = [contentView frame]; + NSRect bannerViewFrame = [managedPrefsBannerView_ frame]; + + // Determine what height the managed prefs banner will use. + CGFloat bannerViewHeight = showBanner ? NSHeight(bannerViewFrame) : 0.0; + + if (animate) { + // NSViewAnimation doesn't seem to honor subview resizing as it animates the + // Window's frame. So instead of trying to get the top in the right place, + // just set the origin where it should be at the end, and let the fade/size + // slide things into the right spot. + prefsViewFrame.origin.y = 0.0; + } else { + // The prefView is anchored to the top of its parent, so set its origin so + // that the top is where it should be. When the window's frame is set, the + // origin will be adjusted to keep it in the right spot. + prefsViewFrame.origin.y = NSHeight(contentViewFrame) - + NSHeight(prefsViewFrame) - bannerViewHeight; + } + bannerViewFrame.origin.y = NSHeight(prefsViewFrame); + bannerViewFrame.size.width = NSWidth(contentViewFrame); + [prefsView setFrame:prefsViewFrame]; + + // Figure out the size of the window. + NSRect windowFrame = [contentView convertRect:[prefsWindow frame] + fromView:nil]; + CGFloat titleToolbarHeight = + NSHeight(windowFrame) - NSHeight(contentViewFrame); + windowFrame.size.height = + NSHeight(prefsViewFrame) + titleToolbarHeight + bannerViewHeight; + DCHECK_GE(NSWidth(windowFrame), NSWidth(prefsViewFrame)) + << "Initial width set wasn't wide enough."; + windowFrame = [contentView convertRect:windowFrame toView:nil]; + windowFrame.origin.y = NSMaxY([prefsWindow frame]) - NSHeight(windowFrame); + + // Now change the size. + if (animate) { + NSMutableArray* animations = [NSMutableArray arrayWithCapacity:4]; + if (oldPrefsView != prefsView) { + // Fade between prefs views if they change. + [contentView addSubview:oldPrefsView + positioned:NSWindowBelow + relativeTo:nil]; + [animations addObject: + [NSDictionary dictionaryWithObjectsAndKeys: + oldPrefsView, NSViewAnimationTargetKey, + NSViewAnimationFadeOutEffect, NSViewAnimationEffectKey, + nil]]; + [animations addObject: + [NSDictionary dictionaryWithObjectsAndKeys: + prefsView, NSViewAnimationTargetKey, + NSViewAnimationFadeInEffect, NSViewAnimationEffectKey, + nil]]; + } else { + // Make sure the prefs pane ends up in the right position in case we + // manipulate the banner. + [animations addObject: + [NSDictionary dictionaryWithObjectsAndKeys: + prefsView, NSViewAnimationTargetKey, + [NSValue valueWithRect:prefsViewFrame], + NSViewAnimationEndFrameKey, + nil]]; + } + if (showBanner != managedPrefsBannerVisible_) { + // Slide the warning banner in or out of view. + [animations addObject: + [NSDictionary dictionaryWithObjectsAndKeys: + managedPrefsBannerView_, NSViewAnimationTargetKey, + [NSValue valueWithRect:bannerViewFrame], + NSViewAnimationEndFrameKey, + nil]]; + } + // Window resize animation. + [animations addObject: + [NSDictionary dictionaryWithObjectsAndKeys: + prefsWindow, NSViewAnimationTargetKey, + [NSValue valueWithRect:windowFrame], NSViewAnimationEndFrameKey, + nil]]; + [animation_ setViewAnimations:animations]; + // The default duration is 0.5s, which actually feels slow in here, so speed + // it up a bit. + [animation_ gtm_setDuration:0.2 + eventMask:NSLeftMouseUpMask]; + [animation_ startAnimation]; + } else { + // If not animating, odds are we don't want to display either (because it + // is initial window setup). + [prefsWindow setFrame:windowFrame display:NO]; + [managedPrefsBannerView_ setFrame:bannerViewFrame]; + } + + managedPrefsBannerVisible_ = showBanner; +} + +- (void)resetSubViews { + // Reset subviews to current prefs view and banner, remove any views that + // might have been left over from previous state or animation. + NSArray* subviews = [NSArray arrayWithObjects: + currentPrefsView_, managedPrefsBannerView_, nil]; + [[[self window] contentView] setSubviews:subviews]; + [[self window] setInitialFirstResponder:currentPrefsView_]; +} + +- (void)animationDidEnd:(NSAnimation*)animation { + DCHECK_EQ(animation_.get(), animation); + // Animation finished, reset subviews to current prefs view and the banner. + [self resetSubViews]; +} + +// Reinitializes the banner state tracker object to watch for managed bits of +// preferences relevant to the given options |page|. +- (void)initBannerStateForPage:(OptionsPage)page { + page = [self normalizePage:page]; + + // During unit tests, there is no local state object, so we fall back to + // the prefs object (where we've explicitly registered this pref so we + // know it's there). + PrefService* local = g_browser_process->local_state(); + if (!local) + local = prefs_; + bannerState_.reset( + new PreferencesWindowControllerInternal::ManagedPrefsBannerState( + self, page, local, prefs_)); +} + +- (void)switchToPage:(OptionsPage)page animate:(BOOL)animate { + [self displayPreferenceViewForPage:page animate:animate]; + NSToolbarItem* toolbarItem = [self getToolbarItemForPage:page]; + [toolbar_ setSelectedItemIdentifier:[toolbarItem itemIdentifier]]; +} + +// Called when the window is being closed. Send out a notification that the user +// is done editing preferences. Make sure there are no pending field editors +// by clearing the first responder. +- (void)windowWillClose:(NSNotification*)notification { + // Setting the first responder to the window ends any in-progress field + // editor. This will update the model appropriately so there's nothing left + // to do. + if (![[self window] makeFirstResponder:[self window]]) { + // We've hit a recalcitrant field editor, force it to go away. + [[self window] endEditingFor:nil]; + } + [self autorelease]; +} + +- (void)controlTextDidEndEditing:(NSNotification*)notification { + [customPagesSource_ validateURLs]; +} + +@end + +@implementation PreferencesWindowController(Testing) + +- (IntegerPrefMember*)lastSelectedPage { + return &lastSelectedPage_; +} + +- (NSToolbar*)toolbar { + return toolbar_; +} + +- (NSView*)basicsView { + return basicsView_; +} + +- (NSView*)personalStuffView { + return personalStuffView_; +} + +- (NSView*)underTheHoodView { + return underTheHoodView_; +} + +- (OptionsPage)normalizePage:(OptionsPage)page { + if (page == OPTIONS_PAGE_DEFAULT) { + // Get the last visited page from local state. + page = static_cast<OptionsPage>(lastSelectedPage_.GetValue()); + if (page == OPTIONS_PAGE_DEFAULT) { + page = OPTIONS_PAGE_GENERAL; + } + } + return page; +} + +- (NSToolbarItem*)getToolbarItemForPage:(OptionsPage)page { + NSUInteger pageIndex = (NSUInteger)[self normalizePage:page]; + NSArray* items = [toolbar_ items]; + NSUInteger itemCount = [items count]; + DCHECK_GE(pageIndex, 0U); + if (pageIndex >= itemCount) { + NOTIMPLEMENTED(); + pageIndex = 0; + } + DCHECK_GT(itemCount, 0U); + return [items objectAtIndex:pageIndex]; +} + +- (OptionsPage)getPageForToolbarItem:(NSToolbarItem*)toolbarItem { + // Tags are set in the nib file. + switch ([toolbarItem tag]) { + case 0: // Basics + return OPTIONS_PAGE_GENERAL; + case 1: // Personal Stuff + return OPTIONS_PAGE_CONTENT; + case 2: // Under the Hood + return OPTIONS_PAGE_ADVANCED; + default: + NOTIMPLEMENTED(); + return OPTIONS_PAGE_GENERAL; + } +} + +- (NSView*)getPrefsViewForPage:(OptionsPage)page { + // The views will be NULL if this is mistakenly called before awakeFromNib. + DCHECK(basicsView_); + DCHECK(personalStuffView_); + DCHECK(underTheHoodView_); + page = [self normalizePage:page]; + switch (page) { + case OPTIONS_PAGE_GENERAL: + return basicsView_; + case OPTIONS_PAGE_CONTENT: + return personalStuffView_; + case OPTIONS_PAGE_ADVANCED: + return underTheHoodView_; + case OPTIONS_PAGE_DEFAULT: + case OPTIONS_PAGE_COUNT: + LOG(DFATAL) << "Invalid page value " << page; + } + return basicsView_; +} + +@end |