Popular sites on the NTP: check that experiment group StartsWith (rather than IS...
[chromium-blink-merge.git] / chrome / browser / ui / cocoa / content_settings / content_setting_bubble_cocoa.mm
blobeeab29d80534f94f15d06323a59720717e3edcb1
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;
29 namespace {
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,
112                                 bool disabled) {
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:)];
120   [button sizeToFit];
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);
142   }
144   if (!model->GetItemCount()) {
145     // Show a "None available" title and grey out the menu when there is no
146     // available device.
147     SetTitleForPopUpButton(
148         button, l10n_util::GetNSString(IDS_MEDIA_MENU_NO_DEVICE_TITLE));
149     [button setEnabled:NO];
150   } else {
151     SetTitleForPopUpButton(button, base::SysUTF8ToNSString(title));
153     // Disable the device selection when the website is managing the devices
154     // itself.
155     if (disabled)
156       [button setEnabled:NO];
157   }
159   return menuWidth;
162 }  // namespace
164 namespace content_setting_bubble {
166 MediaMenuParts::MediaMenuParts(content::MediaStreamType type,
167                                NSTextField* label)
168     : type(type),
169       label(label) {}
170 MediaMenuParts::~MediaMenuParts() {}
172 }  // namespace content_setting_bubble
174 class ContentSettingBubbleWebContentsObserverBridge
175     : public content::WebContentsObserver {
176  public:
177   ContentSettingBubbleWebContentsObserverBridge(
178       content::WebContents* web_contents,
179       ContentSettingBubbleController* controller)
180       : content::WebContentsObserver(web_contents),
181         controller_(controller) {
182   }
184  protected:
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
190     // close up shop.
191     [controller_ closeBubble:nil];
192   }
194  private:
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
207                                  icon:(NSImage*)icon
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;
222 @end
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
236                 anchoredAt:anchor];
239 struct ContentTypeToNibPath {
240   ContentSettingsType type;
241   NSString* path;
244 const ContentTypeToNibPath kNibPaths[] = {
245     {CONTENT_SETTINGS_TYPE_COOKIES, @"ContentBlockedCookies"},
246     {CONTENT_SETTINGS_TYPE_IMAGES, @"ContentBlockedSimple"},
247     {CONTENT_SETTINGS_TYPE_JAVASCRIPT, @"ContentBlockedSimple"},
248     {CONTENT_SETTINGS_TYPE_PPAPI_BROKER, @"ContentBlockedSimple"},
249     {CONTENT_SETTINGS_TYPE_PLUGINS, @"ContentBlockedPlugins"},
250     {CONTENT_SETTINGS_TYPE_POPUPS, @"ContentBlockedPopups"},
251     {CONTENT_SETTINGS_TYPE_GEOLOCATION, @"ContentBlockedGeolocation"},
252     {CONTENT_SETTINGS_TYPE_MIXEDSCRIPT, @"ContentBlockedMixedScript"},
253     {CONTENT_SETTINGS_TYPE_PROTOCOL_HANDLERS, @"ContentProtocolHandlers"},
254     {CONTENT_SETTINGS_TYPE_MEDIASTREAM, @"ContentBlockedMedia"},
255     {CONTENT_SETTINGS_TYPE_AUTOMATIC_DOWNLOADS, @"ContentBlockedDownloads"},
256     {CONTENT_SETTINGS_TYPE_MIDI_SYSEX, @"ContentBlockedMIDISysEx"},
259 - (id)initWithModel:(ContentSettingBubbleModel*)contentSettingBubbleModel
260         webContents:(content::WebContents*)webContents
261        parentWindow:(NSWindow*)parentWindow
262          anchoredAt:(NSPoint)anchoredAt {
263   // This method takes ownership of |contentSettingBubbleModel| in all cases.
264   scoped_ptr<ContentSettingBubbleModel> model(contentSettingBubbleModel);
265   DCHECK(model.get());
266   observerBridge_.reset(
267     new ContentSettingBubbleWebContentsObserverBridge(webContents, self));
269   ContentSettingsType settingsType = model->content_type();
270   DCHECK(ContainsKey(ContentSettingBubbleModel::GetSupportedBubbleTypes(),
271                      settingsType));
272   DCHECK_EQ(ContentSettingBubbleModel::GetSupportedBubbleTypes().size(),
273             arraysize(kNibPaths));
274   NSString* nibPath = @"";
275   for (const ContentTypeToNibPath& type_to_path : kNibPaths) {
276     if (settingsType == type_to_path.type) {
277       nibPath = type_to_path.path;
278       break;
279     }
280   }
281   DCHECK_NE(0u, [nibPath length]);
283   if ((self = [super initWithWindowNibPath:nibPath
284                               parentWindow:parentWindow
285                                 anchoredAt:anchoredAt])) {
286     contentSettingBubbleModel_.reset(model.release());
287     [self showWindow:nil];
288   }
289   return self;
292 - (void)dealloc {
293   STLDeleteValues(&mediaMenus_);
294   [super dealloc];
297 - (void)initializeTitle {
298   if (!titleLabel_)
299     return;
301   NSString* label = base::SysUTF8ToNSString(
302       contentSettingBubbleModel_->bubble_content().title);
303   [titleLabel_ setStringValue:label];
305   // Layout title post-localization.
306   CGFloat deltaY = [GTMUILocalizerAndLayoutTweaker
307       sizeToFitFixedWidthTextField:titleLabel_];
308   NSRect windowFrame = [[self window] frame];
309   windowFrame.size.height += deltaY;
310   [[self window] setFrame:windowFrame display:NO];
311   NSRect titleFrame = [titleLabel_ frame];
312   titleFrame.origin.y -= deltaY;
313   [titleLabel_ setFrame:titleFrame];
316 - (void)initializeRadioGroup {
317   // NOTE! Tags in the xib files must match the order of the radio buttons
318   // passed in the radio_group and be 1-based, not 0-based.
319   const ContentSettingBubbleModel::RadioGroup& radio_group =
320       contentSettingBubbleModel_->bubble_content().radio_group;
322   // Xcode 5.1 Interface Builder doesn't allow a font property to be set for
323   // NSMatrix. The implementation of GTMUILocalizerAndLayoutTweaker assumes that
324   // the font for each of the cells in a NSMatrix is identical, and is the font
325   // of the NSMatrix. This logic sets the font of NSMatrix to be that of its
326   // cells.
327   NSFont* font = nil;
328   for (NSCell* cell in [allowBlockRadioGroup_ cells]) {
329     if (!font)
330       font = [cell font];
331     DCHECK([font isEqual:[cell font]]);
332   }
333   [allowBlockRadioGroup_ setFont:font];
335   // Select appropriate radio button.
336   [allowBlockRadioGroup_ selectCellWithTag: radio_group.default_item + 1];
338   const ContentSettingBubbleModel::RadioItems& radio_items =
339       radio_group.radio_items;
340   for (size_t ii = 0; ii < radio_group.radio_items.size(); ++ii) {
341     NSCell* radioCell = [allowBlockRadioGroup_ cellWithTag: ii + 1];
342     [radioCell setTitle:base::SysUTF8ToNSString(radio_items[ii])];
343   }
345   // Layout radio group labels post-localization.
346   [GTMUILocalizerAndLayoutTweaker
347       wrapRadioGroupForWidth:allowBlockRadioGroup_];
348   CGFloat radioDeltaY = [GTMUILocalizerAndLayoutTweaker
349       sizeToFitView:allowBlockRadioGroup_].height;
350   NSRect windowFrame = [[self window] frame];
351   windowFrame.size.height += radioDeltaY;
352   [[self window] setFrame:windowFrame display:NO];
355 - (NSButton*)hyperlinkButtonWithFrame:(NSRect)frame
356                                 title:(NSString*)title
357                                  icon:(NSImage*)icon
358                        referenceFrame:(NSRect)referenceFrame {
359   base::scoped_nsobject<HyperlinkButtonCell> cell(
360       [[HyperlinkButtonCell alloc] initTextCell:title]);
361   [cell.get() setAlignment:NSNaturalTextAlignment];
362   if (icon) {
363     [cell.get() setImagePosition:NSImageLeft];
364     [cell.get() setImage:icon];
365   } else {
366     [cell.get() setImagePosition:NSNoImage];
367   }
368   [cell.get() setControlSize:NSSmallControlSize];
370   NSButton* button = [[[NSButton alloc] initWithFrame:frame] autorelease];
371   // Cell must be set immediately after construction.
372   [button setCell:cell.get()];
374   // Size to fit the button and add a little extra padding for the small-text
375   // hyperlink button, which sizeToFit gets wrong.
376   [GTMUILocalizerAndLayoutTweaker sizeToFitView:button];
377   NSRect buttonFrame = [button frame];
378   buttonFrame.size.width += 2;
380   // If the link text is too long, clamp it.
381   int maxWidth = NSWidth([[self bubble] frame]) - 2 * NSMinX(referenceFrame);
382   if (NSWidth(buttonFrame) > maxWidth)
383     buttonFrame.size.width = maxWidth;
385   [button setFrame:buttonFrame];
386   [button setTarget:self];
387   [button setAction:@selector(popupLinkClicked:)];
388   return button;
391 - (void)initializeBlockedPluginsList {
392   // Hide the empty label at the top of the dialog.
393   int delta =
394       NSMinY([titleLabel_ frame]) - NSMinY([blockedResourcesField_ frame]);
395   [blockedResourcesField_ removeFromSuperview];
396   NSRect frame = [[self window] frame];
397   frame.size.height -= delta;
398   [[self window] setFrame:frame display:NO];
401 - (void)initializeItemList {
402   // I didn't put the buttons into a NSMatrix because then they are only one
403   // entity in the key view loop. This way, one can tab through all of them.
404   const ContentSettingBubbleModel::ListItems& listItems =
405       contentSettingBubbleModel_->bubble_content().list_items;
407   // Get the pre-resize frame of the radio group. Its origin is where the
408   // popup list should go.
409   NSRect radioFrame = [allowBlockRadioGroup_ frame];
411   // Make room for the popup list. The bubble view and its subviews autosize
412   // themselves when the window is enlarged.
413   // Heading and radio box are already 1 * kLinkOuterPadding apart in the nib,
414   // so only 1 * kLinkOuterPadding more is needed.
415   int delta =
416       listItems.size() * kLinkLineHeight - kLinkPadding + kLinkOuterPadding;
417   NSSize deltaSize = NSMakeSize(0, delta);
418   deltaSize = [[[self window] contentView] convertSize:deltaSize toView:nil];
419   NSRect windowFrame = [[self window] frame];
420   windowFrame.size.height += deltaSize.height;
421   [[self window] setFrame:windowFrame display:NO];
423   // Create item list.
424   int topLinkY = NSMaxY(radioFrame) + delta - kLinkHeight;
425   int row = 0;
426   for (const ContentSettingBubbleModel::ListItem& listItem : listItems) {
427     NSImage* image = listItem.image.AsNSImage();
428     NSRect frame = NSMakeRect(
429         NSMinX(radioFrame), topLinkY - kLinkLineHeight * row, 200, kLinkHeight);
430     if (listItem.has_link) {
431       NSButton* button =
432           [self hyperlinkButtonWithFrame:frame
433                                    title:base::SysUTF8ToNSString(listItem.title)
434                                     icon:image
435                           referenceFrame:radioFrame];
436       [[self bubble] addSubview:button];
437       popupLinks_[button] = row++;
438     } else {
439       NSTextField* label =
440           LabelWithFrame(base::SysUTF8ToNSString(listItem.title), frame);
441       SetControlSize(label, NSSmallControlSize);
442       [[self bubble] addSubview:label];
443       row++;
444     }
445   }
448 - (void)initializeGeoLists {
449   // Cocoa has its origin in the lower left corner. This means elements are
450   // added from bottom to top, which explains why loops run backwards and the
451   // order of operations is the other way than on Linux/Windows.
452   const ContentSettingBubbleModel::BubbleContent& content =
453       contentSettingBubbleModel_->bubble_content();
454   NSRect containerFrame = [contentsContainer_ frame];
455   NSRect frame = NSMakeRect(0, 0, NSWidth(containerFrame), kGeoLabelHeight);
457   // "Clear" button / text field.
458   if (!content.custom_link.empty()) {
459     base::scoped_nsobject<NSControl> control;
460     if(content.custom_link_enabled) {
461       NSRect buttonFrame = NSMakeRect(0, 0,
462                                       NSWidth(containerFrame),
463                                       kGeoClearButtonHeight);
464       NSButton* button = [[NSButton alloc] initWithFrame:buttonFrame];
465       control.reset(button);
466       [button setTitle:base::SysUTF8ToNSString(content.custom_link)];
467       [button setTarget:self];
468       [button setAction:@selector(clearGeolocationForCurrentHost:)];
469       [button setBezelStyle:NSRoundRectBezelStyle];
470       SetControlSize(button, NSSmallControlSize);
471       [button sizeToFit];
472     } else {
473       // Add the notification that settings will be cleared on next reload.
474       control.reset([LabelWithFrame(
475           base::SysUTF8ToNSString(content.custom_link), frame) retain]);
476       SetControlSize(control.get(), NSSmallControlSize);
477     }
479     // If the new control is wider than the container, widen the window.
480     CGFloat controlWidth = NSWidth([control frame]);
481     if (controlWidth > NSWidth(containerFrame)) {
482       NSRect windowFrame = [[self window] frame];
483       windowFrame.size.width += controlWidth - NSWidth(containerFrame);
484       [[self window] setFrame:windowFrame display:NO];
485       // Fetch the updated sizes.
486       containerFrame = [contentsContainer_ frame];
487       frame = NSMakeRect(0, 0, NSWidth(containerFrame), kGeoLabelHeight);
488     }
490     DCHECK(control);
491     [contentsContainer_ addSubview:control];
492     frame.origin.y = NSMaxY([control frame]) + kGeoPadding;
493   }
495   for (auto i = content.domain_lists.rbegin();
496        i != content.domain_lists.rend(); ++i) {
497     // Add all hosts in the current domain list.
498     for (auto j = i->hosts.rbegin(); j != i->hosts.rend(); ++j) {
499       NSTextField* title = LabelWithFrame(base::SysUTF8ToNSString(*j), frame);
500       SetControlSize(title, NSSmallControlSize);
501       [contentsContainer_ addSubview:title];
503       frame.origin.y = NSMaxY(frame) + kGeoHostPadding +
504           [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:title];
505     }
506     if (!i->hosts.empty())
507       frame.origin.y += kGeoPadding - kGeoHostPadding;
509     // Add the domain list's title.
510     NSTextField* title =
511         LabelWithFrame(base::SysUTF8ToNSString(i->title), frame);
512     SetControlSize(title, NSSmallControlSize);
513     [contentsContainer_ addSubview:title];
515     frame.origin.y = NSMaxY(frame) + kGeoPadding +
516         [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:title];
517   }
519   CGFloat containerHeight = frame.origin.y;
520   // Undo last padding.
521   if (!content.domain_lists.empty())
522     containerHeight -= kGeoPadding;
524   // Resize container to fit its subviews, and window to fit the container.
525   NSRect windowFrame = [[self window] frame];
526   windowFrame.size.height += containerHeight - NSHeight(containerFrame);
527   [[self window] setFrame:windowFrame display:NO];
528   containerFrame.size.height = containerHeight;
529   [contentsContainer_ setFrame:containerFrame];
532 - (void)initializeMediaMenus {
533   const ContentSettingBubbleModel::MediaMenuMap& media_menus =
534       contentSettingBubbleModel_->bubble_content().media_menus;
536   // Calculate the longest width of the labels and menus menus to avoid
537   // truncation by the window's edge.
538   CGFloat maxLabelWidth = 0;
539   CGFloat maxMenuWidth = 0;
540   CGFloat maxMenuHeight = 0;
541   NSRect radioFrame = [allowBlockRadioGroup_ frame];
542   for (const std::pair<content::MediaStreamType,
543                        ContentSettingBubbleModel::MediaMenu>& map_entry :
544        media_menus) {
545     // |labelFrame| will be resized later on in this function.
546     NSRect labelFrame = NSMakeRect(NSMinX(radioFrame), 0, 0, 0);
547     NSTextField* label = LabelWithFrame(
548         base::SysUTF8ToNSString(map_entry.second.label), labelFrame);
549     SetControlSize(label, NSSmallControlSize);
550     NSCell* cell = [label cell];
551     [cell setAlignment:NSRightTextAlignment];
552     [GTMUILocalizerAndLayoutTweaker sizeToFitView:label];
553     maxLabelWidth = std::max(maxLabelWidth, [label frame].size.width);
554     [[self bubble] addSubview:label];
556     // |buttonFrame| will be resized and repositioned later on.
557     NSRect buttonFrame = NSMakeRect(NSMinX(radioFrame), 0, 0, 0);
558     base::scoped_nsobject<NSPopUpButton> button(
559         [[NSPopUpButton alloc] initWithFrame:buttonFrame]);
560     [button setTarget:self];
562     // Set the map_entry's key value to |button| tag.
563     // MediaMenuPartsMap uses this value to order its elements.
564     [button setTag:static_cast<NSInteger>(map_entry.first)];
566     // Store the |label| and |button| into MediaMenuParts struct and build
567     // the popup menu from the menu model.
568     content_setting_bubble::MediaMenuParts* menuParts =
569         new content_setting_bubble::MediaMenuParts(map_entry.first, label);
570     menuParts->model.reset(new ContentSettingMediaMenuModel(
571         map_entry.first, contentSettingBubbleModel_.get(),
572         ContentSettingMediaMenuModel::MenuLabelChangedCallback()));
573     mediaMenus_[button] = menuParts;
574     CGFloat width = BuildPopUpMenuFromModel(
575         button, menuParts->model.get(), map_entry.second.selected_device.name,
576         map_entry.second.disabled);
577     maxMenuWidth = std::max(maxMenuWidth, width);
579     [[self bubble] addSubview:button
580                    positioned:NSWindowBelow
581                    relativeTo:nil];
583     maxMenuHeight = std::max(maxMenuHeight, [button frame].size.height);
584   }
586   // Make room for the media menu(s) and enlarge the windows to fit the views.
587   // The bubble view and its subviews autosize themselves when the window is
588   // enlarged.
589   int delta = media_menus.size() * maxMenuHeight +
590       (media_menus.size() - 1) * kMediaMenuElementVerticalPadding;
591   NSSize deltaSize = NSMakeSize(0, delta);
592   deltaSize = [[[self window] contentView] convertSize:deltaSize toView:nil];
593   NSRect windowFrame = [[self window] frame];
594   windowFrame.size.height += deltaSize.height;
595   // If the media menus are wider than the window, widen the window.
596   CGFloat widthNeeded = maxLabelWidth + maxMenuWidth + 2 * NSMinX(radioFrame);
597   if (widthNeeded > windowFrame.size.width)
598     windowFrame.size.width = widthNeeded;
599   [[self window] setFrame:windowFrame display:NO];
601   // The radio group lies above the media menus, move the radio group up.
602   radioFrame.origin.y += delta;
603   [allowBlockRadioGroup_ setFrame:radioFrame];
605   // Resize and reposition the media menus layout.
606   CGFloat topMenuY = NSMinY(radioFrame) - kMediaMenuVerticalPadding;
607   maxMenuWidth = std::max(maxMenuWidth, kMinMediaMenuButtonWidth);
608   for (const std::pair<NSPopUpButton*, content_setting_bubble::MediaMenuParts*>&
609            map_entry : mediaMenus_) {
610     NSRect labelFrame = [map_entry.second->label frame];
611     // Align the label text with the button text.
612     labelFrame.origin.y =
613         topMenuY + (maxMenuHeight - labelFrame.size.height) / 2 + 1;
614     labelFrame.size.width = maxLabelWidth;
615     [map_entry.second->label setFrame:labelFrame];
616     NSRect menuFrame = [map_entry.first frame];
617     menuFrame.origin.y = topMenuY;
618     menuFrame.origin.x = NSMinX(radioFrame) + maxLabelWidth;
619     menuFrame.size.width = maxMenuWidth;
620     menuFrame.size.height = maxMenuHeight;
621     [map_entry.first setFrame:menuFrame];
622     topMenuY -= (maxMenuHeight + kMediaMenuElementVerticalPadding);
623   }
626 - (void)initializeMIDISysExLists {
627   const ContentSettingBubbleModel::BubbleContent& content =
628       contentSettingBubbleModel_->bubble_content();
629   NSRect containerFrame = [contentsContainer_ frame];
630   NSRect frame =
631       NSMakeRect(0, 0, NSWidth(containerFrame), kMIDISysExLabelHeight);
633   // "Clear" button / text field.
634   if (!content.custom_link.empty()) {
635     base::scoped_nsobject<NSControl> control;
636     if (content.custom_link_enabled) {
637       NSRect buttonFrame = NSMakeRect(0, 0,
638                                       NSWidth(containerFrame),
639                                       kMIDISysExClearButtonHeight);
640       NSButton* button = [[NSButton alloc] initWithFrame:buttonFrame];
641       control.reset(button);
642       [button setTitle:base::SysUTF8ToNSString(content.custom_link)];
643       [button setTarget:self];
644       [button setAction:@selector(clearMIDISysExForCurrentHost:)];
645       [button setBezelStyle:NSRoundRectBezelStyle];
646       SetControlSize(button, NSSmallControlSize);
647       [button sizeToFit];
648     } else {
649       // Add the notification that settings will be cleared on next reload.
650       control.reset([LabelWithFrame(
651           base::SysUTF8ToNSString(content.custom_link), frame) retain]);
652       SetControlSize(control.get(), NSSmallControlSize);
653     }
655     // If the new control is wider than the container, widen the window.
656     CGFloat controlWidth = NSWidth([control frame]);
657     if (controlWidth > NSWidth(containerFrame)) {
658       NSRect windowFrame = [[self window] frame];
659       windowFrame.size.width += controlWidth - NSWidth(containerFrame);
660       [[self window] setFrame:windowFrame display:NO];
661       // Fetch the updated sizes.
662       containerFrame = [contentsContainer_ frame];
663       frame = NSMakeRect(0, 0, NSWidth(containerFrame), kMIDISysExLabelHeight);
664     }
666     DCHECK(control);
667     [contentsContainer_ addSubview:control];
668     frame.origin.y = NSMaxY([control frame]) + kMIDISysExPadding;
669   }
671   for (auto i = content.domain_lists.rbegin();
672        i != content.domain_lists.rend(); ++i) {
673     // Add all hosts in the current domain list.
674     for (auto j = i->hosts.rbegin(); j != i->hosts.rend(); ++j) {
675       NSTextField* title = LabelWithFrame(base::SysUTF8ToNSString(*j), frame);
676       SetControlSize(title, NSSmallControlSize);
677       [contentsContainer_ addSubview:title];
679       frame.origin.y = NSMaxY(frame) + kMIDISysExHostPadding +
680           [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:title];
681     }
682     if (!i->hosts.empty())
683       frame.origin.y += kMIDISysExPadding - kMIDISysExHostPadding;
685     // Add the domain list's title.
686     NSTextField* title =
687         LabelWithFrame(base::SysUTF8ToNSString(i->title), frame);
688     SetControlSize(title, NSSmallControlSize);
689     [contentsContainer_ addSubview:title];
691     frame.origin.y = NSMaxY(frame) + kMIDISysExPadding +
692         [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:title];
693   }
695   CGFloat containerHeight = frame.origin.y;
696   // Undo last padding.
697   if (!content.domain_lists.empty())
698     containerHeight -= kMIDISysExPadding;
700   // Resize container to fit its subviews, and window to fit the container.
701   NSRect windowFrame = [[self window] frame];
702   windowFrame.size.height += containerHeight - NSHeight(containerFrame);
703   [[self window] setFrame:windowFrame display:NO];
704   containerFrame.size.height = containerHeight;
705   [contentsContainer_ setFrame:containerFrame];
708 - (void)sizeToFitLoadButton {
709   const ContentSettingBubbleModel::BubbleContent& content =
710       contentSettingBubbleModel_->bubble_content();
711   [loadButton_ setEnabled:content.custom_link_enabled];
713   // Resize horizontally to fit button if necessary.
714   NSRect windowFrame = [[self window] frame];
715   int widthNeeded = NSWidth([loadButton_ frame]) +
716       2 * NSMinX([loadButton_ frame]);
717   if (NSWidth(windowFrame) < widthNeeded) {
718     windowFrame.size.width = widthNeeded;
719     [[self window] setFrame:windowFrame display:NO];
720   }
723 - (void)initManageDoneButtons {
724   const ContentSettingBubbleModel::BubbleContent& content =
725       contentSettingBubbleModel_->bubble_content();
726   [manageButton_ setTitle:base::SysUTF8ToNSString(content.manage_link)];
727   [GTMUILocalizerAndLayoutTweaker sizeToFitView:[manageButton_ superview]];
729   CGFloat actualWidth = NSWidth([[[self window] contentView] frame]);
730   CGFloat requiredWidth = NSMaxX([manageButton_ frame]) + kManageDonePadding +
731       NSWidth([[doneButton_ superview] frame]) - NSMinX([doneButton_ frame]);
732   if (requiredWidth <= actualWidth || !doneButton_ || !manageButton_)
733     return;
735   // Resize window, autoresizing takes care of the rest.
736   NSSize size = NSMakeSize(requiredWidth - actualWidth, 0);
737   size = [[[self window] contentView] convertSize:size toView:nil];
738   NSRect frame = [[self window] frame];
739   frame.origin.x -= size.width;
740   frame.size.width += size.width;
741   [[self window] setFrame:frame display:NO];
744 - (void)awakeFromNib {
745   [super awakeFromNib];
747   [[self bubble] setArrowLocation:info_bubble::kTopRight];
749   // Adapt window size to bottom buttons. Do this before all other layouting.
750   [self initManageDoneButtons];
752   [self initializeTitle];
754   ContentSettingsType type = contentSettingBubbleModel_->content_type();
755   if (type == CONTENT_SETTINGS_TYPE_PLUGINS) {
756     [self sizeToFitLoadButton];
757     [self initializeBlockedPluginsList];
758   }
760   if (allowBlockRadioGroup_)  // not bound in cookie bubble xib
761     [self initializeRadioGroup];
763   if (type == CONTENT_SETTINGS_TYPE_POPUPS ||
764       type == CONTENT_SETTINGS_TYPE_PLUGINS)
765     [self initializeItemList];
766   if (type == CONTENT_SETTINGS_TYPE_GEOLOCATION)
767     [self initializeGeoLists];
768   if (type == CONTENT_SETTINGS_TYPE_MEDIASTREAM)
769     [self initializeMediaMenus];
770   if (type == CONTENT_SETTINGS_TYPE_MIDI_SYSEX)
771     [self initializeMIDISysExLists];
774 ///////////////////////////////////////////////////////////////////////////////
775 // Actual application logic
777 - (IBAction)allowBlockToggled:(id)sender {
778   NSButtonCell *selectedCell = [sender selectedCell];
779   contentSettingBubbleModel_->OnRadioClicked([selectedCell tag] - 1);
782 - (void)popupLinkClicked:(id)sender {
783   content_setting_bubble::PopupLinks::iterator i(popupLinks_.find(sender));
784   DCHECK(i != popupLinks_.end());
785   contentSettingBubbleModel_->OnListItemClicked(i->second);
788 - (void)clearGeolocationForCurrentHost:(id)sender {
789   contentSettingBubbleModel_->OnCustomLinkClicked();
790   [self close];
793 - (void)clearMIDISysExForCurrentHost:(id)sender {
794   contentSettingBubbleModel_->OnCustomLinkClicked();
795   [self close];
798 - (IBAction)showMoreInfo:(id)sender {
799   contentSettingBubbleModel_->OnCustomLinkClicked();
800   [self close];
803 - (IBAction)load:(id)sender {
804   contentSettingBubbleModel_->OnCustomLinkClicked();
805   [self close];
808 - (IBAction)learnMoreLinkClicked:(id)sender {
809   contentSettingBubbleModel_->OnManageLinkClicked();
812 - (IBAction)manageBlocking:(id)sender {
813   contentSettingBubbleModel_->OnManageLinkClicked();
816 - (IBAction)closeBubble:(id)sender {
817   contentSettingBubbleModel_->OnDoneClicked();
818   [self close];
821 - (IBAction)mediaMenuChanged:(id)sender {
822   NSPopUpButton* button = static_cast<NSPopUpButton*>(sender);
823   content_setting_bubble::MediaMenuPartsMap::const_iterator it(
824       mediaMenus_.find(sender));
825   DCHECK(it != mediaMenus_.end());
826   NSInteger index = [[button selectedItem] tag];
828   SetTitleForPopUpButton(
829       button, base::SysUTF16ToNSString(it->second->model->GetLabelAt(index)));
831   it->second->model->ExecuteCommand(index, 0);
834 - (content_setting_bubble::MediaMenuPartsMap*)mediaMenus {
835   return &mediaMenus_;
838 @end  // ContentSettingBubbleController