Add ENABLE_MEDIA_ROUTER define to builds other than Android and iOS.
[chromium-blink-merge.git] / chrome / browser / ui / cocoa / website_settings / permission_bubble_controller.mm
blob5e74eff1e3115ca8e6e6a56bc28e5a887995c709
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/strings/sys_string_conversions.h"
11 #include "base/strings/utf_string_conversions.h"
12 #include "chrome/browser/ui/browser.h"
13 #include "chrome/browser/ui/browser_finder.h"
14 #import "chrome/browser/ui/chrome_style.h"
15 #import "chrome/browser/ui/cocoa/browser_window_controller.h"
16 #import "chrome/browser/ui/cocoa/browser_window_utils.h"
17 #import "chrome/browser/ui/cocoa/constrained_window/constrained_window_button.h"
18 #import "chrome/browser/ui/cocoa/hover_close_button.h"
19 #import "chrome/browser/ui/cocoa/info_bubble_view.h"
20 #import "chrome/browser/ui/cocoa/info_bubble_window.h"
21 #include "chrome/browser/ui/cocoa/website_settings/permission_bubble_cocoa.h"
22 #include "chrome/browser/ui/cocoa/website_settings/permission_selector_button.h"
23 #include "chrome/browser/ui/cocoa/website_settings/split_block_button.h"
24 #include "chrome/browser/ui/cocoa/website_settings/website_settings_utils_cocoa.h"
25 #include "chrome/browser/ui/website_settings/permission_bubble_request.h"
26 #include "chrome/browser/ui/website_settings/permission_bubble_view.h"
27 #include "chrome/browser/ui/website_settings/permission_menu_model.h"
28 #include "chrome/grit/generated_resources.h"
29 #include "content/public/browser/native_web_keyboard_event.h"
30 #include "content/public/browser/user_metrics.h"
31 #include "skia/ext/skia_utils_mac.h"
32 #import "ui/base/cocoa/controls/hyperlink_text_view.h"
33 #import "ui/base/cocoa/menu_controller.h"
34 #include "ui/base/cocoa/window_size_constants.h"
35 #include "ui/base/l10n/l10n_util_mac.h"
36 #include "ui/base/models/simple_menu_model.h"
38 using base::UserMetricsAction;
40 namespace {
42 // Distance between permission icon and permission label.
43 const CGFloat kHorizontalIconPadding = 8.0f;
45 // Distance between two permission labels.
46 const CGFloat kVerticalPermissionPadding = 2.0f;
48 const CGFloat kHorizontalPadding = 13.0f;
49 const CGFloat kVerticalPadding = 15.0f;
50 const CGFloat kBetweenButtonsPadding = 10.0f;
51 const CGFloat kButtonRightEdgePadding = 17.0f;
52 const CGFloat kTitlePaddingX = 50.0f;
53 const CGFloat kBubbleMinWidth = 315.0f;
54 const NSSize kPermissionIconSize = {18, 18};
56 class MenuDelegate : public ui::SimpleMenuModel::Delegate {
57  public:
58   explicit MenuDelegate(PermissionBubbleController* bubble)
59       : bubble_controller_(bubble) {}
60   bool IsCommandIdChecked(int command_id) const override { return false; }
61   bool IsCommandIdEnabled(int command_id) const override { return true; }
62   bool GetAcceleratorForCommandId(int command_id,
63                                   ui::Accelerator* accelerator) override {
64     return false;
65   }
66  private:
67   PermissionBubbleController* bubble_controller_;  // Weak, owns us.
68   DISALLOW_COPY_AND_ASSIGN(MenuDelegate);
71 }  // namespace
73 // NSPopUpButton with a menu containing two items: allow and block.
74 // One AllowBlockMenuButton is used for each requested permission when there are
75 // multiple permissions in the bubble.
76 @interface AllowBlockMenuButton : NSPopUpButton {
77  @private
78   scoped_ptr<PermissionMenuModel> menuModel_;
79   base::scoped_nsobject<MenuController> menuController_;
82 - (id)initForURL:(const GURL&)url
83          allowed:(BOOL)allow
84            index:(int)index
85         delegate:(PermissionBubbleView::Delegate*)delegate;
87 // Returns the maximum width of its possible titles.
88 - (CGFloat)maximumTitleWidth;
89 @end
91 @implementation AllowBlockMenuButton
93 - (id)initForURL:(const GURL&)url
94          allowed:(BOOL)allow
95            index:(int)index
96         delegate:(PermissionBubbleView::Delegate*)delegate {
97   if (self = [super initWithFrame:NSZeroRect pullsDown:NO]) {
98     ContentSetting setting =
99         allow ? CONTENT_SETTING_ALLOW : CONTENT_SETTING_BLOCK;
100     [self setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
101     [self setBordered:NO];
103     __block PermissionBubbleView::Delegate* blockDelegate = delegate;
104     __block AllowBlockMenuButton* blockSelf = self;
105     PermissionMenuModel::ChangeCallback changeCallback =
106         base::BindBlock(^(const WebsiteSettingsUI::PermissionInfo& permission) {
107             blockDelegate->ToggleAccept(
108                 index, permission.setting == CONTENT_SETTING_ALLOW);
109             [blockSelf setFrameSize:
110                 SizeForWebsiteSettingsButtonTitle(blockSelf,
111                                                   [blockSelf title])];
112         });
114     menuModel_.reset(new PermissionMenuModel(url, setting, changeCallback));
115     menuController_.reset([[MenuController alloc] initWithModel:menuModel_.get()
116                                          useWithPopUpButtonCell:NO]);
117     [self setMenu:[menuController_ menu]];
118     [self selectItemAtIndex:menuModel_->GetIndexOfCommandId(setting)];
119     // Although the frame is reset, below, this sizes the cell properly.
120     [self sizeToFit];
121     // Adjust the size to fit the current title.  Using only -sizeToFit leaves
122     // an ugly amount of whitespace between the title and the arrows because it
123     // will fit to the largest element in the menu, not just the selected item.
124     [self setFrameSize:SizeForWebsiteSettingsButtonTitle(self, [self title])];
125   }
126   return self;
129 - (CGFloat)maximumTitleWidth {
130   CGFloat maxTitleWidth = 0;
131   for (NSMenuItem* item in [self itemArray]) {
132     NSSize size = SizeForWebsiteSettingsButtonTitle(self, [item title]);
133     maxTitleWidth = std::max(maxTitleWidth, size.width);
134   }
135   return maxTitleWidth;
138 @end
140 // The window used for the permission bubble controller.
141 // Subclassed to allow browser-handled keyboard events to be passed from the
142 // permission bubble to its parent window, which is a browser window.
143 @interface PermissionBubbleWindow : InfoBubbleWindow
144 @end
146 @implementation PermissionBubbleWindow
147 - (BOOL)performKeyEquivalent:(NSEvent*)event {
148   // Before forwarding to parent, handle locally.
149   if ([super performKeyEquivalent:event])
150     return YES;
152   // Only handle events if they should be forwarded to the parent window.
153   if ([self allowShareParentKeyState]) {
154     content::NativeWebKeyboardEvent wrappedEvent(event);
155     if ([BrowserWindowUtils shouldHandleKeyboardEvent:wrappedEvent]) {
156       // Turn off sharing of key window state while the keyboard event is
157       // processed.  This avoids recursion - with the key window state shared,
158       // the parent window would just forward the event back to this class.
159       [self setAllowShareParentKeyState:NO];
160       BOOL eventHandled =
161           [BrowserWindowUtils handleKeyboardEvent:event
162                                          inWindow:[self parentWindow]];
163       [self setAllowShareParentKeyState:YES];
164       return eventHandled;
165     }
166   }
167   return NO;
169 @end
171 @interface PermissionBubbleController ()
173 // Returns an autoreleased NSView displaying the icon and label for |request|.
174 - (NSView*)labelForRequest:(PermissionBubbleRequest*)request;
176 // Returns an autoreleased NSView displaying the title for the bubble
177 // requesting settings for |host|.
178 - (NSView*)titleWithHostname:(const std::string&)host;
180 // Returns an autoreleased NSView displaying a menu for |request|.  The
181 // menu will be initialized as 'allow' if |allow| is YES.
182 - (NSView*)menuForRequest:(PermissionBubbleRequest*)request
183                   atIndex:(int)index
184                     allow:(BOOL)allow;
186 // Returns an autoreleased NSView of a button with |title| and |action|.
187 - (NSView*)buttonWithTitle:(NSString*)title
188                     action:(SEL)action;
190 // Returns an autoreleased NSView displaying a block button.
191 - (NSView*)blockButton;
193 // Returns an autoreleased NSView displaying the close 'x' button.
194 - (NSView*)closeButton;
196 // Called when the 'ok' button is pressed.
197 - (void)ok:(id)sender;
199 // Called when the 'allow' button is pressed.
200 - (void)onAllow:(id)sender;
202 // Called when the 'block' button is pressed.
203 - (void)onBlock:(id)sender;
205 // Called when the 'close' button is pressed.
206 - (void)onClose:(id)sender;
208 // Sets the width of both |viewA| and |viewB| to be the larger of the
209 // two views' widths.  Does not change either view's origin or height.
210 + (CGFloat)matchWidthsOf:(NSView*)viewA andOf:(NSView*)viewB;
212 // Sets the offset of |viewA| so that its vertical center is aligned with the
213 // vertical center of |viewB|.
214 + (void)alignCenterOf:(NSView*)viewA verticallyToCenterOf:(NSView*)viewB;
216 @end
218 @implementation PermissionBubbleController
220 - (id)initWithParentWindow:(NSWindow*)parentWindow
221                     bridge:(PermissionBubbleCocoa*)bridge {
222   DCHECK(parentWindow);
223   DCHECK(bridge);
224   base::scoped_nsobject<PermissionBubbleWindow> window(
225       [[PermissionBubbleWindow alloc]
226           initWithContentRect:ui::kWindowSizeDeterminedLater
227                     styleMask:NSBorderlessWindowMask
228                       backing:NSBackingStoreBuffered
229                         defer:NO]);
230   [window setAllowedAnimations:info_bubble::kAnimateNone];
231   [window setReleasedWhenClosed:NO];
232   if ((self = [super initWithWindow:window
233                        parentWindow:parentWindow
234                          anchoredAt:NSZeroPoint])) {
235     [self setShouldCloseOnResignKey:NO];
236     [self setShouldOpenAsKeyWindow:YES];
237     [[self bubble] setArrowLocation:info_bubble::kTopLeft];
238     bridge_ = bridge;
239     NSNotificationCenter* center = [NSNotificationCenter defaultCenter];
240     [center addObserver:self
241                selector:@selector(parentWindowDidMove:)
242                    name:NSWindowDidMoveNotification
243                  object:parentWindow];
244   }
245   return self;
248 - (void)windowWillClose:(NSNotification*)notification {
249   bridge_->OnBubbleClosing();
250   [super windowWillClose:notification];
253 - (void)parentWindowWillBecomeFullScreen:(NSNotification*)notification {
254   // Override the base class implementation, which would have closed the bubble.
257 - (void)parentWindowDidResize:(NSNotification*)notification {
258   DCHECK(bridge_);
259   [self setAnchorPoint:bridge_->GetAnchorPoint()];
262 - (void)parentWindowDidMove:(NSNotification*)notification {
263   DCHECK(bridge_);
264   [self setAnchorPoint:bridge_->GetAnchorPoint()];
267 - (void)showAtAnchor:(NSPoint)anchorPoint
268          withDelegate:(PermissionBubbleView::Delegate*)delegate
269           forRequests:(const std::vector<PermissionBubbleRequest*>&)requests
270          acceptStates:(const std::vector<bool>&)acceptStates {
271   DCHECK(!requests.empty());
272   DCHECK(delegate);
273   delegate_ = delegate;
275   NSView* contentView = [[self window] contentView];
276   [contentView setSubviews:@[]];
278   BOOL singlePermission = requests.size() == 1;
280   // Create one button to use as a guide for the permissions' y-offsets.
281   base::scoped_nsobject<NSView> allowOrOkButton;
282   if (singlePermission) {
283     NSString* allowTitle = l10n_util::GetNSString(IDS_PERMISSION_ALLOW);
284     allowOrOkButton.reset([[self buttonWithTitle:allowTitle
285                                           action:@selector(onAllow:)] retain]);
286   } else {
287     NSString* okTitle = l10n_util::GetNSString(IDS_OK);
288     allowOrOkButton.reset([[self buttonWithTitle:okTitle
289                                           action:@selector(ok:)] retain]);
290   }
291   CGFloat yOffset = 2 * kVerticalPadding + NSMaxY([allowOrOkButton frame]);
293   base::scoped_nsobject<NSMutableArray> permissionMenus;
294   if (!singlePermission)
295     permissionMenus.reset([[NSMutableArray alloc] init]);
297   CGFloat maxPermissionLineWidth = 0;
298   CGFloat verticalPadding = 0.0f;
299   for (auto it = requests.begin(); it != requests.end(); it++) {
300     base::scoped_nsobject<NSView> permissionView(
301         [[self labelForRequest:(*it)] retain]);
302     NSPoint origin = [permissionView frame].origin;
303     origin.x += kHorizontalPadding;
304     origin.y += yOffset + verticalPadding;
305     [permissionView setFrameOrigin:origin];
306     [contentView addSubview:permissionView];
308     if (!singlePermission) {
309       int index = it - requests.begin();
310       base::scoped_nsobject<NSView> menu(
311           [[self menuForRequest:(*it)
312                         atIndex:index
313                           allow:acceptStates[index] ? YES : NO] retain]);
314       // Align vertically.  Horizontal alignment will be adjusted once the
315       // widest permission is know.
316       [PermissionBubbleController alignCenterOf:menu
317                            verticallyToCenterOf:permissionView];
318       [permissionMenus addObject:menu];
319       [contentView addSubview:menu];
320     }
321     maxPermissionLineWidth = std::max(
322         maxPermissionLineWidth, NSMaxX([permissionView frame]));
323     yOffset += NSHeight([permissionView frame]);
325     // Add extra padding for all but first permission.
326     verticalPadding = kVerticalPermissionPadding;
327   }
329   base::scoped_nsobject<NSView> titleView(
330       [[self titleWithHostname:requests[0]->GetRequestingHostname().host()]
331           retain]);
332   [contentView addSubview:titleView];
333   [titleView setFrameOrigin:NSMakePoint(kHorizontalPadding,
334                                         kVerticalPadding + yOffset)];
336   // 'x' button in the upper-right-hand corner.
337   base::scoped_nsobject<NSView> closeButton([[self closeButton] retain]);
339   // Determine the dimensions of the bubble.
340   // Once the height and width are set, the buttons and permission menus can
341   // be laid out correctly.
342   NSRect bubbleFrame = NSMakeRect(0, 0, kBubbleMinWidth, 0);
344   // Fix the height of the bubble relative to the title.
345   bubbleFrame.size.height = NSMaxY([titleView frame]) + kVerticalPadding +
346                             info_bubble::kBubbleArrowHeight;
348   if (!singlePermission) {
349     // Add the maximum menu width to the bubble width.
350     CGFloat maxMenuWidth = 0;
351     for (AllowBlockMenuButton* button in permissionMenus.get()) {
352       maxMenuWidth = std::max(maxMenuWidth, [button maximumTitleWidth]);
353     }
354     maxPermissionLineWidth += maxMenuWidth;
355   }
357   // The title and 'x' button row must fit within the bubble.
358   CGFloat titleRowWidth = NSMaxX([titleView frame]) +
359                           NSWidth([closeButton frame]) +
360                           chrome_style::kCloseButtonPadding;
362   bubbleFrame.size.width = std::max(
363       NSWidth(bubbleFrame), std::max(titleRowWidth, maxPermissionLineWidth));
365   // Now that the bubble's dimensions have been set, lay out the buttons and
366   // menus.
368   // Place the close button at the upper-right-hand corner of the bubble.
369   NSPoint closeButtonOrigin =
370       NSMakePoint(NSWidth(bubbleFrame) - NSWidth([closeButton frame]) -
371                       chrome_style::kCloseButtonPadding,
372                   NSHeight(bubbleFrame) - NSWidth([closeButton frame]) -
373                       chrome_style::kCloseButtonPadding);
374   // Account for the bubble's arrow.
375   closeButtonOrigin.y -= info_bubble::kBubbleArrowHeight;
377   [closeButton setFrameOrigin:closeButtonOrigin];
378   [contentView addSubview:closeButton];
380   // Position the allow/ok button.
381   CGFloat xOrigin = NSWidth(bubbleFrame) - NSWidth([allowOrOkButton frame]) -
382                     kButtonRightEdgePadding;
383   [allowOrOkButton setFrameOrigin:NSMakePoint(xOrigin, kVerticalPadding)];
384   [contentView addSubview:allowOrOkButton];
386   if (singlePermission) {
387     base::scoped_nsobject<NSView> blockButton;
388     blockButton.reset([[self blockButton] retain]);
389     CGFloat width = [PermissionBubbleController matchWidthsOf:blockButton
390                                                         andOf:allowOrOkButton];
391     // Ensure the allow/ok button is still in the correct position.
392     xOrigin = NSWidth(bubbleFrame) - width - kHorizontalPadding;
393     [allowOrOkButton setFrameOrigin:NSMakePoint(xOrigin, kVerticalPadding)];
394     // Line up the block button.
395     xOrigin = NSMinX([allowOrOkButton frame]) - width - kBetweenButtonsPadding;
396     [blockButton setFrameOrigin:NSMakePoint(xOrigin, kVerticalPadding)];
397     [contentView addSubview:blockButton];
398   } else {
399     // Adjust the horizontal origin for each menu so that its right edge
400     // lines up with the right edge of the ok button.
401     CGFloat rightEdge = NSMaxX([allowOrOkButton frame]);
402     for (NSView* view in permissionMenus.get()) {
403       [view setFrameOrigin:NSMakePoint(rightEdge - NSWidth([view frame]),
404                                        NSMinY([view frame]))];
405     }
406   }
408   bubbleFrame = [[self window] frameRectForContentRect:bubbleFrame];
409   if ([[self window] isVisible]) {
410     // Unfortunately, calling -setFrame followed by -setFrameOrigin  (called
411     // within -setAnchorPoint) causes flickering.  Avoid the flickering by
412     // manually adjusting the new frame's origin so that the top left stays the
413     // same, and only calling -setFrame.
414     NSRect currentWindowFrame = [[self window] frame];
415     bubbleFrame.origin = currentWindowFrame.origin;
416     bubbleFrame.origin.y = bubbleFrame.origin.y +
417         currentWindowFrame.size.height - bubbleFrame.size.height;
418     [[self window] setFrame:bubbleFrame display:YES];
419   } else {
420     [[self window] setFrame:bubbleFrame display:NO];
421     [self setAnchorPoint:anchorPoint];
422     [self showWindow:nil];
423     [[self window] makeFirstResponder:nil];
424     [[self window] setInitialFirstResponder:allowOrOkButton.get()];
425   }
428 - (NSView*)labelForRequest:(PermissionBubbleRequest*)request {
429   DCHECK(request);
430   base::scoped_nsobject<NSView> permissionView(
431       [[NSView alloc] initWithFrame:NSZeroRect]);
432   base::scoped_nsobject<NSImageView> permissionIcon(
433       [[NSImageView alloc] initWithFrame:NSZeroRect]);
434   [permissionIcon setImage:ui::ResourceBundle::GetSharedInstance().
435       GetNativeImageNamed(request->GetIconID()).ToNSImage()];
436   [permissionIcon setFrameSize:kPermissionIconSize];
437   [permissionView addSubview:permissionIcon];
439   base::scoped_nsobject<NSTextField> permissionLabel(
440       [[NSTextField alloc] initWithFrame:NSZeroRect]);
441   base::string16 label = request->GetMessageTextFragment();
442   [permissionLabel setDrawsBackground:NO];
443   [permissionLabel setBezeled:NO];
444   [permissionLabel setEditable:NO];
445   [permissionLabel setSelectable:NO];
446   [permissionLabel
447       setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
448   [permissionLabel setStringValue:base::SysUTF16ToNSString(label)];
449   [permissionLabel sizeToFit];
450   [permissionLabel setFrameOrigin:
451       NSMakePoint(NSWidth([permissionIcon frame]) + kHorizontalIconPadding, 0)];
452   [permissionView addSubview:permissionLabel];
454   // Match the horizontal centers of the two subviews.  Note that the label's
455   // center is rounded down, and the icon's center, up.  It looks better that
456   // way - with the text's center slightly lower than the icon's center - if the
457   // height delta is not evenly split.
458   NSRect iconFrame = [permissionIcon frame];
459   NSRect labelFrame = [permissionLabel frame];
460   NSRect unionFrame = NSUnionRect(iconFrame, labelFrame);
462   iconFrame.origin.y =
463       std::ceil((NSHeight(unionFrame) - NSHeight(iconFrame)) / 2);
464   labelFrame.origin.y =
465       std::floor((NSHeight(unionFrame) - NSHeight(labelFrame)) / 2);
467   [permissionLabel setFrame:labelFrame];
468   [permissionIcon setFrame:iconFrame];
469   [permissionView setFrame:unionFrame];
471   return permissionView.autorelease();
474 - (NSView*)titleWithHostname:(const std::string&)host {
475   base::scoped_nsobject<NSTextField> titleView(
476       [[NSTextField alloc] initWithFrame:NSZeroRect]);
477   [titleView setDrawsBackground:NO];
478   [titleView setBezeled:NO];
479   [titleView setEditable:NO];
480   [titleView setSelectable:NO];
481   [titleView setStringValue:
482       l10n_util::GetNSStringF(IDS_PERMISSIONS_BUBBLE_PROMPT,
483                               base::UTF8ToUTF16(host))];
484   [titleView setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
485   [titleView sizeToFit];
486   NSRect titleFrame = [titleView frame];
487   [titleView setFrameSize:NSMakeSize(NSWidth(titleFrame) + kTitlePaddingX,
488                                      NSHeight(titleFrame))];
489   return titleView.autorelease();
492 - (NSView*)menuForRequest:(PermissionBubbleRequest*)request
493                   atIndex:(int)index
494                     allow:(BOOL)allow {
495   DCHECK(request);
496   DCHECK(delegate_);
497   base::scoped_nsobject<AllowBlockMenuButton> button(
498       [[AllowBlockMenuButton alloc] initForURL:request->GetRequestingHostname()
499                                        allowed:allow
500                                          index:index
501                                       delegate:delegate_]);
502   return button.autorelease();
505 - (NSView*)buttonWithTitle:(NSString*)title
506                     action:(SEL)action {
507   base::scoped_nsobject<NSButton> button(
508       [[ConstrainedWindowButton alloc] initWithFrame:NSZeroRect]);
509   [button setButtonType:NSMomentaryPushInButton];
510   [button setTitle:title];
511   [button setTarget:self];
512   [button setAction:action];
513   [button sizeToFit];
514   return button.autorelease();
517 - (NSView*)blockButton {
518   NSString* blockTitle = l10n_util::GetNSString(IDS_PERMISSION_DENY);
519   return [self buttonWithTitle:blockTitle
520                         action:@selector(onBlock:)];
523 - (NSView*)closeButton {
524   int dimension = chrome_style::GetCloseButtonSize();
525   NSRect frame = NSMakeRect(0, 0, dimension, dimension);
526   base::scoped_nsobject<NSButton> button(
527       [[WebUIHoverCloseButton alloc] initWithFrame:frame]);
528   [button setAction:@selector(onClose:)];
529   [button setTarget:self];
530   return button.autorelease();
533 - (void)ok:(id)sender {
534   DCHECK(delegate_);
535   delegate_->Accept();
538 - (void)onAllow:(id)sender {
539   DCHECK(delegate_);
540   delegate_->Accept();
543 - (void)onBlock:(id)sender {
544   DCHECK(delegate_);
545   delegate_->Deny();
548 - (void)onClose:(id)sender {
549   DCHECK(delegate_);
550   delegate_->Closing();
553 - (void)activateTabWithContents:(content::WebContents*)newContents
554                previousContents:(content::WebContents*)oldContents
555                         atIndex:(NSInteger)index
556                          reason:(int)reason {
557   // The show/hide of this bubble is handled by the PermissionBubbleManager.
558   // So bypass the base class, which would close the bubble here.
561 + (CGFloat)matchWidthsOf:(NSView*)viewA andOf:(NSView*)viewB {
562   NSRect frameA = [viewA frame];
563   NSRect frameB = [viewB frame];
564   CGFloat width = std::max(NSWidth(frameA), NSWidth(frameB));
565   [viewA setFrameSize:NSMakeSize(width, NSHeight(frameA))];
566   [viewB setFrameSize:NSMakeSize(width, NSHeight(frameB))];
567   return width;
570 + (void)alignCenterOf:(NSView*)viewA verticallyToCenterOf:(NSView*)viewB {
571   NSRect frameA = [viewA frame];
572   NSRect frameB = [viewB frame];
573   frameA.origin.y =
574       NSMinY(frameB) + std::floor((NSHeight(frameB) - NSHeight(frameA)) / 2);
575   [viewA setFrameOrigin:frameA.origin];
578 @end  // implementation PermissionBubbleController