| // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #import "chrome/browser/ui/cocoa/content_settings/content_setting_bubble_cocoa.h" |
| |
| #include "base/command_line.h" |
| #include "base/logging.h" |
| #include "base/stl_util.h" |
| #include "base/strings/sys_string_conversions.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "chrome/browser/plugins/plugin_finder.h" |
| #include "chrome/browser/plugins/plugin_metadata.h" |
| #import "chrome/browser/ui/cocoa/info_bubble_view.h" |
| #import "chrome/browser/ui/cocoa/l10n_util.h" |
| #include "chrome/browser/ui/content_settings/content_setting_bubble_model.h" |
| #include "chrome/browser/ui/content_settings/content_setting_media_menu_model.h" |
| #include "chrome/grit/generated_resources.h" |
| #include "components/content_settings/core/browser/host_content_settings_map.h" |
| #include "content/public/browser/plugin_service.h" |
| #include "content/public/browser/web_contents_observer.h" |
| #include "skia/ext/skia_utils_mac.h" |
| #import "third_party/google_toolbox_for_mac/src/AppKit/GTMUILocalizerAndLayoutTweaker.h" |
| #import "ui/base/cocoa/controls/hyperlink_button_cell.h" |
| #include "ui/base/l10n/l10n_util.h" |
| |
| using content::PluginService; |
| |
| namespace { |
| |
| // Height of one link in the popup list. |
| const int kLinkHeight = 16; |
| |
| // Space between two popup links. |
| const int kLinkPadding = 4; |
| |
| // Space taken in total by one popup link. |
| const int kLinkLineHeight = kLinkHeight + kLinkPadding; |
| |
| // Space between popup list and surrounding UI elements. |
| const int kLinkOuterPadding = 8; |
| |
| // Height of each of the labels in the geolocation bubble. |
| const int kGeoLabelHeight = 14; |
| |
| // Height of the "Clear" button in the geolocation bubble. |
| const int kGeoClearButtonHeight = 17; |
| |
| // General padding between elements in the geolocation bubble. |
| const int kGeoPadding = 8; |
| |
| // Padding between host names in the geolocation bubble. |
| const int kGeoHostPadding = 4; |
| |
| // Minimal padding between "Manage" and "Done" buttons. |
| const int kManageDonePadding = 8; |
| |
| // Padding between radio buttons and media menus buttons in the media bubble. |
| const int kMediaMenuVerticalPadding = 25; |
| |
| // Padding between media menu elements in the media bubble. |
| const int kMediaMenuElementVerticalPadding = 5; |
| |
| // The amount of horizontal space between the media menu title and the border. |
| const int kMediaMenuTitleHorizontalPadding = 10; |
| |
| // The minimum width of the media menu buttons. |
| const CGFloat kMinMediaMenuButtonWidth = 100; |
| |
| // Height of each of the labels in the MIDI bubble. |
| const int kMIDISysExLabelHeight = 14; |
| |
| // Height of the "Clear" button in the MIDI bubble. |
| const int kMIDISysExClearButtonHeight = 17; |
| |
| // General padding between elements in the MIDI bubble. |
| const int kMIDISysExPadding = 8; |
| |
| // Padding between host names in the MIDI bubble. |
| const int kMIDISysExHostPadding = 4; |
| |
| void SetControlSize(NSControl* control, NSControlSize controlSize) { |
| CGFloat fontSize = [NSFont systemFontSizeForControlSize:controlSize]; |
| NSCell* cell = [control cell]; |
| NSFont* font = [NSFont fontWithName:[[cell font] fontName] size:fontSize]; |
| [cell setFont:font]; |
| [cell setControlSize:controlSize]; |
| } |
| |
| // Returns an autoreleased NSTextField that is configured to look like a Label |
| // looks in Interface Builder. |
| NSTextField* LabelWithFrame(NSString* text, const NSRect& frame) { |
| NSTextField* label = [[NSTextField alloc] initWithFrame:frame]; |
| [label setStringValue:text]; |
| [label setSelectable:NO]; |
| [label setBezeled:NO]; |
| return [label autorelease]; |
| } |
| |
| // Sets the title for the popup button. |
| void SetTitleForPopUpButton(NSPopUpButton* button, NSString* title) { |
| base::scoped_nsobject<NSMenuItem> titleItem([[NSMenuItem alloc] init]); |
| [titleItem setTitle:title]; |
| [[button cell] setUsesItemFromMenu:NO]; |
| [[button cell] setMenuItem:titleItem.get()]; |
| } |
| |
| // Builds the popup button menu from the menu model and returns the width of the |
| // longgest item as the width of the popup menu. |
| CGFloat BuildPopUpMenuFromModel(NSPopUpButton* button, |
| ContentSettingMediaMenuModel* model, |
| const std::string& title, |
| bool disabled) { |
| [[button cell] setControlSize:NSSmallControlSize]; |
| [[button cell] setArrowPosition:NSPopUpArrowAtBottom]; |
| [button setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]]; |
| [button setButtonType:NSMomentaryPushInButton]; |
| [button setAlignment:NSLeftTextAlignment]; |
| [button setAutoresizingMask:NSViewMinXMargin]; |
| [button setAction:@selector(mediaMenuChanged:)]; |
| [button sizeToFit]; |
| |
| CGFloat menuWidth = 0; |
| for (int i = 0; i < model->GetItemCount(); ++i) { |
| NSString* itemTitle = |
| base::SysUTF16ToNSString(model->GetLabelAt(i)); |
| [button addItemWithTitle:itemTitle]; |
| [[button lastItem] setTag:i]; |
| |
| if (UTF16ToUTF8(model->GetLabelAt(i)) == title) |
| [button selectItemWithTag:i]; |
| |
| // Determine the largest possible size for this button. |
| NSDictionary* textAttributes = |
| [NSDictionary dictionaryWithObject:[button font] |
| forKey:NSFontAttributeName]; |
| NSSize size = [itemTitle sizeWithAttributes:textAttributes]; |
| NSRect buttonFrame = [button frame]; |
| NSRect titleRect = [[button cell] titleRectForBounds:buttonFrame]; |
| CGFloat width = size.width + NSWidth(buttonFrame) - NSWidth(titleRect) + |
| kMediaMenuTitleHorizontalPadding; |
| menuWidth = std::max(menuWidth, width); |
| } |
| |
| if (!model->GetItemCount()) { |
| // Show a "None available" title and grey out the menu when there is no |
| // available device. |
| SetTitleForPopUpButton( |
| button, l10n_util::GetNSString(IDS_MEDIA_MENU_NO_DEVICE_TITLE)); |
| [button setEnabled:NO]; |
| } else { |
| SetTitleForPopUpButton(button, base::SysUTF8ToNSString(title)); |
| |
| // Disable the device selection when the website is managing the devices |
| // itself. |
| if (disabled) |
| [button setEnabled:NO]; |
| } |
| |
| return menuWidth; |
| } |
| |
| } // namespace |
| |
| namespace content_setting_bubble { |
| |
| MediaMenuParts::MediaMenuParts(content::MediaStreamType type, |
| NSTextField* label) |
| : type(type), |
| label(label) {} |
| MediaMenuParts::~MediaMenuParts() {} |
| |
| } // namespace content_setting_bubble |
| |
| class ContentSettingBubbleWebContentsObserverBridge |
| : public content::WebContentsObserver { |
| public: |
| ContentSettingBubbleWebContentsObserverBridge( |
| content::WebContents* web_contents, |
| ContentSettingBubbleController* controller) |
| : content::WebContentsObserver(web_contents), |
| controller_(controller) { |
| } |
| |
| protected: |
| // WebContentsObserver: |
| void DidNavigateMainFrame( |
| const content::LoadCommittedDetails& details, |
| const content::FrameNavigateParams& params) override { |
| // Content settings are based on the main frame, so if it switches then |
| // close up shop. |
| [controller_ closeBubble:nil]; |
| } |
| |
| private: |
| ContentSettingBubbleController* controller_; // weak |
| |
| DISALLOW_COPY_AND_ASSIGN(ContentSettingBubbleWebContentsObserverBridge); |
| }; |
| |
| @interface ContentSettingBubbleController(Private) |
| - (id)initWithModel:(ContentSettingBubbleModel*)settingsBubbleModel |
| webContents:(content::WebContents*)webContents |
| parentWindow:(NSWindow*)parentWindow |
| anchoredAt:(NSPoint)anchoredAt; |
| - (NSButton*)hyperlinkButtonWithFrame:(NSRect)frame |
| title:(NSString*)title |
| icon:(NSImage*)icon |
| referenceFrame:(NSRect)referenceFrame; |
| - (void)initializeBlockedPluginsList; |
| - (void)initializeTitle; |
| - (void)initializeRadioGroup; |
| - (void)initializePopupList; |
| - (void)initializeGeoLists; |
| - (void)initializeMediaMenus; |
| - (void)initializeMIDISysExLists; |
| - (void)sizeToFitLoadButton; |
| - (void)initManageDoneButtons; |
| - (void)removeInfoButton; |
| - (void)popupLinkClicked:(id)sender; |
| - (void)clearGeolocationForCurrentHost:(id)sender; |
| - (void)clearMIDISysExForCurrentHost:(id)sender; |
| @end |
| |
| @implementation ContentSettingBubbleController |
| |
| + (ContentSettingBubbleController*) |
| showForModel:(ContentSettingBubbleModel*)contentSettingBubbleModel |
| webContents:(content::WebContents*)webContents |
| parentWindow:(NSWindow*)parentWindow |
| anchoredAt:(NSPoint)anchor { |
| // Autoreleases itself on bubble close. |
| return [[ContentSettingBubbleController alloc] |
| initWithModel:contentSettingBubbleModel |
| webContents:webContents |
| parentWindow:parentWindow |
| anchoredAt:anchor]; |
| } |
| |
| - (id)initWithModel:(ContentSettingBubbleModel*)contentSettingBubbleModel |
| webContents:(content::WebContents*)webContents |
| parentWindow:(NSWindow*)parentWindow |
| anchoredAt:(NSPoint)anchoredAt { |
| // This method takes ownership of |contentSettingBubbleModel| in all cases. |
| scoped_ptr<ContentSettingBubbleModel> model(contentSettingBubbleModel); |
| DCHECK(model.get()); |
| observerBridge_.reset( |
| new ContentSettingBubbleWebContentsObserverBridge(webContents, self)); |
| |
| ContentSettingsType settingsType = model->content_type(); |
| NSString* nibPath = @""; |
| switch (settingsType) { |
| case CONTENT_SETTINGS_TYPE_COOKIES: |
| nibPath = @"ContentBlockedCookies"; break; |
| case CONTENT_SETTINGS_TYPE_IMAGES: |
| case CONTENT_SETTINGS_TYPE_JAVASCRIPT: |
| case CONTENT_SETTINGS_TYPE_PPAPI_BROKER: |
| nibPath = @"ContentBlockedSimple"; break; |
| case CONTENT_SETTINGS_TYPE_PLUGINS: |
| nibPath = @"ContentBlockedPlugins"; break; |
| case CONTENT_SETTINGS_TYPE_POPUPS: |
| nibPath = @"ContentBlockedPopups"; break; |
| case CONTENT_SETTINGS_TYPE_GEOLOCATION: |
| nibPath = @"ContentBlockedGeolocation"; break; |
| case CONTENT_SETTINGS_TYPE_MIXEDSCRIPT: |
| nibPath = @"ContentBlockedMixedScript"; break; |
| case CONTENT_SETTINGS_TYPE_PROTOCOL_HANDLERS: |
| nibPath = @"ContentProtocolHandlers"; break; |
| case CONTENT_SETTINGS_TYPE_MEDIASTREAM: |
| nibPath = @"ContentBlockedMedia"; break; |
| case CONTENT_SETTINGS_TYPE_AUTOMATIC_DOWNLOADS: |
| nibPath = @"ContentBlockedDownloads"; break; |
| case CONTENT_SETTINGS_TYPE_MIDI_SYSEX: |
| nibPath = @"ContentBlockedMIDISysEx"; break; |
| // These content types have no bubble: |
| case CONTENT_SETTINGS_TYPE_DEFAULT: |
| case CONTENT_SETTINGS_TYPE_NOTIFICATIONS: |
| case CONTENT_SETTINGS_TYPE_AUTO_SELECT_CERTIFICATE: |
| case CONTENT_SETTINGS_TYPE_FULLSCREEN: |
| case CONTENT_SETTINGS_TYPE_MOUSELOCK: |
| case CONTENT_SETTINGS_TYPE_MEDIASTREAM_MIC: |
| case CONTENT_SETTINGS_TYPE_MEDIASTREAM_CAMERA: |
| case CONTENT_SETTINGS_NUM_TYPES: |
| // TODO(miguelg): Remove this nib content settings support |
| // is implemented |
| case CONTENT_SETTINGS_TYPE_PUSH_MESSAGING: |
| case CONTENT_SETTINGS_TYPE_SSL_CERT_DECISIONS: |
| NOTREACHED(); |
| } |
| if ((self = [super initWithWindowNibPath:nibPath |
| parentWindow:parentWindow |
| anchoredAt:anchoredAt])) { |
| contentSettingBubbleModel_.reset(model.release()); |
| [self showWindow:nil]; |
| } |
| return self; |
| } |
| |
| - (void)dealloc { |
| STLDeleteValues(&mediaMenus_); |
| [super dealloc]; |
| } |
| |
| - (void)initializeTitle { |
| if (!titleLabel_) |
| return; |
| |
| NSString* label = base::SysUTF8ToNSString( |
| contentSettingBubbleModel_->bubble_content().title); |
| [titleLabel_ setStringValue:label]; |
| |
| // Layout title post-localization. |
| CGFloat deltaY = [GTMUILocalizerAndLayoutTweaker |
| sizeToFitFixedWidthTextField:titleLabel_]; |
| NSRect windowFrame = [[self window] frame]; |
| windowFrame.size.height += deltaY; |
| [[self window] setFrame:windowFrame display:NO]; |
| NSRect titleFrame = [titleLabel_ frame]; |
| titleFrame.origin.y -= deltaY; |
| [titleLabel_ setFrame:titleFrame]; |
| } |
| |
| - (void)initializeRadioGroup { |
| // NOTE! Tags in the xib files must match the order of the radio buttons |
| // passed in the radio_group and be 1-based, not 0-based. |
| const ContentSettingBubbleModel::RadioGroup& radio_group = |
| contentSettingBubbleModel_->bubble_content().radio_group; |
| |
| // Select appropriate radio button. |
| [allowBlockRadioGroup_ selectCellWithTag: radio_group.default_item + 1]; |
| |
| const ContentSettingBubbleModel::RadioItems& radio_items = |
| radio_group.radio_items; |
| for (size_t ii = 0; ii < radio_group.radio_items.size(); ++ii) { |
| NSCell* radioCell = [allowBlockRadioGroup_ cellWithTag: ii + 1]; |
| [radioCell setTitle:base::SysUTF8ToNSString(radio_items[ii])]; |
| } |
| |
| // Layout radio group labels post-localization. |
| [GTMUILocalizerAndLayoutTweaker |
| wrapRadioGroupForWidth:allowBlockRadioGroup_]; |
| CGFloat radioDeltaY = [GTMUILocalizerAndLayoutTweaker |
| sizeToFitView:allowBlockRadioGroup_].height; |
| NSRect windowFrame = [[self window] frame]; |
| windowFrame.size.height += radioDeltaY; |
| [[self window] setFrame:windowFrame display:NO]; |
| } |
| |
| - (NSButton*)hyperlinkButtonWithFrame:(NSRect)frame |
| title:(NSString*)title |
| icon:(NSImage*)icon |
| referenceFrame:(NSRect)referenceFrame { |
| base::scoped_nsobject<HyperlinkButtonCell> cell( |
| [[HyperlinkButtonCell alloc] initTextCell:title]); |
| [cell.get() setAlignment:NSNaturalTextAlignment]; |
| if (icon) { |
| [cell.get() setImagePosition:NSImageLeft]; |
| [cell.get() setImage:icon]; |
| } else { |
| [cell.get() setImagePosition:NSNoImage]; |
| } |
| [cell.get() setControlSize:NSSmallControlSize]; |
| |
| NSButton* button = [[[NSButton alloc] initWithFrame:frame] autorelease]; |
| // Cell must be set immediately after construction. |
| [button setCell:cell.get()]; |
| |
| // Size to fit the button and add a little extra padding for the small-text |
| // hyperlink button, which sizeToFit gets wrong. |
| [GTMUILocalizerAndLayoutTweaker sizeToFitView:button]; |
| NSRect buttonFrame = [button frame]; |
| buttonFrame.size.width += 2; |
| |
| // If the link text is too long, clamp it. |
| int maxWidth = NSWidth([[self bubble] frame]) - 2 * NSMinX(referenceFrame); |
| if (NSWidth(buttonFrame) > maxWidth) |
| buttonFrame.size.width = maxWidth; |
| |
| [button setFrame:buttonFrame]; |
| [button setTarget:self]; |
| [button setAction:@selector(popupLinkClicked:)]; |
| return button; |
| } |
| |
| - (void)initializeBlockedPluginsList { |
| NSString* label = base::SysUTF16ToNSString( |
| contentSettingBubbleModel_->bubble_content().plugin_names); |
| [blockedResourcesField_ setStringValue:label]; |
| } |
| |
| - (void)initializePopupList { |
| // I didn't put the buttons into a NSMatrix because then they are only one |
| // entity in the key view loop. This way, one can tab through all of them. |
| const ContentSettingBubbleModel::PopupItems& popupItems = |
| contentSettingBubbleModel_->bubble_content().popup_items; |
| |
| // Get the pre-resize frame of the radio group. Its origin is where the |
| // popup list should go. |
| NSRect radioFrame = [allowBlockRadioGroup_ frame]; |
| |
| // Make room for the popup list. The bubble view and its subviews autosize |
| // themselves when the window is enlarged. |
| // Heading and radio box are already 1 * kLinkOuterPadding apart in the nib, |
| // so only 1 * kLinkOuterPadding more is needed. |
| int delta = popupItems.size() * kLinkLineHeight - kLinkPadding + |
| kLinkOuterPadding; |
| NSSize deltaSize = NSMakeSize(0, delta); |
| deltaSize = [[[self window] contentView] convertSize:deltaSize toView:nil]; |
| NSRect windowFrame = [[self window] frame]; |
| windowFrame.size.height += deltaSize.height; |
| [[self window] setFrame:windowFrame display:NO]; |
| |
| // Create popup list. |
| int topLinkY = NSMaxY(radioFrame) + delta - kLinkHeight; |
| int row = 0; |
| for (std::vector<ContentSettingBubbleModel::PopupItem>::const_iterator |
| it(popupItems.begin()); it != popupItems.end(); ++it, ++row) { |
| NSImage* image = it->image.AsNSImage(); |
| |
| std::string title(it->title); |
| // The popup may not have committed a load yet, in which case it won't |
| // have a URL or title. |
| if (title.empty()) |
| title = l10n_util::GetStringUTF8(IDS_TAB_LOADING_TITLE); |
| |
| NSRect linkFrame = |
| NSMakeRect(NSMinX(radioFrame), topLinkY - kLinkLineHeight * row, |
| 200, kLinkHeight); |
| NSButton* button = [self |
| hyperlinkButtonWithFrame:linkFrame |
| title:base::SysUTF8ToNSString(title) |
| icon:image |
| referenceFrame:radioFrame]; |
| [[self bubble] addSubview:button]; |
| popupLinks_[button] = row; |
| } |
| } |
| |
| - (void)initializeGeoLists { |
| // Cocoa has its origin in the lower left corner. This means elements are |
| // added from bottom to top, which explains why loops run backwards and the |
| // order of operations is the other way than on Linux/Windows. |
| const ContentSettingBubbleModel::BubbleContent& content = |
| contentSettingBubbleModel_->bubble_content(); |
| NSRect containerFrame = [contentsContainer_ frame]; |
| NSRect frame = NSMakeRect(0, 0, NSWidth(containerFrame), kGeoLabelHeight); |
| |
| // "Clear" button / text field. |
| if (!content.custom_link.empty()) { |
| base::scoped_nsobject<NSControl> control; |
| if(content.custom_link_enabled) { |
| NSRect buttonFrame = NSMakeRect(0, 0, |
| NSWidth(containerFrame), |
| kGeoClearButtonHeight); |
| NSButton* button = [[NSButton alloc] initWithFrame:buttonFrame]; |
| control.reset(button); |
| [button setTitle:base::SysUTF8ToNSString(content.custom_link)]; |
| [button setTarget:self]; |
| [button setAction:@selector(clearGeolocationForCurrentHost:)]; |
| [button setBezelStyle:NSRoundRectBezelStyle]; |
| SetControlSize(button, NSSmallControlSize); |
| [button sizeToFit]; |
| } else { |
| // Add the notification that settings will be cleared on next reload. |
| control.reset([LabelWithFrame( |
| base::SysUTF8ToNSString(content.custom_link), frame) retain]); |
| SetControlSize(control.get(), NSSmallControlSize); |
| } |
| |
| // If the new control is wider than the container, widen the window. |
| CGFloat controlWidth = NSWidth([control frame]); |
| if (controlWidth > NSWidth(containerFrame)) { |
| NSRect windowFrame = [[self window] frame]; |
| windowFrame.size.width += controlWidth - NSWidth(containerFrame); |
| [[self window] setFrame:windowFrame display:NO]; |
| // Fetch the updated sizes. |
| containerFrame = [contentsContainer_ frame]; |
| frame = NSMakeRect(0, 0, NSWidth(containerFrame), kGeoLabelHeight); |
| } |
| |
| DCHECK(control); |
| [contentsContainer_ addSubview:control]; |
| frame.origin.y = NSMaxY([control frame]) + kGeoPadding; |
| } |
| |
| for (auto i = content.domain_lists.rbegin(); |
| i != content.domain_lists.rend(); ++i) { |
| // Add all hosts in the current domain list. |
| for (auto j = i->hosts.rbegin(); j != i->hosts.rend(); ++j) { |
| NSTextField* title = LabelWithFrame(base::SysUTF8ToNSString(*j), frame); |
| SetControlSize(title, NSSmallControlSize); |
| [contentsContainer_ addSubview:title]; |
| |
| frame.origin.y = NSMaxY(frame) + kGeoHostPadding + |
| [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:title]; |
| } |
| if (!i->hosts.empty()) |
| frame.origin.y += kGeoPadding - kGeoHostPadding; |
| |
| // Add the domain list's title. |
| NSTextField* title = |
| LabelWithFrame(base::SysUTF8ToNSString(i->title), frame); |
| SetControlSize(title, NSSmallControlSize); |
| [contentsContainer_ addSubview:title]; |
| |
| frame.origin.y = NSMaxY(frame) + kGeoPadding + |
| [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:title]; |
| } |
| |
| CGFloat containerHeight = frame.origin.y; |
| // Undo last padding. |
| if (!content.domain_lists.empty()) |
| containerHeight -= kGeoPadding; |
| |
| // Resize container to fit its subviews, and window to fit the container. |
| NSRect windowFrame = [[self window] frame]; |
| windowFrame.size.height += containerHeight - NSHeight(containerFrame); |
| [[self window] setFrame:windowFrame display:NO]; |
| containerFrame.size.height = containerHeight; |
| [contentsContainer_ setFrame:containerFrame]; |
| } |
| |
| - (void)initializeMediaMenus { |
| const ContentSettingBubbleModel::MediaMenuMap& media_menus = |
| contentSettingBubbleModel_->bubble_content().media_menus; |
| |
| // Calculate the longest width of the labels and menus menus to avoid |
| // truncation by the window's edge. |
| CGFloat maxLabelWidth = 0; |
| CGFloat maxMenuWidth = 0; |
| CGFloat maxMenuHeight = 0; |
| NSRect radioFrame = [allowBlockRadioGroup_ frame]; |
| for (ContentSettingBubbleModel::MediaMenuMap::const_iterator it( |
| media_menus.begin()); it != media_menus.end(); ++it) { |
| // |labelFrame| will be resized later on in this function. |
| NSRect labelFrame = NSMakeRect(NSMinX(radioFrame), 0, 0, 0); |
| NSTextField* label = |
| LabelWithFrame(base::SysUTF8ToNSString(it->second.label), labelFrame); |
| SetControlSize(label, NSSmallControlSize); |
| NSCell* cell = [label cell]; |
| [cell setAlignment:NSRightTextAlignment]; |
| [GTMUILocalizerAndLayoutTweaker sizeToFitView:label]; |
| maxLabelWidth = std::max(maxLabelWidth, [label frame].size.width); |
| [[self bubble] addSubview:label]; |
| |
| // |buttonFrame| will be resized and repositioned later on. |
| NSRect buttonFrame = NSMakeRect(NSMinX(radioFrame), 0, 0, 0); |
| base::scoped_nsobject<NSPopUpButton> button( |
| [[NSPopUpButton alloc] initWithFrame:buttonFrame]); |
| [button setTarget:self]; |
| |
| // Store the |label| and |button| into MediaMenuParts struct and build |
| // the popup menu from the menu model. |
| content_setting_bubble::MediaMenuParts* menuParts = |
| new content_setting_bubble::MediaMenuParts(it->first, label); |
| menuParts->model.reset(new ContentSettingMediaMenuModel( |
| it->first, contentSettingBubbleModel_.get(), |
| ContentSettingMediaMenuModel::MenuLabelChangedCallback())); |
| mediaMenus_[button] = menuParts; |
| CGFloat width = BuildPopUpMenuFromModel(button, |
| menuParts->model.get(), |
| it->second.selected_device.name, |
| it->second.disabled); |
| maxMenuWidth = std::max(maxMenuWidth, width); |
| |
| [[self bubble] addSubview:button |
| positioned:NSWindowBelow |
| relativeTo:nil]; |
| |
| maxMenuHeight = std::max(maxMenuHeight, [button frame].size.height); |
| } |
| |
| // Make room for the media menu(s) and enlarge the windows to fit the views. |
| // The bubble view and its subviews autosize themselves when the window is |
| // enlarged. |
| int delta = media_menus.size() * maxMenuHeight + |
| (media_menus.size() - 1) * kMediaMenuElementVerticalPadding; |
| NSSize deltaSize = NSMakeSize(0, delta); |
| deltaSize = [[[self window] contentView] convertSize:deltaSize toView:nil]; |
| NSRect windowFrame = [[self window] frame]; |
| windowFrame.size.height += deltaSize.height; |
| // If the media menus are wider than the window, widen the window. |
| CGFloat widthNeeded = maxLabelWidth + maxMenuWidth + 2 * NSMinX(radioFrame); |
| if (widthNeeded > windowFrame.size.width) |
| windowFrame.size.width = widthNeeded; |
| [[self window] setFrame:windowFrame display:NO]; |
| |
| // The radio group lies above the media menus, move the radio group up. |
| radioFrame.origin.y += delta; |
| [allowBlockRadioGroup_ setFrame:radioFrame]; |
| |
| // Resize and reposition the media menus layout. |
| CGFloat topMenuY = NSMinY(radioFrame) - kMediaMenuVerticalPadding; |
| maxMenuWidth = std::max(maxMenuWidth, kMinMediaMenuButtonWidth); |
| for (content_setting_bubble::MediaMenuPartsMap::const_iterator i = |
| mediaMenus_.begin(); i != mediaMenus_.end(); ++i) { |
| NSRect labelFrame = [i->second->label frame]; |
| // Align the label text with the button text. |
| labelFrame.origin.y = |
| topMenuY + (maxMenuHeight - labelFrame.size.height) / 2 + 1; |
| labelFrame.size.width = maxLabelWidth; |
| [i->second->label setFrame:labelFrame]; |
| NSRect menuFrame = [i->first frame]; |
| menuFrame.origin.y = topMenuY; |
| menuFrame.origin.x = NSMinX(radioFrame) + maxLabelWidth; |
| menuFrame.size.width = maxMenuWidth; |
| menuFrame.size.height = maxMenuHeight; |
| [i->first setFrame:menuFrame]; |
| topMenuY -= (maxMenuHeight + kMediaMenuElementVerticalPadding); |
| } |
| } |
| |
| - (void)initializeMIDISysExLists { |
| const ContentSettingBubbleModel::BubbleContent& content = |
| contentSettingBubbleModel_->bubble_content(); |
| NSRect containerFrame = [contentsContainer_ frame]; |
| NSRect frame = |
| NSMakeRect(0, 0, NSWidth(containerFrame), kMIDISysExLabelHeight); |
| |
| // "Clear" button / text field. |
| if (!content.custom_link.empty()) { |
| base::scoped_nsobject<NSControl> control; |
| if (content.custom_link_enabled) { |
| NSRect buttonFrame = NSMakeRect(0, 0, |
| NSWidth(containerFrame), |
| kMIDISysExClearButtonHeight); |
| NSButton* button = [[NSButton alloc] initWithFrame:buttonFrame]; |
| control.reset(button); |
| [button setTitle:base::SysUTF8ToNSString(content.custom_link)]; |
| [button setTarget:self]; |
| [button setAction:@selector(clearMIDISysExForCurrentHost:)]; |
| [button setBezelStyle:NSRoundRectBezelStyle]; |
| SetControlSize(button, NSSmallControlSize); |
| [button sizeToFit]; |
| } else { |
| // Add the notification that settings will be cleared on next reload. |
| control.reset([LabelWithFrame( |
| base::SysUTF8ToNSString(content.custom_link), frame) retain]); |
| SetControlSize(control.get(), NSSmallControlSize); |
| } |
| |
| // If the new control is wider than the container, widen the window. |
| CGFloat controlWidth = NSWidth([control frame]); |
| if (controlWidth > NSWidth(containerFrame)) { |
| NSRect windowFrame = [[self window] frame]; |
| windowFrame.size.width += controlWidth - NSWidth(containerFrame); |
| [[self window] setFrame:windowFrame display:NO]; |
| // Fetch the updated sizes. |
| containerFrame = [contentsContainer_ frame]; |
| frame = NSMakeRect(0, 0, NSWidth(containerFrame), kMIDISysExLabelHeight); |
| } |
| |
| DCHECK(control); |
| [contentsContainer_ addSubview:control]; |
| frame.origin.y = NSMaxY([control frame]) + kMIDISysExPadding; |
| } |
| |
| for (auto i = content.domain_lists.rbegin(); |
| i != content.domain_lists.rend(); ++i) { |
| // Add all hosts in the current domain list. |
| for (auto j = i->hosts.rbegin(); j != i->hosts.rend(); ++j) { |
| NSTextField* title = LabelWithFrame(base::SysUTF8ToNSString(*j), frame); |
| SetControlSize(title, NSSmallControlSize); |
| [contentsContainer_ addSubview:title]; |
| |
| frame.origin.y = NSMaxY(frame) + kMIDISysExHostPadding + |
| [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:title]; |
| } |
| if (!i->hosts.empty()) |
| frame.origin.y += kMIDISysExPadding - kMIDISysExHostPadding; |
| |
| // Add the domain list's title. |
| NSTextField* title = |
| LabelWithFrame(base::SysUTF8ToNSString(i->title), frame); |
| SetControlSize(title, NSSmallControlSize); |
| [contentsContainer_ addSubview:title]; |
| |
| frame.origin.y = NSMaxY(frame) + kMIDISysExPadding + |
| [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:title]; |
| } |
| |
| CGFloat containerHeight = frame.origin.y; |
| // Undo last padding. |
| if (!content.domain_lists.empty()) |
| containerHeight -= kMIDISysExPadding; |
| |
| // Resize container to fit its subviews, and window to fit the container. |
| NSRect windowFrame = [[self window] frame]; |
| windowFrame.size.height += containerHeight - NSHeight(containerFrame); |
| [[self window] setFrame:windowFrame display:NO]; |
| containerFrame.size.height = containerHeight; |
| [contentsContainer_ setFrame:containerFrame]; |
| } |
| |
| - (void)sizeToFitLoadButton { |
| const ContentSettingBubbleModel::BubbleContent& content = |
| contentSettingBubbleModel_->bubble_content(); |
| [loadButton_ setEnabled:content.custom_link_enabled]; |
| |
| // Resize horizontally to fit button if necessary. |
| NSRect windowFrame = [[self window] frame]; |
| int widthNeeded = NSWidth([loadButton_ frame]) + |
| 2 * NSMinX([loadButton_ frame]); |
| if (NSWidth(windowFrame) < widthNeeded) { |
| windowFrame.size.width = widthNeeded; |
| [[self window] setFrame:windowFrame display:NO]; |
| } |
| } |
| |
| - (void)initManageDoneButtons { |
| const ContentSettingBubbleModel::BubbleContent& content = |
| contentSettingBubbleModel_->bubble_content(); |
| [manageButton_ setTitle:base::SysUTF8ToNSString(content.manage_link)]; |
| [GTMUILocalizerAndLayoutTweaker sizeToFitView:manageButton_]; |
| |
| CGFloat actualWidth = NSWidth([[[self window] contentView] frame]); |
| CGFloat requiredWidth = NSMaxX([manageButton_ frame]) + kManageDonePadding + |
| NSWidth([[doneButton_ superview] frame]) - NSMinX([doneButton_ frame]); |
| if (requiredWidth <= actualWidth || !doneButton_ || !manageButton_) |
| return; |
| |
| // Resize window, autoresizing takes care of the rest. |
| NSSize size = NSMakeSize(requiredWidth - actualWidth, 0); |
| size = [[[self window] contentView] convertSize:size toView:nil]; |
| NSRect frame = [[self window] frame]; |
| frame.origin.x -= size.width; |
| frame.size.width += size.width; |
| [[self window] setFrame:frame display:NO]; |
| } |
| |
| - (void)awakeFromNib { |
| [super awakeFromNib]; |
| |
| [[self bubble] setArrowLocation:info_bubble::kTopRight]; |
| |
| // Adapt window size to bottom buttons. Do this before all other layouting. |
| [self initManageDoneButtons]; |
| |
| [self initializeTitle]; |
| |
| ContentSettingsType type = contentSettingBubbleModel_->content_type(); |
| if (type == CONTENT_SETTINGS_TYPE_PLUGINS) { |
| [self sizeToFitLoadButton]; |
| [self initializeBlockedPluginsList]; |
| } |
| |
| if (allowBlockRadioGroup_) // not bound in cookie bubble xib |
| [self initializeRadioGroup]; |
| |
| if (type == CONTENT_SETTINGS_TYPE_POPUPS) |
| [self initializePopupList]; |
| if (type == CONTENT_SETTINGS_TYPE_GEOLOCATION) |
| [self initializeGeoLists]; |
| if (type == CONTENT_SETTINGS_TYPE_MEDIASTREAM) |
| [self initializeMediaMenus]; |
| if (type == CONTENT_SETTINGS_TYPE_MIDI_SYSEX) |
| [self initializeMIDISysExLists]; |
| } |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| // Actual application logic |
| |
| - (IBAction)allowBlockToggled:(id)sender { |
| NSButtonCell *selectedCell = [sender selectedCell]; |
| contentSettingBubbleModel_->OnRadioClicked([selectedCell tag] - 1); |
| } |
| |
| - (void)popupLinkClicked:(id)sender { |
| content_setting_bubble::PopupLinks::iterator i(popupLinks_.find(sender)); |
| DCHECK(i != popupLinks_.end()); |
| contentSettingBubbleModel_->OnPopupClicked(i->second); |
| } |
| |
| - (void)clearGeolocationForCurrentHost:(id)sender { |
| contentSettingBubbleModel_->OnCustomLinkClicked(); |
| [self close]; |
| } |
| |
| - (void)clearMIDISysExForCurrentHost:(id)sender { |
| contentSettingBubbleModel_->OnCustomLinkClicked(); |
| [self close]; |
| } |
| |
| - (IBAction)showMoreInfo:(id)sender { |
| contentSettingBubbleModel_->OnCustomLinkClicked(); |
| [self close]; |
| } |
| |
| - (IBAction)load:(id)sender { |
| contentSettingBubbleModel_->OnCustomLinkClicked(); |
| [self close]; |
| } |
| |
| - (IBAction)learnMoreLinkClicked:(id)sender { |
| contentSettingBubbleModel_->OnManageLinkClicked(); |
| } |
| |
| - (IBAction)manageBlocking:(id)sender { |
| contentSettingBubbleModel_->OnManageLinkClicked(); |
| } |
| |
| - (IBAction)closeBubble:(id)sender { |
| contentSettingBubbleModel_->OnDoneClicked(); |
| [self close]; |
| } |
| |
| - (IBAction)mediaMenuChanged:(id)sender { |
| NSPopUpButton* button = static_cast<NSPopUpButton*>(sender); |
| content_setting_bubble::MediaMenuPartsMap::const_iterator it( |
| mediaMenus_.find(sender)); |
| DCHECK(it != mediaMenus_.end()); |
| NSInteger index = [[button selectedItem] tag]; |
| |
| SetTitleForPopUpButton( |
| button, base::SysUTF16ToNSString(it->second->model->GetLabelAt(index))); |
| |
| it->second->model->ExecuteCommand(index, 0); |
| } |
| |
| - (content_setting_bubble::MediaMenuPartsMap*)mediaMenus { |
| return &mediaMenus_; |
| } |
| |
| @end // ContentSettingBubbleController |