[Metrics] Make MetricsStateManager take a callback param to check if UMA is enabled.
[chromium-blink-merge.git] / chrome / browser / ui / cocoa / website_settings / permission_bubble_controller.mm
blobe42710a02ff96c877652c1ed2cd1309e9a1ca794
1 // Copyright 2014 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/website_settings/permission_bubble_controller.h"
7 #include <algorithm>
9 #include "base/mac/bind_objc_block.h"
10 #include "base/mac/foundation_util.h"
11 #include "base/mac/mac_util.h"
12 #include "base/strings/sys_string_conversions.h"
13 #include "base/strings/utf_string_conversions.h"
14 #include "chrome/browser/ui/browser.h"
15 #include "chrome/browser/ui/browser_finder.h"
16 #import "chrome/browser/ui/chrome_style.h"
17 #import "chrome/browser/ui/cocoa/browser_window_controller.h"
18 #import "chrome/browser/ui/cocoa/browser_window_utils.h"
19 #import "chrome/browser/ui/cocoa/constrained_window/constrained_window_button.h"
20 #import "chrome/browser/ui/cocoa/hover_close_button.h"
21 #import "chrome/browser/ui/cocoa/hyperlink_text_view.h"
22 #import "chrome/browser/ui/cocoa/info_bubble_view.h"
23 #import "chrome/browser/ui/cocoa/info_bubble_window.h"
24 #include "chrome/browser/ui/cocoa/website_settings/permission_bubble_cocoa.h"
25 #include "chrome/browser/ui/cocoa/website_settings/permission_selector_button.h"
26 #include "chrome/browser/ui/cocoa/website_settings/split_block_button.h"
27 #include "chrome/browser/ui/cocoa/website_settings/website_settings_utils_cocoa.h"
28 #include "chrome/browser/ui/website_settings/permission_bubble_request.h"
29 #include "chrome/browser/ui/website_settings/permission_bubble_view.h"
30 #include "chrome/browser/ui/website_settings/permission_menu_model.h"
31 #include "content/public/browser/native_web_keyboard_event.h"
32 #include "content/public/browser/user_metrics.h"
33 #include "grit/generated_resources.h"
34 #include "skia/ext/skia_utils_mac.h"
35 #import "ui/base/cocoa/menu_controller.h"
36 #include "ui/base/cocoa/window_size_constants.h"
37 #import "ui/base/cocoa/menu_controller.h"
38 #include "ui/base/l10n/l10n_util_mac.h"
39 #include "ui/base/models/simple_menu_model.h"
41 using base::UserMetricsAction;
43 namespace {
45 const CGFloat kHorizontalPadding = 20.0f;
46 const CGFloat kVerticalPadding = 20.0f;
47 const CGFloat kButtonPadding = 10.0f;
48 const CGFloat kTitlePaddingX = 50.0f;
49 const CGFloat kTitleFontSize = 15.0f;
50 const CGFloat kPermissionFontSize = 12.0f;
52 class MenuDelegate : public ui::SimpleMenuModel::Delegate {
53  public:
54   explicit MenuDelegate(PermissionBubbleController* bubble)
55       : bubble_controller_(bubble) {}
56   virtual bool IsCommandIdChecked(int command_id) const OVERRIDE {
57     return false;
58   }
59   virtual bool IsCommandIdEnabled(int command_id) const OVERRIDE {
60     return true;
61   }
62   virtual bool GetAcceleratorForCommandId(
63       int command_id,
64       ui::Accelerator* accelerator) OVERRIDE {
65     return false;
66   }
67   virtual void ExecuteCommand(int command_id, int event_flags) OVERRIDE {
68     [bubble_controller_ onMenuItemClicked:command_id];
69   }
70  private:
71   PermissionBubbleController* bubble_controller_;  // Weak, owns us.
72   DISALLOW_COPY_AND_ASSIGN(MenuDelegate);
75 }  // namespace
77 // NSPopUpButton with a menu containing two items: allow and block.
78 // One AllowBlockMenuButton is used for each requested permission, but only when
79 // the permission bubble is in 'customize' mode.
80 @interface AllowBlockMenuButton : NSPopUpButton {
81  @private
82   scoped_ptr<PermissionMenuModel> menuModel_;
83   base::scoped_nsobject<MenuController> menuController_;
86 - (id)initForURL:(const GURL&)url
87          allowed:(BOOL)allow
88            index:(int)index
89         delegate:(PermissionBubbleView::Delegate*)delegate;
90 @end
92 @implementation AllowBlockMenuButton
94 - (id)initForURL:(const GURL&)url
95          allowed:(BOOL)allow
96            index:(int)index
97         delegate:(PermissionBubbleView::Delegate*)delegate {
98   if (self = [super initWithFrame:NSZeroRect pullsDown:NO]) {
99     ContentSetting setting =
100         allow ? CONTENT_SETTING_ALLOW : CONTENT_SETTING_BLOCK;
101     [self setFont:[NSFont systemFontOfSize:kPermissionFontSize]];
102     [self setBordered:NO];
104     __block PermissionBubbleView::Delegate* blockDelegate = delegate;
105     __block AllowBlockMenuButton* blockSelf = self;
106     PermissionMenuModel::ChangeCallback changeCallback =
107         base::BindBlock(^(const WebsiteSettingsUI::PermissionInfo& permission) {
108             blockDelegate->ToggleAccept(
109                 index, permission.setting == CONTENT_SETTING_ALLOW);
110             [blockSelf setFrameSize:
111                 SizeForWebsiteSettingsButtonTitle(blockSelf,
112                                                   [blockSelf title])];
113         });
115     menuModel_.reset(new PermissionMenuModel(url, setting, changeCallback));
116     menuController_.reset([[MenuController alloc] initWithModel:menuModel_.get()
117                                          useWithPopUpButtonCell:NO]);
118     [self setMenu:[menuController_ menu]];
119     [self selectItemAtIndex:menuModel_->GetIndexOfCommandId(setting)];
120     // Although the frame is reset, below, this sizes the cell properly.
121     [self sizeToFit];
122     // Adjust the size to fit the current title.  Using only -sizeToFit leaves
123     // an ugly amount of whitespace between the title and the arrows because it
124     // will fit to the largest element in the menu, not just the selected item.
125     [self setFrameSize:SizeForWebsiteSettingsButtonTitle(self, [self title])];
126   }
127   return self;
130 @end
132 // The window used for the permission bubble controller.
133 // Subclassed to allow browser-handled keyboard events to be passed from the
134 // permission bubble to its parent window, which is a browser window.
135 @interface PermissionBubbleWindow : InfoBubbleWindow
136 @end
138 @implementation PermissionBubbleWindow
139 - (BOOL)performKeyEquivalent:(NSEvent*)event {
140   content::NativeWebKeyboardEvent wrappedEvent(event);
141   if ([BrowserWindowUtils shouldHandleKeyboardEvent:wrappedEvent]) {
142     return [BrowserWindowUtils handleKeyboardEvent:event
143                                           inWindow:[self parentWindow]];
144   }
145   return [super performKeyEquivalent:event];
147 @end
149 @interface PermissionBubbleController ()
151 // Returns an autoreleased NSView displaying the icon and label for |request|.
152 - (NSView*)labelForRequest:(PermissionBubbleRequest*)request;
154 // Returns an autoreleased NSView displaying the title for the bubble
155 // requesting settings for |host|.
156 - (NSView*)titleWithHostname:(const std::string&)host;
158 // Returns an autoreleased NSView displaying a menu for |request|.  The
159 // menu will be initialized as 'allow' if |allow| is YES.
160 - (NSView*)menuForRequest:(PermissionBubbleRequest*)request
161                   atIndex:(int)index
162                     allow:(BOOL)allow;
164 // Returns an autoreleased NSView of a button with |title| and |action|.
165 - (NSView*)buttonWithTitle:(NSString*)title
166                     action:(SEL)action;
168 // Returns an autoreleased NSView displaying a block button.
169 - (NSView*)blockButton;
171 // Returns an autoreleased NSView with a block button and a drop-down menu
172 // with one item, which will change the UI to allow customizing the permissions.
173 - (NSView*)blockButtonWithCustomizeMenu;
175 // Returns an autoreleased NSView displaying the close 'x' button.
176 - (NSView*)closeButton;
178 // Called when the 'ok' button is pressed.
179 - (void)ok:(id)sender;
181 // Called when the 'allow' button is pressed.
182 - (void)onAllow:(id)sender;
184 // Called when the 'block' button is pressed.
185 - (void)onBlock:(id)sender;
187 // Called when the 'close' button is pressed.
188 - (void)onClose:(id)sender;
190 // Called when the 'customize' button is pressed.
191 - (void)onCustomize:(id)sender;
193 // Sets the width of both |viewA| and |viewB| to be the larger of the
194 // two views' widths.  Does not change either view's origin or height.
195 + (CGFloat)matchWidthsOf:(NSView*)viewA andOf:(NSView*)viewB;
197 // Sets the offset of |viewA| so that its vertical center is aligned with the
198 // vertical center of |viewB|.
199 + (void)alignCenterOf:(NSView*)viewA verticallyToCenterOf:(NSView*)viewB;
201 @end
203 @implementation PermissionBubbleController
205 - (id)initWithParentWindow:(NSWindow*)parentWindow
206                     bridge:(PermissionBubbleCocoa*)bridge {
207   DCHECK(parentWindow);
208   DCHECK(bridge);
209   base::scoped_nsobject<PermissionBubbleWindow> window(
210       [[PermissionBubbleWindow alloc]
211           initWithContentRect:ui::kWindowSizeDeterminedLater
212                     styleMask:NSBorderlessWindowMask
213                       backing:NSBackingStoreBuffered
214                         defer:NO]);
215   [window setAllowedAnimations:info_bubble::kAnimateNone];
216   if ((self = [super initWithWindow:window
217                        parentWindow:parentWindow
218                          anchoredAt:NSZeroPoint])) {
219     [self setShouldCloseOnResignKey:NO];
220     [[self bubble] setArrowLocation:info_bubble::kTopLeft];
221     bridge_ = bridge;
222   }
223   return self;
226 - (void)windowWillClose:(NSNotification*)notification {
227   bridge_->OnBubbleClosing();
228   [super windowWillClose:notification];
231 - (void)showAtAnchor:(NSPoint)anchorPoint
232          withDelegate:(PermissionBubbleView::Delegate*)delegate
233           forRequests:(const std::vector<PermissionBubbleRequest*>&)requests
234          acceptStates:(const std::vector<bool>&)acceptStates
235     customizationMode:(BOOL)customizationMode {
236   DCHECK(!requests.empty());
237   DCHECK(delegate);
238   DCHECK(!customizationMode || (requests.size() == acceptStates.size()));
239   delegate_ = delegate;
241   NSView* contentView = [[self window] contentView];
242   [contentView setSubviews:@[]];
244   // Create one button to use as a guide for the permissions' y-offsets.
245   base::scoped_nsobject<NSView> allowOrOkButton;
246   if (customizationMode) {
247     NSString* okTitle = l10n_util::GetNSString(IDS_OK);
248     allowOrOkButton.reset([[self buttonWithTitle:okTitle
249                                           action:@selector(ok:)] retain]);
250   } else {
251     NSString* allowTitle = l10n_util::GetNSString(IDS_PERMISSION_ALLOW);
252     allowOrOkButton.reset([[self buttonWithTitle:allowTitle
253                                           action:@selector(onAllow:)] retain]);
254   }
255   CGFloat yOffset = 2 * kVerticalPadding + NSMaxY([allowOrOkButton frame]);
256   BOOL singlePermission = requests.size() == 1;
258   base::scoped_nsobject<NSMutableArray> permissionMenus;
259   if (customizationMode)
260     permissionMenus.reset([[NSMutableArray alloc] init]);
262   for (auto it = requests.begin(); it != requests.end(); it++) {
263     base::scoped_nsobject<NSView> permissionView(
264         [[self labelForRequest:(*it)] retain]);
265     NSPoint origin = [permissionView frame].origin;
266     origin.x += kHorizontalPadding;
267     origin.y += yOffset;
268     [permissionView setFrameOrigin:origin];
269     [contentView addSubview:permissionView];
271     if (customizationMode) {
272       int index = it - requests.begin();
273       base::scoped_nsobject<NSView> menu(
274           [[self menuForRequest:(*it)
275                         atIndex:index
276                           allow:acceptStates[index] ? YES : NO] retain]);
277       // Align vertically.  Horizontal alignment will be adjusted once the
278       // widest permission is know.
279       [PermissionBubbleController alignCenterOf:menu
280                            verticallyToCenterOf:permissionView];
281       [permissionMenus addObject:menu];
282       [contentView addSubview:menu];
283     }
284     yOffset += NSHeight([permissionView frame]);
285   }
287   // The maximum width of the above permissions will dictate the width of the
288   // bubble.  It is calculated here so that it can be used for the positioning
289   // of the buttons.
290   NSRect bubbleFrame = NSZeroRect;
291   for (NSView* view in [contentView subviews]) {
292     bubbleFrame = NSUnionRect(
293         bubbleFrame, NSInsetRect([view frame], -kHorizontalPadding, 0));
294   }
296   if (customizationMode) {
297     // Adjust the horizontal origin for each menu.
298     CGFloat xOffset = NSWidth(bubbleFrame) - kHorizontalPadding;
299     CGFloat maxMenuWidth = 0;
300     for (NSView* view in permissionMenus.get()) {
301       [view setFrameOrigin:NSMakePoint(xOffset, NSMinY([view frame]))];
302       maxMenuWidth = std::max(maxMenuWidth, NSWidth([view frame]));
303     }
304     // And add the menu width to the bubble's width.
305     bubbleFrame.size.width += maxMenuWidth;
306   }
308   base::scoped_nsobject<NSView> titleView(
309       [[self titleWithHostname:requests[0]->GetRequestingHostname().host()]
310           retain]);
311   [contentView addSubview:titleView];
312   [titleView setFrameOrigin:NSMakePoint(kHorizontalPadding,
313                                         kVerticalPadding + yOffset)];
314   yOffset += NSHeight([titleView frame]) + kVerticalPadding;
316   // The title must fit within the bubble.
317   bubbleFrame.size.width = std::max(NSWidth(bubbleFrame),
318                                     NSWidth([titleView frame]));
320   // 'x' button in the upper-right-hand corner.
321   base::scoped_nsobject<NSView> closeButton([[self closeButton] retain]);
322   // Place the close button at the rightmost edge of the bubble.
323   [closeButton setFrameOrigin:NSMakePoint(
324       NSMaxX(bubbleFrame), yOffset - chrome_style::kCloseButtonPadding)];
325   // Increase the size of the bubble by the width of the close button and its
326   // padding.
327   bubbleFrame.size.width +=
328       NSWidth([closeButton frame]) + chrome_style::kCloseButtonPadding;
329   [contentView addSubview:closeButton];
331   // Position the allow/ok button.
332   CGFloat xOrigin = NSWidth(bubbleFrame) - NSWidth([allowOrOkButton frame]) -
333       kHorizontalPadding;
334   [allowOrOkButton setFrameOrigin:NSMakePoint(xOrigin, kVerticalPadding)];
335   [contentView addSubview:allowOrOkButton];
337   if (!customizationMode) {
338     base::scoped_nsobject<NSView> blockButton;
339     if (singlePermission)
340       blockButton.reset([[self blockButton] retain]);
341     else
342       blockButton.reset([[self blockButtonWithCustomizeMenu] retain]);
343     CGFloat width = [PermissionBubbleController matchWidthsOf:blockButton
344                                                         andOf:allowOrOkButton];
345     // Ensure the allow/ok button is still in the correct position.
346     xOrigin = NSWidth(bubbleFrame) - width - kHorizontalPadding;
347     [allowOrOkButton setFrameOrigin:NSMakePoint(xOrigin, kVerticalPadding)];
348     // Line up the block button.
349     xOrigin = NSMinX([allowOrOkButton frame]) - width - kButtonPadding;
350     [blockButton setFrameOrigin:NSMakePoint(xOrigin, kVerticalPadding)];
351     [contentView addSubview:blockButton];
352   }
354   bubbleFrame.size.height = yOffset + kVerticalPadding;
355   bubbleFrame = [[self window] frameRectForContentRect:bubbleFrame];
357   if ([[self window] isVisible]) {
358     // Unfortunately, calling -setFrame followed by -setFrameOrigin  (called
359     // within -setAnchorPoint) causes flickering.  Avoid the flickering by
360     // manually adjusting the new frame's origin so that the top left stays the
361     // same, and only calling -setFrame.
362     NSRect currentWindowFrame = [[self window] frame];
363     bubbleFrame.origin = currentWindowFrame.origin;
364     bubbleFrame.origin.y = bubbleFrame.origin.y +
365         currentWindowFrame.size.height - bubbleFrame.size.height;
366     [[self window] setFrame:bubbleFrame display:YES];
367   } else {
368     [[self window] setFrame:bubbleFrame display:NO];
369     [self setAnchorPoint:anchorPoint];
370     [self showWindow:nil];
371   }
374 - (NSView*)labelForRequest:(PermissionBubbleRequest*)request {
375   DCHECK(request);
376   base::scoped_nsobject<NSView> permissionView(
377       [[NSView alloc] initWithFrame:NSZeroRect]);
378   base::scoped_nsobject<NSImageView> permissionIcon(
379       [[NSImageView alloc] initWithFrame:NSZeroRect]);
380   [permissionIcon setImage:ui::ResourceBundle::GetSharedInstance().
381       GetNativeImageNamed(request->GetIconID()).ToNSImage()];
382   [permissionIcon setFrameSize:[[permissionIcon image] size]];
383   [permissionView addSubview:permissionIcon];
385   base::scoped_nsobject<NSTextField> permissionLabel(
386       [[NSTextField alloc] initWithFrame:NSZeroRect]);
387   base::string16 label = request->GetMessageTextFragment();
388   [permissionLabel setDrawsBackground:NO];
389   [permissionLabel setBezeled:NO];
390   [permissionLabel setEditable:NO];
391   [permissionLabel setSelectable:NO];
392   [permissionLabel setFont:[NSFont systemFontOfSize:kPermissionFontSize]];
393   [permissionLabel setStringValue:base::SysUTF16ToNSString(label)];
394   [permissionLabel sizeToFit];
395   [permissionLabel setFrameOrigin:
396       NSMakePoint(NSWidth([permissionIcon frame]), 0)];
397   [permissionView addSubview:permissionLabel];
399   // Match the horizontal centers of the two subviews.  Note that the label's
400   // center is rounded down, and the icon's center, up.  It looks better that
401   // way - with the text's center slightly lower than the icon's center - if the
402   // height delta is not evenly split.
403   NSRect iconFrame = [permissionIcon frame];
404   NSRect labelFrame = [permissionLabel frame];
405   NSRect unionFrame = NSUnionRect(iconFrame, labelFrame);
407   iconFrame.origin.y =
408       std::ceil((NSHeight(unionFrame) - NSHeight(iconFrame)) / 2);
409   labelFrame.origin.y =
410       std::floor((NSHeight(unionFrame) - NSHeight(labelFrame)) / 2);
412   [permissionLabel setFrame:labelFrame];
413   [permissionIcon setFrame:iconFrame];
414   [permissionView setFrame:unionFrame];
416   return permissionView.autorelease();
419 - (NSView*)titleWithHostname:(const std::string&)host {
420   base::scoped_nsobject<NSTextField> titleView(
421       [[NSTextField alloc] initWithFrame:NSZeroRect]);
422   [titleView setDrawsBackground:NO];
423   [titleView setBezeled:NO];
424   [titleView setEditable:NO];
425   [titleView setSelectable:NO];
426   [titleView setStringValue:
427       l10n_util::GetNSStringF(IDS_PERMISSIONS_BUBBLE_PROMPT,
428                               base::UTF8ToUTF16(host))];
429   [titleView setFont:[NSFont systemFontOfSize:kTitleFontSize]];
430   [titleView sizeToFit];
431   NSRect titleFrame = [titleView frame];
432   [titleView setFrameSize:NSMakeSize(NSWidth(titleFrame) + kTitlePaddingX,
433                                      NSHeight(titleFrame))];
434   return titleView.autorelease();
437 - (NSView*)menuForRequest:(PermissionBubbleRequest*)request
438                   atIndex:(int)index
439                     allow:(BOOL)allow {
440   DCHECK(request);
441   DCHECK(delegate_);
442   base::scoped_nsobject<AllowBlockMenuButton> button(
443       [[AllowBlockMenuButton alloc] initForURL:request->GetRequestingHostname()
444                                        allowed:allow
445                                          index:index
446                                       delegate:delegate_]);
447   return button.autorelease();
450 - (NSView*)buttonWithTitle:(NSString*)title
451                     action:(SEL)action {
452   base::scoped_nsobject<NSButton> button(
453       [[ConstrainedWindowButton alloc] initWithFrame:NSZeroRect]);
454   [button setButtonType:NSMomentaryPushInButton];
455   [button setTitle:title];
456   [button setTarget:self];
457   [button setAction:action];
458   [button sizeToFit];
459   return button.autorelease();
462 - (NSView*)blockButton {
463   NSString* blockTitle = l10n_util::GetNSString(IDS_PERMISSION_DENY);
464   return [self buttonWithTitle:blockTitle
465                         action:@selector(onBlock:)];
468 - (NSView*)blockButtonWithCustomizeMenu {
469   menuDelegate_.reset(new MenuDelegate(self));
470   base::scoped_nsobject<SplitBlockButton> blockButton([[SplitBlockButton alloc]
471       initWithMenuDelegate:menuDelegate_.get()]);
472   [blockButton sizeToFit];
473   [blockButton setEnabled:YES];
474   [blockButton setAction:@selector(onBlock:)];
475   [blockButton setTarget:self];
476   return blockButton.autorelease();
479 - (NSView*)closeButton {
480   int dimension = chrome_style::GetCloseButtonSize();
481   NSRect frame = NSMakeRect(0, 0, dimension, dimension);
482   base::scoped_nsobject<NSButton> button(
483       [[WebUIHoverCloseButton alloc] initWithFrame:frame]);
484   [button setAction:@selector(onClose:)];
485   [button setTarget:self];
486   return button.autorelease();
489 - (void)ok:(id)sender {
490   DCHECK(delegate_);
491   delegate_->Closing();
494 - (void)onAllow:(id)sender {
495   DCHECK(delegate_);
496   delegate_->Accept();
499 - (void)onBlock:(id)sender {
500   DCHECK(delegate_);
501   delegate_->Deny();
504 - (void)onClose:(id)sender {
505   DCHECK(delegate_);
506   delegate_->Closing();
509 - (void)onCustomize:(id)sender {
510   DCHECK(delegate_);
511   delegate_->SetCustomizationMode();
514 - (void)onMenuItemClicked:(int)commandId {
515   DCHECK(commandId == 0);
516   [self onCustomize:nil];
519 - (void)activateTabWithContents:(content::WebContents*)newContents
520                previousContents:(content::WebContents*)oldContents
521                         atIndex:(NSInteger)index
522                          reason:(int)reason {
523   // The show/hide of this bubble is handled by the PermissionBubbleManager.
524   // So bypass the base class, which would close the bubble here.
527 + (CGFloat)matchWidthsOf:(NSView*)viewA andOf:(NSView*)viewB {
528   NSRect frameA = [viewA frame];
529   NSRect frameB = [viewB frame];
530   CGFloat width = std::max(NSWidth(frameA), NSWidth(frameB));
531   [viewA setFrameSize:NSMakeSize(width, NSHeight(frameA))];
532   [viewB setFrameSize:NSMakeSize(width, NSHeight(frameB))];
533   return width;
536 + (void)alignCenterOf:(NSView*)viewA verticallyToCenterOf:(NSView*)viewB {
537   NSRect frameA = [viewA frame];
538   NSRect frameB = [viewB frame];
539   frameA.origin.y =
540       NSMinY(frameB) + std::floor((NSHeight(frameB) - NSHeight(frameA)) / 2);
541   [viewA setFrameOrigin:frameA.origin];
544 @end  // implementation PermissionBubbleController