Upstream version 5.34.104.0
[platform/framework/web/crosswalk.git] / src / chrome / browser / ui / cocoa / browser / profile_chooser_controller.mm
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.
4
5 #import <Cocoa/Cocoa.h>
6
7 #import "chrome/browser/ui/cocoa/browser/profile_chooser_controller.h"
8
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"
55
56 namespace {
57
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;
63
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;
74
75 // Minimum size for embedded sign in pages as defined in Gaia.
76 const CGFloat kMinGaiaViewWidth = 320;
77 const CGFloat kMinGaiaViewHeight = 440;
78
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);
84 }
85
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;
91   frame.size = size;
92   [window setFrame:frame display:YES];
93 }
94
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),
100       width);
101   return base::SysUTF16ToNSString(elidedEmail);
102 }
103
104 }  // namespace
105
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 {
111  public:
112   ActiveProfileObserverBridge(ProfileChooserController* controller,
113                               Browser* browser)
114       : controller_(controller),
115         browser_(browser),
116         token_observer_registered_(false) {
117     registrar_.Add(this, chrome::NOTIFICATION_BROWSER_CLOSING,
118                    content::NotificationService::AllSources());
119     if (!browser_->profile()->IsGuestSession())
120       AddTokenServiceObserver();
121   }
122
123   virtual ~ActiveProfileObserverBridge() {
124     RemoveTokenServiceObserver();
125   }
126
127  private:
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;
134   }
135
136   void RemoveTokenServiceObserver() {
137     if (!token_observer_registered_)
138       return;
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;
145   }
146
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
151     // update.
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];
157     }
158   }
159
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];
165   }
166
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];
172   }
173
174   // content::NotificationObserver:
175   virtual void Observe(
176       int type,
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];
188     }
189   }
190
191   ProfileChooserController* controller_;  // Weak; owns this.
192   Browser* browser_;  // Weak.
193   content::NotificationRegistrar registrar_;
194
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
199   // removed once.
200   bool token_observer_registered_;
201
202   DISALLOW_COPY_AND_ASSIGN(ActiveProfileObserverBridge);
203 };
204
205 // A custom button that has a transparent backround.
206 @interface TransparentBackgroundButton : NSButton
207 @end
208
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];
215   }
216   return self;
217 }
218
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];
224 }
225 @end
226
227 // A custom image control that shows a "Change" button when moused over.
228 @interface EditableProfilePhoto : NSImageView {
229  @private
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_;
234 }
235
236 - (id)initWithFrame:(NSRect)frameRect
237          avatarMenu:(AvatarMenu*)avatarMenu
238         profileIcon:(const gfx::Image&)profileIcon
239      editingAllowed:(BOOL)editingAllowed;
240
241 // Called when the "Change" button is clicked.
242 - (void)editPhoto:(id)sender;
243
244 // When hovering over the profile photo, show the "Change" button.
245 - (void)mouseEntered:(NSEvent*)event;
246
247 // When hovering away from the profile photo, hide the "Change" button.
248 - (void)mouseExited:(NSEvent*)event;
249 @end
250
251 @interface EditableProfilePhoto (Private)
252 // Create the "Change" avatar photo button.
253 - (TransparentBackgroundButton*)changePhotoButtonWithRect:(NSRect)rect;
254 @end
255
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()];
265
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
270                owner:self
271             userInfo:nil]);
272     [self addTrackingArea:trackingArea_.get()];
273
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,
280           kOverlayHeight)]);
281       [self addSubview:changePhotoButton_];
282
283       // Hide the button until the image is hovered over.
284       [changePhotoButton_ setHidden:YES];
285     }
286   }
287   return self;
288 }
289
290 - (void)editPhoto:(id)sender {
291   avatarMenu_->EditProfile(avatarMenu_->GetActiveProfileIndex());
292 }
293
294 - (void)mouseEntered:(NSEvent*)event {
295   [changePhotoButton_ setHidden:NO];
296 }
297
298 - (void)mouseExited:(NSEvent*)event {
299   [changePhotoButton_ setHidden:YES];
300 }
301
302 - (TransparentBackgroundButton*)changePhotoButtonWithRect:(NSRect)rect {
303   TransparentBackgroundButton* button =
304       [[TransparentBackgroundButton alloc] initWithFrame:rect];
305
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]
313   };
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:)];
322   return button;
323 }
324 @end
325
326 // A custom text control that turns into a textfield for editing when clicked.
327 @interface EditableProfileNameButton : HoverImageButton<NSTextFieldDelegate> {
328  @private
329   base::scoped_nsobject<NSTextField> profileNameTextField_;
330   Profile* profile_;  // Weak.
331 }
332
333 - (id)initWithFrame:(NSRect)frameRect
334             profile:(Profile*)profile
335         profileName:(NSString*)profileName
336      editingAllowed:(BOOL)editingAllowed;
337
338 // Called when the button is clicked.
339 - (void)showEditableView:(id)sender;
340
341 // Called when the user presses "Enter" in the textfield.
342 - (void)controlTextDidEndEditing:(NSNotification *)obj;
343 @end
344
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])) {
351     profile_ = profile;
352
353     [self setBordered:NO];
354     [self setFont:[NSFont labelFontOfSize:kTitleFontSize]];
355     [self setAlignment:NSLeftTextAlignment];
356     [[self cell] setLineBreakMode:NSLineBreakByTruncatingTail];
357     [self setTitle:profileName];
358
359     if (editingAllowed) {
360       // Show an "edit" pencil icon when hovering over.
361       ui::ResourceBundle* rb = &ui::ResourceBundle::GetSharedInstance();
362       [self setHoverImage:
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:)];
369
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_];
386
387       // Hide the textfield until the user clicks on the button.
388       [profileNameTextField_ setHidden:YES];
389     }
390   }
391   return self;
392 }
393
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];
402   }
403   [profileNameTextField_ setHidden:YES];
404   [profileNameTextField_ resignFirstResponder];
405 }
406
407 - (void)showEditableView:(id)sender {
408   [profileNameTextField_ setHidden:NO];
409   [profileNameTextField_ becomeFirstResponder];
410 }
411
412 @end
413
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 {
417  @private
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_;
422 }
423
424 - (id)initWithLeftMarginSpacing:(int)leftMarginSpacing
425               imageTitleSpacing:(int)imageTitleSpacing;
426 @end
427
428 @implementation CustomPaddingImageButtonCell
429 - (id)initWithLeftMarginSpacing:(int)leftMarginSpacing
430               imageTitleSpacing:(int)imageTitleSpacing {
431   if ((self = [super init])) {
432     leftMarginSpacing_ = leftMarginSpacing;
433     imageTitleSpacing_ = imageTitleSpacing;
434   }
435   return self;
436 }
437
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];
445 }
446
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];
452 }
453
454 - (NSSize)cellSize {
455   NSSize buttonSize = [super cellSize];
456   buttonSize.width += leftMarginSpacing_ + imageTitleSpacing_;
457   return buttonSize;
458 }
459
460 @end
461
462 // A custom button that allows for setting a background color when hovered over.
463 @interface BackgroundColorHoverButton : HoverImageButton
464 @end
465
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];
472
473     base::scoped_nsobject<CustomPaddingImageButtonCell> cell(
474         [[CustomPaddingImageButtonCell alloc]
475             initWithLeftMarginSpacing:kHorizontalSpacing
476                     imageTitleSpacing:kImageTitleSpacing]);
477     [self setCell:cell.get()];
478   }
479   return self;
480 }
481
482 - (void)setHoverState:(HoverState)state {
483   [super setHoverState:state];
484   bool isHighlighted = ([self hoverState] != kHoverStateNone);
485
486   NSColor* backgroundColor = gfx::SkColorToCalibratedNSColor(
487       ui::NativeTheme::instance()->GetSystemColor(isHighlighted?
488           ui::NativeTheme::kColorId_FocusedMenuItemBackgroundColor :
489           ui::NativeTheme::kColorId_MenuBackgroundColor));
490
491   [[self cell] setBackgroundColor:backgroundColor];
492
493   // When hovered, the button text should be white.
494   NSColor* textColor =
495       isHighlighted ? [NSColor whiteColor] : [NSColor blackColor];
496   base::scoped_nsobject<NSMutableParagraphStyle> textStyle(
497       [[NSMutableParagraphStyle alloc] init]);
498   [textStyle setAlignment:NSLeftTextAlignment];
499
500   base::scoped_nsobject<NSAttributedString> attributedTitle(
501       [[NSAttributedString alloc]
502           initWithString:[self title]
503               attributes:@{ NSParagraphStyleAttributeName : textStyle,
504                             NSForegroundColorAttributeName : textColor }]);
505   [self setAttributedTitle:attributedTitle];
506 }
507
508 @end
509
510
511 @interface ProfileChooserController ()
512 // Creates the main profile card for the profile |item| at the top of
513 // the bubble.
514 - (NSView*)createCurrentProfileView:(const AvatarMenu::Item&)item;
515
516 // Creates the possible links for the main profile card with profile |item|.
517 - (NSView*)createCurrentProfileLinksForItem:(const AvatarMenu::Item&)item
518                                 withXOffset:(CGFloat)xOffset;
519
520 // Creates a main profile card for the guest user.
521 - (NSView*)createGuestProfileView;
522
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;
526
527 // Creates the Guest / Add person / View all persons buttons.
528 - (NSView*)createOptionsViewWithRect:(NSRect)rect;
529
530 // Creates the account management view for the active profile.
531 - (NSView*)createCurrentProfileAccountsView:(NSRect)rect;
532
533 // Creates the list of accounts for the active profile.
534 - (NSView*)createAccountsListWithRect:(NSRect)rect;
535
536 // Creates the Gaia sign-in/add account view.
537 - (NSView*)createGaiaEmbeddedView;
538
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
546                           action:(SEL)action;
547
548 // Creates a generic link button with |title| and an |action| positioned at
549 // |frameOrigin|.
550 - (NSButton*)linkButtonWithTitle:(NSString*)title
551                      frameOrigin:(NSPoint)frameOrigin
552                           action:(SEL)action;
553
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;
559
560 @end
561
562 @implementation ProfileChooserController
563 - (BubbleViewMode) viewMode {
564   return viewMode_;
565 }
566
567 - (IBAction)addNewProfile:(id)sender {
568   profiles::CreateAndSwitchToNewProfile(
569       browser_->host_desktop_type(),
570       profiles::ProfileSwitchingDoneCallback(),
571       ProfileMetrics::ADD_NEW_USER_ICON);
572 }
573
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);
580 }
581
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;
588   }
589   chrome::ShowUserManager(profile_path);
590 }
591
592 - (IBAction)switchToGuestProfile:(id)sender {
593   profiles::SwitchToGuestProfile(browser_->host_desktop_type(),
594                                  profiles::ProfileSwitchingDoneCallback());
595 }
596
597 - (IBAction)exitGuestProfile:(id)sender {
598   profiles::CloseGuestProfileWindows();
599 }
600
601 - (IBAction)showAccountManagement:(id)sender {
602   [self initMenuContentsWithView:ACCOUNT_MANAGEMENT_VIEW];
603 }
604
605 - (IBAction)lockProfile:(id)sender {
606   profiles::LockProfile(browser_->profile());
607 }
608
609 - (IBAction)showSigninPage:(id)sender {
610   [self initMenuContentsWithView:GAIA_SIGNIN_VIEW];
611 }
612
613 - (IBAction)addAccount:(id)sender {
614   [self initMenuContentsWithView:GAIA_ADD_ACCOUNT_VIEW];
615 }
616
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);
624 }
625
626 - (void)cleanUpEmbeddedViewContents {
627   webContents_.reset();
628 }
629
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
635                     defer:NO]);
636
637   if ((self = [super initWithWindow:window
638                        parentWindow:browser->window()->GetNativeWindow()
639                          anchoredAt:point])) {
640     browser_ = browser;
641     viewMode_ = PROFILE_CHOOSER_VIEW;
642     observer_.reset(new ActiveProfileObserverBridge(self, browser_));
643
644     avatarMenu_.reset(new AvatarMenu(
645         &g_browser_process->profile_manager()->GetProfileInfoCache(),
646         observer_.get(),
647         browser_));
648     avatarMenu_->RebuildMenu();
649
650     // Guest profiles do not have a token service.
651     isGuestSession_ = browser_->profile()->IsGuestSession();
652
653     [[self bubble] setArrowLocation:info_bubble::kTopRight];
654     [self initMenuContentsWithView:viewMode_];
655   }
656
657   return self;
658 }
659
660 - (void)initMenuContentsWithView:(BubbleViewMode)viewToDisplay {
661   viewMode_ = viewToDisplay;
662   NSView* contentView = [[self window] contentView];
663   [contentView setSubviews:[NSArray array]];
664
665   if (viewMode_ == GAIA_SIGNIN_VIEW || viewMode_ == GAIA_ADD_ACCOUNT_VIEW) {
666     [contentView addSubview:[self createGaiaEmbeddedView]];
667     SetWindowSize([self window],
668                   NSMakeSize(kMinGaiaViewWidth, kMinGaiaViewHeight));
669     return;
670   }
671
672   NSView* currentProfileView = nil;
673   base::scoped_nsobject<NSMutableArray> otherProfiles(
674       [[NSMutableArray alloc] init]);
675
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);
680     if (item.active) {
681       currentProfileView = [self createCurrentProfileView:item];
682     } else {
683       [otherProfiles addObject:[self createOtherProfileView:i]];
684     }
685   }
686   if (!currentProfileView)  // Guest windows don't have an active profile.
687     currentProfileView = [self createGuestProfileView];
688
689   // |yOffset| is the next position at which to draw in |contentView|
690   // coordinates.
691   CGFloat yOffset = kSmallVerticalSpacing;
692
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;
698
699   NSBox* separator = [self separatorWithFrame:
700       NSMakeRect(0, yOffset, kFixedMenuWidth, 0)];
701   [contentView addSubview:separator];
702   yOffset = NSMaxY([separator frame]) + kVerticalSpacing;
703
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,
709                                                    yOffset)];
710       [contentView addSubview:otherProfileView];
711       yOffset = NSMaxY([otherProfileView frame]) + kSmallVerticalSpacing;
712     }
713
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,
722                    yOffset,
723                    kFixedMenuWidth - 2 * kHorizontalSpacing,
724                    0)];
725     [contentView addSubview:currentProfileAccountsView];
726     yOffset = NSMaxY([currentProfileAccountsView frame]) + kVerticalSpacing;
727
728     NSBox* accountsSeparator = [self separatorWithFrame:
729         NSMakeRect(0, yOffset, kFixedMenuWidth, 0)];
730     [contentView addSubview:accountsSeparator];
731     yOffset = NSMaxY([accountsSeparator frame]) + kVerticalSpacing;
732   }
733
734   // Active profile card.
735   if (currentProfileView) {
736     [currentProfileView setFrameOrigin:NSMakePoint(kHorizontalSpacing,
737                                                    yOffset)];
738     [contentView addSubview:currentProfileView];
739     yOffset = NSMaxY([currentProfileView frame]) + kVerticalSpacing;
740   }
741
742   SetWindowSize([self window], NSMakeSize(kFixedMenuWidth, yOffset));
743 }
744
745 - (NSView*)createCurrentProfileView:(const AvatarMenu::Item&)item {
746   base::scoped_nsobject<NSView> container([[NSView alloc]
747       initWithFrame:NSZeroRect]);
748
749   // Profile icon.
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_]);
756
757   [container addSubview:iconView];
758
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]);
766   }
767
768   // Profile name.
769   CGFloat availableTextWidth =
770       kFixedMenuWidth - xOffset - 2 * kHorizontalSpacing;
771   base::scoped_nsobject<EditableProfileNameButton> profileName(
772       [[EditableProfileNameButton alloc]
773           initWithFrame:NSMakeRect(xOffset, yOffset,
774                                    availableTextWidth,
775                                    kProfileButtonHeight)
776                 profile:browser_->profile()
777             profileName:base::SysUTF16ToNSString(item.name)
778          editingAllowed:!isGuestSession_]);
779
780   [container addSubview:profileName];
781   [container setFrameSize:NSMakeSize(kFixedMenuWidth,
782                                      NSHeight([iconView frame]))];
783   return container.autorelease();
784 }
785
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.
791   CGFloat yOffset;
792
793   // The available links depend on the type of profile that is active.
794   if (item.signed_in) {
795     yOffset = 0;
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;
805
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]);
812
813     maxX = std::max(NSMaxX([manageAccountsLink frame]),
814                                  NSMaxX([signOutLink frame]));
815     [container addSubview:manageAccountsLink];
816     [container addSubview:signOutLink];
817   } else {
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]);
827
828     [container addSubview:signInLink];
829   }
830
831   [container setFrameSize:NSMakeSize(maxX, yOffset)];
832   return container.autorelease();
833 }
834
835 - (NSView*)createGuestProfileView {
836   gfx::Image guestIcon =
837       ui::ResourceBundle::GetSharedInstance().GetNativeImageNamed(
838           IDR_LOGIN_GUEST);
839   AvatarMenu::Item guestItem(std::string::npos, /* menu_index, not used */
840                              std::string::npos, /* profile_index, not used */
841                              guestIcon);
842   guestItem.active = true;
843   guestItem.name = base::SysNSStringToUTF16(
844       l10n_util::GetNSString(IDS_PROFILES_GUEST_PROFILE_NAME));
845
846   return [self createCurrentProfileView:guestItem];
847 }
848
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()];
858
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:)];
870
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]))];
877
878   return profileButton.autorelease();
879 }
880
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]);
890
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]);
898
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();
914 }
915
916 - (NSView*)createCurrentProfileAccountsView:(NSRect)rect {
917   const CGFloat kAccountButtonHeight = 15;
918
919   const AvatarMenu::Item& item =
920       avatarMenu_->GetItemAt(avatarMenu_->GetActiveProfileIndex());
921   DCHECK(item.signed_in);
922
923   base::scoped_nsobject<NSView> container([[NSView alloc] initWithFrame:rect]);
924
925   NSRect viewRect = NSMakeRect(0, 0, rect.size.width, kBlueButtonHeight);
926   base::scoped_nsobject<NSButton> addAccountsButton([[BlueLabelButton alloc]
927       initWithFrame:viewRect]);
928
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),
937       rect.size.width,
938       gfx::ELIDE_AT_END));
939
940   [addAccountsButton setTitle:elidedButtonText];
941   [addAccountsButton setTarget:self];
942   [addAccountsButton setAction:@selector(addAccount:)];
943   [container addSubview:addAccountsButton];
944
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;
949
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();
955 }
956
957 - (NSView*)createAccountsListWithRect:(NSRect)rect {
958   base::scoped_nsobject<NSView> container([[NSView alloc] initWithFrame:rect]);
959   currentProfileAccounts_.clear();
960
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);
967
968   rect.origin.y = 0;
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
973                                                     title:accounts[i]
974                                              canBeDeleted:true];
975     [accountButton setTag:i];
976     [container addSubview:accountButton];
977     rect.origin.y = NSMaxY([accountButton frame]) + kSmallVerticalSpacing;
978   }
979
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:
984   // crbug.com/311124.
985   NSButton* accountButton = [self accountButtonWithRect:rect
986                                                   title:primaryAccount
987                                            canBeDeleted:false];
988   [container addSubview:accountButton];
989   [container setFrameSize:NSMakeSize(NSWidth([container frame]),
990                                      NSMaxY([accountButton frame]))];
991   return container.autorelease();
992 }
993
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;
998
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,
1005       std::string());
1006   NSView* webview = webContents_->GetView()->GetNativeView();
1007   [webview setFrameSize:NSMakeSize(kMinGaiaViewWidth, kMinGaiaViewHeight)];
1008   return webview;
1009 }
1010
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]);
1018
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];
1031
1032   return button.autorelease();
1033 }
1034
1035 - (NSButton*)linkButtonWithTitle:(NSString*)title
1036                      frameOrigin:(NSPoint)frameOrigin
1037                           action:(SEL)action {
1038   base::scoped_nsobject<NSButton> link(
1039       [[HyperlinkButtonCell buttonWithString:title] retain]);
1040
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];
1050   [link sizeToFit];
1051
1052   return link.autorelease();
1053 }
1054
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];
1062
1063   if (canBeDeleted) {
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:)];
1069   }
1070
1071   return button.autorelease();
1072 }
1073
1074 @end
1075