1 // Copyright (c) 2010 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/content_settings/content_setting_bubble_cocoa.h" 6 7 #include "base/command_line.h" 8 #include "base/logging.h" 9 #include "base/sys_string_conversions.h" 10 #include "base/utf_string_conversions.h" 11 #include "chrome/browser/blocked_content_container.h" 12 #include "chrome/browser/content_setting_bubble_model.h" 13 #include "chrome/browser/content_settings/host_content_settings_map.h" 14 #include "chrome/browser/plugin_updater.h" 15 #import "chrome/browser/ui/cocoa/hyperlink_button_cell.h" 16 #import "chrome/browser/ui/cocoa/info_bubble_view.h" 17 #import "chrome/browser/ui/cocoa/l10n_util.h" 18 #include "grit/generated_resources.h" 19 #include "skia/ext/skia_utils_mac.h" 20 #import "third_party/GTM/AppKit/GTMUILocalizerAndLayoutTweaker.h" 21 #include "ui/base/l10n/l10n_util.h" 22 #include "webkit/glue/plugins/plugin_list.h" 23 24 namespace { 25 26 // Must match the tag of the unblock radio button in the xib files. 27 const int kAllowTag = 1; 28 29 // Must match the tag of the block radio button in the xib files. 30 const int kBlockTag = 2; 31 32 // Height of one link in the popup list. 33 const int kLinkHeight = 16; 34 35 // Space between two popup links. 36 const int kLinkPadding = 4; 37 38 // Space taken in total by one popup link. 39 const int kLinkLineHeight = kLinkHeight + kLinkPadding; 40 41 // Space between popup list and surrounding UI elements. 42 const int kLinkOuterPadding = 8; 43 44 // Height of each of the labels in the geolocation bubble. 45 const int kGeoLabelHeight = 14; 46 47 // Height of the "Clear" button in the geolocation bubble. 48 const int kGeoClearButtonHeight = 17; 49 50 // Padding between radio buttons and "Load all plugins" button 51 // in the plugin bubble. 52 const int kLoadAllPluginsButtonVerticalPadding = 8; 53 54 // General padding between elements in the geolocation bubble. 55 const int kGeoPadding = 8; 56 57 // Padding between host names in the geolocation bubble. 58 const int kGeoHostPadding = 4; 59 60 // Minimal padding between "Manage" and "Done" buttons. 61 const int kManageDonePadding = 8; 62 63 void SetControlSize(NSControl* control, NSControlSize controlSize) { 64 CGFloat fontSize = [NSFont systemFontSizeForControlSize:controlSize]; 65 NSCell* cell = [control cell]; 66 NSFont* font = [NSFont fontWithName:[[cell font] fontName] size:fontSize]; 67 [cell setFont:font]; 68 [cell setControlSize:controlSize]; 69 } 70 71 // Returns an autoreleased NSTextField that is configured to look like a Label 72 // looks in Interface Builder. 73 NSTextField* LabelWithFrame(NSString* text, const NSRect& frame) { 74 NSTextField* label = [[NSTextField alloc] initWithFrame:frame]; 75 [label setStringValue:text]; 76 [label setSelectable:NO]; 77 [label setBezeled:NO]; 78 return [label autorelease]; 79 } 80 81 } // namespace 82 83 @interface ContentSettingBubbleController(Private) 84 - (id)initWithModel:(ContentSettingBubbleModel*)settingsBubbleModel 85 parentWindow:(NSWindow*)parentWindow 86 anchoredAt:(NSPoint)anchoredAt; 87 - (NSButton*)hyperlinkButtonWithFrame:(NSRect)frame 88 title:(NSString*)title 89 icon:(NSImage*)icon 90 referenceFrame:(NSRect)referenceFrame; 91 - (void)initializeBlockedPluginsList; 92 - (void)initializeTitle; 93 - (void)initializeRadioGroup; 94 - (void)initializePopupList; 95 - (void)initializeGeoLists; 96 - (void)sizeToFitLoadPluginsButton; 97 - (void)sizeToFitManageDoneButtons; 98 - (void)removeInfoButton; 99 - (void)popupLinkClicked:(id)sender; 100 - (void)clearGeolocationForCurrentHost:(id)sender; 101 @end 102 103 @implementation ContentSettingBubbleController 104 105 + (ContentSettingBubbleController*) 106 showForModel:(ContentSettingBubbleModel*)contentSettingBubbleModel 107 parentWindow:(NSWindow*)parentWindow 108 anchoredAt:(NSPoint)anchor { 109 // Autoreleases itself on bubble close. 110 return [[ContentSettingBubbleController alloc] 111 initWithModel:contentSettingBubbleModel 112 parentWindow:parentWindow 113 anchoredAt:anchor]; 114 } 115 116 - (id)initWithModel:(ContentSettingBubbleModel*)contentSettingBubbleModel 117 parentWindow:(NSWindow*)parentWindow 118 anchoredAt:(NSPoint)anchoredAt { 119 // This method takes ownership of |contentSettingBubbleModel| in all cases. 120 scoped_ptr<ContentSettingBubbleModel> model(contentSettingBubbleModel); 121 DCHECK(model.get()); 122 123 NSString* const nibPaths[] = { 124 @"ContentBlockedCookies", 125 @"ContentBlockedImages", 126 @"ContentBlockedJavaScript", 127 @"ContentBlockedPlugins", 128 @"ContentBlockedPopups", 129 @"ContentBubbleGeolocation", 130 @"", // Notifications do not have a bubble. 131 @"", // Prerender does not have a bubble. 132 }; 133 COMPILE_ASSERT(arraysize(nibPaths) == CONTENT_SETTINGS_NUM_TYPES, 134 nibPaths_requires_an_entry_for_every_setting_type); 135 const int settingsType = model->content_type(); 136 // Nofifications do not have a bubble. 137 CHECK_NE(settingsType, CONTENT_SETTINGS_TYPE_NOTIFICATIONS); 138 DCHECK_LT(settingsType, CONTENT_SETTINGS_NUM_TYPES); 139 if ((self = [super initWithWindowNibPath:nibPaths[settingsType] 140 parentWindow:parentWindow 141 anchoredAt:anchoredAt])) { 142 contentSettingBubbleModel_.reset(model.release()); 143 [self showWindow:nil]; 144 } 145 return self; 146 } 147 148 - (void)initializeTitle { 149 if (!titleLabel_) 150 return; 151 152 NSString* label = base::SysUTF8ToNSString( 153 contentSettingBubbleModel_->bubble_content().title); 154 [titleLabel_ setStringValue:label]; 155 156 // Layout title post-localization. 157 CGFloat deltaY = [GTMUILocalizerAndLayoutTweaker 158 sizeToFitFixedWidthTextField:titleLabel_]; 159 NSRect windowFrame = [[self window] frame]; 160 windowFrame.size.height += deltaY; 161 [[self window] setFrame:windowFrame display:NO]; 162 NSRect titleFrame = [titleLabel_ frame]; 163 titleFrame.origin.y -= deltaY; 164 [titleLabel_ setFrame:titleFrame]; 165 } 166 167 - (void)initializeRadioGroup { 168 // Configure the radio group. For now, only deal with the 169 // strictly needed case of group containing 2 radio buttons. 170 const ContentSettingBubbleModel::RadioGroup& radio_group = 171 contentSettingBubbleModel_->bubble_content().radio_group; 172 173 // Select appropriate radio button. 174 [allowBlockRadioGroup_ selectCellWithTag: 175 radio_group.default_item == 0 ? kAllowTag : kBlockTag]; 176 177 const ContentSettingBubbleModel::RadioItems& radio_items = 178 radio_group.radio_items; 179 DCHECK_EQ(2u, radio_items.size()) << "Only 2 radio items per group supported"; 180 // Set radio group labels from model. 181 NSCell* radioCell = [allowBlockRadioGroup_ cellWithTag:kAllowTag]; 182 [radioCell setTitle:base::SysUTF8ToNSString(radio_items[0])]; 183 184 radioCell = [allowBlockRadioGroup_ cellWithTag:kBlockTag]; 185 [radioCell setTitle:base::SysUTF8ToNSString(radio_items[1])]; 186 187 // Layout radio group labels post-localization. 188 [GTMUILocalizerAndLayoutTweaker 189 wrapRadioGroupForWidth:allowBlockRadioGroup_]; 190 CGFloat radioDeltaY = [GTMUILocalizerAndLayoutTweaker 191 sizeToFitView:allowBlockRadioGroup_].height; 192 NSRect windowFrame = [[self window] frame]; 193 windowFrame.size.height += radioDeltaY; 194 [[self window] setFrame:windowFrame display:NO]; 195 } 196 197 - (NSButton*)hyperlinkButtonWithFrame:(NSRect)frame 198 title:(NSString*)title 199 icon:(NSImage*)icon 200 referenceFrame:(NSRect)referenceFrame { 201 scoped_nsobject<HyperlinkButtonCell> cell([[HyperlinkButtonCell alloc] 202 initTextCell:title]); 203 [cell.get() setAlignment:NSNaturalTextAlignment]; 204 if (icon) { 205 [cell.get() setImagePosition:NSImageLeft]; 206 [cell.get() setImage:icon]; 207 } else { 208 [cell.get() setImagePosition:NSNoImage]; 209 } 210 [cell.get() setControlSize:NSSmallControlSize]; 211 212 NSButton* button = [[[NSButton alloc] initWithFrame:frame] autorelease]; 213 // Cell must be set immediately after construction. 214 [button setCell:cell.get()]; 215 216 // If the link text is too long, clamp it. 217 [button sizeToFit]; 218 int maxWidth = NSWidth([[self bubble] frame]) - 2 * NSMinX(referenceFrame); 219 NSRect buttonFrame = [button frame]; 220 if (NSWidth(buttonFrame) > maxWidth) { 221 buttonFrame.size.width = maxWidth; 222 [button setFrame:buttonFrame]; 223 } 224 225 [button setTarget:self]; 226 [button setAction:@selector(popupLinkClicked:)]; 227 return button; 228 } 229 230 - (void)initializeBlockedPluginsList { 231 NSMutableArray* pluginArray = [NSMutableArray array]; 232 const std::set<std::string>& plugins = 233 contentSettingBubbleModel_->bubble_content().resource_identifiers; 234 if (plugins.empty()) { 235 int delta = NSMinY([titleLabel_ frame]) - 236 NSMinY([blockedResourcesField_ frame]); 237 [blockedResourcesField_ removeFromSuperview]; 238 NSRect frame = [[self window] frame]; 239 frame.size.height -= delta; 240 [[self window] setFrame:frame display:NO]; 241 } else { 242 for (std::set<std::string>::iterator it = plugins.begin(); 243 it != plugins.end(); ++it) { 244 NSString* name = SysUTF16ToNSString( 245 NPAPI::PluginList::Singleton()->GetPluginGroupName(*it)); 246 if ([name length] == 0) 247 name = base::SysUTF8ToNSString(*it); 248 [pluginArray addObject:name]; 249 } 250 [blockedResourcesField_ 251 setStringValue:[pluginArray componentsJoinedByString:@"\n"]]; 252 [GTMUILocalizerAndLayoutTweaker 253 sizeToFitFixedWidthTextField:blockedResourcesField_]; 254 } 255 } 256 257 - (void)initializePopupList { 258 // I didn't put the buttons into a NSMatrix because then they are only one 259 // entity in the key view loop. This way, one can tab through all of them. 260 const ContentSettingBubbleModel::PopupItems& popupItems = 261 contentSettingBubbleModel_->bubble_content().popup_items; 262 263 // Get the pre-resize frame of the radio group. Its origin is where the 264 // popup list should go. 265 NSRect radioFrame = [allowBlockRadioGroup_ frame]; 266 267 // Make room for the popup list. The bubble view and its subviews autosize 268 // themselves when the window is enlarged. 269 // Heading and radio box are already 1 * kLinkOuterPadding apart in the nib, 270 // so only 1 * kLinkOuterPadding more is needed. 271 int delta = popupItems.size() * kLinkLineHeight - kLinkPadding + 272 kLinkOuterPadding; 273 NSSize deltaSize = NSMakeSize(0, delta); 274 deltaSize = [[[self window] contentView] convertSize:deltaSize toView:nil]; 275 NSRect windowFrame = [[self window] frame]; 276 windowFrame.size.height += deltaSize.height; 277 [[self window] setFrame:windowFrame display:NO]; 278 279 // Create popup list. 280 int topLinkY = NSMaxY(radioFrame) + delta - kLinkHeight; 281 int row = 0; 282 for (std::vector<ContentSettingBubbleModel::PopupItem>::const_iterator 283 it(popupItems.begin()); it != popupItems.end(); ++it, ++row) { 284 const SkBitmap& icon = it->bitmap; 285 NSImage* image = nil; 286 if (!icon.empty()) 287 image = gfx::SkBitmapToNSImage(icon); 288 289 std::string title(it->title); 290 // The popup may not have committed a load yet, in which case it won't 291 // have a URL or title. 292 if (title.empty()) 293 title = l10n_util::GetStringUTF8(IDS_TAB_LOADING_TITLE); 294 295 NSRect linkFrame = 296 NSMakeRect(NSMinX(radioFrame), topLinkY - kLinkLineHeight * row, 297 200, kLinkHeight); 298 NSButton* button = [self 299 hyperlinkButtonWithFrame:linkFrame 300 title:base::SysUTF8ToNSString(title) 301 icon:image 302 referenceFrame:radioFrame]; 303 [[self bubble] addSubview:button]; 304 popupLinks_[button] = row; 305 } 306 } 307 308 - (void)initializeGeoLists { 309 // Cocoa has its origin in the lower left corner. This means elements are 310 // added from bottom to top, which explains why loops run backwards and the 311 // order of operations is the other way than on Linux/Windows. 312 const ContentSettingBubbleModel::BubbleContent& content = 313 contentSettingBubbleModel_->bubble_content(); 314 NSRect containerFrame = [contentsContainer_ frame]; 315 NSRect frame = NSMakeRect(0, 0, NSWidth(containerFrame), kGeoLabelHeight); 316 317 // "Clear" button / text field. 318 if (!content.custom_link.empty()) { 319 scoped_nsobject<NSControl> control; 320 if(content.custom_link_enabled) { 321 NSRect buttonFrame = NSMakeRect(0, 0, 322 NSWidth(containerFrame), 323 kGeoClearButtonHeight); 324 NSButton* button = [[NSButton alloc] initWithFrame:buttonFrame]; 325 control.reset(button); 326 [button setTitle:base::SysUTF8ToNSString(content.custom_link)]; 327 [button setTarget:self]; 328 [button setAction:@selector(clearGeolocationForCurrentHost:)]; 329 [button setBezelStyle:NSRoundRectBezelStyle]; 330 SetControlSize(button, NSSmallControlSize); 331 [button sizeToFit]; 332 } else { 333 // Add the notification that settings will be cleared on next reload. 334 control.reset([LabelWithFrame( 335 base::SysUTF8ToNSString(content.custom_link), frame) retain]); 336 SetControlSize(control.get(), NSSmallControlSize); 337 } 338 339 // If the new control is wider than the container, widen the window. 340 CGFloat controlWidth = NSWidth([control frame]); 341 if (controlWidth > NSWidth(containerFrame)) { 342 NSRect windowFrame = [[self window] frame]; 343 windowFrame.size.width += controlWidth - NSWidth(containerFrame); 344 [[self window] setFrame:windowFrame display:NO]; 345 // Fetch the updated sizes. 346 containerFrame = [contentsContainer_ frame]; 347 frame = NSMakeRect(0, 0, NSWidth(containerFrame), kGeoLabelHeight); 348 } 349 350 DCHECK(control); 351 [contentsContainer_ addSubview:control]; 352 frame.origin.y = NSMaxY([control frame]) + kGeoPadding; 353 } 354 355 typedef 356 std::vector<ContentSettingBubbleModel::DomainList>::const_reverse_iterator 357 GeolocationGroupIterator; 358 for (GeolocationGroupIterator i = content.domain_lists.rbegin(); 359 i != content.domain_lists.rend(); ++i) { 360 // Add all hosts in the current domain list. 361 for (std::set<std::string>::const_reverse_iterator j = i->hosts.rbegin(); 362 j != i->hosts.rend(); ++j) { 363 NSTextField* title = LabelWithFrame(base::SysUTF8ToNSString(*j), frame); 364 SetControlSize(title, NSSmallControlSize); 365 [contentsContainer_ addSubview:title]; 366 367 frame.origin.y = NSMaxY(frame) + kGeoHostPadding + 368 [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:title]; 369 } 370 if (!i->hosts.empty()) 371 frame.origin.y += kGeoPadding - kGeoHostPadding; 372 373 // Add the domain list's title. 374 NSTextField* title = 375 LabelWithFrame(base::SysUTF8ToNSString(i->title), frame); 376 SetControlSize(title, NSSmallControlSize); 377 [contentsContainer_ addSubview:title]; 378 379 frame.origin.y = NSMaxY(frame) + kGeoPadding + 380 [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:title]; 381 } 382 383 CGFloat containerHeight = frame.origin.y; 384 // Undo last padding. 385 if (!content.domain_lists.empty()) 386 containerHeight -= kGeoPadding; 387 388 // Resize container to fit its subviews, and window to fit the container. 389 NSRect windowFrame = [[self window] frame]; 390 windowFrame.size.height += containerHeight - NSHeight(containerFrame); 391 [[self window] setFrame:windowFrame display:NO]; 392 containerFrame.size.height = containerHeight; 393 [contentsContainer_ setFrame:containerFrame]; 394 } 395 396 - (void)sizeToFitLoadPluginsButton { 397 const ContentSettingBubbleModel::BubbleContent& content = 398 contentSettingBubbleModel_->bubble_content(); 399 [loadAllPluginsButton_ setEnabled:content.custom_link_enabled]; 400 401 // Resize horizontally to fit button if necessary. 402 NSRect windowFrame = [[self window] frame]; 403 int widthNeeded = NSWidth([loadAllPluginsButton_ frame]) + 404 2 * NSMinX([loadAllPluginsButton_ frame]); 405 if (NSWidth(windowFrame) < widthNeeded) { 406 windowFrame.size.width = widthNeeded; 407 [[self window] setFrame:windowFrame display:NO]; 408 } 409 } 410 411 - (void)sizeToFitManageDoneButtons { 412 CGFloat actualWidth = NSWidth([[[self window] contentView] frame]); 413 CGFloat requiredWidth = NSMaxX([manageButton_ frame]) + kManageDonePadding + 414 NSWidth([[doneButton_ superview] frame]) - NSMinX([doneButton_ frame]); 415 if (requiredWidth <= actualWidth || !doneButton_ || !manageButton_) 416 return; 417 418 // Resize window, autoresizing takes care of the rest. 419 NSSize size = NSMakeSize(requiredWidth - actualWidth, 0); 420 size = [[[self window] contentView] convertSize:size toView:nil]; 421 NSRect frame = [[self window] frame]; 422 frame.origin.x -= size.width; 423 frame.size.width += size.width; 424 [[self window] setFrame:frame display:NO]; 425 } 426 427 - (void)awakeFromNib { 428 [super awakeFromNib]; 429 430 [[self bubble] setArrowLocation:info_bubble::kTopRight]; 431 432 // Adapt window size to bottom buttons. Do this before all other layouting. 433 [self sizeToFitManageDoneButtons]; 434 435 [self initializeTitle]; 436 437 ContentSettingsType type = contentSettingBubbleModel_->content_type(); 438 if (type == CONTENT_SETTINGS_TYPE_PLUGINS) { 439 [self sizeToFitLoadPluginsButton]; 440 [self initializeBlockedPluginsList]; 441 } 442 if (allowBlockRadioGroup_) // not bound in cookie bubble xib 443 [self initializeRadioGroup]; 444 445 if (type == CONTENT_SETTINGS_TYPE_POPUPS) 446 [self initializePopupList]; 447 if (type == CONTENT_SETTINGS_TYPE_GEOLOCATION) 448 [self initializeGeoLists]; 449 } 450 451 /////////////////////////////////////////////////////////////////////////////// 452 // Actual application logic 453 454 - (IBAction)allowBlockToggled:(id)sender { 455 NSButtonCell *selectedCell = [sender selectedCell]; 456 contentSettingBubbleModel_->OnRadioClicked( 457 [selectedCell tag] == kAllowTag ? 0 : 1); 458 } 459 460 - (void)popupLinkClicked:(id)sender { 461 content_setting_bubble::PopupLinks::iterator i(popupLinks_.find(sender)); 462 DCHECK(i != popupLinks_.end()); 463 contentSettingBubbleModel_->OnPopupClicked(i->second); 464 } 465 466 - (void)clearGeolocationForCurrentHost:(id)sender { 467 contentSettingBubbleModel_->OnCustomLinkClicked(); 468 [self close]; 469 } 470 471 - (IBAction)showMoreInfo:(id)sender { 472 contentSettingBubbleModel_->OnCustomLinkClicked(); 473 [self close]; 474 } 475 476 - (IBAction)loadAllPlugins:(id)sender { 477 contentSettingBubbleModel_->OnCustomLinkClicked(); 478 [self close]; 479 } 480 481 - (IBAction)manageBlocking:(id)sender { 482 contentSettingBubbleModel_->OnManageLinkClicked(); 483 } 484 485 - (IBAction)closeBubble:(id)sender { 486 [self close]; 487 } 488 489 @end // ContentSettingBubbleController 490