// 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/mac_util.h" #include "base/string_util.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" #include "chrome/browser/host_content_settings_map.h" #include "chrome/browser/profile.h" #include "chrome/browser/tab_contents/tab_contents.h" #include "chrome/common/notification_service.h" #include "chrome/common/notification_type.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; } // namespace // If the bubble's tab contents get destroyed, tells the bubble about it. class BubbleCloser : public NotificationObserver { public: BubbleCloser(ContentBlockedBubbleController* controller, TabContents* tab_contents) : controller_(controller) { registrar_.Add(this, NotificationType::TAB_CONTENTS_DESTROYED, Source(tab_contents)); } virtual ~BubbleCloser() {} virtual void Observe(NotificationType type, const NotificationSource& source, const NotificationDetails& details) { DCHECK(type == NotificationType::TAB_CONTENTS_DESTROYED); DCHECK(source == Source([controller_ tabContents])); [controller_ setTabContents:NULL]; } private: ContentBlockedBubbleController* controller_; // weak, owns us NotificationRegistrar registrar_; }; // Like |ReplaceStringPlaceholders(const string16&, const string16&, size_t*)|, // but for a NSString formatString. static NSString* ReplaceNSStringPlaceholders(NSString* formatString, const string16& a, size_t* offset) { return base::SysUTF16ToNSString( ReplaceStringPlaceholders(base::SysNSStringToUTF16(formatString), a, offset)); } @interface ContentBlockedBubbleController(Private) - (id)initWithType:(ContentSettingsType)settingsType parentWindow:(NSWindow*)parentWindow anchoredAt:(NSPoint)anchoredAt host:(const std::string&)host displayHost:(NSString*)displayHost tabContents:(TabContents*)tabContents profile:(Profile*)profile; - (NSButton*)hyperlinkButtonWithFrame:(NSRect)frame title:(NSString*)title icon:(NSImage*)icon referenceFrame:(NSRect)referenceFrame; - (void)initializeTitle; - (void)initializeRadioGroup; - (void)initializePopupList; - (void)popupLinkClicked:(id)sender; @end @implementation ContentBlockedBubbleController + (ContentBlockedBubbleController*)showForType:(ContentSettingsType)settingsType parentWindow:(NSWindow*)parentWindow anchoredAt:(NSPoint)anchor host:(const std::string&)host displayHost:(NSString*)displayHost tabContents:(TabContents*)tabContents profile:(Profile*)profile { // Autoreleases itself on bubble close. return [[ContentBlockedBubbleController alloc] initWithType:settingsType parentWindow:parentWindow anchoredAt:anchor host:host displayHost:displayHost tabContents:tabContents profile:profile]; } @synthesize tabContents = tabContents_; - (id)initWithType:(ContentSettingsType)settingsType parentWindow:(NSWindow*)parentWindow anchoredAt:(NSPoint)anchoredAt host:(const std::string&)host displayHost:(NSString*)displayHost tabContents:(TabContents*)tabContents profile:(Profile*)profile { NSString* const nibPaths[CONTENT_SETTINGS_NUM_TYPES] = { @"ContentBlockedCookies", @"ContentBlockedImages", @"ContentBlockedJavaScript", @"ContentBlockedPlugins", @"ContentBlockedPopups", }; DCHECK_EQ(arraysize(nibPaths), static_cast(CONTENT_SETTINGS_NUM_TYPES)); NSString* nibPath = [mac_util::MainAppBundle() pathForResource:nibPaths[settingsType] ofType:@"nib"]; if ((self = [super initWithWindowNibPath:nibPath owner:self])) { parentWindow_ = parentWindow; anchor_ = anchoredAt; settingsType_ = settingsType; host_ = host; displayHost_.reset([displayHost retain]); tabContents_ = tabContents; profile_ = profile; closer_.reset(new BubbleCloser(self, tabContents_)); // Watch to see if the parent window closes, and if so, close this one. NSNotificationCenter* center = [NSNotificationCenter defaultCenter]; [center addObserver:self selector:@selector(parentWindowWillClose:) name:NSWindowWillCloseNotification object:parentWindow_]; } return self; } - (void)initializeTitle { // Layout title post-localization. CGFloat titleDeltaY = [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:titleLabel_]; NSRect windowFrame = [[self window] frame]; windowFrame.size.height += titleDeltaY; [[self window] setFrame:windowFrame display:NO]; NSRect titleFrame = [titleLabel_ frame]; titleFrame.origin.y -= titleDeltaY; [titleLabel_ setFrame:titleFrame]; } - (void)initializeRadioGroup { // Select appropriate radio button.. if (profile_) { // NULL in tests. HostContentSettingsMap* contentSettingsMap = profile_->GetHostContentSettingsMap(); ContentSetting setting = contentSettingsMap->GetContentSetting( host_, settingsType_); [allowBlockRadioGroup_ selectCellWithTag: setting == CONTENT_SETTING_ALLOW ? kAllowTag : kBlockTag]; } // Copy |host_| into radio group label. NSCell* radioCell = [allowBlockRadioGroup_ cellWithTag:kAllowTag]; [radioCell setTitle:ReplaceNSStringPlaceholders([radioCell title], UTF8ToUTF16(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([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. BlockedPopupContainer::BlockedContents blockedContents; DCHECK(tabContents_->blocked_popup_container()); tabContents_->blocked_popup_container()->GetBlockedContents( &blockedContents); // 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 = blockedContents.size() * kLinkLineHeight - kLinkPadding + kLinkOuterPadding; NSRect windowFrame = [[self window] frame]; windowFrame.size.height += delta; [[self window] setFrame:windowFrame display:NO]; // Create popup list. int topLinkY = NSMaxY(radioFrame) + delta - kLinkHeight; int row = 0; for (BlockedPopupContainer::BlockedContents::const_iterator it(blockedContents.begin()); it != blockedContents.end(); ++it, ++row) { SkBitmap icon = (*it)->GetFavIcon(); NSImage* image = nil; if (!icon.empty()) image = gfx::SkBitmapToNSImage(icon); string16 title((*it)->GetTitle()); // 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::GetStringUTF16(IDS_TAB_LOADING_TITLE); NSRect linkFrame = NSMakeRect(NSMinX(radioFrame), topLinkY - kLinkLineHeight * row, 200, kLinkHeight); NSButton* button = [self hyperlinkButtonWithFrame:linkFrame title:base::SysUTF16ToNSString(title) icon:image referenceFrame:radioFrame]; [bubble_ addSubview:button]; popupLinks_[button] = *it; } } - (void)awakeFromNib { DCHECK([self window]); DCHECK_EQ(self, [[self window] delegate]); [bubble_ setBubbleType:kWhiteInfoBubble]; [bubble_ setArrowLocation:kTopRight]; [self initializeTitle]; if (allowBlockRadioGroup_) // not bound in cookie bubble xib [self initializeRadioGroup]; if (settingsType_ == CONTENT_SETTINGS_TYPE_POPUPS && tabContents_) [self initializePopupList]; } /////////////////////////////////////////////////////////////////////////////// // Bubble-management related stuff // TODO(thakis): All that junk below should be in some superclass that all the // bubble controllers (bookmark bubble, extension installed bubble, page/browser // action bubble, content blocked bubble) derive from -- http://crbug.com/36366 - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; [super dealloc]; } - (void)parentWindowWillClose:(NSNotification*)notification { [self close]; } - (void)windowWillClose:(NSNotification*)notification { // We caught a close so we don't need to watch for the parent closing. [[NSNotificationCenter defaultCenter] removeObserver:self]; [self autorelease]; } // We want this to be a child of a browser window. addChildWindow: // (called from this function) will bring the window on-screen; // unfortunately, [NSWindowController showWindow:] will also bring it // on-screen (but will cause unexpected changes to the window's // position). We cannot have an addChildWindow: and a subsequent // showWindow:. Thus, we have our own version. - (void)showWindow:(id)sender { NSWindow* window = [self window]; // completes nib load NSPoint origin = [parentWindow_ convertBaseToScreen:anchor_]; origin.x -= NSWidth([window frame]) - kBubbleArrowXOffset - kBubbleArrowWidth / 2; origin.y -= NSHeight([window frame]); [window setFrameOrigin:origin]; [parentWindow_ addChildWindow:window ordered:NSWindowAbove]; [window makeKeyAndOrderFront:self]; } - (void)close { [parentWindow_ removeChildWindow:[self window]]; [super close]; } // The controller is the delegate of the window so it receives did resign key // notifications. When key is resigned mirror Windows behavior and close the // window. - (void)windowDidResignKey:(NSNotification*)notification { NSWindow* window = [self window]; DCHECK_EQ([notification object], window); if ([window isVisible]) { // If the window isn't visible, it is already closed, and this notification // has been sent as part of the closing operation, so no need to close. [self close]; } } // By implementing this, ESC causes the window to go away. - (IBAction)cancel:(id)sender { // This is not a "real" cancel as potential changes to the radio group are not // undone. That's ok. [self close]; } /////////////////////////////////////////////////////////////////////////////// // Actual application logic - (IBAction)allowBlockToggled:(id)sender { NSButtonCell *selectedCell = [sender selectedCell]; ContentSetting setting = [selectedCell tag] == kAllowTag ? CONTENT_SETTING_ALLOW : CONTENT_SETTING_BLOCK; profile_->GetHostContentSettingsMap()->SetContentSetting(host_, settingsType_, setting); } - (IBAction)closeBubble:(id)sender { [self close]; } - (IBAction)manageBlocking:(id)sender { if (tabContents_) { tabContents_->delegate()->ShowContentSettingsWindow(settingsType_); } else { [ContentSettingsDialogController showContentSettingsForType:settingsType_ profile:profile_]; } } - (void)popupLinkClicked:(id)sender { content_blocked_bubble::PopupLinks::iterator i(popupLinks_.find(sender)); DCHECK(i != popupLinks_.end()); if (tabContents_ && tabContents_->blocked_popup_container()) tabContents_->blocked_popup_container()->LaunchPopupForContents(i->second); } @end // ContentBlockedBubbleController