| // Copyright (c) 2009 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/cocoa/preferences_window_controller.h" |
| |
| #include <algorithm> |
| #include "app/l10n_util.h" |
| #include "app/l10n_util_mac.h" |
| #include "base/logging.h" |
| #include "base/mac_util.h" |
| #include "base/string16.h" |
| #include "base/string_util.h" |
| #include "base/sys_string_conversions.h" |
| #include "chrome/browser/browser.h" |
| #include "chrome/browser/browser_list.h" |
| #include "chrome/browser/browser_process.h" |
| #import "chrome/browser/cocoa/clear_browsing_data_controller.h" |
| #import "chrome/browser/cocoa/cookies_window_controller.h" |
| #import "chrome/browser/cocoa/custom_home_pages_model.h" |
| #import "chrome/browser/cocoa/font_language_settings_controller.h" |
| #import "chrome/browser/cocoa/keyword_editor_cocoa_controller.h" |
| #import "chrome/browser/cocoa/search_engine_list_model.h" |
| #include "chrome/browser/extensions/extensions_service.h" |
| #include "chrome/browser/metrics/metrics_service.h" |
| #include "chrome/browser/metrics/user_metrics.h" |
| #include "chrome/browser/net/dns_global.h" |
| #include "chrome/browser/net/url_fixer_upper.h" |
| #include "chrome/browser/options_window.h" |
| #include "chrome/browser/profile.h" |
| #include "chrome/browser/safe_browsing/safe_browsing_service.h" |
| #include "chrome/browser/session_startup_pref.h" |
| #include "chrome/browser/shell_integration.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/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/pref_service.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 "net/base/cookie_policy.h" |
| #import "third_party/GTM/AppKit/GTMUILocalizerAndLayoutTweaker.h" |
| #import "third_party/GTM/AppKit/GTMNSAnimation+Duration.h" |
| |
| namespace { |
| |
| std::string GetNewTabUIURLString() { |
| return URLFixerUpper::FixupURL(chrome::kChromeUINewTabURL, std::string()); |
| } |
| |
| // Helper to remove all but the last view from the view heirarchy. |
| void RemoveAllButLastView(NSArray* views) { |
| NSArray* toRemove = [views subarrayWithRange:NSMakeRange(0, [views count]-1)]; |
| for (NSView* view in toRemove) { |
| [view removeFromSuperviewWithoutNeedingDisplay]; |
| } |
| } |
| |
| // 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; |
| } |
| |
| // Helper for tweaking the prefs window, if view is a: |
| // checkbox, radio group or label: it gets a forced wrap at current size |
| // editable field: left as is |
| // anything else: do +[GTMUILocalizerAndLayoutTweaker sizeToFitView:] |
| NSSize WrapOrSizeToFit(NSView* view) { |
| if ([view isKindOfClass:[NSTextField class]]) { |
| NSTextField* textField = static_cast<NSTextField*>(view); |
| if ([textField isEditable]) |
| return NSZeroSize; |
| CGFloat heightChange = |
| [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:textField]; |
| return NSMakeSize(0.0, heightChange); |
| } |
| if ([view isKindOfClass:[NSMatrix class]]) { |
| NSMatrix* radioGroup = static_cast<NSMatrix*>(view); |
| [GTMUILocalizerAndLayoutTweaker wrapRadioGroupForWidth:radioGroup]; |
| return [GTMUILocalizerAndLayoutTweaker sizeToFitView:view]; |
| } |
| if ([view isKindOfClass:[NSButton class]]) { |
| NSButton* button = static_cast<NSButton*>(view); |
| NSButtonCell* buttonCell = [button cell]; |
| // Decide it's a checkbox via showsStateBy and highlightsBy. |
| if (([buttonCell showsStateBy] == NSCellState) && |
| ([buttonCell highlightsBy] == NSCellState)) { |
| [GTMUILocalizerAndLayoutTweaker wrapButtonTitleForWidth:button]; |
| return [GTMUILocalizerAndLayoutTweaker sizeToFitView:view]; |
| } |
| } |
| return [GTMUILocalizerAndLayoutTweaker sizeToFitView:view]; |
| } |
| |
| // The different behaviors for the "pref group" auto sizing. |
| enum AutoSizeGroupBehavior { |
| kAutoSizeGroupBehaviorVerticalToFit, |
| kAutoSizeGroupBehaviorVerticalFirstToFit, |
| kAutoSizeGroupBehaviorHorizontalToFit, |
| kAutoSizeGroupBehaviorHorizontalFirstGrows |
| }; |
| |
| // 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 = 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 = 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 = 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 = 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; |
| } |
| 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; |
| } |
| |
| // Compare function for -[NSArray sortedArrayUsingFunction:context:] that |
| // sorts the views in Y order bottom up. |
| NSInteger CompareFrameY(id view1, id view2, void* context) { |
| CGFloat y1 = NSMinY([view1 frame]); |
| CGFloat y2 = NSMinY([view2 frame]); |
| if (y1 < y2) |
| return NSOrderedAscending; |
| else if (y1 > y2) |
| return NSOrderedDescending; |
| else |
| return NSOrderedSame; |
| } |
| |
| // 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: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. |
| 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:CompareFrameY context:NULL]; |
| for (NSView* view in views) { |
| NSSize delta = 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::wstring*)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 char*)action; |
| - (void)registerPrefObservers; |
| - (void)unregisterPrefObservers; |
| |
| - (void)customHomePagesChanged; |
| |
| // KVC setter methods. |
| - (void)setNewTabPageIsHomePageIndex:(NSInteger)val; |
| - (void)setHomepageURL:(NSString*)urlString; |
| - (void)setRestoreOnStartupIndex:(NSInteger)type; |
| - (void)setShowHomeButton:(BOOL)value; |
| - (void)setShowPageOptionsButtons:(BOOL)value; |
| - (void)setPasswordManagerEnabledIndex:(NSInteger)value; |
| - (void)setFormAutofillEnabledIndex:(NSInteger)value; |
| - (void)setIsUsingDefaultTheme:(BOOL)value; |
| - (void)setShowAlternateErrorPages:(BOOL)value; |
| - (void)setUseSuggest:(BOOL)value; |
| - (void)setDnsPrefetch:(BOOL)value; |
| - (void)setSafeBrowsing:(BOOL)value; |
| - (void)setMetricsRecording:(BOOL)value; |
| - (void)setCookieBehavior:(NSInteger)value; |
| - (void)setAskForSaveLocation:(BOOL)value; |
| - (void)displayPreferenceViewForPage:(OptionsPage)page |
| animate:(BOOL)animate; |
| @end |
| |
| // 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::wstring>(details).ptr()]; |
| } |
| |
| // Overridden from ProfileSyncServiceObserver. |
| virtual void OnStateChanged() { |
| [controller_ syncStateChanged]; |
| } |
| |
| private: |
| PreferencesWindowController* controller_; // weak, owns us |
| }; |
| |
| @implementation PreferencesWindowController |
| |
| - (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 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]; |
| [customPagesSource_ addObserver:self |
| forKeyPath:@"customHomePages" |
| options:0L |
| context:NULL]; |
| [[NSNotificationCenter defaultCenter] |
| addObserver:self |
| selector:@selector(homepageEntryChanged:) |
| name:kHomepageEntryChangedNotification |
| object:nil]; |
| |
| // 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]; |
| // The default duration is 0.5s, which actually feels slow in here, so speed |
| // it up a bit. |
| [animation_ gtm_setDuration:0.2]; |
| [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]); |
| } |
| 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 three problem children within the groups: |
| // Basics - Default Browser |
| // Personal Stuff - Themes |
| // Personal Stuff - Browser Data |
| // These three have buttons that with some localizations are wider then the |
| // view. So the three 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"; |
| |
| // 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]); |
| |
| // Find the most any row changed in size. |
| CGFloat maxWidthChange = std::max(defaultBrowserChange.width, themeRowChange); |
| |
| // 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); |
| verticalShift += AutoSizeGroup(basicsGroupSearchEngine_, |
| kAutoSizeGroupBehaviorHorizontalFirstGrows, |
| 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]; |
| NSRect frame = [prefsWindow frame]; |
| frame.size.width = newWidth; |
| [prefsWindow setFrame:frame display:NO]; |
| |
| // The Under the Hood prefs is a scroller, it shouldn't get any border, so it |
| // gets resized to the 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)]; |
| |
| [self switchToPage:initialPage_ animate:NO]; |
| |
| // TODO(pinkerton): save/restore position based on prefs. |
| [[self window] center]; |
| } |
| |
| - (void)dealloc { |
| if (syncService_) { |
| syncService_->RemoveObserver(observer_.get()); |
| } |
| [customPagesSource_ removeObserver:self forKeyPath:@"customHomePages"]; |
| [[NSNotificationCenter defaultCenter] removeObserver:self]; |
| [self unregisterPrefObservers]; |
| [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 |
| prefs_->AddPrefObserver(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()); |
| showPageOptionButtons_.Init(prefs::kShowPageOptionsButtons, prefs_, |
| observer_.get()); |
| // TODO(pinkerton): Register Default search. |
| |
| // Personal Stuff panel |
| askSavePasswords_.Init(prefs::kPasswordManagerEnabled, |
| prefs_, observer_.get()); |
| formAutofill_.Init(prefs::kFormAutofillEnabled, 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()); |
| |
| // 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_; |
| metricsRecording_.Init(prefs::kMetricsReportingEnabled, |
| local, observer_.get()); |
| cookieBehavior_.Init(prefs::kCookieBehavior, prefs_, 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); |
| } |
| |
| // Clean up what was registered in -registerPrefObservers. We only have to |
| // clean up the non-PrefMember registrations. |
| - (void)unregisterPrefObservers { |
| if (!prefs_) return; |
| |
| // Basics |
| prefs_->RemovePrefObserver(prefs::kURLsToRestoreOnStartup, observer_.get()); |
| |
| // User Data panel |
| // Nothing to do here. |
| |
| // TODO(pinkerton): do other panels... |
| } |
| |
| // Called when a key we're observing via KVO changes. |
| - (void)observeValueForKeyPath:(NSString*)keyPath |
| ofObject:(id)object |
| change:(NSDictionary*)change |
| context:(void*)context { |
| if ([keyPath isEqualToString:@"customHomePages"]) { |
| [self customHomePagesChanged]; |
| return; |
| } |
| [super observeValueForKeyPath:keyPath |
| ofObject:object |
| change:change |
| context:context]; |
| } |
| |
| // 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 char*)action { |
| UserMetrics::RecordComputedAction(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"]; |
| } else if ([key isEqualToString:@"enableRestoreButtons"]) { |
| paths = [paths setByAddingObject:@"restoreOnStartupIndex"]; |
| } 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 string is passed in we revert to using NewTab page as the Home page. |
| // When setting the Home Page to NewTab page, we preserve the old value of |
| // kHomePage (we don't overwrite it). 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 std::string&)homepage { |
| if (homepage.empty() || homepage == GetNewTabUIURLString()) { |
| newTabPageIsHomePage_.SetValue(true); |
| } else { |
| newTabPageIsHomePage_.SetValue(false); |
| homepage_.SetValue(UTF8ToWide(homepage)); |
| } |
| } |
| |
| // 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::wstring*)prefName { |
| if (*prefName == prefs::kRestoreOnStartup) { |
| const SessionStartupPref startupPref = |
| SessionStartupPref::GetStartupPref(prefs_); |
| [self setRestoreOnStartupIndex:startupPref.type]; |
| } |
| |
| // TODO(beng): Note that the kURLsToRestoreOnStartup pref is a mutable list, |
| // and changes to mutable lists aren't broadcast through the |
| // observer system, so this condition will |
| // never match. Once support for broadcasting such updates is |
| // added, this will automagically start to work, and this comment |
| // can be removed. |
| if (*prefName == prefs::kURLsToRestoreOnStartup) { |
| const SessionStartupPref startupPref = |
| SessionStartupPref::GetStartupPref(prefs_); |
| [customPagesSource_ setURLs:startupPref.urls]; |
| } |
| |
| if (*prefName == prefs::kHomePageIsNewTabPage) { |
| NSInteger useNewTabPage = newTabPageIsHomePage_.GetValue() ? 0 : 1; |
| [self setNewTabPageIsHomePageIndex:useNewTabPage]; |
| } |
| if (*prefName == prefs::kHomePage) { |
| NSString* value = base::SysWideToNSString(homepage_.GetValue()); |
| [self setHomepageURL:value]; |
| } |
| |
| if (*prefName == prefs::kShowHomeButton) { |
| [self setShowHomeButton:showHomeButton_.GetValue() ? YES : NO]; |
| } |
| if (*prefName == prefs::kShowPageOptionsButtons) { |
| [self setShowPageOptionsButtons:showPageOptionButtons_.GetValue() ? |
| YES : NO]; |
| } |
| } |
| |
| // 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); |
| } |
| |
| // Called when the custom home pages array changes. Force a save to prefs, but |
| // in order to save it, we have to look up what the current radio button |
| // setting is (since they're set together). What a pain. |
| - (void)customHomePagesChanged { |
| const SessionStartupPref pref = SessionStartupPref::GetStartupPref(prefs_); |
| [self saveSessionStartupWithType:pref.type]; |
| } |
| |
| // Called when an entry in the custom home page array changes URLs. Force |
| // a save to prefs. |
| - (void)homepageEntryChanged:(NSNotification*)notify { |
| [self customHomePagesChanged]; |
| } |
| |
| // 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:"Options_Startup_Homepage"]; |
| break; |
| case SessionStartupPref::LAST: |
| [self recordUserAction:"Options_Startup_LastSession"]; |
| break; |
| case SessionStartupPref::URLS: |
| [self recordUserAction:"Options_Startup_Custom"]; |
| break; |
| default: |
| NOTREACHED(); |
| } |
| [self saveSessionStartupWithType:startupType]; |
| } |
| |
| // Returns whether or not the +/-/Current buttons should be enabled, based on |
| // the current pref value for the startup urls. |
| - (BOOL)enableRestoreButtons { |
| return [self restoreOnStartupIndex] == SessionStartupPref::URLS; |
| } |
| |
| // Getter for the |customPagesSource| property for bindings. |
| - (id)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 }; |
| |
| // 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() ? |
| 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:"Options_Homepage_UseNewTab"]; |
| else |
| [self recordUserAction:"Options_Homepage_UseURL"]; |
| newTabPageIsHomePage_.SetValue(useNewTabPage); |
| } |
| |
| // 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() ? NO : YES; |
| } |
| |
| // Returns the homepage URL. |
| - (NSString*)homepageURL { |
| NSString* value = base::SysWideToNSString(homepage_.GetValue()); |
| return 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"). |
| if (!urlString) |
| urlString = [NSString stringWithFormat:@"%s", chrome::kChromeUINewTabURL]; |
| std::string temp = base::SysNSStringToUTF8(urlString); |
| std::string fixedString = URLFixerUpper::FixupURL(temp, std::string()); |
| if (GURL(fixedString).is_valid()) |
| [self setHomepage:fixedString]; |
| } |
| |
| // 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:"Options_Homepage_ShowHomeButton"]; |
| else |
| [self recordUserAction:"Options_Homepage_HideHomeButton"]; |
| showHomeButton_.SetValue(value ? true : false); |
| } |
| |
| // Returns whether the page and options button should be checked based on the |
| // preference. |
| - (BOOL)showPageOptionsButtons { |
| return showPageOptionButtons_.GetValue() ? YES : NO; |
| } |
| |
| // Sets the backend pref for whether or not the page and options buttons should |
| // be displayed based on |value|. |
| - (void)setShowPageOptionsButtons:(BOOL)value { |
| if (value) |
| [self recordUserAction:"Options_Homepage_ShowPageOptionsButtons"]; |
| else |
| [self recordUserAction:"Options_Homepage_HidePageOptionsButtons"]; |
| showPageOptionButtons_.SetValue(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:"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]]; |
| } |
| |
| - (IBAction)manageSearchEngines:(id)sender { |
| [KeywordEditorCocoaController showKeywordEditor:profile_]; |
| } |
| |
| // 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:"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; |
| std::wstring text = |
| l10n_util::GetStringF(stringId, l10n_util::GetString(IDS_PRODUCT_NAME)); |
| return base::SysWideToNSString(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::wstring*)prefName { |
| if (*prefName == prefs::kPasswordManagerEnabled) { |
| [self setPasswordManagerEnabledIndex:askSavePasswords_.GetValue() ? |
| kEnabledIndex : kDisabledIndex]; |
| } |
| if (*prefName == prefs::kFormAutofillEnabled) { |
| [self setFormAutofillEnabledIndex:formAutofill_.GetValue() ? |
| kEnabledIndex : kDisabledIndex]; |
| } |
| 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:"Options_ShowPasswordsExceptions"]; |
| [self launchKeychainAccess]; |
| } |
| |
| // Called to import data from other browsers (Safari, Firefox, etc). |
| - (IBAction)importData:(id)sender { |
| NOTIMPLEMENTED(); |
| } |
| |
| // 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_]; |
| } |
| |
| - (IBAction)resetThemeToDefault:(id)sender { |
| [self recordUserAction:"Options_ThemesReset"]; |
| profile_->ClearTheme(); |
| } |
| |
| - (IBAction)themesGallery:(id)sender { |
| [self recordUserAction:"Options_ThemesGallery"]; |
| Browser* browser = |
| BrowserList::FindBrowserWithType(profile_, Browser::TYPE_NORMAL); |
| |
| if (!browser || !browser->GetSelectedTabContents()) { |
| browser = Browser::Create(profile_); |
| browser->OpenURL( |
| GURL(l10n_util::GetStringUTF8(IDS_THEMES_GALLERY_URL)), |
| GURL(), NEW_WINDOW, PageTransition::LINK); |
| } else { |
| browser->OpenURL( |
| GURL(l10n_util::GetStringUTF8(IDS_THEMES_GALLERY_URL)), |
| GURL(), NEW_FOREGROUND_TAB, PageTransition::LINK); |
| } |
| } |
| |
| // 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_); |
| 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_); |
| 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_BUTTON_LABEL)]; |
| [alert setInformativeText:l10n_util::GetNSStringWithFixup( |
| IDS_SYNC_STOP_SYNCING_EXPLANATION_LABEL)]; |
| [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_->EnableForUser(); |
| ProfileSyncService::SyncEvent(ProfileSyncService::START_FROM_OPTIONS); |
| } |
| } |
| |
| - (IBAction)doSyncReauthentication:(id)sender { |
| DCHECK(syncService_); |
| syncService_->ShowLoginDialog(); |
| } |
| |
| - (void)setPasswordManagerEnabledIndex:(NSInteger)value { |
| if (value == kEnabledIndex) |
| [self recordUserAction:"Options_PasswordManager_Enable"]; |
| else |
| [self recordUserAction:"Options_PasswordManager_Disable"]; |
| askSavePasswords_.SetValue(value == kEnabledIndex ? true : false); |
| } |
| |
| - (NSInteger)passwordManagerEnabledIndex { |
| return askSavePasswords_.GetValue() ? kEnabledIndex : kDisabledIndex; |
| } |
| |
| - (void)setFormAutofillEnabledIndex:(NSInteger)value { |
| if (value == kEnabledIndex) |
| [self recordUserAction:"Options_FormAutofill_Enable"]; |
| else |
| [self recordUserAction:"Options_FormAutofill_Disable"]; |
| formAutofill_.SetValue(value == kEnabledIndex ? true : false); |
| } |
| |
| - (NSInteger)formAutofillEnabledIndex { |
| return formAutofill_.GetValue() ? kEnabledIndex : kDisabledIndex; |
| } |
| |
| - (void)setIsUsingDefaultTheme:(BOOL)value { |
| if (value) |
| [self recordUserAction:"Options_IsUsingDefaultTheme_Enable"]; |
| else |
| [self recordUserAction:"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::wstring*)prefName { |
| if (*prefName == prefs::kAlternateErrorPagesEnabled) { |
| [self setShowAlternateErrorPages: |
| alternateErrorPages_.GetValue() ? YES : NO]; |
| } |
| else if (*prefName == prefs::kSearchSuggestEnabled) { |
| [self setUseSuggest:useSuggest_.GetValue() ? YES : NO]; |
| } |
| else if (*prefName == prefs::kDnsPrefetchingEnabled) { |
| [self setDnsPrefetch:dnsPrefetch_.GetValue() ? YES : NO]; |
| } |
| else if (*prefName == prefs::kSafeBrowsingEnabled) { |
| [self setSafeBrowsing:safeBrowsing_.GetValue() ? YES : NO]; |
| } |
| else if (*prefName == prefs::kMetricsReportingEnabled) { |
| [self setMetricsRecording:metricsRecording_.GetValue() ? YES : NO]; |
| } |
| else if (*prefName == prefs::kCookieBehavior) { |
| [self setCookieBehavior:cookieBehavior_.GetValue()]; |
| } |
| else if (*prefName == prefs::kPromptForDownload) { |
| [self setAskForSaveLocation:askForSaveLocation_.GetValue() ? YES : NO]; |
| } |
| } |
| |
| // 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:"Options_SetDownloadDirectory"]; |
| NSURL* path = [[panel URLs] lastObject]; // We only allow 1 item. |
| [self willChangeValueForKey:@"defaultDownloadLocation"]; |
| defaultDownloadLocation_.SetValue(base::SysNSStringToWide([path path])); |
| [self didChangeValueForKey:@"defaultDownloadLocation"]; |
| } |
| } |
| |
| // Shows the cookies controller. |
| - (IBAction)showCookies:(id)sender { |
| // The controller will clean itself up. |
| BrowsingDataLocalStorageHelper* storageHelper = |
| new BrowsingDataLocalStorageHelper(profile_); |
| CookiesWindowController* controller = |
| [[CookiesWindowController alloc] initWithProfile:profile_ |
| storageHelper:storageHelper]; |
| [controller attachSheetTo:[self window]]; |
| } |
| |
| // 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::SysWideToNSString(defaultDownloadLocation_.GetValue()); |
| [panel beginSheetForDirectory:path |
| file:nil |
| types:nil |
| modalForWindow:[self window] |
| modalDelegate:self |
| didEndSelector:@selector(downloadPathPanelDidEnd:code:context:) |
| contextInfo:NULL]; |
| } |
| |
| - (IBAction)privacyLearnMore:(id)sender { |
| // We open a new browser window so the Options dialog doesn't get lost |
| // behind other windows. |
| Browser* browser = Browser::Create(profile_); |
| browser->OpenURL(GURL(l10n_util::GetStringUTF16(IDS_LEARN_MORE_PRIVACY_URL)), |
| GURL(), NEW_WINDOW, PageTransition::LINK); |
| } |
| |
| // 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:"Options_LinkDoctorCheckbox_Enable"]; |
| else |
| [self recordUserAction:"Options_LinkDoctorCheckbox_Disable"]; |
| alternateErrorPages_.SetValue(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:"Options_UseSuggestCheckbox_Enable"]; |
| else |
| [self recordUserAction:"Options_UseSuggestCheckbox_Disable"]; |
| useSuggest_.SetValue(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:"Options_DnsPrefetchCheckbox_Enable"]; |
| else |
| [self recordUserAction:"Options_DnsPrefetchCheckbox_Disable"]; |
| dnsPrefetch_.SetValue(value ? true : false); |
| chrome_browser_net::EnableDnsPrefetch(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:"Options_SafeBrowsingCheckbox_Enable"]; |
| else |
| [self recordUserAction:"Options_SafeBrowsingCheckbox_Disable"]; |
| bool enabled = value ? true : false; |
| safeBrowsing_.SetValue(enabled); |
| SafeBrowsingService* safeBrowsingService = |
| g_browser_process->resource_dispatcher_host()->safe_browsing_service(); |
| MessageLoop::current()->PostTask(FROM_HERE, NewRunnableMethod( |
| safeBrowsingService, &SafeBrowsingService::OnEnable, enabled)); |
| } |
| |
| // Returns whether the suggest checkbox should be checked based on the |
| // preference. |
| - (BOOL)metricsRecording { |
| return metricsRecording_.GetValue() ? YES : NO; |
| } |
| |
| // Sets the backend pref for whether or not the suggest checkbox should be |
| // displayed based on |value|. |
| - (void)setMetricsRecording:(BOOL)value { |
| if (value) |
| [self recordUserAction:"Options_MetricsReportingCheckbox_Enable"]; |
| else |
| [self recordUserAction:"Options_MetricsReportingCheckbox_Disable"]; |
| bool enabled = value ? true : false; |
| |
| GoogleUpdateSettings::SetCollectStatsConsent(enabled); |
| bool update_pref = GoogleUpdateSettings::GetCollectStatsConsent(); |
| if (enabled != update_pref) { |
| DLOG(INFO) << |
| "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(); |
| } |
| |
| // TODO(pinkerton): windows shows a dialog here telling the user they need to |
| // restart for this to take effect. Is that really necessary? |
| metricsRecording_.SetValue(enabled); |
| } |
| |
| // Returns the index of the cookie popup based on the preference. |
| - (NSInteger)cookieBehavior { |
| return cookieBehavior_.GetValue(); |
| } |
| |
| // Sets the backend pref for whether or not to accept cookies based on |index|. |
| - (void)setCookieBehavior:(NSInteger)index { |
| net::CookiePolicy::Type policy = net::CookiePolicy::ALLOW_ALL_COOKIES; |
| if (net::CookiePolicy::ValidType(index)) |
| policy = net::CookiePolicy::FromInt(index); |
| const char* kUserMetrics[] = { |
| "Options_AllowAllCookies", |
| "Options_BlockThirdPartyCookies", |
| "Options_BlockAllCookies" |
| }; |
| DCHECK(policy >= 0 && (unsigned int)policy < arraysize(kUserMetrics)); |
| [self recordUserAction:kUserMetrics[policy]]; |
| cookieBehavior_.SetValue(policy); |
| } |
| |
| - (NSURL*)defaultDownloadLocation { |
| NSString* pathString = |
| base::SysWideToNSString(defaultDownloadLocation_.GetValue()); |
| return [NSURL fileURLWithPath:pathString]; |
| } |
| |
| - (BOOL)askForSaveLocation { |
| return askForSaveLocation_.GetValue(); |
| } |
| |
| - (void)setAskForSaveLocation:(BOOL)value { |
| if (value) { |
| [self recordUserAction:"Options_AskForSaveLocation_Enable"]; |
| } else { |
| [self recordUserAction:"Options_AskForSaveLocation_Disable"]; |
| } |
| askForSaveLocation_.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:"Options_ManagerCerts"]; |
| [self launchKeychainAccess]; |
| } |
| |
| //------------------------------------------------------------------------- |
| |
| // Callback when preferences are changed. |prefName| is the name of the |
| // pref that has changed and should not be NULL. |
| - (void)prefChanged:(std::wstring*)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_); |
| |
| [syncButton_ setEnabled:!syncService_->WizardIsVisible()]; |
| NSString* buttonLabel; |
| if (syncService_->HasSyncSetupCompleted()) { |
| buttonLabel = l10n_util::GetNSStringWithFixup( |
| IDS_SYNC_STOP_SYNCING_BUTTON_LABEL); |
| } else if (syncService_->SetupInProgress()) { |
| buttonLabel = l10n_util::GetNSStringWithFixup( |
| IDS_SYNC_NTP_SETUP_IN_PROGRESS); |
| } else { |
| buttonLabel = l10n_util::GetNSStringWithFixup( |
| IDS_SYNC_START_SYNC_BUTTON_LABEL); |
| } |
| [syncButton_ setTitle:buttonLabel]; |
| |
| string16 statusLabel, linkLabel; |
| sync_ui_util::MessageType status = |
| sync_ui_util::GetStatusLabels(syncService_, &statusLabel, &linkLabel); |
| [syncStatus_ setStringValue:base::SysUTF16ToNSString(statusLabel)]; |
| [syncLink_ setHidden:linkLabel.empty()]; |
| [syncLink_ setTitle:base::SysUTF16ToNSString(linkLabel)]; |
| |
| 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]; |
| |
| // Normally there is only one view, but if the user clicks really quickly, the |
| // animation could still been running, and the last view is the one that was |
| // animating in. |
| NSArray* subviews = [contentView subviews]; |
| NSView* currentPrefsView = nil; |
| if ([subviews count]) { |
| currentPrefsView = [subviews lastObject]; |
| } |
| |
| // Make sure we aren't being told to display the same thing again. |
| if (currentPrefsView == prefsView) { |
| return; |
| } |
| |
| // Remember new options page as current page. |
| if (page != OPTIONS_PAGE_DEFAULT) |
| lastSelectedPage_.SetValue(page); |
| |
| // Stop any running animation, and remove any past views that were on the way |
| // out. |
| [animation_ stopAnimation]; |
| if ([subviews count]) { |
| RemoveAllButLastView(subviews); |
| } |
| |
| NSRect prefsViewFrame = [prefsView frame]; |
| NSRect contentViewFrame = [contentView frame]; |
| 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); |
| } |
| [prefsView setFrame:prefsViewFrame]; |
| |
| // Add the view. |
| [contentView addSubview:prefsView]; |
| [prefsWindow setInitialFirstResponder:prefsView]; |
| |
| // Update the window title. |
| NSToolbarItem* toolbarItem = [self getToolbarItemForPage:page]; |
| [prefsWindow setTitle:[toolbarItem label]]; |
| |
| // Figure out the size of the window. |
| NSRect windowFrame = [prefsWindow frame]; |
| CGFloat titleToolbarHeight = |
| NSHeight(windowFrame) - NSHeight(contentViewFrame); |
| windowFrame.size.height = |
| NSHeight(prefsViewFrame) + titleToolbarHeight; |
| DCHECK_GE(NSWidth(windowFrame), NSWidth(prefsViewFrame)) |
| << "Initial width set wasn't wide enough."; |
| windowFrame.origin.y = NSMaxY([prefsWindow frame]) - NSHeight(windowFrame); |
| |
| // Now change the size. |
| if (animate) { |
| NSDictionary* oldViewOut = |
| [NSDictionary dictionaryWithObjectsAndKeys: |
| currentPrefsView, NSViewAnimationTargetKey, |
| NSViewAnimationFadeOutEffect, NSViewAnimationEffectKey, |
| nil]; |
| NSDictionary* newViewIn = |
| [NSDictionary dictionaryWithObjectsAndKeys: |
| prefsView, NSViewAnimationTargetKey, |
| NSViewAnimationFadeInEffect, NSViewAnimationEffectKey, |
| nil]; |
| NSDictionary* windowResize = |
| [NSDictionary dictionaryWithObjectsAndKeys: |
| prefsWindow, NSViewAnimationTargetKey, |
| [NSValue valueWithRect:windowFrame], NSViewAnimationEndFrameKey, |
| nil]; |
| [animation_ setViewAnimations: |
| [NSArray arrayWithObjects:oldViewOut, newViewIn, windowResize, nil]]; |
| [animation_ startAnimation]; |
| } else { |
| [currentPrefsView removeFromSuperviewWithoutNeedingDisplay]; |
| // If not animating, odds are we don't want to display either (because it |
| // is initial window setup). |
| [prefsWindow setFrame:windowFrame display:NO]; |
| } |
| } |
| |
| - (void)animationDidEnd:(NSAnimation*)animation { |
| DCHECK_EQ(animation_.get(), animation); |
| // Animation finished, remove everything but the view we just added (it will |
| // be last in the list). |
| NSArray* subviews = [[[self window] contentView] subviews]; |
| RemoveAllButLastView(subviews); |
| } |
| |
| - (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 |