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