1// Copyright 2014 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#import <Cocoa/Cocoa.h>
6
7#import "chrome/browser/ui/cocoa/profiles/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/lifetime/application_lifetime.h"
16#include "chrome/browser/profiles/avatar_menu.h"
17#include "chrome/browser/profiles/avatar_menu_observer.h"
18#include "chrome/browser/profiles/profile_avatar_icon_util.h"
19#include "chrome/browser/profiles/profile_info_cache.h"
20#include "chrome/browser/profiles/profile_manager.h"
21#include "chrome/browser/profiles/profile_metrics.h"
22#include "chrome/browser/profiles/profile_window.h"
23#include "chrome/browser/profiles/profiles_state.h"
24#include "chrome/browser/signin/profile_oauth2_token_service_factory.h"
25#include "chrome/browser/signin/signin_header_helper.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_commands.h"
30#include "chrome/browser/ui/browser_dialogs.h"
31#include "chrome/browser/ui/browser_window.h"
32#include "chrome/browser/ui/chrome_pages.h"
33#include "chrome/browser/ui/chrome_style.h"
34#import "chrome/browser/ui/cocoa/hyperlink_text_view.h"
35#import "chrome/browser/ui/cocoa/info_bubble_view.h"
36#import "chrome/browser/ui/cocoa/info_bubble_window.h"
37#import "chrome/browser/ui/cocoa/profiles/user_manager_mac.h"
38#include "chrome/browser/ui/singleton_tabs.h"
39#include "chrome/common/pref_names.h"
40#include "chrome/common/url_constants.h"
41#include "components/signin/core/common/profile_management_switches.h"
42#include "components/signin/core/browser/mutable_profile_oauth2_token_service.h"
43#include "components/signin/core/browser/profile_oauth2_token_service.h"
44#include "components/signin/core/browser/signin_manager.h"
45#include "content/public/browser/notification_service.h"
46#include "content/public/browser/web_contents.h"
47#include "google_apis/gaia/oauth2_token_service.h"
48#include "grit/chromium_strings.h"
49#include "grit/generated_resources.h"
50#include "grit/theme_resources.h"
51#include "skia/ext/skia_utils_mac.h"
52#import "third_party/google_toolbox_for_mac/src/AppKit/GTMUILocalizerAndLayoutTweaker.h"
53#import "ui/base/cocoa/cocoa_base_utils.h"
54#import "ui/base/cocoa/controls/blue_label_button.h"
55#import "ui/base/cocoa/controls/hyperlink_button_cell.h"
56#import "ui/base/cocoa/hover_image_button.h"
57#include "ui/base/cocoa/window_size_constants.h"
58#include "ui/base/l10n/l10n_util.h"
59#include "ui/base/l10n/l10n_util_mac.h"
60#include "ui/base/resource/resource_bundle.h"
61#include "ui/gfx/image/image.h"
62#include "ui/gfx/text_elider.h"
63#include "ui/native_theme/native_theme.h"
64
65namespace {
66
67// Constants taken from the Windows/Views implementation at:
68// chrome/browser/ui/views/profile_chooser_view.cc
69const int kLargeImageSide = 88;
70const int kSmallImageSide = 32;
71const CGFloat kFixedMenuWidth = 250;
72
73const CGFloat kVerticalSpacing = 16.0;
74const CGFloat kSmallVerticalSpacing = 10.0;
75const CGFloat kHorizontalSpacing = 16.0;
76const CGFloat kTitleFontSize = 15.0;
77const CGFloat kTextFontSize = 12.0;
78const CGFloat kProfileButtonHeight = 30;
79const int kBezelThickness = 3;  // Width of the bezel on an NSButton.
80const int kImageTitleSpacing = 10;
81const int kBlueButtonHeight = 30;
82
83// Fixed size for embedded sign in pages as defined in Gaia.
84const CGFloat kFixedGaiaViewWidth = 360;
85const CGFloat kFixedGaiaViewHeight = 400;
86
87// Fixed size for the account removal view.
88const CGFloat kFixedAccountRemovalViewWidth = 280;
89// Fixed size for the end-preview view.
90const int kFixedEndPreviewViewWidth = 280;
91
92// Maximum number of times to show the welcome tutorial in the profile avatar
93// bubble.
94const int kProfileAvatarTutorialShowMax = 1;
95
96// The tag number for the primary account.
97const int kPrimaryProfileTag = -1;
98
99gfx::Image CreateProfileImage(const gfx::Image& icon, int imageSize) {
100  return profiles::GetSizedAvatarIcon(
101      icon, true /* image is a square */, imageSize, imageSize);
102}
103
104// Updates the window size and position.
105void SetWindowSize(NSWindow* window, NSSize size) {
106  NSRect frame = [window frame];
107  frame.origin.x += frame.size.width - size.width;
108  frame.origin.y += frame.size.height - size.height;
109  frame.size = size;
110  [window setFrame:frame display:YES];
111}
112
113NSString* ElideEmail(const std::string& email, CGFloat width) {
114  const base::string16 elidedEmail = gfx::ElideText(
115      base::UTF8ToUTF16(email), gfx::FontList(), width, gfx::ELIDE_EMAIL);
116  return base::SysUTF16ToNSString(elidedEmail);
117}
118
119// Builds a label with the given |title| anchored at |frame_origin|. Sets the
120// text color and background color to the given colors if not null.
121NSTextField* BuildLabel(NSString* title,
122                        NSPoint frame_origin,
123                        NSColor* background_color,
124                        NSColor* text_color) {
125  base::scoped_nsobject<NSTextField> label(
126      [[NSTextField alloc] initWithFrame:NSZeroRect]);
127  [label setStringValue:title];
128  [label setEditable:NO];
129  [label setAlignment:NSLeftTextAlignment];
130  [label setBezeled:NO];
131  [label setFont:[NSFont labelFontOfSize:kTextFontSize]];
132  [label setFrameOrigin:frame_origin];
133  [label sizeToFit];
134
135  if (background_color)
136    [[label cell] setBackgroundColor:background_color];
137  if (text_color)
138    [[label cell] setTextColor:text_color];
139
140  return label.autorelease();
141}
142
143// Builds an NSTextView that has the contents set to the specified |message|,
144// with a non-underlined |link| inserted at |link_offset|. The view is anchored
145// at the specified |frame_origin| and has a fixed |frame_width|.
146NSTextView* BuildFixedWidthTextViewWithLink(
147    id<NSTextViewDelegate> delegate,
148    NSString* message,
149    NSString* link,
150    int link_offset,
151    NSPoint frame_origin,
152    CGFloat frame_width) {
153  base::scoped_nsobject<HyperlinkTextView> text_view(
154      [[HyperlinkTextView alloc] initWithFrame:NSZeroRect]);
155  NSColor* link_color = gfx::SkColorToCalibratedNSColor(
156      chrome_style::GetLinkColor());
157  // Adds a padding row at the bottom, because |boundingRectWithSize| below cuts
158  // off the last row sometimes.
159  [text_view setMessageAndLink:[NSString stringWithFormat:@"%@\n", message]
160                      withLink:link
161                      atOffset:link_offset
162                          font:[NSFont labelFontOfSize:kTextFontSize]
163                  messageColor:[NSColor blackColor]
164                     linkColor:link_color];
165
166  // Removes the underlining from the link.
167  [text_view setLinkTextAttributes:nil];
168  NSTextStorage* text_storage = [text_view textStorage];
169  NSRange link_range = NSMakeRange(link_offset, [link length]);
170  [text_storage addAttribute:NSUnderlineStyleAttributeName
171                       value:[NSNumber numberWithInt:NSUnderlineStyleNone]
172                       range:link_range];
173
174  NSRect frame = [[text_view attributedString]
175      boundingRectWithSize:NSMakeSize(frame_width, 0)
176                   options:NSStringDrawingUsesLineFragmentOrigin];
177  frame.origin = frame_origin;
178  [text_view setFrame:frame];
179  [text_view setDelegate:delegate];
180  return text_view.autorelease();
181}
182
183// Returns the native dialog background color.
184NSColor* GetDialogBackgroundColor() {
185  return gfx::SkColorToCalibratedNSColor(
186      ui::NativeTheme::instance()->GetSystemColor(
187          ui::NativeTheme::kColorId_DialogBackground));
188}
189
190// Builds a title card with one back button right aligned and one label center
191// aligned.
192NSView* BuildTitleCard(NSRect frame_rect,
193                       int message_id,
194                       id back_button_target,
195                       SEL back_button_action) {
196  base::scoped_nsobject<NSView> container(
197      [[NSView alloc] initWithFrame:frame_rect]);
198
199  base::scoped_nsobject<HoverImageButton> button(
200      [[HoverImageButton alloc] initWithFrame:frame_rect]);
201  [button setBordered:NO];
202  ui::ResourceBundle* rb = &ui::ResourceBundle::GetSharedInstance();
203  [button setDefaultImage:rb->GetNativeImageNamed(IDR_BACK).ToNSImage()];
204  [button setHoverImage:rb->GetNativeImageNamed(IDR_BACK_H).ToNSImage()];
205  [button setPressedImage:rb->GetNativeImageNamed(IDR_BACK_P).ToNSImage()];
206  [button setTarget:back_button_target];
207  [button setAction:back_button_action];
208  [button setFrameSize:NSMakeSize(kProfileButtonHeight, kProfileButtonHeight)];
209  [button setFrameOrigin:NSMakePoint(kHorizontalSpacing, 0)];
210
211  NSTextField* title_label =
212      BuildLabel(l10n_util::GetNSString(message_id), NSZeroPoint,
213                 GetDialogBackgroundColor(), nil /* text_color */);
214  [title_label setAlignment:NSCenterTextAlignment];
215  [title_label setFont:[NSFont labelFontOfSize:kTitleFontSize]];
216  [title_label sizeToFit];
217  CGFloat x_offset = (frame_rect.size.width - NSWidth([title_label frame])) / 2;
218  CGFloat y_offset =
219      (NSHeight([button frame]) - NSHeight([title_label frame])) / 2;
220  [title_label setFrameOrigin:NSMakePoint(x_offset, y_offset)];
221
222  [container addSubview:button];
223  [container addSubview:title_label];
224  CGFloat height = std::max(NSMaxY([title_label frame]),
225                            NSMaxY([button frame])) + kSmallVerticalSpacing;
226  [container setFrameSize:NSMakeSize(NSWidth([container frame]), height)];
227
228  return container.autorelease();
229}
230
231bool HasAuthError(Profile* profile) {
232  const SigninErrorController* error_controller =
233      profiles::GetSigninErrorController(profile);
234  return error_controller && error_controller->HasError();
235}
236
237}  // namespace
238
239// Class that listens to changes to the OAuth2Tokens for the active profile,
240// changes to the avatar menu model or browser close notifications.
241class ActiveProfileObserverBridge : public AvatarMenuObserver,
242                                    public content::NotificationObserver,
243                                    public OAuth2TokenService::Observer {
244 public:
245  ActiveProfileObserverBridge(ProfileChooserController* controller,
246                              Browser* browser)
247      : controller_(controller),
248        browser_(browser),
249        token_observer_registered_(false) {
250    registrar_.Add(this, chrome::NOTIFICATION_BROWSER_CLOSING,
251                   content::NotificationService::AllSources());
252    if (!browser_->profile()->IsGuestSession())
253      AddTokenServiceObserver();
254  }
255
256  virtual ~ActiveProfileObserverBridge() {
257    RemoveTokenServiceObserver();
258  }
259
260 private:
261  void AddTokenServiceObserver() {
262    ProfileOAuth2TokenService* oauth2_token_service =
263        ProfileOAuth2TokenServiceFactory::GetForProfile(browser_->profile());
264    DCHECK(oauth2_token_service);
265    oauth2_token_service->AddObserver(this);
266    token_observer_registered_ = true;
267  }
268
269  void RemoveTokenServiceObserver() {
270    if (!token_observer_registered_)
271      return;
272    DCHECK(browser_->profile());
273    ProfileOAuth2TokenService* oauth2_token_service =
274        ProfileOAuth2TokenServiceFactory::GetForProfile(browser_->profile());
275    DCHECK(oauth2_token_service);
276    oauth2_token_service->RemoveObserver(this);
277    token_observer_registered_ = false;
278  }
279
280  // OAuth2TokenService::Observer:
281  virtual void OnRefreshTokenAvailable(const std::string& account_id) OVERRIDE {
282    // Tokens can only be added by adding an account through the inline flow,
283    // which is started from the account management view. Refresh it to show the
284    // update.
285    profiles::BubbleViewMode viewMode = [controller_ viewMode];
286    if (viewMode == profiles::BUBBLE_VIEW_MODE_ACCOUNT_MANAGEMENT ||
287        viewMode == profiles::BUBBLE_VIEW_MODE_GAIA_SIGNIN ||
288        viewMode == profiles::BUBBLE_VIEW_MODE_GAIA_ADD_ACCOUNT ||
289        viewMode == profiles::BUBBLE_VIEW_MODE_GAIA_REAUTH) {
290      [controller_ initMenuContentsWithView:
291          profiles::BUBBLE_VIEW_MODE_ACCOUNT_MANAGEMENT];
292    }
293  }
294
295  virtual void OnRefreshTokenRevoked(const std::string& account_id) OVERRIDE {
296    // Tokens can only be removed from the account management view. Refresh it
297    // to show the update.
298    if ([controller_ viewMode] == profiles::BUBBLE_VIEW_MODE_ACCOUNT_MANAGEMENT)
299      [controller_ initMenuContentsWithView:
300          profiles::BUBBLE_VIEW_MODE_ACCOUNT_MANAGEMENT];
301  }
302
303  // AvatarMenuObserver:
304  virtual void OnAvatarMenuChanged(AvatarMenu* avatar_menu) OVERRIDE {
305    // While the bubble is open, the avatar menu can only change from the
306    // profile chooser view by modifying the current profile's photo or name.
307    [controller_
308        initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER];
309  }
310
311  // content::NotificationObserver:
312  virtual void Observe(
313      int type,
314      const content::NotificationSource& source,
315      const content::NotificationDetails& details) OVERRIDE {
316    DCHECK_EQ(chrome::NOTIFICATION_BROWSER_CLOSING, type);
317    if (browser_ == content::Source<Browser>(source).ptr()) {
318      RemoveTokenServiceObserver();
319      // Clean up the bubble's WebContents (used by the Gaia embedded view), to
320      // make sure the guest profile doesn't have any dangling host renderers.
321      // This can happen if Chrome is quit using Command-Q while the bubble is
322      // still open, which won't give the bubble a chance to be closed and
323      // clean up the WebContents itself.
324      [controller_ cleanUpEmbeddedViewContents];
325    }
326  }
327
328  ProfileChooserController* controller_;  // Weak; owns this.
329  Browser* browser_;  // Weak.
330  content::NotificationRegistrar registrar_;
331
332  // The observer can be removed both when closing the browser, and by just
333  // closing the avatar bubble. However, in the case of closing the browser,
334  // the avatar bubble will also be closed afterwards, resulting in a second
335  // attempt to remove the observer. This ensures the observer is only
336  // removed once.
337  bool token_observer_registered_;
338
339  DISALLOW_COPY_AND_ASSIGN(ActiveProfileObserverBridge);
340};
341
342// Custom button cell that adds a left padding before the button image, and
343// a custom spacing between the button image and title.
344@interface CustomPaddingImageButtonCell : NSButtonCell {
345 @private
346  // Padding added to the left margin of the button.
347  int leftMarginSpacing_;
348  // Spacing between the cell image and title.
349  int imageTitleSpacing_;
350}
351
352- (id)initWithLeftMarginSpacing:(int)leftMarginSpacing
353              imageTitleSpacing:(int)imageTitleSpacing;
354@end
355
356@implementation CustomPaddingImageButtonCell
357- (id)initWithLeftMarginSpacing:(int)leftMarginSpacing
358              imageTitleSpacing:(int)imageTitleSpacing {
359  if ((self = [super init])) {
360    leftMarginSpacing_ = leftMarginSpacing;
361    imageTitleSpacing_ = imageTitleSpacing;
362  }
363  return self;
364}
365
366- (NSRect)drawTitle:(NSAttributedString*)title
367          withFrame:(NSRect)frame
368             inView:(NSView*)controlView {
369  NSRect marginRect;
370  NSDivideRect(frame, &marginRect, &frame, leftMarginSpacing_, NSMinXEdge);
371
372  // The title frame origin isn't aware of the left margin spacing added
373  // in -drawImage, so it must be added when drawing the title as well.
374  if ([self imagePosition] == NSImageLeft)
375    NSDivideRect(frame, &marginRect, &frame, imageTitleSpacing_, NSMinXEdge);
376
377  return [super drawTitle:title withFrame:frame inView:controlView];
378}
379
380- (void)drawImage:(NSImage*)image
381        withFrame:(NSRect)frame
382           inView:(NSView*)controlView {
383  if ([self imagePosition] == NSImageLeft)
384    frame.origin.x = leftMarginSpacing_;
385  [super drawImage:image withFrame:frame inView:controlView];
386}
387
388- (NSSize)cellSize {
389  NSSize buttonSize = [super cellSize];
390  buttonSize.width += leftMarginSpacing_;
391  if ([self imagePosition] == NSImageLeft)
392    buttonSize.width += imageTitleSpacing_;
393  return buttonSize;
394}
395
396@end
397
398// A custom button that has a transparent backround.
399@interface TransparentBackgroundButton : NSButton
400@end
401
402@implementation TransparentBackgroundButton
403- (id)initWithFrame:(NSRect)frameRect {
404  if ((self = [super initWithFrame:frameRect])) {
405    [self setBordered:NO];
406    [self setFont:[NSFont labelFontOfSize:kTextFontSize]];
407    [self setButtonType:NSMomentaryChangeButton];
408  }
409  return self;
410}
411
412- (void)drawRect:(NSRect)dirtyRect {
413  NSColor* backgroundColor = [NSColor colorWithCalibratedWhite:1 alpha:0.6f];
414  [backgroundColor setFill];
415  NSRectFillUsingOperation(dirtyRect, NSCompositeSourceAtop);
416  [super drawRect:dirtyRect];
417}
418@end
419
420// A custom image control that shows a "Change" button when moused over.
421@interface EditableProfilePhoto : NSImageView {
422 @private
423  AvatarMenu* avatarMenu_;  // Weak; Owned by ProfileChooserController.
424  base::scoped_nsobject<TransparentBackgroundButton> changePhotoButton_;
425  // Used to display the "Change" button on hover.
426  ui::ScopedCrTrackingArea trackingArea_;
427  ProfileChooserController* controller_;
428}
429
430- (id)initWithFrame:(NSRect)frameRect
431         avatarMenu:(AvatarMenu*)avatarMenu
432        profileIcon:(const gfx::Image&)profileIcon
433     editingAllowed:(BOOL)editingAllowed
434     withController:(ProfileChooserController*)controller;
435
436// Called when the "Change" button is clicked.
437- (void)editPhoto:(id)sender;
438
439// When hovering over the profile photo, show the "Change" button.
440- (void)mouseEntered:(NSEvent*)event;
441
442// When hovering away from the profile photo, hide the "Change" button.
443- (void)mouseExited:(NSEvent*)event;
444@end
445
446@interface EditableProfilePhoto (Private)
447// Create the "Change" avatar photo button.
448- (TransparentBackgroundButton*)changePhotoButtonWithRect:(NSRect)rect;
449@end
450
451@implementation EditableProfilePhoto
452- (id)initWithFrame:(NSRect)frameRect
453         avatarMenu:(AvatarMenu*)avatarMenu
454        profileIcon:(const gfx::Image&)profileIcon
455     editingAllowed:(BOOL)editingAllowed
456     withController:(ProfileChooserController*)controller {
457  if ((self = [super initWithFrame:frameRect])) {
458    avatarMenu_ = avatarMenu;
459    controller_ = controller;
460    [self setImage:CreateProfileImage(
461        profileIcon, kLargeImageSide).ToNSImage()];
462
463    // Add a tracking area so that we can show/hide the button when hovering.
464    trackingArea_.reset([[CrTrackingArea alloc]
465        initWithRect:[self bounds]
466             options:NSTrackingMouseEnteredAndExited | NSTrackingActiveAlways
467               owner:self
468            userInfo:nil]);
469    [self addTrackingArea:trackingArea_.get()];
470
471    NSRect bounds = NSMakeRect(0, 0, kLargeImageSide, kLargeImageSide);
472    if (editingAllowed) {
473      changePhotoButton_.reset([self changePhotoButtonWithRect:bounds]);
474      [self addSubview:changePhotoButton_];
475
476      // Hide the button until the image is hovered over.
477      [changePhotoButton_ setHidden:YES];
478    }
479  }
480  return self;
481}
482
483- (void)drawRect:(NSRect)dirtyRect {
484  NSRect bounds = [self bounds];
485
486  // Display the profile picture as a circle.
487  NSBezierPath* path = [NSBezierPath bezierPathWithOvalInRect:bounds];
488  [path addClip];
489  [self.image drawAtPoint:bounds.origin
490                 fromRect:bounds
491                operation:NSCompositeSourceOver
492                 fraction:1.0];
493
494}
495
496- (void)editPhoto:(id)sender {
497  avatarMenu_->EditProfile(avatarMenu_->GetActiveProfileIndex());
498  [controller_
499      postActionPerformed:ProfileMetrics::PROFILE_DESKTOP_MENU_EDIT_IMAGE];
500}
501
502- (void)mouseEntered:(NSEvent*)event {
503  [changePhotoButton_ setHidden:NO];
504}
505
506- (void)mouseExited:(NSEvent*)event {
507  [changePhotoButton_ setHidden:YES];
508}
509
510- (TransparentBackgroundButton*)changePhotoButtonWithRect:(NSRect)rect {
511  TransparentBackgroundButton* button =
512      [[TransparentBackgroundButton alloc] initWithFrame:rect];
513  [button setImage:ui::ResourceBundle::GetSharedInstance().GetNativeImageNamed(
514      IDR_ICON_PROFILES_EDIT_CAMERA).AsNSImage()];
515  [button setImagePosition:NSImageOnly];
516  [button setTarget:self];
517  [button setAction:@selector(editPhoto:)];
518  return button;
519}
520@end
521
522// A custom text control that turns into a textfield for editing when clicked.
523@interface EditableProfileNameButton : HoverImageButton {
524 @private
525  base::scoped_nsobject<NSTextField> profileNameTextField_;
526  Profile* profile_;  // Weak.
527  ProfileChooserController* controller_;
528}
529
530- (id)initWithFrame:(NSRect)frameRect
531            profile:(Profile*)profile
532        profileName:(NSString*)profileName
533     editingAllowed:(BOOL)editingAllowed
534     withController:(ProfileChooserController*)controller;
535
536// Called when the button is clicked.
537- (void)showEditableView:(id)sender;
538
539// Called when enter is pressed in the text field.
540- (void)saveProfileName:(id)sender;
541
542@end
543
544@implementation EditableProfileNameButton
545- (id)initWithFrame:(NSRect)frameRect
546            profile:(Profile*)profile
547        profileName:(NSString*)profileName
548     editingAllowed:(BOOL)editingAllowed
549     withController:(ProfileChooserController*)controller {
550  if ((self = [super initWithFrame:frameRect])) {
551    profile_ = profile;
552    controller_ = controller;
553
554    if (editingAllowed) {
555      // Show an "edit" pencil icon when hovering over. In the default state,
556      // we need to create an empty placeholder of the correct size, so that
557      // the text doesn't jump around when the hovered icon appears.
558      ui::ResourceBundle* rb = &ui::ResourceBundle::GetSharedInstance();
559      NSImage* hoverImage = rb->GetNativeImageNamed(
560          IDR_ICON_PROFILES_EDIT_HOVER).AsNSImage();
561
562      // In order to center the button title, we need to add a left padding of
563      // the same width as the pencil icon.
564      base::scoped_nsobject<CustomPaddingImageButtonCell> cell(
565          [[CustomPaddingImageButtonCell alloc]
566              initWithLeftMarginSpacing:[hoverImage size].width
567                      imageTitleSpacing:0]);
568      [self setCell:cell.get()];
569
570      NSImage* placeholder = [[NSImage alloc] initWithSize:[hoverImage size]];
571      [self setDefaultImage:placeholder];
572      [self setHoverImage:hoverImage];
573      [self setAlternateImage:
574          rb->GetNativeImageNamed(IDR_ICON_PROFILES_EDIT_PRESSED).AsNSImage()];
575      [self setImagePosition:NSImageRight];
576      [self setTarget:self];
577      [self setAction:@selector(showEditableView:)];
578
579      // We need to subtract the width of the bezel from the frame rect, so that
580      // the textfield can take the exact same space as the button.
581      frameRect.size.height -= 2 * kBezelThickness;
582      frameRect.origin = NSMakePoint(0, kBezelThickness);
583      profileNameTextField_.reset(
584          [[NSTextField alloc] initWithFrame:frameRect]);
585      [profileNameTextField_ setStringValue:profileName];
586      [profileNameTextField_ setFont:[NSFont labelFontOfSize:kTitleFontSize]];
587      [profileNameTextField_ setEditable:YES];
588      [profileNameTextField_ setDrawsBackground:YES];
589      [profileNameTextField_ setBezeled:YES];
590      [profileNameTextField_ setAlignment:NSCenterTextAlignment];
591      [[profileNameTextField_ cell] setWraps:NO];
592      [[profileNameTextField_ cell] setLineBreakMode:
593          NSLineBreakByTruncatingTail];
594      [self addSubview:profileNameTextField_];
595      [profileNameTextField_ setTarget:self];
596      [profileNameTextField_ setAction:@selector(saveProfileName:)];
597
598      // Hide the textfield until the user clicks on the button.
599      [profileNameTextField_ setHidden:YES];
600    }
601
602    [self setBordered:NO];
603    [self setFont:[NSFont labelFontOfSize:kTitleFontSize]];
604    [self setAlignment:NSCenterTextAlignment];
605    [[self cell] setLineBreakMode:NSLineBreakByTruncatingTail];
606    [self setTitle:profileName];
607  }
608  return self;
609}
610
611- (void)saveProfileName:(id)sender {
612  NSString* text = [profileNameTextField_ stringValue];
613  // Empty profile names are not allowed, and are treated as a cancel.
614  if ([text length] > 0) {
615    profiles::UpdateProfileName(profile_, base::SysNSStringToUTF16(text));
616    [controller_
617        postActionPerformed:ProfileMetrics::PROFILE_DESKTOP_MENU_EDIT_NAME];
618    [self setTitle:text];
619  }
620  [profileNameTextField_ setHidden:YES];
621}
622
623- (void)showEditableView:(id)sender {
624  [profileNameTextField_ setHidden:NO];
625  [[self window] makeFirstResponder:profileNameTextField_];
626}
627
628@end
629
630// A custom button that allows for setting a background color when hovered over.
631@interface BackgroundColorHoverButton : HoverImageButton {
632 @private
633  base::scoped_nsobject<NSColor> backgroundColor_;
634  base::scoped_nsobject<NSColor> hoverColor_;
635}
636@end
637
638@implementation BackgroundColorHoverButton
639
640- (id)initWithFrame:(NSRect)frameRect
641  imageTitleSpacing:(int)imageTitleSpacing
642    backgroundColor:(NSColor*)backgroundColor {
643  if ((self = [super initWithFrame:frameRect])) {
644    backgroundColor_.reset([backgroundColor retain]);
645    hoverColor_.reset([gfx::SkColorToCalibratedNSColor(
646        ui::NativeTheme::instance()->GetSystemColor(
647            ui::NativeTheme::kColorId_ButtonHoverBackgroundColor)) retain]);
648
649    [self setBordered:NO];
650    [self setFont:[NSFont labelFontOfSize:kTextFontSize]];
651    [self setButtonType:NSMomentaryChangeButton];
652
653    base::scoped_nsobject<CustomPaddingImageButtonCell> cell(
654        [[CustomPaddingImageButtonCell alloc]
655            initWithLeftMarginSpacing:kHorizontalSpacing
656                    imageTitleSpacing:imageTitleSpacing]);
657    [cell setLineBreakMode:NSLineBreakByTruncatingTail];
658    [self setCell:cell.get()];
659  }
660  return self;
661}
662
663- (void)setHoverState:(HoverState)state {
664  [super setHoverState:state];
665  bool isHighlighted = ([self hoverState] != kHoverStateNone);
666
667  NSColor* backgroundColor = isHighlighted ? hoverColor_ : backgroundColor_;
668  [[self cell] setBackgroundColor:backgroundColor];
669}
670
671@end
672
673// A custom view with the given background color.
674@interface BackgroundColorView : NSView {
675 @private
676  base::scoped_nsobject<NSColor> backgroundColor_;
677}
678@end
679
680@implementation BackgroundColorView
681- (id)initWithFrame:(NSRect)frameRect
682          withColor:(NSColor*)color {
683  if ((self = [super initWithFrame:frameRect]))
684    backgroundColor_.reset([color retain]);
685  return self;
686}
687
688- (void)drawRect:(NSRect)dirtyRect {
689  [backgroundColor_ setFill];
690  NSRectFill(dirtyRect);
691  [super drawRect:dirtyRect];
692}
693@end
694
695@interface ProfileChooserController ()
696// Builds the profile chooser view.
697- (NSView*)buildProfileChooserView;
698
699// Builds a tutorial card with a title label using |titleMessageId|, a content
700// label using |contentMessageId|, and a bottom row with a right-aligned link
701// using |linkMessageId|, and a left aligned button using |buttonMessageId|.
702// On click, the link would execute |linkAction|, and the button would execute
703// |buttonAction|. It sets |tutorialMode_| to the given |mode|.
704- (NSView*)tutorialViewWithMode:(profiles::TutorialMode)mode
705                   titleMessage:(int)titleMessageId
706                 contentMessage:(int)contentMessageId
707                    linkMessage:(int)linkMessageId
708                  buttonMessage:(int)buttonMessageId
709                     linkAction:(SEL)linkAction
710                   buttonAction:(SEL)buttonAction;
711
712// Builds a a tutorial card for new profile management preview if needed. if
713// new profile management is not enabled yet, then it prompts the user to try
714// out the feature. Otherwise, it notifies the user that the feature has been
715// enabled if needed.
716- (NSView*)buildPreviewTutorialIfNeeded:(const AvatarMenu::Item&)item;
717
718// Creates the main profile card for the profile |item| at the top of
719// the bubble.
720- (NSView*)createCurrentProfileView:(const AvatarMenu::Item&)item;
721
722// Creates the possible links for the main profile card with profile |item|.
723- (NSView*)createCurrentProfileLinksForItem:(const AvatarMenu::Item&)item
724                                       rect:(NSRect)rect;
725
726// Creates the disclaimer text for supervised users, telling them that the
727// manager can view their history etc.
728- (NSView*)createSupervisedUserDisclaimerView;
729
730// Creates a main profile card for the guest user.
731- (NSView*)createGuestProfileView;
732
733// Creates an item for the profile |itemIndex| that is used in the fast profile
734// switcher in the middle of the bubble.
735- (NSButton*)createOtherProfileView:(int)itemIndex;
736
737// Creates the "Not you" and Lock option buttons.
738- (NSView*)createOptionsViewWithRect:(NSRect)rect
739                          enableLock:(BOOL)enableLock;
740
741// Creates the account management view for the active profile.
742- (NSView*)createCurrentProfileAccountsView:(NSRect)rect;
743
744// Creates the list of accounts for the active profile.
745- (NSView*)createAccountsListWithRect:(NSRect)rect;
746
747// Creates the Gaia sign-in/add account view.
748- (NSView*)buildGaiaEmbeddedView;
749
750// Creates the account removal view.
751- (NSView*)buildAccountRemovalView;
752
753// Creates the end-preview view.
754- (NSView*)buildEndPreviewView;
755
756// Creates a button with |text|, an icon given by |imageResourceId| and with
757// |action|. The icon |alternateImageResourceId| is displayed in the button's
758// hovered and pressed states.
759- (NSButton*)hoverButtonWithRect:(NSRect)rect
760                            text:(NSString*)text
761                 imageResourceId:(int)imageResourceId
762        alternateImageResourceId:(int)alternateImageResourceId
763                          action:(SEL)action;
764
765// Creates a generic link button with |title| and an |action| positioned at
766// |frameOrigin|.
767- (NSButton*)linkButtonWithTitle:(NSString*)title
768                     frameOrigin:(NSPoint)frameOrigin
769                          action:(SEL)action;
770
771// Creates an email account button with |title| and a remove icon. If
772// |reauthRequired| is true, the button also displays a warning icon. |tag|
773// indicates which account the button refers to.
774- (NSButton*)accountButtonWithRect:(NSRect)rect
775                             title:(const std::string&)title
776                               tag:(int)tag
777                    reauthRequired:(BOOL)reauthRequired;
778@end
779
780@implementation ProfileChooserController
781- (profiles::BubbleViewMode) viewMode {
782  return viewMode_;
783}
784
785- (IBAction)switchToProfile:(id)sender {
786  // Check the event flags to see if a new window should be created.
787  bool alwaysCreate = ui::WindowOpenDispositionFromNSEvent(
788      [NSApp currentEvent]) == NEW_WINDOW;
789  avatarMenu_->SwitchToProfile([sender tag], alwaysCreate,
790                               ProfileMetrics::SWITCH_PROFILE_ICON);
791}
792
793- (IBAction)showUserManager:(id)sender {
794  profiles::ShowUserManagerMaybeWithTutorial(browser_->profile());
795}
796
797- (IBAction)exitGuest:(id)sender {
798  DCHECK(browser_->profile()->IsGuestSession());
799  [self showUserManager:sender];
800  profiles::CloseGuestProfileWindows();
801}
802
803- (IBAction)showAccountManagement:(id)sender {
804  [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_ACCOUNT_MANAGEMENT];
805}
806
807- (IBAction)hideAccountManagement:(id)sender {
808  [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER];
809}
810
811- (IBAction)lockProfile:(id)sender {
812  profiles::LockProfile(browser_->profile());
813  [self postActionPerformed:ProfileMetrics::PROFILE_DESKTOP_MENU_LOCK];
814}
815
816- (IBAction)showInlineSigninPage:(id)sender {
817  [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_GAIA_SIGNIN];
818}
819
820- (IBAction)showTabbedSigninPage:(id)sender {
821  chrome::ShowBrowserSignin(browser_, signin::SOURCE_MENU);
822}
823
824- (IBAction)addAccount:(id)sender {
825  [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_GAIA_ADD_ACCOUNT];
826  [self postActionPerformed:ProfileMetrics::PROFILE_DESKTOP_MENU_ADD_ACCT];
827}
828
829- (IBAction)navigateBackFromSigninPage:(id)sender {
830  std::string primaryAccount = SigninManagerFactory::GetForProfile(
831      browser_->profile())->GetAuthenticatedUsername();
832  [self initMenuContentsWithView:primaryAccount.empty() ?
833      profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER :
834      profiles::BUBBLE_VIEW_MODE_ACCOUNT_MANAGEMENT];
835}
836
837- (IBAction)showAccountRemovalView:(id)sender {
838  DCHECK(!isGuestSession_);
839
840  // Tag is either |kPrimaryProfileTag| for the primary account, or equal to the
841  // index in |currentProfileAccounts_| for a secondary account.
842  int tag = [sender tag];
843  if (tag == kPrimaryProfileTag) {
844    accountIdToRemove_ = SigninManagerFactory::GetForProfile(
845        browser_->profile())->GetAuthenticatedUsername();
846  } else {
847    DCHECK(ContainsKey(currentProfileAccounts_, tag));
848    accountIdToRemove_ = currentProfileAccounts_[tag];
849  }
850
851  [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_ACCOUNT_REMOVAL];
852}
853
854- (IBAction)showAccountReauthenticationView:(id)sender {
855  DCHECK(!isGuestSession_);
856  [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_GAIA_REAUTH];
857}
858
859- (IBAction)removeAccount:(id)sender {
860  DCHECK(!accountIdToRemove_.empty());
861  ProfileOAuth2TokenServiceFactory::GetPlatformSpecificForProfile(
862      browser_->profile())->RevokeCredentials(accountIdToRemove_);
863  [self postActionPerformed:ProfileMetrics::PROFILE_DESKTOP_MENU_REMOVE_ACCT];
864  accountIdToRemove_.clear();
865
866  [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_ACCOUNT_MANAGEMENT];
867}
868
869- (IBAction)openTutorialLearnMoreURL:(id)sender {
870  ProfileMetrics::LogProfileUpgradeEnrollment(
871      ProfileMetrics::PROFILE_ENROLLMENT_LAUNCH_LEARN_MORE);
872  // TODO(guohui): update |learnMoreUrl| once it is decided.
873  const GURL learnMoreUrl("https://support.google.com/chrome/?hl=en#to");
874  chrome::NavigateParams params(browser_->profile(), learnMoreUrl,
875                                content::PAGE_TRANSITION_LINK);
876  params.disposition = NEW_FOREGROUND_TAB;
877  chrome::Navigate(&params);
878}
879
880- (IBAction)enableNewProfileManagementPreview:(id)sender {
881  ProfileMetrics::LogProfileUpgradeEnrollment(
882      ProfileMetrics::PROFILE_ENROLLMENT_ACCEPT_NEW_PROFILE_MGMT);
883  profiles::EnableNewProfileManagementPreview(browser_->profile());
884}
885
886- (IBAction)dismissTutorial:(id)sender {
887  ProfileMetrics::LogProfileUpgradeEnrollment(
888      ProfileMetrics::PROFILE_ENROLLMENT_CLOSE_WELCOME_CARD);
889  // If the user manually dismissed the tutorial, never show it again by setting
890  // the number of times shown to the maximum plus 1, so that later we could
891  // distinguish between the dismiss case and the case when the tutorial is
892  // indeed shown for the maximum number of times.
893  browser_->profile()->GetPrefs()->SetInteger(
894      prefs::kProfileAvatarTutorialShown, kProfileAvatarTutorialShowMax + 1);
895  [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER];
896}
897
898- (IBAction)showSendFeedbackTutorial:(id)sender {
899  ProfileMetrics::LogProfileUpgradeEnrollment(
900      ProfileMetrics::PROFILE_ENROLLMENT_SEND_FEEDBACK);
901  tutorialMode_ = profiles::TUTORIAL_MODE_SEND_FEEDBACK;
902  [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER];
903}
904
905- (IBAction)showEndPreviewView:(id)sender {
906  [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_END_PREVIEW];
907}
908
909- (IBAction)sendFeedback:(id)sender {
910  chrome::OpenFeedbackDialog(browser_);
911}
912
913- (IBAction)endPreviewAndRelaunch:(id)sender {
914  profiles::DisableNewProfileManagementPreview(browser_->profile());
915}
916
917- (void)cleanUpEmbeddedViewContents {
918  webContents_.reset();
919}
920
921- (id)initWithBrowser:(Browser*)browser
922           anchoredAt:(NSPoint)point
923             withMode:(profiles::BubbleViewMode)mode
924      withServiceType:(signin::GAIAServiceType)serviceType {
925  base::scoped_nsobject<InfoBubbleWindow> window([[InfoBubbleWindow alloc]
926      initWithContentRect:ui::kWindowSizeDeterminedLater
927                styleMask:NSBorderlessWindowMask
928                  backing:NSBackingStoreBuffered
929                    defer:NO]);
930
931  if ((self = [super initWithWindow:window
932                       parentWindow:browser->window()->GetNativeWindow()
933                         anchoredAt:point])) {
934    browser_ = browser;
935    viewMode_ = mode;
936    tutorialMode_ = profiles::TUTORIAL_MODE_NONE;
937    observer_.reset(new ActiveProfileObserverBridge(self, browser_));
938    serviceType_ = serviceType;
939
940    avatarMenu_.reset(new AvatarMenu(
941        &g_browser_process->profile_manager()->GetProfileInfoCache(),
942        observer_.get(),
943        browser_));
944    avatarMenu_->RebuildMenu();
945
946    // Guest profiles do not have a token service.
947    isGuestSession_ = browser_->profile()->IsGuestSession();
948
949    // If view mode is PROFILE_CHOOSER but there is an auth error, force
950    // ACCOUNT_MANAGEMENT mode.
951    if (viewMode_ == profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER &&
952        HasAuthError(browser_->profile())) {
953      viewMode_ = profiles::BUBBLE_VIEW_MODE_ACCOUNT_MANAGEMENT;
954    }
955
956    [[self bubble] setAlignment:info_bubble::kAlignRightEdgeToAnchorEdge];
957    [[self bubble] setArrowLocation:info_bubble::kNoArrow];
958    [[self bubble] setBackgroundColor:GetDialogBackgroundColor()];
959    [self initMenuContentsWithView:viewMode_];
960  }
961
962  return self;
963}
964
965- (void)initMenuContentsWithView:(profiles::BubbleViewMode)viewToDisplay {
966  viewMode_ = viewToDisplay;
967  NSView* contentView = [[self window] contentView];
968  [contentView setSubviews:[NSArray array]];
969  NSView* subView;
970
971  switch (viewMode_) {
972    case profiles::BUBBLE_VIEW_MODE_GAIA_SIGNIN:
973    case profiles::BUBBLE_VIEW_MODE_GAIA_ADD_ACCOUNT:
974    case profiles::BUBBLE_VIEW_MODE_GAIA_REAUTH:
975      subView = [self buildGaiaEmbeddedView];
976      break;
977    case profiles::BUBBLE_VIEW_MODE_ACCOUNT_REMOVAL:
978      subView = [self buildAccountRemovalView];
979      break;
980    case profiles::BUBBLE_VIEW_MODE_END_PREVIEW:
981      subView = [self buildEndPreviewView];
982      break;
983    case profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER:
984    case profiles::BUBBLE_VIEW_MODE_ACCOUNT_MANAGEMENT:
985      subView = [self buildProfileChooserView];
986      break;
987  }
988
989  [contentView addSubview:subView];
990  SetWindowSize([self window],
991      NSMakeSize(NSWidth([subView frame]), NSHeight([subView frame])));
992}
993
994- (NSView*)buildProfileChooserView {
995  base::scoped_nsobject<NSView> container(
996      [[NSView alloc] initWithFrame:NSZeroRect]);
997
998  NSView* tutorialView = nil;
999  NSView* currentProfileView = nil;
1000  base::scoped_nsobject<NSMutableArray> otherProfiles(
1001      [[NSMutableArray alloc] init]);
1002  // Local and guest profiles cannot lock their profile.
1003  bool enableLock = false;
1004  // Store the most recently displayed tutorial mode
1005  profiles::TutorialMode lastTutorialMode = tutorialMode_;
1006
1007  // Loop over the profiles in reverse, so that they are sorted by their
1008  // y-coordinate, and separate them into active and "other" profiles.
1009  for (int i = avatarMenu_->GetNumberOfItems() - 1; i >= 0; --i) {
1010    const AvatarMenu::Item& item = avatarMenu_->GetItemAt(i);
1011    if (item.active) {
1012      if (viewMode_ == profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER)
1013        tutorialView = [self buildPreviewTutorialIfNeeded:item];
1014      currentProfileView = [self createCurrentProfileView:item];
1015      enableLock = item.signed_in;
1016    } else {
1017      [otherProfiles addObject:[self createOtherProfileView:i]];
1018    }
1019  }
1020  if (!currentProfileView)  // Guest windows don't have an active profile.
1021    currentProfileView = [self createGuestProfileView];
1022
1023  // |yOffset| is the next position at which to draw in |container|
1024  // coordinates. Add a pixel offset so that the bottom option buttons don't
1025  // overlap the bubble's rounded corners.
1026  CGFloat yOffset = 1;
1027
1028  // Option buttons. Only available with the new profile management flag.
1029  if (switches::IsNewProfileManagement()) {
1030    NSRect rect = NSMakeRect(0, yOffset, kFixedMenuWidth, 0);
1031    NSView* optionsView = [self createOptionsViewWithRect:rect
1032                                               enableLock:enableLock];
1033    [container addSubview:optionsView];
1034    rect.origin.y = NSMaxY([optionsView frame]);
1035
1036    NSBox* separator = [self horizontalSeparatorWithFrame:rect];
1037    [container addSubview:separator];
1038    yOffset = NSMaxY([separator frame]);
1039  }
1040
1041  if (viewMode_ == profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER &&
1042      switches::IsFastUserSwitching()) {
1043    // Other profiles switcher. The profiles have already been sorted
1044    // by their y-coordinate, so they can be added in the existing order.
1045    for (NSView *otherProfileView in otherProfiles.get()) {
1046      [otherProfileView setFrameOrigin:NSMakePoint(0, yOffset)];
1047      [container addSubview:otherProfileView];
1048      yOffset = NSMaxY([otherProfileView frame]);
1049
1050      NSBox* separator = [self horizontalSeparatorWithFrame:NSMakeRect(
1051          0, yOffset, kFixedMenuWidth, 0)];
1052      [container addSubview:separator];
1053      yOffset = NSMaxY([separator frame]);
1054    }
1055  }
1056
1057  // For supervised users, add the disclaimer text.
1058  if (browser_->profile()->IsSupervised()) {
1059    yOffset += kSmallVerticalSpacing;
1060    NSView* disclaimerContainer = [self createSupervisedUserDisclaimerView];
1061    [disclaimerContainer setFrameOrigin:NSMakePoint(0, yOffset)];
1062    [container addSubview:disclaimerContainer];
1063    yOffset = NSMaxY([disclaimerContainer frame]);
1064    yOffset += kSmallVerticalSpacing;
1065
1066    NSBox* separator = [self horizontalSeparatorWithFrame:NSMakeRect(
1067        0, yOffset, kFixedMenuWidth, 0)];
1068    [container addSubview:separator];
1069    yOffset = NSMaxY([separator frame]);
1070  }
1071
1072  if (viewMode_ == profiles::BUBBLE_VIEW_MODE_ACCOUNT_MANAGEMENT) {
1073    NSView* currentProfileAccountsView = [self createCurrentProfileAccountsView:
1074        NSMakeRect(0, yOffset, kFixedMenuWidth, 0)];
1075    [container addSubview:currentProfileAccountsView];
1076    yOffset = NSMaxY([currentProfileAccountsView frame]);
1077
1078    NSBox* accountsSeparator = [self horizontalSeparatorWithFrame:
1079        NSMakeRect(0, yOffset, kFixedMenuWidth, 0)];
1080    [container addSubview:accountsSeparator];
1081    yOffset = NSMaxY([accountsSeparator frame]);
1082  }
1083
1084  // Active profile card.
1085  if (currentProfileView) {
1086    yOffset += kVerticalSpacing;
1087    [currentProfileView setFrameOrigin:NSMakePoint(0, yOffset)];
1088    [container addSubview:currentProfileView];
1089    yOffset = NSMaxY([currentProfileView frame]) + kVerticalSpacing;
1090  }
1091
1092  if (tutorialView) {
1093    [tutorialView setFrameOrigin:NSMakePoint(0, yOffset)];
1094    [container addSubview:tutorialView];
1095    yOffset = NSMaxY([tutorialView frame]);
1096    if (!switches::IsNewProfileManagement() &&
1097        tutorialMode_ != lastTutorialMode) {
1098      ProfileMetrics::LogProfileUpgradeEnrollment(
1099          ProfileMetrics::PROFILE_ENROLLMENT_SHOW_PREVIEW_PROMO);
1100    }
1101  } else {
1102    tutorialMode_ = profiles::TUTORIAL_MODE_NONE;
1103  }
1104
1105  [container setFrameSize:NSMakeSize(kFixedMenuWidth, yOffset)];
1106  return container.autorelease();
1107}
1108
1109- (NSView*)buildPreviewTutorialIfNeeded:(const AvatarMenu::Item&)item {
1110  if (!switches::IsNewProfileManagement()) {
1111    return [self tutorialViewWithMode:profiles::TUTORIAL_MODE_ENABLE_PREVIEW
1112                         titleMessage:IDS_PROFILES_PREVIEW_TUTORIAL_TITLE
1113                       contentMessage:IDS_PROFILES_PREVIEW_TUTORIAL_CONTENT_TEXT
1114                          linkMessage:IDS_PROFILES_PROFILE_TUTORIAL_LEARN_MORE
1115                        buttonMessage:IDS_PROFILES_TUTORIAL_TRY_BUTTON
1116                           linkAction:@selector(openTutorialLearnMoreURL:)
1117                         buttonAction:
1118        @selector(enableNewProfileManagementPreview:)];
1119  }
1120
1121  if (!switches::IsNewProfileManagementPreviewEnabled())
1122    return nil;
1123
1124  if (tutorialMode_ == profiles::TUTORIAL_MODE_SEND_FEEDBACK) {
1125    return [self tutorialViewWithMode:profiles::TUTORIAL_MODE_SEND_FEEDBACK
1126                         titleMessage:IDS_PROFILES_FEEDBACK_TUTORIAL_TITLE
1127                       contentMessage:
1128        IDS_PROFILES_FEEDBACK_TUTORIAL_CONTENT_TEXT
1129                          linkMessage:IDS_PROFILES_END_PREVIEW
1130                        buttonMessage:IDS_PROFILES_SEND_FEEDBACK_BUTTON
1131                           linkAction:@selector(showEndPreviewView:)
1132                         buttonAction:@selector(sendFeedback:)];
1133  }
1134
1135  Profile* profile = browser_->profile();
1136  const int showCount = profile->GetPrefs()->GetInteger(
1137      prefs::kProfileAvatarTutorialShown);
1138  // Do not show the tutorial if user has dismissed it.
1139  if (showCount > kProfileAvatarTutorialShowMax)
1140    return nil;
1141
1142  if (tutorialMode_ != profiles::TUTORIAL_MODE_WELCOME) {
1143    if (showCount == kProfileAvatarTutorialShowMax)
1144      return nil;
1145    profile->GetPrefs()->SetInteger(
1146        prefs::kProfileAvatarTutorialShown, showCount + 1);
1147  }
1148
1149  return [self tutorialViewWithMode:profiles::TUTORIAL_MODE_WELCOME
1150                       titleMessage:IDS_PROFILES_PREVIEW_ENABLED_TUTORIAL_TITLE
1151                     contentMessage:
1152      IDS_PROFILES_PREVIEW_ENABLED_TUTORIAL_CONTENT_TEXT
1153                        linkMessage:IDS_PROFILES_PROFILE_TUTORIAL_LEARN_MORE
1154                      buttonMessage:IDS_PROFILES_TUTORIAL_OK_BUTTON
1155                         linkAction:@selector(openTutorialLearnMoreURL:)
1156                       buttonAction:@selector(dismissTutorial:)];
1157}
1158
1159- (NSView*)tutorialViewWithMode:(profiles::TutorialMode)mode
1160                   titleMessage:(int)titleMessageId
1161                 contentMessage:(int)contentMessageId
1162                    linkMessage:(int)linkMessageId
1163                  buttonMessage:(int)buttonMessageId
1164                     linkAction:(SEL)linkAction
1165                   buttonAction:(SEL)buttonAction {
1166  tutorialMode_ = mode;
1167
1168  NSColor* tutorialBackgroundColor =
1169      gfx::SkColorToSRGBNSColor(profiles::kAvatarTutorialBackgroundColor);
1170  base::scoped_nsobject<NSView> container([[BackgroundColorView alloc]
1171      initWithFrame:NSMakeRect(0, 0, kFixedMenuWidth, 0)
1172          withColor:tutorialBackgroundColor]);
1173  CGFloat availableWidth = kFixedMenuWidth - 2 * kHorizontalSpacing;
1174  CGFloat yOffset = kSmallVerticalSpacing;
1175
1176  // Adds links and buttons at the bottom.
1177  base::scoped_nsobject<NSButton> tutorialOkButton([[HoverButton alloc]
1178      initWithFrame:NSZeroRect]);
1179  [tutorialOkButton setTitle:l10n_util::GetNSString(
1180      buttonMessageId)];
1181  [tutorialOkButton setBezelStyle:NSRoundedBezelStyle];
1182  [tutorialOkButton setTarget:self];
1183  [tutorialOkButton setAction:buttonAction];
1184  [tutorialOkButton sizeToFit];
1185  NSSize buttonSize = [tutorialOkButton frame].size;
1186  const CGFloat kTopBottomTextPadding = 6;
1187  const CGFloat kLeftRightTextPadding = 15;
1188  buttonSize.width += 2 * kLeftRightTextPadding;
1189  buttonSize.height += 2 * kTopBottomTextPadding;
1190  [tutorialOkButton setFrameSize:buttonSize];
1191  [tutorialOkButton setAlignment:NSCenterTextAlignment];
1192  [tutorialOkButton setFrameOrigin:NSMakePoint(
1193      kFixedMenuWidth - NSWidth([tutorialOkButton frame]) - kHorizontalSpacing,
1194      yOffset)];
1195  [container addSubview:tutorialOkButton];
1196
1197  NSButton* learnMoreLink =
1198      [self linkButtonWithTitle:l10n_util::GetNSString(linkMessageId)
1199                    frameOrigin:NSZeroPoint
1200                         action:linkAction];
1201  [[learnMoreLink cell] setTextColor:[NSColor whiteColor]];
1202  CGFloat linkYOffset = yOffset + (NSHeight([tutorialOkButton frame]) -
1203                                   NSHeight([learnMoreLink frame])) / 2;
1204  [learnMoreLink setFrameOrigin:NSMakePoint(kHorizontalSpacing, linkYOffset)];
1205  [container addSubview:learnMoreLink];
1206
1207  yOffset = std::max(NSMaxY([learnMoreLink frame]),
1208                     NSMaxY([tutorialOkButton frame])) + kVerticalSpacing;
1209
1210  // Adds body content.
1211  NSTextField* contentLabel = BuildLabel(
1212      l10n_util::GetNSString(contentMessageId),
1213      NSMakePoint(kHorizontalSpacing, yOffset),
1214      tutorialBackgroundColor,
1215      gfx::SkColorToSRGBNSColor(profiles::kAvatarTutorialContentTextColor));
1216  [contentLabel setFrameSize:NSMakeSize(availableWidth, 0)];
1217  [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:contentLabel];
1218  [container addSubview:contentLabel];
1219  yOffset = NSMaxY([contentLabel frame]) + kSmallVerticalSpacing;
1220
1221  // Adds title.
1222  NSTextField* titleLabel =
1223      BuildLabel(l10n_util::GetNSString(titleMessageId),
1224                 NSMakePoint(kHorizontalSpacing, yOffset),
1225                 tutorialBackgroundColor,
1226                 [NSColor whiteColor] /* text_color */);
1227  [titleLabel setFont:[NSFont labelFontOfSize:kTitleFontSize]];
1228  [titleLabel sizeToFit];
1229  [titleLabel setFrameSize:
1230      NSMakeSize(availableWidth, NSHeight([titleLabel frame]))];
1231  [container addSubview:titleLabel];
1232  yOffset = NSMaxY([titleLabel frame]) + kVerticalSpacing;
1233
1234  [container setFrameSize:NSMakeSize(kFixedMenuWidth, yOffset)];
1235
1236  // Adds caret at the bottom.
1237  NSImage* caretImage = ui::ResourceBundle::GetSharedInstance().
1238      GetNativeImageNamed(IDR_ICON_PROFILES_MENU_CARET).AsNSImage();
1239  base::scoped_nsobject<NSImageView> caretView(
1240      [[NSImageView alloc] initWithFrame:NSMakeRect(
1241          kHorizontalSpacing, 0, caretImage.size.width,
1242          caretImage.size.height)]);
1243  [caretView setImage:caretImage];
1244
1245  base::scoped_nsobject<NSView> containerWithCaret([[NSView alloc]
1246      initWithFrame:NSMakeRect(0, 0, kFixedMenuWidth, 0)]);
1247  [containerWithCaret addSubview:caretView];
1248
1249  [container setFrameOrigin:NSMakePoint(0, caretImage.size.height)];
1250  [containerWithCaret addSubview:container];
1251
1252  [containerWithCaret setFrameSize:
1253      NSMakeSize(kFixedMenuWidth, NSMaxY([container frame]))];
1254  return containerWithCaret.autorelease();
1255}
1256
1257- (NSView*)createCurrentProfileView:(const AvatarMenu::Item&)item {
1258  base::scoped_nsobject<NSView> container([[NSView alloc]
1259      initWithFrame:NSZeroRect]);
1260
1261  CGFloat xOffset = kHorizontalSpacing;
1262  CGFloat yOffset = 0;
1263  CGFloat availableTextWidth = kFixedMenuWidth - 2 * kHorizontalSpacing;
1264
1265  // Profile options. This can be a link to the accounts view, the profile's
1266  // username for signed in users, or a "Sign in" button for local profiles.
1267  SigninManagerBase* signinManager =
1268      SigninManagerFactory::GetForProfile(
1269          browser_->profile()->GetOriginalProfile());
1270  if (!isGuestSession_ && signinManager->IsSigninAllowed()) {
1271    NSView* linksContainer =
1272        [self createCurrentProfileLinksForItem:item
1273                                          rect:NSMakeRect(xOffset, yOffset,
1274                                                          availableTextWidth,
1275                                                          0)];
1276    [container addSubview:linksContainer];
1277    yOffset = NSMaxY([linksContainer frame]);
1278  }
1279
1280  // Profile name, centered.
1281  bool editingAllowed = !isGuestSession_ &&
1282                        !browser_->profile()->IsSupervised();
1283  base::scoped_nsobject<EditableProfileNameButton> profileName(
1284      [[EditableProfileNameButton alloc]
1285          initWithFrame:NSMakeRect(xOffset, yOffset,
1286                                   availableTextWidth,
1287                                   kProfileButtonHeight)
1288                profile:browser_->profile()
1289            profileName:base::SysUTF16ToNSString(
1290                profiles::GetAvatarNameForProfile(browser_->profile()))
1291         editingAllowed:editingAllowed
1292         withController:self]);
1293
1294  [container addSubview:profileName];
1295  yOffset = NSMaxY([profileName frame]);
1296
1297  // Profile icon, centered.
1298  xOffset = (kFixedMenuWidth - kLargeImageSide) / 2;
1299  base::scoped_nsobject<EditableProfilePhoto> iconView(
1300      [[EditableProfilePhoto alloc]
1301          initWithFrame:NSMakeRect(xOffset, yOffset,
1302                                   kLargeImageSide, kLargeImageSide)
1303             avatarMenu:avatarMenu_.get()
1304            profileIcon:item.icon
1305         editingAllowed:!isGuestSession_
1306         withController:self]);
1307
1308  [container addSubview:iconView];
1309  yOffset = NSMaxY([iconView frame]);
1310
1311  if (browser_->profile()->IsSupervised()) {
1312    base::scoped_nsobject<NSImageView> supervisedIcon(
1313        [[NSImageView alloc] initWithFrame:NSZeroRect]);
1314    ui::ResourceBundle* rb = &ui::ResourceBundle::GetSharedInstance();
1315    [supervisedIcon setImage:rb->GetNativeImageNamed(
1316        IDR_ICON_PROFILES_MENU_SUPERVISED).ToNSImage()];
1317    NSSize size = [[supervisedIcon image] size];
1318    [supervisedIcon setFrameSize:size];
1319    NSRect parentFrame = [iconView frame];
1320    [supervisedIcon setFrameOrigin:NSMakePoint(NSMaxX(parentFrame) - size.width,
1321                                               NSMinY(parentFrame))];
1322    [container addSubview:supervisedIcon];
1323  }
1324
1325  if (switches::IsNewProfileManagementPreviewEnabled()) {
1326    base::scoped_nsobject<HoverImageButton> questionButton(
1327        [[HoverImageButton alloc] initWithFrame:NSZeroRect]);
1328    [questionButton setBordered:NO];
1329    ui::ResourceBundle* rb = &ui::ResourceBundle::GetSharedInstance();
1330    [questionButton setDefaultImage:rb->GetNativeImageNamed(
1331        IDR_ICON_PROFILES_MENU_QUESTION_STABLE).ToNSImage()];
1332    [questionButton setHoverImage:rb->GetNativeImageNamed(
1333        IDR_ICON_PROFILES_MENU_QUESTION_HOVER).ToNSImage()];
1334    [questionButton setPressedImage:rb->GetNativeImageNamed(
1335        IDR_ICON_PROFILES_MENU_QUESTION_SELECT).ToNSImage()];
1336    [questionButton setTarget:self];
1337    [questionButton setAction:@selector(showSendFeedbackTutorial:)];
1338    [questionButton sizeToFit];
1339    const CGFloat size = NSHeight([questionButton frame]) + 2;
1340    [questionButton setFrame:
1341        NSMakeRect(kHorizontalSpacing, yOffset - size, size, size)];
1342    [container addSubview:questionButton];
1343  }
1344
1345  [container setFrameSize:NSMakeSize(kFixedMenuWidth, yOffset)];
1346  return container.autorelease();
1347}
1348
1349- (NSView*)createCurrentProfileLinksForItem:(const AvatarMenu::Item&)item
1350                                       rect:(NSRect)rect {
1351  base::scoped_nsobject<NSView> container([[NSView alloc] initWithFrame:rect]);
1352
1353  // Don't double-apply the left margin to the sub-views.
1354  rect.origin.x = 0;
1355
1356  // The available links depend on the type of profile that is active.
1357  NSButton* link;
1358  if (item.signed_in) {
1359    if (switches::IsNewProfileManagement()) {
1360      NSString* linkTitle = l10n_util::GetNSString(
1361          viewMode_ == profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER ?
1362              IDS_PROFILES_PROFILE_MANAGE_ACCOUNTS_BUTTON :
1363              IDS_PROFILES_PROFILE_HIDE_MANAGE_ACCOUNTS_BUTTON);
1364      SEL linkSelector =
1365          (viewMode_ == profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER) ?
1366          @selector(showAccountManagement:) : @selector(hideAccountManagement:);
1367      link = [self linkButtonWithTitle:linkTitle
1368                           frameOrigin:rect.origin
1369                                action:linkSelector];
1370    } else {
1371      link = [self linkButtonWithTitle:base::SysUTF16ToNSString(item.sync_state)
1372                           frameOrigin:rect.origin
1373                                action:nil];
1374      [[link cell] setTextColor:[NSColor blackColor]];
1375    }
1376    // -linkButtonWithTitle sizeToFit's the link, so re-stretch it so that it
1377    // can be centered correctly in the view.
1378    rect.size.height = NSMaxY([link frame]);
1379    [link setFrame:rect];
1380    [link setAlignment:NSCenterTextAlignment];
1381  } else {
1382    rect.size.height = kBlueButtonHeight;
1383    link = [[BlueLabelButton alloc] initWithFrame:rect];
1384
1385    // Manually elide the button text so that the contents fit inside the bubble
1386    // This is needed because the BlueLabelButton cell resets the style on
1387    // every call to -cellSize, which prevents setting a custom lineBreakMode.
1388    NSString* elidedButtonText = base::SysUTF16ToNSString(gfx::ElideText(
1389        l10n_util::GetStringFUTF16(
1390            IDS_SYNC_START_SYNC_BUTTON_LABEL,
1391            l10n_util::GetStringUTF16(IDS_SHORT_PRODUCT_NAME)),
1392        gfx::FontList(), rect.size.width, gfx::ELIDE_TAIL));
1393
1394    [link setTitle:elidedButtonText];
1395    [link setTarget:self];
1396    [link setAction:switches::IsNewProfileManagement() ?
1397        @selector(showInlineSigninPage:) : @selector(showTabbedSigninPage:)];
1398  }
1399
1400  [container addSubview:link];
1401  [container setFrameSize:rect.size];
1402  return container.autorelease();
1403}
1404
1405- (NSView*)createSupervisedUserDisclaimerView {
1406  base::scoped_nsobject<NSView> container(
1407      [[NSView alloc] initWithFrame:NSZeroRect]);
1408
1409  int yOffset = 0;
1410  int availableTextWidth = kFixedMenuWidth - 2 * kHorizontalSpacing;
1411
1412  NSTextField* disclaimer = BuildLabel(
1413      base::SysUTF16ToNSString(avatarMenu_->GetSupervisedUserInformation()),
1414      NSMakePoint(kHorizontalSpacing, yOffset),
1415      nil /* background_color */,
1416      nil /* text_color */);
1417  [disclaimer setFrameSize:NSMakeSize(availableTextWidth, 0)];
1418  [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:disclaimer];
1419  yOffset = NSMaxY([disclaimer frame]);
1420
1421  [container addSubview:disclaimer];
1422  [container setFrameSize:NSMakeSize(kFixedMenuWidth, yOffset)];
1423  return container.autorelease();
1424}
1425
1426- (NSView*)createGuestProfileView {
1427  gfx::Image guestIcon =
1428      ui::ResourceBundle::GetSharedInstance().GetNativeImageNamed(
1429          profiles::GetPlaceholderAvatarIconResourceID());
1430  AvatarMenu::Item guestItem(std::string::npos, /* menu_index, not used */
1431                             std::string::npos, /* profile_index, not used */
1432                             guestIcon);
1433  guestItem.active = true;
1434  guestItem.name = base::SysNSStringToUTF16(
1435      l10n_util::GetNSString(IDS_PROFILES_GUEST_PROFILE_NAME));
1436
1437  return [self createCurrentProfileView:guestItem];
1438}
1439
1440- (NSButton*)createOtherProfileView:(int)itemIndex {
1441  const AvatarMenu::Item& item = avatarMenu_->GetItemAt(itemIndex);
1442
1443  NSRect rect = NSMakeRect(0, 0, kFixedMenuWidth,
1444                           kBlueButtonHeight + kSmallVerticalSpacing);
1445  base::scoped_nsobject<BackgroundColorHoverButton> profileButton(
1446      [[BackgroundColorHoverButton alloc]
1447          initWithFrame:rect
1448      imageTitleSpacing:kImageTitleSpacing
1449        backgroundColor:GetDialogBackgroundColor()]);
1450  [profileButton setTitle:base::SysUTF16ToNSString(item.name)];
1451  [profileButton setDefaultImage:CreateProfileImage(
1452      item.icon, kSmallImageSide).ToNSImage()];
1453  [profileButton setImagePosition:NSImageLeft];
1454  [profileButton setAlignment:NSLeftTextAlignment];
1455  [profileButton setBordered:NO];
1456  [profileButton setTag:itemIndex];
1457  [profileButton setTarget:self];
1458  [profileButton setAction:@selector(switchToProfile:)];
1459
1460  return profileButton.autorelease();
1461}
1462
1463- (NSView*)createOptionsViewWithRect:(NSRect)rect
1464                          enableLock:(BOOL)enableLock {
1465  int widthOfLockButton = enableLock ? 2 * kHorizontalSpacing + 14 : 0;
1466  NSRect viewRect = NSMakeRect(0, 0,
1467                               rect.size.width - widthOfLockButton,
1468                               kBlueButtonHeight + kVerticalSpacing);
1469  NSString* text = isGuestSession_ ?
1470      l10n_util::GetNSString(IDS_PROFILES_EXIT_GUEST) :
1471      l10n_util::GetNSStringF(IDS_PROFILES_NOT_YOU_BUTTON,
1472          profiles::GetAvatarNameForProfile(browser_->profile()));
1473  NSButton* notYouButton =
1474      [self hoverButtonWithRect:viewRect
1475                           text:text
1476                imageResourceId:IDR_ICON_PROFILES_MENU_AVATAR
1477       alternateImageResourceId:IDR_ICON_PROFILES_MENU_AVATAR
1478                         action:isGuestSession_? @selector(exitGuest:) :
1479                                                 @selector(showUserManager:)];
1480
1481  rect.size.height = NSMaxY([notYouButton frame]);
1482  base::scoped_nsobject<NSView> container([[NSView alloc] initWithFrame:rect]);
1483  [container addSubview:notYouButton];
1484
1485  if (enableLock) {
1486    viewRect.origin.x = NSMaxX([notYouButton frame]);
1487    NSBox* separator = [self verticalSeparatorWithFrame:viewRect];
1488    [container addSubview:separator];
1489
1490    viewRect.origin.x = NSMaxX([separator frame]);
1491    viewRect.size.width = widthOfLockButton;
1492    NSButton* lockButton =
1493        [self hoverButtonWithRect:viewRect
1494                             text:@""
1495                  imageResourceId:IDR_ICON_PROFILES_MENU_LOCK
1496         alternateImageResourceId:IDR_ICON_PROFILES_MENU_LOCK
1497                           action:@selector(lockProfile:)];
1498    [container addSubview:lockButton];
1499  }
1500
1501  return container.autorelease();
1502}
1503
1504- (NSView*)createCurrentProfileAccountsView:(NSRect)rect {
1505  const CGFloat kAccountButtonHeight = 34;
1506
1507  const AvatarMenu::Item& item =
1508      avatarMenu_->GetItemAt(avatarMenu_->GetActiveProfileIndex());
1509  DCHECK(item.signed_in);
1510
1511  NSColor* backgroundColor = gfx::SkColorToCalibratedNSColor(
1512      profiles::kAvatarBubbleAccountsBackgroundColor);
1513  base::scoped_nsobject<NSView> container([[BackgroundColorView alloc]
1514      initWithFrame:rect
1515          withColor:backgroundColor]);
1516
1517  // Manually elide the button text so that the contents fit inside the bubble.
1518  // This is needed because the BlueLabelButton cell resets the style on
1519  // every call to -cellSize, which prevents setting a custom lineBreakMode.
1520  NSString* elidedButtonText = base::SysUTF16ToNSString(gfx::ElideText(
1521      l10n_util::GetStringFUTF16(
1522          IDS_PROFILES_PROFILE_ADD_ACCOUNT_BUTTON, item.name),
1523      gfx::FontList(), rect.size.width, gfx::ELIDE_TAIL));
1524
1525  NSButton* addAccountsButton =
1526      [self linkButtonWithTitle:elidedButtonText
1527                    frameOrigin:NSMakePoint(
1528          kHorizontalSpacing, kSmallVerticalSpacing)
1529                         action:@selector(addAccount:)];
1530  [container addSubview:addAccountsButton];
1531
1532  NSView* accountEmails = [self createAccountsListWithRect:NSMakeRect(
1533      0, kAccountButtonHeight, rect.size.width, kAccountButtonHeight)];
1534  [container addSubview:accountEmails];
1535
1536  [container setFrameSize:NSMakeSize(rect.size.width,
1537                                     NSMaxY([accountEmails frame]))];
1538  return container.autorelease();
1539}
1540
1541- (NSView*)createAccountsListWithRect:(NSRect)rect {
1542  base::scoped_nsobject<NSView> container([[NSView alloc] initWithFrame:rect]);
1543  currentProfileAccounts_.clear();
1544
1545  Profile* profile = browser_->profile();
1546  std::string primaryAccount =
1547      SigninManagerFactory::GetForProfile(profile)->GetAuthenticatedUsername();
1548  DCHECK(!primaryAccount.empty());
1549  std::vector<std::string>accounts =
1550      profiles::GetSecondaryAccountsForProfile(profile, primaryAccount);
1551
1552  // If there is an account with an authentication error, it needs to be
1553  // badged with a warning icon.
1554  const SigninErrorController* errorController =
1555      profiles::GetSigninErrorController(profile);
1556  std::string errorAccountId =
1557      errorController ? errorController->error_account_id() : std::string();
1558
1559  rect.origin.y = 0;
1560  for (size_t i = 0; i < accounts.size(); ++i) {
1561    // Save the original email address, as the button text could be elided.
1562    currentProfileAccounts_[i] = accounts[i];
1563    NSButton* accountButton =
1564        [self accountButtonWithRect:rect
1565                              title:accounts[i]
1566                                tag:i
1567                     reauthRequired:errorAccountId == accounts[i]];
1568    [container addSubview:accountButton];
1569    rect.origin.y = NSMaxY([accountButton frame]);
1570  }
1571
1572  // The primary account should always be listed first.
1573  NSButton* accountButton =
1574      [self accountButtonWithRect:rect
1575                            title:primaryAccount
1576                              tag:kPrimaryProfileTag
1577                   reauthRequired:errorAccountId == primaryAccount];
1578  [container addSubview:accountButton];
1579  [container setFrameSize:NSMakeSize(NSWidth([container frame]),
1580                                     NSMaxY([accountButton frame]))];
1581  return container.autorelease();
1582}
1583
1584- (NSView*)buildGaiaEmbeddedView {
1585  base::scoped_nsobject<NSView> container(
1586      [[NSView alloc] initWithFrame:NSZeroRect]);
1587  CGFloat yOffset = 0;
1588
1589  GURL url;
1590  int messageId = -1;
1591  SigninErrorController* errorController = NULL;
1592  switch (viewMode_) {
1593    case profiles::BUBBLE_VIEW_MODE_GAIA_SIGNIN:
1594      url = signin::GetPromoURL(signin::SOURCE_AVATAR_BUBBLE_SIGN_IN,
1595                                false /* auto_close */,
1596                                true /* is_constrained */);
1597      messageId = IDS_PROFILES_GAIA_SIGNIN_TITLE;
1598      break;
1599    case profiles::BUBBLE_VIEW_MODE_GAIA_ADD_ACCOUNT:
1600      url = signin::GetPromoURL(signin::SOURCE_AVATAR_BUBBLE_ADD_ACCOUNT,
1601                                false /* auto_close */,
1602                                true /* is_constrained */);
1603      messageId = IDS_PROFILES_GAIA_ADD_ACCOUNT_TITLE;
1604      break;
1605    case profiles::BUBBLE_VIEW_MODE_GAIA_REAUTH:
1606      DCHECK(HasAuthError(browser_->profile()));
1607      errorController = profiles::GetSigninErrorController(browser_->profile());
1608      url = signin::GetReauthURL(
1609          browser_->profile(),
1610          errorController ? errorController->error_username() : std::string());
1611      messageId = IDS_PROFILES_GAIA_REAUTH_TITLE;
1612      break;
1613    default:
1614      NOTREACHED() << "Called with invalid mode=" << viewMode_;
1615      break;
1616  }
1617
1618  webContents_.reset(content::WebContents::Create(
1619      content::WebContents::CreateParams(browser_->profile())));
1620  webContents_->GetController().LoadURL(url,
1621                                        content::Referrer(),
1622                                        content::PAGE_TRANSITION_AUTO_TOPLEVEL,
1623                                        std::string());
1624  NSView* webview = webContents_->GetNativeView();
1625  [webview setFrameSize:NSMakeSize(kFixedGaiaViewWidth, kFixedGaiaViewHeight)];
1626  [container addSubview:webview];
1627  yOffset = NSMaxY([webview frame]);
1628
1629  // Adds the title card.
1630  NSBox* separator = [self horizontalSeparatorWithFrame:
1631      NSMakeRect(0, yOffset, kFixedGaiaViewWidth, 0)];
1632  [container addSubview:separator];
1633  yOffset = NSMaxY([separator frame]) + kSmallVerticalSpacing;
1634
1635  NSView* titleView = BuildTitleCard(
1636      NSMakeRect(0, yOffset, kFixedGaiaViewWidth, 0),
1637      messageId,
1638      self /* backButtonTarget*/,
1639      @selector(navigateBackFromSigninPage:) /* backButtonAction */);
1640  [container addSubview:titleView];
1641  yOffset = NSMaxY([titleView frame]);
1642
1643  [container setFrameSize:NSMakeSize(kFixedGaiaViewWidth, yOffset)];
1644  return container.autorelease();
1645}
1646
1647- (NSView*)buildAccountRemovalView {
1648  DCHECK(!accountIdToRemove_.empty());
1649
1650  base::scoped_nsobject<NSView> container(
1651      [[NSView alloc] initWithFrame:NSZeroRect]);
1652  CGFloat availableWidth =
1653      kFixedAccountRemovalViewWidth - 2 * kHorizontalSpacing;
1654  CGFloat yOffset = kVerticalSpacing;
1655
1656  const std::string& primaryAccount = SigninManagerFactory::GetForProfile(
1657      browser_->profile())->GetAuthenticatedUsername();
1658  bool isPrimaryAccount = primaryAccount == accountIdToRemove_;
1659
1660  // Adds "remove account" button at the bottom if needed.
1661  if (!isPrimaryAccount) {
1662    base::scoped_nsobject<NSButton> removeAccountButton(
1663        [[BlueLabelButton alloc] initWithFrame:NSZeroRect]);
1664    [removeAccountButton setTitle:l10n_util::GetNSString(
1665        IDS_PROFILES_ACCOUNT_REMOVAL_BUTTON)];
1666    [removeAccountButton setTarget:self];
1667    [removeAccountButton setAction:@selector(removeAccount:)];
1668    [removeAccountButton sizeToFit];
1669    [removeAccountButton setAlignment:NSCenterTextAlignment];
1670    CGFloat xOffset = (kFixedAccountRemovalViewWidth -
1671        NSWidth([removeAccountButton frame])) / 2;
1672    [removeAccountButton setFrameOrigin:NSMakePoint(xOffset, yOffset)];
1673    [container addSubview:removeAccountButton];
1674
1675    yOffset = NSMaxY([removeAccountButton frame]) + kVerticalSpacing;
1676  }
1677
1678  NSView* contentView;
1679  NSPoint contentFrameOrigin = NSMakePoint(kHorizontalSpacing, yOffset);
1680  if (isPrimaryAccount) {
1681    std::vector<size_t> offsets;
1682    NSString* contentStr = l10n_util::GetNSStringF(
1683        IDS_PROFILES_PRIMARY_ACCOUNT_REMOVAL_TEXT,
1684        base::UTF8ToUTF16(accountIdToRemove_), base::string16(), &offsets);
1685    NSString* linkStr = l10n_util::GetNSString(IDS_PROFILES_SETTINGS_LINK);
1686    contentView = BuildFixedWidthTextViewWithLink(self, contentStr, linkStr,
1687        offsets[1], contentFrameOrigin, availableWidth);
1688  } else {
1689    NSString* contentStr =
1690        l10n_util::GetNSString(IDS_PROFILES_ACCOUNT_REMOVAL_TEXT);
1691    NSTextField* contentLabel = BuildLabel(contentStr, contentFrameOrigin,
1692        GetDialogBackgroundColor(), nil /* text_color */);
1693    [contentLabel setFrameSize:NSMakeSize(availableWidth, 0)];
1694    [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:contentLabel];
1695    contentView = contentLabel;
1696  }
1697  [container addSubview:contentView];
1698  yOffset = NSMaxY([contentView frame]) + kVerticalSpacing;
1699
1700  // Adds the title card.
1701  NSBox* separator = [self horizontalSeparatorWithFrame:
1702      NSMakeRect(0, yOffset, kFixedAccountRemovalViewWidth, 0)];
1703  [container addSubview:separator];
1704  yOffset = NSMaxY([separator frame]) + kSmallVerticalSpacing;
1705
1706  NSView* titleView = BuildTitleCard(
1707      NSMakeRect(0, yOffset, kFixedAccountRemovalViewWidth,0),
1708      IDS_PROFILES_ACCOUNT_REMOVAL_TITLE,
1709      self /* backButtonTarget*/,
1710      @selector(showAccountManagement:) /* backButtonAction */);
1711  [container addSubview:titleView];
1712  yOffset = NSMaxY([titleView frame]);
1713
1714  [container setFrameSize:NSMakeSize(kFixedAccountRemovalViewWidth, yOffset)];
1715  return container.autorelease();
1716}
1717
1718- (NSView*)buildEndPreviewView {
1719  base::scoped_nsobject<NSView> container(
1720      [[NSView alloc] initWithFrame:NSZeroRect]);
1721  CGFloat availableWidth =
1722      kFixedEndPreviewViewWidth - 2 * kHorizontalSpacing;
1723  CGFloat yOffset = kVerticalSpacing;
1724
1725  // Adds the "end preview and relaunch" button at the bottom.
1726  base::scoped_nsobject<NSButton> endPreviewAndRelaunchButton(
1727      [[BlueLabelButton alloc] initWithFrame:NSZeroRect]);
1728  [endPreviewAndRelaunchButton setTitle:l10n_util::GetNSString(
1729      IDS_PROFILES_END_PREVIEW_AND_RELAUNCH)];
1730  [endPreviewAndRelaunchButton setTarget:self];
1731  [endPreviewAndRelaunchButton setAction:@selector(endPreviewAndRelaunch:)];
1732  [endPreviewAndRelaunchButton sizeToFit];
1733  [endPreviewAndRelaunchButton setAlignment:NSCenterTextAlignment];
1734  CGFloat xOffset = (kFixedEndPreviewViewWidth -
1735      NSWidth([endPreviewAndRelaunchButton frame])) / 2;
1736  [endPreviewAndRelaunchButton setFrameOrigin:NSMakePoint(xOffset, yOffset)];
1737  [container addSubview:endPreviewAndRelaunchButton];
1738  yOffset = NSMaxY([endPreviewAndRelaunchButton frame]) + kVerticalSpacing;
1739
1740  // Adds the main text label.
1741  NSPoint contentFrameOrigin = NSMakePoint(kHorizontalSpacing, yOffset);
1742  NSString* contentStr =
1743      l10n_util::GetNSString(IDS_PROFILES_END_PREVIEW_TEXT);
1744  NSTextField* contentLabel = BuildLabel(contentStr, contentFrameOrigin,
1745      GetDialogBackgroundColor(), nil /* text_color */);
1746  [contentLabel setFrameSize:NSMakeSize(availableWidth, 0)];
1747  [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:contentLabel];
1748  [container addSubview:contentLabel];
1749  yOffset = NSMaxY([contentLabel frame]) + kVerticalSpacing;
1750
1751  // Adds the title card.
1752  NSBox* separator = [self horizontalSeparatorWithFrame:
1753      NSMakeRect(0, yOffset, kFixedEndPreviewViewWidth, 0)];
1754  [container addSubview:separator];
1755  yOffset = NSMaxY([separator frame]) + kSmallVerticalSpacing;
1756
1757  NSView* titleView = BuildTitleCard(
1758      NSMakeRect(0, yOffset, kFixedEndPreviewViewWidth, 0),
1759      IDS_PROFILES_END_PREVIEW,
1760      self /* backButtonTarget*/,
1761      @selector(showSendFeedbackTutorial:) /* backButtonAction */);
1762  [container addSubview:titleView];
1763  yOffset = NSMaxY([titleView frame]);
1764
1765  [container setFrameSize:NSMakeSize(kFixedEndPreviewViewWidth, yOffset)];
1766  return container.autorelease();
1767}
1768
1769// Called when clicked on the settings link.
1770- (BOOL)textView:(NSTextView*)textView
1771   clickedOnLink:(id)link
1772         atIndex:(NSUInteger)charIndex {
1773  chrome::ShowSettings(browser_);
1774  return YES;
1775}
1776
1777- (NSButton*)hoverButtonWithRect:(NSRect)rect
1778                            text:(NSString*)text
1779                 imageResourceId:(int)imageResourceId
1780        alternateImageResourceId:(int)alternateImageResourceId
1781                          action:(SEL)action {
1782  base::scoped_nsobject<BackgroundColorHoverButton> button(
1783      [[BackgroundColorHoverButton alloc]
1784          initWithFrame:rect
1785      imageTitleSpacing:kImageTitleSpacing
1786        backgroundColor:GetDialogBackgroundColor()]);
1787
1788  [button setTitle:text];
1789  ui::ResourceBundle* rb = &ui::ResourceBundle::GetSharedInstance();
1790  NSImage* alternateImage = rb->GetNativeImageNamed(
1791      alternateImageResourceId).ToNSImage();
1792  [button setDefaultImage:rb->GetNativeImageNamed(imageResourceId).ToNSImage()];
1793  [button setHoverImage:alternateImage];
1794  [button setPressedImage:alternateImage];
1795  [button setImagePosition:NSImageLeft];
1796  [button setAlignment:NSLeftTextAlignment];
1797  [button setBordered:NO];
1798  [button setTarget:self];
1799  [button setAction:action];
1800
1801  return button.autorelease();
1802}
1803
1804- (NSButton*)linkButtonWithTitle:(NSString*)title
1805                     frameOrigin:(NSPoint)frameOrigin
1806                          action:(SEL)action {
1807  base::scoped_nsobject<NSButton> link(
1808      [[HyperlinkButtonCell buttonWithString:title] retain]);
1809
1810  [[link cell] setShouldUnderline:NO];
1811  [[link cell] setTextColor:gfx::SkColorToCalibratedNSColor(
1812      chrome_style::GetLinkColor())];
1813  [link setTitle:title];
1814  [link setBordered:NO];
1815  [link setFont:[NSFont labelFontOfSize:kTextFontSize]];
1816  [link setTarget:self];
1817  [link setAction:action];
1818  [link setFrameOrigin:frameOrigin];
1819  [link sizeToFit];
1820
1821  return link.autorelease();
1822}
1823
1824- (NSButton*)accountButtonWithRect:(NSRect)rect
1825                             title:(const std::string&)title
1826                               tag:(int)tag
1827                    reauthRequired:(BOOL)reauthRequired {
1828  ui::ResourceBundle* rb = &ui::ResourceBundle::GetSharedInstance();
1829  NSImage* deleteImage = rb->GetNativeImageNamed(IDR_CLOSE_1).ToNSImage();
1830  CGFloat deleteImageWidth = [deleteImage size].width;
1831  NSImage* warningImage = reauthRequired ? rb->GetNativeImageNamed(
1832      IDR_ICON_PROFILES_ACCOUNT_BUTTON_ERROR).ToNSImage() : nil;
1833  CGFloat warningImageWidth = [warningImage size].width;
1834
1835  CGFloat availableTextWidth = rect.size.width - kHorizontalSpacing -
1836      warningImageWidth - deleteImageWidth;
1837  if (warningImage)
1838    availableTextWidth -= kHorizontalSpacing;
1839
1840  NSColor* backgroundColor = gfx::SkColorToCalibratedNSColor(
1841      profiles::kAvatarBubbleAccountsBackgroundColor);
1842  base::scoped_nsobject<BackgroundColorHoverButton> button(
1843      [[BackgroundColorHoverButton alloc] initWithFrame:rect
1844                                      imageTitleSpacing:0
1845                                        backgroundColor:backgroundColor]);
1846  [button setTitle:ElideEmail(title, availableTextWidth)];
1847  [button setAlignment:NSLeftTextAlignment];
1848  [button setBordered:NO];
1849  if (reauthRequired) {
1850    [button setDefaultImage:warningImage];
1851    [button setImagePosition:NSImageLeft];
1852    [button setTarget:self];
1853    [button setAction:@selector(showAccountReauthenticationView:)];
1854    [button setTag:tag];
1855  }
1856
1857  // Delete button.
1858  NSRect buttonRect;
1859  NSDivideRect(rect, &buttonRect, &rect,
1860      deleteImageWidth + kHorizontalSpacing, NSMaxXEdge);
1861  buttonRect.origin.y = 0;
1862
1863  base::scoped_nsobject<HoverImageButton> deleteButton(
1864      [[HoverImageButton alloc] initWithFrame:buttonRect]);
1865  [deleteButton setBordered:NO];
1866  [deleteButton setDefaultImage:deleteImage];
1867  [deleteButton setHoverImage:rb->GetNativeImageNamed(
1868      IDR_CLOSE_1_H).ToNSImage()];
1869  [deleteButton setPressedImage:rb->GetNativeImageNamed(
1870      IDR_CLOSE_1_P).ToNSImage()];
1871  [deleteButton setTarget:self];
1872  [deleteButton setAction:@selector(showAccountRemovalView:)];
1873  [deleteButton setTag:tag];
1874
1875  [button addSubview:deleteButton];
1876
1877  return button.autorelease();
1878}
1879
1880- (void)postActionPerformed:(ProfileMetrics::ProfileDesktopMenu)action {
1881  ProfileMetrics::LogProfileDesktopMenu(action, serviceType_);
1882  serviceType_ = signin::GAIA_SERVICE_TYPE_NONE;
1883}
1884
1885@end
1886