// Copyright (c) 2010 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/cocoa/content_blocked_bubble_controller.h" #include "app/l10n_util.h" #include "base/logging.h" #include "base/sys_string_conversions.h" #include "chrome/browser/blocked_popup_container.h" #import "chrome/browser/cocoa/content_settings_dialog_controller.h" #import "chrome/browser/cocoa/hyperlink_button_cell.h" #import "chrome/browser/cocoa/info_bubble_view.h" #import "chrome/browser/cocoa/l10n_util.h" #include "chrome/browser/content_setting_bubble_model.h" #include "chrome/browser/host_content_settings_map.h" #include "grit/generated_resources.h" #include "skia/ext/skia_utils_mac.h" #import "third_party/GTM/AppKit/GTMUILocalizerAndLayoutTweaker.h" namespace { // Must match the tag of the unblock radio button in the xib files. const int kAllowTag = 1; // Must match the tag of the block radio button in the xib files. const int kBlockTag = 2; // 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; 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]; } } // namespace @interface ContentBlockedBubbleController(Private) - (id)initWithModel:(ContentSettingBubbleModel*)settingsBubbleModel parentWindow:(NSWindow*)parentWindow anchoredAt:(NSPoint)anchoredAt; - (NSButton*)hyperlinkButtonWithFrame:(NSRect)frame title:(NSString*)title icon:(NSImage*)icon referenceFrame:(NSRect)referenceFrame; - (void)initializeTitle; - (void)initializeRadioGroup; - (void)initializePopupList; - (void)initializeGeoLists; - (void)sizeToFitManageDoneButtons; - (void)popupLinkClicked:(id)sender; - (void)clearGeolocationForCurrentHost:(id)sender; @end @implementation ContentBlockedBubbleController + (ContentBlockedBubbleController*) showForModel:(ContentSettingBubbleModel*)contentSettingBubbleModel parentWindow:(NSWindow*)parentWindow anchoredAt:(NSPoint)anchor { // Autoreleases itself on bubble close. return [[ContentBlockedBubbleController alloc] initWithModel:contentSettingBubbleModel parentWindow:parentWindow anchoredAt:anchor]; } - (id)initWithModel:(ContentSettingBubbleModel*)contentSettingBubbleModel parentWindow:(NSWindow*)parentWindow anchoredAt:(NSPoint)anchoredAt { // This method takes ownership of |contentSettingBubbleModel| in all cases. scoped_ptr model(contentSettingBubbleModel); DCHECK(model.get()); NSString* const nibPaths[] = { @"ContentBlockedCookies", @"ContentBlockedImages", @"ContentBlockedJavaScript", @"ContentBlockedPlugins", @"ContentBlockedPopups", @"ContentBubbleGeolocation", @"", // Notifications do not have a bubble. }; COMPILE_ASSERT(arraysize(nibPaths) == CONTENT_SETTINGS_NUM_TYPES, nibPaths_requires_an_entry_for_every_setting_type); const int settingsType = model->content_type(); // Nofifications do not have a bubble. CHECK_NE(settingsType, CONTENT_SETTINGS_TYPE_NOTIFICATIONS); DCHECK_LT(settingsType, CONTENT_SETTINGS_NUM_TYPES); if ((self = [super initWithWindowNibPath:nibPaths[settingsType] parentWindow:parentWindow anchoredAt:anchoredAt])) { contentSettingBubbleModel_.reset(model.release()); [self showWindow:nil]; } return self; } - (void)initializeTitle { if (!titleLabel_) return; // 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 { // Configure the radio group. For now, only deal with the // strictly needed case of group containing 2 radio buttons. // TODO(joth): Implement the generic case, getting localized strings from the // bubble model instead of the xib, or remove it if it's never needed. // http://crbug.com/38432 const ContentSettingBubbleModel::RadioGroup& radioGroup = contentSettingBubbleModel_->bubble_content().radio_group; // Select appropriate radio button. [allowBlockRadioGroup_ selectCellWithTag: radioGroup.default_item == 0 ? kAllowTag : kBlockTag]; // Copy |host_| into radio group label. NSCell* radioCell = [allowBlockRadioGroup_ cellWithTag:kAllowTag]; [radioCell setTitle:cocoa_l10n_util::ReplaceNSStringPlaceholders( [radioCell title], UTF8ToUTF16(radioGroup.url.host()), NULL)]; // 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 { scoped_nsobject 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()]; // If the link text is too long, clamp it. [button sizeToFit]; int maxWidth = NSWidth([[self bubble] frame]) - 2 * NSMinX(referenceFrame); NSRect buttonFrame = [button frame]; if (NSWidth(buttonFrame) > maxWidth) { buttonFrame.size.width = maxWidth; [button setFrame:buttonFrame]; } [button setTarget:self]; [button setAction:@selector(popupLinkClicked:)]; return button; } - (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::const_iterator it(popupItems.begin()); it != popupItems.end(); ++it, ++row) { const SkBitmap& icon = it->bitmap; NSImage* image = nil; if (!icon.empty()) image = gfx::SkBitmapToNSImage(icon); 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. if (!content.clear_link.empty()) { NSRect buttonFrame = NSMakeRect(0, 0, NSWidth(containerFrame), kGeoClearButtonHeight); scoped_nsobject button([[NSButton alloc] initWithFrame:buttonFrame]); [button setTitle:base::SysUTF8ToNSString(content.clear_link)]; [button setTarget:self]; [button setAction:@selector(clearGeolocationForCurrentHost:)]; [button setBezelStyle:NSRoundRectBezelStyle]; SetControlSize(button, NSSmallControlSize); [button sizeToFit]; // If the button is wider than the container, widen the window. CGFloat buttonWidth = NSWidth([button frame]); if (buttonWidth > NSWidth(containerFrame)) { NSRect windowFrame = [[self window] frame]; windowFrame.size.width += buttonWidth - NSWidth(containerFrame); [[self window] setFrame:windowFrame display:NO]; // Fetch the updated sizes. containerFrame = [contentsContainer_ frame]; frame = NSMakeRect(0, 0, NSWidth(containerFrame), kGeoLabelHeight); } // Add the button. [contentsContainer_ addSubview:button]; frame.origin.y = NSMaxY([button frame]) + kGeoPadding; } typedef std::vector::const_reverse_iterator GeolocationGroupIterator; for (GeolocationGroupIterator i = content.domain_lists.rbegin(); i != content.domain_lists.rend(); ++i) { // Add all hosts in the current domain list. for (std::set::const_reverse_iterator 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)sizeToFitManageDoneButtons { 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 { [[self bubble] setBubbleType:info_bubble::kWhiteInfoBubble]; [[self bubble] setArrowLocation:info_bubble::kTopRight]; // Adapt window size to bottom buttons. Do this before all other layouting. [self sizeToFitManageDoneButtons]; [self initializeTitle]; if (allowBlockRadioGroup_) // not bound in cookie bubble xib [self initializeRadioGroup]; if (contentSettingBubbleModel_->content_type() == CONTENT_SETTINGS_TYPE_POPUPS) [self initializePopupList]; if (contentSettingBubbleModel_->content_type() == CONTENT_SETTINGS_TYPE_GEOLOCATION) [self initializeGeoLists]; } /////////////////////////////////////////////////////////////////////////////// // Actual application logic - (IBAction)allowBlockToggled:(id)sender { NSButtonCell *selectedCell = [sender selectedCell]; contentSettingBubbleModel_->OnRadioClicked( [selectedCell tag] == kAllowTag ? 0 : 1); } - (IBAction)closeBubble:(id)sender { [self close]; } - (IBAction)manageBlocking:(id)sender { contentSettingBubbleModel_->OnManageLinkClicked(); } - (IBAction)showMoreInfo:(id)sender { contentSettingBubbleModel_->OnInfoLinkClicked(); [self close]; } - (void)popupLinkClicked:(id)sender { content_blocked_bubble::PopupLinks::iterator i(popupLinks_.find(sender)); DCHECK(i != popupLinks_.end()); contentSettingBubbleModel_->OnPopupClicked(i->second); } - (void)clearGeolocationForCurrentHost:(id)sender { contentSettingBubbleModel_->OnClearLinkClicked(); [self close]; } @end // ContentBlockedBubbleController