Upstream version 7.35.144.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/prefs/pref_service.h"
11 #include "base/strings/sys_string_conversions.h"
12 #include "base/strings/utf_string_conversions.h"
13 #include "chrome/browser/browser_process.h"
14 #include "chrome/browser/chrome_notification_types.h"
15 #include "chrome/browser/profiles/avatar_menu.h"
16 #include "chrome/browser/profiles/avatar_menu_observer.h"
17 #include "chrome/browser/profiles/profile_info_cache.h"
18 #include "chrome/browser/profiles/profile_info_util.h"
19 #include "chrome/browser/profiles/profile_manager.h"
20 #include "chrome/browser/profiles/profile_metrics.h"
21 #include "chrome/browser/profiles/profile_window.h"
22 #include "chrome/browser/profiles/profiles_state.h"
23 #include "chrome/browser/signin/profile_oauth2_token_service_factory.h"
24 #include "chrome/browser/signin/signin_manager.h"
25 #include "chrome/browser/signin/signin_manager_factory.h"
26 #include "chrome/browser/signin/signin_promo.h"
27 #include "chrome/browser/ui/browser.h"
28 #include "chrome/browser/ui/browser_dialogs.h"
29 #include "chrome/browser/ui/browser_window.h"
30 #include "chrome/browser/ui/chrome_style.h"
31 #import "chrome/browser/ui/cocoa/info_bubble_view.h"
32 #import "chrome/browser/ui/cocoa/info_bubble_window.h"
33 #import "chrome/browser/ui/cocoa/user_manager_mac.h"
34 #include "chrome/browser/ui/singleton_tabs.h"
35 #include "chrome/common/pref_names.h"
36 #include "chrome/common/profile_management_switches.h"
37 #include "chrome/common/url_constants.h"
38 #include "components/signin/core/browser/mutable_profile_oauth2_token_service.h"
39 #include "components/signin/core/browser/profile_oauth2_token_service.h"
40 #include "content/public/browser/notification_service.h"
41 #include "content/public/browser/web_contents.h"
42 #include "content/public/browser/web_contents_view.h"
43 #include "google_apis/gaia/oauth2_token_service.h"
44 #include "grit/chromium_strings.h"
45 #include "grit/generated_resources.h"
46 #include "grit/theme_resources.h"
47 #include "skia/ext/skia_utils_mac.h"
48 #import "third_party/google_toolbox_for_mac/src/AppKit/GTMUILocalizerAndLayoutTweaker.h"
49 #import "ui/base/cocoa/cocoa_base_utils.h"
50 #import "ui/base/cocoa/controls/blue_label_button.h"
51 #import "ui/base/cocoa/controls/hyperlink_button_cell.h"
52 #import "ui/base/cocoa/hover_image_button.h"
53 #include "ui/base/cocoa/window_size_constants.h"
54 #include "ui/base/l10n/l10n_util.h"
55 #include "ui/base/l10n/l10n_util_mac.h"
56 #include "ui/base/resource/resource_bundle.h"
57 #include "ui/gfx/image/image.h"
58 #include "ui/gfx/text_elider.h"
59 #include "ui/native_theme/native_theme.h"
60
61 namespace {
62
63 // Constants taken from the Windows/Views implementation at:
64 // chrome/browser/ui/views/profile_chooser_view.cc
65 const int kLargeImageSide = 64;
66 const int kSmallImageSide = 32;
67 const CGFloat kFixedMenuWidth = 250;
68
69 const CGFloat kVerticalSpacing = 20.0;
70 const CGFloat kSmallVerticalSpacing = 10.0;
71 const CGFloat kHorizontalSpacing = 20.0;
72 const CGFloat kTitleFontSize = 15.0;
73 const CGFloat kTextFontSize = 12.0;
74 const CGFloat kProfileButtonHeight = 30;
75 const int kOverlayHeight = 20;  // Height of the "Change" avatar photo overlay.
76 const int kBezelThickness = 3;  // Width of the bezel on an NSButton.
77 const int kImageTitleSpacing = 10;
78 const int kBlueButtonHeight = 30;
79
80 // Minimum size for embedded sign in pages as defined in Gaia.
81 const CGFloat kMinGaiaViewWidth = 320;
82 const CGFloat kMinGaiaViewHeight = 440;
83
84 // Maximum number of times to show the tutorial in the profile avatar bubble.
85 const int kProfileAvatarTutorialShowMax = 5;
86
87 gfx::Image CreateProfileImage(const gfx::Image& icon, int imageSize) {
88   return profiles::GetSizedAvatarIconWithBorder(
89       icon, true /* image is a square */,
90       imageSize + profiles::kAvatarIconPadding,
91       imageSize + profiles::kAvatarIconPadding);
92 }
93
94 // Updates the window size and position.
95 void SetWindowSize(NSWindow* window, NSSize size) {
96   NSRect frame = [window frame];
97   frame.origin.x += frame.size.width - size.width;
98   frame.origin.y += frame.size.height - size.height;
99   frame.size = size;
100   [window setFrame:frame display:YES];
101 }
102
103 NSString* ElideEmail(const std::string& email, CGFloat width) {
104   base::string16 elidedEmail = gfx::ElideEmail(
105       base::UTF8ToUTF16(email),
106       ui::ResourceBundle::GetSharedInstance().GetFontList(
107           ui::ResourceBundle::BaseFont),
108       width);
109   return base::SysUTF16ToNSString(elidedEmail);
110 }
111
112 }  // namespace
113
114 // Class that listens to changes to the OAuth2Tokens for the active profile,
115 // changes to the avatar menu model or browser close notifications.
116 class ActiveProfileObserverBridge : public AvatarMenuObserver,
117                                     public content::NotificationObserver,
118                                     public OAuth2TokenService::Observer {
119  public:
120   ActiveProfileObserverBridge(ProfileChooserController* controller,
121                               Browser* browser)
122       : controller_(controller),
123         browser_(browser),
124         token_observer_registered_(false) {
125     registrar_.Add(this, chrome::NOTIFICATION_BROWSER_CLOSING,
126                    content::NotificationService::AllSources());
127     if (!browser_->profile()->IsGuestSession())
128       AddTokenServiceObserver();
129   }
130
131   virtual ~ActiveProfileObserverBridge() {
132     RemoveTokenServiceObserver();
133   }
134
135  private:
136   void AddTokenServiceObserver() {
137     ProfileOAuth2TokenService* oauth2_token_service =
138         ProfileOAuth2TokenServiceFactory::GetForProfile(browser_->profile());
139     DCHECK(oauth2_token_service);
140     oauth2_token_service->AddObserver(this);
141     token_observer_registered_ = true;
142   }
143
144   void RemoveTokenServiceObserver() {
145     if (!token_observer_registered_)
146       return;
147     DCHECK(browser_->profile());
148     ProfileOAuth2TokenService* oauth2_token_service =
149         ProfileOAuth2TokenServiceFactory::GetForProfile(browser_->profile());
150     DCHECK(oauth2_token_service);
151     oauth2_token_service->RemoveObserver(this);
152     token_observer_registered_ = false;
153   }
154
155   // OAuth2TokenService::Observer:
156   virtual void OnRefreshTokenAvailable(const std::string& account_id) OVERRIDE {
157     // Tokens can only be added by adding an account through the inline flow,
158     // which is started from the account management view. Refresh it to show the
159     // update.
160     BubbleViewMode viewMode = [controller_ viewMode];
161     if (viewMode == ACCOUNT_MANAGEMENT_VIEW ||
162         viewMode == GAIA_SIGNIN_VIEW ||
163         viewMode == GAIA_ADD_ACCOUNT_VIEW) {
164       [controller_ initMenuContentsWithView:ACCOUNT_MANAGEMENT_VIEW];
165     }
166   }
167
168   virtual void OnRefreshTokenRevoked(const std::string& account_id) OVERRIDE {
169     // Tokens can only be removed from the account management view. Refresh it
170     // to show the update.
171     if ([controller_ viewMode] == ACCOUNT_MANAGEMENT_VIEW)
172       [controller_ initMenuContentsWithView:ACCOUNT_MANAGEMENT_VIEW];
173   }
174
175   // AvatarMenuObserver:
176   virtual void OnAvatarMenuChanged(AvatarMenu* avatar_menu) OVERRIDE {
177     // While the bubble is open, the avatar menu can only change from the
178     // profile chooser view by modifying the current profile's photo or name.
179     [controller_ initMenuContentsWithView:PROFILE_CHOOSER_VIEW];
180   }
181
182   // content::NotificationObserver:
183   virtual void Observe(
184       int type,
185       const content::NotificationSource& source,
186       const content::NotificationDetails& details) OVERRIDE {
187     DCHECK_EQ(chrome::NOTIFICATION_BROWSER_CLOSING, type);
188     if (browser_ == content::Source<Browser>(source).ptr()) {
189       RemoveTokenServiceObserver();
190       // Clean up the bubble's WebContents (used by the Gaia embedded view), to
191       // make sure the guest profile doesn't have any dangling host renderers.
192       // This can happen if Chrome is quit using Command-Q while the bubble is
193       // still open, which won't give the bubble a chance to be closed and
194       // clean up the WebContents itself.
195       [controller_ cleanUpEmbeddedViewContents];
196     }
197   }
198
199   ProfileChooserController* controller_;  // Weak; owns this.
200   Browser* browser_;  // Weak.
201   content::NotificationRegistrar registrar_;
202
203   // The observer can be removed both when closing the browser, and by just
204   // closing the avatar bubble. However, in the case of closing the browser,
205   // the avatar bubble will also be closed afterwards, resulting in a second
206   // attempt to remove the observer. This ensures the observer is only
207   // removed once.
208   bool token_observer_registered_;
209
210   DISALLOW_COPY_AND_ASSIGN(ActiveProfileObserverBridge);
211 };
212
213 // A custom button that has a transparent backround.
214 @interface TransparentBackgroundButton : NSButton
215 @end
216
217 @implementation TransparentBackgroundButton
218 - (id)initWithFrame:(NSRect)frameRect {
219   if ((self = [super initWithFrame:frameRect])) {
220     [self setBordered:NO];
221     [self setFont:[NSFont labelFontOfSize:kTextFontSize]];
222     [self setButtonType:NSMomentaryChangeButton];
223   }
224   return self;
225 }
226
227 - (void)drawRect:(NSRect)dirtyRect {
228   NSColor* backgroundColor = [NSColor colorWithCalibratedWhite:0 alpha:0.5f];
229   [backgroundColor setFill];
230   NSRectFillUsingOperation(dirtyRect, NSCompositeSourceAtop);
231   [super drawRect:dirtyRect];
232 }
233 @end
234
235 // A custom image control that shows a "Change" button when moused over.
236 @interface EditableProfilePhoto : NSImageView {
237  @private
238   AvatarMenu* avatarMenu_;  // Weak; Owned by ProfileChooserController.
239   base::scoped_nsobject<TransparentBackgroundButton> changePhotoButton_;
240   // Used to display the "Change" button on hover.
241   ui::ScopedCrTrackingArea trackingArea_;
242 }
243
244 - (id)initWithFrame:(NSRect)frameRect
245          avatarMenu:(AvatarMenu*)avatarMenu
246         profileIcon:(const gfx::Image&)profileIcon
247      editingAllowed:(BOOL)editingAllowed;
248
249 // Called when the "Change" button is clicked.
250 - (void)editPhoto:(id)sender;
251
252 // When hovering over the profile photo, show the "Change" button.
253 - (void)mouseEntered:(NSEvent*)event;
254
255 // When hovering away from the profile photo, hide the "Change" button.
256 - (void)mouseExited:(NSEvent*)event;
257 @end
258
259 @interface EditableProfilePhoto (Private)
260 // Create the "Change" avatar photo button.
261 - (TransparentBackgroundButton*)changePhotoButtonWithRect:(NSRect)rect;
262 @end
263
264 @implementation EditableProfilePhoto
265 - (id)initWithFrame:(NSRect)frameRect
266          avatarMenu:(AvatarMenu*)avatarMenu
267         profileIcon:(const gfx::Image&)profileIcon
268      editingAllowed:(BOOL)editingAllowed {
269   if ((self = [super initWithFrame:frameRect])) {
270     avatarMenu_ = avatarMenu;
271     [self setImage:CreateProfileImage(
272         profileIcon, kLargeImageSide).ToNSImage()];
273
274     // Add a tracking area so that we can show/hide the button when hovering.
275     trackingArea_.reset([[CrTrackingArea alloc]
276         initWithRect:[self bounds]
277              options:NSTrackingMouseEnteredAndExited | NSTrackingActiveAlways
278                owner:self
279             userInfo:nil]);
280     [self addTrackingArea:trackingArea_.get()];
281
282     if (editingAllowed) {
283       // The avatar photo uses a frame of width profiles::kAvatarIconPadding,
284       // which we must subtract from the button's bounds.
285       changePhotoButton_.reset([self changePhotoButtonWithRect:NSMakeRect(
286           profiles::kAvatarIconPadding, profiles::kAvatarIconPadding,
287           kLargeImageSide - 2 * profiles::kAvatarIconPadding,
288           kOverlayHeight)]);
289       [self addSubview:changePhotoButton_];
290
291       // Hide the button until the image is hovered over.
292       [changePhotoButton_ setHidden:YES];
293     }
294   }
295   return self;
296 }
297
298 - (void)editPhoto:(id)sender {
299   avatarMenu_->EditProfile(avatarMenu_->GetActiveProfileIndex());
300 }
301
302 - (void)mouseEntered:(NSEvent*)event {
303   [changePhotoButton_ setHidden:NO];
304 }
305
306 - (void)mouseExited:(NSEvent*)event {
307   [changePhotoButton_ setHidden:YES];
308 }
309
310 - (TransparentBackgroundButton*)changePhotoButtonWithRect:(NSRect)rect {
311   TransparentBackgroundButton* button =
312       [[TransparentBackgroundButton alloc] initWithFrame:rect];
313
314   // The button has a centered white text and a transparent background.
315   base::scoped_nsobject<NSMutableParagraphStyle> textStyle(
316       [[NSMutableParagraphStyle alloc] init]);
317   [textStyle setAlignment:NSCenterTextAlignment];
318   NSDictionary* titleAttributes = @{
319       NSParagraphStyleAttributeName : textStyle,
320       NSForegroundColorAttributeName : [NSColor whiteColor]
321   };
322   NSString* buttonTitle = l10n_util::GetNSString(
323       IDS_PROFILES_PROFILE_CHANGE_PHOTO_BUTTON);
324   base::scoped_nsobject<NSAttributedString> attributedTitle(
325       [[NSAttributedString alloc] initWithString:buttonTitle
326                                       attributes:titleAttributes]);
327   [button setAttributedTitle:attributedTitle];
328   [button setTarget:self];
329   [button setAction:@selector(editPhoto:)];
330   return button;
331 }
332 @end
333
334 // A custom text control that turns into a textfield for editing when clicked.
335 @interface EditableProfileNameButton : HoverImageButton<NSTextFieldDelegate> {
336  @private
337   base::scoped_nsobject<NSTextField> profileNameTextField_;
338   Profile* profile_;  // Weak.
339 }
340
341 - (id)initWithFrame:(NSRect)frameRect
342             profile:(Profile*)profile
343         profileName:(NSString*)profileName
344      editingAllowed:(BOOL)editingAllowed;
345
346 // Called when the button is clicked.
347 - (void)showEditableView:(id)sender;
348
349 // Called when the user presses "Enter" in the textfield.
350 - (void)controlTextDidEndEditing:(NSNotification *)obj;
351 @end
352
353 @implementation EditableProfileNameButton
354 - (id)initWithFrame:(NSRect)frameRect
355             profile:(Profile*)profile
356         profileName:(NSString*)profileName
357      editingAllowed:(BOOL)editingAllowed {
358   if ((self = [super initWithFrame:frameRect])) {
359     profile_ = profile;
360
361     [self setBordered:NO];
362     [self setFont:[NSFont labelFontOfSize:kTitleFontSize]];
363     [self setAlignment:NSLeftTextAlignment];
364     [[self cell] setLineBreakMode:NSLineBreakByTruncatingTail];
365     [self setTitle:profileName];
366
367     if (editingAllowed) {
368       // Show an "edit" pencil icon when hovering over.
369       ui::ResourceBundle* rb = &ui::ResourceBundle::GetSharedInstance();
370       [self setHoverImage:
371           rb->GetNativeImageNamed(IDR_ICON_PROFILES_EDIT_HOVER).AsNSImage()];
372       [self setAlternateImage:
373           rb->GetNativeImageNamed(IDR_ICON_PROFILES_EDIT_PRESSED).AsNSImage()];
374       [self setImagePosition:NSImageRight];
375       [self setTarget:self];
376       [self setAction:@selector(showEditableView:)];
377
378       // We need to subtract the width of the bezel from the frame rect, so that
379       // the textfield can take the exact same space as the button.
380       frameRect.size.height -= 2 * kBezelThickness;
381       frameRect.origin = NSMakePoint(0, kBezelThickness);
382       profileNameTextField_.reset(
383           [[NSTextField alloc] initWithFrame:frameRect]);
384       [profileNameTextField_ setStringValue:profileName];
385       [profileNameTextField_ setFont:[NSFont labelFontOfSize:kTitleFontSize]];
386       [profileNameTextField_ setEditable:YES];
387       [profileNameTextField_ setDrawsBackground:YES];
388       [profileNameTextField_ setBezeled:YES];
389       [[profileNameTextField_ cell] setWraps:NO];
390       [[profileNameTextField_ cell] setLineBreakMode:
391           NSLineBreakByTruncatingTail];
392       [profileNameTextField_ setDelegate:self];
393       [self addSubview:profileNameTextField_];
394
395       // Hide the textfield until the user clicks on the button.
396       [profileNameTextField_ setHidden:YES];
397     }
398   }
399   return self;
400 }
401
402 // NSTextField objects send an NSNotification to a delegate if
403 // it implements this method:
404 - (void)controlTextDidEndEditing:(NSNotification *)obj {
405   NSString* text = [profileNameTextField_ stringValue];
406   // Empty profile names are not allowed, and are treated as a cancel.
407   if ([text length] > 0) {
408     profiles::UpdateProfileName(profile_, base::SysNSStringToUTF16(text));
409     [self setTitle:text];
410   }
411   [profileNameTextField_ setHidden:YES];
412   [profileNameTextField_ resignFirstResponder];
413 }
414
415 - (void)showEditableView:(id)sender {
416   [profileNameTextField_ setHidden:NO];
417   [profileNameTextField_ becomeFirstResponder];
418 }
419
420 @end
421
422 // Custom button cell that adds a left padding before the button image, and
423 // a custom spacing between the button image and title.
424 @interface CustomPaddingImageButtonCell : NSButtonCell {
425  @private
426   // Padding between the left margin of the button and the cell image.
427   int leftMarginSpacing_;
428   // Spacing between the cell image and title.
429   int imageTitleSpacing_;
430 }
431
432 - (id)initWithLeftMarginSpacing:(int)leftMarginSpacing
433               imageTitleSpacing:(int)imageTitleSpacing;
434 @end
435
436 @implementation CustomPaddingImageButtonCell
437 - (id)initWithLeftMarginSpacing:(int)leftMarginSpacing
438               imageTitleSpacing:(int)imageTitleSpacing {
439   if ((self = [super init])) {
440     leftMarginSpacing_ = leftMarginSpacing;
441     imageTitleSpacing_ = imageTitleSpacing;
442   }
443   return self;
444 }
445
446 - (NSRect)drawTitle:(NSAttributedString*)title
447           withFrame:(NSRect)frame
448              inView:(NSView*)controlView {
449   // The title frame origin isn't aware of the left margin spacing added
450   // in -drawImage, so it must be added when drawing the title as well.
451   frame.origin.x += leftMarginSpacing_ + imageTitleSpacing_;
452   return [super drawTitle:title withFrame:frame inView:controlView];
453 }
454
455 - (void)drawImage:(NSImage*)image
456        withFrame:(NSRect)frame
457           inView:(NSView*)controlView {
458   frame.origin.x = leftMarginSpacing_;
459   [super drawImage:image withFrame:frame inView:controlView];
460 }
461
462 - (NSSize)cellSize {
463   NSSize buttonSize = [super cellSize];
464   buttonSize.width += leftMarginSpacing_ + imageTitleSpacing_;
465   return buttonSize;
466 }
467
468 @end
469
470 // A custom button that allows for setting a background color when hovered over.
471 @interface BackgroundColorHoverButton : HoverImageButton
472 @end
473
474 @implementation BackgroundColorHoverButton
475 - (id)initWithFrame:(NSRect)frameRect {
476   if ((self = [super initWithFrame:frameRect])) {
477     [self setBordered:NO];
478     [self setFont:[NSFont labelFontOfSize:kTextFontSize]];
479     [self setButtonType:NSMomentaryChangeButton];
480
481     base::scoped_nsobject<CustomPaddingImageButtonCell> cell(
482         [[CustomPaddingImageButtonCell alloc]
483             initWithLeftMarginSpacing:kHorizontalSpacing
484                     imageTitleSpacing:kImageTitleSpacing]);
485     [self setCell:cell.get()];
486   }
487   return self;
488 }
489
490 - (void)setHoverState:(HoverState)state {
491   [super setHoverState:state];
492   bool isHighlighted = ([self hoverState] != kHoverStateNone);
493
494   NSColor* backgroundColor = gfx::SkColorToCalibratedNSColor(
495       ui::NativeTheme::instance()->GetSystemColor(isHighlighted?
496           ui::NativeTheme::kColorId_MenuSeparatorColor :
497           ui::NativeTheme::kColorId_DialogBackground));
498
499   [[self cell] setBackgroundColor:backgroundColor];
500 }
501
502 @end
503
504
505 @interface ProfileChooserController ()
506 // Creates a tutorial card for the profile |avatar_item| if needed.
507 - (NSView*)createTutorialViewIfNeeded:(const AvatarMenu::Item&)item;
508
509 // Creates the main profile card for the profile |item| at the top of
510 // the bubble.
511 - (NSView*)createCurrentProfileView:(const AvatarMenu::Item&)item;
512
513 // Creates the possible links for the main profile card with profile |item|.
514 - (NSView*)createCurrentProfileLinksForItem:(const AvatarMenu::Item&)item
515                                 withXOffset:(CGFloat)xOffset;
516
517 // Creates a main profile card for the guest user.
518 - (NSView*)createGuestProfileView;
519
520 // Creates an item for the profile |itemIndex| that is used in the fast profile
521 // switcher in the middle of the bubble.
522 - (NSButton*)createOtherProfileView:(int)itemIndex;
523
524 // Creates the "Not you" and Lock option buttons.
525 - (NSView*)createOptionsViewWithRect:(NSRect)rect
526                           enableLock:(BOOL)enableLock;
527
528 // Creates the account management view for the active profile.
529 - (NSView*)createCurrentProfileAccountsView:(NSRect)rect;
530
531 // Creates the list of accounts for the active profile.
532 - (NSView*)createAccountsListWithRect:(NSRect)rect;
533
534 // Creates the Gaia sign-in/add account view.
535 - (NSView*)createGaiaEmbeddedView;
536
537 // Creates a button with |text|, an icon given by |imageResourceId| and with
538 // |action|. The icon |alternateImageResourceId| is displayed in the button's
539 // hovered and pressed states.
540 - (NSButton*)hoverButtonWithRect:(NSRect)rect
541                             text:(NSString*)text
542                  imageResourceId:(int)imageResourceId
543         alternateImageResourceId:(int)alternateImageResourceId
544                           action:(SEL)action;
545
546 // Creates a generic link button with |title| and an |action| positioned at
547 // |frameOrigin|.
548 - (NSButton*)linkButtonWithTitle:(NSString*)title
549                      frameOrigin:(NSPoint)frameOrigin
550                           action:(SEL)action;
551
552 // Creates an email account button with |title|. If |canBeDeleted| is YES, then
553 // the button is clickable and has a remove icon.
554 - (NSButton*)accountButtonWithRect:(NSRect)rect
555                              title:(const std::string&)title
556                       canBeDeleted:(BOOL)canBeDeleted;
557
558 - (NSTextField*)labelWithTitle:(NSString*)title
559                    frameOrigin:(NSPoint)frameOrigin;
560
561 @end
562
563 @implementation ProfileChooserController
564 - (BubbleViewMode) viewMode {
565   return viewMode_;
566 }
567
568 - (IBAction)switchToProfile:(id)sender {
569   // Check the event flags to see if a new window should be created.
570   bool alwaysCreate = ui::WindowOpenDispositionFromNSEvent(
571       [NSApp currentEvent]) == NEW_WINDOW;
572   avatarMenu_->SwitchToProfile([sender tag], alwaysCreate,
573                                ProfileMetrics::SWITCH_PROFILE_ICON);
574 }
575
576 - (IBAction)showUserManager:(id)sender {
577   // Guest users cannot appear in the User Manager, nor display a tutorial.
578   profiles::ShowUserManagerMaybeWithTutorial(
579       isGuestSession_ ? NULL : browser_->profile());
580 }
581
582 - (IBAction)showAccountManagement:(id)sender {
583   [self initMenuContentsWithView:ACCOUNT_MANAGEMENT_VIEW];
584 }
585
586 - (IBAction)lockProfile:(id)sender {
587   profiles::LockProfile(browser_->profile());
588 }
589
590 - (IBAction)showSigninPage:(id)sender {
591   [self initMenuContentsWithView:GAIA_SIGNIN_VIEW];
592 }
593
594 - (IBAction)addAccount:(id)sender {
595   [self initMenuContentsWithView:GAIA_ADD_ACCOUNT_VIEW];
596 }
597
598 - (IBAction)removeAccount:(id)sender {
599   DCHECK(!isGuestSession_);
600   DCHECK_GE([sender tag], 0);  // Should not be called for the primary account.
601   DCHECK(ContainsKey(currentProfileAccounts_, [sender tag]));
602   std::string account = currentProfileAccounts_[[sender tag]];
603   ProfileOAuth2TokenServiceFactory::GetPlatformSpecificForProfile(
604       browser_->profile())->RevokeCredentials(account);
605 }
606
607 - (IBAction)openTutorialLearnMoreURL:(id)sender {
608   // TODO(guohui): update |learnMoreUrl| once it is decided.
609   const GURL learnMoreUrl("https://support.google.com/chrome/?hl=en#to");
610   chrome::NavigateParams params(browser_->profile(), learnMoreUrl,
611                                 content::PAGE_TRANSITION_LINK);
612   params.disposition = NEW_FOREGROUND_TAB;
613   chrome::Navigate(&params);
614 }
615
616 - (IBAction)dismissTutorial:(id)sender {
617   // If the user manually dismissed the tutorial, never show it again by setting
618   // the number of times shown to the maximum plus 1, so that later we could
619   // distinguish between the dismiss case and the case when the tutorial is
620   // indeed shown for the maximum number of times.
621   browser_->profile()->GetPrefs()->SetInteger(
622       prefs::kProfileAvatarTutorialShown, kProfileAvatarTutorialShowMax + 1);
623   [self initMenuContentsWithView:PROFILE_CHOOSER_VIEW];
624 }
625
626 - (void)cleanUpEmbeddedViewContents {
627   webContents_.reset();
628 }
629
630 - (id)initWithBrowser:(Browser*)browser
631            anchoredAt:(NSPoint)point
632              withMode:(BubbleViewMode)mode {
633   base::scoped_nsobject<InfoBubbleWindow> window([[InfoBubbleWindow alloc]
634       initWithContentRect:ui::kWindowSizeDeterminedLater
635                 styleMask:NSBorderlessWindowMask
636                   backing:NSBackingStoreBuffered
637                     defer:NO]);
638
639   if ((self = [super initWithWindow:window
640                        parentWindow:browser->window()->GetNativeWindow()
641                          anchoredAt:point])) {
642     browser_ = browser;
643     viewMode_ = mode;
644     tutorialShowing_ = false;
645     observer_.reset(new ActiveProfileObserverBridge(self, browser_));
646
647     avatarMenu_.reset(new AvatarMenu(
648         &g_browser_process->profile_manager()->GetProfileInfoCache(),
649         observer_.get(),
650         browser_));
651     avatarMenu_->RebuildMenu();
652
653     // Guest profiles do not have a token service.
654     isGuestSession_ = browser_->profile()->IsGuestSession();
655
656     ui::NativeTheme* nativeTheme = ui::NativeTheme::instance();
657     [[self bubble] setAlignment:info_bubble::kAlignRightEdgeToAnchorEdge];
658     [[self bubble] setArrowLocation:info_bubble::kNoArrow];
659     [[self bubble] setBackgroundColor:
660         gfx::SkColorToCalibratedNSColor(nativeTheme->GetSystemColor(
661             ui::NativeTheme::kColorId_DialogBackground))];
662     [self initMenuContentsWithView:viewMode_];
663   }
664
665   return self;
666 }
667
668 - (void)initMenuContentsWithView:(BubbleViewMode)viewToDisplay {
669   viewMode_ = viewToDisplay;
670   NSView* contentView = [[self window] contentView];
671   [contentView setSubviews:[NSArray array]];
672
673   if (viewMode_ == GAIA_SIGNIN_VIEW || viewMode_ == GAIA_ADD_ACCOUNT_VIEW) {
674     [contentView addSubview:[self createGaiaEmbeddedView]];
675     SetWindowSize([self window],
676                   NSMakeSize(kMinGaiaViewWidth, kMinGaiaViewHeight));
677     return;
678   }
679
680   NSView* tutorialView = nil;
681   NSView* currentProfileView = nil;
682   base::scoped_nsobject<NSMutableArray> otherProfiles(
683       [[NSMutableArray alloc] init]);
684   // Local and guest profiles cannot lock their profile.
685   bool enableLock = false;
686
687   // Loop over the profiles in reverse, so that they are sorted by their
688   // y-coordinate, and separate them into active and "other" profiles.
689   for (int i = avatarMenu_->GetNumberOfItems() - 1; i >= 0; --i) {
690     const AvatarMenu::Item& item = avatarMenu_->GetItemAt(i);
691     if (item.active) {
692       if (viewMode_ == PROFILE_CHOOSER_VIEW)
693         tutorialView = [self createTutorialViewIfNeeded:item];
694       currentProfileView = [self createCurrentProfileView:item];
695       enableLock = item.signed_in;
696     } else {
697       [otherProfiles addObject:[self createOtherProfileView:i]];
698     }
699   }
700   if (!currentProfileView)  // Guest windows don't have an active profile.
701     currentProfileView = [self createGuestProfileView];
702
703   // |yOffset| is the next position at which to draw in |contentView|
704   // coordinates.
705   CGFloat yOffset = kSmallVerticalSpacing;
706
707   // Option buttons.
708   NSView* optionsView = [self createOptionsViewWithRect:
709       NSMakeRect(0, yOffset, kFixedMenuWidth, 0)
710                                         enableLock:enableLock];
711   [contentView addSubview:optionsView];
712   yOffset = NSMaxY([optionsView frame]) + kSmallVerticalSpacing;
713
714   NSBox* separator = [self separatorWithFrame:
715       NSMakeRect(0, yOffset, kFixedMenuWidth, 0)];
716   [contentView addSubview:separator];
717   yOffset = NSMaxY([separator frame]) + kVerticalSpacing;
718
719   if (viewToDisplay == PROFILE_CHOOSER_VIEW &&
720       switches::IsFastUserSwitching()) {
721     // Other profiles switcher. The profiles have already been sorted
722     // by their y-coordinate, so they can be added in the existing order.
723     for (NSView *otherProfileView in otherProfiles.get()) {
724       [otherProfileView setFrameOrigin:NSMakePoint(kHorizontalSpacing,
725                                                    yOffset)];
726       [contentView addSubview:otherProfileView];
727       yOffset = NSMaxY([otherProfileView frame]) + kSmallVerticalSpacing;
728     }
729
730     // If we displayed other profiles, ensure the spacing between the last item
731     // and the active profile card is the same as the spacing between the active
732     // profile card and the bottom of the bubble.
733     if ([otherProfiles.get() count] > 0)
734       yOffset += kSmallVerticalSpacing;
735   } else if (viewToDisplay == ACCOUNT_MANAGEMENT_VIEW) {
736     NSView* currentProfileAccountsView = [self createCurrentProfileAccountsView:
737         NSMakeRect(kHorizontalSpacing,
738                    yOffset,
739                    kFixedMenuWidth - 2 * kHorizontalSpacing,
740                    0)];
741     [contentView addSubview:currentProfileAccountsView];
742     yOffset = NSMaxY([currentProfileAccountsView frame]) + kVerticalSpacing;
743
744     NSBox* accountsSeparator = [self separatorWithFrame:
745         NSMakeRect(0, yOffset, kFixedMenuWidth, 0)];
746     [contentView addSubview:accountsSeparator];
747     yOffset = NSMaxY([accountsSeparator frame]) + kVerticalSpacing;
748   }
749
750   // Active profile card.
751   if (currentProfileView) {
752     [currentProfileView setFrameOrigin:NSMakePoint(kHorizontalSpacing,
753                                                    yOffset)];
754     [contentView addSubview:currentProfileView];
755     yOffset = NSMaxY([currentProfileView frame]) + kVerticalSpacing;
756   }
757
758   if (tutorialView) {
759     NSBox* accountsSeparator = [self separatorWithFrame:
760         NSMakeRect(0, yOffset, kFixedMenuWidth, 0)];
761     [contentView addSubview:accountsSeparator];
762     yOffset = NSMaxY([accountsSeparator frame]) + kVerticalSpacing;
763
764     [tutorialView setFrameOrigin:NSMakePoint(kHorizontalSpacing,
765                                              yOffset)];
766     [contentView addSubview:tutorialView];
767     yOffset = NSMaxY([tutorialView frame]) + kVerticalSpacing;
768   } else {
769     tutorialShowing_ = false;
770   }
771
772   SetWindowSize([self window], NSMakeSize(kFixedMenuWidth, yOffset));
773 }
774
775 - (NSView*)createTutorialViewIfNeeded:(const AvatarMenu::Item&)item {
776   if (!item.signed_in)
777     return nil;
778
779   Profile* profile = browser_->profile();
780   const int showCount = profile->GetPrefs()->GetInteger(
781       prefs::kProfileAvatarTutorialShown);
782   // Do not show the tutorial if user has dismissed it.
783   if (showCount > kProfileAvatarTutorialShowMax)
784     return nil;
785
786   if (!tutorialShowing_) {
787     if (showCount == kProfileAvatarTutorialShowMax)
788       return nil;
789     profile->GetPrefs()->SetInteger(
790       prefs::kProfileAvatarTutorialShown, showCount + 1);
791     tutorialShowing_ = true;
792   }
793
794   CGFloat availableWidth = kFixedMenuWidth - 2 * kHorizontalSpacing;
795   base::scoped_nsobject<NSView> container([[NSView alloc]
796       initWithFrame:NSMakeRect(0, 0, availableWidth, 0)]);
797   CGFloat yOffset = 0;
798
799   // Adds links and buttons at the bottom.
800   NSButton* learnMoreLink =
801       [self linkButtonWithTitle:l10n_util::GetNSString(IDS_LEARN_MORE)
802                     frameOrigin:NSMakePoint(0, yOffset)
803                          action:@selector(openTutorialLearnMoreURL:)];
804   [container addSubview:learnMoreLink];
805
806   base::scoped_nsobject<NSButton> tutorialOkButton([[BlueLabelButton alloc]
807       initWithFrame:NSZeroRect]);
808   [tutorialOkButton setTitle:l10n_util::GetNSString(
809       IDS_PROFILES_SIGNIN_TUTORIAL_OK_BUTTON)];
810   [tutorialOkButton setTarget:self];
811   [tutorialOkButton setAction:@selector(dismissTutorial:)];
812   [tutorialOkButton sizeToFit];
813   [tutorialOkButton setFrameOrigin:NSMakePoint(
814       availableWidth - NSWidth([tutorialOkButton frame]), yOffset)];
815   [container addSubview:tutorialOkButton];
816
817   yOffset = std::max(NSMaxY([learnMoreLink frame]),
818                      NSMaxY([tutorialOkButton frame])) + kVerticalSpacing;
819
820   // Adds body content consisting of three bulleted lines.
821   const int kTextHorizIndentation = 10;
822   NSTextField* bulletLabel =
823       [self labelWithTitle:l10n_util::GetNSString(
824           IDS_PROFILES_SIGNIN_TUTORIAL_CONTENT_TEXT)
825                frameOrigin:NSMakePoint(kTextHorizIndentation, yOffset)];
826   [bulletLabel setFrameSize:NSMakeSize(availableWidth,
827       NSHeight([bulletLabel frame]))];
828   [container addSubview:bulletLabel];
829   yOffset = NSMaxY([bulletLabel frame]) + kSmallVerticalSpacing;
830
831   // Adds body header.
832   NSTextField* contentHeaderLabel =
833       [self labelWithTitle:l10n_util::GetNSString(
834           IDS_PROFILES_SIGNIN_TUTORIAL_CONTENT_HEADER)
835                frameOrigin:NSMakePoint(0, yOffset)];
836   [contentHeaderLabel setFrameSize:NSMakeSize(availableWidth, 0)];
837   [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:
838       contentHeaderLabel];
839   [container addSubview:contentHeaderLabel];
840   yOffset = NSMaxY([contentHeaderLabel frame]) + kSmallVerticalSpacing;
841
842   // Adds title.
843   NSTextField* titleLabel =
844       [self labelWithTitle:l10n_util::GetNSStringF(
845           IDS_PROFILES_SIGNIN_TUTORIAL_TITLE,
846           profiles::GetAvatarNameForProfile(profile))
847                frameOrigin:NSMakePoint(0, yOffset)];
848   [titleLabel setFont:[NSFont labelFontOfSize:kTitleFontSize]];
849   [[titleLabel cell] setTextColor:
850       gfx::SkColorToCalibratedNSColor(chrome_style::GetLinkColor())];
851   [titleLabel sizeToFit];
852   [titleLabel setFrameSize:
853       NSMakeSize(availableWidth, NSHeight([titleLabel frame]))];
854   [container addSubview:titleLabel];
855   yOffset = NSMaxY([titleLabel frame]);
856
857   [container setFrameSize:NSMakeSize(kFixedMenuWidth, yOffset)];
858   return container.autorelease();
859 }
860
861 - (NSView*)createCurrentProfileView:(const AvatarMenu::Item&)item {
862   base::scoped_nsobject<NSView> container([[NSView alloc]
863       initWithFrame:NSZeroRect]);
864
865   // Profile icon.
866   base::scoped_nsobject<EditableProfilePhoto> iconView(
867       [[EditableProfilePhoto alloc]
868           initWithFrame:NSMakeRect(0, 0, kLargeImageSide, kLargeImageSide)
869              avatarMenu:avatarMenu_.get()
870             profileIcon:item.icon
871          editingAllowed:!isGuestSession_]);
872
873   [container addSubview:iconView];
874
875   CGFloat xOffset = NSMaxX([iconView frame]) + kHorizontalSpacing;
876   CGFloat yOffset = kVerticalSpacing;
877   if (!isGuestSession_ && viewMode_ == PROFILE_CHOOSER_VIEW) {
878     NSView* linksContainer =
879         [self createCurrentProfileLinksForItem:item withXOffset:xOffset];
880     [container addSubview:linksContainer];
881     yOffset = NSMaxY([linksContainer frame]);
882   }
883
884   // Profile name.
885   CGFloat availableTextWidth =
886       kFixedMenuWidth - xOffset - 2 * kHorizontalSpacing;
887   base::scoped_nsobject<EditableProfileNameButton> profileName(
888       [[EditableProfileNameButton alloc]
889           initWithFrame:NSMakeRect(xOffset, yOffset,
890                                    availableTextWidth,
891                                    kProfileButtonHeight)
892                 profile:browser_->profile()
893             profileName:base::SysUTF16ToNSString(
894                 profiles::GetAvatarNameForProfile(browser_->profile()))
895          editingAllowed:!isGuestSession_]);
896
897   [container addSubview:profileName];
898   [container setFrameSize:NSMakeSize(kFixedMenuWidth,
899                                      NSHeight([iconView frame]))];
900   return container.autorelease();
901 }
902
903 - (NSView*)createCurrentProfileLinksForItem:(const AvatarMenu::Item&)item
904                                 withXOffset:(CGFloat)xOffset {
905   base::scoped_nsobject<NSView> container([[NSView alloc]
906       initWithFrame:NSZeroRect]);
907
908   NSButton* link;
909   NSPoint frameOrigin = NSMakePoint(xOffset, kSmallVerticalSpacing);
910   // The available links depend on the type of profile that is active.
911   if (item.signed_in) {
912     link = [self linkButtonWithTitle:l10n_util::GetNSString(
913         IDS_PROFILES_PROFILE_MANAGE_ACCOUNTS_BUTTON)
914                          frameOrigin:frameOrigin
915                               action:@selector(showAccountManagement:)];
916   } else {
917     link = [self linkButtonWithTitle:l10n_util::GetNSStringFWithFixup(
918         IDS_SYNC_START_SYNC_BUTTON_LABEL,
919         l10n_util::GetStringUTF16(IDS_SHORT_PRODUCT_NAME))
920                          frameOrigin:frameOrigin
921                               action:@selector(showSigninPage:)];
922   }
923
924   [container addSubview:link];
925   [container setFrameSize:NSMakeSize(
926       NSMaxX([link frame]), NSMaxY([link frame]) + kSmallVerticalSpacing)];
927   return container.autorelease();
928 }
929
930 - (NSView*)createGuestProfileView {
931   gfx::Image guestIcon =
932       ui::ResourceBundle::GetSharedInstance().GetNativeImageNamed(
933           IDR_LOGIN_GUEST);
934   AvatarMenu::Item guestItem(std::string::npos, /* menu_index, not used */
935                              std::string::npos, /* profile_index, not used */
936                              guestIcon);
937   guestItem.active = true;
938   guestItem.name = base::SysNSStringToUTF16(
939       l10n_util::GetNSString(IDS_PROFILES_GUEST_PROFILE_NAME));
940
941   return [self createCurrentProfileView:guestItem];
942 }
943
944 - (NSButton*)createOtherProfileView:(int)itemIndex {
945   const AvatarMenu::Item& item = avatarMenu_->GetItemAt(itemIndex);
946   base::scoped_nsobject<NSButton> profileButton([[NSButton alloc]
947       initWithFrame:NSZeroRect]);
948   base::scoped_nsobject<CustomPaddingImageButtonCell> cell(
949   [[CustomPaddingImageButtonCell alloc]
950       initWithLeftMarginSpacing:0
951               imageTitleSpacing:kImageTitleSpacing]);
952   [profileButton setCell:cell.get()];
953
954   [[profileButton cell] setLineBreakMode:NSLineBreakByTruncatingTail];
955   [profileButton setTitle:base::SysUTF16ToNSString(item.name)];
956   [profileButton setImage:CreateProfileImage(
957       item.icon, kSmallImageSide).ToNSImage()];
958   [profileButton setImagePosition:NSImageLeft];
959   [profileButton setAlignment:NSLeftTextAlignment];
960   [profileButton setBordered:NO];
961   [profileButton setFont:[NSFont labelFontOfSize:kTitleFontSize]];
962   [profileButton setTag:itemIndex];
963   [profileButton setTarget:self];
964   [profileButton setAction:@selector(switchToProfile:)];
965
966   // Since the bubble is fixed width, we need to calculate the width available
967   // for the profile name, as longer names will have to be elided.
968   CGFloat availableTextWidth = kFixedMenuWidth - 2 * kHorizontalSpacing;
969   [profileButton sizeToFit];
970   [profileButton setFrameSize:NSMakeSize(availableTextWidth,
971                                          NSHeight([profileButton frame]))];
972
973   return profileButton.autorelease();
974 }
975
976 - (NSView*)createOptionsViewWithRect:(NSRect)rect
977                           enableLock:(BOOL)enableLock {
978   int widthOfLockButton = enableLock? 2 * kHorizontalSpacing + 12 : 0;
979   NSRect viewRect = NSMakeRect(0, 0,
980                                rect.size.width - widthOfLockButton,
981                                kBlueButtonHeight);
982   NSButton* notYouButton =
983       [self hoverButtonWithRect:viewRect
984                            text:l10n_util::GetNSStringF(
985           IDS_PROFILES_NOT_YOU_BUTTON,
986           profiles::GetAvatarNameForProfile(browser_->profile()))
987                 imageResourceId:IDR_ICON_PROFILES_MENU_AVATAR
988        alternateImageResourceId:IDR_ICON_PROFILES_MENU_AVATAR
989                          action:@selector(showUserManager:)];
990
991   rect.size.height = NSMaxY([notYouButton frame]);
992   base::scoped_nsobject<NSView> container([[NSView alloc]
993       initWithFrame:rect]);
994   [container addSubview:notYouButton];
995
996   if (enableLock) {
997     viewRect.origin.x = NSMaxX([notYouButton frame]);
998     viewRect.size.width = widthOfLockButton;
999     NSButton* lockButton =
1000         [self hoverButtonWithRect:viewRect
1001                              text:@""
1002                   imageResourceId:IDR_ICON_PROFILES_MENU_LOCK
1003          alternateImageResourceId:IDR_ICON_PROFILES_MENU_LOCK
1004                            action:@selector(lockProfile:)];
1005     [container addSubview:lockButton];
1006   }
1007
1008   return container.autorelease();
1009 }
1010
1011 - (NSView*)createCurrentProfileAccountsView:(NSRect)rect {
1012   const CGFloat kAccountButtonHeight = 15;
1013
1014   const AvatarMenu::Item& item =
1015       avatarMenu_->GetItemAt(avatarMenu_->GetActiveProfileIndex());
1016   DCHECK(item.signed_in);
1017
1018   base::scoped_nsobject<NSView> container([[NSView alloc] initWithFrame:rect]);
1019
1020   NSRect viewRect = NSMakeRect(0, 0, rect.size.width, kBlueButtonHeight);
1021   base::scoped_nsobject<NSButton> addAccountsButton([[BlueLabelButton alloc]
1022       initWithFrame:viewRect]);
1023
1024   // Manually elide the button text so that the contents fit inside the bubble.
1025   // This is needed because the BlueLabelButton cell resets the style on
1026   // every call to -cellSize, which prevents setting a custom lineBreakMode.
1027   NSString* elidedButtonText = base::SysUTF16ToNSString(gfx::ElideText(
1028       l10n_util::GetStringFUTF16(
1029           IDS_PROFILES_PROFILE_ADD_ACCOUNT_BUTTON, item.name),
1030       ui::ResourceBundle::GetSharedInstance().GetFontList(
1031           ui::ResourceBundle::BaseFont),
1032       rect.size.width,
1033       gfx::ELIDE_AT_END));
1034
1035   [addAccountsButton setTitle:elidedButtonText];
1036   [addAccountsButton setTarget:self];
1037   [addAccountsButton setAction:@selector(addAccount:)];
1038   [container addSubview:addAccountsButton];
1039
1040   // Update the height of the email account buttons. This is needed so that the
1041   // all the buttons span the entire width of the bubble.
1042   viewRect.origin.y = NSMaxY([addAccountsButton frame]) + kVerticalSpacing;
1043   viewRect.size.height = kAccountButtonHeight;
1044
1045   NSView* accountEmails = [self createAccountsListWithRect:viewRect];
1046   [container addSubview:accountEmails];
1047   [container setFrameSize:NSMakeSize(
1048       NSWidth([container frame]), NSMaxY([accountEmails frame]))];
1049   return container.autorelease();
1050 }
1051
1052 - (NSView*)createAccountsListWithRect:(NSRect)rect {
1053   base::scoped_nsobject<NSView> container([[NSView alloc] initWithFrame:rect]);
1054   currentProfileAccounts_.clear();
1055
1056   Profile* profile = browser_->profile();
1057   std::string primaryAccount =
1058       SigninManagerFactory::GetForProfile(profile)->GetAuthenticatedUsername();
1059   DCHECK(!primaryAccount.empty());
1060   std::vector<std::string>accounts =
1061       profiles::GetSecondaryAccountsForProfile(profile, primaryAccount);
1062
1063   rect.origin.y = 0;
1064   for (size_t i = 0; i < accounts.size(); ++i) {
1065     // Save the original email address, as the button text could be elided.
1066     currentProfileAccounts_[i] = accounts[i];
1067     NSButton* accountButton = [self accountButtonWithRect:rect
1068                                                     title:accounts[i]
1069                                              canBeDeleted:true];
1070     [accountButton setTag:i];
1071     [container addSubview:accountButton];
1072     rect.origin.y = NSMaxY([accountButton frame]) + kSmallVerticalSpacing;
1073   }
1074
1075   // The primary account should always be listed first. It doesn't need a tag,
1076   // as it cannot be removed.
1077   // TODO(rogerta): we still need to further differentiate the primary account
1078   // from the others in the UI, so more work is likely required here:
1079   // crbug.com/311124.
1080   NSButton* accountButton = [self accountButtonWithRect:rect
1081                                                   title:primaryAccount
1082                                            canBeDeleted:false];
1083   [container addSubview:accountButton];
1084   [container setFrameSize:NSMakeSize(NSWidth([container frame]),
1085                                      NSMaxY([accountButton frame]))];
1086   return container.autorelease();
1087 }
1088
1089 - (NSView*) createGaiaEmbeddedView {
1090   signin::Source source = (viewMode_ == GAIA_SIGNIN_VIEW) ?
1091       signin::SOURCE_AVATAR_BUBBLE_SIGN_IN :
1092       signin::SOURCE_AVATAR_BUBBLE_ADD_ACCOUNT;
1093
1094   webContents_.reset(content::WebContents::Create(
1095       content::WebContents::CreateParams(browser_->profile())));
1096   webContents_->GetController().LoadURL(
1097       signin::GetPromoURL(
1098           source, false /* auto_close */, true /* is_constrained */),
1099       content::Referrer(),
1100       content::PAGE_TRANSITION_AUTO_TOPLEVEL,
1101       std::string());
1102   NSView* webview = webContents_->GetView()->GetNativeView();
1103   [webview setFrameSize:NSMakeSize(kMinGaiaViewWidth, kMinGaiaViewHeight)];
1104   return webview;
1105 }
1106
1107 - (NSButton*)hoverButtonWithRect:(NSRect)rect
1108                             text:(NSString*)text
1109                  imageResourceId:(int)imageResourceId
1110         alternateImageResourceId:(int)alternateImageResourceId
1111                           action:(SEL)action  {
1112   base::scoped_nsobject<BackgroundColorHoverButton> button(
1113       [[BackgroundColorHoverButton alloc] initWithFrame:rect]);
1114
1115   [button setTitle:text];
1116   ui::ResourceBundle* rb = &ui::ResourceBundle::GetSharedInstance();
1117   NSImage* alternateImage = rb->GetNativeImageNamed(
1118       alternateImageResourceId).ToNSImage();
1119   [button setDefaultImage:rb->GetNativeImageNamed(imageResourceId).ToNSImage()];
1120   [button setHoverImage:alternateImage];
1121   [button setPressedImage:alternateImage];
1122   [button setImagePosition:NSImageLeft];
1123   [button setAlignment:NSLeftTextAlignment];
1124   [button setBordered:NO];
1125   [button setTarget:self];
1126   [button setAction:action];
1127
1128   return button.autorelease();
1129 }
1130
1131 - (NSButton*)linkButtonWithTitle:(NSString*)title
1132                      frameOrigin:(NSPoint)frameOrigin
1133                           action:(SEL)action {
1134   base::scoped_nsobject<NSButton> link(
1135       [[HyperlinkButtonCell buttonWithString:title] retain]);
1136
1137   [[link cell] setShouldUnderline:NO];
1138   [[link cell] setTextColor:gfx::SkColorToCalibratedNSColor(
1139       chrome_style::GetLinkColor())];
1140   [link setTitle:title];
1141   [link setBordered:NO];
1142   [link setFont:[NSFont labelFontOfSize:kTextFontSize]];
1143   [link setTarget:self];
1144   [link setAction:action];
1145   [link setFrameOrigin:frameOrigin];
1146   [link sizeToFit];
1147
1148   return link.autorelease();
1149 }
1150
1151 - (NSButton*)accountButtonWithRect:(NSRect)rect
1152                              title:(const std::string&)title
1153                       canBeDeleted:(BOOL)canBeDeleted {
1154   base::scoped_nsobject<NSButton> button([[NSButton alloc] initWithFrame:rect]);
1155   [button setTitle:ElideEmail(title, rect.size.width)];
1156   [button setAlignment:NSLeftTextAlignment];
1157   [button setBordered:NO];
1158
1159   if (canBeDeleted) {
1160     [button setImage:ui::ResourceBundle::GetSharedInstance().
1161         GetNativeImageNamed(IDR_CLOSE_1).ToNSImage()];
1162     [button setImagePosition:NSImageRight];
1163     [button setTarget:self];
1164     [button setAction:@selector(removeAccount:)];
1165   }
1166
1167   return button.autorelease();
1168 }
1169
1170 - (NSTextField*)labelWithTitle:(NSString*)title
1171                    frameOrigin:(NSPoint)frameOrigin {
1172   base::scoped_nsobject<NSTextField> label(
1173       [[NSTextField alloc] initWithFrame:NSZeroRect]);
1174   [label setStringValue:title];
1175   [label setEditable:NO];
1176   [label setAlignment:NSLeftTextAlignment];
1177   [label setBezeled:NO];
1178   [label setFont:[NSFont labelFontOfSize:kTextFontSize]];
1179   [label setFrameOrigin:frameOrigin];
1180   [label sizeToFit];
1181
1182   return label.autorelease();
1183 }
1184 @end