Add ICU message format support
[chromium-blink-merge.git] / chrome / browser / ui / cocoa / content_settings / content_setting_bubble_cocoa.mm
blob8dcbb663a58994223350e765661bc81a166f4dc3
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 - (id)initWithModel:(ContentSettingBubbleModel*)contentSettingBubbleModel
240         webContents:(content::WebContents*)webContents
241        parentWindow:(NSWindow*)parentWindow
242          anchoredAt:(NSPoint)anchoredAt {
243   // This method takes ownership of |contentSettingBubbleModel| in all cases.
244   scoped_ptr<ContentSettingBubbleModel> model(contentSettingBubbleModel);
245   DCHECK(model.get());
246   observerBridge_.reset(
247     new ContentSettingBubbleWebContentsObserverBridge(webContents, self));
249   ContentSettingsType settingsType = model->content_type();
250   NSString* nibPath = @"";
251   switch (settingsType) {
252     case CONTENT_SETTINGS_TYPE_COOKIES:
253       nibPath = @"ContentBlockedCookies"; break;
254     case CONTENT_SETTINGS_TYPE_IMAGES:
255     case CONTENT_SETTINGS_TYPE_JAVASCRIPT:
256     case CONTENT_SETTINGS_TYPE_PPAPI_BROKER:
257       nibPath = @"ContentBlockedSimple"; break;
258     case CONTENT_SETTINGS_TYPE_PLUGINS:
259       nibPath = @"ContentBlockedPlugins"; break;
260     case CONTENT_SETTINGS_TYPE_POPUPS:
261       nibPath = @"ContentBlockedPopups"; break;
262     case CONTENT_SETTINGS_TYPE_GEOLOCATION:
263       nibPath = @"ContentBlockedGeolocation"; break;
264     case CONTENT_SETTINGS_TYPE_MIXEDSCRIPT:
265       nibPath = @"ContentBlockedMixedScript"; break;
266     case CONTENT_SETTINGS_TYPE_PROTOCOL_HANDLERS:
267       nibPath = @"ContentProtocolHandlers"; break;
268     case CONTENT_SETTINGS_TYPE_MEDIASTREAM:
269       nibPath = @"ContentBlockedMedia"; break;
270     case CONTENT_SETTINGS_TYPE_AUTOMATIC_DOWNLOADS:
271       nibPath = @"ContentBlockedDownloads"; break;
272     case CONTENT_SETTINGS_TYPE_MIDI_SYSEX:
273       nibPath = @"ContentBlockedMIDISysEx"; break;
274     // These content types have no bubble:
275     case CONTENT_SETTINGS_TYPE_DEFAULT:
276     case CONTENT_SETTINGS_TYPE_NOTIFICATIONS:
277     case CONTENT_SETTINGS_TYPE_AUTO_SELECT_CERTIFICATE:
278     case CONTENT_SETTINGS_TYPE_FULLSCREEN:
279     case CONTENT_SETTINGS_TYPE_MOUSELOCK:
280     case CONTENT_SETTINGS_TYPE_MEDIASTREAM_MIC:
281     case CONTENT_SETTINGS_TYPE_MEDIASTREAM_CAMERA:
282     case CONTENT_SETTINGS_NUM_TYPES:
283     // TODO(miguelg): Remove this nib content settings support
284     // is implemented
285     case CONTENT_SETTINGS_TYPE_PUSH_MESSAGING:
286     case CONTENT_SETTINGS_TYPE_SSL_CERT_DECISIONS:
287     case CONTENT_SETTINGS_TYPE_APP_BANNER:
288     case CONTENT_SETTINGS_TYPE_SITE_ENGAGEMENT:
289     case CONTENT_SETTINGS_TYPE_DURABLE_STORAGE:
290       NOTREACHED();
291   }
292   if ((self = [super initWithWindowNibPath:nibPath
293                               parentWindow:parentWindow
294                                 anchoredAt:anchoredAt])) {
295     contentSettingBubbleModel_.reset(model.release());
296     [self showWindow:nil];
297   }
298   return self;
301 - (void)dealloc {
302   STLDeleteValues(&mediaMenus_);
303   [super dealloc];
306 - (void)initializeTitle {
307   if (!titleLabel_)
308     return;
310   NSString* label = base::SysUTF8ToNSString(
311       contentSettingBubbleModel_->bubble_content().title);
312   [titleLabel_ setStringValue:label];
314   // Layout title post-localization.
315   CGFloat deltaY = [GTMUILocalizerAndLayoutTweaker
316       sizeToFitFixedWidthTextField:titleLabel_];
317   NSRect windowFrame = [[self window] frame];
318   windowFrame.size.height += deltaY;
319   [[self window] setFrame:windowFrame display:NO];
320   NSRect titleFrame = [titleLabel_ frame];
321   titleFrame.origin.y -= deltaY;
322   [titleLabel_ setFrame:titleFrame];
325 - (void)initializeRadioGroup {
326   // NOTE! Tags in the xib files must match the order of the radio buttons
327   // passed in the radio_group and be 1-based, not 0-based.
328   const ContentSettingBubbleModel::RadioGroup& radio_group =
329       contentSettingBubbleModel_->bubble_content().radio_group;
331   // Xcode 5.1 Interface Builder doesn't allow a font property to be set for
332   // NSMatrix. The implementation of GTMUILocalizerAndLayoutTweaker assumes that
333   // the font for each of the cells in a NSMatrix is identical, and is the font
334   // of the NSMatrix. This logic sets the font of NSMatrix to be that of its
335   // cells.
336   NSFont* font = nil;
337   for (NSCell* cell in [allowBlockRadioGroup_ cells]) {
338     if (!font)
339       font = [cell font];
340     DCHECK([font isEqual:[cell font]]);
341   }
342   [allowBlockRadioGroup_ setFont:font];
344   // Select appropriate radio button.
345   [allowBlockRadioGroup_ selectCellWithTag: radio_group.default_item + 1];
347   const ContentSettingBubbleModel::RadioItems& radio_items =
348       radio_group.radio_items;
349   for (size_t ii = 0; ii < radio_group.radio_items.size(); ++ii) {
350     NSCell* radioCell = [allowBlockRadioGroup_ cellWithTag: ii + 1];
351     [radioCell setTitle:base::SysUTF8ToNSString(radio_items[ii])];
352   }
354   // Layout radio group labels post-localization.
355   [GTMUILocalizerAndLayoutTweaker
356       wrapRadioGroupForWidth:allowBlockRadioGroup_];
357   CGFloat radioDeltaY = [GTMUILocalizerAndLayoutTweaker
358       sizeToFitView:allowBlockRadioGroup_].height;
359   NSRect windowFrame = [[self window] frame];
360   windowFrame.size.height += radioDeltaY;
361   [[self window] setFrame:windowFrame display:NO];
364 - (NSButton*)hyperlinkButtonWithFrame:(NSRect)frame
365                                 title:(NSString*)title
366                                  icon:(NSImage*)icon
367                        referenceFrame:(NSRect)referenceFrame {
368   base::scoped_nsobject<HyperlinkButtonCell> cell(
369       [[HyperlinkButtonCell alloc] initTextCell:title]);
370   [cell.get() setAlignment:NSNaturalTextAlignment];
371   if (icon) {
372     [cell.get() setImagePosition:NSImageLeft];
373     [cell.get() setImage:icon];
374   } else {
375     [cell.get() setImagePosition:NSNoImage];
376   }
377   [cell.get() setControlSize:NSSmallControlSize];
379   NSButton* button = [[[NSButton alloc] initWithFrame:frame] autorelease];
380   // Cell must be set immediately after construction.
381   [button setCell:cell.get()];
383   // Size to fit the button and add a little extra padding for the small-text
384   // hyperlink button, which sizeToFit gets wrong.
385   [GTMUILocalizerAndLayoutTweaker sizeToFitView:button];
386   NSRect buttonFrame = [button frame];
387   buttonFrame.size.width += 2;
389   // If the link text is too long, clamp it.
390   int maxWidth = NSWidth([[self bubble] frame]) - 2 * NSMinX(referenceFrame);
391   if (NSWidth(buttonFrame) > maxWidth)
392     buttonFrame.size.width = maxWidth;
394   [button setFrame:buttonFrame];
395   [button setTarget:self];
396   [button setAction:@selector(popupLinkClicked:)];
397   return button;
400 - (void)initializeBlockedPluginsList {
401   // Hide the empty label at the top of the dialog.
402   int delta =
403       NSMinY([titleLabel_ frame]) - NSMinY([blockedResourcesField_ frame]);
404   [blockedResourcesField_ removeFromSuperview];
405   NSRect frame = [[self window] frame];
406   frame.size.height -= delta;
407   [[self window] setFrame:frame display:NO];
410 - (void)initializeItemList {
411   // I didn't put the buttons into a NSMatrix because then they are only one
412   // entity in the key view loop. This way, one can tab through all of them.
413   const ContentSettingBubbleModel::ListItems& listItems =
414       contentSettingBubbleModel_->bubble_content().list_items;
416   // Get the pre-resize frame of the radio group. Its origin is where the
417   // popup list should go.
418   NSRect radioFrame = [allowBlockRadioGroup_ frame];
420   // Make room for the popup list. The bubble view and its subviews autosize
421   // themselves when the window is enlarged.
422   // Heading and radio box are already 1 * kLinkOuterPadding apart in the nib,
423   // so only 1 * kLinkOuterPadding more is needed.
424   int delta =
425       listItems.size() * kLinkLineHeight - kLinkPadding + kLinkOuterPadding;
426   NSSize deltaSize = NSMakeSize(0, delta);
427   deltaSize = [[[self window] contentView] convertSize:deltaSize toView:nil];
428   NSRect windowFrame = [[self window] frame];
429   windowFrame.size.height += deltaSize.height;
430   [[self window] setFrame:windowFrame display:NO];
432   // Create item list.
433   int topLinkY = NSMaxY(radioFrame) + delta - kLinkHeight;
434   int row = 0;
435   for (const ContentSettingBubbleModel::ListItem& listItem : listItems) {
436     NSImage* image = listItem.image.AsNSImage();
437     NSRect frame = NSMakeRect(
438         NSMinX(radioFrame), topLinkY - kLinkLineHeight * row, 200, kLinkHeight);
439     if (listItem.has_link) {
440       NSButton* button =
441           [self hyperlinkButtonWithFrame:frame
442                                    title:base::SysUTF8ToNSString(listItem.title)
443                                     icon:image
444                           referenceFrame:radioFrame];
445       [[self bubble] addSubview:button];
446       popupLinks_[button] = row++;
447     } else {
448       NSTextField* label =
449           LabelWithFrame(base::SysUTF8ToNSString(listItem.title), frame);
450       SetControlSize(label, NSSmallControlSize);
451       [[self bubble] addSubview:label];
452       row++;
453     }
454   }
457 - (void)initializeGeoLists {
458   // Cocoa has its origin in the lower left corner. This means elements are
459   // added from bottom to top, which explains why loops run backwards and the
460   // order of operations is the other way than on Linux/Windows.
461   const ContentSettingBubbleModel::BubbleContent& content =
462       contentSettingBubbleModel_->bubble_content();
463   NSRect containerFrame = [contentsContainer_ frame];
464   NSRect frame = NSMakeRect(0, 0, NSWidth(containerFrame), kGeoLabelHeight);
466   // "Clear" button / text field.
467   if (!content.custom_link.empty()) {
468     base::scoped_nsobject<NSControl> control;
469     if(content.custom_link_enabled) {
470       NSRect buttonFrame = NSMakeRect(0, 0,
471                                       NSWidth(containerFrame),
472                                       kGeoClearButtonHeight);
473       NSButton* button = [[NSButton alloc] initWithFrame:buttonFrame];
474       control.reset(button);
475       [button setTitle:base::SysUTF8ToNSString(content.custom_link)];
476       [button setTarget:self];
477       [button setAction:@selector(clearGeolocationForCurrentHost:)];
478       [button setBezelStyle:NSRoundRectBezelStyle];
479       SetControlSize(button, NSSmallControlSize);
480       [button sizeToFit];
481     } else {
482       // Add the notification that settings will be cleared on next reload.
483       control.reset([LabelWithFrame(
484           base::SysUTF8ToNSString(content.custom_link), frame) retain]);
485       SetControlSize(control.get(), NSSmallControlSize);
486     }
488     // If the new control is wider than the container, widen the window.
489     CGFloat controlWidth = NSWidth([control frame]);
490     if (controlWidth > NSWidth(containerFrame)) {
491       NSRect windowFrame = [[self window] frame];
492       windowFrame.size.width += controlWidth - NSWidth(containerFrame);
493       [[self window] setFrame:windowFrame display:NO];
494       // Fetch the updated sizes.
495       containerFrame = [contentsContainer_ frame];
496       frame = NSMakeRect(0, 0, NSWidth(containerFrame), kGeoLabelHeight);
497     }
499     DCHECK(control);
500     [contentsContainer_ addSubview:control];
501     frame.origin.y = NSMaxY([control frame]) + kGeoPadding;
502   }
504   for (auto i = content.domain_lists.rbegin();
505        i != content.domain_lists.rend(); ++i) {
506     // Add all hosts in the current domain list.
507     for (auto j = i->hosts.rbegin(); j != i->hosts.rend(); ++j) {
508       NSTextField* title = LabelWithFrame(base::SysUTF8ToNSString(*j), frame);
509       SetControlSize(title, NSSmallControlSize);
510       [contentsContainer_ addSubview:title];
512       frame.origin.y = NSMaxY(frame) + kGeoHostPadding +
513           [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:title];
514     }
515     if (!i->hosts.empty())
516       frame.origin.y += kGeoPadding - kGeoHostPadding;
518     // Add the domain list's title.
519     NSTextField* title =
520         LabelWithFrame(base::SysUTF8ToNSString(i->title), frame);
521     SetControlSize(title, NSSmallControlSize);
522     [contentsContainer_ addSubview:title];
524     frame.origin.y = NSMaxY(frame) + kGeoPadding +
525         [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:title];
526   }
528   CGFloat containerHeight = frame.origin.y;
529   // Undo last padding.
530   if (!content.domain_lists.empty())
531     containerHeight -= kGeoPadding;
533   // Resize container to fit its subviews, and window to fit the container.
534   NSRect windowFrame = [[self window] frame];
535   windowFrame.size.height += containerHeight - NSHeight(containerFrame);
536   [[self window] setFrame:windowFrame display:NO];
537   containerFrame.size.height = containerHeight;
538   [contentsContainer_ setFrame:containerFrame];
541 - (void)initializeMediaMenus {
542   const ContentSettingBubbleModel::MediaMenuMap& media_menus =
543       contentSettingBubbleModel_->bubble_content().media_menus;
545   // Calculate the longest width of the labels and menus menus to avoid
546   // truncation by the window's edge.
547   CGFloat maxLabelWidth = 0;
548   CGFloat maxMenuWidth = 0;
549   CGFloat maxMenuHeight = 0;
550   NSRect radioFrame = [allowBlockRadioGroup_ frame];
551   for (const std::pair<content::MediaStreamType,
552                        ContentSettingBubbleModel::MediaMenu>& map_entry :
553        media_menus) {
554     // |labelFrame| will be resized later on in this function.
555     NSRect labelFrame = NSMakeRect(NSMinX(radioFrame), 0, 0, 0);
556     NSTextField* label = LabelWithFrame(
557         base::SysUTF8ToNSString(map_entry.second.label), labelFrame);
558     SetControlSize(label, NSSmallControlSize);
559     NSCell* cell = [label cell];
560     [cell setAlignment:NSRightTextAlignment];
561     [GTMUILocalizerAndLayoutTweaker sizeToFitView:label];
562     maxLabelWidth = std::max(maxLabelWidth, [label frame].size.width);
563     [[self bubble] addSubview:label];
565     // |buttonFrame| will be resized and repositioned later on.
566     NSRect buttonFrame = NSMakeRect(NSMinX(radioFrame), 0, 0, 0);
567     base::scoped_nsobject<NSPopUpButton> button(
568         [[NSPopUpButton alloc] initWithFrame:buttonFrame]);
569     [button setTarget:self];
571     // Set the map_entry's key value to |button| tag.
572     // MediaMenuPartsMap uses this value to order its elements.
573     [button setTag:static_cast<NSInteger>(map_entry.first)];
575     // Store the |label| and |button| into MediaMenuParts struct and build
576     // the popup menu from the menu model.
577     content_setting_bubble::MediaMenuParts* menuParts =
578         new content_setting_bubble::MediaMenuParts(map_entry.first, label);
579     menuParts->model.reset(new ContentSettingMediaMenuModel(
580         map_entry.first, contentSettingBubbleModel_.get(),
581         ContentSettingMediaMenuModel::MenuLabelChangedCallback()));
582     mediaMenus_[button] = menuParts;
583     CGFloat width = BuildPopUpMenuFromModel(
584         button, menuParts->model.get(), map_entry.second.selected_device.name,
585         map_entry.second.disabled);
586     maxMenuWidth = std::max(maxMenuWidth, width);
588     [[self bubble] addSubview:button
589                    positioned:NSWindowBelow
590                    relativeTo:nil];
592     maxMenuHeight = std::max(maxMenuHeight, [button frame].size.height);
593   }
595   // Make room for the media menu(s) and enlarge the windows to fit the views.
596   // The bubble view and its subviews autosize themselves when the window is
597   // enlarged.
598   int delta = media_menus.size() * maxMenuHeight +
599       (media_menus.size() - 1) * kMediaMenuElementVerticalPadding;
600   NSSize deltaSize = NSMakeSize(0, delta);
601   deltaSize = [[[self window] contentView] convertSize:deltaSize toView:nil];
602   NSRect windowFrame = [[self window] frame];
603   windowFrame.size.height += deltaSize.height;
604   // If the media menus are wider than the window, widen the window.
605   CGFloat widthNeeded = maxLabelWidth + maxMenuWidth + 2 * NSMinX(radioFrame);
606   if (widthNeeded > windowFrame.size.width)
607     windowFrame.size.width = widthNeeded;
608   [[self window] setFrame:windowFrame display:NO];
610   // The radio group lies above the media menus, move the radio group up.
611   radioFrame.origin.y += delta;
612   [allowBlockRadioGroup_ setFrame:radioFrame];
614   // Resize and reposition the media menus layout.
615   CGFloat topMenuY = NSMinY(radioFrame) - kMediaMenuVerticalPadding;
616   maxMenuWidth = std::max(maxMenuWidth, kMinMediaMenuButtonWidth);
617   for (const std::pair<NSPopUpButton*, content_setting_bubble::MediaMenuParts*>&
618            map_entry : mediaMenus_) {
619     NSRect labelFrame = [map_entry.second->label frame];
620     // Align the label text with the button text.
621     labelFrame.origin.y =
622         topMenuY + (maxMenuHeight - labelFrame.size.height) / 2 + 1;
623     labelFrame.size.width = maxLabelWidth;
624     [map_entry.second->label setFrame:labelFrame];
625     NSRect menuFrame = [map_entry.first frame];
626     menuFrame.origin.y = topMenuY;
627     menuFrame.origin.x = NSMinX(radioFrame) + maxLabelWidth;
628     menuFrame.size.width = maxMenuWidth;
629     menuFrame.size.height = maxMenuHeight;
630     [map_entry.first setFrame:menuFrame];
631     topMenuY -= (maxMenuHeight + kMediaMenuElementVerticalPadding);
632   }
635 - (void)initializeMIDISysExLists {
636   const ContentSettingBubbleModel::BubbleContent& content =
637       contentSettingBubbleModel_->bubble_content();
638   NSRect containerFrame = [contentsContainer_ frame];
639   NSRect frame =
640       NSMakeRect(0, 0, NSWidth(containerFrame), kMIDISysExLabelHeight);
642   // "Clear" button / text field.
643   if (!content.custom_link.empty()) {
644     base::scoped_nsobject<NSControl> control;
645     if (content.custom_link_enabled) {
646       NSRect buttonFrame = NSMakeRect(0, 0,
647                                       NSWidth(containerFrame),
648                                       kMIDISysExClearButtonHeight);
649       NSButton* button = [[NSButton alloc] initWithFrame:buttonFrame];
650       control.reset(button);
651       [button setTitle:base::SysUTF8ToNSString(content.custom_link)];
652       [button setTarget:self];
653       [button setAction:@selector(clearMIDISysExForCurrentHost:)];
654       [button setBezelStyle:NSRoundRectBezelStyle];
655       SetControlSize(button, NSSmallControlSize);
656       [button sizeToFit];
657     } else {
658       // Add the notification that settings will be cleared on next reload.
659       control.reset([LabelWithFrame(
660           base::SysUTF8ToNSString(content.custom_link), frame) retain]);
661       SetControlSize(control.get(), NSSmallControlSize);
662     }
664     // If the new control is wider than the container, widen the window.
665     CGFloat controlWidth = NSWidth([control frame]);
666     if (controlWidth > NSWidth(containerFrame)) {
667       NSRect windowFrame = [[self window] frame];
668       windowFrame.size.width += controlWidth - NSWidth(containerFrame);
669       [[self window] setFrame:windowFrame display:NO];
670       // Fetch the updated sizes.
671       containerFrame = [contentsContainer_ frame];
672       frame = NSMakeRect(0, 0, NSWidth(containerFrame), kMIDISysExLabelHeight);
673     }
675     DCHECK(control);
676     [contentsContainer_ addSubview:control];
677     frame.origin.y = NSMaxY([control frame]) + kMIDISysExPadding;
678   }
680   for (auto i = content.domain_lists.rbegin();
681        i != content.domain_lists.rend(); ++i) {
682     // Add all hosts in the current domain list.
683     for (auto j = i->hosts.rbegin(); j != i->hosts.rend(); ++j) {
684       NSTextField* title = LabelWithFrame(base::SysUTF8ToNSString(*j), frame);
685       SetControlSize(title, NSSmallControlSize);
686       [contentsContainer_ addSubview:title];
688       frame.origin.y = NSMaxY(frame) + kMIDISysExHostPadding +
689           [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:title];
690     }
691     if (!i->hosts.empty())
692       frame.origin.y += kMIDISysExPadding - kMIDISysExHostPadding;
694     // Add the domain list's title.
695     NSTextField* title =
696         LabelWithFrame(base::SysUTF8ToNSString(i->title), frame);
697     SetControlSize(title, NSSmallControlSize);
698     [contentsContainer_ addSubview:title];
700     frame.origin.y = NSMaxY(frame) + kMIDISysExPadding +
701         [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:title];
702   }
704   CGFloat containerHeight = frame.origin.y;
705   // Undo last padding.
706   if (!content.domain_lists.empty())
707     containerHeight -= kMIDISysExPadding;
709   // Resize container to fit its subviews, and window to fit the container.
710   NSRect windowFrame = [[self window] frame];
711   windowFrame.size.height += containerHeight - NSHeight(containerFrame);
712   [[self window] setFrame:windowFrame display:NO];
713   containerFrame.size.height = containerHeight;
714   [contentsContainer_ setFrame:containerFrame];
717 - (void)sizeToFitLoadButton {
718   const ContentSettingBubbleModel::BubbleContent& content =
719       contentSettingBubbleModel_->bubble_content();
720   [loadButton_ setEnabled:content.custom_link_enabled];
722   // Resize horizontally to fit button if necessary.
723   NSRect windowFrame = [[self window] frame];
724   int widthNeeded = NSWidth([loadButton_ frame]) +
725       2 * NSMinX([loadButton_ frame]);
726   if (NSWidth(windowFrame) < widthNeeded) {
727     windowFrame.size.width = widthNeeded;
728     [[self window] setFrame:windowFrame display:NO];
729   }
732 - (void)initManageDoneButtons {
733   const ContentSettingBubbleModel::BubbleContent& content =
734       contentSettingBubbleModel_->bubble_content();
735   [manageButton_ setTitle:base::SysUTF8ToNSString(content.manage_link)];
736   [GTMUILocalizerAndLayoutTweaker sizeToFitView:manageButton_];
738   CGFloat actualWidth = NSWidth([[[self window] contentView] frame]);
739   CGFloat requiredWidth = NSMaxX([manageButton_ frame]) + kManageDonePadding +
740       NSWidth([[doneButton_ superview] frame]) - NSMinX([doneButton_ frame]);
741   if (requiredWidth <= actualWidth || !doneButton_ || !manageButton_)
742     return;
744   // Resize window, autoresizing takes care of the rest.
745   NSSize size = NSMakeSize(requiredWidth - actualWidth, 0);
746   size = [[[self window] contentView] convertSize:size toView:nil];
747   NSRect frame = [[self window] frame];
748   frame.origin.x -= size.width;
749   frame.size.width += size.width;
750   [[self window] setFrame:frame display:NO];
753 - (void)awakeFromNib {
754   [super awakeFromNib];
756   [[self bubble] setArrowLocation:info_bubble::kTopRight];
758   // Adapt window size to bottom buttons. Do this before all other layouting.
759   [self initManageDoneButtons];
761   [self initializeTitle];
763   ContentSettingsType type = contentSettingBubbleModel_->content_type();
764   if (type == CONTENT_SETTINGS_TYPE_PLUGINS) {
765     [self sizeToFitLoadButton];
766     [self initializeBlockedPluginsList];
767   }
769   if (allowBlockRadioGroup_)  // not bound in cookie bubble xib
770     [self initializeRadioGroup];
772   if (type == CONTENT_SETTINGS_TYPE_POPUPS ||
773       type == CONTENT_SETTINGS_TYPE_PLUGINS)
774     [self initializeItemList];
775   if (type == CONTENT_SETTINGS_TYPE_GEOLOCATION)
776     [self initializeGeoLists];
777   if (type == CONTENT_SETTINGS_TYPE_MEDIASTREAM)
778     [self initializeMediaMenus];
779   if (type == CONTENT_SETTINGS_TYPE_MIDI_SYSEX)
780     [self initializeMIDISysExLists];
783 ///////////////////////////////////////////////////////////////////////////////
784 // Actual application logic
786 - (IBAction)allowBlockToggled:(id)sender {
787   NSButtonCell *selectedCell = [sender selectedCell];
788   contentSettingBubbleModel_->OnRadioClicked([selectedCell tag] - 1);
791 - (void)popupLinkClicked:(id)sender {
792   content_setting_bubble::PopupLinks::iterator i(popupLinks_.find(sender));
793   DCHECK(i != popupLinks_.end());
794   contentSettingBubbleModel_->OnListItemClicked(i->second);
797 - (void)clearGeolocationForCurrentHost:(id)sender {
798   contentSettingBubbleModel_->OnCustomLinkClicked();
799   [self close];
802 - (void)clearMIDISysExForCurrentHost:(id)sender {
803   contentSettingBubbleModel_->OnCustomLinkClicked();
804   [self close];
807 - (IBAction)showMoreInfo:(id)sender {
808   contentSettingBubbleModel_->OnCustomLinkClicked();
809   [self close];
812 - (IBAction)load:(id)sender {
813   contentSettingBubbleModel_->OnCustomLinkClicked();
814   [self close];
817 - (IBAction)learnMoreLinkClicked:(id)sender {
818   contentSettingBubbleModel_->OnManageLinkClicked();
821 - (IBAction)manageBlocking:(id)sender {
822   contentSettingBubbleModel_->OnManageLinkClicked();
825 - (IBAction)closeBubble:(id)sender {
826   contentSettingBubbleModel_->OnDoneClicked();
827   [self close];
830 - (IBAction)mediaMenuChanged:(id)sender {
831   NSPopUpButton* button = static_cast<NSPopUpButton*>(sender);
832   content_setting_bubble::MediaMenuPartsMap::const_iterator it(
833       mediaMenus_.find(sender));
834   DCHECK(it != mediaMenus_.end());
835   NSInteger index = [[button selectedItem] tag];
837   SetTitleForPopUpButton(
838       button, base::SysUTF16ToNSString(it->second->model->GetLabelAt(index)));
840   it->second->model->ExecuteCommand(index, 0);
843 - (content_setting_bubble::MediaMenuPartsMap*)mediaMenus {
844   return &mediaMenus_;
847 @end  // ContentSettingBubbleController