Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(2270)

Unified Diff: chrome/browser/ui/cocoa/website_settings/chooser_bubble_ui_cocoa.mm

Issue 1473393003: Add chooser permission UI code for Mac (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@wpu_1
Patch Set: address palmer@'s comments Created 5 years ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: chrome/browser/ui/cocoa/website_settings/chooser_bubble_ui_cocoa.mm
diff --git a/chrome/browser/ui/cocoa/website_settings/chooser_bubble_ui_cocoa.mm b/chrome/browser/ui/cocoa/website_settings/chooser_bubble_ui_cocoa.mm
new file mode 100644
index 0000000000000000000000000000000000000000..f54bbbda7f9b9fe4d9ed455acd61549df22f5983
--- /dev/null
+++ b/chrome/browser/ui/cocoa/website_settings/chooser_bubble_ui_cocoa.mm
@@ -0,0 +1,519 @@
+// Copyright 2015 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/ui/cocoa/website_settings/chooser_bubble_ui_cocoa.h"
+
+#include <algorithm>
+#include <cmath>
+#include <vector>
+
+#include "base/mac/scoped_nsobject.h"
+#include "base/strings/sys_string_conversions.h"
+#include "base/strings/utf_string_conversions.h"
+#include "chrome/browser/ui/browser.h"
+#include "chrome/browser/ui/browser_finder.h"
+#include "chrome/browser/ui/browser_window.h"
+#import "chrome/browser/ui/chrome_style.h"
+#import "chrome/browser/ui/cocoa/base_bubble_controller.h"
+#import "chrome/browser/ui/cocoa/browser_window_controller.h"
+#import "chrome/browser/ui/cocoa/browser_window_utils.h"
+#import "chrome/browser/ui/cocoa/constrained_window/constrained_window_button.h"
+#import "chrome/browser/ui/cocoa/hover_close_button.h"
+#import "chrome/browser/ui/cocoa/info_bubble_view.h"
+#import "chrome/browser/ui/cocoa/info_bubble_window.h"
+#import "chrome/browser/ui/cocoa/location_bar/location_bar_view_mac.h"
+#include "chrome/browser/ui/website_settings/chooser_bubble_delegate.h"
+#include "chrome/grit/generated_resources.h"
+#include "content/public/browser/native_web_keyboard_event.h"
+#include "ui/base/cocoa/window_size_constants.h"
+#include "ui/base/l10n/l10n_util_mac.h"
+
+namespace {
+
+// Chooser bubble width.
+const CGFloat kChooserBubbleWidth = 320.0f;
+
+// Chooser bubble height.
+const CGFloat kChooserBubbleHeight = 220.0f;
+
+// Distance between the bubble border and the view that is closest to the
+// border.
+const CGFloat kMarginX = 20.0f;
+const CGFloat kMarginY = 20.0f;
+
+// Distance between two views inside the bubble.
+const CGFloat kHorizontalPadding = 10.0f;
+const CGFloat kVerticalPadding = 10.0f;
+
+const CGFloat kTitlePaddingX = 50.0f;
+}
+
+scoped_ptr<BubbleUi> ChooserBubbleDelegate::BuildBubbleUi() {
+ return make_scoped_ptr(new ChooserBubbleUiCocoa(browser_, this));
+}
+
+@interface ChooserBubbleUiController
+ : BaseBubbleController<NSTableViewDataSource, NSTableViewDelegate> {
+ @private
+ // Bridge to the C++ class that created this object.
+ ChooserBubbleUiCocoa* bridge_;
+
+ base::scoped_nsobject<NSTextField> titleView_;
+ base::scoped_nsobject<NSScrollView> scrollView_;
+ base::scoped_nsobject<NSTableColumn> tableColumn_;
+ base::scoped_nsobject<NSTableView> tableView_;
+ base::scoped_nsobject<NSButton> connectButton_;
+ base::scoped_nsobject<NSButton> cancelButton_;
+
+ Browser* browser_; // Weak.
+ ChooserBubbleDelegate* chooser_bubble_delegate_; // Weak.
Robert Sesek 2015/12/14 23:13:48 naming: chooserBubbleDelegate_
juncai 2015/12/15 02:09:28 Done.
+}
+
+// Designated initializer. |browser| and |bridge| must both be non-nil.
+- (id)initWithBrowser:(Browser*)browser
+ initWithChooserBubbleDelegate:
+ (ChooserBubbleDelegate*)chooser_bubble_delegate
Robert Sesek 2015/12/14 23:13:48 naming: chooserBubbleDelegate_
juncai 2015/12/15 02:09:28 Done.
+ bridge:(ChooserBubbleUiCocoa*)bridge;
+
+// Makes the bubble visible.
+- (void)show;
+
+// Will reposition the bubble based in case the anchor or parent should change.
+- (void)updateAnchorPosition;
+
+// Will calculate the expected anchor point for this bubble.
+// Should only be used outside this class for tests.
+- (NSPoint)getExpectedAnchorPoint;
+
+// Returns true if the browser has support for the location bar.
+// Should only be used outside this class for tests.
+- (bool)hasLocationBar;
+
+// Update |tableView_| when chooser options were initialized.
+- (void)onOptionsInitialized;
+
+// Update |tableView_| when chooser option was added.
+- (void)onOptionAdded:(NSInteger)index;
+
+// Update |tableView_| when chooser option was removed.
+- (void)onOptionRemoved:(NSInteger)index;
+
+// Update |tableView_| when chooser options changed.
+- (void)updateTableView;
+
+// Determines if the bubble has an anchor in a corner or no anchor at all.
+- (info_bubble::BubbleArrowLocation)getExpectedArrowLocation;
+
+// Returns the expected parent for this bubble.
+- (NSWindow*)getExpectedParentWindow;
+
+// Creates the title for the bubble.
+- (base::scoped_nsobject<NSTextField>)bubbleTitle;
+
+// Creates a button with |title| and |action|.
+- (base::scoped_nsobject<NSButton>)buttonWithTitle:(NSString*)title
+ action:(SEL)action;
+
+// Creates the "Connect" button.
+- (base::scoped_nsobject<NSButton>)connectButton;
+
+// Creates the "Cancel" button.
+- (base::scoped_nsobject<NSButton>)cancelButton;
+
+// Called when the "Connect" button is pressed.
+- (void)onConnect:(id)sender;
+
+// Called when the "Cancel" button is pressed.
+- (void)onCancel:(id)sender;
+
+@end
+
+@implementation ChooserBubbleUiController
+
+- (id)initWithBrowser:(Browser*)browser
+ initWithChooserBubbleDelegate:
+ (ChooserBubbleDelegate*)chooser_bubble_delegate
+ bridge:(ChooserBubbleUiCocoa*)bridge {
+ DCHECK(browser);
+ DCHECK(chooser_bubble_delegate);
+ DCHECK(bridge);
+
+ if (browser == nil || chooser_bubble_delegate == nil || bridge == nil)
Robert Sesek 2015/12/14 23:13:48 This is also DCHECK'd, so I'd remove this.
juncai 2015/12/15 02:09:28 Done.
+ return nil;
+
+ browser_ = browser;
+ chooser_bubble_delegate_ = chooser_bubble_delegate;
+
+ base::scoped_nsobject<InfoBubbleWindow> window([[InfoBubbleWindow alloc]
+ initWithContentRect:ui::kWindowSizeDeterminedLater
+ styleMask:NSBorderlessWindowMask
+ backing:NSBackingStoreBuffered
+ defer:NO]);
+ [window setAllowedAnimations:info_bubble::kAnimateNone];
+ [window setReleasedWhenClosed:NO];
+ if ((self = [super initWithWindow:window
+ parentWindow:[self getExpectedParentWindow]
+ anchoredAt:NSZeroPoint])) {
+ [self setShouldCloseOnResignKey:NO];
+ [self setShouldOpenAsKeyWindow:YES];
+ [[self bubble] setArrowLocation:[self getExpectedArrowLocation]];
+ bridge_ = bridge;
+ NSNotificationCenter* center = [NSNotificationCenter defaultCenter];
+ [center addObserver:self
Robert Sesek 2015/12/14 23:13:48 This observer is leaked and should be removed eith
juncai 2015/12/15 02:09:28 Done.
+ selector:@selector(parentWindowDidMove:)
+ name:NSWindowDidMoveNotification
+ object:[self getExpectedParentWindow]];
+ }
+ return self;
+}
+
+- (void)windowWillClose:(NSNotification*)notification {
+ bridge_->OnBubbleClosing();
+ [super windowWillClose:notification];
+}
+
+- (void)parentWindowWillToggleFullScreen:(NSNotification*)notification {
+ // Override the base class implementation, which would have closed the bubble.
+}
+
+- (void)parentWindowDidResize:(NSNotification*)notification {
+ [self setAnchorPoint:[self getExpectedAnchorPoint]];
+}
+
+- (void)parentWindowDidMove:(NSNotification*)notification {
+ DCHECK(bridge_);
+ [self setAnchorPoint:[self getExpectedAnchorPoint]];
+}
+
+- (void)show {
+ NSView* view = [[self window] contentView];
+ [view setSubviews:@[]];
Robert Sesek 2015/12/14 23:13:48 What's the rationale behind this?
juncai 2015/12/15 02:09:28 Done.
+
+ // ------------------------------------
+ // | Chooser bubble title |
+ // | -------------------------------- |
+ // | | option 0 | |
+ // | | option 1 | |
+ // | | option 2 | |
+ // | | | |
+ // | | | |
+ // | | | |
+ // | -------------------------------- |
+ // | [ Connect] [ Cancel ] |
+ // ------------------------------------
+
+ // Determine the dimensions of the bubble.
+ // Once the height and width are set, the buttons and permission menus can
+ // be laid out correctly.
+ NSRect bubbleFrame =
+ NSMakeRect(0, 0, kChooserBubbleWidth, kChooserBubbleHeight);
+
+ // Create the views.
+ // Title.
+ titleView_ = [self bubbleTitle];
+ CGFloat titleOriginX = kMarginX;
+ CGFloat titleHeight = NSHeight([titleView_ frame]);
+ CGFloat titleOriginY = kChooserBubbleHeight - kMarginY - titleHeight;
+ [titleView_ setFrameOrigin:NSMakePoint(titleOriginX, titleOriginY)];
+ [view addSubview:titleView_];
+
+ // Connect button.
+ connectButton_ = [self connectButton];
+ // Cancel button.
+ cancelButton_ = [self cancelButton];
+ CGFloat connectButtonWidth = NSWidth([connectButton_ frame]);
+ CGFloat connectButtonHeight = NSHeight([connectButton_ frame]);
+ CGFloat cancelButtonWidth = NSWidth([cancelButton_ frame]);
+
+ // ScollView embedding with TableView.
+ CGFloat scrollViewWidth = kChooserBubbleWidth - 2 * kMarginX;
+ CGFloat scrollViewHeight = kChooserBubbleHeight - 2 * kMarginY -
+ 2 * kVerticalPadding - titleHeight -
+ connectButtonHeight;
+ NSRect scrollFrame =
+ NSMakeRect(kMarginX, kMarginY + connectButtonHeight + kVerticalPadding,
+ scrollViewWidth, scrollViewHeight);
+ scrollView_.reset([[NSScrollView alloc] initWithFrame:scrollFrame]);
+ [scrollView_ setBorderType:NSBezelBorder];
+ [scrollView_ setHasVerticalScroller:YES];
+ [scrollView_ setHasHorizontalScroller:YES];
+ [scrollView_ setAutohidesScrollers:YES];
+
+ // TableView.
+ tableView_.reset([[NSTableView alloc] initWithFrame:NSZeroRect]);
+ tableColumn_.reset([[NSTableColumn alloc] initWithIdentifier:@""]);
+ [tableColumn_ setWidth:(scrollViewWidth - kMarginX)];
+ [tableView_ addTableColumn:tableColumn_];
+ [tableView_ setDelegate:self];
+ [tableView_ setDataSource:self];
+ // Make the column title invisible.
+ [tableView_ setHeaderView:nil];
+ [tableView_ setFocusRingType:NSFocusRingTypeNone];
+
+ [scrollView_ setDocumentView:tableView_];
+ [view addSubview:scrollView_];
+
+ // Set connect button and cancel button to the right place.
+ CGFloat connectButtonOriginX = kChooserBubbleWidth - kMarginX -
+ kHorizontalPadding - connectButtonWidth -
+ cancelButtonWidth;
+ CGFloat connectButtonOriginY = kMarginY;
+ [connectButton_
+ setFrameOrigin:NSMakePoint(connectButtonOriginX, connectButtonOriginY)];
+ [connectButton_ setEnabled:NO];
+ [view addSubview:connectButton_];
+
+ CGFloat cancelButtonOriginX =
+ kChooserBubbleWidth - kMarginX - cancelButtonWidth;
+ CGFloat cancelButtonOriginY = kMarginY;
+ [cancelButton_
+ setFrameOrigin:NSMakePoint(cancelButtonOriginX, cancelButtonOriginY)];
+ [view addSubview:cancelButton_];
+
+ bubbleFrame = [[self window] frameRectForContentRect:bubbleFrame];
+ if ([[self window] isVisible]) {
+ // Unfortunately, calling -setFrame followed by -setFrameOrigin (called
+ // within -setAnchorPoint) causes flickering. Avoid the flickering by
+ // manually adjusting the new frame's origin so that the top left stays the
+ // same, and only calling -setFrame.
+ NSRect currentWindowFrame = [[self window] frame];
+ bubbleFrame.origin = currentWindowFrame.origin;
+ bubbleFrame.origin.y = bubbleFrame.origin.y +
+ currentWindowFrame.size.height -
+ bubbleFrame.size.height;
+ [[self window] setFrame:bubbleFrame display:YES];
+ } else {
+ [[self window] setFrame:bubbleFrame display:NO];
+ [self setAnchorPoint:[self getExpectedAnchorPoint]];
+ [self showWindow:nil];
+ [[self window] makeFirstResponder:nil];
+ [[self window] setInitialFirstResponder:connectButton_.get()];
+ }
+}
+
+- (NSInteger)numberOfRowsInTableView:(NSTableView*)tableView {
+ const std::vector<base::string16>& device_names =
+ chooser_bubble_delegate_->GetOptions();
+ if (device_names.empty()) {
+ return 1;
+ } else {
+ return static_cast<NSInteger>(device_names.size());
+ }
+}
+
+- (id)tableView:(NSTableView*)tableView
+ objectValueForTableColumn:(NSTableColumn*)tableColumn
+ row:(NSInteger)rowIndex {
+ const std::vector<base::string16>& device_names =
+ chooser_bubble_delegate_->GetOptions();
+ if (device_names.empty()) {
+ DCHECK(rowIndex == 0);
+ return l10n_util::GetNSString(IDS_CHOOSER_BUBBLE_NO_DEVICES_FOUND_PROMPT);
+ } else {
+ if (rowIndex >= 0 &&
+ rowIndex < static_cast<NSInteger>(device_names.size())) {
+ return base::SysUTF16ToNSString(device_names[rowIndex]);
+ } else {
+ return @"";
+ }
+ }
+}
+
+- (BOOL)tableView:(NSTableView*)aTableView
+ shouldEditTableColumn:(NSTableColumn*)aTableColumn
+ row:(NSInteger)rowIndex {
+ return NO;
+}
+
+- (void)onOptionsInitialized {
+ [self updateTableView];
+}
+
+- (void)onOptionAdded:(NSInteger)index {
+ [self updateTableView];
+}
+
+- (void)onOptionRemoved:(NSInteger)index {
+ // |tableView_| will automatically selects the next item if the current
+ // item is removed, so here it tracks if the removed item is the item
+ // that was previously selected, if so, deselect it.
+ if ([tableView_ selectedRow] == index)
+ [tableView_ deselectRow:index];
+
+ [self updateTableView];
+}
+
+- (void)updateTableView {
+ const std::vector<base::string16>& device_names =
+ chooser_bubble_delegate_->GetOptions();
+ [tableView_ setEnabled:!device_names.empty()];
+ [tableView_ reloadData];
+}
+
+- (void)tableViewSelectionDidChange:(NSNotification*)aNotification {
+ [connectButton_ setEnabled:[tableView_ numberOfSelectedRows] > 0];
+}
+
+- (void)updateAnchorPosition {
+ [self setParentWindow:[self getExpectedParentWindow]];
+ [self setAnchorPoint:[self getExpectedAnchorPoint]];
+}
+
+- (NSPoint)getExpectedAnchorPoint {
+ NSPoint anchor;
+ if ([self hasLocationBar]) {
+ LocationBarViewMac* location_bar =
Robert Sesek 2015/12/14 23:13:48 naming: locationBar
juncai 2015/12/15 02:09:28 Done.
+ [[[self getExpectedParentWindow] windowController] locationBarBridge];
+ anchor = location_bar->GetPageInfoBubblePoint();
+ } else {
+ // Center the bubble if there's no location bar.
+ NSRect contentFrame = [[[self getExpectedParentWindow] contentView] frame];
+ anchor = NSMakePoint(NSMidX(contentFrame), NSMaxY(contentFrame));
+ }
+
+ return [[self getExpectedParentWindow] convertBaseToScreen:anchor];
+}
+
+- (bool)hasLocationBar {
+ return browser_->SupportsWindowFeature(Browser::FEATURE_LOCATIONBAR);
+}
+
+- (info_bubble::BubbleArrowLocation)getExpectedArrowLocation {
+ return [self hasLocationBar] ? info_bubble::kTopLeft : info_bubble::kNoArrow;
+}
+
+- (NSWindow*)getExpectedParentWindow {
+ DCHECK(browser_->window());
+ return browser_->window()->GetNativeWindow();
+}
+
+- (base::scoped_nsobject<NSTextField>)bubbleTitle {
+ base::scoped_nsobject<NSTextField> titleView(
+ [[NSTextField alloc] initWithFrame:NSZeroRect]);
+ [titleView setDrawsBackground:NO];
+ [titleView setBezeled:NO];
+ [titleView setEditable:NO];
+ [titleView setSelectable:NO];
+ [titleView setStringValue:l10n_util::GetNSString(IDS_CHOOSER_BUBBLE_PROMPT)];
+ [titleView setFont:[NSFont systemFontOfSize:[NSFont systemFontSize]]];
+ [titleView sizeToFit];
+ NSRect titleFrame = [titleView frame];
+ [titleView setFrameSize:NSMakeSize(NSWidth(titleFrame) + kTitlePaddingX,
+ NSHeight(titleFrame))];
+ return titleView;
+}
+
+- (base::scoped_nsobject<NSButton>)buttonWithTitle:(NSString*)title
+ action:(SEL)action {
+ base::scoped_nsobject<NSButton> button(
+ [[ConstrainedWindowButton alloc] initWithFrame:NSZeroRect]);
+ [button setButtonType:NSMomentaryPushInButton];
+ [button setTitle:title];
+ [button setTarget:self];
+ [button setAction:action];
+ [button sizeToFit];
+ return button;
+}
+
+- (base::scoped_nsobject<NSButton>)connectButton {
+ NSString* connectTitle =
+ l10n_util::GetNSString(IDS_CHOOSER_BUBBLE_CONNECT_BUTTON_TEXT);
+ return [self buttonWithTitle:connectTitle action:@selector(onConnect:)];
+}
+
+- (base::scoped_nsobject<NSButton>)cancelButton {
+ NSString* cancelTitle =
+ l10n_util::GetNSString(IDS_CHOOSER_BUBBLE_CANCEL_BUTTON_TEXT);
+ return [self buttonWithTitle:cancelTitle action:@selector(onCancel:)];
+}
+
++ (CGFloat)matchWidthsOf:(NSView*)viewA andOf:(NSView*)viewB {
+ NSRect frameA = [viewA frame];
+ NSRect frameB = [viewB frame];
+ CGFloat width = std::max(NSWidth(frameA), NSWidth(frameB));
+ [viewA setFrameSize:NSMakeSize(width, NSHeight(frameA))];
+ [viewB setFrameSize:NSMakeSize(width, NSHeight(frameB))];
+ return width;
+}
+
++ (void)alignCenterOf:(NSView*)viewA verticallyToCenterOf:(NSView*)viewB {
+ NSRect frameA = [viewA frame];
+ NSRect frameB = [viewB frame];
+ frameA.origin.y =
+ NSMinY(frameB) + std::floor((NSHeight(frameB) - NSHeight(frameA)) / 2);
+ [viewA setFrameOrigin:frameA.origin];
+}
+
+- (void)onConnect:(id)sender {
+ NSInteger row = [tableView_ selectedRow];
+ chooser_bubble_delegate_->Select(row);
+ [self close];
+}
+
+- (void)onCancel:(id)sender {
+ chooser_bubble_delegate_->Cancel();
+ [self close];
+}
+
+@end
+
+ChooserBubbleUiCocoa::ChooserBubbleUiCocoa(
+ Browser* browser,
+ ChooserBubbleDelegate* chooser_bubble_delegate)
+ : browser_(browser),
+ chooser_bubble_delegate_(chooser_bubble_delegate),
+ chooser_bubble_ui_controller_(nil) {
+ DCHECK(browser);
+ DCHECK(chooser_bubble_delegate);
+ chooser_bubble_delegate_->set_observer(this);
+}
+
+ChooserBubbleUiCocoa::~ChooserBubbleUiCocoa() {
+ chooser_bubble_delegate_->set_observer(nullptr);
+ if (chooser_bubble_ui_controller_) {
Robert Sesek 2015/12/14 23:13:48 You don't need to nil-check ObjC objects, as messa
juncai 2015/12/15 02:09:28 Done.
+ [chooser_bubble_ui_controller_ close];
+ chooser_bubble_ui_controller_ = nil;
+ }
+}
+
+void ChooserBubbleUiCocoa::Show(BubbleReference bubble_reference) {
+ if (!chooser_bubble_ui_controller_) {
+ chooser_bubble_ui_controller_ = [[ChooserBubbleUiController alloc]
+ initWithBrowser:browser_
+ initWithChooserBubbleDelegate:chooser_bubble_delegate_
+ bridge:this];
+ }
+
+ if (chooser_bubble_ui_controller_) {
+ [chooser_bubble_ui_controller_ show];
+ [chooser_bubble_ui_controller_ updateTableView];
+ }
+}
+
+void ChooserBubbleUiCocoa::Close() {
+ if (chooser_bubble_ui_controller_) {
+ [chooser_bubble_ui_controller_ close];
+ chooser_bubble_ui_controller_ = nil;
+ }
+}
+
+void ChooserBubbleUiCocoa::UpdateAnchorPosition() {
+ [chooser_bubble_ui_controller_ updateAnchorPosition];
+}
+
+void ChooserBubbleUiCocoa::OnOptionsInitialized() {
+ [chooser_bubble_ui_controller_ onOptionsInitialized];
+}
+
+void ChooserBubbleUiCocoa::OnOptionAdded(int index) {
+ [chooser_bubble_ui_controller_ onOptionAdded:index];
+}
+
+void ChooserBubbleUiCocoa::OnOptionRemoved(int index) {
+ [chooser_bubble_ui_controller_ onOptionRemoved:index];
+}
+
+void ChooserBubbleUiCocoa::OnBubbleClosing() {
+ chooser_bubble_ui_controller_ = nil;
+}

Powered by Google App Engine
This is Rietveld 408576698