| OLD | NEW |
| (Empty) |
| 1 // Copyright (c) 2011 The Chromium Authors. All rights reserved. | |
| 2 // Use of this source code is governed by a BSD-style license that can be | |
| 3 // found in the LICENSE file. | |
| 4 | |
| 5 #import "chrome/browser/ui/cocoa/options/preferences_window_controller.h" | |
| 6 | |
| 7 #include <algorithm> | |
| 8 | |
| 9 #include "base/logging.h" | |
| 10 #include "base/mac/mac_util.h" | |
| 11 #include "base/mac/scoped_aedesc.h" | |
| 12 #include "base/string16.h" | |
| 13 #include "base/string_util.h" | |
| 14 #include "base/sys_string_conversions.h" | |
| 15 #include "chrome/browser/autofill/autofill_dialog.h" | |
| 16 #include "chrome/browser/autofill/autofill_type.h" | |
| 17 #include "chrome/browser/autofill/personal_data_manager.h" | |
| 18 #include "chrome/browser/browser_process.h" | |
| 19 #include "chrome/browser/download/download_manager.h" | |
| 20 #include "chrome/browser/download/download_prefs.h" | |
| 21 #include "chrome/browser/extensions/extension_service.h" | |
| 22 #include "chrome/browser/google/google_util.h" | |
| 23 #include "chrome/browser/instant/instant_confirm_dialog.h" | |
| 24 #include "chrome/browser/instant/instant_controller.h" | |
| 25 #include "chrome/browser/metrics/metrics_service.h" | |
| 26 #include "chrome/browser/metrics/user_metrics.h" | |
| 27 #include "chrome/browser/net/url_fixer_upper.h" | |
| 28 #include "chrome/browser/policy/managed_prefs_banner_base.h" | |
| 29 #include "chrome/browser/prefs/pref_service.h" | |
| 30 #include "chrome/browser/prefs/session_startup_pref.h" | |
| 31 #include "chrome/browser/profiles/profile.h" | |
| 32 #include "chrome/browser/safe_browsing/safe_browsing_service.h" | |
| 33 #include "chrome/browser/shell_integration.h" | |
| 34 #include "chrome/browser/sync/profile_sync_service.h" | |
| 35 #include "chrome/browser/sync/sync_ui_util.h" | |
| 36 #include "chrome/browser/ui/browser.h" | |
| 37 #include "chrome/browser/ui/browser_list.h" | |
| 38 #import "chrome/browser/ui/cocoa/clear_browsing_data_controller.h" | |
| 39 #import "chrome/browser/ui/cocoa/importer/import_dialog_cocoa.h" | |
| 40 #import "chrome/browser/ui/cocoa/l10n_util.h" | |
| 41 #import "chrome/browser/ui/cocoa/options/content_settings_dialog_controller.h" | |
| 42 #import "chrome/browser/ui/cocoa/options/custom_home_pages_model.h" | |
| 43 #import "chrome/browser/ui/cocoa/options/font_language_settings_controller.h" | |
| 44 #import "chrome/browser/ui/cocoa/options/keyword_editor_cocoa_controller.h" | |
| 45 #import "chrome/browser/ui/cocoa/options/search_engine_list_model.h" | |
| 46 #import "chrome/browser/ui/cocoa/vertical_gradient_view.h" | |
| 47 #import "chrome/browser/ui/cocoa/window_size_autosaver.h" | |
| 48 #include "chrome/browser/ui/options/options_util.h" | |
| 49 #include "chrome/browser/ui/options/options_window.h" | |
| 50 #include "chrome/browser/ui/options/show_options_url.h" | |
| 51 #include "chrome/common/notification_details.h" | |
| 52 #include "chrome/common/notification_observer.h" | |
| 53 #include "chrome/common/notification_type.h" | |
| 54 #include "chrome/common/pref_names.h" | |
| 55 #include "chrome/common/url_constants.h" | |
| 56 #include "chrome/installer/util/google_update_settings.h" | |
| 57 #include "content/browser/renderer_host/resource_dispatcher_host.h" | |
| 58 #include "content/browser/tab_contents/tab_contents.h" | |
| 59 #include "grit/chromium_strings.h" | |
| 60 #include "grit/generated_resources.h" | |
| 61 #include "grit/locale_settings.h" | |
| 62 #include "grit/theme_resources.h" | |
| 63 #import "third_party/GTM/AppKit/GTMNSAnimation+Duration.h" | |
| 64 #import "third_party/GTM/AppKit/GTMUILocalizerAndLayoutTweaker.h" | |
| 65 #include "ui/base/l10n/l10n_util.h" | |
| 66 #include "ui/base/l10n/l10n_util_mac.h" | |
| 67 #include "ui/base/resource/resource_bundle.h" | |
| 68 #include "ui/gfx/image.h" | |
| 69 | |
| 70 namespace { | |
| 71 | |
| 72 // Colors for the managed preferences warning banner. | |
| 73 static const double kBannerGradientColorTop[3] = | |
| 74 {255.0 / 255.0, 242.0 / 255.0, 183.0 / 255.0}; | |
| 75 static const double kBannerGradientColorBottom[3] = | |
| 76 {250.0 / 255.0, 230.0 / 255.0, 145.0 / 255.0}; | |
| 77 static const double kBannerStrokeColor = 135.0 / 255.0; | |
| 78 | |
| 79 // Tag id for retrieval via viewWithTag in NSView (from IB). | |
| 80 static const uint32 kBasicsStartupPageTableTag = 1000; | |
| 81 | |
| 82 bool IsNewTabUIURLString(const GURL& url) { | |
| 83 return url == GURL(chrome::kChromeUINewTabURL); | |
| 84 } | |
| 85 | |
| 86 // Helper that sizes two buttons to fit in a row keeping their spacing, returns | |
| 87 // the total horizontal size change. | |
| 88 CGFloat SizeToFitButtonPair(NSButton* leftButton, NSButton* rightButton) { | |
| 89 CGFloat widthShift = 0.0; | |
| 90 | |
| 91 NSSize delta = [GTMUILocalizerAndLayoutTweaker sizeToFitView:leftButton]; | |
| 92 DCHECK_EQ(delta.height, 0.0) << "Height changes unsupported"; | |
| 93 widthShift += delta.width; | |
| 94 | |
| 95 if (widthShift != 0.0) { | |
| 96 NSPoint origin = [rightButton frame].origin; | |
| 97 origin.x += widthShift; | |
| 98 [rightButton setFrameOrigin:origin]; | |
| 99 } | |
| 100 delta = [GTMUILocalizerAndLayoutTweaker sizeToFitView:rightButton]; | |
| 101 DCHECK_EQ(delta.height, 0.0) << "Height changes unsupported"; | |
| 102 widthShift += delta.width; | |
| 103 | |
| 104 return widthShift; | |
| 105 } | |
| 106 | |
| 107 // The different behaviors for the "pref group" auto sizing. | |
| 108 enum AutoSizeGroupBehavior { | |
| 109 kAutoSizeGroupBehaviorVerticalToFit, | |
| 110 kAutoSizeGroupBehaviorVerticalFirstToFit, | |
| 111 kAutoSizeGroupBehaviorHorizontalToFit, | |
| 112 kAutoSizeGroupBehaviorHorizontalFirstGrows, | |
| 113 kAutoSizeGroupBehaviorFirstTwoAsRowVerticalToFit | |
| 114 }; | |
| 115 | |
| 116 // Helper to tweak the layout of the "pref groups" and also ripple any height | |
| 117 // changes from one group to the next groups' origins. | |
| 118 // |views| is an ordered list of views with first being the label for the | |
| 119 // group and the rest being top down or left to right ordering of the views. | |
| 120 // The label is assumed to already be the same height as all the views it is | |
| 121 // next too. | |
| 122 CGFloat AutoSizeGroup(NSArray* views, AutoSizeGroupBehavior behavior, | |
| 123 CGFloat verticalShift) { | |
| 124 DCHECK_GE([views count], 2U) << "Should be at least a label and a control"; | |
| 125 NSTextField* label = [views objectAtIndex:0]; | |
| 126 DCHECK([label isKindOfClass:[NSTextField class]]) | |
| 127 << "First view should be the label for the group"; | |
| 128 | |
| 129 // Auto size the label to see if we need more vertical space for its localized | |
| 130 // string. | |
| 131 CGFloat labelHeightChange = | |
| 132 [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:label]; | |
| 133 | |
| 134 CGFloat localVerticalShift = 0.0; | |
| 135 switch (behavior) { | |
| 136 case kAutoSizeGroupBehaviorVerticalToFit: { | |
| 137 // Walk bottom up doing the sizing and moves. | |
| 138 for (NSUInteger index = [views count] - 1; index > 0; --index) { | |
| 139 NSView* view = [views objectAtIndex:index]; | |
| 140 NSSize delta = cocoa_l10n_util::WrapOrSizeToFit(view); | |
| 141 DCHECK_GE(delta.height, 0.0) << "Should NOT shrink in height"; | |
| 142 if (localVerticalShift) { | |
| 143 NSPoint origin = [view frame].origin; | |
| 144 origin.y += localVerticalShift; | |
| 145 [view setFrameOrigin:origin]; | |
| 146 } | |
| 147 localVerticalShift += delta.height; | |
| 148 } | |
| 149 break; | |
| 150 } | |
| 151 case kAutoSizeGroupBehaviorVerticalFirstToFit: { | |
| 152 // Just size the top one. | |
| 153 NSView* view = [views objectAtIndex:1]; | |
| 154 NSSize delta = cocoa_l10n_util::WrapOrSizeToFit(view); | |
| 155 DCHECK_GE(delta.height, 0.0) << "Should NOT shrink in height"; | |
| 156 localVerticalShift += delta.height; | |
| 157 break; | |
| 158 } | |
| 159 case kAutoSizeGroupBehaviorHorizontalToFit: { | |
| 160 // Walk left to right doing the sizing and moves. | |
| 161 // NOTE: Don't worry about vertical, assume it always fits. | |
| 162 CGFloat horizontalShift = 0.0; | |
| 163 NSUInteger count = [views count]; | |
| 164 for (NSUInteger index = 1; index < count; ++index) { | |
| 165 NSView* view = [views objectAtIndex:index]; | |
| 166 NSSize delta = cocoa_l10n_util::WrapOrSizeToFit(view); | |
| 167 DCHECK_GE(delta.height, 0.0) << "Should NOT shrink in height"; | |
| 168 if (horizontalShift) { | |
| 169 NSPoint origin = [view frame].origin; | |
| 170 origin.x += horizontalShift; | |
| 171 [view setFrameOrigin:origin]; | |
| 172 } | |
| 173 horizontalShift += delta.width; | |
| 174 } | |
| 175 break; | |
| 176 } | |
| 177 case kAutoSizeGroupBehaviorHorizontalFirstGrows: { | |
| 178 // Walk right to left doing the sizing and moves, then apply the space | |
| 179 // collected into the first. | |
| 180 // NOTE: Don't worry about vertical, assume it always all fits. | |
| 181 CGFloat horizontalShift = 0.0; | |
| 182 for (NSUInteger index = [views count] - 1; index > 1; --index) { | |
| 183 NSView* view = [views objectAtIndex:index]; | |
| 184 NSSize delta = cocoa_l10n_util::WrapOrSizeToFit(view); | |
| 185 DCHECK_GE(delta.height, 0.0) << "Should NOT shrink in height"; | |
| 186 horizontalShift -= delta.width; | |
| 187 NSPoint origin = [view frame].origin; | |
| 188 origin.x += horizontalShift; | |
| 189 [view setFrameOrigin:origin]; | |
| 190 } | |
| 191 if (horizontalShift) { | |
| 192 NSView* view = [views objectAtIndex:1]; | |
| 193 NSSize delta = NSMakeSize(horizontalShift, 0.0); | |
| 194 [GTMUILocalizerAndLayoutTweaker | |
| 195 resizeViewWithoutAutoResizingSubViews:view | |
| 196 delta:delta]; | |
| 197 } | |
| 198 break; | |
| 199 } | |
| 200 case kAutoSizeGroupBehaviorFirstTwoAsRowVerticalToFit: { | |
| 201 // Start out like kAutoSizeGroupBehaviorVerticalToFit but don't do | |
| 202 // the first two. Then handle the two as a row, but apply any | |
| 203 // vertical shift. | |
| 204 // All but the first two (in the row); walk bottom up. | |
| 205 for (NSUInteger index = [views count] - 1; index > 2; --index) { | |
| 206 NSView* view = [views objectAtIndex:index]; | |
| 207 NSSize delta = cocoa_l10n_util::WrapOrSizeToFit(view); | |
| 208 DCHECK_GE(delta.height, 0.0) << "Should NOT shrink in height"; | |
| 209 if (localVerticalShift) { | |
| 210 NSPoint origin = [view frame].origin; | |
| 211 origin.y += localVerticalShift; | |
| 212 [view setFrameOrigin:origin]; | |
| 213 } | |
| 214 localVerticalShift += delta.height; | |
| 215 } | |
| 216 // Deal with the two for the horizontal row. Size the second one. | |
| 217 CGFloat horizontalShift = 0.0; | |
| 218 NSView* view = [views objectAtIndex:2]; | |
| 219 NSSize delta = cocoa_l10n_util::WrapOrSizeToFit(view); | |
| 220 DCHECK_GE(delta.height, 0.0) << "Should NOT shrink in height"; | |
| 221 horizontalShift -= delta.width; | |
| 222 NSPoint origin = [view frame].origin; | |
| 223 origin.x += horizontalShift; | |
| 224 if (localVerticalShift) { | |
| 225 origin.y += localVerticalShift; | |
| 226 } | |
| 227 [view setFrameOrigin:origin]; | |
| 228 // Now expand the first item in the row to consume the space opened up. | |
| 229 view = [views objectAtIndex:1]; | |
| 230 if (horizontalShift) { | |
| 231 NSSize delta = NSMakeSize(horizontalShift, 0.0); | |
| 232 [GTMUILocalizerAndLayoutTweaker | |
| 233 resizeViewWithoutAutoResizingSubViews:view | |
| 234 delta:delta]; | |
| 235 } | |
| 236 // And move it up by any amount needed from the previous items. | |
| 237 if (localVerticalShift) { | |
| 238 NSPoint origin = [view frame].origin; | |
| 239 origin.y += localVerticalShift; | |
| 240 [view setFrameOrigin:origin]; | |
| 241 } | |
| 242 break; | |
| 243 } | |
| 244 default: | |
| 245 NOTREACHED(); | |
| 246 break; | |
| 247 } | |
| 248 | |
| 249 // If the label grew more then the views, the other views get an extra shift. | |
| 250 // Otherwise, move the label to its top is aligned with the other views. | |
| 251 CGFloat nonLabelShift = 0.0; | |
| 252 if (labelHeightChange > localVerticalShift) { | |
| 253 // Since the lable is taller, centering the other views looks best, just | |
| 254 // shift the views by 1/2 of the size difference. | |
| 255 nonLabelShift = (labelHeightChange - localVerticalShift) / 2.0; | |
| 256 } else { | |
| 257 NSPoint origin = [label frame].origin; | |
| 258 origin.y += localVerticalShift - labelHeightChange; | |
| 259 [label setFrameOrigin:origin]; | |
| 260 } | |
| 261 | |
| 262 // Apply the input shift requested along with any the shift from label being | |
| 263 // taller then the rest of the group. | |
| 264 for (NSView* view in views) { | |
| 265 NSPoint origin = [view frame].origin; | |
| 266 origin.y += verticalShift; | |
| 267 if (view != label) { | |
| 268 origin.y += nonLabelShift; | |
| 269 } | |
| 270 [view setFrameOrigin:origin]; | |
| 271 } | |
| 272 | |
| 273 // Return how much the group grew. | |
| 274 return localVerticalShift + nonLabelShift; | |
| 275 } | |
| 276 | |
| 277 // Helper to remove a view and move everything above it down to take over the | |
| 278 // space. | |
| 279 void RemoveViewFromView(NSView* view, NSView* toRemove) { | |
| 280 // Sort bottom up so we can spin over what is above it. | |
| 281 NSArray* views = | |
| 282 [[view subviews] sortedArrayUsingFunction:cocoa_l10n_util::CompareFrameY | |
| 283 context:NULL]; | |
| 284 | |
| 285 // Find where |toRemove| was. | |
| 286 NSUInteger index = [views indexOfObject:toRemove]; | |
| 287 DCHECK_NE(index, NSNotFound); | |
| 288 NSUInteger count = [views count]; | |
| 289 CGFloat shrinkHeight = 0; | |
| 290 if (index < (count - 1)) { | |
| 291 // If we're not the topmost control, the amount to shift is the bottom of | |
| 292 // |toRemove| to the bottom of the view above it. | |
| 293 CGFloat shiftDown = | |
| 294 NSMinY([[views objectAtIndex:index + 1] frame]) - | |
| 295 NSMinY([toRemove frame]); | |
| 296 | |
| 297 // Now cycle over the views above it moving them down. | |
| 298 for (++index; index < count; ++index) { | |
| 299 NSView* view = [views objectAtIndex:index]; | |
| 300 NSPoint origin = [view frame].origin; | |
| 301 origin.y -= shiftDown; | |
| 302 [view setFrameOrigin:origin]; | |
| 303 } | |
| 304 | |
| 305 shrinkHeight = shiftDown; | |
| 306 } else if (index > 0) { | |
| 307 // If we're the topmost control, there's nothing to shift but we want to | |
| 308 // shrink until the top edge of the second-topmost control, unless it is | |
| 309 // actually higher than the topmost control (since we're sorting by the | |
| 310 // bottom edge). | |
| 311 shrinkHeight = std::max(0.f, | |
| 312 NSMaxY([toRemove frame]) - | |
| 313 NSMaxY([[views objectAtIndex:index - 1] frame])); | |
| 314 } | |
| 315 // If we only have one control, don't do any resizing (for now). | |
| 316 | |
| 317 // Remove |toRemove|. | |
| 318 [toRemove removeFromSuperview]; | |
| 319 | |
| 320 [GTMUILocalizerAndLayoutTweaker | |
| 321 resizeViewWithoutAutoResizingSubViews:view | |
| 322 delta:NSMakeSize(0, -shrinkHeight)]; | |
| 323 } | |
| 324 | |
| 325 // Simply removes all the views in |toRemove|. | |
| 326 void RemoveGroupFromView(NSView* view, NSArray* toRemove) { | |
| 327 for (NSView* viewToRemove in toRemove) { | |
| 328 RemoveViewFromView(view, viewToRemove); | |
| 329 } | |
| 330 } | |
| 331 | |
| 332 // Helper to tweak the layout of the "Under the Hood" content by autosizing all | |
| 333 // the views and moving things up vertically. Special case the two controls for | |
| 334 // download location as they are horizontal, and should fill the row. Special | |
| 335 // case "Content Settings" and "Clear browsing data" as they are horizontal as | |
| 336 // well. | |
| 337 CGFloat AutoSizeUnderTheHoodContent(NSView* view, | |
| 338 NSPathControl* downloadLocationControl, | |
| 339 NSButton* downloadLocationButton) { | |
| 340 CGFloat verticalShift = 0.0; | |
| 341 | |
| 342 // Loop bottom up through the views sizing and shifting. | |
| 343 NSArray* views = | |
| 344 [[view subviews] sortedArrayUsingFunction:cocoa_l10n_util::CompareFrameY | |
| 345 context:NULL]; | |
| 346 for (NSView* view in views) { | |
| 347 NSSize delta = cocoa_l10n_util::WrapOrSizeToFit(view); | |
| 348 DCHECK_GE(delta.height, 0.0) << "Should NOT shrink in height"; | |
| 349 if (verticalShift) { | |
| 350 NSPoint origin = [view frame].origin; | |
| 351 origin.y += verticalShift; | |
| 352 [view setFrameOrigin:origin]; | |
| 353 } | |
| 354 verticalShift += delta.height; | |
| 355 | |
| 356 // The Download Location controls go in a row with the button aligned to the | |
| 357 // right edge and the path control using all the rest of the space. | |
| 358 if (view == downloadLocationButton) { | |
| 359 NSPoint origin = [downloadLocationButton frame].origin; | |
| 360 origin.x -= delta.width; | |
| 361 [downloadLocationButton setFrameOrigin:origin]; | |
| 362 NSSize controlSize = [downloadLocationControl frame].size; | |
| 363 controlSize.width -= delta.width; | |
| 364 [downloadLocationControl setFrameSize:controlSize]; | |
| 365 } | |
| 366 } | |
| 367 | |
| 368 return verticalShift; | |
| 369 } | |
| 370 | |
| 371 } // namespace | |
| 372 | |
| 373 //------------------------------------------------------------------------- | |
| 374 | |
| 375 @interface PreferencesWindowController(Private) | |
| 376 // Callback when preferences are changed. |prefName| is the name of the | |
| 377 // pref that has changed. | |
| 378 - (void)prefChanged:(std::string*)prefName; | |
| 379 // Callback when sync state has changed. syncService_ needs to be | |
| 380 // queried to find out what happened. | |
| 381 - (void)syncStateChanged; | |
| 382 // Record the user performed a certain action and save the preferences. | |
| 383 - (void)recordUserAction:(const UserMetricsAction&) action; | |
| 384 - (void)registerPrefObservers; | |
| 385 - (void)configureInstant; | |
| 386 | |
| 387 // KVC setter methods. | |
| 388 - (void)setNewTabPageIsHomePageIndex:(NSInteger)val; | |
| 389 - (void)setHomepageURL:(NSString*)urlString; | |
| 390 - (void)setRestoreOnStartupIndex:(NSInteger)type; | |
| 391 - (void)setShowHomeButton:(BOOL)value; | |
| 392 - (void)setPasswordManagerEnabledIndex:(NSInteger)value; | |
| 393 - (void)setIsUsingDefaultTheme:(BOOL)value; | |
| 394 - (void)setShowAlternateErrorPages:(BOOL)value; | |
| 395 - (void)setUseSuggest:(BOOL)value; | |
| 396 - (void)setDnsPrefetch:(BOOL)value; | |
| 397 - (void)setSafeBrowsing:(BOOL)value; | |
| 398 - (void)setMetricsReporting:(BOOL)value; | |
| 399 - (void)setAskForSaveLocation:(BOOL)value; | |
| 400 - (void)setFileHandlerUIEnabled:(BOOL)value; | |
| 401 - (void)setTranslateEnabled:(BOOL)value; | |
| 402 - (void)setTabsToLinks:(BOOL)value; | |
| 403 - (void)displayPreferenceViewForPage:(OptionsPage)page | |
| 404 animate:(BOOL)animate; | |
| 405 - (void)resetSubViews; | |
| 406 - (void)initBannerStateForPage:(OptionsPage)page; | |
| 407 | |
| 408 // KVC getter methods. | |
| 409 - (BOOL)fileHandlerUIEnabled; | |
| 410 - (BOOL)canChangeDefaultBrowser; | |
| 411 @end | |
| 412 | |
| 413 namespace PreferencesWindowControllerInternal { | |
| 414 | |
| 415 // A C++ class registered for changes in preferences. Bridges the | |
| 416 // notification back to the PWC. | |
| 417 class PrefObserverBridge : public NotificationObserver, | |
| 418 public ProfileSyncServiceObserver { | |
| 419 public: | |
| 420 PrefObserverBridge(PreferencesWindowController* controller) | |
| 421 : controller_(controller) {} | |
| 422 | |
| 423 virtual ~PrefObserverBridge() {} | |
| 424 | |
| 425 // Overridden from NotificationObserver: | |
| 426 virtual void Observe(NotificationType type, | |
| 427 const NotificationSource& source, | |
| 428 const NotificationDetails& details) { | |
| 429 if (type == NotificationType::PREF_CHANGED) | |
| 430 [controller_ prefChanged:Details<std::string>(details).ptr()]; | |
| 431 } | |
| 432 | |
| 433 // Overridden from ProfileSyncServiceObserver. | |
| 434 virtual void OnStateChanged() { | |
| 435 [controller_ syncStateChanged]; | |
| 436 } | |
| 437 | |
| 438 private: | |
| 439 PreferencesWindowController* controller_; // weak, owns us | |
| 440 }; | |
| 441 | |
| 442 // Tracks state for a managed prefs banner and triggers UI updates through the | |
| 443 // PreferencesWindowController as appropriate. | |
| 444 class ManagedPrefsBannerState : public policy::ManagedPrefsBannerBase { | |
| 445 public: | |
| 446 virtual ~ManagedPrefsBannerState() { } | |
| 447 | |
| 448 explicit ManagedPrefsBannerState(PreferencesWindowController* controller, | |
| 449 OptionsPage page, | |
| 450 PrefService* local_state, | |
| 451 PrefService* prefs) | |
| 452 : policy::ManagedPrefsBannerBase(local_state, prefs, page), | |
| 453 controller_(controller), | |
| 454 page_(page) { } | |
| 455 | |
| 456 BOOL IsVisible() { | |
| 457 return DetermineVisibility(); | |
| 458 } | |
| 459 | |
| 460 protected: | |
| 461 // Overridden from ManagedPrefsBannerBase. | |
| 462 virtual void OnUpdateVisibility() { | |
| 463 [controller_ switchToPage:page_ animate:YES]; | |
| 464 } | |
| 465 | |
| 466 private: | |
| 467 PreferencesWindowController* controller_; // weak, owns us | |
| 468 OptionsPage page_; // current options page | |
| 469 }; | |
| 470 | |
| 471 } // namespace PreferencesWindowControllerInternal | |
| 472 | |
| 473 @implementation PreferencesWindowController | |
| 474 | |
| 475 @synthesize restoreButtonsEnabled = restoreButtonsEnabled_; | |
| 476 @synthesize restoreURLsEnabled = restoreURLsEnabled_; | |
| 477 @synthesize showHomeButtonEnabled = showHomeButtonEnabled_; | |
| 478 @synthesize defaultSearchEngineEnabled = defaultSearchEngineEnabled_; | |
| 479 @synthesize passwordManagerChoiceEnabled = passwordManagerChoiceEnabled_; | |
| 480 @synthesize passwordManagerButtonEnabled = passwordManagerButtonEnabled_; | |
| 481 @synthesize autoFillSettingsButtonEnabled = autoFillSettingsButtonEnabled_; | |
| 482 @synthesize showAlternateErrorPagesEnabled = showAlternateErrorPagesEnabled_; | |
| 483 @synthesize useSuggestEnabled = useSuggestEnabled_; | |
| 484 @synthesize dnsPrefetchEnabled = dnsPrefetchEnabled_; | |
| 485 @synthesize safeBrowsingEnabled = safeBrowsingEnabled_; | |
| 486 @synthesize metricsReportingEnabled = metricsReportingEnabled_; | |
| 487 @synthesize downloadLocationEnabled = downloadLocationEnabled_; | |
| 488 @synthesize proxiesConfigureButtonEnabled = proxiesConfigureButtonEnabled_; | |
| 489 | |
| 490 - (id)initWithProfile:(Profile*)profile initialPage:(OptionsPage)initialPage { | |
| 491 DCHECK(profile); | |
| 492 // Use initWithWindowNibPath:: instead of initWithWindowNibName: so we | |
| 493 // can override it in a unit test. | |
| 494 NSString* nibPath = [base::mac::MainAppBundle() | |
| 495 pathForResource:@"Preferences" | |
| 496 ofType:@"nib"]; | |
| 497 if ((self = [super initWithWindowNibPath:nibPath owner:self])) { | |
| 498 profile_ = profile->GetOriginalProfile(); | |
| 499 initialPage_ = initialPage; | |
| 500 prefs_ = profile->GetPrefs(); | |
| 501 DCHECK(prefs_); | |
| 502 observer_.reset( | |
| 503 new PreferencesWindowControllerInternal::PrefObserverBridge(self)); | |
| 504 | |
| 505 // Set up the model for the custom home page table. The KVO observation | |
| 506 // tells us when the number of items in the array changes. The normal | |
| 507 // observation tells us when one of the URLs of an item changes. | |
| 508 customPagesSource_.reset([[CustomHomePagesModel alloc] | |
| 509 initWithProfile:profile_]); | |
| 510 const SessionStartupPref startupPref = | |
| 511 SessionStartupPref::GetStartupPref(prefs_); | |
| 512 [customPagesSource_ setURLs:startupPref.urls]; | |
| 513 | |
| 514 // Set up the model for the default search popup. Register for notifications | |
| 515 // about when the model changes so we can update the selection in the view. | |
| 516 searchEngineModel_.reset( | |
| 517 [[SearchEngineListModel alloc] | |
| 518 initWithModel:profile->GetTemplateURLModel()]); | |
| 519 [[NSNotificationCenter defaultCenter] | |
| 520 addObserver:self | |
| 521 selector:@selector(searchEngineModelChanged:) | |
| 522 name:kSearchEngineListModelChangedNotification | |
| 523 object:searchEngineModel_.get()]; | |
| 524 | |
| 525 // This needs to be done before awakeFromNib: because the bindings set up | |
| 526 // in the nib rely on it. | |
| 527 [self registerPrefObservers]; | |
| 528 | |
| 529 // Use one animation so we can stop it if the user clicks quickly, and | |
| 530 // start the new animation. | |
| 531 animation_.reset([[NSViewAnimation alloc] init]); | |
| 532 // Make this the delegate so it can remove the old view at the end of the | |
| 533 // animation (once it is faded out). | |
| 534 [animation_ setDelegate:self]; | |
| 535 [animation_ setAnimationBlockingMode:NSAnimationNonblocking]; | |
| 536 | |
| 537 // TODO(akalin): handle incognito profiles? The windows version of this | |
| 538 // (in chrome/browser/ui/views/options/content_page_view.cc) just does what | |
| 539 // we do below. | |
| 540 syncService_ = profile_->GetProfileSyncService(); | |
| 541 | |
| 542 // TODO(akalin): This color is taken from kSyncLabelErrorBgColor in | |
| 543 // content_page_view.cc. Either decomp that color out into a | |
| 544 // function/variable that is referenced by both this file and | |
| 545 // content_page_view.cc, or maybe pick a more suitable color. | |
| 546 syncErrorBackgroundColor_.reset( | |
| 547 [[NSColor colorWithDeviceRed:0xff/255.0 | |
| 548 green:0x9a/255.0 | |
| 549 blue:0x9a/255.0 | |
| 550 alpha:1.0] retain]); | |
| 551 | |
| 552 // Disable the |autoFillSettingsButton_| if we have no | |
| 553 // |personalDataManager|. | |
| 554 PersonalDataManager* personalDataManager = | |
| 555 profile_->GetPersonalDataManager(); | |
| 556 [autoFillSettingsButton_ setHidden:(personalDataManager == NULL)]; | |
| 557 bool autofill_disabled_by_policy = | |
| 558 autoFillEnabled_.IsManaged() && !autoFillEnabled_.GetValue(); | |
| 559 [self setAutoFillSettingsButtonEnabled:!autofill_disabled_by_policy]; | |
| 560 [self setPasswordManagerChoiceEnabled:!askSavePasswords_.IsManaged()]; | |
| 561 [self setPasswordManagerButtonEnabled: | |
| 562 !askSavePasswords_.IsManaged() || askSavePasswords_.GetValue()]; | |
| 563 | |
| 564 // Initialize the enabled state of the elements on the general tab. | |
| 565 [self setShowHomeButtonEnabled:!showHomeButton_.IsManaged()]; | |
| 566 [self setEnabledStateOfRestoreOnStartup]; | |
| 567 [self setDefaultSearchEngineEnabled:![searchEngineModel_ isDefaultManaged]]; | |
| 568 | |
| 569 // Initialize UI state for the advanced page. | |
| 570 [self setShowAlternateErrorPagesEnabled:!alternateErrorPages_.IsManaged()]; | |
| 571 [self setUseSuggestEnabled:!useSuggest_.IsManaged()]; | |
| 572 [self setDnsPrefetchEnabled:!dnsPrefetch_.IsManaged()]; | |
| 573 [self setSafeBrowsingEnabled:!safeBrowsing_.IsManaged()]; | |
| 574 [self setMetricsReportingEnabled:!metricsReporting_.IsManaged()]; | |
| 575 [self setDownloadLocationEnabled:!defaultDownloadLocation_.IsManaged()]; | |
| 576 proxyPrefs_.reset( | |
| 577 PrefSetObserver::CreateProxyPrefSetObserver(prefs_, observer_.get())); | |
| 578 [self setProxiesConfigureButtonEnabled:!proxyPrefs_->IsManaged()]; | |
| 579 } | |
| 580 return self; | |
| 581 } | |
| 582 | |
| 583 - (void)awakeFromNib { | |
| 584 | |
| 585 // Validate some assumptions in debug builds. | |
| 586 | |
| 587 // "Basics", "Personal Stuff", and "Under the Hood" views should be the same | |
| 588 // width. They should be the same width so they are laid out to look as good | |
| 589 // as possible at that width with controls just having to wrap if their text | |
| 590 // is too long. | |
| 591 DCHECK_EQ(NSWidth([basicsView_ frame]), NSWidth([personalStuffView_ frame])) | |
| 592 << "Basics and Personal Stuff should be the same widths"; | |
| 593 DCHECK_EQ(NSWidth([basicsView_ frame]), NSWidth([underTheHoodView_ frame])) | |
| 594 << "Basics and Under the Hood should be the same widths"; | |
| 595 // "Under the Hood" content should always be skinnier than the scroller it | |
| 596 // goes into (we resize it). | |
| 597 DCHECK_LE(NSWidth([underTheHoodContentView_ frame]), | |
| 598 [underTheHoodScroller_ contentSize].width) | |
| 599 << "The Under the Hood content should be narrower than the content " | |
| 600 "of the scroller it goes into"; | |
| 601 | |
| 602 #if !defined(GOOGLE_CHROME_BUILD) | |
| 603 // "Enable logging" (breakpad and stats) is only in Google Chrome builds, | |
| 604 // remove the checkbox and slide everything above it down. | |
| 605 RemoveViewFromView(underTheHoodContentView_, enableLoggingCheckbox_); | |
| 606 #endif // !defined(GOOGLE_CHROME_BUILD) | |
| 607 | |
| 608 // There are four problem children within the groups: | |
| 609 // Basics - Default Browser | |
| 610 // Personal Stuff - Sync | |
| 611 // Personal Stuff - Themes | |
| 612 // Personal Stuff - Browser Data | |
| 613 // These four have buttons that with some localizations are wider then the | |
| 614 // view. So the four get manually laid out before doing the general work so | |
| 615 // the views/window can be made wide enough to fit them. The layout in the | |
| 616 // general pass is a noop for these buttons (since they are already sized). | |
| 617 | |
| 618 // Size the default browser button. | |
| 619 const NSUInteger kDefaultBrowserGroupCount = 3; | |
| 620 const NSUInteger kDefaultBrowserButtonIndex = 1; | |
| 621 DCHECK_EQ([basicsGroupDefaultBrowser_ count], kDefaultBrowserGroupCount) | |
| 622 << "Expected only two items in Default Browser group"; | |
| 623 NSButton* defaultBrowserButton = | |
| 624 [basicsGroupDefaultBrowser_ objectAtIndex:kDefaultBrowserButtonIndex]; | |
| 625 NSSize defaultBrowserChange = | |
| 626 [GTMUILocalizerAndLayoutTweaker sizeToFitView:defaultBrowserButton]; | |
| 627 DCHECK_EQ(defaultBrowserChange.height, 0.0) | |
| 628 << "Button should have been right height in nib"; | |
| 629 | |
| 630 [self configureInstant]; | |
| 631 | |
| 632 // Size the sync row. | |
| 633 CGFloat syncRowChange = SizeToFitButtonPair(syncButton_, | |
| 634 syncCustomizeButton_); | |
| 635 | |
| 636 // Size the themes row. | |
| 637 const NSUInteger kThemeGroupCount = 3; | |
| 638 const NSUInteger kThemeResetButtonIndex = 1; | |
| 639 const NSUInteger kThemeThemesButtonIndex = 2; | |
| 640 DCHECK_EQ([personalStuffGroupThemes_ count], kThemeGroupCount) | |
| 641 << "Expected only two items in Themes group"; | |
| 642 CGFloat themeRowChange = SizeToFitButtonPair( | |
| 643 [personalStuffGroupThemes_ objectAtIndex:kThemeResetButtonIndex], | |
| 644 [personalStuffGroupThemes_ objectAtIndex:kThemeThemesButtonIndex]); | |
| 645 | |
| 646 // Size the Privacy and Clear buttons that make a row in Under the Hood. | |
| 647 CGFloat privacyRowChange = SizeToFitButtonPair(contentSettingsButton_, | |
| 648 clearDataButton_); | |
| 649 // Under the Hood view is narrower (then the other panes) in the nib, subtract | |
| 650 // out the amount it was already going to grow to match the other panes when | |
| 651 // calculating how much the row needs things to grow. | |
| 652 privacyRowChange -= | |
| 653 ([underTheHoodScroller_ contentSize].width - | |
| 654 NSWidth([underTheHoodContentView_ frame])); | |
| 655 | |
| 656 // Find the most any row changed in size. | |
| 657 CGFloat maxWidthChange = std::max(defaultBrowserChange.width, syncRowChange); | |
| 658 maxWidthChange = std::max(maxWidthChange, themeRowChange); | |
| 659 maxWidthChange = std::max(maxWidthChange, privacyRowChange); | |
| 660 | |
| 661 // If any grew wider, make the views wider. If they all shrank, they fit the | |
| 662 // existing view widths, so no change is needed//. | |
| 663 if (maxWidthChange > 0.0) { | |
| 664 NSSize viewSize = [basicsView_ frame].size; | |
| 665 viewSize.width += maxWidthChange; | |
| 666 [basicsView_ setFrameSize:viewSize]; | |
| 667 viewSize = [personalStuffView_ frame].size; | |
| 668 viewSize.width += maxWidthChange; | |
| 669 [personalStuffView_ setFrameSize:viewSize]; | |
| 670 } | |
| 671 | |
| 672 // Now that we have the width needed for Basics and Personal Stuff, lay out | |
| 673 // those pages bottom up making sure the strings fit and moving things up as | |
| 674 // needed. | |
| 675 | |
| 676 CGFloat newWidth = NSWidth([basicsView_ frame]); | |
| 677 CGFloat verticalShift = 0.0; | |
| 678 verticalShift += AutoSizeGroup(basicsGroupDefaultBrowser_, | |
| 679 kAutoSizeGroupBehaviorVerticalFirstToFit, | |
| 680 verticalShift); | |
| 681 verticalShift += AutoSizeGroup( | |
| 682 basicsGroupSearchEngine_, | |
| 683 kAutoSizeGroupBehaviorFirstTwoAsRowVerticalToFit, | |
| 684 verticalShift); | |
| 685 verticalShift += AutoSizeGroup(basicsGroupToolbar_, | |
| 686 kAutoSizeGroupBehaviorVerticalToFit, | |
| 687 verticalShift); | |
| 688 verticalShift += AutoSizeGroup(basicsGroupHomePage_, | |
| 689 kAutoSizeGroupBehaviorVerticalToFit, | |
| 690 verticalShift); | |
| 691 verticalShift += AutoSizeGroup(basicsGroupStartup_, | |
| 692 kAutoSizeGroupBehaviorVerticalFirstToFit, | |
| 693 verticalShift); | |
| 694 [GTMUILocalizerAndLayoutTweaker | |
| 695 resizeViewWithoutAutoResizingSubViews:basicsView_ | |
| 696 delta:NSMakeSize(0.0, verticalShift)]; | |
| 697 | |
| 698 verticalShift = 0.0; | |
| 699 verticalShift += AutoSizeGroup(personalStuffGroupThemes_, | |
| 700 kAutoSizeGroupBehaviorHorizontalToFit, | |
| 701 verticalShift); | |
| 702 verticalShift += AutoSizeGroup(personalStuffGroupBrowserData_, | |
| 703 kAutoSizeGroupBehaviorVerticalToFit, | |
| 704 verticalShift); | |
| 705 verticalShift += AutoSizeGroup(personalStuffGroupAutofill_, | |
| 706 kAutoSizeGroupBehaviorVerticalToFit, | |
| 707 verticalShift); | |
| 708 verticalShift += AutoSizeGroup(personalStuffGroupPasswords_, | |
| 709 kAutoSizeGroupBehaviorVerticalToFit, | |
| 710 verticalShift); | |
| 711 // TODO(akalin): Here we rely on the initial contents of the sync | |
| 712 // group's text field/link field to be large enough to hold all | |
| 713 // possible messages so that we don't have to re-layout when sync | |
| 714 // state changes. This isn't perfect, since e.g. some sync messages | |
| 715 // use the user's e-mail address (which may be really long), and the | |
| 716 // link field is usually not shown (leaving a big empty space). | |
| 717 // Rethink sync preferences UI for Mac. | |
| 718 verticalShift += AutoSizeGroup(personalStuffGroupSync_, | |
| 719 kAutoSizeGroupBehaviorVerticalToFit, | |
| 720 verticalShift); | |
| 721 [GTMUILocalizerAndLayoutTweaker | |
| 722 resizeViewWithoutAutoResizingSubViews:personalStuffView_ | |
| 723 delta:NSMakeSize(0.0, verticalShift)]; | |
| 724 | |
| 725 if (syncService_) { | |
| 726 syncService_->AddObserver(observer_.get()); | |
| 727 // Update the controls according to the initial state. | |
| 728 [self syncStateChanged]; | |
| 729 } else { | |
| 730 // If sync is disabled we don't want to show the sync controls at all. | |
| 731 RemoveGroupFromView(personalStuffView_, personalStuffGroupSync_); | |
| 732 } | |
| 733 | |
| 734 // Make the window as wide as the views. | |
| 735 NSWindow* prefsWindow = [self window]; | |
| 736 NSView* prefsContentView = [prefsWindow contentView]; | |
| 737 NSRect frame = [prefsContentView convertRect:[prefsWindow frame] | |
| 738 fromView:nil]; | |
| 739 frame.size.width = newWidth; | |
| 740 frame = [prefsContentView convertRect:frame toView:nil]; | |
| 741 [prefsWindow setFrame:frame display:NO]; | |
| 742 | |
| 743 // The Under the Hood prefs is a scroller, it shouldn't get any border, so it | |
| 744 // gets resized to be as wide as the window ended up. | |
| 745 NSSize underTheHoodSize = [underTheHoodView_ frame].size; | |
| 746 underTheHoodSize.width = newWidth; | |
| 747 [underTheHoodView_ setFrameSize:underTheHoodSize]; | |
| 748 | |
| 749 // Widen the Under the Hood content so things can rewrap to the full width. | |
| 750 NSSize underTheHoodContentSize = [underTheHoodContentView_ frame].size; | |
| 751 underTheHoodContentSize.width = [underTheHoodScroller_ contentSize].width; | |
| 752 [underTheHoodContentView_ setFrameSize:underTheHoodContentSize]; | |
| 753 | |
| 754 // Now that Under the Hood is the right width, auto-size to the new width to | |
| 755 // get the final height. | |
| 756 verticalShift = AutoSizeUnderTheHoodContent(underTheHoodContentView_, | |
| 757 downloadLocationControl_, | |
| 758 downloadLocationButton_); | |
| 759 [GTMUILocalizerAndLayoutTweaker | |
| 760 resizeViewWithoutAutoResizingSubViews:underTheHoodContentView_ | |
| 761 delta:NSMakeSize(0.0, verticalShift)]; | |
| 762 underTheHoodContentSize = [underTheHoodContentView_ frame].size; | |
| 763 | |
| 764 // Put the Under the Hood content view into the scroller and scroll it to the | |
| 765 // top. | |
| 766 [underTheHoodScroller_ setDocumentView:underTheHoodContentView_]; | |
| 767 [underTheHoodContentView_ scrollPoint: | |
| 768 NSMakePoint(0, underTheHoodContentSize.height)]; | |
| 769 | |
| 770 ResourceBundle& rb = ResourceBundle::GetSharedInstance(); | |
| 771 NSImage* alertIcon = rb.GetNativeImageNamed(IDR_WARNING); | |
| 772 DCHECK(alertIcon); | |
| 773 [managedPrefsBannerWarningImage_ setImage:alertIcon]; | |
| 774 | |
| 775 [self initBannerStateForPage:initialPage_]; | |
| 776 [self switchToPage:initialPage_ animate:NO]; | |
| 777 | |
| 778 // Save/restore position based on prefs. | |
| 779 if (g_browser_process && g_browser_process->local_state()) { | |
| 780 sizeSaver_.reset([[WindowSizeAutosaver alloc] | |
| 781 initWithWindow:[self window] | |
| 782 prefService:profile_->GetPrefs() | |
| 783 path:prefs::kPreferencesWindowPlacement]); | |
| 784 } | |
| 785 | |
| 786 // Initialize the banner gradient and stroke color. | |
| 787 NSColor* bannerStartingColor = | |
| 788 [NSColor colorWithCalibratedRed:kBannerGradientColorTop[0] | |
| 789 green:kBannerGradientColorTop[1] | |
| 790 blue:kBannerGradientColorTop[2] | |
| 791 alpha:1.0]; | |
| 792 NSColor* bannerEndingColor = | |
| 793 [NSColor colorWithCalibratedRed:kBannerGradientColorBottom[0] | |
| 794 green:kBannerGradientColorBottom[1] | |
| 795 blue:kBannerGradientColorBottom[2] | |
| 796 alpha:1.0]; | |
| 797 scoped_nsobject<NSGradient> bannerGradient( | |
| 798 [[NSGradient alloc] initWithStartingColor:bannerStartingColor | |
| 799 endingColor:bannerEndingColor]); | |
| 800 [managedPrefsBannerView_ setGradient:bannerGradient]; | |
| 801 | |
| 802 NSColor* bannerStrokeColor = | |
| 803 [NSColor colorWithCalibratedWhite:kBannerStrokeColor | |
| 804 alpha:1.0]; | |
| 805 [managedPrefsBannerView_ setStrokeColor:bannerStrokeColor]; | |
| 806 | |
| 807 // Set accessibility related attributes. | |
| 808 NSTableView* tableView = [basicsView_ viewWithTag:kBasicsStartupPageTableTag]; | |
| 809 NSString* description = | |
| 810 l10n_util::GetNSStringWithFixup(IDS_OPTIONS_STARTUP_SHOW_PAGES); | |
| 811 [tableView accessibilitySetOverrideValue:description | |
| 812 forAttribute:NSAccessibilityDescriptionAttribute]; | |
| 813 } | |
| 814 | |
| 815 - (void)dealloc { | |
| 816 if (syncService_) { | |
| 817 syncService_->RemoveObserver(observer_.get()); | |
| 818 } | |
| 819 [[NSNotificationCenter defaultCenter] removeObserver:self]; | |
| 820 [animation_ setDelegate:nil]; | |
| 821 [animation_ stopAnimation]; | |
| 822 [super dealloc]; | |
| 823 } | |
| 824 | |
| 825 // Xcode 3.1.x version of Interface Builder doesn't do a lot for editing | |
| 826 // toolbars in XIB. So the toolbar's delegate is set to the controller so it | |
| 827 // can tell the toolbar what items are selectable. | |
| 828 - (NSArray*)toolbarSelectableItemIdentifiers:(NSToolbar*)toolbar { | |
| 829 DCHECK(toolbar == toolbar_); | |
| 830 return [[toolbar_ items] valueForKey:@"itemIdentifier"]; | |
| 831 } | |
| 832 | |
| 833 // Register our interest in the preferences we're displaying so if anything | |
| 834 // else in the UI changes them we will be updated. | |
| 835 - (void)registerPrefObservers { | |
| 836 if (!prefs_) return; | |
| 837 | |
| 838 PrefService* local = g_browser_process->local_state(); | |
| 839 | |
| 840 // Basics panel | |
| 841 registrar_.Init(prefs_); | |
| 842 registrar_.Add(prefs::kURLsToRestoreOnStartup, observer_.get()); | |
| 843 restoreOnStartup_.Init(prefs::kRestoreOnStartup, prefs_, observer_.get()); | |
| 844 newTabPageIsHomePage_.Init(prefs::kHomePageIsNewTabPage, | |
| 845 prefs_, observer_.get()); | |
| 846 homepage_.Init(prefs::kHomePage, prefs_, observer_.get()); | |
| 847 showHomeButton_.Init(prefs::kShowHomeButton, prefs_, observer_.get()); | |
| 848 instantEnabled_.Init(prefs::kInstantEnabled, prefs_, observer_.get()); | |
| 849 default_browser_policy_.Init(prefs::kDefaultBrowserSettingEnabled, | |
| 850 local, observer_.get()); | |
| 851 | |
| 852 // Personal Stuff panel | |
| 853 askSavePasswords_.Init(prefs::kPasswordManagerEnabled, | |
| 854 prefs_, observer_.get()); | |
| 855 autoFillEnabled_.Init(prefs::kAutoFillEnabled, prefs_, observer_.get()); | |
| 856 currentTheme_.Init(prefs::kCurrentThemeID, prefs_, observer_.get()); | |
| 857 | |
| 858 // Under the hood panel | |
| 859 alternateErrorPages_.Init(prefs::kAlternateErrorPagesEnabled, | |
| 860 prefs_, observer_.get()); | |
| 861 useSuggest_.Init(prefs::kSearchSuggestEnabled, prefs_, observer_.get()); | |
| 862 dnsPrefetch_.Init(prefs::kDnsPrefetchingEnabled, prefs_, observer_.get()); | |
| 863 safeBrowsing_.Init(prefs::kSafeBrowsingEnabled, prefs_, observer_.get()); | |
| 864 autoOpenFiles_.Init( | |
| 865 prefs::kDownloadExtensionsToOpen, prefs_, observer_.get()); | |
| 866 translateEnabled_.Init(prefs::kEnableTranslate, prefs_, observer_.get()); | |
| 867 tabsToLinks_.Init(prefs::kWebkitTabsToLinks, prefs_, observer_.get()); | |
| 868 | |
| 869 metricsReporting_.Init(prefs::kMetricsReportingEnabled, | |
| 870 local, observer_.get()); | |
| 871 defaultDownloadLocation_.Init(prefs::kDownloadDefaultDirectory, prefs_, | |
| 872 observer_.get()); | |
| 873 askForSaveLocation_.Init(prefs::kPromptForDownload, prefs_, observer_.get()); | |
| 874 | |
| 875 // We don't need to observe changes in this value. | |
| 876 lastSelectedPage_.Init(prefs::kOptionsWindowLastTabIndex, local, NULL); | |
| 877 } | |
| 878 | |
| 879 // Called when the window wants to be closed. | |
| 880 - (BOOL)windowShouldClose:(id)sender { | |
| 881 // Stop any animation and clear the delegate to avoid stale pointers. | |
| 882 [animation_ setDelegate:nil]; | |
| 883 [animation_ stopAnimation]; | |
| 884 | |
| 885 return YES; | |
| 886 } | |
| 887 | |
| 888 // Called when the user hits the escape key. Closes the window. | |
| 889 - (void)cancel:(id)sender { | |
| 890 [[self window] performClose:self]; | |
| 891 } | |
| 892 | |
| 893 // Record the user performed a certain action and save the preferences. | |
| 894 - (void)recordUserAction:(const UserMetricsAction &)action { | |
| 895 UserMetrics::RecordAction(action, profile_); | |
| 896 if (prefs_) | |
| 897 prefs_->ScheduleSavePersistentPrefs(); | |
| 898 } | |
| 899 | |
| 900 // Returns the set of keys that |key| depends on for its value so it can be | |
| 901 // re-computed when any of those change as well. | |
| 902 + (NSSet*)keyPathsForValuesAffectingValueForKey:(NSString*)key { | |
| 903 NSSet* paths = [super keyPathsForValuesAffectingValueForKey:key]; | |
| 904 if ([key isEqualToString:@"isHomepageURLEnabled"]) { | |
| 905 paths = [paths setByAddingObject:@"newTabPageIsHomePageIndex"]; | |
| 906 paths = [paths setByAddingObject:@"homepageURL"]; | |
| 907 } else if ([key isEqualToString:@"restoreURLsEnabled"]) { | |
| 908 paths = [paths setByAddingObject:@"restoreOnStartupIndex"]; | |
| 909 } else if ([key isEqualToString:@"isHomepageChoiceEnabled"]) { | |
| 910 paths = [paths setByAddingObject:@"newTabPageIsHomePageIndex"]; | |
| 911 paths = [paths setByAddingObject:@"homepageURL"]; | |
| 912 } else if ([key isEqualToString:@"newTabPageIsHomePageIndex"]) { | |
| 913 paths = [paths setByAddingObject:@"homepageURL"]; | |
| 914 } else if ([key isEqualToString:@"hompageURL"]) { | |
| 915 paths = [paths setByAddingObject:@"newTabPageIsHomePageIndex"]; | |
| 916 } else if ([key isEqualToString:@"canChangeDefaultBrowser"]) { | |
| 917 paths = [paths setByAddingObject:@"defaultBrowser"]; | |
| 918 } else if ([key isEqualToString:@"defaultBrowserTextColor"]) { | |
| 919 paths = [paths setByAddingObject:@"defaultBrowser"]; | |
| 920 } else if ([key isEqualToString:@"defaultBrowserText"]) { | |
| 921 paths = [paths setByAddingObject:@"defaultBrowser"]; | |
| 922 } | |
| 923 return paths; | |
| 924 } | |
| 925 | |
| 926 // Launch the Keychain Access app. | |
| 927 - (void)launchKeychainAccess { | |
| 928 NSString* const kKeychainBundleId = @"com.apple.keychainaccess"; | |
| 929 [[NSWorkspace sharedWorkspace] | |
| 930 launchAppWithBundleIdentifier:kKeychainBundleId | |
| 931 options:0L | |
| 932 additionalEventParamDescriptor:nil | |
| 933 launchIdentifier:nil]; | |
| 934 } | |
| 935 | |
| 936 //------------------------------------------------------------------------- | |
| 937 // Basics panel | |
| 938 | |
| 939 // Sets the home page preferences for kNewTabPageIsHomePage and kHomePage. If a | |
| 940 // blank or null-host URL is passed in we revert to using NewTab page | |
| 941 // as the Home page. Note: using SetValue() causes the observers not to fire, | |
| 942 // which is actually a good thing as we could end up in a state where setting | |
| 943 // the homepage to an empty url would automatically reset the prefs back to | |
| 944 // using the NTP, so we'd be never be able to change it. | |
| 945 - (void)setHomepage:(const GURL&)homepage { | |
| 946 if (IsNewTabUIURLString(homepage)) { | |
| 947 newTabPageIsHomePage_.SetValueIfNotManaged(true); | |
| 948 homepage_.SetValueIfNotManaged(std::string()); | |
| 949 } else if (!homepage.is_valid()) { | |
| 950 newTabPageIsHomePage_.SetValueIfNotManaged(true); | |
| 951 if (!homepage.has_host()) | |
| 952 homepage_.SetValueIfNotManaged(std::string()); | |
| 953 } else { | |
| 954 homepage_.SetValueIfNotManaged(homepage.spec()); | |
| 955 } | |
| 956 } | |
| 957 | |
| 958 // Callback when preferences are changed by someone modifying the prefs backend | |
| 959 // externally. |prefName| is the name of the pref that has changed. Unlike on | |
| 960 // Windows, we don't need to use this method for initializing, that's handled by | |
| 961 // Cocoa Bindings. | |
| 962 // Handles prefs for the "Basics" panel. | |
| 963 - (void)basicsPrefChanged:(std::string*)prefName { | |
| 964 if (*prefName == prefs::kRestoreOnStartup) { | |
| 965 const SessionStartupPref startupPref = | |
| 966 SessionStartupPref::GetStartupPref(prefs_); | |
| 967 [self setRestoreOnStartupIndex:startupPref.type]; | |
| 968 [self setEnabledStateOfRestoreOnStartup]; | |
| 969 } else if (*prefName == prefs::kURLsToRestoreOnStartup) { | |
| 970 [customPagesSource_ reloadURLs]; | |
| 971 [self setEnabledStateOfRestoreOnStartup]; | |
| 972 } else if (*prefName == prefs::kHomePageIsNewTabPage) { | |
| 973 NSInteger useNewTabPage = newTabPageIsHomePage_.GetValue() ? 0 : 1; | |
| 974 [self setNewTabPageIsHomePageIndex:useNewTabPage]; | |
| 975 } else if (*prefName == prefs::kHomePage) { | |
| 976 NSString* value = base::SysUTF8ToNSString(homepage_.GetValue()); | |
| 977 [self setHomepageURL:value]; | |
| 978 } else if (*prefName == prefs::kShowHomeButton) { | |
| 979 [self setShowHomeButton:showHomeButton_.GetValue() ? YES : NO]; | |
| 980 [self setShowHomeButtonEnabled:!showHomeButton_.IsManaged()]; | |
| 981 } else if (*prefName == prefs::kInstantEnabled) { | |
| 982 [self configureInstant]; | |
| 983 } else if (*prefName == prefs::kDefaultBrowserSettingEnabled) { | |
| 984 [self willChangeValueForKey:@"defaultBrowser"]; | |
| 985 [self didChangeValueForKey:@"defaultBrowser"]; | |
| 986 } | |
| 987 } | |
| 988 | |
| 989 // Returns the index of the selected cell in the "on startup" matrix based | |
| 990 // on the "restore on startup" pref. The ordering of the cells is in the | |
| 991 // same order as the pref. | |
| 992 - (NSInteger)restoreOnStartupIndex { | |
| 993 const SessionStartupPref pref = SessionStartupPref::GetStartupPref(prefs_); | |
| 994 return pref.type; | |
| 995 } | |
| 996 | |
| 997 // A helper function that takes the startup session type, grabs the URLs to | |
| 998 // restore, and saves it all in prefs. | |
| 999 - (void)saveSessionStartupWithType:(SessionStartupPref::Type)type { | |
| 1000 SessionStartupPref pref; | |
| 1001 pref.type = type; | |
| 1002 pref.urls = [customPagesSource_.get() URLs]; | |
| 1003 SessionStartupPref::SetStartupPref(prefs_, pref); | |
| 1004 } | |
| 1005 | |
| 1006 // Sets the pref based on the index of the selected cell in the matrix and | |
| 1007 // marks the appropriate user metric. | |
| 1008 - (void)setRestoreOnStartupIndex:(NSInteger)type { | |
| 1009 SessionStartupPref::Type startupType = | |
| 1010 static_cast<SessionStartupPref::Type>(type); | |
| 1011 switch (startupType) { | |
| 1012 case SessionStartupPref::DEFAULT: | |
| 1013 [self recordUserAction:UserMetricsAction("Options_Startup_Homepage")]; | |
| 1014 break; | |
| 1015 case SessionStartupPref::LAST: | |
| 1016 [self recordUserAction:UserMetricsAction("Options_Startup_LastSession")]; | |
| 1017 break; | |
| 1018 case SessionStartupPref::URLS: | |
| 1019 [self recordUserAction:UserMetricsAction("Options_Startup_Custom")]; | |
| 1020 break; | |
| 1021 default: | |
| 1022 NOTREACHED(); | |
| 1023 } | |
| 1024 [self saveSessionStartupWithType:startupType]; | |
| 1025 } | |
| 1026 | |
| 1027 // Enables or disables the restoreOnStartup elements | |
| 1028 - (void) setEnabledStateOfRestoreOnStartup { | |
| 1029 const SessionStartupPref startupPref = | |
| 1030 SessionStartupPref::GetStartupPref(prefs_); | |
| 1031 [self setRestoreButtonsEnabled:!SessionStartupPref::TypeIsManaged(prefs_)]; | |
| 1032 [self setRestoreURLsEnabled:!SessionStartupPref::URLsAreManaged(prefs_) && | |
| 1033 [self restoreOnStartupIndex] == SessionStartupPref::URLS]; | |
| 1034 } | |
| 1035 | |
| 1036 // Getter for the |customPagesSource| property for bindings. | |
| 1037 - (CustomHomePagesModel*)customPagesSource { | |
| 1038 return customPagesSource_.get(); | |
| 1039 } | |
| 1040 | |
| 1041 // Called when the selection in the table changes. If a flag is set indicating | |
| 1042 // that we're waiting for a special select message, edit the cell. Otherwise | |
| 1043 // just ignore it, we don't normally care. | |
| 1044 - (void)tableViewSelectionDidChange:(NSNotification*)aNotification { | |
| 1045 if (pendingSelectForEdit_) { | |
| 1046 NSTableView* table = [aNotification object]; | |
| 1047 NSUInteger selectedRow = [table selectedRow]; | |
| 1048 [table editColumn:0 row:selectedRow withEvent:nil select:YES]; | |
| 1049 pendingSelectForEdit_ = NO; | |
| 1050 } | |
| 1051 } | |
| 1052 | |
| 1053 // Called when the user hits the (+) button for adding a new homepage to the | |
| 1054 // list. This will also attempt to make the new item editable so the user can | |
| 1055 // just start typing. | |
| 1056 - (IBAction)addHomepage:(id)sender { | |
| 1057 [customPagesArrayController_ add:sender]; | |
| 1058 | |
| 1059 // When the new item is added to the model, the array controller will select | |
| 1060 // it. We'll watch for that notification (because we are the table view's | |
| 1061 // delegate) and then make the cell editable. Note that this can't be | |
| 1062 // accomplished simply by subclassing the array controller's add method (I | |
| 1063 // did try). The update of the table is asynchronous with the controller | |
| 1064 // updating the model. | |
| 1065 pendingSelectForEdit_ = YES; | |
| 1066 } | |
| 1067 | |
| 1068 // Called when the user hits the (-) button for removing the selected items in | |
| 1069 // the homepage table. The controller does all the work. | |
| 1070 - (IBAction)removeSelectedHomepages:(id)sender { | |
| 1071 [customPagesArrayController_ remove:sender]; | |
| 1072 } | |
| 1073 | |
| 1074 // Add all entries for all open browsers with our profile. | |
| 1075 - (IBAction)useCurrentPagesAsHomepage:(id)sender { | |
| 1076 std::vector<GURL> urls; | |
| 1077 for (BrowserList::const_iterator browserIter = BrowserList::begin(); | |
| 1078 browserIter != BrowserList::end(); ++browserIter) { | |
| 1079 Browser* browser = *browserIter; | |
| 1080 if (browser->profile() != profile_) | |
| 1081 continue; // Only want entries for open profile. | |
| 1082 | |
| 1083 for (int tabIndex = 0; tabIndex < browser->tab_count(); ++tabIndex) { | |
| 1084 TabContents* tab = browser->GetTabContentsAt(tabIndex); | |
| 1085 if (tab->ShouldDisplayURL()) { | |
| 1086 const GURL url = browser->GetTabContentsAt(tabIndex)->GetURL(); | |
| 1087 if (!url.is_empty()) | |
| 1088 urls.push_back(url); | |
| 1089 } | |
| 1090 } | |
| 1091 } | |
| 1092 [customPagesSource_ setURLs:urls]; | |
| 1093 } | |
| 1094 | |
| 1095 enum { kHomepageNewTabPage, kHomepageURL }; | |
| 1096 | |
| 1097 // Here's a table describing the desired characteristics of the homepage choice | |
| 1098 // radio value, it's enabled state and the URL field enabled state. They depend | |
| 1099 // on the values of the managed bits for homepage (m_hp) and | |
| 1100 // homepageIsNewTabPage (m_ntp) preferences, as well as the value of the | |
| 1101 // homepageIsNewTabPage preference (ntp) and whether the homepage preference | |
| 1102 // is equal to the new tab page URL (hpisntp). | |
| 1103 // | |
| 1104 // m_hp m_ntp ntp hpisntp | choice value | choice enabled | URL field enabled | |
| 1105 // -------------------------------------------------------------------------- | |
| 1106 // 0 0 0 0 | homepage | 1 | 1 | |
| 1107 // 0 0 0 1 | new tab page | 1 | 0 | |
| 1108 // 0 0 1 0 | new tab page | 1 | 0 | |
| 1109 // 0 0 1 1 | new tab page | 1 | 0 | |
| 1110 // 0 1 0 0 | homepage | 0 | 1 | |
| 1111 // 0 1 0 1 | homepage | 0 | 1 | |
| 1112 // 0 1 1 0 | new tab page | 0 | 0 | |
| 1113 // 0 1 1 1 | new tab page | 0 | 0 | |
| 1114 // 1 0 0 0 | homepage | 1 | 0 | |
| 1115 // 1 0 0 1 | new tab page | 0 | 0 | |
| 1116 // 1 0 1 0 | new tab page | 1 | 0 | |
| 1117 // 1 0 1 1 | new tab page | 0 | 0 | |
| 1118 // 1 1 0 0 | homepage | 0 | 0 | |
| 1119 // 1 1 0 1 | new tab page | 0 | 0 | |
| 1120 // 1 1 1 0 | new tab page | 0 | 0 | |
| 1121 // 1 1 1 1 | new tab page | 0 | 0 | |
| 1122 // | |
| 1123 // thus, we have: | |
| 1124 // | |
| 1125 // choice value is new tab page === ntp || (hpisntp && (m_hp || !m_ntp)) | |
| 1126 // choice enabled === !m_ntp && !(m_hp && hpisntp) | |
| 1127 // URL field enabled === !ntp && !mhp && !(hpisntp && !m_ntp) | |
| 1128 // | |
| 1129 // which also make sense if you think about them. | |
| 1130 | |
| 1131 // Checks whether the homepage URL refers to the new tab page. | |
| 1132 - (BOOL)isHomepageNewTabUIURL { | |
| 1133 return IsNewTabUIURLString(GURL(homepage_.GetValue().c_str())); | |
| 1134 } | |
| 1135 | |
| 1136 // Returns the index of the selected cell in the "home page" marix based on | |
| 1137 // the "new tab is home page" pref. Sadly, the ordering is reversed from the | |
| 1138 // pref value. | |
| 1139 - (NSInteger)newTabPageIsHomePageIndex { | |
| 1140 return newTabPageIsHomePage_.GetValue() || | |
| 1141 ([self isHomepageNewTabUIURL] && | |
| 1142 (homepage_.IsManaged() || !newTabPageIsHomePage_.IsManaged())) ? | |
| 1143 kHomepageNewTabPage : kHomepageURL; | |
| 1144 } | |
| 1145 | |
| 1146 // Sets the pref based on the given index into the matrix and marks the | |
| 1147 // appropriate user metric. | |
| 1148 - (void)setNewTabPageIsHomePageIndex:(NSInteger)index { | |
| 1149 bool useNewTabPage = index == kHomepageNewTabPage ? true : false; | |
| 1150 if (useNewTabPage) { | |
| 1151 [self recordUserAction:UserMetricsAction("Options_Homepage_UseNewTab")]; | |
| 1152 } else { | |
| 1153 [self recordUserAction:UserMetricsAction("Options_Homepage_UseURL")]; | |
| 1154 if ([self isHomepageNewTabUIURL]) | |
| 1155 homepage_.SetValueIfNotManaged(std::string()); | |
| 1156 } | |
| 1157 newTabPageIsHomePage_.SetValueIfNotManaged(useNewTabPage); | |
| 1158 } | |
| 1159 | |
| 1160 // Check whether the new tab and URL homepage radios should be enabled, i.e. if | |
| 1161 // the corresponding preference is not managed through configuration policy. | |
| 1162 - (BOOL)isHomepageChoiceEnabled { | |
| 1163 return !newTabPageIsHomePage_.IsManaged() && | |
| 1164 !(homepage_.IsManaged() && [self isHomepageNewTabUIURL]); | |
| 1165 } | |
| 1166 | |
| 1167 // Returns whether or not the homepage URL text field should be enabled | |
| 1168 // based on if the new tab page is the home page. | |
| 1169 - (BOOL)isHomepageURLEnabled { | |
| 1170 return !newTabPageIsHomePage_.GetValue() && !homepage_.IsManaged() && | |
| 1171 !([self isHomepageNewTabUIURL] && !newTabPageIsHomePage_.IsManaged()); | |
| 1172 } | |
| 1173 | |
| 1174 // Returns the homepage URL. | |
| 1175 - (NSString*)homepageURL { | |
| 1176 NSString* value = base::SysUTF8ToNSString(homepage_.GetValue()); | |
| 1177 return [self isHomepageNewTabUIURL] ? nil : value; | |
| 1178 } | |
| 1179 | |
| 1180 // Sets the homepage URL to |urlString| with some fixing up. | |
| 1181 - (void)setHomepageURL:(NSString*)urlString { | |
| 1182 // If the text field contains a valid URL, sync it to prefs. We run it | |
| 1183 // through the fixer upper to allow input like "google.com" to be converted | |
| 1184 // to something valid ("http://google.com"). | |
| 1185 std::string unfixedURL = urlString ? base::SysNSStringToUTF8(urlString) : | |
| 1186 chrome::kChromeUINewTabURL; | |
| 1187 [self setHomepage:URLFixerUpper::FixupURL(unfixedURL, std::string())]; | |
| 1188 } | |
| 1189 | |
| 1190 // Returns whether the home button should be checked based on the preference. | |
| 1191 - (BOOL)showHomeButton { | |
| 1192 return showHomeButton_.GetValue() ? YES : NO; | |
| 1193 } | |
| 1194 | |
| 1195 // Sets the backend pref for whether or not the home button should be displayed | |
| 1196 // based on |value|. | |
| 1197 - (void)setShowHomeButton:(BOOL)value { | |
| 1198 if (value) | |
| 1199 [self recordUserAction:UserMetricsAction( | |
| 1200 "Options_Homepage_ShowHomeButton")]; | |
| 1201 else | |
| 1202 [self recordUserAction:UserMetricsAction( | |
| 1203 "Options_Homepage_HideHomeButton")]; | |
| 1204 showHomeButton_.SetValueIfNotManaged(value ? true : false); | |
| 1205 } | |
| 1206 | |
| 1207 // Getter for the |searchEngineModel| property for bindings. | |
| 1208 - (id)searchEngineModel { | |
| 1209 return searchEngineModel_.get(); | |
| 1210 } | |
| 1211 | |
| 1212 // Bindings for the search engine popup. We not binding directly to the model | |
| 1213 // in order to siphon off the setter so we can record the metric. If we're | |
| 1214 // doing it with one, might as well do it with both. | |
| 1215 - (NSUInteger)searchEngineSelectedIndex { | |
| 1216 return [searchEngineModel_ defaultIndex]; | |
| 1217 } | |
| 1218 | |
| 1219 - (void)setSearchEngineSelectedIndex:(NSUInteger)index { | |
| 1220 [self recordUserAction:UserMetricsAction("Options_SearchEngineChanged")]; | |
| 1221 [searchEngineModel_ setDefaultIndex:index]; | |
| 1222 } | |
| 1223 | |
| 1224 // Called when the search engine model changes. Update the selection in the | |
| 1225 // popup by tickling the bindings with the new value. | |
| 1226 - (void)searchEngineModelChanged:(NSNotification*)notify { | |
| 1227 [self setSearchEngineSelectedIndex:[self searchEngineSelectedIndex]]; | |
| 1228 [self setDefaultSearchEngineEnabled:![searchEngineModel_ isDefaultManaged]]; | |
| 1229 | |
| 1230 } | |
| 1231 | |
| 1232 - (IBAction)manageSearchEngines:(id)sender { | |
| 1233 [KeywordEditorCocoaController showKeywordEditor:profile_]; | |
| 1234 } | |
| 1235 | |
| 1236 - (IBAction)toggleInstant:(id)sender { | |
| 1237 if (instantEnabled_.GetValue()) { | |
| 1238 InstantController::Disable(profile_); | |
| 1239 } else { | |
| 1240 [instantCheckbox_ setState:NSOffState]; | |
| 1241 browser::ShowInstantConfirmDialogIfNecessary([self window], profile_); | |
| 1242 } | |
| 1243 } | |
| 1244 | |
| 1245 // Sets the state of the Instant checkbox and adds the type information to the | |
| 1246 // label. | |
| 1247 - (void)configureInstant { | |
| 1248 bool enabled = instantEnabled_.GetValue(); | |
| 1249 NSInteger state = enabled ? NSOnState : NSOffState; | |
| 1250 [instantCheckbox_ setState:state]; | |
| 1251 } | |
| 1252 | |
| 1253 - (IBAction)learnMoreAboutInstant:(id)sender { | |
| 1254 browser::ShowOptionsURL(profile_, browser::InstantLearnMoreURL()); | |
| 1255 } | |
| 1256 | |
| 1257 // Called when the user clicks the button to make Chromium the default | |
| 1258 // browser. Registers http and https. | |
| 1259 - (IBAction)makeDefaultBrowser:(id)sender { | |
| 1260 [self willChangeValueForKey:@"defaultBrowser"]; | |
| 1261 | |
| 1262 ShellIntegration::SetAsDefaultBrowser(); | |
| 1263 [self recordUserAction:UserMetricsAction("Options_SetAsDefaultBrowser")]; | |
| 1264 // If the user made Chrome the default browser, then he/she arguably wants | |
| 1265 // to be notified when that changes. | |
| 1266 prefs_->SetBoolean(prefs::kCheckDefaultBrowser, true); | |
| 1267 | |
| 1268 // Tickle KVO so that the UI updates. | |
| 1269 [self didChangeValueForKey:@"defaultBrowser"]; | |
| 1270 } | |
| 1271 | |
| 1272 // Returns the Chromium default browser state and whether this is user | |
| 1273 // controlled or locked by a policy. | |
| 1274 - (BOOL)canChangeDefaultBrowser { | |
| 1275 return !default_browser_policy_.IsManaged() && | |
| 1276 ShellIntegration::IsDefaultBrowser() != | |
| 1277 ShellIntegration::IS_DEFAULT_BROWSER; | |
| 1278 } | |
| 1279 | |
| 1280 // Returns the text color of the "chromium is your default browser" text (green | |
| 1281 // for yes, red for no). | |
| 1282 - (NSColor*)defaultBrowserTextColor { | |
| 1283 ShellIntegration::DefaultBrowserState state = | |
| 1284 ShellIntegration::IsDefaultBrowser(); | |
| 1285 return (state == ShellIntegration::IS_DEFAULT_BROWSER) ? | |
| 1286 [NSColor colorWithCalibratedRed:0.0 green:135.0/255.0 blue:0 alpha:1.0] : | |
| 1287 [NSColor colorWithCalibratedRed:135.0/255.0 green:0 blue:0 alpha:1.0]; | |
| 1288 } | |
| 1289 | |
| 1290 // Returns the text for the "chromium is your default browser" string dependent | |
| 1291 // on if Chromium actually is or not. | |
| 1292 - (NSString*)defaultBrowserText { | |
| 1293 ShellIntegration::DefaultBrowserState state = | |
| 1294 ShellIntegration::IsDefaultBrowser(); | |
| 1295 int stringId; | |
| 1296 if (state == ShellIntegration::IS_DEFAULT_BROWSER) | |
| 1297 stringId = IDS_OPTIONS_DEFAULTBROWSER_DEFAULT; | |
| 1298 else if (state == ShellIntegration::NOT_DEFAULT_BROWSER) | |
| 1299 stringId = IDS_OPTIONS_DEFAULTBROWSER_NOTDEFAULT; | |
| 1300 else | |
| 1301 stringId = IDS_OPTIONS_DEFAULTBROWSER_UNKNOWN; | |
| 1302 string16 text = | |
| 1303 l10n_util::GetStringFUTF16(stringId, | |
| 1304 l10n_util::GetStringUTF16(IDS_PRODUCT_NAME)); | |
| 1305 return base::SysUTF16ToNSString(text); | |
| 1306 } | |
| 1307 | |
| 1308 //------------------------------------------------------------------------- | |
| 1309 // User Data panel | |
| 1310 | |
| 1311 // Since passwords and forms are radio groups, 'enabled' is index 0 and | |
| 1312 // 'disabled' is index 1. Yay. | |
| 1313 const int kEnabledIndex = 0; | |
| 1314 const int kDisabledIndex = 1; | |
| 1315 | |
| 1316 // Callback when preferences are changed. |prefName| is the name of the pref | |
| 1317 // that has changed. Unlike on Windows, we don't need to use this method for | |
| 1318 // initializing, that's handled by Cocoa Bindings. | |
| 1319 // Handles prefs for the "Personal Stuff" panel. | |
| 1320 - (void)userDataPrefChanged:(std::string*)prefName { | |
| 1321 if (*prefName == prefs::kPasswordManagerEnabled) { | |
| 1322 [self setPasswordManagerEnabledIndex:askSavePasswords_.GetValue() ? | |
| 1323 kEnabledIndex : kDisabledIndex]; | |
| 1324 [self setPasswordManagerChoiceEnabled:!askSavePasswords_.IsManaged()]; | |
| 1325 [self setPasswordManagerButtonEnabled: | |
| 1326 !askSavePasswords_.IsManaged() || askSavePasswords_.GetValue()]; | |
| 1327 } | |
| 1328 if (*prefName == prefs::kAutoFillEnabled) { | |
| 1329 bool autofill_disabled_by_policy = | |
| 1330 autoFillEnabled_.IsManaged() && !autoFillEnabled_.GetValue(); | |
| 1331 [self setAutoFillSettingsButtonEnabled:!autofill_disabled_by_policy]; | |
| 1332 } | |
| 1333 if (*prefName == prefs::kCurrentThemeID) { | |
| 1334 [self setIsUsingDefaultTheme:currentTheme_.GetValue().length() == 0]; | |
| 1335 } | |
| 1336 } | |
| 1337 | |
| 1338 // Called to launch the Keychain Access app to show the user's stored | |
| 1339 // passwords. | |
| 1340 - (IBAction)showSavedPasswords:(id)sender { | |
| 1341 [self recordUserAction:UserMetricsAction("Options_ShowPasswordsExceptions")]; | |
| 1342 [self launchKeychainAccess]; | |
| 1343 } | |
| 1344 | |
| 1345 // Called to show the Auto Fill Settings dialog. | |
| 1346 - (IBAction)showAutoFillSettings:(id)sender { | |
| 1347 [self recordUserAction:UserMetricsAction("Options_ShowAutoFillSettings")]; | |
| 1348 | |
| 1349 PersonalDataManager* personalDataManager = profile_->GetPersonalDataManager(); | |
| 1350 if (!personalDataManager) { | |
| 1351 // Should not reach here because button is disabled when | |
| 1352 // |personalDataManager| is NULL. | |
| 1353 NOTREACHED(); | |
| 1354 return; | |
| 1355 } | |
| 1356 | |
| 1357 ShowAutoFillDialog(NULL, personalDataManager, profile_); | |
| 1358 } | |
| 1359 | |
| 1360 // Called to import data from other browsers (Safari, Firefox, etc). | |
| 1361 - (IBAction)importData:(id)sender { | |
| 1362 UserMetrics::RecordAction(UserMetricsAction("Import_ShowDlg"), profile_); | |
| 1363 [ImportDialogController showImportSettingsDialogForProfile:profile_]; | |
| 1364 } | |
| 1365 | |
| 1366 - (IBAction)resetThemeToDefault:(id)sender { | |
| 1367 [self recordUserAction:UserMetricsAction("Options_ThemesReset")]; | |
| 1368 profile_->ClearTheme(); | |
| 1369 } | |
| 1370 | |
| 1371 - (IBAction)themesGallery:(id)sender { | |
| 1372 [self recordUserAction:UserMetricsAction("Options_ThemesGallery")]; | |
| 1373 Browser* browser = BrowserList::GetLastActive(); | |
| 1374 | |
| 1375 if (!browser || !browser->GetSelectedTabContents()) | |
| 1376 browser = Browser::Create(profile_); | |
| 1377 browser->OpenThemeGalleryTabAndActivate(); | |
| 1378 } | |
| 1379 | |
| 1380 // Called when the "stop syncing" confirmation dialog started by | |
| 1381 // doSyncAction is finished. Stop syncing only If the user clicked | |
| 1382 // OK. | |
| 1383 - (void)stopSyncAlertDidEnd:(NSAlert*)alert | |
| 1384 returnCode:(int)returnCode | |
| 1385 contextInfo:(void*)contextInfo { | |
| 1386 DCHECK(syncService_ && !syncService_->IsManaged()); | |
| 1387 if (returnCode == NSAlertFirstButtonReturn) { | |
| 1388 syncService_->DisableForUser(); | |
| 1389 ProfileSyncService::SyncEvent(ProfileSyncService::STOP_FROM_OPTIONS); | |
| 1390 } | |
| 1391 } | |
| 1392 | |
| 1393 // Called when the user clicks the multi-purpose sync button in the | |
| 1394 // "Personal Stuff" pane. | |
| 1395 - (IBAction)doSyncAction:(id)sender { | |
| 1396 DCHECK(syncService_ && !syncService_->IsManaged()); | |
| 1397 if (syncService_->HasSyncSetupCompleted()) { | |
| 1398 // If sync setup has completed that means the sync button was a | |
| 1399 // "stop syncing" button. Bring up a confirmation dialog before | |
| 1400 // actually stopping syncing (see stopSyncAlertDidEnd). | |
| 1401 scoped_nsobject<NSAlert> alert([[NSAlert alloc] init]); | |
| 1402 [alert addButtonWithTitle:l10n_util::GetNSStringWithFixup( | |
| 1403 IDS_SYNC_STOP_SYNCING_CONFIRM_BUTTON_LABEL)]; | |
| 1404 [alert addButtonWithTitle:l10n_util::GetNSStringWithFixup( | |
| 1405 IDS_CANCEL)]; | |
| 1406 [alert setMessageText:l10n_util::GetNSStringWithFixup( | |
| 1407 IDS_SYNC_STOP_SYNCING_DIALOG_TITLE)]; | |
| 1408 [alert setInformativeText:l10n_util::GetNSStringWithFixup( | |
| 1409 IDS_SYNC_STOP_SYNCING_EXPLANATION_LABEL)]; | |
| 1410 [alert setAlertStyle:NSWarningAlertStyle]; | |
| 1411 const SEL kEndSelector = | |
| 1412 @selector(stopSyncAlertDidEnd:returnCode:contextInfo:); | |
| 1413 [alert beginSheetModalForWindow:[self window] | |
| 1414 modalDelegate:self | |
| 1415 didEndSelector:kEndSelector | |
| 1416 contextInfo:NULL]; | |
| 1417 } else { | |
| 1418 // Otherwise, the sync button was a "sync my bookmarks" button. | |
| 1419 // Kick off the sync setup process. | |
| 1420 syncService_->ShowLoginDialog(NULL); | |
| 1421 ProfileSyncService::SyncEvent(ProfileSyncService::START_FROM_OPTIONS); | |
| 1422 } | |
| 1423 } | |
| 1424 | |
| 1425 // Called when the user clicks on the link to the privacy dashboard. | |
| 1426 - (IBAction)showPrivacyDashboard:(id)sender { | |
| 1427 Browser* browser = BrowserList::GetLastActive(); | |
| 1428 | |
| 1429 if (!browser || !browser->GetSelectedTabContents()) | |
| 1430 browser = Browser::Create(profile_); | |
| 1431 browser->OpenPrivacyDashboardTabAndActivate(); | |
| 1432 } | |
| 1433 | |
| 1434 // Called when the user clicks the "Customize Sync" button in the | |
| 1435 // "Personal Stuff" pane. Spawns a dialog-modal sheet that cleans | |
| 1436 // itself up on close. | |
| 1437 - (IBAction)doSyncCustomize:(id)sender { | |
| 1438 syncService_->ShowConfigure(NULL); | |
| 1439 } | |
| 1440 | |
| 1441 // Called when the user clicks on the multi-purpose 'sync problem' | |
| 1442 // link. | |
| 1443 - (IBAction)doSyncReauthentication:(id)sender { | |
| 1444 DCHECK(syncService_ && !syncService_->IsManaged()); | |
| 1445 syncService_->ShowErrorUI(NULL); | |
| 1446 } | |
| 1447 | |
| 1448 - (void)setPasswordManagerEnabledIndex:(NSInteger)value { | |
| 1449 if (value == kEnabledIndex) | |
| 1450 [self recordUserAction:UserMetricsAction( | |
| 1451 "Options_PasswordManager_Enable")]; | |
| 1452 else | |
| 1453 [self recordUserAction:UserMetricsAction( | |
| 1454 "Options_PasswordManager_Disable")]; | |
| 1455 askSavePasswords_.SetValueIfNotManaged(value == kEnabledIndex ? true : false); | |
| 1456 } | |
| 1457 | |
| 1458 - (NSInteger)passwordManagerEnabledIndex { | |
| 1459 return askSavePasswords_.GetValue() ? kEnabledIndex : kDisabledIndex; | |
| 1460 } | |
| 1461 | |
| 1462 - (void)setIsUsingDefaultTheme:(BOOL)value { | |
| 1463 if (value) | |
| 1464 [self recordUserAction:UserMetricsAction( | |
| 1465 "Options_IsUsingDefaultTheme_Enable")]; | |
| 1466 else | |
| 1467 [self recordUserAction:UserMetricsAction( | |
| 1468 "Options_IsUsingDefaultTheme_Disable")]; | |
| 1469 } | |
| 1470 | |
| 1471 - (BOOL)isUsingDefaultTheme { | |
| 1472 return currentTheme_.GetValue().length() == 0; | |
| 1473 } | |
| 1474 | |
| 1475 //------------------------------------------------------------------------- | |
| 1476 // Under the hood panel | |
| 1477 | |
| 1478 // Callback when preferences are changed. |prefName| is the name of the pref | |
| 1479 // that has changed. Unlike on Windows, we don't need to use this method for | |
| 1480 // initializing, that's handled by Cocoa Bindings. | |
| 1481 // Handles prefs for the "Under the hood" panel. | |
| 1482 - (void)underHoodPrefChanged:(std::string*)prefName { | |
| 1483 if (*prefName == prefs::kAlternateErrorPagesEnabled) { | |
| 1484 [self setShowAlternateErrorPages: | |
| 1485 alternateErrorPages_.GetValue() ? YES : NO]; | |
| 1486 [self setShowAlternateErrorPagesEnabled:!alternateErrorPages_.IsManaged()]; | |
| 1487 } | |
| 1488 else if (*prefName == prefs::kSearchSuggestEnabled) { | |
| 1489 [self setUseSuggest:useSuggest_.GetValue() ? YES : NO]; | |
| 1490 [self setUseSuggestEnabled:!useSuggest_.IsManaged()]; | |
| 1491 } | |
| 1492 else if (*prefName == prefs::kDnsPrefetchingEnabled) { | |
| 1493 [self setDnsPrefetch:dnsPrefetch_.GetValue() ? YES : NO]; | |
| 1494 [self setDnsPrefetchEnabled:!dnsPrefetch_.IsManaged()]; | |
| 1495 } | |
| 1496 else if (*prefName == prefs::kSafeBrowsingEnabled) { | |
| 1497 [self setSafeBrowsing:safeBrowsing_.GetValue() ? YES : NO]; | |
| 1498 [self setSafeBrowsingEnabled:!safeBrowsing_.IsManaged()]; | |
| 1499 } | |
| 1500 else if (*prefName == prefs::kMetricsReportingEnabled) { | |
| 1501 [self setMetricsReporting:metricsReporting_.GetValue() ? YES : NO]; | |
| 1502 [self setMetricsReportingEnabled:!metricsReporting_.IsManaged()]; | |
| 1503 } | |
| 1504 else if (*prefName == prefs::kDownloadDefaultDirectory) { | |
| 1505 [self setDownloadLocationEnabled:!defaultDownloadLocation_.IsManaged()]; | |
| 1506 // Poke KVO. | |
| 1507 [self willChangeValueForKey:@"defaultDownloadLocation"]; | |
| 1508 [self didChangeValueForKey:@"defaultDownloadLocation"]; | |
| 1509 } | |
| 1510 else if (*prefName == prefs::kPromptForDownload) { | |
| 1511 [self setAskForSaveLocation:askForSaveLocation_.GetValue() ? YES : NO]; | |
| 1512 } | |
| 1513 else if (*prefName == prefs::kEnableTranslate) { | |
| 1514 [self setTranslateEnabled:translateEnabled_.GetValue() ? YES : NO]; | |
| 1515 } | |
| 1516 else if (*prefName == prefs::kWebkitTabsToLinks) { | |
| 1517 [self setTabsToLinks:tabsToLinks_.GetValue() ? YES : NO]; | |
| 1518 } | |
| 1519 else if (*prefName == prefs::kDownloadExtensionsToOpen) { | |
| 1520 // Poke KVC. | |
| 1521 [self setFileHandlerUIEnabled:[self fileHandlerUIEnabled]]; | |
| 1522 } | |
| 1523 else if (proxyPrefs_->IsObserved(*prefName)) { | |
| 1524 [self setProxiesConfigureButtonEnabled:!proxyPrefs_->IsManaged()]; | |
| 1525 } | |
| 1526 } | |
| 1527 | |
| 1528 // Set the new download path and notify the UI via KVO. | |
| 1529 - (void)downloadPathPanelDidEnd:(NSOpenPanel*)panel | |
| 1530 code:(NSInteger)returnCode | |
| 1531 context:(void*)context { | |
| 1532 if (returnCode == NSOKButton) { | |
| 1533 [self recordUserAction:UserMetricsAction("Options_SetDownloadDirectory")]; | |
| 1534 NSURL* path = [[panel URLs] lastObject]; // We only allow 1 item. | |
| 1535 [self willChangeValueForKey:@"defaultDownloadLocation"]; | |
| 1536 defaultDownloadLocation_.SetValue(base::SysNSStringToUTF8([path path])); | |
| 1537 [self didChangeValueForKey:@"defaultDownloadLocation"]; | |
| 1538 } | |
| 1539 } | |
| 1540 | |
| 1541 // Bring up an open panel to allow the user to set a new downloads location. | |
| 1542 - (void)browseDownloadLocation:(id)sender { | |
| 1543 NSOpenPanel* panel = [NSOpenPanel openPanel]; | |
| 1544 [panel setAllowsMultipleSelection:NO]; | |
| 1545 [panel setCanChooseFiles:NO]; | |
| 1546 [panel setCanChooseDirectories:YES]; | |
| 1547 NSString* path = base::SysUTF8ToNSString(defaultDownloadLocation_.GetValue()); | |
| 1548 [panel beginSheetForDirectory:path | |
| 1549 file:nil | |
| 1550 types:nil | |
| 1551 modalForWindow:[self window] | |
| 1552 modalDelegate:self | |
| 1553 didEndSelector:@selector(downloadPathPanelDidEnd:code:context:) | |
| 1554 contextInfo:NULL]; | |
| 1555 } | |
| 1556 | |
| 1557 // Called to clear user's browsing data. This puts up an application-modal | |
| 1558 // dialog to guide the user through clearing the data. | |
| 1559 - (IBAction)clearData:(id)sender { | |
| 1560 [ClearBrowsingDataController | |
| 1561 showClearBrowsingDialogForProfile:profile_]; | |
| 1562 } | |
| 1563 | |
| 1564 // Opens the "Content Settings" dialog. | |
| 1565 - (IBAction)showContentSettings:(id)sender { | |
| 1566 [ContentSettingsDialogController | |
| 1567 showContentSettingsForType:CONTENT_SETTINGS_TYPE_DEFAULT | |
| 1568 profile:profile_]; | |
| 1569 } | |
| 1570 | |
| 1571 - (IBAction)privacyLearnMore:(id)sender { | |
| 1572 GURL url = google_util::AppendGoogleLocaleParam( | |
| 1573 GURL(chrome::kPrivacyLearnMoreURL)); | |
| 1574 // We open a new browser window so the Options dialog doesn't get lost | |
| 1575 // behind other windows. | |
| 1576 browser::ShowOptionsURL(profile_, url); | |
| 1577 } | |
| 1578 | |
| 1579 - (IBAction)resetAutoOpenFiles:(id)sender { | |
| 1580 profile_->GetDownloadManager()->download_prefs()->ResetAutoOpen(); | |
| 1581 [self recordUserAction:UserMetricsAction("Options_ResetAutoOpenFiles")]; | |
| 1582 } | |
| 1583 | |
| 1584 - (IBAction)openProxyPreferences:(id)sender { | |
| 1585 NSArray* itemsToOpen = [NSArray arrayWithObject:[NSURL fileURLWithPath: | |
| 1586 @"/System/Library/PreferencePanes/Network.prefPane"]]; | |
| 1587 | |
| 1588 const char* proxyPrefCommand = "Proxies"; | |
| 1589 base::mac::ScopedAEDesc<> openParams; | |
| 1590 OSStatus status = AECreateDesc('ptru', | |
| 1591 proxyPrefCommand, | |
| 1592 strlen(proxyPrefCommand), | |
| 1593 openParams.OutPointer()); | |
| 1594 LOG_IF(ERROR, status != noErr) << "Failed to create open params: " << status; | |
| 1595 | |
| 1596 LSLaunchURLSpec launchSpec = { 0 }; | |
| 1597 launchSpec.itemURLs = (CFArrayRef)itemsToOpen; | |
| 1598 launchSpec.passThruParams = openParams; | |
| 1599 launchSpec.launchFlags = kLSLaunchAsync | kLSLaunchDontAddToRecents; | |
| 1600 LSOpenFromURLSpec(&launchSpec, NULL); | |
| 1601 } | |
| 1602 | |
| 1603 // Returns whether the alternate error page checkbox should be checked based | |
| 1604 // on the preference. | |
| 1605 - (BOOL)showAlternateErrorPages { | |
| 1606 return alternateErrorPages_.GetValue() ? YES : NO; | |
| 1607 } | |
| 1608 | |
| 1609 // Sets the backend pref for whether or not the alternate error page checkbox | |
| 1610 // should be displayed based on |value|. | |
| 1611 - (void)setShowAlternateErrorPages:(BOOL)value { | |
| 1612 if (value) | |
| 1613 [self recordUserAction:UserMetricsAction( | |
| 1614 "Options_LinkDoctorCheckbox_Enable")]; | |
| 1615 else | |
| 1616 [self recordUserAction:UserMetricsAction( | |
| 1617 "Options_LinkDoctorCheckbox_Disable")]; | |
| 1618 alternateErrorPages_.SetValueIfNotManaged(value ? true : false); | |
| 1619 } | |
| 1620 | |
| 1621 // Returns whether the suggest checkbox should be checked based on the | |
| 1622 // preference. | |
| 1623 - (BOOL)useSuggest { | |
| 1624 return useSuggest_.GetValue() ? YES : NO; | |
| 1625 } | |
| 1626 | |
| 1627 // Sets the backend pref for whether or not the suggest checkbox should be | |
| 1628 // displayed based on |value|. | |
| 1629 - (void)setUseSuggest:(BOOL)value { | |
| 1630 if (value) | |
| 1631 [self recordUserAction:UserMetricsAction( | |
| 1632 "Options_UseSuggestCheckbox_Enable")]; | |
| 1633 else | |
| 1634 [self recordUserAction:UserMetricsAction( | |
| 1635 "Options_UseSuggestCheckbox_Disable")]; | |
| 1636 useSuggest_.SetValueIfNotManaged(value ? true : false); | |
| 1637 } | |
| 1638 | |
| 1639 // Returns whether the DNS prefetch checkbox should be checked based on the | |
| 1640 // preference. | |
| 1641 - (BOOL)dnsPrefetch { | |
| 1642 return dnsPrefetch_.GetValue() ? YES : NO; | |
| 1643 } | |
| 1644 | |
| 1645 // Sets the backend pref for whether or not the DNS prefetch checkbox should be | |
| 1646 // displayed based on |value|. | |
| 1647 - (void)setDnsPrefetch:(BOOL)value { | |
| 1648 if (value) | |
| 1649 [self recordUserAction:UserMetricsAction( | |
| 1650 "Options_DnsPrefetchCheckbox_Enable")]; | |
| 1651 else | |
| 1652 [self recordUserAction:UserMetricsAction( | |
| 1653 "Options_DnsPrefetchCheckbox_Disable")]; | |
| 1654 dnsPrefetch_.SetValueIfNotManaged(value ? true : false); | |
| 1655 } | |
| 1656 | |
| 1657 // Returns whether the safe browsing checkbox should be checked based on the | |
| 1658 // preference. | |
| 1659 - (BOOL)safeBrowsing { | |
| 1660 return safeBrowsing_.GetValue() ? YES : NO; | |
| 1661 } | |
| 1662 | |
| 1663 // Sets the backend pref for whether or not the safe browsing checkbox should be | |
| 1664 // displayed based on |value|. | |
| 1665 - (void)setSafeBrowsing:(BOOL)value { | |
| 1666 if (value) | |
| 1667 [self recordUserAction:UserMetricsAction( | |
| 1668 "Options_SafeBrowsingCheckbox_Enable")]; | |
| 1669 else | |
| 1670 [self recordUserAction:UserMetricsAction( | |
| 1671 "Options_SafeBrowsingCheckbox_Disable")]; | |
| 1672 safeBrowsing_.SetValueIfNotManaged(value ? true : false); | |
| 1673 SafeBrowsingService* safeBrowsingService = | |
| 1674 g_browser_process->resource_dispatcher_host()->safe_browsing_service(); | |
| 1675 MessageLoop::current()->PostTask( | |
| 1676 FROM_HERE, | |
| 1677 NewRunnableMethod(safeBrowsingService, | |
| 1678 &SafeBrowsingService::OnEnable, | |
| 1679 safeBrowsing_.GetValue())); | |
| 1680 } | |
| 1681 | |
| 1682 // Returns whether the metrics reporting checkbox should be checked based on the | |
| 1683 // preference. | |
| 1684 - (BOOL)metricsReporting { | |
| 1685 return metricsReporting_.GetValue() ? YES : NO; | |
| 1686 } | |
| 1687 | |
| 1688 // Sets the backend pref for whether or not the metrics reporting checkbox | |
| 1689 // should be displayed based on |value|. | |
| 1690 - (void)setMetricsReporting:(BOOL)value { | |
| 1691 if (value) | |
| 1692 [self recordUserAction:UserMetricsAction( | |
| 1693 "Options_MetricsReportingCheckbox_Enable")]; | |
| 1694 else | |
| 1695 [self recordUserAction:UserMetricsAction( | |
| 1696 "Options_MetricsReportingCheckbox_Disable")]; | |
| 1697 | |
| 1698 // TODO(pinkerton): windows shows a dialog here telling the user they need to | |
| 1699 // restart for this to take effect. http://crbug.com/34653 | |
| 1700 metricsReporting_.SetValueIfNotManaged(value ? true : false); | |
| 1701 | |
| 1702 bool enabled = metricsReporting_.GetValue(); | |
| 1703 GoogleUpdateSettings::SetCollectStatsConsent(enabled); | |
| 1704 bool update_pref = GoogleUpdateSettings::GetCollectStatsConsent(); | |
| 1705 if (enabled != update_pref) { | |
| 1706 DVLOG(1) << "GENERAL SECTION: Unable to set crash report status to " | |
| 1707 << enabled; | |
| 1708 } | |
| 1709 // Only change the pref if GoogleUpdateSettings::GetCollectStatsConsent | |
| 1710 // succeeds. | |
| 1711 enabled = update_pref; | |
| 1712 | |
| 1713 MetricsService* metrics = g_browser_process->metrics_service(); | |
| 1714 DCHECK(metrics); | |
| 1715 if (metrics) { | |
| 1716 metrics->SetUserPermitsUpload(enabled); | |
| 1717 if (enabled) | |
| 1718 metrics->Start(); | |
| 1719 else | |
| 1720 metrics->Stop(); | |
| 1721 } | |
| 1722 } | |
| 1723 | |
| 1724 - (NSURL*)defaultDownloadLocation { | |
| 1725 NSString* pathString = | |
| 1726 base::SysUTF8ToNSString(defaultDownloadLocation_.GetValue()); | |
| 1727 return [NSURL fileURLWithPath:pathString]; | |
| 1728 } | |
| 1729 | |
| 1730 - (BOOL)askForSaveLocation { | |
| 1731 return askForSaveLocation_.GetValue(); | |
| 1732 } | |
| 1733 | |
| 1734 - (void)setAskForSaveLocation:(BOOL)value { | |
| 1735 if (value) { | |
| 1736 [self recordUserAction:UserMetricsAction( | |
| 1737 "Options_AskForSaveLocation_Enable")]; | |
| 1738 } else { | |
| 1739 [self recordUserAction:UserMetricsAction( | |
| 1740 "Options_AskForSaveLocation_Disable")]; | |
| 1741 } | |
| 1742 askForSaveLocation_.SetValue(value); | |
| 1743 } | |
| 1744 | |
| 1745 - (BOOL)fileHandlerUIEnabled { | |
| 1746 if (!profile_->GetDownloadManager()) // Not set in unit tests. | |
| 1747 return NO; | |
| 1748 return profile_->GetDownloadManager()->download_prefs()->IsAutoOpenUsed(); | |
| 1749 } | |
| 1750 | |
| 1751 - (void)setFileHandlerUIEnabled:(BOOL)value { | |
| 1752 [resetFileHandlersButton_ setEnabled:value]; | |
| 1753 } | |
| 1754 | |
| 1755 - (BOOL)translateEnabled { | |
| 1756 return translateEnabled_.GetValue(); | |
| 1757 } | |
| 1758 | |
| 1759 - (void)setTranslateEnabled:(BOOL)value { | |
| 1760 if (value) { | |
| 1761 [self recordUserAction:UserMetricsAction("Options_Translate_Enable")]; | |
| 1762 } else { | |
| 1763 [self recordUserAction:UserMetricsAction("Options_Translate_Disable")]; | |
| 1764 } | |
| 1765 translateEnabled_.SetValue(value); | |
| 1766 } | |
| 1767 | |
| 1768 - (BOOL)tabsToLinks { | |
| 1769 return tabsToLinks_.GetValue(); | |
| 1770 } | |
| 1771 | |
| 1772 - (void)setTabsToLinks:(BOOL)value { | |
| 1773 if (value) { | |
| 1774 [self recordUserAction:UserMetricsAction("Options_TabsToLinks_Enable")]; | |
| 1775 } else { | |
| 1776 [self recordUserAction:UserMetricsAction("Options_TabsToLinks_Disable")]; | |
| 1777 } | |
| 1778 tabsToLinks_.SetValue(value); | |
| 1779 } | |
| 1780 | |
| 1781 - (void)fontAndLanguageEndSheet:(NSWindow*)sheet | |
| 1782 returnCode:(NSInteger)returnCode | |
| 1783 contextInfo:(void*)context { | |
| 1784 [sheet close]; | |
| 1785 [sheet orderOut:self]; | |
| 1786 fontLanguageSettings_ = nil; | |
| 1787 } | |
| 1788 | |
| 1789 - (IBAction)changeFontAndLanguageSettings:(id)sender { | |
| 1790 // Intentionally leak the controller as it will clean itself up when the | |
| 1791 // sheet closes. | |
| 1792 fontLanguageSettings_ = | |
| 1793 [[FontLanguageSettingsController alloc] initWithProfile:profile_]; | |
| 1794 [NSApp beginSheet:[fontLanguageSettings_ window] | |
| 1795 modalForWindow:[self window] | |
| 1796 modalDelegate:self | |
| 1797 didEndSelector:@selector(fontAndLanguageEndSheet:returnCode:contextInfo:) | |
| 1798 contextInfo:nil]; | |
| 1799 } | |
| 1800 | |
| 1801 // Called to launch the Keychain Access app to show the user's stored | |
| 1802 // certificates. Note there's no way to script the app to auto-select the | |
| 1803 // certificates. | |
| 1804 - (IBAction)showCertificates:(id)sender { | |
| 1805 [self recordUserAction:UserMetricsAction("Options_ManagerCerts")]; | |
| 1806 [self launchKeychainAccess]; | |
| 1807 } | |
| 1808 | |
| 1809 - (IBAction)resetToDefaults:(id)sender { | |
| 1810 // The alert will clean itself up in the did-end selector. | |
| 1811 NSAlert* alert = [[NSAlert alloc] init]; | |
| 1812 [alert setMessageText:l10n_util::GetNSString(IDS_OPTIONS_RESET_MESSAGE)]; | |
| 1813 NSButton* resetButton = [alert addButtonWithTitle: | |
| 1814 l10n_util::GetNSString(IDS_OPTIONS_RESET_OKLABEL)]; | |
| 1815 [resetButton setKeyEquivalent:@""]; | |
| 1816 NSButton* cancelButton = [alert addButtonWithTitle: | |
| 1817 l10n_util::GetNSString(IDS_OPTIONS_RESET_CANCELLABEL)]; | |
| 1818 [cancelButton setKeyEquivalent:@"\r"]; | |
| 1819 | |
| 1820 [alert beginSheetModalForWindow:[self window] | |
| 1821 modalDelegate:self | |
| 1822 didEndSelector:@selector(resetToDefaults:returned:context:) | |
| 1823 contextInfo:nil]; | |
| 1824 } | |
| 1825 | |
| 1826 - (void)resetToDefaults:(NSAlert*)alert | |
| 1827 returned:(NSInteger)code | |
| 1828 context:(void*)context { | |
| 1829 if (code == NSAlertFirstButtonReturn) { | |
| 1830 OptionsUtil::ResetToDefaults(profile_); | |
| 1831 } | |
| 1832 [alert autorelease]; | |
| 1833 } | |
| 1834 | |
| 1835 //------------------------------------------------------------------------- | |
| 1836 | |
| 1837 // Callback when preferences are changed. |prefName| is the name of the | |
| 1838 // pref that has changed and should not be NULL. | |
| 1839 - (void)prefChanged:(std::string*)prefName { | |
| 1840 DCHECK(prefName); | |
| 1841 if (!prefName) return; | |
| 1842 [self basicsPrefChanged:prefName]; | |
| 1843 [self userDataPrefChanged:prefName]; | |
| 1844 [self underHoodPrefChanged:prefName]; | |
| 1845 } | |
| 1846 | |
| 1847 // Callback when sync service state has changed. | |
| 1848 // | |
| 1849 // TODO(akalin): Decomp this out since a lot of it is copied from the | |
| 1850 // Windows version. | |
| 1851 // TODO(akalin): Change the background of the status label/link on error. | |
| 1852 - (void)syncStateChanged { | |
| 1853 DCHECK(syncService_); | |
| 1854 | |
| 1855 string16 statusLabel, linkLabel; | |
| 1856 sync_ui_util::MessageType status = | |
| 1857 sync_ui_util::GetStatusLabels(syncService_, &statusLabel, &linkLabel); | |
| 1858 bool managed = syncService_->IsManaged(); | |
| 1859 | |
| 1860 [syncButton_ setEnabled:!syncService_->WizardIsVisible()]; | |
| 1861 NSString* buttonLabel; | |
| 1862 if (syncService_->HasSyncSetupCompleted()) { | |
| 1863 buttonLabel = l10n_util::GetNSStringWithFixup( | |
| 1864 IDS_SYNC_STOP_SYNCING_BUTTON_LABEL); | |
| 1865 [syncCustomizeButton_ setHidden:false]; | |
| 1866 } else if (syncService_->SetupInProgress()) { | |
| 1867 buttonLabel = l10n_util::GetNSStringWithFixup( | |
| 1868 IDS_SYNC_NTP_SETUP_IN_PROGRESS); | |
| 1869 [syncCustomizeButton_ setHidden:true]; | |
| 1870 } else { | |
| 1871 buttonLabel = l10n_util::GetNSStringWithFixup( | |
| 1872 IDS_SYNC_START_SYNC_BUTTON_LABEL); | |
| 1873 [syncCustomizeButton_ setHidden:true]; | |
| 1874 } | |
| 1875 [syncCustomizeButton_ setEnabled:!managed]; | |
| 1876 [syncButton_ setTitle:buttonLabel]; | |
| 1877 [syncButton_ setEnabled:!managed]; | |
| 1878 | |
| 1879 [syncStatus_ setStringValue:base::SysUTF16ToNSString(statusLabel)]; | |
| 1880 [syncLink_ setHidden:linkLabel.empty()]; | |
| 1881 [syncLink_ setTitle:base::SysUTF16ToNSString(linkLabel)]; | |
| 1882 [syncLink_ setEnabled:!managed]; | |
| 1883 | |
| 1884 NSButtonCell* syncLinkCell = static_cast<NSButtonCell*>([syncLink_ cell]); | |
| 1885 if (!syncStatusNoErrorBackgroundColor_) { | |
| 1886 DCHECK(!syncLinkNoErrorBackgroundColor_); | |
| 1887 // We assume that the sync controls start off in a non-error | |
| 1888 // state. | |
| 1889 syncStatusNoErrorBackgroundColor_.reset( | |
| 1890 [[syncStatus_ backgroundColor] retain]); | |
| 1891 syncLinkNoErrorBackgroundColor_.reset( | |
| 1892 [[syncLinkCell backgroundColor] retain]); | |
| 1893 } | |
| 1894 if (status == sync_ui_util::SYNC_ERROR) { | |
| 1895 [syncStatus_ setBackgroundColor:syncErrorBackgroundColor_]; | |
| 1896 [syncLinkCell setBackgroundColor:syncErrorBackgroundColor_]; | |
| 1897 } else { | |
| 1898 [syncStatus_ setBackgroundColor:syncStatusNoErrorBackgroundColor_]; | |
| 1899 [syncLinkCell setBackgroundColor:syncLinkNoErrorBackgroundColor_]; | |
| 1900 } | |
| 1901 } | |
| 1902 | |
| 1903 // Show the preferences window. | |
| 1904 - (IBAction)showPreferences:(id)sender { | |
| 1905 [self showWindow:sender]; | |
| 1906 } | |
| 1907 | |
| 1908 - (IBAction)toolbarButtonSelected:(id)sender { | |
| 1909 DCHECK([sender isKindOfClass:[NSToolbarItem class]]); | |
| 1910 OptionsPage page = [self getPageForToolbarItem:sender]; | |
| 1911 [self displayPreferenceViewForPage:page animate:YES]; | |
| 1912 } | |
| 1913 | |
| 1914 // Helper to update the window to display a preferences view for a page. | |
| 1915 - (void)displayPreferenceViewForPage:(OptionsPage)page | |
| 1916 animate:(BOOL)animate { | |
| 1917 NSWindow* prefsWindow = [self window]; | |
| 1918 | |
| 1919 // Needs to go *after* the call to [self window], which triggers | |
| 1920 // awakeFromNib if necessary. | |
| 1921 NSView* prefsView = [self getPrefsViewForPage:page]; | |
| 1922 NSView* contentView = [prefsWindow contentView]; | |
| 1923 | |
| 1924 // Make sure we aren't being told to display the same thing again. | |
| 1925 if (currentPrefsView_ == prefsView && | |
| 1926 managedPrefsBannerVisible_ == bannerState_->IsVisible()) { | |
| 1927 return; | |
| 1928 } | |
| 1929 | |
| 1930 // Remember new options page as current page. | |
| 1931 if (page != OPTIONS_PAGE_DEFAULT) | |
| 1932 lastSelectedPage_.SetValue(page); | |
| 1933 | |
| 1934 // Stop any running animation, and reset the subviews to the new state. We | |
| 1935 // re-add any views we need for animation later. | |
| 1936 [animation_ stopAnimation]; | |
| 1937 NSView* oldPrefsView = currentPrefsView_; | |
| 1938 currentPrefsView_ = prefsView; | |
| 1939 [self resetSubViews]; | |
| 1940 | |
| 1941 // Update the banner state. | |
| 1942 [self initBannerStateForPage:page]; | |
| 1943 BOOL showBanner = bannerState_->IsVisible(); | |
| 1944 | |
| 1945 // Update the window title. | |
| 1946 NSToolbarItem* toolbarItem = [self getToolbarItemForPage:page]; | |
| 1947 [prefsWindow setTitle:[toolbarItem label]]; | |
| 1948 | |
| 1949 // Calculate new frames for the subviews. | |
| 1950 NSRect prefsViewFrame = [prefsView frame]; | |
| 1951 NSRect contentViewFrame = [contentView frame]; | |
| 1952 NSRect bannerViewFrame = [managedPrefsBannerView_ frame]; | |
| 1953 | |
| 1954 // Determine what height the managed prefs banner will use. | |
| 1955 CGFloat bannerViewHeight = showBanner ? NSHeight(bannerViewFrame) : 0.0; | |
| 1956 | |
| 1957 if (animate) { | |
| 1958 // NSViewAnimation doesn't seem to honor subview resizing as it animates the | |
| 1959 // Window's frame. So instead of trying to get the top in the right place, | |
| 1960 // just set the origin where it should be at the end, and let the fade/size | |
| 1961 // slide things into the right spot. | |
| 1962 prefsViewFrame.origin.y = 0.0; | |
| 1963 } else { | |
| 1964 // The prefView is anchored to the top of its parent, so set its origin so | |
| 1965 // that the top is where it should be. When the window's frame is set, the | |
| 1966 // origin will be adjusted to keep it in the right spot. | |
| 1967 prefsViewFrame.origin.y = NSHeight(contentViewFrame) - | |
| 1968 NSHeight(prefsViewFrame) - bannerViewHeight; | |
| 1969 } | |
| 1970 bannerViewFrame.origin.y = NSHeight(prefsViewFrame); | |
| 1971 bannerViewFrame.size.width = NSWidth(contentViewFrame); | |
| 1972 [prefsView setFrame:prefsViewFrame]; | |
| 1973 | |
| 1974 // Figure out the size of the window. | |
| 1975 NSRect windowFrame = [contentView convertRect:[prefsWindow frame] | |
| 1976 fromView:nil]; | |
| 1977 CGFloat titleToolbarHeight = | |
| 1978 NSHeight(windowFrame) - NSHeight(contentViewFrame); | |
| 1979 windowFrame.size.height = | |
| 1980 NSHeight(prefsViewFrame) + titleToolbarHeight + bannerViewHeight; | |
| 1981 DCHECK_GE(NSWidth(windowFrame), NSWidth(prefsViewFrame)) | |
| 1982 << "Initial width set wasn't wide enough."; | |
| 1983 windowFrame = [contentView convertRect:windowFrame toView:nil]; | |
| 1984 windowFrame.origin.y = NSMaxY([prefsWindow frame]) - NSHeight(windowFrame); | |
| 1985 | |
| 1986 // Now change the size. | |
| 1987 if (animate) { | |
| 1988 NSMutableArray* animations = [NSMutableArray arrayWithCapacity:4]; | |
| 1989 if (oldPrefsView != prefsView) { | |
| 1990 // Fade between prefs views if they change. | |
| 1991 [contentView addSubview:oldPrefsView | |
| 1992 positioned:NSWindowBelow | |
| 1993 relativeTo:nil]; | |
| 1994 [animations addObject: | |
| 1995 [NSDictionary dictionaryWithObjectsAndKeys: | |
| 1996 oldPrefsView, NSViewAnimationTargetKey, | |
| 1997 NSViewAnimationFadeOutEffect, NSViewAnimationEffectKey, | |
| 1998 nil]]; | |
| 1999 [animations addObject: | |
| 2000 [NSDictionary dictionaryWithObjectsAndKeys: | |
| 2001 prefsView, NSViewAnimationTargetKey, | |
| 2002 NSViewAnimationFadeInEffect, NSViewAnimationEffectKey, | |
| 2003 nil]]; | |
| 2004 } else { | |
| 2005 // Make sure the prefs pane ends up in the right position in case we | |
| 2006 // manipulate the banner. | |
| 2007 [animations addObject: | |
| 2008 [NSDictionary dictionaryWithObjectsAndKeys: | |
| 2009 prefsView, NSViewAnimationTargetKey, | |
| 2010 [NSValue valueWithRect:prefsViewFrame], | |
| 2011 NSViewAnimationEndFrameKey, | |
| 2012 nil]]; | |
| 2013 } | |
| 2014 if (showBanner != managedPrefsBannerVisible_) { | |
| 2015 // Slide the warning banner in or out of view. | |
| 2016 [animations addObject: | |
| 2017 [NSDictionary dictionaryWithObjectsAndKeys: | |
| 2018 managedPrefsBannerView_, NSViewAnimationTargetKey, | |
| 2019 [NSValue valueWithRect:bannerViewFrame], | |
| 2020 NSViewAnimationEndFrameKey, | |
| 2021 nil]]; | |
| 2022 } | |
| 2023 // Window resize animation. | |
| 2024 [animations addObject: | |
| 2025 [NSDictionary dictionaryWithObjectsAndKeys: | |
| 2026 prefsWindow, NSViewAnimationTargetKey, | |
| 2027 [NSValue valueWithRect:windowFrame], NSViewAnimationEndFrameKey, | |
| 2028 nil]]; | |
| 2029 [animation_ setViewAnimations:animations]; | |
| 2030 // The default duration is 0.5s, which actually feels slow in here, so speed | |
| 2031 // it up a bit. | |
| 2032 [animation_ gtm_setDuration:0.2 | |
| 2033 eventMask:NSLeftMouseUpMask]; | |
| 2034 [animation_ startAnimation]; | |
| 2035 } else { | |
| 2036 // If not animating, odds are we don't want to display either (because it | |
| 2037 // is initial window setup). | |
| 2038 [prefsWindow setFrame:windowFrame display:NO]; | |
| 2039 [managedPrefsBannerView_ setFrame:bannerViewFrame]; | |
| 2040 } | |
| 2041 | |
| 2042 managedPrefsBannerVisible_ = showBanner; | |
| 2043 } | |
| 2044 | |
| 2045 - (void)resetSubViews { | |
| 2046 // Reset subviews to current prefs view and banner, remove any views that | |
| 2047 // might have been left over from previous state or animation. | |
| 2048 NSArray* subviews = [NSArray arrayWithObjects: | |
| 2049 currentPrefsView_, managedPrefsBannerView_, nil]; | |
| 2050 [[[self window] contentView] setSubviews:subviews]; | |
| 2051 [[self window] setInitialFirstResponder:currentPrefsView_]; | |
| 2052 } | |
| 2053 | |
| 2054 - (void)animationDidEnd:(NSAnimation*)animation { | |
| 2055 DCHECK_EQ(animation_.get(), animation); | |
| 2056 // Animation finished, reset subviews to current prefs view and the banner. | |
| 2057 [self resetSubViews]; | |
| 2058 } | |
| 2059 | |
| 2060 // Reinitializes the banner state tracker object to watch for managed bits of | |
| 2061 // preferences relevant to the given options |page|. | |
| 2062 - (void)initBannerStateForPage:(OptionsPage)page { | |
| 2063 page = [self normalizePage:page]; | |
| 2064 | |
| 2065 bannerState_.reset( | |
| 2066 new PreferencesWindowControllerInternal::ManagedPrefsBannerState( | |
| 2067 self, page, g_browser_process->local_state(), prefs_)); | |
| 2068 } | |
| 2069 | |
| 2070 - (void)switchToPage:(OptionsPage)page animate:(BOOL)animate { | |
| 2071 [self displayPreferenceViewForPage:page animate:animate]; | |
| 2072 NSToolbarItem* toolbarItem = [self getToolbarItemForPage:page]; | |
| 2073 [toolbar_ setSelectedItemIdentifier:[toolbarItem itemIdentifier]]; | |
| 2074 } | |
| 2075 | |
| 2076 // Called when the window is being closed. Send out a notification that the user | |
| 2077 // is done editing preferences. Make sure there are no pending field editors | |
| 2078 // by clearing the first responder. | |
| 2079 - (void)windowWillClose:(NSNotification*)notification { | |
| 2080 // Setting the first responder to the window ends any in-progress field | |
| 2081 // editor. This will update the model appropriately so there's nothing left | |
| 2082 // to do. | |
| 2083 if (![[self window] makeFirstResponder:[self window]]) { | |
| 2084 // We've hit a recalcitrant field editor, force it to go away. | |
| 2085 [[self window] endEditingFor:nil]; | |
| 2086 } | |
| 2087 [self autorelease]; | |
| 2088 } | |
| 2089 | |
| 2090 - (void)controlTextDidEndEditing:(NSNotification*)notification { | |
| 2091 [customPagesSource_ validateURLs]; | |
| 2092 } | |
| 2093 | |
| 2094 @end | |
| 2095 | |
| 2096 @implementation PreferencesWindowController(Testing) | |
| 2097 | |
| 2098 - (IntegerPrefMember*)lastSelectedPage { | |
| 2099 return &lastSelectedPage_; | |
| 2100 } | |
| 2101 | |
| 2102 - (NSToolbar*)toolbar { | |
| 2103 return toolbar_; | |
| 2104 } | |
| 2105 | |
| 2106 - (NSView*)basicsView { | |
| 2107 return basicsView_; | |
| 2108 } | |
| 2109 | |
| 2110 - (NSView*)personalStuffView { | |
| 2111 return personalStuffView_; | |
| 2112 } | |
| 2113 | |
| 2114 - (NSView*)underTheHoodView { | |
| 2115 return underTheHoodView_; | |
| 2116 } | |
| 2117 | |
| 2118 - (OptionsPage)normalizePage:(OptionsPage)page { | |
| 2119 if (page == OPTIONS_PAGE_DEFAULT) { | |
| 2120 // Get the last visited page from local state. | |
| 2121 page = static_cast<OptionsPage>(lastSelectedPage_.GetValue()); | |
| 2122 if (page == OPTIONS_PAGE_DEFAULT) { | |
| 2123 page = OPTIONS_PAGE_GENERAL; | |
| 2124 } | |
| 2125 } | |
| 2126 return page; | |
| 2127 } | |
| 2128 | |
| 2129 - (NSToolbarItem*)getToolbarItemForPage:(OptionsPage)page { | |
| 2130 NSUInteger pageIndex = (NSUInteger)[self normalizePage:page]; | |
| 2131 NSArray* items = [toolbar_ items]; | |
| 2132 NSUInteger itemCount = [items count]; | |
| 2133 DCHECK_GE(pageIndex, 0U); | |
| 2134 if (pageIndex >= itemCount) { | |
| 2135 NOTIMPLEMENTED(); | |
| 2136 pageIndex = 0; | |
| 2137 } | |
| 2138 DCHECK_GT(itemCount, 0U); | |
| 2139 return [items objectAtIndex:pageIndex]; | |
| 2140 } | |
| 2141 | |
| 2142 - (OptionsPage)getPageForToolbarItem:(NSToolbarItem*)toolbarItem { | |
| 2143 // Tags are set in the nib file. | |
| 2144 switch ([toolbarItem tag]) { | |
| 2145 case 0: // Basics | |
| 2146 return OPTIONS_PAGE_GENERAL; | |
| 2147 case 1: // Personal Stuff | |
| 2148 return OPTIONS_PAGE_CONTENT; | |
| 2149 case 2: // Under the Hood | |
| 2150 return OPTIONS_PAGE_ADVANCED; | |
| 2151 default: | |
| 2152 NOTIMPLEMENTED(); | |
| 2153 return OPTIONS_PAGE_GENERAL; | |
| 2154 } | |
| 2155 } | |
| 2156 | |
| 2157 - (NSView*)getPrefsViewForPage:(OptionsPage)page { | |
| 2158 // The views will be NULL if this is mistakenly called before awakeFromNib. | |
| 2159 DCHECK(basicsView_); | |
| 2160 DCHECK(personalStuffView_); | |
| 2161 DCHECK(underTheHoodView_); | |
| 2162 page = [self normalizePage:page]; | |
| 2163 switch (page) { | |
| 2164 case OPTIONS_PAGE_GENERAL: | |
| 2165 return basicsView_; | |
| 2166 case OPTIONS_PAGE_CONTENT: | |
| 2167 return personalStuffView_; | |
| 2168 case OPTIONS_PAGE_ADVANCED: | |
| 2169 return underTheHoodView_; | |
| 2170 case OPTIONS_PAGE_DEFAULT: | |
| 2171 case OPTIONS_PAGE_COUNT: | |
| 2172 LOG(DFATAL) << "Invalid page value " << page; | |
| 2173 } | |
| 2174 return basicsView_; | |
| 2175 } | |
| 2176 | |
| 2177 @end | |
| OLD | NEW |