1 // Copyright 2013 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 <Cocoa/Cocoa.h>
7 #import "chrome/browser/ui/cocoa/browser/profile_chooser_controller.h"
9 #include "base/mac/bundle_locations.h"
10 #include "base/strings/sys_string_conversions.h"
11 #include "base/strings/utf_string_conversions.h"
12 #include "chrome/browser/browser_process.h"
13 #include "chrome/browser/chrome_notification_types.h"
14 #include "chrome/browser/profiles/avatar_menu.h"
15 #include "chrome/browser/profiles/avatar_menu_observer.h"
16 #include "chrome/browser/profiles/profile_info_cache.h"
17 #include "chrome/browser/profiles/profile_info_util.h"
18 #include "chrome/browser/profiles/profile_manager.h"
19 #include "chrome/browser/profiles/profile_metrics.h"
20 #include "chrome/browser/profiles/profile_window.h"
21 #include "chrome/browser/profiles/profiles_state.h"
22 #include "chrome/browser/signin/mutable_profile_oauth2_token_service.h"
23 #include "chrome/browser/signin/profile_oauth2_token_service.h"
24 #include "chrome/browser/signin/profile_oauth2_token_service_factory.h"
25 #include "chrome/browser/signin/signin_manager.h"
26 #include "chrome/browser/signin/signin_manager_factory.h"
27 #include "chrome/browser/signin/signin_promo.h"
28 #include "chrome/browser/ui/browser.h"
29 #include "chrome/browser/ui/browser_dialogs.h"
30 #include "chrome/browser/ui/browser_window.h"
31 #include "chrome/browser/ui/chrome_style.h"
32 #import "chrome/browser/ui/cocoa/info_bubble_view.h"
33 #import "chrome/browser/ui/cocoa/info_bubble_window.h"
34 #include "chrome/browser/ui/singleton_tabs.h"
35 #include "chrome/common/url_constants.h"
36 #include "content/public/browser/notification_service.h"
37 #include "content/public/browser/web_contents.h"
38 #include "content/public/browser/web_contents_view.h"
39 #include "google_apis/gaia/oauth2_token_service.h"
40 #include "grit/chromium_strings.h"
41 #include "grit/generated_resources.h"
42 #include "grit/theme_resources.h"
43 #include "skia/ext/skia_utils_mac.h"
44 #import "ui/base/cocoa/cocoa_event_utils.h"
45 #import "ui/base/cocoa/controls/blue_label_button.h"
46 #import "ui/base/cocoa/controls/hyperlink_button_cell.h"
47 #import "ui/base/cocoa/hover_image_button.h"
48 #include "ui/base/cocoa/window_size_constants.h"
49 #include "ui/base/l10n/l10n_util.h"
50 #include "ui/base/l10n/l10n_util_mac.h"
51 #include "ui/base/resource/resource_bundle.h"
52 #include "ui/gfx/image/image.h"
53 #include "ui/gfx/text_elider.h"
54 #include "ui/native_theme/native_theme.h"
58 // Constants taken from the Windows/Views implementation at:
59 // chrome/browser/ui/views/profile_chooser_view.cc
60 const int kLargeImageSide = 64;
61 const int kSmallImageSide = 32;
62 const CGFloat kFixedMenuWidth = 250;
64 const CGFloat kVerticalSpacing = 20.0;
65 const CGFloat kSmallVerticalSpacing = 10.0;
66 const CGFloat kHorizontalSpacing = 20.0;
67 const CGFloat kTitleFontSize = 15.0;
68 const CGFloat kTextFontSize = 12.0;
69 const CGFloat kProfileButtonHeight = 30;
70 const int kOverlayHeight = 20; // Height of the "Change" avatar photo overlay.
71 const int kBezelThickness = 3; // Width of the bezel on an NSButton.
72 const int kImageTitleSpacing = 10;
73 const int kBlueButtonHeight = 30;
75 // Minimum size for embedded sign in pages as defined in Gaia.
76 const CGFloat kMinGaiaViewWidth = 320;
77 const CGFloat kMinGaiaViewHeight = 440;
79 gfx::Image CreateProfileImage(const gfx::Image& icon, int imageSize) {
80 return profiles::GetSizedAvatarIconWithBorder(
81 icon, true /* image is a square */,
82 imageSize + profiles::kAvatarIconPadding,
83 imageSize + profiles::kAvatarIconPadding);
86 // Updates the window size and position.
87 void SetWindowSize(NSWindow* window, NSSize size) {
88 NSRect frame = [window frame];
89 frame.origin.x += frame.size.width - size.width;
90 frame.origin.y += frame.size.height - size.height;
92 [window setFrame:frame display:YES];
95 NSString* ElideEmail(const std::string& email, CGFloat width) {
96 base::string16 elidedEmail = gfx::ElideEmail(
97 base::UTF8ToUTF16(email),
98 ui::ResourceBundle::GetSharedInstance().GetFontList(
99 ui::ResourceBundle::BaseFont),
101 return base::SysUTF16ToNSString(elidedEmail);
106 // Class that listens to changes to the OAuth2Tokens for the active profile,
107 // changes to the avatar menu model or browser close notifications.
108 class ActiveProfileObserverBridge : public AvatarMenuObserver,
109 public content::NotificationObserver,
110 public OAuth2TokenService::Observer {
112 ActiveProfileObserverBridge(ProfileChooserController* controller,
114 : controller_(controller),
116 token_observer_registered_(false) {
117 registrar_.Add(this, chrome::NOTIFICATION_BROWSER_CLOSING,
118 content::NotificationService::AllSources());
119 if (!browser_->profile()->IsGuestSession())
120 AddTokenServiceObserver();
123 virtual ~ActiveProfileObserverBridge() {
124 RemoveTokenServiceObserver();
128 void AddTokenServiceObserver() {
129 ProfileOAuth2TokenService* oauth2_token_service =
130 ProfileOAuth2TokenServiceFactory::GetForProfile(browser_->profile());
131 DCHECK(oauth2_token_service);
132 oauth2_token_service->AddObserver(this);
133 token_observer_registered_ = true;
136 void RemoveTokenServiceObserver() {
137 if (!token_observer_registered_)
139 DCHECK(browser_->profile());
140 ProfileOAuth2TokenService* oauth2_token_service =
141 ProfileOAuth2TokenServiceFactory::GetForProfile(browser_->profile());
142 DCHECK(oauth2_token_service);
143 oauth2_token_service->RemoveObserver(this);
144 token_observer_registered_ = false;
147 // OAuth2TokenService::Observer:
148 virtual void OnRefreshTokenAvailable(const std::string& account_id) OVERRIDE {
149 // Tokens can only be added by adding an account through the inline flow,
150 // which is started from the account management view. Refresh it to show the
152 BubbleViewMode viewMode = [controller_ viewMode];
153 if (viewMode == ACCOUNT_MANAGEMENT_VIEW ||
154 viewMode == GAIA_SIGNIN_VIEW ||
155 viewMode == GAIA_ADD_ACCOUNT_VIEW) {
156 [controller_ initMenuContentsWithView:ACCOUNT_MANAGEMENT_VIEW];
160 virtual void OnRefreshTokenRevoked(const std::string& account_id) OVERRIDE {
161 // Tokens can only be removed from the account management view. Refresh it
162 // to show the update.
163 if ([controller_ viewMode] == ACCOUNT_MANAGEMENT_VIEW)
164 [controller_ initMenuContentsWithView:ACCOUNT_MANAGEMENT_VIEW];
167 // AvatarMenuObserver:
168 virtual void OnAvatarMenuChanged(AvatarMenu* avatar_menu) OVERRIDE {
169 // While the bubble is open, the avatar menu can only change from the
170 // profile chooser view by modifying the current profile's photo or name.
171 [controller_ initMenuContentsWithView:PROFILE_CHOOSER_VIEW];
174 // content::NotificationObserver:
175 virtual void Observe(
177 const content::NotificationSource& source,
178 const content::NotificationDetails& details) OVERRIDE {
179 DCHECK_EQ(chrome::NOTIFICATION_BROWSER_CLOSING, type);
180 if (browser_ == content::Source<Browser>(source).ptr()) {
181 RemoveTokenServiceObserver();
182 // Clean up the bubble's WebContents (used by the Gaia embedded view), to
183 // make sure the guest profile doesn't have any dangling host renderers.
184 // This can happen if Chrome is quit using Command-Q while the bubble is
185 // still open, which won't give the bubble a chance to be closed and
186 // clean up the WebContents itself.
187 [controller_ cleanUpEmbeddedViewContents];
191 ProfileChooserController* controller_; // Weak; owns this.
192 Browser* browser_; // Weak.
193 content::NotificationRegistrar registrar_;
195 // The observer can be removed both when closing the browser, and by just
196 // closing the avatar bubble. However, in the case of closing the browser,
197 // the avatar bubble will also be closed afterwards, resulting in a second
198 // attempt to remove the observer. This ensures the observer is only
200 bool token_observer_registered_;
202 DISALLOW_COPY_AND_ASSIGN(ActiveProfileObserverBridge);
205 // A custom button that has a transparent backround.
206 @interface TransparentBackgroundButton : NSButton
209 @implementation TransparentBackgroundButton
210 - (id)initWithFrame:(NSRect)frameRect {
211 if ((self = [super initWithFrame:frameRect])) {
212 [self setBordered:NO];
213 [self setFont:[NSFont labelFontOfSize:kTextFontSize]];
214 [self setButtonType:NSMomentaryChangeButton];
219 - (void)drawRect:(NSRect)dirtyRect {
220 NSColor* backgroundColor = [NSColor colorWithCalibratedWhite:0 alpha:0.5f];
221 [backgroundColor setFill];
222 NSRectFillUsingOperation(dirtyRect, NSCompositeSourceAtop);
223 [super drawRect:dirtyRect];
227 // A custom image control that shows a "Change" button when moused over.
228 @interface EditableProfilePhoto : NSImageView {
230 AvatarMenu* avatarMenu_; // Weak; Owned by ProfileChooserController.
231 base::scoped_nsobject<TransparentBackgroundButton> changePhotoButton_;
232 // Used to display the "Change" button on hover.
233 ui::ScopedCrTrackingArea trackingArea_;
236 - (id)initWithFrame:(NSRect)frameRect
237 avatarMenu:(AvatarMenu*)avatarMenu
238 profileIcon:(const gfx::Image&)profileIcon
239 editingAllowed:(BOOL)editingAllowed;
241 // Called when the "Change" button is clicked.
242 - (void)editPhoto:(id)sender;
244 // When hovering over the profile photo, show the "Change" button.
245 - (void)mouseEntered:(NSEvent*)event;
247 // When hovering away from the profile photo, hide the "Change" button.
248 - (void)mouseExited:(NSEvent*)event;
251 @interface EditableProfilePhoto (Private)
252 // Create the "Change" avatar photo button.
253 - (TransparentBackgroundButton*)changePhotoButtonWithRect:(NSRect)rect;
256 @implementation EditableProfilePhoto
257 - (id)initWithFrame:(NSRect)frameRect
258 avatarMenu:(AvatarMenu*)avatarMenu
259 profileIcon:(const gfx::Image&)profileIcon
260 editingAllowed:(BOOL)editingAllowed {
261 if ((self = [super initWithFrame:frameRect])) {
262 avatarMenu_ = avatarMenu;
263 [self setImage:CreateProfileImage(
264 profileIcon, kLargeImageSide).ToNSImage()];
266 // Add a tracking area so that we can show/hide the button when hovering.
267 trackingArea_.reset([[CrTrackingArea alloc]
268 initWithRect:[self bounds]
269 options:NSTrackingMouseEnteredAndExited | NSTrackingActiveAlways
272 [self addTrackingArea:trackingArea_.get()];
274 if (editingAllowed) {
275 // The avatar photo uses a frame of width profiles::kAvatarIconPadding,
276 // which we must subtract from the button's bounds.
277 changePhotoButton_.reset([self changePhotoButtonWithRect:NSMakeRect(
278 profiles::kAvatarIconPadding, profiles::kAvatarIconPadding,
279 kLargeImageSide - 2 * profiles::kAvatarIconPadding,
281 [self addSubview:changePhotoButton_];
283 // Hide the button until the image is hovered over.
284 [changePhotoButton_ setHidden:YES];
290 - (void)editPhoto:(id)sender {
291 avatarMenu_->EditProfile(avatarMenu_->GetActiveProfileIndex());
294 - (void)mouseEntered:(NSEvent*)event {
295 [changePhotoButton_ setHidden:NO];
298 - (void)mouseExited:(NSEvent*)event {
299 [changePhotoButton_ setHidden:YES];
302 - (TransparentBackgroundButton*)changePhotoButtonWithRect:(NSRect)rect {
303 TransparentBackgroundButton* button =
304 [[TransparentBackgroundButton alloc] initWithFrame:rect];
306 // The button has a centered white text and a transparent background.
307 base::scoped_nsobject<NSMutableParagraphStyle> textStyle(
308 [[NSMutableParagraphStyle alloc] init]);
309 [textStyle setAlignment:NSCenterTextAlignment];
310 NSDictionary* titleAttributes = @{
311 NSParagraphStyleAttributeName : textStyle,
312 NSForegroundColorAttributeName : [NSColor whiteColor]
314 NSString* buttonTitle = l10n_util::GetNSString(
315 IDS_PROFILES_PROFILE_CHANGE_PHOTO_BUTTON);
316 base::scoped_nsobject<NSAttributedString> attributedTitle(
317 [[NSAttributedString alloc] initWithString:buttonTitle
318 attributes:titleAttributes]);
319 [button setAttributedTitle:attributedTitle];
320 [button setTarget:self];
321 [button setAction:@selector(editPhoto:)];
326 // A custom text control that turns into a textfield for editing when clicked.
327 @interface EditableProfileNameButton : HoverImageButton<NSTextFieldDelegate> {
329 base::scoped_nsobject<NSTextField> profileNameTextField_;
330 Profile* profile_; // Weak.
333 - (id)initWithFrame:(NSRect)frameRect
334 profile:(Profile*)profile
335 profileName:(NSString*)profileName
336 editingAllowed:(BOOL)editingAllowed;
338 // Called when the button is clicked.
339 - (void)showEditableView:(id)sender;
341 // Called when the user presses "Enter" in the textfield.
342 - (void)controlTextDidEndEditing:(NSNotification *)obj;
345 @implementation EditableProfileNameButton
346 - (id)initWithFrame:(NSRect)frameRect
347 profile:(Profile*)profile
348 profileName:(NSString*)profileName
349 editingAllowed:(BOOL)editingAllowed {
350 if ((self = [super initWithFrame:frameRect])) {
353 [self setBordered:NO];
354 [self setFont:[NSFont labelFontOfSize:kTitleFontSize]];
355 [self setAlignment:NSLeftTextAlignment];
356 [[self cell] setLineBreakMode:NSLineBreakByTruncatingTail];
357 [self setTitle:profileName];
359 if (editingAllowed) {
360 // Show an "edit" pencil icon when hovering over.
361 ui::ResourceBundle* rb = &ui::ResourceBundle::GetSharedInstance();
363 rb->GetNativeImageNamed(IDR_ICON_PROFILES_EDIT_HOVER).AsNSImage()];
364 [self setAlternateImage:
365 rb->GetNativeImageNamed(IDR_ICON_PROFILES_EDIT_PRESSED).AsNSImage()];
366 [self setImagePosition:NSImageRight];
367 [self setTarget:self];
368 [self setAction:@selector(showEditableView:)];
370 // We need to subtract the width of the bezel from the frame rect, so that
371 // the textfield can take the exact same space as the button.
372 frameRect.size.height -= 2 * kBezelThickness;
373 frameRect.origin = NSMakePoint(0, kBezelThickness);
374 profileNameTextField_.reset(
375 [[NSTextField alloc] initWithFrame:frameRect]);
376 [profileNameTextField_ setStringValue:profileName];
377 [profileNameTextField_ setFont:[NSFont labelFontOfSize:kTitleFontSize]];
378 [profileNameTextField_ setEditable:YES];
379 [profileNameTextField_ setDrawsBackground:YES];
380 [profileNameTextField_ setBezeled:YES];
381 [[profileNameTextField_ cell] setWraps:NO];
382 [[profileNameTextField_ cell] setLineBreakMode:
383 NSLineBreakByTruncatingTail];
384 [profileNameTextField_ setDelegate:self];
385 [self addSubview:profileNameTextField_];
387 // Hide the textfield until the user clicks on the button.
388 [profileNameTextField_ setHidden:YES];
394 // NSTextField objects send an NSNotification to a delegate if
395 // it implements this method:
396 - (void)controlTextDidEndEditing:(NSNotification *)obj {
397 NSString* text = [profileNameTextField_ stringValue];
398 // Empty profile names are not allowed, and are treated as a cancel.
399 if ([text length] > 0) {
400 profiles::UpdateProfileName(profile_, base::SysNSStringToUTF16(text));
401 [self setTitle:text];
403 [profileNameTextField_ setHidden:YES];
404 [profileNameTextField_ resignFirstResponder];
407 - (void)showEditableView:(id)sender {
408 [profileNameTextField_ setHidden:NO];
409 [profileNameTextField_ becomeFirstResponder];
414 // Custom button cell that adds a left padding before the button image, and
415 // a custom spacing between the button image and title.
416 @interface CustomPaddingImageButtonCell : NSButtonCell {
418 // Padding between the left margin of the button and the cell image.
419 int leftMarginSpacing_;
420 // Spacing between the cell image and title.
421 int imageTitleSpacing_;
424 - (id)initWithLeftMarginSpacing:(int)leftMarginSpacing
425 imageTitleSpacing:(int)imageTitleSpacing;
428 @implementation CustomPaddingImageButtonCell
429 - (id)initWithLeftMarginSpacing:(int)leftMarginSpacing
430 imageTitleSpacing:(int)imageTitleSpacing {
431 if ((self = [super init])) {
432 leftMarginSpacing_ = leftMarginSpacing;
433 imageTitleSpacing_ = imageTitleSpacing;
438 - (NSRect)drawTitle:(NSAttributedString*)title
439 withFrame:(NSRect)frame
440 inView:(NSView*)controlView {
441 // The title frame origin isn't aware of the left margin spacing added
442 // in -drawImage, so it must be added when drawing the title as well.
443 frame.origin.x += leftMarginSpacing_ + imageTitleSpacing_;
444 return [super drawTitle:title withFrame:frame inView:controlView];
447 - (void)drawImage:(NSImage*)image
448 withFrame:(NSRect)frame
449 inView:(NSView*)controlView {
450 frame.origin.x = leftMarginSpacing_;
451 [super drawImage:image withFrame:frame inView:controlView];
455 NSSize buttonSize = [super cellSize];
456 buttonSize.width += leftMarginSpacing_ + imageTitleSpacing_;
462 // A custom button that allows for setting a background color when hovered over.
463 @interface BackgroundColorHoverButton : HoverImageButton
466 @implementation BackgroundColorHoverButton
467 - (id)initWithFrame:(NSRect)frameRect {
468 if ((self = [super initWithFrame:frameRect])) {
469 [self setBordered:NO];
470 [self setFont:[NSFont labelFontOfSize:kTextFontSize]];
471 [self setButtonType:NSMomentaryChangeButton];
473 base::scoped_nsobject<CustomPaddingImageButtonCell> cell(
474 [[CustomPaddingImageButtonCell alloc]
475 initWithLeftMarginSpacing:kHorizontalSpacing
476 imageTitleSpacing:kImageTitleSpacing]);
477 [self setCell:cell.get()];
482 - (void)setHoverState:(HoverState)state {
483 [super setHoverState:state];
484 bool isHighlighted = ([self hoverState] != kHoverStateNone);
486 NSColor* backgroundColor = gfx::SkColorToCalibratedNSColor(
487 ui::NativeTheme::instance()->GetSystemColor(isHighlighted?
488 ui::NativeTheme::kColorId_FocusedMenuItemBackgroundColor :
489 ui::NativeTheme::kColorId_MenuBackgroundColor));
491 [[self cell] setBackgroundColor:backgroundColor];
493 // When hovered, the button text should be white.
495 isHighlighted ? [NSColor whiteColor] : [NSColor blackColor];
496 base::scoped_nsobject<NSMutableParagraphStyle> textStyle(
497 [[NSMutableParagraphStyle alloc] init]);
498 [textStyle setAlignment:NSLeftTextAlignment];
500 base::scoped_nsobject<NSAttributedString> attributedTitle(
501 [[NSAttributedString alloc]
502 initWithString:[self title]
503 attributes:@{ NSParagraphStyleAttributeName : textStyle,
504 NSForegroundColorAttributeName : textColor }]);
505 [self setAttributedTitle:attributedTitle];
511 @interface ProfileChooserController ()
512 // Creates the main profile card for the profile |item| at the top of
514 - (NSView*)createCurrentProfileView:(const AvatarMenu::Item&)item;
516 // Creates the possible links for the main profile card with profile |item|.
517 - (NSView*)createCurrentProfileLinksForItem:(const AvatarMenu::Item&)item
518 withXOffset:(CGFloat)xOffset;
520 // Creates a main profile card for the guest user.
521 - (NSView*)createGuestProfileView;
523 // Creates an item for the profile |itemIndex| that is used in the fast profile
524 // switcher in the middle of the bubble.
525 - (NSButton*)createOtherProfileView:(int)itemIndex;
527 // Creates the Guest / Add person / View all persons buttons.
528 - (NSView*)createOptionsViewWithRect:(NSRect)rect;
530 // Creates the account management view for the active profile.
531 - (NSView*)createCurrentProfileAccountsView:(NSRect)rect;
533 // Creates the list of accounts for the active profile.
534 - (NSView*)createAccountsListWithRect:(NSRect)rect;
536 // Creates the Gaia sign-in/add account view.
537 - (NSView*)createGaiaEmbeddedView;
539 // Creates a button with text given by |textResourceId|, an icon given by
540 // |imageResourceId| and with |action|. The icon |alternateImageResourceId| is
541 // displayed in the button's hovered and pressed states.
542 - (NSButton*)hoverButtonWithRect:(NSRect)rect
543 textResourceId:(int)textResourceId
544 imageResourceId:(int)imageResourceId
545 alternateImageResourceId:(int)alternateImageResourceId
548 // Creates a generic link button with |title| and an |action| positioned at
550 - (NSButton*)linkButtonWithTitle:(NSString*)title
551 frameOrigin:(NSPoint)frameOrigin
554 // Creates an email account button with |title|. If |canBeDeleted| is YES, then
555 // the button is clickable and has a remove icon.
556 - (NSButton*)accountButtonWithRect:(NSRect)rect
557 title:(const std::string&)title
558 canBeDeleted:(BOOL)canBeDeleted;
562 @implementation ProfileChooserController
563 - (BubbleViewMode) viewMode {
567 - (IBAction)addNewProfile:(id)sender {
568 profiles::CreateAndSwitchToNewProfile(
569 browser_->host_desktop_type(),
570 profiles::ProfileSwitchingDoneCallback(),
571 ProfileMetrics::ADD_NEW_USER_ICON);
574 - (IBAction)switchToProfile:(id)sender {
575 // Check the event flags to see if a new window should be created.
576 bool always_create = ui::WindowOpenDispositionFromNSEvent(
577 [NSApp currentEvent]) == NEW_WINDOW;
578 avatarMenu_->SwitchToProfile([sender tag], always_create,
579 ProfileMetrics::SWITCH_PROFILE_ICON);
582 - (IBAction)showUserManager:(id)sender {
583 // Only non-guest users appear in the User Manager.
584 base::FilePath profile_path;
585 if (!isGuestSession_) {
586 size_t active_index = avatarMenu_->GetActiveProfileIndex();
587 profile_path = avatarMenu_->GetItemAt(active_index).profile_path;
589 chrome::ShowUserManager(profile_path);
592 - (IBAction)switchToGuestProfile:(id)sender {
593 profiles::SwitchToGuestProfile(browser_->host_desktop_type(),
594 profiles::ProfileSwitchingDoneCallback());
597 - (IBAction)exitGuestProfile:(id)sender {
598 profiles::CloseGuestProfileWindows();
601 - (IBAction)showAccountManagement:(id)sender {
602 [self initMenuContentsWithView:ACCOUNT_MANAGEMENT_VIEW];
605 - (IBAction)lockProfile:(id)sender {
606 profiles::LockProfile(browser_->profile());
609 - (IBAction)showSigninPage:(id)sender {
610 [self initMenuContentsWithView:GAIA_SIGNIN_VIEW];
613 - (IBAction)addAccount:(id)sender {
614 [self initMenuContentsWithView:GAIA_ADD_ACCOUNT_VIEW];
617 - (IBAction)removeAccount:(id)sender {
618 DCHECK(!isGuestSession_);
619 DCHECK_GE([sender tag], 0); // Should not be called for the primary account.
620 DCHECK(ContainsKey(currentProfileAccounts_, [sender tag]));
621 std::string account = currentProfileAccounts_[[sender tag]];
622 ProfileOAuth2TokenServiceFactory::GetPlatformSpecificForProfile(
623 browser_->profile())->RevokeCredentials(account);
626 - (void)cleanUpEmbeddedViewContents {
627 webContents_.reset();
630 - (id)initWithBrowser:(Browser*)browser anchoredAt:(NSPoint)point {
631 base::scoped_nsobject<InfoBubbleWindow> window([[InfoBubbleWindow alloc]
632 initWithContentRect:ui::kWindowSizeDeterminedLater
633 styleMask:NSBorderlessWindowMask
634 backing:NSBackingStoreBuffered
637 if ((self = [super initWithWindow:window
638 parentWindow:browser->window()->GetNativeWindow()
639 anchoredAt:point])) {
641 viewMode_ = PROFILE_CHOOSER_VIEW;
642 observer_.reset(new ActiveProfileObserverBridge(self, browser_));
644 avatarMenu_.reset(new AvatarMenu(
645 &g_browser_process->profile_manager()->GetProfileInfoCache(),
648 avatarMenu_->RebuildMenu();
650 // Guest profiles do not have a token service.
651 isGuestSession_ = browser_->profile()->IsGuestSession();
653 [[self bubble] setArrowLocation:info_bubble::kTopRight];
654 [self initMenuContentsWithView:viewMode_];
660 - (void)initMenuContentsWithView:(BubbleViewMode)viewToDisplay {
661 viewMode_ = viewToDisplay;
662 NSView* contentView = [[self window] contentView];
663 [contentView setSubviews:[NSArray array]];
665 if (viewMode_ == GAIA_SIGNIN_VIEW || viewMode_ == GAIA_ADD_ACCOUNT_VIEW) {
666 [contentView addSubview:[self createGaiaEmbeddedView]];
667 SetWindowSize([self window],
668 NSMakeSize(kMinGaiaViewWidth, kMinGaiaViewHeight));
672 NSView* currentProfileView = nil;
673 base::scoped_nsobject<NSMutableArray> otherProfiles(
674 [[NSMutableArray alloc] init]);
676 // Loop over the profiles in reverse, so that they are sorted by their
677 // y-coordinate, and separate them into active and "other" profiles.
678 for (int i = avatarMenu_->GetNumberOfItems() - 1; i >= 0; --i) {
679 const AvatarMenu::Item& item = avatarMenu_->GetItemAt(i);
681 currentProfileView = [self createCurrentProfileView:item];
683 [otherProfiles addObject:[self createOtherProfileView:i]];
686 if (!currentProfileView) // Guest windows don't have an active profile.
687 currentProfileView = [self createGuestProfileView];
689 // |yOffset| is the next position at which to draw in |contentView|
691 CGFloat yOffset = kSmallVerticalSpacing;
693 // Guest / Add Person / View All Persons buttons.
694 NSView* optionsView = [self createOptionsViewWithRect:
695 NSMakeRect(0, yOffset, kFixedMenuWidth, 0)];
696 [contentView addSubview:optionsView];
697 yOffset = NSMaxY([optionsView frame]) + kSmallVerticalSpacing;
699 NSBox* separator = [self separatorWithFrame:
700 NSMakeRect(0, yOffset, kFixedMenuWidth, 0)];
701 [contentView addSubview:separator];
702 yOffset = NSMaxY([separator frame]) + kVerticalSpacing;
704 if (viewToDisplay == PROFILE_CHOOSER_VIEW) {
705 // Other profiles switcher. The profiles have already been sorted
706 // by their y-coordinate, so they can be added in the existing order.
707 for (NSView *otherProfileView in otherProfiles.get()) {
708 [otherProfileView setFrameOrigin:NSMakePoint(kHorizontalSpacing,
710 [contentView addSubview:otherProfileView];
711 yOffset = NSMaxY([otherProfileView frame]) + kSmallVerticalSpacing;
714 // If we displayed other profiles, ensure the spacing between the last item
715 // and the active profile card is the same as the spacing between the active
716 // profile card and the bottom of the bubble.
717 if ([otherProfiles.get() count] > 0)
718 yOffset += kSmallVerticalSpacing;
719 } else if (viewToDisplay == ACCOUNT_MANAGEMENT_VIEW) {
720 NSView* currentProfileAccountsView = [self createCurrentProfileAccountsView:
721 NSMakeRect(kHorizontalSpacing,
723 kFixedMenuWidth - 2 * kHorizontalSpacing,
725 [contentView addSubview:currentProfileAccountsView];
726 yOffset = NSMaxY([currentProfileAccountsView frame]) + kVerticalSpacing;
728 NSBox* accountsSeparator = [self separatorWithFrame:
729 NSMakeRect(0, yOffset, kFixedMenuWidth, 0)];
730 [contentView addSubview:accountsSeparator];
731 yOffset = NSMaxY([accountsSeparator frame]) + kVerticalSpacing;
734 // Active profile card.
735 if (currentProfileView) {
736 [currentProfileView setFrameOrigin:NSMakePoint(kHorizontalSpacing,
738 [contentView addSubview:currentProfileView];
739 yOffset = NSMaxY([currentProfileView frame]) + kVerticalSpacing;
742 SetWindowSize([self window], NSMakeSize(kFixedMenuWidth, yOffset));
745 - (NSView*)createCurrentProfileView:(const AvatarMenu::Item&)item {
746 base::scoped_nsobject<NSView> container([[NSView alloc]
747 initWithFrame:NSZeroRect]);
750 base::scoped_nsobject<EditableProfilePhoto> iconView(
751 [[EditableProfilePhoto alloc]
752 initWithFrame:NSMakeRect(0, 0, kLargeImageSide, kLargeImageSide)
753 avatarMenu:avatarMenu_.get()
754 profileIcon:item.icon
755 editingAllowed:!isGuestSession_]);
757 [container addSubview:iconView];
759 CGFloat xOffset = NSMaxX([iconView frame]) + kHorizontalSpacing;
760 CGFloat yOffset = kVerticalSpacing;
761 if (!isGuestSession_ && viewMode_ == PROFILE_CHOOSER_VIEW) {
762 NSView* linksContainer =
763 [self createCurrentProfileLinksForItem:item withXOffset:xOffset];
764 [container addSubview:linksContainer];
765 yOffset = NSMaxY([linksContainer frame]);
769 CGFloat availableTextWidth =
770 kFixedMenuWidth - xOffset - 2 * kHorizontalSpacing;
771 base::scoped_nsobject<EditableProfileNameButton> profileName(
772 [[EditableProfileNameButton alloc]
773 initWithFrame:NSMakeRect(xOffset, yOffset,
775 kProfileButtonHeight)
776 profile:browser_->profile()
777 profileName:base::SysUTF16ToNSString(item.name)
778 editingAllowed:!isGuestSession_]);
780 [container addSubview:profileName];
781 [container setFrameSize:NSMakeSize(kFixedMenuWidth,
782 NSHeight([iconView frame]))];
783 return container.autorelease();
786 - (NSView*)createCurrentProfileLinksForItem:(const AvatarMenu::Item&)item
787 withXOffset:(CGFloat)xOffset {
788 base::scoped_nsobject<NSView> container([[NSView alloc]
789 initWithFrame:NSZeroRect]);
790 CGFloat maxX = 0; // Ensure the container is wide enough for all the links.
793 // The available links depend on the type of profile that is active.
794 if (item.signed_in) {
796 // We need to display 2 links instead of 1, so make the padding in between
797 // the links even smaller to fit.
798 const CGFloat kLinkSpacing = kSmallVerticalSpacing / 2;
799 NSButton* manageAccountsLink =
800 [self linkButtonWithTitle:l10n_util::GetNSString(
801 IDS_PROFILES_PROFILE_MANAGE_ACCOUNTS_BUTTON)
802 frameOrigin:NSMakePoint(xOffset, yOffset)
803 action:@selector(showAccountManagement:)];
804 yOffset = NSMaxY([manageAccountsLink frame]) + kLinkSpacing;
806 NSButton* signOutLink =
807 [self linkButtonWithTitle:l10n_util::GetNSString(
808 IDS_PROFILES_PROFILE_SIGNOUT_BUTTON)
809 frameOrigin:NSMakePoint(xOffset, yOffset)
810 action:@selector(lockProfile:)];
811 yOffset = NSMaxY([signOutLink frame]);
813 maxX = std::max(NSMaxX([manageAccountsLink frame]),
814 NSMaxX([signOutLink frame]));
815 [container addSubview:manageAccountsLink];
816 [container addSubview:signOutLink];
818 yOffset = kSmallVerticalSpacing;
819 NSButton* signInLink =
820 [self linkButtonWithTitle:l10n_util::GetNSStringFWithFixup(
821 IDS_SYNC_START_SYNC_BUTTON_LABEL,
822 l10n_util::GetStringUTF16(IDS_SHORT_PRODUCT_NAME))
823 frameOrigin:NSMakePoint(xOffset, yOffset)
824 action:@selector(showSigninPage:)];
825 yOffset = NSMaxY([signInLink frame]) + kSmallVerticalSpacing;
826 maxX = NSMaxX([signInLink frame]);
828 [container addSubview:signInLink];
831 [container setFrameSize:NSMakeSize(maxX, yOffset)];
832 return container.autorelease();
835 - (NSView*)createGuestProfileView {
836 gfx::Image guestIcon =
837 ui::ResourceBundle::GetSharedInstance().GetNativeImageNamed(
839 AvatarMenu::Item guestItem(std::string::npos, /* menu_index, not used */
840 std::string::npos, /* profile_index, not used */
842 guestItem.active = true;
843 guestItem.name = base::SysNSStringToUTF16(
844 l10n_util::GetNSString(IDS_PROFILES_GUEST_PROFILE_NAME));
846 return [self createCurrentProfileView:guestItem];
849 - (NSButton*)createOtherProfileView:(int)itemIndex {
850 const AvatarMenu::Item& item = avatarMenu_->GetItemAt(itemIndex);
851 base::scoped_nsobject<NSButton> profileButton([[NSButton alloc]
852 initWithFrame:NSZeroRect]);
853 base::scoped_nsobject<CustomPaddingImageButtonCell> cell(
854 [[CustomPaddingImageButtonCell alloc]
855 initWithLeftMarginSpacing:0
856 imageTitleSpacing:kImageTitleSpacing]);
857 [profileButton setCell:cell.get()];
859 [[profileButton cell] setLineBreakMode:NSLineBreakByTruncatingTail];
860 [profileButton setTitle:base::SysUTF16ToNSString(item.name)];
861 [profileButton setImage:CreateProfileImage(
862 item.icon, kSmallImageSide).ToNSImage()];
863 [profileButton setImagePosition:NSImageLeft];
864 [profileButton setAlignment:NSLeftTextAlignment];
865 [profileButton setBordered:NO];
866 [profileButton setFont:[NSFont labelFontOfSize:kTitleFontSize]];
867 [profileButton setTag:itemIndex];
868 [profileButton setTarget:self];
869 [profileButton setAction:@selector(switchToProfile:)];
871 // Since the bubble is fixed width, we need to calculate the width available
872 // for the profile name, as longer names will have to be elided.
873 CGFloat availableTextWidth = kFixedMenuWidth - 2 * kHorizontalSpacing;
874 [profileButton sizeToFit];
875 [profileButton setFrameSize:NSMakeSize(availableTextWidth,
876 NSHeight([profileButton frame]))];
878 return profileButton.autorelease();
881 - (NSView*)createOptionsViewWithRect:(NSRect)rect {
882 NSRect viewRect = NSMakeRect(0, 0, rect.size.width, kBlueButtonHeight);
883 NSButton* allUsersButton =
884 [self hoverButtonWithRect:viewRect
885 textResourceId:IDS_PROFILES_ALL_PEOPLE_BUTTON
886 imageResourceId:IDR_ICON_PROFILES_ADD_USER
887 alternateImageResourceId:IDR_ICON_PROFILES_ADD_USER_WHITE
888 action:@selector(showUserManager:)];
889 viewRect.origin.y = NSMaxY([allUsersButton frame]);
891 NSButton* addUserButton =
892 [self hoverButtonWithRect:viewRect
893 textResourceId:IDS_PROFILES_ADD_PERSON_BUTTON
894 imageResourceId:IDR_ICON_PROFILES_ADD_USER
895 alternateImageResourceId:IDR_ICON_PROFILES_ADD_USER_WHITE
896 action:@selector(addNewProfile:)];
897 viewRect.origin.y = NSMaxY([addUserButton frame]);
899 int guestButtonText = isGuestSession_ ? IDS_PROFILES_EXIT_GUEST_BUTTON :
900 IDS_PROFILES_GUEST_BUTTON;
901 SEL guestButtonAction = isGuestSession_ ? @selector(exitGuestProfile:) :
902 @selector(switchToGuestProfile:);
903 NSButton* guestButton =
904 [self hoverButtonWithRect:viewRect
905 textResourceId:guestButtonText
906 imageResourceId:IDR_ICON_PROFILES_BROWSE_GUEST
907 alternateImageResourceId:IDR_ICON_PROFILES_BROWSE_GUEST_WHITE
908 action:guestButtonAction];
909 rect.size.height = NSMaxY([guestButton frame]);
910 base::scoped_nsobject<NSView> container([[NSView alloc]
911 initWithFrame:rect]);
912 [container setSubviews:@[allUsersButton, addUserButton, guestButton]];
913 return container.autorelease();
916 - (NSView*)createCurrentProfileAccountsView:(NSRect)rect {
917 const CGFloat kAccountButtonHeight = 15;
919 const AvatarMenu::Item& item =
920 avatarMenu_->GetItemAt(avatarMenu_->GetActiveProfileIndex());
921 DCHECK(item.signed_in);
923 base::scoped_nsobject<NSView> container([[NSView alloc] initWithFrame:rect]);
925 NSRect viewRect = NSMakeRect(0, 0, rect.size.width, kBlueButtonHeight);
926 base::scoped_nsobject<NSButton> addAccountsButton([[BlueLabelButton alloc]
927 initWithFrame:viewRect]);
929 // Manually elide the button text so that the contents fit inside the bubble.
930 // This is needed because the BlueLabelButton cell resets the style on
931 // every call to -cellSize, which prevents setting a custom lineBreakMode.
932 NSString* elidedButtonText = base::SysUTF16ToNSString(gfx::ElideText(
933 l10n_util::GetStringFUTF16(
934 IDS_PROFILES_PROFILE_ADD_ACCOUNT_BUTTON, item.name),
935 ui::ResourceBundle::GetSharedInstance().GetFontList(
936 ui::ResourceBundle::BaseFont),
940 [addAccountsButton setTitle:elidedButtonText];
941 [addAccountsButton setTarget:self];
942 [addAccountsButton setAction:@selector(addAccount:)];
943 [container addSubview:addAccountsButton];
945 // Update the height of the email account buttons. This is needed so that the
946 // all the buttons span the entire width of the bubble.
947 viewRect.origin.y = NSMaxY([addAccountsButton frame]) + kVerticalSpacing;
948 viewRect.size.height = kAccountButtonHeight;
950 NSView* accountEmails = [self createAccountsListWithRect:viewRect];
951 [container addSubview:accountEmails];
952 [container setFrameSize:NSMakeSize(
953 NSWidth([container frame]), NSMaxY([accountEmails frame]))];
954 return container.autorelease();
957 - (NSView*)createAccountsListWithRect:(NSRect)rect {
958 base::scoped_nsobject<NSView> container([[NSView alloc] initWithFrame:rect]);
959 currentProfileAccounts_.clear();
961 Profile* profile = browser_->profile();
962 std::string primaryAccount =
963 SigninManagerFactory::GetForProfile(profile)->GetAuthenticatedUsername();
964 DCHECK(!primaryAccount.empty());
965 std::vector<std::string>accounts =
966 profiles::GetSecondaryAccountsForProfile(profile, primaryAccount);
969 for (size_t i = 0; i < accounts.size(); ++i) {
970 // Save the original email address, as the button text could be elided.
971 currentProfileAccounts_[i] = accounts[i];
972 NSButton* accountButton = [self accountButtonWithRect:rect
975 [accountButton setTag:i];
976 [container addSubview:accountButton];
977 rect.origin.y = NSMaxY([accountButton frame]) + kSmallVerticalSpacing;
980 // The primary account should always be listed first. It doesn't need a tag,
981 // as it cannot be removed.
982 // TODO(rogerta): we still need to further differentiate the primary account
983 // from the others in the UI, so more work is likely required here:
985 NSButton* accountButton = [self accountButtonWithRect:rect
988 [container addSubview:accountButton];
989 [container setFrameSize:NSMakeSize(NSWidth([container frame]),
990 NSMaxY([accountButton frame]))];
991 return container.autorelease();
994 - (NSView*) createGaiaEmbeddedView {
995 signin::Source source = (viewMode_ == GAIA_SIGNIN_VIEW) ?
996 signin::SOURCE_AVATAR_BUBBLE_SIGN_IN :
997 signin::SOURCE_AVATAR_BUBBLE_ADD_ACCOUNT;
999 webContents_.reset(content::WebContents::Create(
1000 content::WebContents::CreateParams(browser_->profile())));
1001 webContents_->GetController().LoadURL(
1002 signin::GetPromoURL(source, false),
1003 content::Referrer(),
1004 content::PAGE_TRANSITION_AUTO_TOPLEVEL,
1006 NSView* webview = webContents_->GetView()->GetNativeView();
1007 [webview setFrameSize:NSMakeSize(kMinGaiaViewWidth, kMinGaiaViewHeight)];
1011 - (NSButton*)hoverButtonWithRect:(NSRect)rect
1012 textResourceId:(int)textResourceId
1013 imageResourceId:(int)imageResourceId
1014 alternateImageResourceId:(int)alternateImageResourceId
1015 action:(SEL)action {
1016 base::scoped_nsobject<BackgroundColorHoverButton> button(
1017 [[BackgroundColorHoverButton alloc] initWithFrame:rect]);
1019 [button setTitle:l10n_util::GetNSString(textResourceId)];
1020 ui::ResourceBundle* rb = &ui::ResourceBundle::GetSharedInstance();
1021 NSImage* alternateImage = rb->GetNativeImageNamed(
1022 alternateImageResourceId).ToNSImage();
1023 [button setDefaultImage:rb->GetNativeImageNamed(imageResourceId).ToNSImage()];
1024 [button setHoverImage:alternateImage];
1025 [button setPressedImage:alternateImage];
1026 [button setImagePosition:NSImageLeft];
1027 [button setAlignment:NSLeftTextAlignment];
1028 [button setBordered:NO];
1029 [button setTarget:self];
1030 [button setAction:action];
1032 return button.autorelease();
1035 - (NSButton*)linkButtonWithTitle:(NSString*)title
1036 frameOrigin:(NSPoint)frameOrigin
1037 action:(SEL)action {
1038 base::scoped_nsobject<NSButton> link(
1039 [[HyperlinkButtonCell buttonWithString:title] retain]);
1041 [[link cell] setShouldUnderline:NO];
1042 [[link cell] setTextColor:gfx::SkColorToCalibratedNSColor(
1043 chrome_style::GetLinkColor())];
1044 [link setTitle:title];
1045 [link setBordered:NO];
1046 [link setFont:[NSFont labelFontOfSize:kTextFontSize]];
1047 [link setTarget:self];
1048 [link setAction:action];
1049 [link setFrameOrigin:frameOrigin];
1052 return link.autorelease();
1055 - (NSButton*)accountButtonWithRect:(NSRect)rect
1056 title:(const std::string&)title
1057 canBeDeleted:(BOOL)canBeDeleted {
1058 base::scoped_nsobject<NSButton> button([[NSButton alloc] initWithFrame:rect]);
1059 [button setTitle:ElideEmail(title, rect.size.width)];
1060 [button setAlignment:NSLeftTextAlignment];
1061 [button setBordered:NO];
1064 [button setImage:ui::ResourceBundle::GetSharedInstance().
1065 GetNativeImageNamed(IDR_CLOSE_1).ToNSImage()];
1066 [button setImagePosition:NSImageRight];
1067 [button setTarget:self];
1068 [button setAction:@selector(removeAccount:)];
1071 return button.autorelease();