Home | History | Annotate | Download | only in content_settings
      1 // Copyright (c) 2010 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 #import "chrome/browser/ui/cocoa/content_settings/content_setting_bubble_cocoa.h"
      6 
      7 #include "base/command_line.h"
      8 #include "base/logging.h"
      9 #include "base/sys_string_conversions.h"
     10 #include "base/utf_string_conversions.h"
     11 #include "chrome/browser/blocked_content_container.h"
     12 #include "chrome/browser/content_setting_bubble_model.h"
     13 #include "chrome/browser/content_settings/host_content_settings_map.h"
     14 #include "chrome/browser/plugin_updater.h"
     15 #import "chrome/browser/ui/cocoa/hyperlink_button_cell.h"
     16 #import "chrome/browser/ui/cocoa/info_bubble_view.h"
     17 #import "chrome/browser/ui/cocoa/l10n_util.h"
     18 #include "grit/generated_resources.h"
     19 #include "skia/ext/skia_utils_mac.h"
     20 #import "third_party/GTM/AppKit/GTMUILocalizerAndLayoutTweaker.h"
     21 #include "ui/base/l10n/l10n_util.h"
     22 #include "webkit/glue/plugins/plugin_list.h"
     23 
     24 namespace {
     25 
     26 // Must match the tag of the unblock radio button in the xib files.
     27 const int kAllowTag = 1;
     28 
     29 // Must match the tag of the block radio button in the xib files.
     30 const int kBlockTag = 2;
     31 
     32 // Height of one link in the popup list.
     33 const int kLinkHeight = 16;
     34 
     35 // Space between two popup links.
     36 const int kLinkPadding = 4;
     37 
     38 // Space taken in total by one popup link.
     39 const int kLinkLineHeight = kLinkHeight + kLinkPadding;
     40 
     41 // Space between popup list and surrounding UI elements.
     42 const int kLinkOuterPadding = 8;
     43 
     44 // Height of each of the labels in the geolocation bubble.
     45 const int kGeoLabelHeight = 14;
     46 
     47 // Height of the "Clear" button in the geolocation bubble.
     48 const int kGeoClearButtonHeight = 17;
     49 
     50 // Padding between radio buttons and "Load all plugins" button
     51 // in the plugin bubble.
     52 const int kLoadAllPluginsButtonVerticalPadding = 8;
     53 
     54 // General padding between elements in the geolocation bubble.
     55 const int kGeoPadding = 8;
     56 
     57 // Padding between host names in the geolocation bubble.
     58 const int kGeoHostPadding = 4;
     59 
     60 // Minimal padding between "Manage" and "Done" buttons.
     61 const int kManageDonePadding = 8;
     62 
     63 void SetControlSize(NSControl* control, NSControlSize controlSize) {
     64   CGFloat fontSize = [NSFont systemFontSizeForControlSize:controlSize];
     65   NSCell* cell = [control cell];
     66   NSFont* font = [NSFont fontWithName:[[cell font] fontName] size:fontSize];
     67   [cell setFont:font];
     68   [cell setControlSize:controlSize];
     69 }
     70 
     71 // Returns an autoreleased NSTextField that is configured to look like a Label
     72 // looks in Interface Builder.
     73 NSTextField* LabelWithFrame(NSString* text, const NSRect& frame) {
     74   NSTextField* label = [[NSTextField alloc] initWithFrame:frame];
     75   [label setStringValue:text];
     76   [label setSelectable:NO];
     77   [label setBezeled:NO];
     78   return [label autorelease];
     79 }
     80 
     81 }  // namespace
     82 
     83 @interface ContentSettingBubbleController(Private)
     84 - (id)initWithModel:(ContentSettingBubbleModel*)settingsBubbleModel
     85        parentWindow:(NSWindow*)parentWindow
     86          anchoredAt:(NSPoint)anchoredAt;
     87 - (NSButton*)hyperlinkButtonWithFrame:(NSRect)frame
     88                                 title:(NSString*)title
     89                                  icon:(NSImage*)icon
     90                        referenceFrame:(NSRect)referenceFrame;
     91 - (void)initializeBlockedPluginsList;
     92 - (void)initializeTitle;
     93 - (void)initializeRadioGroup;
     94 - (void)initializePopupList;
     95 - (void)initializeGeoLists;
     96 - (void)sizeToFitLoadPluginsButton;
     97 - (void)sizeToFitManageDoneButtons;
     98 - (void)removeInfoButton;
     99 - (void)popupLinkClicked:(id)sender;
    100 - (void)clearGeolocationForCurrentHost:(id)sender;
    101 @end
    102 
    103 @implementation ContentSettingBubbleController
    104 
    105 + (ContentSettingBubbleController*)
    106     showForModel:(ContentSettingBubbleModel*)contentSettingBubbleModel
    107     parentWindow:(NSWindow*)parentWindow
    108       anchoredAt:(NSPoint)anchor {
    109   // Autoreleases itself on bubble close.
    110   return [[ContentSettingBubbleController alloc]
    111              initWithModel:contentSettingBubbleModel
    112               parentWindow:parentWindow
    113                 anchoredAt:anchor];
    114 }
    115 
    116 - (id)initWithModel:(ContentSettingBubbleModel*)contentSettingBubbleModel
    117        parentWindow:(NSWindow*)parentWindow
    118          anchoredAt:(NSPoint)anchoredAt {
    119   // This method takes ownership of |contentSettingBubbleModel| in all cases.
    120   scoped_ptr<ContentSettingBubbleModel> model(contentSettingBubbleModel);
    121   DCHECK(model.get());
    122 
    123   NSString* const nibPaths[] = {
    124     @"ContentBlockedCookies",
    125     @"ContentBlockedImages",
    126     @"ContentBlockedJavaScript",
    127     @"ContentBlockedPlugins",
    128     @"ContentBlockedPopups",
    129     @"ContentBubbleGeolocation",
    130     @"",  // Notifications do not have a bubble.
    131     @"",  // Prerender does not have a bubble.
    132   };
    133   COMPILE_ASSERT(arraysize(nibPaths) == CONTENT_SETTINGS_NUM_TYPES,
    134                  nibPaths_requires_an_entry_for_every_setting_type);
    135   const int settingsType = model->content_type();
    136   // Nofifications do not have a bubble.
    137   CHECK_NE(settingsType, CONTENT_SETTINGS_TYPE_NOTIFICATIONS);
    138   DCHECK_LT(settingsType, CONTENT_SETTINGS_NUM_TYPES);
    139   if ((self = [super initWithWindowNibPath:nibPaths[settingsType]
    140                               parentWindow:parentWindow
    141                                 anchoredAt:anchoredAt])) {
    142     contentSettingBubbleModel_.reset(model.release());
    143     [self showWindow:nil];
    144   }
    145   return self;
    146 }
    147 
    148 - (void)initializeTitle {
    149   if (!titleLabel_)
    150     return;
    151 
    152   NSString* label = base::SysUTF8ToNSString(
    153       contentSettingBubbleModel_->bubble_content().title);
    154   [titleLabel_ setStringValue:label];
    155 
    156   // Layout title post-localization.
    157   CGFloat deltaY = [GTMUILocalizerAndLayoutTweaker
    158       sizeToFitFixedWidthTextField:titleLabel_];
    159   NSRect windowFrame = [[self window] frame];
    160   windowFrame.size.height += deltaY;
    161   [[self window] setFrame:windowFrame display:NO];
    162   NSRect titleFrame = [titleLabel_ frame];
    163   titleFrame.origin.y -= deltaY;
    164   [titleLabel_ setFrame:titleFrame];
    165 }
    166 
    167 - (void)initializeRadioGroup {
    168   // Configure the radio group. For now, only deal with the
    169   // strictly needed case of group containing 2 radio buttons.
    170   const ContentSettingBubbleModel::RadioGroup& radio_group =
    171       contentSettingBubbleModel_->bubble_content().radio_group;
    172 
    173   // Select appropriate radio button.
    174   [allowBlockRadioGroup_ selectCellWithTag:
    175       radio_group.default_item == 0 ? kAllowTag : kBlockTag];
    176 
    177   const ContentSettingBubbleModel::RadioItems& radio_items =
    178       radio_group.radio_items;
    179   DCHECK_EQ(2u, radio_items.size()) << "Only 2 radio items per group supported";
    180   // Set radio group labels from model.
    181   NSCell* radioCell = [allowBlockRadioGroup_ cellWithTag:kAllowTag];
    182   [radioCell setTitle:base::SysUTF8ToNSString(radio_items[0])];
    183 
    184   radioCell = [allowBlockRadioGroup_ cellWithTag:kBlockTag];
    185   [radioCell setTitle:base::SysUTF8ToNSString(radio_items[1])];
    186 
    187   // Layout radio group labels post-localization.
    188   [GTMUILocalizerAndLayoutTweaker
    189       wrapRadioGroupForWidth:allowBlockRadioGroup_];
    190   CGFloat radioDeltaY = [GTMUILocalizerAndLayoutTweaker
    191       sizeToFitView:allowBlockRadioGroup_].height;
    192   NSRect windowFrame = [[self window] frame];
    193   windowFrame.size.height += radioDeltaY;
    194   [[self window] setFrame:windowFrame display:NO];
    195 }
    196 
    197 - (NSButton*)hyperlinkButtonWithFrame:(NSRect)frame
    198                                 title:(NSString*)title
    199                                  icon:(NSImage*)icon
    200                        referenceFrame:(NSRect)referenceFrame {
    201   scoped_nsobject<HyperlinkButtonCell> cell([[HyperlinkButtonCell alloc]
    202       initTextCell:title]);
    203   [cell.get() setAlignment:NSNaturalTextAlignment];
    204   if (icon) {
    205     [cell.get() setImagePosition:NSImageLeft];
    206     [cell.get() setImage:icon];
    207   } else {
    208     [cell.get() setImagePosition:NSNoImage];
    209   }
    210   [cell.get() setControlSize:NSSmallControlSize];
    211 
    212   NSButton* button = [[[NSButton alloc] initWithFrame:frame] autorelease];
    213   // Cell must be set immediately after construction.
    214   [button setCell:cell.get()];
    215 
    216   // If the link text is too long, clamp it.
    217   [button sizeToFit];
    218   int maxWidth = NSWidth([[self bubble] frame]) - 2 * NSMinX(referenceFrame);
    219   NSRect buttonFrame = [button frame];
    220   if (NSWidth(buttonFrame) > maxWidth) {
    221     buttonFrame.size.width = maxWidth;
    222     [button setFrame:buttonFrame];
    223   }
    224 
    225   [button setTarget:self];
    226   [button setAction:@selector(popupLinkClicked:)];
    227   return button;
    228 }
    229 
    230 - (void)initializeBlockedPluginsList {
    231   NSMutableArray* pluginArray = [NSMutableArray array];
    232   const std::set<std::string>& plugins =
    233       contentSettingBubbleModel_->bubble_content().resource_identifiers;
    234   if (plugins.empty()) {
    235     int delta = NSMinY([titleLabel_ frame]) -
    236                 NSMinY([blockedResourcesField_ frame]);
    237     [blockedResourcesField_ removeFromSuperview];
    238     NSRect frame = [[self window] frame];
    239     frame.size.height -= delta;
    240     [[self window] setFrame:frame display:NO];
    241   } else {
    242     for (std::set<std::string>::iterator it = plugins.begin();
    243          it != plugins.end(); ++it) {
    244       NSString* name = SysUTF16ToNSString(
    245           NPAPI::PluginList::Singleton()->GetPluginGroupName(*it));
    246       if ([name length] == 0)
    247         name = base::SysUTF8ToNSString(*it);
    248       [pluginArray addObject:name];
    249     }
    250     [blockedResourcesField_
    251         setStringValue:[pluginArray componentsJoinedByString:@"\n"]];
    252     [GTMUILocalizerAndLayoutTweaker
    253         sizeToFitFixedWidthTextField:blockedResourcesField_];
    254   }
    255 }
    256 
    257 - (void)initializePopupList {
    258   // I didn't put the buttons into a NSMatrix because then they are only one
    259   // entity in the key view loop. This way, one can tab through all of them.
    260   const ContentSettingBubbleModel::PopupItems& popupItems =
    261       contentSettingBubbleModel_->bubble_content().popup_items;
    262 
    263   // Get the pre-resize frame of the radio group. Its origin is where the
    264   // popup list should go.
    265   NSRect radioFrame = [allowBlockRadioGroup_ frame];
    266 
    267   // Make room for the popup list. The bubble view and its subviews autosize
    268   // themselves when the window is enlarged.
    269   // Heading and radio box are already 1 * kLinkOuterPadding apart in the nib,
    270   // so only 1 * kLinkOuterPadding more is needed.
    271   int delta = popupItems.size() * kLinkLineHeight - kLinkPadding +
    272               kLinkOuterPadding;
    273   NSSize deltaSize = NSMakeSize(0, delta);
    274   deltaSize = [[[self window] contentView] convertSize:deltaSize toView:nil];
    275   NSRect windowFrame = [[self window] frame];
    276   windowFrame.size.height += deltaSize.height;
    277   [[self window] setFrame:windowFrame display:NO];
    278 
    279   // Create popup list.
    280   int topLinkY = NSMaxY(radioFrame) + delta - kLinkHeight;
    281   int row = 0;
    282   for (std::vector<ContentSettingBubbleModel::PopupItem>::const_iterator
    283        it(popupItems.begin()); it != popupItems.end(); ++it, ++row) {
    284     const SkBitmap& icon = it->bitmap;
    285     NSImage* image = nil;
    286     if (!icon.empty())
    287       image = gfx::SkBitmapToNSImage(icon);
    288 
    289     std::string title(it->title);
    290     // The popup may not have committed a load yet, in which case it won't
    291     // have a URL or title.
    292     if (title.empty())
    293       title = l10n_util::GetStringUTF8(IDS_TAB_LOADING_TITLE);
    294 
    295     NSRect linkFrame =
    296         NSMakeRect(NSMinX(radioFrame), topLinkY - kLinkLineHeight * row,
    297                    200, kLinkHeight);
    298     NSButton* button = [self
    299         hyperlinkButtonWithFrame:linkFrame
    300                            title:base::SysUTF8ToNSString(title)
    301                             icon:image
    302                   referenceFrame:radioFrame];
    303     [[self bubble] addSubview:button];
    304     popupLinks_[button] = row;
    305   }
    306 }
    307 
    308 - (void)initializeGeoLists {
    309   // Cocoa has its origin in the lower left corner. This means elements are
    310   // added from bottom to top, which explains why loops run backwards and the
    311   // order of operations is the other way than on Linux/Windows.
    312   const ContentSettingBubbleModel::BubbleContent& content =
    313       contentSettingBubbleModel_->bubble_content();
    314   NSRect containerFrame = [contentsContainer_ frame];
    315   NSRect frame = NSMakeRect(0, 0, NSWidth(containerFrame), kGeoLabelHeight);
    316 
    317   // "Clear" button / text field.
    318   if (!content.custom_link.empty()) {
    319     scoped_nsobject<NSControl> control;
    320     if(content.custom_link_enabled) {
    321       NSRect buttonFrame = NSMakeRect(0, 0,
    322                                       NSWidth(containerFrame),
    323                                       kGeoClearButtonHeight);
    324       NSButton* button = [[NSButton alloc] initWithFrame:buttonFrame];
    325       control.reset(button);
    326       [button setTitle:base::SysUTF8ToNSString(content.custom_link)];
    327       [button setTarget:self];
    328       [button setAction:@selector(clearGeolocationForCurrentHost:)];
    329       [button setBezelStyle:NSRoundRectBezelStyle];
    330       SetControlSize(button, NSSmallControlSize);
    331       [button sizeToFit];
    332     } else {
    333       // Add the notification that settings will be cleared on next reload.
    334       control.reset([LabelWithFrame(
    335           base::SysUTF8ToNSString(content.custom_link), frame) retain]);
    336       SetControlSize(control.get(), NSSmallControlSize);
    337     }
    338 
    339     // If the new control is wider than the container, widen the window.
    340     CGFloat controlWidth = NSWidth([control frame]);
    341     if (controlWidth > NSWidth(containerFrame)) {
    342       NSRect windowFrame = [[self window] frame];
    343       windowFrame.size.width += controlWidth - NSWidth(containerFrame);
    344       [[self window] setFrame:windowFrame display:NO];
    345       // Fetch the updated sizes.
    346       containerFrame = [contentsContainer_ frame];
    347       frame = NSMakeRect(0, 0, NSWidth(containerFrame), kGeoLabelHeight);
    348     }
    349 
    350     DCHECK(control);
    351     [contentsContainer_ addSubview:control];
    352     frame.origin.y = NSMaxY([control frame]) + kGeoPadding;
    353   }
    354 
    355   typedef
    356       std::vector<ContentSettingBubbleModel::DomainList>::const_reverse_iterator
    357       GeolocationGroupIterator;
    358   for (GeolocationGroupIterator i = content.domain_lists.rbegin();
    359        i != content.domain_lists.rend(); ++i) {
    360     // Add all hosts in the current domain list.
    361     for (std::set<std::string>::const_reverse_iterator j = i->hosts.rbegin();
    362          j != i->hosts.rend(); ++j) {
    363       NSTextField* title = LabelWithFrame(base::SysUTF8ToNSString(*j), frame);
    364       SetControlSize(title, NSSmallControlSize);
    365       [contentsContainer_ addSubview:title];
    366 
    367       frame.origin.y = NSMaxY(frame) + kGeoHostPadding +
    368           [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:title];
    369     }
    370     if (!i->hosts.empty())
    371       frame.origin.y += kGeoPadding - kGeoHostPadding;
    372 
    373     // Add the domain list's title.
    374     NSTextField* title =
    375         LabelWithFrame(base::SysUTF8ToNSString(i->title), frame);
    376     SetControlSize(title, NSSmallControlSize);
    377     [contentsContainer_ addSubview:title];
    378 
    379     frame.origin.y = NSMaxY(frame) + kGeoPadding +
    380         [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:title];
    381   }
    382 
    383   CGFloat containerHeight = frame.origin.y;
    384   // Undo last padding.
    385   if (!content.domain_lists.empty())
    386     containerHeight -= kGeoPadding;
    387 
    388   // Resize container to fit its subviews, and window to fit the container.
    389   NSRect windowFrame = [[self window] frame];
    390   windowFrame.size.height += containerHeight - NSHeight(containerFrame);
    391   [[self window] setFrame:windowFrame display:NO];
    392   containerFrame.size.height = containerHeight;
    393   [contentsContainer_ setFrame:containerFrame];
    394 }
    395 
    396 - (void)sizeToFitLoadPluginsButton {
    397   const ContentSettingBubbleModel::BubbleContent& content =
    398       contentSettingBubbleModel_->bubble_content();
    399   [loadAllPluginsButton_ setEnabled:content.custom_link_enabled];
    400 
    401   // Resize horizontally to fit button if necessary.
    402   NSRect windowFrame = [[self window] frame];
    403   int widthNeeded = NSWidth([loadAllPluginsButton_ frame]) +
    404       2 * NSMinX([loadAllPluginsButton_ frame]);
    405   if (NSWidth(windowFrame) < widthNeeded) {
    406     windowFrame.size.width = widthNeeded;
    407     [[self window] setFrame:windowFrame display:NO];
    408   }
    409 }
    410 
    411 - (void)sizeToFitManageDoneButtons {
    412   CGFloat actualWidth = NSWidth([[[self window] contentView] frame]);
    413   CGFloat requiredWidth = NSMaxX([manageButton_ frame]) + kManageDonePadding +
    414       NSWidth([[doneButton_ superview] frame]) - NSMinX([doneButton_ frame]);
    415   if (requiredWidth <= actualWidth || !doneButton_ || !manageButton_)
    416     return;
    417 
    418   // Resize window, autoresizing takes care of the rest.
    419   NSSize size = NSMakeSize(requiredWidth - actualWidth, 0);
    420   size = [[[self window] contentView] convertSize:size toView:nil];
    421   NSRect frame = [[self window] frame];
    422   frame.origin.x -= size.width;
    423   frame.size.width += size.width;
    424   [[self window] setFrame:frame display:NO];
    425 }
    426 
    427 - (void)awakeFromNib {
    428   [super awakeFromNib];
    429 
    430   [[self bubble] setArrowLocation:info_bubble::kTopRight];
    431 
    432   // Adapt window size to bottom buttons. Do this before all other layouting.
    433   [self sizeToFitManageDoneButtons];
    434 
    435   [self initializeTitle];
    436 
    437   ContentSettingsType type = contentSettingBubbleModel_->content_type();
    438   if (type == CONTENT_SETTINGS_TYPE_PLUGINS) {
    439     [self sizeToFitLoadPluginsButton];
    440     [self initializeBlockedPluginsList];
    441   }
    442   if (allowBlockRadioGroup_)  // not bound in cookie bubble xib
    443     [self initializeRadioGroup];
    444 
    445   if (type == CONTENT_SETTINGS_TYPE_POPUPS)
    446     [self initializePopupList];
    447   if (type == CONTENT_SETTINGS_TYPE_GEOLOCATION)
    448     [self initializeGeoLists];
    449 }
    450 
    451 ///////////////////////////////////////////////////////////////////////////////
    452 // Actual application logic
    453 
    454 - (IBAction)allowBlockToggled:(id)sender {
    455   NSButtonCell *selectedCell = [sender selectedCell];
    456   contentSettingBubbleModel_->OnRadioClicked(
    457       [selectedCell tag] == kAllowTag ? 0 : 1);
    458 }
    459 
    460 - (void)popupLinkClicked:(id)sender {
    461   content_setting_bubble::PopupLinks::iterator i(popupLinks_.find(sender));
    462   DCHECK(i != popupLinks_.end());
    463   contentSettingBubbleModel_->OnPopupClicked(i->second);
    464 }
    465 
    466 - (void)clearGeolocationForCurrentHost:(id)sender {
    467   contentSettingBubbleModel_->OnCustomLinkClicked();
    468   [self close];
    469 }
    470 
    471 - (IBAction)showMoreInfo:(id)sender {
    472   contentSettingBubbleModel_->OnCustomLinkClicked();
    473   [self close];
    474 }
    475 
    476 - (IBAction)loadAllPlugins:(id)sender {
    477   contentSettingBubbleModel_->OnCustomLinkClicked();
    478   [self close];
    479 }
    480 
    481 - (IBAction)manageBlocking:(id)sender {
    482   contentSettingBubbleModel_->OnManageLinkClicked();
    483 }
    484 
    485 - (IBAction)closeBubble:(id)sender {
    486   [self close];
    487 }
    488 
    489 @end  // ContentSettingBubbleController
    490