- add sources.
[platform/framework/web/crosswalk.git] / src / chrome / browser / ui / cocoa / content_settings / content_setting_bubble_cocoa.mm
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.
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/stl_util.h"
10 #include "base/strings/sys_string_conversions.h"
11 #include "base/strings/utf_string_conversions.h"
12 #include "chrome/browser/content_settings/host_content_settings_map.h"
13 #include "chrome/browser/plugins/plugin_finder.h"
14 #include "chrome/browser/plugins/plugin_metadata.h"
15 #import "chrome/browser/ui/cocoa/info_bubble_view.h"
16 #import "chrome/browser/ui/cocoa/l10n_util.h"
17 #include "chrome/browser/ui/content_settings/content_setting_bubble_model.h"
18 #include "chrome/browser/ui/content_settings/content_setting_media_menu_model.h"
19 #include "content/public/browser/plugin_service.h"
20 #include "grit/generated_resources.h"
21 #include "skia/ext/skia_utils_mac.h"
22 #import "third_party/GTM/AppKit/GTMUILocalizerAndLayoutTweaker.h"
23 #import "ui/base/cocoa/controls/hyperlink_button_cell.h"
24 #include "ui/base/l10n/l10n_util.h"
25
26 using content::PluginService;
27
28 namespace {
29
30 // Height of one link in the popup list.
31 const int kLinkHeight = 16;
32
33 // Space between two popup links.
34 const int kLinkPadding = 4;
35
36 // Space taken in total by one popup link.
37 const int kLinkLineHeight = kLinkHeight + kLinkPadding;
38
39 // Space between popup list and surrounding UI elements.
40 const int kLinkOuterPadding = 8;
41
42 // Height of each of the labels in the geolocation bubble.
43 const int kGeoLabelHeight = 14;
44
45 // Height of the "Clear" button in the geolocation bubble.
46 const int kGeoClearButtonHeight = 17;
47
48 // General padding between elements in the geolocation bubble.
49 const int kGeoPadding = 8;
50
51 // Padding between host names in the geolocation bubble.
52 const int kGeoHostPadding = 4;
53
54 // Minimal padding between "Manage" and "Done" buttons.
55 const int kManageDonePadding = 8;
56
57 // Padding between radio buttons and media menus buttons in the media bubble.
58 const int kMediaMenuVerticalPadding = 25;
59
60 // Padding between media menu elements in the media bubble.
61 const int kMediaMenuElementVerticalPadding = 5;
62
63 // The amount of horizontal space between the media menu title and the border.
64 const int kMediaMenuTitleHorizontalPadding = 10;
65
66 // The minimum width of the media menu buttons.
67 const CGFloat kMinMediaMenuButtonWidth = 100;
68
69 // Height of each of the labels in the MIDI bubble.
70 const int kMIDISysExLabelHeight = 14;
71
72 // Height of the "Clear" button in the MIDI bubble.
73 const int kMIDISysExClearButtonHeight = 17;
74
75 // General padding between elements in the MIDI bubble.
76 const int kMIDISysExPadding = 8;
77
78 // Padding between host names in the MIDI bubble.
79 const int kMIDISysExHostPadding = 4;
80
81 void SetControlSize(NSControl* control, NSControlSize controlSize) {
82   CGFloat fontSize = [NSFont systemFontSizeForControlSize:controlSize];
83   NSCell* cell = [control cell];
84   NSFont* font = [NSFont fontWithName:[[cell font] fontName] size:fontSize];
85   [cell setFont:font];
86   [cell setControlSize:controlSize];
87 }
88
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];
97 }
98
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()];
105 }
106
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];
121
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];
128
129     if (UTF16ToUTF8(model->GetLabelAt(i)) == title)
130       [button selectItemWithTag:i];
131
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   }
143
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));
152
153     // Disable the device selection when the website is managing the devices
154     // itself.
155     if (disabled)
156       [button setEnabled:NO];
157   }
158
159   return menuWidth;
160 }
161
162 }  // namespace
163
164 namespace content_setting_bubble {
165
166 MediaMenuParts::MediaMenuParts(content::MediaStreamType type,
167                                NSTextField* label)
168     : type(type),
169       label(label) {}
170 MediaMenuParts::~MediaMenuParts() {}
171
172 }  // namespace content_setting_bubble
173
174 @interface ContentSettingBubbleController(Private)
175 - (id)initWithModel:(ContentSettingBubbleModel*)settingsBubbleModel
176        parentWindow:(NSWindow*)parentWindow
177          anchoredAt:(NSPoint)anchoredAt;
178 - (NSButton*)hyperlinkButtonWithFrame:(NSRect)frame
179                                 title:(NSString*)title
180                                  icon:(NSImage*)icon
181                        referenceFrame:(NSRect)referenceFrame;
182 - (void)initializeBlockedPluginsList;
183 - (void)initializeTitle;
184 - (void)initializeRadioGroup;
185 - (void)initializePopupList;
186 - (void)initializeGeoLists;
187 - (void)initializeMediaMenus;
188 - (void)initializeMIDISysExLists;
189 - (void)sizeToFitLoadButton;
190 - (void)initManageDoneButtons;
191 - (void)removeInfoButton;
192 - (void)popupLinkClicked:(id)sender;
193 - (void)clearGeolocationForCurrentHost:(id)sender;
194 - (void)clearMIDISysExForCurrentHost:(id)sender;
195 @end
196
197 @implementation ContentSettingBubbleController
198
199 + (ContentSettingBubbleController*)
200     showForModel:(ContentSettingBubbleModel*)contentSettingBubbleModel
201     parentWindow:(NSWindow*)parentWindow
202       anchoredAt:(NSPoint)anchor {
203   // Autoreleases itself on bubble close.
204   return [[ContentSettingBubbleController alloc]
205              initWithModel:contentSettingBubbleModel
206               parentWindow:parentWindow
207                 anchoredAt:anchor];
208 }
209
210 - (id)initWithModel:(ContentSettingBubbleModel*)contentSettingBubbleModel
211        parentWindow:(NSWindow*)parentWindow
212          anchoredAt:(NSPoint)anchoredAt {
213   // This method takes ownership of |contentSettingBubbleModel| in all cases.
214   scoped_ptr<ContentSettingBubbleModel> model(contentSettingBubbleModel);
215   DCHECK(model.get());
216
217   ContentSettingsType settingsType = model->content_type();
218   NSString* nibPath = @"";
219   switch (settingsType) {
220     case CONTENT_SETTINGS_TYPE_COOKIES:
221       nibPath = @"ContentBlockedCookies"; break;
222     case CONTENT_SETTINGS_TYPE_IMAGES:
223     case CONTENT_SETTINGS_TYPE_JAVASCRIPT:
224     case CONTENT_SETTINGS_TYPE_PPAPI_BROKER:
225       nibPath = @"ContentBlockedSimple"; break;
226     case CONTENT_SETTINGS_TYPE_PLUGINS:
227       nibPath = @"ContentBlockedPlugins"; break;
228     case CONTENT_SETTINGS_TYPE_POPUPS:
229       nibPath = @"ContentBlockedPopups"; break;
230     case CONTENT_SETTINGS_TYPE_GEOLOCATION:
231       nibPath = @"ContentBlockedGeolocation"; break;
232     case CONTENT_SETTINGS_TYPE_MIXEDSCRIPT:
233       nibPath = @"ContentBlockedMixedScript"; break;
234     case CONTENT_SETTINGS_TYPE_PROTOCOL_HANDLERS:
235       nibPath = @"ContentProtocolHandlers"; break;
236     case CONTENT_SETTINGS_TYPE_MEDIASTREAM:
237       nibPath = @"ContentBlockedMedia"; break;
238     case CONTENT_SETTINGS_TYPE_AUTOMATIC_DOWNLOADS:
239       nibPath = @"ContentBlockedDownloads"; break;
240     case CONTENT_SETTINGS_TYPE_MIDI_SYSEX:
241       nibPath = @"ContentBlockedMIDISysEx"; break;
242     case CONTENT_SETTINGS_TYPE_SAVE_PASSWORD:
243       nibPath = @"ContentBlockedSavePassword"; break;
244     // These content types have no bubble:
245     case CONTENT_SETTINGS_TYPE_DEFAULT:
246     case CONTENT_SETTINGS_TYPE_NOTIFICATIONS:
247     case CONTENT_SETTINGS_TYPE_AUTO_SELECT_CERTIFICATE:
248     case CONTENT_SETTINGS_TYPE_FULLSCREEN:
249     case CONTENT_SETTINGS_TYPE_MOUSELOCK:
250     case CONTENT_SETTINGS_TYPE_MEDIASTREAM_MIC:
251     case CONTENT_SETTINGS_TYPE_MEDIASTREAM_CAMERA:
252     case CONTENT_SETTINGS_NUM_TYPES:
253       NOTREACHED();
254   }
255   if ((self = [super initWithWindowNibPath:nibPath
256                               parentWindow:parentWindow
257                                 anchoredAt:anchoredAt])) {
258     contentSettingBubbleModel_.reset(model.release());
259     [self showWindow:nil];
260   }
261   return self;
262 }
263
264 - (void)dealloc {
265   STLDeleteValues(&mediaMenus_);
266   [super dealloc];
267 }
268
269 - (void)initializeTitle {
270   if (!titleLabel_)
271     return;
272
273   NSString* label = base::SysUTF8ToNSString(
274       contentSettingBubbleModel_->bubble_content().title);
275   [titleLabel_ setStringValue:label];
276
277   // Layout title post-localization.
278   CGFloat deltaY = [GTMUILocalizerAndLayoutTweaker
279       sizeToFitFixedWidthTextField:titleLabel_];
280   NSRect windowFrame = [[self window] frame];
281   windowFrame.size.height += deltaY;
282   [[self window] setFrame:windowFrame display:NO];
283   NSRect titleFrame = [titleLabel_ frame];
284   titleFrame.origin.y -= deltaY;
285   [titleLabel_ setFrame:titleFrame];
286 }
287
288 - (void)initializeRadioGroup {
289   // NOTE! Tags in the xib files must match the order of the radio buttons
290   // passed in the radio_group and be 1-based, not 0-based.
291   const ContentSettingBubbleModel::RadioGroup& radio_group =
292       contentSettingBubbleModel_->bubble_content().radio_group;
293
294   // Select appropriate radio button.
295   [allowBlockRadioGroup_ selectCellWithTag: radio_group.default_item + 1];
296
297   const ContentSettingBubbleModel::RadioItems& radio_items =
298       radio_group.radio_items;
299   for (size_t ii = 0; ii < radio_group.radio_items.size(); ++ii) {
300     NSCell* radioCell = [allowBlockRadioGroup_ cellWithTag: ii + 1];
301     [radioCell setTitle:base::SysUTF8ToNSString(radio_items[ii])];
302   }
303
304   // Layout radio group labels post-localization.
305   [GTMUILocalizerAndLayoutTweaker
306       wrapRadioGroupForWidth:allowBlockRadioGroup_];
307   CGFloat radioDeltaY = [GTMUILocalizerAndLayoutTweaker
308       sizeToFitView:allowBlockRadioGroup_].height;
309   NSRect windowFrame = [[self window] frame];
310   windowFrame.size.height += radioDeltaY;
311   [[self window] setFrame:windowFrame display:NO];
312 }
313
314 - (NSButton*)hyperlinkButtonWithFrame:(NSRect)frame
315                                 title:(NSString*)title
316                                  icon:(NSImage*)icon
317                        referenceFrame:(NSRect)referenceFrame {
318   base::scoped_nsobject<HyperlinkButtonCell> cell(
319       [[HyperlinkButtonCell alloc] initTextCell:title]);
320   [cell.get() setAlignment:NSNaturalTextAlignment];
321   if (icon) {
322     [cell.get() setImagePosition:NSImageLeft];
323     [cell.get() setImage:icon];
324   } else {
325     [cell.get() setImagePosition:NSNoImage];
326   }
327   [cell.get() setControlSize:NSSmallControlSize];
328
329   NSButton* button = [[[NSButton alloc] initWithFrame:frame] autorelease];
330   // Cell must be set immediately after construction.
331   [button setCell:cell.get()];
332
333   // Size to fit the button and add a little extra padding for the small-text
334   // hyperlink button, which sizeToFit gets wrong.
335   [GTMUILocalizerAndLayoutTweaker sizeToFitView:button];
336   NSRect buttonFrame = [button frame];
337   buttonFrame.size.width += 2;
338
339   // If the link text is too long, clamp it.
340   int maxWidth = NSWidth([[self bubble] frame]) - 2 * NSMinX(referenceFrame);
341   if (NSWidth(buttonFrame) > maxWidth)
342     buttonFrame.size.width = maxWidth;
343
344   [button setFrame:buttonFrame];
345   [button setTarget:self];
346   [button setAction:@selector(popupLinkClicked:)];
347   return button;
348 }
349
350 - (void)initializeBlockedPluginsList {
351   NSMutableArray* pluginArray = [NSMutableArray array];
352   const std::set<std::string>& plugins =
353       contentSettingBubbleModel_->bubble_content().resource_identifiers;
354   if (plugins.empty()) {
355     int delta = NSMinY([titleLabel_ frame]) -
356                 NSMinY([blockedResourcesField_ frame]);
357     [blockedResourcesField_ removeFromSuperview];
358     NSRect frame = [[self window] frame];
359     frame.size.height -= delta;
360     [[self window] setFrame:frame display:NO];
361   } else {
362     PluginFinder* finder = PluginFinder::GetInstance();
363     for (std::set<std::string>::iterator it = plugins.begin();
364          it != plugins.end(); ++it) {
365       NSString* name =
366           SysUTF16ToNSString(finder->FindPluginNameWithIdentifier(*it));
367       [pluginArray addObject:name];
368     }
369     [blockedResourcesField_
370         setStringValue:[pluginArray componentsJoinedByString:@"\n"]];
371     [GTMUILocalizerAndLayoutTweaker
372         sizeToFitFixedWidthTextField:blockedResourcesField_];
373   }
374 }
375
376 - (void)initializePopupList {
377   // I didn't put the buttons into a NSMatrix because then they are only one
378   // entity in the key view loop. This way, one can tab through all of them.
379   const ContentSettingBubbleModel::PopupItems& popupItems =
380       contentSettingBubbleModel_->bubble_content().popup_items;
381
382   // Get the pre-resize frame of the radio group. Its origin is where the
383   // popup list should go.
384   NSRect radioFrame = [allowBlockRadioGroup_ frame];
385
386   // Make room for the popup list. The bubble view and its subviews autosize
387   // themselves when the window is enlarged.
388   // Heading and radio box are already 1 * kLinkOuterPadding apart in the nib,
389   // so only 1 * kLinkOuterPadding more is needed.
390   int delta = popupItems.size() * kLinkLineHeight - kLinkPadding +
391               kLinkOuterPadding;
392   NSSize deltaSize = NSMakeSize(0, delta);
393   deltaSize = [[[self window] contentView] convertSize:deltaSize toView:nil];
394   NSRect windowFrame = [[self window] frame];
395   windowFrame.size.height += deltaSize.height;
396   [[self window] setFrame:windowFrame display:NO];
397
398   // Create popup list.
399   int topLinkY = NSMaxY(radioFrame) + delta - kLinkHeight;
400   int row = 0;
401   for (std::vector<ContentSettingBubbleModel::PopupItem>::const_iterator
402        it(popupItems.begin()); it != popupItems.end(); ++it, ++row) {
403     NSImage* image = it->image.AsNSImage();
404
405     std::string title(it->title);
406     // The popup may not have committed a load yet, in which case it won't
407     // have a URL or title.
408     if (title.empty())
409       title = l10n_util::GetStringUTF8(IDS_TAB_LOADING_TITLE);
410
411     NSRect linkFrame =
412         NSMakeRect(NSMinX(radioFrame), topLinkY - kLinkLineHeight * row,
413                    200, kLinkHeight);
414     NSButton* button = [self
415         hyperlinkButtonWithFrame:linkFrame
416                            title:base::SysUTF8ToNSString(title)
417                             icon:image
418                   referenceFrame:radioFrame];
419     [[self bubble] addSubview:button];
420     popupLinks_[button] = row;
421   }
422 }
423
424 - (void)initializeGeoLists {
425   // Cocoa has its origin in the lower left corner. This means elements are
426   // added from bottom to top, which explains why loops run backwards and the
427   // order of operations is the other way than on Linux/Windows.
428   const ContentSettingBubbleModel::BubbleContent& content =
429       contentSettingBubbleModel_->bubble_content();
430   NSRect containerFrame = [contentsContainer_ frame];
431   NSRect frame = NSMakeRect(0, 0, NSWidth(containerFrame), kGeoLabelHeight);
432
433   // "Clear" button / text field.
434   if (!content.custom_link.empty()) {
435     base::scoped_nsobject<NSControl> control;
436     if(content.custom_link_enabled) {
437       NSRect buttonFrame = NSMakeRect(0, 0,
438                                       NSWidth(containerFrame),
439                                       kGeoClearButtonHeight);
440       NSButton* button = [[NSButton alloc] initWithFrame:buttonFrame];
441       control.reset(button);
442       [button setTitle:base::SysUTF8ToNSString(content.custom_link)];
443       [button setTarget:self];
444       [button setAction:@selector(clearGeolocationForCurrentHost:)];
445       [button setBezelStyle:NSRoundRectBezelStyle];
446       SetControlSize(button, NSSmallControlSize);
447       [button sizeToFit];
448     } else {
449       // Add the notification that settings will be cleared on next reload.
450       control.reset([LabelWithFrame(
451           base::SysUTF8ToNSString(content.custom_link), frame) retain]);
452       SetControlSize(control.get(), NSSmallControlSize);
453     }
454
455     // If the new control is wider than the container, widen the window.
456     CGFloat controlWidth = NSWidth([control frame]);
457     if (controlWidth > NSWidth(containerFrame)) {
458       NSRect windowFrame = [[self window] frame];
459       windowFrame.size.width += controlWidth - NSWidth(containerFrame);
460       [[self window] setFrame:windowFrame display:NO];
461       // Fetch the updated sizes.
462       containerFrame = [contentsContainer_ frame];
463       frame = NSMakeRect(0, 0, NSWidth(containerFrame), kGeoLabelHeight);
464     }
465
466     DCHECK(control);
467     [contentsContainer_ addSubview:control];
468     frame.origin.y = NSMaxY([control frame]) + kGeoPadding;
469   }
470
471   for (auto i = content.domain_lists.rbegin();
472        i != content.domain_lists.rend(); ++i) {
473     // Add all hosts in the current domain list.
474     for (auto j = i->hosts.rbegin(); j != i->hosts.rend(); ++j) {
475       NSTextField* title = LabelWithFrame(base::SysUTF8ToNSString(*j), frame);
476       SetControlSize(title, NSSmallControlSize);
477       [contentsContainer_ addSubview:title];
478
479       frame.origin.y = NSMaxY(frame) + kGeoHostPadding +
480           [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:title];
481     }
482     if (!i->hosts.empty())
483       frame.origin.y += kGeoPadding - kGeoHostPadding;
484
485     // Add the domain list's title.
486     NSTextField* title =
487         LabelWithFrame(base::SysUTF8ToNSString(i->title), frame);
488     SetControlSize(title, NSSmallControlSize);
489     [contentsContainer_ addSubview:title];
490
491     frame.origin.y = NSMaxY(frame) + kGeoPadding +
492         [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:title];
493   }
494
495   CGFloat containerHeight = frame.origin.y;
496   // Undo last padding.
497   if (!content.domain_lists.empty())
498     containerHeight -= kGeoPadding;
499
500   // Resize container to fit its subviews, and window to fit the container.
501   NSRect windowFrame = [[self window] frame];
502   windowFrame.size.height += containerHeight - NSHeight(containerFrame);
503   [[self window] setFrame:windowFrame display:NO];
504   containerFrame.size.height = containerHeight;
505   [contentsContainer_ setFrame:containerFrame];
506 }
507
508 - (void)initializeMediaMenus {
509   const ContentSettingBubbleModel::MediaMenuMap& media_menus =
510       contentSettingBubbleModel_->bubble_content().media_menus;
511
512   // Calculate the longest width of the labels and menus menus to avoid
513   // truncation by the window's edge.
514   CGFloat maxLabelWidth = 0;
515   CGFloat maxMenuWidth = 0;
516   CGFloat maxMenuHeight = 0;
517   NSRect radioFrame = [allowBlockRadioGroup_ frame];
518   for (ContentSettingBubbleModel::MediaMenuMap::const_iterator it(
519        media_menus.begin()); it != media_menus.end(); ++it) {
520     // |labelFrame| will be resized later on in this function.
521     NSRect labelFrame = NSMakeRect(NSMinX(radioFrame), 0, 0, 0);
522     NSTextField* label =
523         LabelWithFrame(base::SysUTF8ToNSString(it->second.label), labelFrame);
524     SetControlSize(label, NSSmallControlSize);
525     NSCell* cell = [label cell];
526     [cell setAlignment:NSRightTextAlignment];
527     [GTMUILocalizerAndLayoutTweaker sizeToFitView:label];
528     maxLabelWidth = std::max(maxLabelWidth, [label frame].size.width);
529     [[self bubble] addSubview:label];
530
531     // |buttonFrame| will be resized and repositioned later on.
532     NSRect buttonFrame = NSMakeRect(NSMinX(radioFrame), 0, 0, 0);
533     base::scoped_nsobject<NSPopUpButton> button(
534         [[NSPopUpButton alloc] initWithFrame:buttonFrame]);
535     [button setTarget:self];
536
537     // Store the |label| and |button| into MediaMenuParts struct and build
538     // the popup menu from the menu model.
539     content_setting_bubble::MediaMenuParts* menuParts =
540         new content_setting_bubble::MediaMenuParts(it->first, label);
541     menuParts->model.reset(new ContentSettingMediaMenuModel(
542         it->first, contentSettingBubbleModel_.get(),
543         ContentSettingMediaMenuModel::MenuLabelChangedCallback()));
544     mediaMenus_[button] = menuParts;
545     CGFloat width = BuildPopUpMenuFromModel(button,
546                                             menuParts->model.get(),
547                                             it->second.selected_device.name,
548                                             it->second.disabled);
549     maxMenuWidth = std::max(maxMenuWidth, width);
550
551     [[self bubble] addSubview:button
552                    positioned:NSWindowBelow
553                    relativeTo:nil];
554
555     maxMenuHeight = std::max(maxMenuHeight, [button frame].size.height);
556   }
557
558   // Make room for the media menu(s) and enlarge the windows to fit the views.
559   // The bubble view and its subviews autosize themselves when the window is
560   // enlarged.
561   int delta = media_menus.size() * maxMenuHeight +
562       (media_menus.size() - 1) * kMediaMenuElementVerticalPadding;
563   NSSize deltaSize = NSMakeSize(0, delta);
564   deltaSize = [[[self window] contentView] convertSize:deltaSize toView:nil];
565   NSRect windowFrame = [[self window] frame];
566   windowFrame.size.height += deltaSize.height;
567   // If the media menus are wider than the window, widen the window.
568   CGFloat widthNeeded = maxLabelWidth + maxMenuWidth + 2 * NSMinX(radioFrame);
569   if (widthNeeded > windowFrame.size.width)
570     windowFrame.size.width = widthNeeded;
571   [[self window] setFrame:windowFrame display:NO];
572
573   // The radio group lies above the media menus, move the radio group up.
574   radioFrame.origin.y += delta;
575   [allowBlockRadioGroup_ setFrame:radioFrame];
576
577   // Resize and reposition the media menus layout.
578   CGFloat topMenuY = NSMinY(radioFrame) - kMediaMenuVerticalPadding;
579   maxMenuWidth = std::max(maxMenuWidth, kMinMediaMenuButtonWidth);
580   for (content_setting_bubble::MediaMenuPartsMap::const_iterator i =
581        mediaMenus_.begin(); i != mediaMenus_.end(); ++i) {
582     NSRect labelFrame = [i->second->label frame];
583     // Align the label text with the button text.
584     labelFrame.origin.y =
585         topMenuY + (maxMenuHeight - labelFrame.size.height) / 2 + 1;
586     labelFrame.size.width = maxLabelWidth;
587     [i->second->label setFrame:labelFrame];
588     NSRect menuFrame = [i->first frame];
589     menuFrame.origin.y = topMenuY;
590     menuFrame.origin.x = NSMinX(radioFrame) + maxLabelWidth;
591     menuFrame.size.width = maxMenuWidth;
592     menuFrame.size.height = maxMenuHeight;
593     [i->first setFrame:menuFrame];
594     topMenuY -= (maxMenuHeight + kMediaMenuElementVerticalPadding);
595   }
596 }
597
598 - (void)initializeMIDISysExLists {
599   const ContentSettingBubbleModel::BubbleContent& content =
600       contentSettingBubbleModel_->bubble_content();
601   NSRect containerFrame = [contentsContainer_ frame];
602   NSRect frame =
603       NSMakeRect(0, 0, NSWidth(containerFrame), kMIDISysExLabelHeight);
604
605   // "Clear" button / text field.
606   if (!content.custom_link.empty()) {
607     base::scoped_nsobject<NSControl> control;
608     if (content.custom_link_enabled) {
609       NSRect buttonFrame = NSMakeRect(0, 0,
610                                       NSWidth(containerFrame),
611                                       kMIDISysExClearButtonHeight);
612       NSButton* button = [[NSButton alloc] initWithFrame:buttonFrame];
613       control.reset(button);
614       [button setTitle:base::SysUTF8ToNSString(content.custom_link)];
615       [button setTarget:self];
616       [button setAction:@selector(clearMIDISysExForCurrentHost:)];
617       [button setBezelStyle:NSRoundRectBezelStyle];
618       SetControlSize(button, NSSmallControlSize);
619       [button sizeToFit];
620     } else {
621       // Add the notification that settings will be cleared on next reload.
622       control.reset([LabelWithFrame(
623           base::SysUTF8ToNSString(content.custom_link), frame) retain]);
624       SetControlSize(control.get(), NSSmallControlSize);
625     }
626
627     // If the new control is wider than the container, widen the window.
628     CGFloat controlWidth = NSWidth([control frame]);
629     if (controlWidth > NSWidth(containerFrame)) {
630       NSRect windowFrame = [[self window] frame];
631       windowFrame.size.width += controlWidth - NSWidth(containerFrame);
632       [[self window] setFrame:windowFrame display:NO];
633       // Fetch the updated sizes.
634       containerFrame = [contentsContainer_ frame];
635       frame = NSMakeRect(0, 0, NSWidth(containerFrame), kMIDISysExLabelHeight);
636     }
637
638     DCHECK(control);
639     [contentsContainer_ addSubview:control];
640     frame.origin.y = NSMaxY([control frame]) + kMIDISysExPadding;
641   }
642
643   for (auto i = content.domain_lists.rbegin();
644        i != content.domain_lists.rend(); ++i) {
645     // Add all hosts in the current domain list.
646     for (auto j = i->hosts.rbegin(); j != i->hosts.rend(); ++j) {
647       NSTextField* title = LabelWithFrame(base::SysUTF8ToNSString(*j), frame);
648       SetControlSize(title, NSSmallControlSize);
649       [contentsContainer_ addSubview:title];
650
651       frame.origin.y = NSMaxY(frame) + kMIDISysExHostPadding +
652           [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:title];
653     }
654     if (!i->hosts.empty())
655       frame.origin.y += kMIDISysExPadding - kMIDISysExHostPadding;
656
657     // Add the domain list's title.
658     NSTextField* title =
659         LabelWithFrame(base::SysUTF8ToNSString(i->title), frame);
660     SetControlSize(title, NSSmallControlSize);
661     [contentsContainer_ addSubview:title];
662
663     frame.origin.y = NSMaxY(frame) + kMIDISysExPadding +
664         [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:title];
665   }
666
667   CGFloat containerHeight = frame.origin.y;
668   // Undo last padding.
669   if (!content.domain_lists.empty())
670     containerHeight -= kMIDISysExPadding;
671
672   // Resize container to fit its subviews, and window to fit the container.
673   NSRect windowFrame = [[self window] frame];
674   windowFrame.size.height += containerHeight - NSHeight(containerFrame);
675   [[self window] setFrame:windowFrame display:NO];
676   containerFrame.size.height = containerHeight;
677   [contentsContainer_ setFrame:containerFrame];
678 }
679
680 - (void)sizeToFitLoadButton {
681   const ContentSettingBubbleModel::BubbleContent& content =
682       contentSettingBubbleModel_->bubble_content();
683   [loadButton_ setEnabled:content.custom_link_enabled];
684
685   // Resize horizontally to fit button if necessary.
686   NSRect windowFrame = [[self window] frame];
687   int widthNeeded = NSWidth([loadButton_ frame]) +
688       2 * NSMinX([loadButton_ frame]);
689   if (NSWidth(windowFrame) < widthNeeded) {
690     windowFrame.size.width = widthNeeded;
691     [[self window] setFrame:windowFrame display:NO];
692   }
693 }
694
695 - (void)initManageDoneButtons {
696   const ContentSettingBubbleModel::BubbleContent& content =
697       contentSettingBubbleModel_->bubble_content();
698   [manageButton_ setTitle:base::SysUTF8ToNSString(content.manage_link)];
699   [GTMUILocalizerAndLayoutTweaker sizeToFitView:manageButton_];
700
701   CGFloat actualWidth = NSWidth([[[self window] contentView] frame]);
702   CGFloat requiredWidth = NSMaxX([manageButton_ frame]) + kManageDonePadding +
703       NSWidth([[doneButton_ superview] frame]) - NSMinX([doneButton_ frame]);
704   if (requiredWidth <= actualWidth || !doneButton_ || !manageButton_)
705     return;
706
707   // Resize window, autoresizing takes care of the rest.
708   NSSize size = NSMakeSize(requiredWidth - actualWidth, 0);
709   size = [[[self window] contentView] convertSize:size toView:nil];
710   NSRect frame = [[self window] frame];
711   frame.origin.x -= size.width;
712   frame.size.width += size.width;
713   [[self window] setFrame:frame display:NO];
714 }
715
716 - (void)awakeFromNib {
717   [super awakeFromNib];
718
719   [[self bubble] setArrowLocation:info_bubble::kTopRight];
720
721   // Adapt window size to bottom buttons. Do this before all other layouting.
722   [self initManageDoneButtons];
723
724   [self initializeTitle];
725
726   ContentSettingsType type = contentSettingBubbleModel_->content_type();
727   if (type == CONTENT_SETTINGS_TYPE_PLUGINS) {
728     [self sizeToFitLoadButton];
729     [self initializeBlockedPluginsList];
730   }
731
732   if (allowBlockRadioGroup_)  // not bound in cookie bubble xib
733     [self initializeRadioGroup];
734
735   if (type == CONTENT_SETTINGS_TYPE_POPUPS)
736     [self initializePopupList];
737   if (type == CONTENT_SETTINGS_TYPE_GEOLOCATION)
738     [self initializeGeoLists];
739   if (type == CONTENT_SETTINGS_TYPE_MEDIASTREAM)
740     [self initializeMediaMenus];
741   if (type == CONTENT_SETTINGS_TYPE_MIDI_SYSEX)
742     [self initializeMIDISysExLists];
743 }
744
745 ///////////////////////////////////////////////////////////////////////////////
746 // Actual application logic
747
748 - (IBAction)allowBlockToggled:(id)sender {
749   NSButtonCell *selectedCell = [sender selectedCell];
750   contentSettingBubbleModel_->OnRadioClicked([selectedCell tag] - 1);
751 }
752
753 - (void)popupLinkClicked:(id)sender {
754   content_setting_bubble::PopupLinks::iterator i(popupLinks_.find(sender));
755   DCHECK(i != popupLinks_.end());
756   contentSettingBubbleModel_->OnPopupClicked(i->second);
757 }
758
759 - (void)clearGeolocationForCurrentHost:(id)sender {
760   contentSettingBubbleModel_->OnCustomLinkClicked();
761   [self close];
762 }
763
764 - (void)clearMIDISysExForCurrentHost:(id)sender {
765   contentSettingBubbleModel_->OnCustomLinkClicked();
766   [self close];
767 }
768
769 - (IBAction)showMoreInfo:(id)sender {
770   contentSettingBubbleModel_->OnCustomLinkClicked();
771   [self close];
772 }
773
774 - (IBAction)load:(id)sender {
775   contentSettingBubbleModel_->OnCustomLinkClicked();
776   [self close];
777 }
778
779 - (IBAction)learnMoreLinkClicked:(id)sender {
780   contentSettingBubbleModel_->OnManageLinkClicked();
781 }
782
783 - (IBAction)manageBlocking:(id)sender {
784   contentSettingBubbleModel_->OnManageLinkClicked();
785 }
786
787 - (IBAction)saveBubble:(id)sender {
788   contentSettingBubbleModel_->OnSaveClicked();
789   [self close];
790 }
791
792 - (IBAction)blacklistBubble:(id)sender {
793   contentSettingBubbleModel_->OnCancelClicked();
794   [self close];
795 }
796
797 - (IBAction)closeBubble:(id)sender {
798   contentSettingBubbleModel_->OnDoneClicked();
799   [self close];
800 }
801
802 - (IBAction)mediaMenuChanged:(id)sender {
803   NSPopUpButton* button = static_cast<NSPopUpButton*>(sender);
804   content_setting_bubble::MediaMenuPartsMap::const_iterator it(
805       mediaMenus_.find(sender));
806   DCHECK(it != mediaMenus_.end());
807   NSInteger index = [[button selectedItem] tag];
808
809   SetTitleForPopUpButton(
810       button, base::SysUTF16ToNSString(it->second->model->GetLabelAt(index)));
811
812   it->second->model->ExecuteCommand(index, 0);
813 }
814
815 - (content_setting_bubble::MediaMenuPartsMap*)mediaMenus {
816   return &mediaMenus_;
817 }
818
819 @end  // ContentSettingBubbleController