1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #import "chrome/browser/ui/cocoa/content_settings/content_setting_bubble_cocoa.h"
7 #include "base/command_line.h"
8 #include "base/logging.h"
9 #include "base/stl_util.h"
10 #include "base/strings/sys_string_conversions.h"
11 #include "base/strings/utf_string_conversions.h"
12 #include "chrome/browser/plugins/plugin_finder.h"
13 #include "chrome/browser/plugins/plugin_metadata.h"
14 #import "chrome/browser/ui/cocoa/info_bubble_view.h"
15 #import "chrome/browser/ui/cocoa/l10n_util.h"
16 #include "chrome/browser/ui/content_settings/content_setting_bubble_model.h"
17 #include "chrome/browser/ui/content_settings/content_setting_media_menu_model.h"
18 #include "chrome/grit/generated_resources.h"
19 #include "components/content_settings/core/browser/host_content_settings_map.h"
20 #include "content/public/browser/plugin_service.h"
21 #include "content/public/browser/web_contents_observer.h"
22 #include "skia/ext/skia_utils_mac.h"
23 #import "third_party/google_toolbox_for_mac/src/AppKit/GTMUILocalizerAndLayoutTweaker.h"
24 #import "ui/base/cocoa/controls/hyperlink_button_cell.h"
25 #include "ui/base/l10n/l10n_util.h"
27 using content::PluginService;
31 // Height of one link in the popup list.
32 const int kLinkHeight = 16;
34 // Space between two popup links.
35 const int kLinkPadding = 4;
37 // Space taken in total by one popup link.
38 const int kLinkLineHeight = kLinkHeight + kLinkPadding;
40 // Space between popup list and surrounding UI elements.
41 const int kLinkOuterPadding = 8;
43 // Height of each of the labels in the geolocation bubble.
44 const int kGeoLabelHeight = 14;
46 // Height of the "Clear" button in the geolocation bubble.
47 const int kGeoClearButtonHeight = 17;
49 // General padding between elements in the geolocation bubble.
50 const int kGeoPadding = 8;
52 // Padding between host names in the geolocation bubble.
53 const int kGeoHostPadding = 4;
55 // Minimal padding between "Manage" and "Done" buttons.
56 const int kManageDonePadding = 8;
58 // Padding between radio buttons and media menus buttons in the media bubble.
59 const int kMediaMenuVerticalPadding = 25;
61 // Padding between media menu elements in the media bubble.
62 const int kMediaMenuElementVerticalPadding = 5;
64 // The amount of horizontal space between the media menu title and the border.
65 const int kMediaMenuTitleHorizontalPadding = 10;
67 // The minimum width of the media menu buttons.
68 const CGFloat kMinMediaMenuButtonWidth = 100;
70 // Height of each of the labels in the MIDI bubble.
71 const int kMIDISysExLabelHeight = 14;
73 // Height of the "Clear" button in the MIDI bubble.
74 const int kMIDISysExClearButtonHeight = 17;
76 // General padding between elements in the MIDI bubble.
77 const int kMIDISysExPadding = 8;
79 // Padding between host names in the MIDI bubble.
80 const int kMIDISysExHostPadding = 4;
82 void SetControlSize(NSControl* control, NSControlSize controlSize) {
83 CGFloat fontSize = [NSFont systemFontSizeForControlSize:controlSize];
84 NSCell* cell = [control cell];
85 [cell setFont:[NSFont systemFontOfSize:fontSize]];
86 [cell setControlSize:controlSize];
89 // Returns an autoreleased NSTextField that is configured to look like a Label
90 // looks in Interface Builder.
91 NSTextField* LabelWithFrame(NSString* text, const NSRect& frame) {
92 NSTextField* label = [[NSTextField alloc] initWithFrame:frame];
93 [label setStringValue:text];
94 [label setSelectable:NO];
95 [label setBezeled:NO];
96 return [label autorelease];
99 // Sets the title for the popup button.
100 void SetTitleForPopUpButton(NSPopUpButton* button, NSString* title) {
101 base::scoped_nsobject<NSMenuItem> titleItem([[NSMenuItem alloc] init]);
102 [titleItem setTitle:title];
103 [[button cell] setUsesItemFromMenu:NO];
104 [[button cell] setMenuItem:titleItem.get()];
107 // Builds the popup button menu from the menu model and returns the width of the
108 // longgest item as the width of the popup menu.
109 CGFloat BuildPopUpMenuFromModel(NSPopUpButton* button,
110 ContentSettingMediaMenuModel* model,
111 const std::string& title,
113 [[button cell] setControlSize:NSSmallControlSize];
114 [[button cell] setArrowPosition:NSPopUpArrowAtBottom];
115 [button setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
116 [button setButtonType:NSMomentaryPushInButton];
117 [button setAlignment:NSLeftTextAlignment];
118 [button setAutoresizingMask:NSViewMinXMargin];
119 [button setAction:@selector(mediaMenuChanged:)];
122 CGFloat menuWidth = 0;
123 for (int i = 0; i < model->GetItemCount(); ++i) {
124 NSString* itemTitle =
125 base::SysUTF16ToNSString(model->GetLabelAt(i));
126 [button addItemWithTitle:itemTitle];
127 [[button lastItem] setTag:i];
129 if (UTF16ToUTF8(model->GetLabelAt(i)) == title)
130 [button selectItemWithTag:i];
132 // Determine the largest possible size for this button.
133 NSDictionary* textAttributes =
134 [NSDictionary dictionaryWithObject:[button font]
135 forKey:NSFontAttributeName];
136 NSSize size = [itemTitle sizeWithAttributes:textAttributes];
137 NSRect buttonFrame = [button frame];
138 NSRect titleRect = [[button cell] titleRectForBounds:buttonFrame];
139 CGFloat width = size.width + NSWidth(buttonFrame) - NSWidth(titleRect) +
140 kMediaMenuTitleHorizontalPadding;
141 menuWidth = std::max(menuWidth, width);
144 if (!model->GetItemCount()) {
145 // Show a "None available" title and grey out the menu when there is no
147 SetTitleForPopUpButton(
148 button, l10n_util::GetNSString(IDS_MEDIA_MENU_NO_DEVICE_TITLE));
149 [button setEnabled:NO];
151 SetTitleForPopUpButton(button, base::SysUTF8ToNSString(title));
153 // Disable the device selection when the website is managing the devices
156 [button setEnabled:NO];
164 namespace content_setting_bubble {
166 MediaMenuParts::MediaMenuParts(content::MediaStreamType type,
170 MediaMenuParts::~MediaMenuParts() {}
172 } // namespace content_setting_bubble
174 class ContentSettingBubbleWebContentsObserverBridge
175 : public content::WebContentsObserver {
177 ContentSettingBubbleWebContentsObserverBridge(
178 content::WebContents* web_contents,
179 ContentSettingBubbleController* controller)
180 : content::WebContentsObserver(web_contents),
181 controller_(controller) {
185 // WebContentsObserver:
186 void DidNavigateMainFrame(
187 const content::LoadCommittedDetails& details,
188 const content::FrameNavigateParams& params) override {
189 // Content settings are based on the main frame, so if it switches then
191 [controller_ closeBubble:nil];
195 ContentSettingBubbleController* controller_; // weak
197 DISALLOW_COPY_AND_ASSIGN(ContentSettingBubbleWebContentsObserverBridge);
200 @interface ContentSettingBubbleController(Private)
201 - (id)initWithModel:(ContentSettingBubbleModel*)settingsBubbleModel
202 webContents:(content::WebContents*)webContents
203 parentWindow:(NSWindow*)parentWindow
204 anchoredAt:(NSPoint)anchoredAt;
205 - (NSButton*)hyperlinkButtonWithFrame:(NSRect)frame
206 title:(NSString*)title
208 referenceFrame:(NSRect)referenceFrame;
209 - (void)initializeBlockedPluginsList;
210 - (void)initializeTitle;
211 - (void)initializeRadioGroup;
212 - (void)initializeItemList;
213 - (void)initializeGeoLists;
214 - (void)initializeMediaMenus;
215 - (void)initializeMIDISysExLists;
216 - (void)sizeToFitLoadButton;
217 - (void)initManageDoneButtons;
218 - (void)removeInfoButton;
219 - (void)popupLinkClicked:(id)sender;
220 - (void)clearGeolocationForCurrentHost:(id)sender;
221 - (void)clearMIDISysExForCurrentHost:(id)sender;
224 @implementation ContentSettingBubbleController
226 + (ContentSettingBubbleController*)
227 showForModel:(ContentSettingBubbleModel*)contentSettingBubbleModel
228 webContents:(content::WebContents*)webContents
229 parentWindow:(NSWindow*)parentWindow
230 anchoredAt:(NSPoint)anchor {
231 // Autoreleases itself on bubble close.
232 return [[ContentSettingBubbleController alloc]
233 initWithModel:contentSettingBubbleModel
234 webContents:webContents
235 parentWindow:parentWindow
239 - (id)initWithModel:(ContentSettingBubbleModel*)contentSettingBubbleModel
240 webContents:(content::WebContents*)webContents
241 parentWindow:(NSWindow*)parentWindow
242 anchoredAt:(NSPoint)anchoredAt {
243 // This method takes ownership of |contentSettingBubbleModel| in all cases.
244 scoped_ptr<ContentSettingBubbleModel> model(contentSettingBubbleModel);
246 observerBridge_.reset(
247 new ContentSettingBubbleWebContentsObserverBridge(webContents, self));
249 ContentSettingsType settingsType = model->content_type();
250 NSString* nibPath = @"";
251 switch (settingsType) {
252 case CONTENT_SETTINGS_TYPE_COOKIES:
253 nibPath = @"ContentBlockedCookies"; break;
254 case CONTENT_SETTINGS_TYPE_IMAGES:
255 case CONTENT_SETTINGS_TYPE_JAVASCRIPT:
256 case CONTENT_SETTINGS_TYPE_PPAPI_BROKER:
257 nibPath = @"ContentBlockedSimple"; break;
258 case CONTENT_SETTINGS_TYPE_PLUGINS:
259 nibPath = @"ContentBlockedPlugins"; break;
260 case CONTENT_SETTINGS_TYPE_POPUPS:
261 nibPath = @"ContentBlockedPopups"; break;
262 case CONTENT_SETTINGS_TYPE_GEOLOCATION:
263 nibPath = @"ContentBlockedGeolocation"; break;
264 case CONTENT_SETTINGS_TYPE_MIXEDSCRIPT:
265 nibPath = @"ContentBlockedMixedScript"; break;
266 case CONTENT_SETTINGS_TYPE_PROTOCOL_HANDLERS:
267 nibPath = @"ContentProtocolHandlers"; break;
268 case CONTENT_SETTINGS_TYPE_MEDIASTREAM:
269 nibPath = @"ContentBlockedMedia"; break;
270 case CONTENT_SETTINGS_TYPE_AUTOMATIC_DOWNLOADS:
271 nibPath = @"ContentBlockedDownloads"; break;
272 case CONTENT_SETTINGS_TYPE_MIDI_SYSEX:
273 nibPath = @"ContentBlockedMIDISysEx"; break;
274 // These content types have no bubble:
275 case CONTENT_SETTINGS_TYPE_DEFAULT:
276 case CONTENT_SETTINGS_TYPE_NOTIFICATIONS:
277 case CONTENT_SETTINGS_TYPE_AUTO_SELECT_CERTIFICATE:
278 case CONTENT_SETTINGS_TYPE_FULLSCREEN:
279 case CONTENT_SETTINGS_TYPE_MOUSELOCK:
280 case CONTENT_SETTINGS_TYPE_MEDIASTREAM_MIC:
281 case CONTENT_SETTINGS_TYPE_MEDIASTREAM_CAMERA:
282 case CONTENT_SETTINGS_NUM_TYPES:
283 // TODO(miguelg): Remove this nib content settings support
285 case CONTENT_SETTINGS_TYPE_PUSH_MESSAGING:
286 case CONTENT_SETTINGS_TYPE_SSL_CERT_DECISIONS:
287 case CONTENT_SETTINGS_TYPE_APP_BANNER:
288 case CONTENT_SETTINGS_TYPE_SITE_ENGAGEMENT:
289 case CONTENT_SETTINGS_TYPE_DURABLE_STORAGE:
292 if ((self = [super initWithWindowNibPath:nibPath
293 parentWindow:parentWindow
294 anchoredAt:anchoredAt])) {
295 contentSettingBubbleModel_.reset(model.release());
296 [self showWindow:nil];
302 STLDeleteValues(&mediaMenus_);
306 - (void)initializeTitle {
310 NSString* label = base::SysUTF8ToNSString(
311 contentSettingBubbleModel_->bubble_content().title);
312 [titleLabel_ setStringValue:label];
314 // Layout title post-localization.
315 CGFloat deltaY = [GTMUILocalizerAndLayoutTweaker
316 sizeToFitFixedWidthTextField:titleLabel_];
317 NSRect windowFrame = [[self window] frame];
318 windowFrame.size.height += deltaY;
319 [[self window] setFrame:windowFrame display:NO];
320 NSRect titleFrame = [titleLabel_ frame];
321 titleFrame.origin.y -= deltaY;
322 [titleLabel_ setFrame:titleFrame];
325 - (void)initializeRadioGroup {
326 // NOTE! Tags in the xib files must match the order of the radio buttons
327 // passed in the radio_group and be 1-based, not 0-based.
328 const ContentSettingBubbleModel::RadioGroup& radio_group =
329 contentSettingBubbleModel_->bubble_content().radio_group;
331 // Xcode 5.1 Interface Builder doesn't allow a font property to be set for
332 // NSMatrix. The implementation of GTMUILocalizerAndLayoutTweaker assumes that
333 // the font for each of the cells in a NSMatrix is identical, and is the font
334 // of the NSMatrix. This logic sets the font of NSMatrix to be that of its
337 for (NSCell* cell in [allowBlockRadioGroup_ cells]) {
340 DCHECK([font isEqual:[cell font]]);
342 [allowBlockRadioGroup_ setFont:font];
344 // Select appropriate radio button.
345 [allowBlockRadioGroup_ selectCellWithTag: radio_group.default_item + 1];
347 const ContentSettingBubbleModel::RadioItems& radio_items =
348 radio_group.radio_items;
349 for (size_t ii = 0; ii < radio_group.radio_items.size(); ++ii) {
350 NSCell* radioCell = [allowBlockRadioGroup_ cellWithTag: ii + 1];
351 [radioCell setTitle:base::SysUTF8ToNSString(radio_items[ii])];
354 // Layout radio group labels post-localization.
355 [GTMUILocalizerAndLayoutTweaker
356 wrapRadioGroupForWidth:allowBlockRadioGroup_];
357 CGFloat radioDeltaY = [GTMUILocalizerAndLayoutTweaker
358 sizeToFitView:allowBlockRadioGroup_].height;
359 NSRect windowFrame = [[self window] frame];
360 windowFrame.size.height += radioDeltaY;
361 [[self window] setFrame:windowFrame display:NO];
364 - (NSButton*)hyperlinkButtonWithFrame:(NSRect)frame
365 title:(NSString*)title
367 referenceFrame:(NSRect)referenceFrame {
368 base::scoped_nsobject<HyperlinkButtonCell> cell(
369 [[HyperlinkButtonCell alloc] initTextCell:title]);
370 [cell.get() setAlignment:NSNaturalTextAlignment];
372 [cell.get() setImagePosition:NSImageLeft];
373 [cell.get() setImage:icon];
375 [cell.get() setImagePosition:NSNoImage];
377 [cell.get() setControlSize:NSSmallControlSize];
379 NSButton* button = [[[NSButton alloc] initWithFrame:frame] autorelease];
380 // Cell must be set immediately after construction.
381 [button setCell:cell.get()];
383 // Size to fit the button and add a little extra padding for the small-text
384 // hyperlink button, which sizeToFit gets wrong.
385 [GTMUILocalizerAndLayoutTweaker sizeToFitView:button];
386 NSRect buttonFrame = [button frame];
387 buttonFrame.size.width += 2;
389 // If the link text is too long, clamp it.
390 int maxWidth = NSWidth([[self bubble] frame]) - 2 * NSMinX(referenceFrame);
391 if (NSWidth(buttonFrame) > maxWidth)
392 buttonFrame.size.width = maxWidth;
394 [button setFrame:buttonFrame];
395 [button setTarget:self];
396 [button setAction:@selector(popupLinkClicked:)];
400 - (void)initializeBlockedPluginsList {
401 // Hide the empty label at the top of the dialog.
403 NSMinY([titleLabel_ frame]) - NSMinY([blockedResourcesField_ frame]);
404 [blockedResourcesField_ removeFromSuperview];
405 NSRect frame = [[self window] frame];
406 frame.size.height -= delta;
407 [[self window] setFrame:frame display:NO];
410 - (void)initializeItemList {
411 // I didn't put the buttons into a NSMatrix because then they are only one
412 // entity in the key view loop. This way, one can tab through all of them.
413 const ContentSettingBubbleModel::ListItems& listItems =
414 contentSettingBubbleModel_->bubble_content().list_items;
416 // Get the pre-resize frame of the radio group. Its origin is where the
417 // popup list should go.
418 NSRect radioFrame = [allowBlockRadioGroup_ frame];
420 // Make room for the popup list. The bubble view and its subviews autosize
421 // themselves when the window is enlarged.
422 // Heading and radio box are already 1 * kLinkOuterPadding apart in the nib,
423 // so only 1 * kLinkOuterPadding more is needed.
425 listItems.size() * kLinkLineHeight - kLinkPadding + kLinkOuterPadding;
426 NSSize deltaSize = NSMakeSize(0, delta);
427 deltaSize = [[[self window] contentView] convertSize:deltaSize toView:nil];
428 NSRect windowFrame = [[self window] frame];
429 windowFrame.size.height += deltaSize.height;
430 [[self window] setFrame:windowFrame display:NO];
433 int topLinkY = NSMaxY(radioFrame) + delta - kLinkHeight;
435 for (const ContentSettingBubbleModel::ListItem& listItem : listItems) {
436 NSImage* image = listItem.image.AsNSImage();
437 NSRect frame = NSMakeRect(
438 NSMinX(radioFrame), topLinkY - kLinkLineHeight * row, 200, kLinkHeight);
439 if (listItem.has_link) {
441 [self hyperlinkButtonWithFrame:frame
442 title:base::SysUTF8ToNSString(listItem.title)
444 referenceFrame:radioFrame];
445 [[self bubble] addSubview:button];
446 popupLinks_[button] = row++;
449 LabelWithFrame(base::SysUTF8ToNSString(listItem.title), frame);
450 SetControlSize(label, NSSmallControlSize);
451 [[self bubble] addSubview:label];
457 - (void)initializeGeoLists {
458 // Cocoa has its origin in the lower left corner. This means elements are
459 // added from bottom to top, which explains why loops run backwards and the
460 // order of operations is the other way than on Linux/Windows.
461 const ContentSettingBubbleModel::BubbleContent& content =
462 contentSettingBubbleModel_->bubble_content();
463 NSRect containerFrame = [contentsContainer_ frame];
464 NSRect frame = NSMakeRect(0, 0, NSWidth(containerFrame), kGeoLabelHeight);
466 // "Clear" button / text field.
467 if (!content.custom_link.empty()) {
468 base::scoped_nsobject<NSControl> control;
469 if(content.custom_link_enabled) {
470 NSRect buttonFrame = NSMakeRect(0, 0,
471 NSWidth(containerFrame),
472 kGeoClearButtonHeight);
473 NSButton* button = [[NSButton alloc] initWithFrame:buttonFrame];
474 control.reset(button);
475 [button setTitle:base::SysUTF8ToNSString(content.custom_link)];
476 [button setTarget:self];
477 [button setAction:@selector(clearGeolocationForCurrentHost:)];
478 [button setBezelStyle:NSRoundRectBezelStyle];
479 SetControlSize(button, NSSmallControlSize);
482 // Add the notification that settings will be cleared on next reload.
483 control.reset([LabelWithFrame(
484 base::SysUTF8ToNSString(content.custom_link), frame) retain]);
485 SetControlSize(control.get(), NSSmallControlSize);
488 // If the new control is wider than the container, widen the window.
489 CGFloat controlWidth = NSWidth([control frame]);
490 if (controlWidth > NSWidth(containerFrame)) {
491 NSRect windowFrame = [[self window] frame];
492 windowFrame.size.width += controlWidth - NSWidth(containerFrame);
493 [[self window] setFrame:windowFrame display:NO];
494 // Fetch the updated sizes.
495 containerFrame = [contentsContainer_ frame];
496 frame = NSMakeRect(0, 0, NSWidth(containerFrame), kGeoLabelHeight);
500 [contentsContainer_ addSubview:control];
501 frame.origin.y = NSMaxY([control frame]) + kGeoPadding;
504 for (auto i = content.domain_lists.rbegin();
505 i != content.domain_lists.rend(); ++i) {
506 // Add all hosts in the current domain list.
507 for (auto j = i->hosts.rbegin(); j != i->hosts.rend(); ++j) {
508 NSTextField* title = LabelWithFrame(base::SysUTF8ToNSString(*j), frame);
509 SetControlSize(title, NSSmallControlSize);
510 [contentsContainer_ addSubview:title];
512 frame.origin.y = NSMaxY(frame) + kGeoHostPadding +
513 [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:title];
515 if (!i->hosts.empty())
516 frame.origin.y += kGeoPadding - kGeoHostPadding;
518 // Add the domain list's title.
520 LabelWithFrame(base::SysUTF8ToNSString(i->title), frame);
521 SetControlSize(title, NSSmallControlSize);
522 [contentsContainer_ addSubview:title];
524 frame.origin.y = NSMaxY(frame) + kGeoPadding +
525 [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:title];
528 CGFloat containerHeight = frame.origin.y;
529 // Undo last padding.
530 if (!content.domain_lists.empty())
531 containerHeight -= kGeoPadding;
533 // Resize container to fit its subviews, and window to fit the container.
534 NSRect windowFrame = [[self window] frame];
535 windowFrame.size.height += containerHeight - NSHeight(containerFrame);
536 [[self window] setFrame:windowFrame display:NO];
537 containerFrame.size.height = containerHeight;
538 [contentsContainer_ setFrame:containerFrame];
541 - (void)initializeMediaMenus {
542 const ContentSettingBubbleModel::MediaMenuMap& media_menus =
543 contentSettingBubbleModel_->bubble_content().media_menus;
545 // Calculate the longest width of the labels and menus menus to avoid
546 // truncation by the window's edge.
547 CGFloat maxLabelWidth = 0;
548 CGFloat maxMenuWidth = 0;
549 CGFloat maxMenuHeight = 0;
550 NSRect radioFrame = [allowBlockRadioGroup_ frame];
551 for (const std::pair<content::MediaStreamType,
552 ContentSettingBubbleModel::MediaMenu>& map_entry :
554 // |labelFrame| will be resized later on in this function.
555 NSRect labelFrame = NSMakeRect(NSMinX(radioFrame), 0, 0, 0);
556 NSTextField* label = LabelWithFrame(
557 base::SysUTF8ToNSString(map_entry.second.label), labelFrame);
558 SetControlSize(label, NSSmallControlSize);
559 NSCell* cell = [label cell];
560 [cell setAlignment:NSRightTextAlignment];
561 [GTMUILocalizerAndLayoutTweaker sizeToFitView:label];
562 maxLabelWidth = std::max(maxLabelWidth, [label frame].size.width);
563 [[self bubble] addSubview:label];
565 // |buttonFrame| will be resized and repositioned later on.
566 NSRect buttonFrame = NSMakeRect(NSMinX(radioFrame), 0, 0, 0);
567 base::scoped_nsobject<NSPopUpButton> button(
568 [[NSPopUpButton alloc] initWithFrame:buttonFrame]);
569 [button setTarget:self];
571 // Set the map_entry's key value to |button| tag.
572 // MediaMenuPartsMap uses this value to order its elements.
573 [button setTag:static_cast<NSInteger>(map_entry.first)];
575 // Store the |label| and |button| into MediaMenuParts struct and build
576 // the popup menu from the menu model.
577 content_setting_bubble::MediaMenuParts* menuParts =
578 new content_setting_bubble::MediaMenuParts(map_entry.first, label);
579 menuParts->model.reset(new ContentSettingMediaMenuModel(
580 map_entry.first, contentSettingBubbleModel_.get(),
581 ContentSettingMediaMenuModel::MenuLabelChangedCallback()));
582 mediaMenus_[button] = menuParts;
583 CGFloat width = BuildPopUpMenuFromModel(
584 button, menuParts->model.get(), map_entry.second.selected_device.name,
585 map_entry.second.disabled);
586 maxMenuWidth = std::max(maxMenuWidth, width);
588 [[self bubble] addSubview:button
589 positioned:NSWindowBelow
592 maxMenuHeight = std::max(maxMenuHeight, [button frame].size.height);
595 // Make room for the media menu(s) and enlarge the windows to fit the views.
596 // The bubble view and its subviews autosize themselves when the window is
598 int delta = media_menus.size() * maxMenuHeight +
599 (media_menus.size() - 1) * kMediaMenuElementVerticalPadding;
600 NSSize deltaSize = NSMakeSize(0, delta);
601 deltaSize = [[[self window] contentView] convertSize:deltaSize toView:nil];
602 NSRect windowFrame = [[self window] frame];
603 windowFrame.size.height += deltaSize.height;
604 // If the media menus are wider than the window, widen the window.
605 CGFloat widthNeeded = maxLabelWidth + maxMenuWidth + 2 * NSMinX(radioFrame);
606 if (widthNeeded > windowFrame.size.width)
607 windowFrame.size.width = widthNeeded;
608 [[self window] setFrame:windowFrame display:NO];
610 // The radio group lies above the media menus, move the radio group up.
611 radioFrame.origin.y += delta;
612 [allowBlockRadioGroup_ setFrame:radioFrame];
614 // Resize and reposition the media menus layout.
615 CGFloat topMenuY = NSMinY(radioFrame) - kMediaMenuVerticalPadding;
616 maxMenuWidth = std::max(maxMenuWidth, kMinMediaMenuButtonWidth);
617 for (const std::pair<NSPopUpButton*, content_setting_bubble::MediaMenuParts*>&
618 map_entry : mediaMenus_) {
619 NSRect labelFrame = [map_entry.second->label frame];
620 // Align the label text with the button text.
621 labelFrame.origin.y =
622 topMenuY + (maxMenuHeight - labelFrame.size.height) / 2 + 1;
623 labelFrame.size.width = maxLabelWidth;
624 [map_entry.second->label setFrame:labelFrame];
625 NSRect menuFrame = [map_entry.first frame];
626 menuFrame.origin.y = topMenuY;
627 menuFrame.origin.x = NSMinX(radioFrame) + maxLabelWidth;
628 menuFrame.size.width = maxMenuWidth;
629 menuFrame.size.height = maxMenuHeight;
630 [map_entry.first setFrame:menuFrame];
631 topMenuY -= (maxMenuHeight + kMediaMenuElementVerticalPadding);
635 - (void)initializeMIDISysExLists {
636 const ContentSettingBubbleModel::BubbleContent& content =
637 contentSettingBubbleModel_->bubble_content();
638 NSRect containerFrame = [contentsContainer_ frame];
640 NSMakeRect(0, 0, NSWidth(containerFrame), kMIDISysExLabelHeight);
642 // "Clear" button / text field.
643 if (!content.custom_link.empty()) {
644 base::scoped_nsobject<NSControl> control;
645 if (content.custom_link_enabled) {
646 NSRect buttonFrame = NSMakeRect(0, 0,
647 NSWidth(containerFrame),
648 kMIDISysExClearButtonHeight);
649 NSButton* button = [[NSButton alloc] initWithFrame:buttonFrame];
650 control.reset(button);
651 [button setTitle:base::SysUTF8ToNSString(content.custom_link)];
652 [button setTarget:self];
653 [button setAction:@selector(clearMIDISysExForCurrentHost:)];
654 [button setBezelStyle:NSRoundRectBezelStyle];
655 SetControlSize(button, NSSmallControlSize);
658 // Add the notification that settings will be cleared on next reload.
659 control.reset([LabelWithFrame(
660 base::SysUTF8ToNSString(content.custom_link), frame) retain]);
661 SetControlSize(control.get(), NSSmallControlSize);
664 // If the new control is wider than the container, widen the window.
665 CGFloat controlWidth = NSWidth([control frame]);
666 if (controlWidth > NSWidth(containerFrame)) {
667 NSRect windowFrame = [[self window] frame];
668 windowFrame.size.width += controlWidth - NSWidth(containerFrame);
669 [[self window] setFrame:windowFrame display:NO];
670 // Fetch the updated sizes.
671 containerFrame = [contentsContainer_ frame];
672 frame = NSMakeRect(0, 0, NSWidth(containerFrame), kMIDISysExLabelHeight);
676 [contentsContainer_ addSubview:control];
677 frame.origin.y = NSMaxY([control frame]) + kMIDISysExPadding;
680 for (auto i = content.domain_lists.rbegin();
681 i != content.domain_lists.rend(); ++i) {
682 // Add all hosts in the current domain list.
683 for (auto j = i->hosts.rbegin(); j != i->hosts.rend(); ++j) {
684 NSTextField* title = LabelWithFrame(base::SysUTF8ToNSString(*j), frame);
685 SetControlSize(title, NSSmallControlSize);
686 [contentsContainer_ addSubview:title];
688 frame.origin.y = NSMaxY(frame) + kMIDISysExHostPadding +
689 [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:title];
691 if (!i->hosts.empty())
692 frame.origin.y += kMIDISysExPadding - kMIDISysExHostPadding;
694 // Add the domain list's title.
696 LabelWithFrame(base::SysUTF8ToNSString(i->title), frame);
697 SetControlSize(title, NSSmallControlSize);
698 [contentsContainer_ addSubview:title];
700 frame.origin.y = NSMaxY(frame) + kMIDISysExPadding +
701 [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:title];
704 CGFloat containerHeight = frame.origin.y;
705 // Undo last padding.
706 if (!content.domain_lists.empty())
707 containerHeight -= kMIDISysExPadding;
709 // Resize container to fit its subviews, and window to fit the container.
710 NSRect windowFrame = [[self window] frame];
711 windowFrame.size.height += containerHeight - NSHeight(containerFrame);
712 [[self window] setFrame:windowFrame display:NO];
713 containerFrame.size.height = containerHeight;
714 [contentsContainer_ setFrame:containerFrame];
717 - (void)sizeToFitLoadButton {
718 const ContentSettingBubbleModel::BubbleContent& content =
719 contentSettingBubbleModel_->bubble_content();
720 [loadButton_ setEnabled:content.custom_link_enabled];
722 // Resize horizontally to fit button if necessary.
723 NSRect windowFrame = [[self window] frame];
724 int widthNeeded = NSWidth([loadButton_ frame]) +
725 2 * NSMinX([loadButton_ frame]);
726 if (NSWidth(windowFrame) < widthNeeded) {
727 windowFrame.size.width = widthNeeded;
728 [[self window] setFrame:windowFrame display:NO];
732 - (void)initManageDoneButtons {
733 const ContentSettingBubbleModel::BubbleContent& content =
734 contentSettingBubbleModel_->bubble_content();
735 [manageButton_ setTitle:base::SysUTF8ToNSString(content.manage_link)];
736 [GTMUILocalizerAndLayoutTweaker sizeToFitView:manageButton_];
738 CGFloat actualWidth = NSWidth([[[self window] contentView] frame]);
739 CGFloat requiredWidth = NSMaxX([manageButton_ frame]) + kManageDonePadding +
740 NSWidth([[doneButton_ superview] frame]) - NSMinX([doneButton_ frame]);
741 if (requiredWidth <= actualWidth || !doneButton_ || !manageButton_)
744 // Resize window, autoresizing takes care of the rest.
745 NSSize size = NSMakeSize(requiredWidth - actualWidth, 0);
746 size = [[[self window] contentView] convertSize:size toView:nil];
747 NSRect frame = [[self window] frame];
748 frame.origin.x -= size.width;
749 frame.size.width += size.width;
750 [[self window] setFrame:frame display:NO];
753 - (void)awakeFromNib {
754 [super awakeFromNib];
756 [[self bubble] setArrowLocation:info_bubble::kTopRight];
758 // Adapt window size to bottom buttons. Do this before all other layouting.
759 [self initManageDoneButtons];
761 [self initializeTitle];
763 ContentSettingsType type = contentSettingBubbleModel_->content_type();
764 if (type == CONTENT_SETTINGS_TYPE_PLUGINS) {
765 [self sizeToFitLoadButton];
766 [self initializeBlockedPluginsList];
769 if (allowBlockRadioGroup_) // not bound in cookie bubble xib
770 [self initializeRadioGroup];
772 if (type == CONTENT_SETTINGS_TYPE_POPUPS ||
773 type == CONTENT_SETTINGS_TYPE_PLUGINS)
774 [self initializeItemList];
775 if (type == CONTENT_SETTINGS_TYPE_GEOLOCATION)
776 [self initializeGeoLists];
777 if (type == CONTENT_SETTINGS_TYPE_MEDIASTREAM)
778 [self initializeMediaMenus];
779 if (type == CONTENT_SETTINGS_TYPE_MIDI_SYSEX)
780 [self initializeMIDISysExLists];
783 ///////////////////////////////////////////////////////////////////////////////
784 // Actual application logic
786 - (IBAction)allowBlockToggled:(id)sender {
787 NSButtonCell *selectedCell = [sender selectedCell];
788 contentSettingBubbleModel_->OnRadioClicked([selectedCell tag] - 1);
791 - (void)popupLinkClicked:(id)sender {
792 content_setting_bubble::PopupLinks::iterator i(popupLinks_.find(sender));
793 DCHECK(i != popupLinks_.end());
794 contentSettingBubbleModel_->OnListItemClicked(i->second);
797 - (void)clearGeolocationForCurrentHost:(id)sender {
798 contentSettingBubbleModel_->OnCustomLinkClicked();
802 - (void)clearMIDISysExForCurrentHost:(id)sender {
803 contentSettingBubbleModel_->OnCustomLinkClicked();
807 - (IBAction)showMoreInfo:(id)sender {
808 contentSettingBubbleModel_->OnCustomLinkClicked();
812 - (IBAction)load:(id)sender {
813 contentSettingBubbleModel_->OnCustomLinkClicked();
817 - (IBAction)learnMoreLinkClicked:(id)sender {
818 contentSettingBubbleModel_->OnManageLinkClicked();
821 - (IBAction)manageBlocking:(id)sender {
822 contentSettingBubbleModel_->OnManageLinkClicked();
825 - (IBAction)closeBubble:(id)sender {
826 contentSettingBubbleModel_->OnDoneClicked();
830 - (IBAction)mediaMenuChanged:(id)sender {
831 NSPopUpButton* button = static_cast<NSPopUpButton*>(sender);
832 content_setting_bubble::MediaMenuPartsMap::const_iterator it(
833 mediaMenus_.find(sender));
834 DCHECK(it != mediaMenus_.end());
835 NSInteger index = [[button selectedItem] tag];
837 SetTitleForPopUpButton(
838 button, base::SysUTF16ToNSString(it->second->model->GetLabelAt(index)));
840 it->second->model->ExecuteCommand(index, 0);
843 - (content_setting_bubble::MediaMenuPartsMap*)mediaMenus {
847 @end // ContentSettingBubbleController