Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / chrome / browser / ui / cocoa / profiles / profile_chooser_controller.mm
blob71210374cb6908bcc5492279daabca375b54e6e3
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.
5 #import "chrome/browser/ui/cocoa/profiles/profile_chooser_controller.h"
7 #import <Carbon/Carbon.h>  // kVK_Return.
8 #import <Cocoa/Cocoa.h>
10 #include "base/mac/bundle_locations.h"
11 #include "base/prefs/pref_service.h"
12 #include "base/strings/string_util.h"
13 #include "base/strings/sys_string_conversions.h"
14 #include "base/strings/utf_string_conversions.h"
15 #include "chrome/app/chrome_command_ids.h"
16 #include "chrome/browser/browser_process.h"
17 #include "chrome/browser/chrome_notification_types.h"
18 #include "chrome/browser/lifetime/application_lifetime.h"
19 #include "chrome/browser/prefs/incognito_mode_prefs.h"
20 #include "chrome/browser/profiles/avatar_menu.h"
21 #include "chrome/browser/profiles/avatar_menu_observer.h"
22 #include "chrome/browser/profiles/profile_avatar_icon_util.h"
23 #include "chrome/browser/profiles/profile_info_cache.h"
24 #include "chrome/browser/profiles/profile_manager.h"
25 #include "chrome/browser/profiles/profile_metrics.h"
26 #include "chrome/browser/profiles/profile_window.h"
27 #include "chrome/browser/profiles/profiles_state.h"
28 #include "chrome/browser/signin/chrome_signin_helper.h"
29 #include "chrome/browser/signin/profile_oauth2_token_service_factory.h"
30 #include "chrome/browser/signin/signin_error_controller_factory.h"
31 #include "chrome/browser/signin/signin_manager_factory.h"
32 #include "chrome/browser/signin/signin_promo.h"
33 #include "chrome/browser/signin/signin_ui_util.h"
34 #include "chrome/browser/ui/browser.h"
35 #include "chrome/browser/ui/browser_commands.h"
36 #include "chrome/browser/ui/browser_window.h"
37 #include "chrome/browser/ui/chrome_pages.h"
38 #include "chrome/browser/ui/chrome_style.h"
39 #import "chrome/browser/ui/cocoa/browser_window_utils.h"
40 #import "chrome/browser/ui/cocoa/info_bubble_view.h"
41 #import "chrome/browser/ui/cocoa/info_bubble_window.h"
42 #import "chrome/browser/ui/cocoa/profiles/user_manager_mac.h"
43 #include "chrome/browser/ui/singleton_tabs.h"
44 #include "chrome/browser/ui/user_manager.h"
45 #include "chrome/browser/ui/webui/signin/login_ui_service.h"
46 #include "chrome/browser/ui/webui/signin/login_ui_service_factory.h"
47 #include "chrome/common/pref_names.h"
48 #include "chrome/common/url_constants.h"
49 #include "chrome/grit/chromium_strings.h"
50 #include "chrome/grit/generated_resources.h"
51 #include "components/signin/core/browser/profile_oauth2_token_service.h"
52 #include "components/signin/core/browser/signin_manager.h"
53 #include "components/signin/core/common/profile_management_switches.h"
54 #include "content/public/browser/native_web_keyboard_event.h"
55 #include "content/public/browser/notification_service.h"
56 #include "content/public/browser/render_widget_host_view.h"
57 #include "content/public/browser/web_contents.h"
58 #include "google_apis/gaia/oauth2_token_service.h"
59 #include "grit/theme_resources.h"
60 #include "skia/ext/skia_utils_mac.h"
61 #import "third_party/google_toolbox_for_mac/src/AppKit/GTMUILocalizerAndLayoutTweaker.h"
62 #import "ui/base/cocoa/cocoa_base_utils.h"
63 #import "ui/base/cocoa/controls/blue_label_button.h"
64 #import "ui/base/cocoa/controls/hyperlink_button_cell.h"
65 #import "ui/base/cocoa/controls/hyperlink_text_view.h"
66 #import "ui/base/cocoa/hover_image_button.h"
67 #include "ui/base/cocoa/window_size_constants.h"
68 #include "ui/base/l10n/l10n_util.h"
69 #include "ui/base/l10n/l10n_util_mac.h"
70 #include "ui/base/resource/resource_bundle.h"
71 #include "ui/gfx/image/image.h"
72 #include "ui/gfx/text_elider.h"
73 #include "ui/native_theme/common_theme.h"
74 #include "ui/native_theme/native_theme.h"
76 namespace {
78 // Constants taken from the Windows/Views implementation at:
79 // chrome/browser/ui/views/profile_chooser_view.cc
80 const int kLargeImageSide = 88;
81 const int kSmallImageSide = 32;
82 const CGFloat kFixedMenuWidth = 250;
84 const CGFloat kVerticalSpacing = 16.0;
85 const CGFloat kSmallVerticalSpacing = 10.0;
86 const CGFloat kHorizontalSpacing = 16.0;
87 const CGFloat kTitleFontSize = 15.0;
88 const CGFloat kTextFontSize = 12.0;
89 const CGFloat kProfileButtonHeight = 30;
90 const int kBezelThickness = 3;  // Width of the bezel on an NSButton.
91 const int kImageTitleSpacing = 10;
92 const int kBlueButtonHeight = 30;
93 const CGFloat kFocusRingLineWidth = 2;
95 // Fixed size for embedded sign in pages as defined in Gaia.
96 const CGFloat kFixedGaiaViewWidth = 360;
97 const CGFloat kFixedGaiaViewHeight = 440;
99 // Fixed size for the account removal view.
100 const CGFloat kFixedAccountRemovalViewWidth = 280;
102 // Fixed size for the switch user view.
103 const int kFixedSwitchUserViewWidth = 320;
105 // The tag number for the primary account.
106 const int kPrimaryProfileTag = -1;
108 gfx::Image CreateProfileImage(const gfx::Image& icon, int imageSize) {
109   return profiles::GetSizedAvatarIcon(
110       icon, true /* image is a square */, imageSize, imageSize);
113 // Updates the window size and position.
114 void SetWindowSize(NSWindow* window, NSSize size) {
115   NSRect frame = [window frame];
116   frame.origin.x += frame.size.width - size.width;
117   frame.origin.y += frame.size.height - size.height;
118   frame.size = size;
119   [window setFrame:frame display:YES];
122 NSString* ElideEmail(const std::string& email, CGFloat width) {
123   const base::string16 elidedEmail = gfx::ElideText(
124       base::UTF8ToUTF16(email), gfx::FontList(), width, gfx::ELIDE_EMAIL);
125   return base::SysUTF16ToNSString(elidedEmail);
128 NSString* ElideMessage(const base::string16& message, CGFloat width) {
129   return base::SysUTF16ToNSString(
130       gfx::ElideText(message, gfx::FontList(), width, gfx::ELIDE_TAIL));
133 // Builds a label with the given |title| anchored at |frame_origin|. Sets the
134 // text color to |text_color| if not null.
135 NSTextField* BuildLabel(NSString* title,
136                         NSPoint frame_origin,
137                         NSColor* text_color) {
138   base::scoped_nsobject<NSTextField> label(
139       [[NSTextField alloc] initWithFrame:NSZeroRect]);
140   [label setStringValue:title];
141   [label setEditable:NO];
142   [label setAlignment:NSLeftTextAlignment];
143   [label setBezeled:NO];
144   [label setFont:[NSFont labelFontOfSize:kTextFontSize]];
145   [label setDrawsBackground:NO];
146   [label setFrameOrigin:frame_origin];
147   [label sizeToFit];
149   if (text_color)
150     [[label cell] setTextColor:text_color];
152   return label.autorelease();
155 // Builds an NSTextView that has the contents set to the specified |message|,
156 // with a non-underlined |link| inserted at |link_offset|. The view is anchored
157 // at the specified |frame_origin| and has a fixed |frame_width|.
158 NSTextView* BuildFixedWidthTextViewWithLink(
159     id<NSTextViewDelegate> delegate,
160     NSString* message,
161     NSString* link,
162     int link_offset,
163     NSPoint frame_origin,
164     CGFloat frame_width) {
165   base::scoped_nsobject<HyperlinkTextView> text_view(
166       [[HyperlinkTextView alloc] initWithFrame:NSZeroRect]);
167   NSColor* link_color = gfx::SkColorToCalibratedNSColor(
168       chrome_style::GetLinkColor());
169   NSMutableString* finalMessage =
170       [NSMutableString stringWithFormat:@"%@\n", message];
171   [finalMessage insertString:link atIndex:link_offset];
172   // Adds a padding row at the bottom, because |boundingRectWithSize| below cuts
173   // off the last row sometimes.
174   [text_view setMessage:finalMessage
175                withFont:[NSFont labelFontOfSize:kTextFontSize]
176            messageColor:[NSColor blackColor]];
177   [text_view addLinkRange:NSMakeRange(link_offset, [link length])
178                  withName:@""
179                 linkColor:link_color];
181   // Removes the underlining from the link.
182   [text_view setLinkTextAttributes:nil];
183   NSTextStorage* text_storage = [text_view textStorage];
184   NSRange link_range = NSMakeRange(link_offset, [link length]);
185   [text_storage addAttribute:NSUnderlineStyleAttributeName
186                        value:[NSNumber numberWithInt:NSUnderlineStyleNone]
187                        range:link_range];
189   NSRect frame = [[text_view attributedString]
190       boundingRectWithSize:NSMakeSize(frame_width, 0)
191                    options:NSStringDrawingUsesLineFragmentOrigin];
192   frame.origin = frame_origin;
193   [text_view setFrame:frame];
194   [text_view setDelegate:delegate];
195   return text_view.autorelease();
198 // Returns the native dialog background color.
199 NSColor* GetDialogBackgroundColor() {
200   return gfx::SkColorToCalibratedNSColor(
201       ui::NativeTheme::instance()->GetSystemColor(
202           ui::NativeTheme::kColorId_DialogBackground));
205 // Builds a title card with one back button right aligned and one label center
206 // aligned.
207 NSView* BuildTitleCard(NSRect frame_rect,
208                        const base::string16& message,
209                        id back_button_target,
210                        SEL back_button_action) {
211   base::scoped_nsobject<NSView> container(
212       [[NSView alloc] initWithFrame:frame_rect]);
214   base::scoped_nsobject<HoverImageButton> button(
215       [[HoverImageButton alloc] initWithFrame:frame_rect]);
216   [button setBordered:NO];
217   ui::ResourceBundle* rb = &ui::ResourceBundle::GetSharedInstance();
218   [button setDefaultImage:rb->GetNativeImageNamed(IDR_BACK).ToNSImage()];
219   [button setHoverImage:rb->GetNativeImageNamed(IDR_BACK_H).ToNSImage()];
220   [button setPressedImage:rb->GetNativeImageNamed(IDR_BACK_P).ToNSImage()];
221   [button setTarget:back_button_target];
222   [button setAction:back_button_action];
223   [button setFrameSize:NSMakeSize(kProfileButtonHeight, kProfileButtonHeight)];
224   [button setFrameOrigin:NSMakePoint(kHorizontalSpacing, 0)];
226   CGFloat max_label_width = frame_rect.size.width -
227       (kHorizontalSpacing * 2 + kProfileButtonHeight) * 2;
228   NSTextField* title_label = BuildLabel(
229       ElideMessage(message, max_label_width),
230       NSZeroPoint, nil);
231   [title_label setAlignment:NSCenterTextAlignment];
232   [title_label setFont:[NSFont labelFontOfSize:kTitleFontSize]];
233   [title_label sizeToFit];
234   CGFloat x_offset = (frame_rect.size.width - NSWidth([title_label frame])) / 2;
235   CGFloat y_offset =
236       (NSHeight([button frame]) - NSHeight([title_label frame])) / 2;
237   [title_label setFrameOrigin:NSMakePoint(x_offset, y_offset)];
239   [container addSubview:button];
240   [container addSubview:title_label];
241   CGFloat height = std::max(NSMaxY([title_label frame]),
242                             NSMaxY([button frame])) + kVerticalSpacing;
243   [container setFrameSize:NSMakeSize(NSWidth([container frame]), height)];
245   return container.autorelease();
248 bool HasAuthError(Profile* profile) {
249   const SigninErrorController* error_controller =
250       SigninErrorControllerFactory::GetForProfile(profile);
251   return error_controller && error_controller->HasError();
254 std::string GetAuthErrorAccountId(Profile* profile) {
255   const SigninErrorController* error_controller =
256       SigninErrorControllerFactory::GetForProfile(profile);
257   if (!error_controller)
258     return std::string();
260   return error_controller->error_account_id();
263 }  // namespace
265 // Custom WebContentsDelegate that allows handling of hotkeys and suppresses
266 // the context menu.
267 class GaiaWebContentsDelegate : public content::WebContentsDelegate {
268  public:
269   GaiaWebContentsDelegate() {}
270   ~GaiaWebContentsDelegate() override {}
272  private:
273   // content::WebContentsDelegate:
274   bool HandleContextMenu(const content::ContextMenuParams& params) override;
275   void HandleKeyboardEvent(
276       content::WebContents* source,
277       const content::NativeWebKeyboardEvent& event) override;
279   DISALLOW_COPY_AND_ASSIGN(GaiaWebContentsDelegate);
282 bool GaiaWebContentsDelegate::HandleContextMenu(
283     const content::ContextMenuParams& params) {
284   // Suppresses the context menu because some features, such as inspecting
285   // elements, are not appropriate in a bubble.
286   return true;
289 void GaiaWebContentsDelegate::HandleKeyboardEvent(
290     content::WebContents* source,
291     const content::NativeWebKeyboardEvent& event) {
292   if (![BrowserWindowUtils shouldHandleKeyboardEvent:event])
293     return;
295   int chrome_command_id = [BrowserWindowUtils getCommandId:event];
297   bool is_text_editing_command =
298       (event.modifiers & blink::WebInputEvent::MetaKey) &&
299       (event.windowsKeyCode == ui::VKEY_A ||
300        event.windowsKeyCode == ui::VKEY_V);
302   // TODO(guohui): maybe should add an accelerator for the back button.
303   if (chrome_command_id == IDC_CLOSE_WINDOW || chrome_command_id == IDC_EXIT ||
304       is_text_editing_command) {
305     [[NSApp mainMenu] performKeyEquivalent:event.os_event];
306   }
309 // Class that listens to changes to the OAuth2Tokens for the active profile,
310 // changes to the avatar menu model or browser close notifications.
311 class ActiveProfileObserverBridge : public AvatarMenuObserver,
312                                     public content::NotificationObserver,
313                                     public OAuth2TokenService::Observer {
314  public:
315   ActiveProfileObserverBridge(ProfileChooserController* controller,
316                               Browser* browser)
317       : controller_(controller),
318         browser_(browser),
319         token_observer_registered_(false) {
320     registrar_.Add(this, chrome::NOTIFICATION_BROWSER_CLOSING,
321                    content::NotificationService::AllSources());
322     if (!browser_->profile()->IsGuestSession())
323       AddTokenServiceObserver();
324   }
326   ~ActiveProfileObserverBridge() override { RemoveTokenServiceObserver(); }
328  private:
329   void AddTokenServiceObserver() {
330     ProfileOAuth2TokenService* oauth2_token_service =
331         ProfileOAuth2TokenServiceFactory::GetForProfile(browser_->profile());
332     DCHECK(oauth2_token_service);
333     oauth2_token_service->AddObserver(this);
334     token_observer_registered_ = true;
335   }
337   void RemoveTokenServiceObserver() {
338     if (!token_observer_registered_)
339       return;
340     DCHECK(browser_->profile());
341     ProfileOAuth2TokenService* oauth2_token_service =
342         ProfileOAuth2TokenServiceFactory::GetForProfile(browser_->profile());
343     DCHECK(oauth2_token_service);
344     oauth2_token_service->RemoveObserver(this);
345     token_observer_registered_ = false;
346   }
348   // OAuth2TokenService::Observer:
349   void OnRefreshTokenAvailable(const std::string& account_id) override {
350     // Tokens can only be added by adding an account through the inline flow,
351     // which is started from the account management view. Refresh it to show the
352     // update.
353     profiles::BubbleViewMode viewMode = [controller_ viewMode];
354     if (viewMode == profiles::BUBBLE_VIEW_MODE_ACCOUNT_MANAGEMENT ||
355         viewMode == profiles::BUBBLE_VIEW_MODE_GAIA_ADD_ACCOUNT ||
356         viewMode == profiles::BUBBLE_VIEW_MODE_GAIA_REAUTH) {
357       [controller_ initMenuContentsWithView:
358           switches::IsEnableAccountConsistency() ?
359               profiles::BUBBLE_VIEW_MODE_ACCOUNT_MANAGEMENT :
360               profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER];
361     }
362   }
364   void OnRefreshTokenRevoked(const std::string& account_id) override {
365     // Tokens can only be removed from the account management view. Refresh it
366     // to show the update.
367     if ([controller_ viewMode] == profiles::BUBBLE_VIEW_MODE_ACCOUNT_MANAGEMENT)
368       [controller_ initMenuContentsWithView:
369           profiles::BUBBLE_VIEW_MODE_ACCOUNT_MANAGEMENT];
370   }
372   // AvatarMenuObserver:
373   void OnAvatarMenuChanged(AvatarMenu* avatar_menu) override {
374     profiles::BubbleViewMode viewMode = [controller_ viewMode];
375     if (viewMode == profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER ||
376         viewMode == profiles::BUBBLE_VIEW_MODE_ACCOUNT_MANAGEMENT) {
377       [controller_ initMenuContentsWithView:viewMode];
378     }
379   }
381   // content::NotificationObserver:
382   void Observe(int type,
383                const content::NotificationSource& source,
384                const content::NotificationDetails& details) override {
385     DCHECK_EQ(chrome::NOTIFICATION_BROWSER_CLOSING, type);
386     if (browser_ == content::Source<Browser>(source).ptr()) {
387       RemoveTokenServiceObserver();
388       // Clean up the bubble's WebContents (used by the Gaia embedded view), to
389       // make sure the guest profile doesn't have any dangling host renderers.
390       // This can happen if Chrome is quit using Command-Q while the bubble is
391       // still open, which won't give the bubble a chance to be closed and
392       // clean up the WebContents itself.
393       [controller_ cleanUpEmbeddedViewContents];
394     }
395   }
397   ProfileChooserController* controller_;  // Weak; owns this.
398   Browser* browser_;  // Weak.
399   content::NotificationRegistrar registrar_;
401   // The observer can be removed both when closing the browser, and by just
402   // closing the avatar bubble. However, in the case of closing the browser,
403   // the avatar bubble will also be closed afterwards, resulting in a second
404   // attempt to remove the observer. This ensures the observer is only
405   // removed once.
406   bool token_observer_registered_;
408   DISALLOW_COPY_AND_ASSIGN(ActiveProfileObserverBridge);
411 // Custom button cell that adds a left padding before the button image, and
412 // a custom spacing between the button image and title.
413 @interface CustomPaddingImageButtonCell : NSButtonCell {
414  @private
415   // Padding added to the left margin of the button.
416   int leftMarginSpacing_;
417   // Spacing between the cell image and title.
418   int imageTitleSpacing_;
421 - (id)initWithLeftMarginSpacing:(int)leftMarginSpacing
422               imageTitleSpacing:(int)imageTitleSpacing;
423 @end
425 @implementation CustomPaddingImageButtonCell
426 - (id)initWithLeftMarginSpacing:(int)leftMarginSpacing
427               imageTitleSpacing:(int)imageTitleSpacing {
428   if ((self = [super init])) {
429     leftMarginSpacing_ = leftMarginSpacing;
430     imageTitleSpacing_ = imageTitleSpacing;
431   }
432   return self;
435 - (NSRect)drawTitle:(NSAttributedString*)title
436           withFrame:(NSRect)frame
437              inView:(NSView*)controlView {
438   NSRect marginRect;
439   NSDivideRect(frame, &marginRect, &frame, leftMarginSpacing_, NSMinXEdge);
441   // The title frame origin isn't aware of the left margin spacing added
442   // in -drawImage, so it must be added when drawing the title as well.
443   if ([self imagePosition] == NSImageLeft)
444     NSDivideRect(frame, &marginRect, &frame, imageTitleSpacing_, NSMinXEdge);
446   return [super drawTitle:title withFrame:frame inView:controlView];
449 - (void)drawImage:(NSImage*)image
450         withFrame:(NSRect)frame
451            inView:(NSView*)controlView {
452   if ([self imagePosition] == NSImageLeft)
453     frame.origin.x = leftMarginSpacing_;
454   [super drawImage:image withFrame:frame inView:controlView];
457 - (NSSize)cellSize {
458   NSSize buttonSize = [super cellSize];
459   buttonSize.width += leftMarginSpacing_;
460   if ([self imagePosition] == NSImageLeft)
461     buttonSize.width += imageTitleSpacing_;
462   return buttonSize;
465 - (NSFocusRingType)focusRingType {
466   // This is taken care of by the custom drawing code.
467   return NSFocusRingTypeNone;
470 - (void)drawWithFrame:(NSRect)frame inView:(NSView *)controlView {
471   [super drawInteriorWithFrame:frame inView:controlView];
473   // Focus ring.
474   if ([self showsFirstResponder]) {
475     NSRect focusRingRect =
476         NSInsetRect(frame, kFocusRingLineWidth, kFocusRingLineWidth);
477     // TODO(noms): When we are targetting 10.7, we should change this to use
478     // -drawFocusRingMaskWithFrame instead.
479     [[[NSColor keyboardFocusIndicatorColor] colorWithAlphaComponent:1] set];
480     NSBezierPath* path = [NSBezierPath bezierPathWithRect:focusRingRect];
481     [path setLineWidth:kFocusRingLineWidth];
482     [path stroke];
483   }
486 @end
488 // A custom image view that has a transparent backround.
489 @interface TransparentBackgroundImageView : NSImageView
490 @end
492 @implementation TransparentBackgroundImageView
493 - (void)drawRect:(NSRect)dirtyRect {
494   NSColor* backgroundColor = [NSColor colorWithCalibratedWhite:1 alpha:0.6f];
495   [backgroundColor setFill];
496   NSRectFillUsingOperation(dirtyRect, NSCompositeSourceAtop);
497   [super drawRect:dirtyRect];
499 @end
501 @interface CustomCircleImageCell : NSButtonCell
502 @end
504 @implementation CustomCircleImageCell
505 - (void)drawWithFrame:(NSRect)frame inView:(NSView *)controlView {
506   // Display everything as a circle that spans the entire control.
507   NSBezierPath* path = [NSBezierPath bezierPathWithOvalInRect:frame];
508   [path addClip];
509   [super drawImage:[self image] withFrame:frame inView:controlView];
511 @end
513 // A custom image control that shows a "Change" button when moused over.
514 @interface EditableProfilePhoto : HoverImageButton {
515  @private
516   AvatarMenu* avatarMenu_;  // Weak; Owned by ProfileChooserController.
517   base::scoped_nsobject<TransparentBackgroundImageView> changePhotoImage_;
518   ProfileChooserController* controller_;
521 - (id)initWithFrame:(NSRect)frameRect
522          avatarMenu:(AvatarMenu*)avatarMenu
523         profileIcon:(const gfx::Image&)profileIcon
524      editingAllowed:(BOOL)editingAllowed
525      withController:(ProfileChooserController*)controller;
527 // Called when the "Change" button is clicked.
528 - (void)editPhoto:(id)sender;
530 @end
532 @implementation EditableProfilePhoto
533 - (id)initWithFrame:(NSRect)frameRect
534          avatarMenu:(AvatarMenu*)avatarMenu
535         profileIcon:(const gfx::Image&)profileIcon
536      editingAllowed:(BOOL)editingAllowed
537      withController:(ProfileChooserController*)controller {
538   if ((self = [super initWithFrame:frameRect])) {
539     avatarMenu_ = avatarMenu;
540     controller_ = controller;
542     [self setBordered:NO];
544     base::scoped_nsobject<CustomCircleImageCell> cell(
545         [[CustomCircleImageCell alloc] init]);
546     [self setCell:cell.get()];
548     [self setDefaultImage:CreateProfileImage(
549         profileIcon, kLargeImageSide).ToNSImage()];
550     [self setImagePosition:NSImageOnly];
552     NSRect bounds = NSMakeRect(0, 0, kLargeImageSide, kLargeImageSide);
553     if (editingAllowed) {
554       [self setTarget:self];
555       [self setAction:@selector(editPhoto:)];
556       changePhotoImage_.reset([[TransparentBackgroundImageView alloc]
557           initWithFrame:bounds]);
558       [changePhotoImage_ setImage:ui::ResourceBundle::GetSharedInstance().
559           GetNativeImageNamed(IDR_ICON_PROFILES_EDIT_CAMERA).AsNSImage()];
560       [self addSubview:changePhotoImage_];
562       // Hide the image until the button is hovered over.
563       [changePhotoImage_ setHidden:YES];
564     }
566     // Set the image cell's accessibility strings to be the same as the
567     // button's strings.
568     [[self cell] accessibilitySetOverrideValue:l10n_util::GetNSString(
569         editingAllowed ?
570         IDS_PROFILES_NEW_AVATAR_MENU_CHANGE_PHOTO_ACCESSIBLE_NAME :
571         IDS_PROFILES_NEW_AVATAR_MENU_PHOTO_ACCESSIBLE_NAME)
572                                   forAttribute:NSAccessibilityTitleAttribute];
573     [[self cell] accessibilitySetOverrideValue:
574         editingAllowed ? NSAccessibilityButtonRole : NSAccessibilityImageRole
575                                   forAttribute:NSAccessibilityRoleAttribute];
576     [[self cell] accessibilitySetOverrideValue:
577         NSAccessibilityRoleDescription(NSAccessibilityButtonRole, nil)
578             forAttribute:NSAccessibilityRoleDescriptionAttribute];
580     // The button and the cell should read the same thing.
581     [self accessibilitySetOverrideValue:l10n_util::GetNSString(
582         editingAllowed ?
583         IDS_PROFILES_NEW_AVATAR_MENU_CHANGE_PHOTO_ACCESSIBLE_NAME :
584         IDS_PROFILES_NEW_AVATAR_MENU_PHOTO_ACCESSIBLE_NAME)
585                                   forAttribute:NSAccessibilityTitleAttribute];
586     [self accessibilitySetOverrideValue:NSAccessibilityButtonRole
587                                   forAttribute:NSAccessibilityRoleAttribute];
588     [self accessibilitySetOverrideValue:
589         NSAccessibilityRoleDescription(NSAccessibilityButtonRole, nil)
590             forAttribute:NSAccessibilityRoleDescriptionAttribute];
591   }
592   return self;
595 - (void)editPhoto:(id)sender {
596   avatarMenu_->EditProfile(avatarMenu_->GetActiveProfileIndex());
597   [controller_
598       postActionPerformed:ProfileMetrics::PROFILE_DESKTOP_MENU_EDIT_IMAGE];
601 - (void)setHoverState:(HoverState)state {
602   [super setHoverState:state];
603   [changePhotoImage_ setHidden:([self hoverState] == kHoverStateNone)];
606 - (BOOL)canBecomeKeyView {
607   return false;
610 - (BOOL)accessibilityIsIgnored {
611   return NO;
614 - (NSArray*)accessibilityActionNames {
615   NSArray* parentActions = [super accessibilityActionNames];
616   return [parentActions arrayByAddingObject:NSAccessibilityPressAction];
619 - (void)accessibilityPerformAction:(NSString*)action {
620   if ([action isEqualToString:NSAccessibilityPressAction]) {
621     avatarMenu_->EditProfile(avatarMenu_->GetActiveProfileIndex());
622   }
624   [super accessibilityPerformAction:action];
627 @end
629 // A custom text control that turns into a textfield for editing when clicked.
630 @interface EditableProfileNameButton : HoverImageButton<NSTextFieldDelegate> {
631  @private
632   base::scoped_nsobject<NSTextField> profileNameTextField_;
633   Profile* profile_;  // Weak.
634   ProfileChooserController* controller_;
637 - (id)initWithFrame:(NSRect)frameRect
638             profile:(Profile*)profile
639         profileName:(NSString*)profileName
640      editingAllowed:(BOOL)editingAllowed
641      withController:(ProfileChooserController*)controller;
643 // Called when the button is clicked.
644 - (void)showEditableView:(id)sender;
646 // Called when enter is pressed in the text field.
647 - (void)saveProfileName;
649 @end
651 @implementation EditableProfileNameButton
652 - (id)initWithFrame:(NSRect)frameRect
653             profile:(Profile*)profile
654         profileName:(NSString*)profileName
655      editingAllowed:(BOOL)editingAllowed
656      withController:(ProfileChooserController*)controller {
657   if ((self = [super initWithFrame:frameRect])) {
658     profile_ = profile;
659     controller_ = controller;
661     CGFloat availableWidth = frameRect.size.width;
662     NSSize textSize = [profileName sizeWithAttributes:@{
663       NSFontAttributeName : [self font]
664     }];
666     if (editingAllowed) {
667       // Show an "edit" pencil icon when hovering over. In the default state,
668       // we need to create an empty placeholder of the correct size, so that
669       // the text doesn't jump around when the hovered icon appears.
670       ui::ResourceBundle* rb = &ui::ResourceBundle::GetSharedInstance();
671       NSImage* hoverImage = rb->GetNativeImageNamed(
672           IDR_ICON_PROFILES_EDIT_HOVER).AsNSImage();
674       // In order to center the button title, we need to add a left padding of
675       // the same width as the pencil icon.
676       base::scoped_nsobject<CustomPaddingImageButtonCell> cell(
677           [[CustomPaddingImageButtonCell alloc]
678               initWithLeftMarginSpacing:[hoverImage size].width
679                       imageTitleSpacing:0]);
680       [self setCell:cell.get()];
682       NSImage* placeholder = [[NSImage alloc] initWithSize:[hoverImage size]];
683       [self setDefaultImage:placeholder];
684       [self setHoverImage:hoverImage];
685       [self setAlternateImage:
686           rb->GetNativeImageNamed(IDR_ICON_PROFILES_EDIT_PRESSED).AsNSImage()];
687       [self setImagePosition:NSImageRight];
688       [self setTarget:self];
689       [self setAction:@selector(showEditableView:)];
691       // We need to subtract the width of the bezel from the frame rect, so that
692       // the textfield can take the exact same space as the button.
693       frameRect.size.height -= 2 * kBezelThickness;
694       frameRect.origin = NSMakePoint(0, kBezelThickness);
695       profileNameTextField_.reset(
696           [[NSTextField alloc] initWithFrame:frameRect]);
697       [profileNameTextField_ setStringValue:profileName];
698       [profileNameTextField_ setFont:[NSFont labelFontOfSize:kTitleFontSize]];
699       [profileNameTextField_ setEditable:YES];
700       [profileNameTextField_ setDrawsBackground:YES];
701       [profileNameTextField_ setBezeled:YES];
702       [profileNameTextField_ setAlignment:NSCenterTextAlignment];
703       [[profileNameTextField_ cell] setWraps:NO];
704       [[profileNameTextField_ cell] setLineBreakMode:
705           NSLineBreakByTruncatingTail];
706       [[profileNameTextField_ cell] setUsesSingleLineMode:YES];
707       [self addSubview:profileNameTextField_];
708       [profileNameTextField_ setDelegate:self];
710       // Hide the textfield until the user clicks on the button.
711       [profileNameTextField_ setHidden:YES];
713       [[self cell] accessibilitySetOverrideValue:l10n_util::GetNSStringF(
714           IDS_PROFILES_NEW_AVATAR_MENU_EDIT_NAME_ACCESSIBLE_NAME,
715           base::SysNSStringToUTF16(profileName))
716                                     forAttribute:NSAccessibilityTitleAttribute];
718       // Recompute the available width for the name since the icon takes space.
719       availableWidth -= [hoverImage size].width * 2;
720       // The profileNameTextField_ might size the text differently.
721       textSize = [profileName sizeWithAttributes:@{
722         NSFontAttributeName : [profileNameTextField_ font]
723       }];
724     }
726     if (textSize.width > availableWidth)
727       [self setToolTip:profileName];
729     [[self cell] accessibilitySetOverrideValue:NSAccessibilityButtonRole
730                                   forAttribute:NSAccessibilityRoleAttribute];
731     [[self cell]
732         accessibilitySetOverrideValue:NSAccessibilityRoleDescription(
733                                           NSAccessibilityButtonRole, nil)
734                          forAttribute:NSAccessibilityRoleDescriptionAttribute];
736     [self setBordered:NO];
737     [self setFont:[NSFont labelFontOfSize:kTitleFontSize]];
738     [self setAlignment:NSCenterTextAlignment];
739     [[self cell] setLineBreakMode:NSLineBreakByTruncatingTail];
740     [self setTitle:profileName];
741   }
742   return self;
745 - (void)saveProfileName {
746   base::string16 newProfileName =
747       base::SysNSStringToUTF16([profileNameTextField_ stringValue]);
749   // Empty profile names are not allowed, and do nothing.
750   base::TrimWhitespace(newProfileName, base::TRIM_ALL, &newProfileName);
751   if (!newProfileName.empty()) {
752     profiles::UpdateProfileName(profile_, newProfileName);
753     [controller_
754         postActionPerformed:ProfileMetrics::PROFILE_DESKTOP_MENU_EDIT_NAME];
755     [profileNameTextField_ setHidden:YES];
756   }
759 - (void)showEditableView:(id)sender {
760   [profileNameTextField_ setHidden:NO];
761   [[self window] makeFirstResponder:profileNameTextField_];
764 - (BOOL)canBecomeKeyView {
765   return false;
768 - (BOOL)control:(NSControl*)control
769                textView:(NSTextView*)textView
770     doCommandBySelector:(SEL)commandSelector {
771   if (commandSelector == @selector(insertTab:) ||
772       commandSelector == @selector(insertNewline:)) {
773     [self saveProfileName];
774     return YES;
775   }
776   return NO;
779 @end
781 // A custom button that allows for setting a background color when hovered over.
782 @interface BackgroundColorHoverButton : HoverImageButton {
783  @private
784   base::scoped_nsobject<NSColor> backgroundColor_;
785   base::scoped_nsobject<NSColor> hoverColor_;
787 @end
789 @implementation BackgroundColorHoverButton
791 - (id)initWithFrame:(NSRect)frameRect
792   imageTitleSpacing:(int)imageTitleSpacing
793     backgroundColor:(NSColor*)backgroundColor {
794   if ((self = [super initWithFrame:frameRect])) {
795     backgroundColor_.reset([backgroundColor retain]);
796     // Use a color from the common theme, since this button is not trying to
797     // look like a native control.
798     SkColor hoverColor;
799     bool found = ui::CommonThemeGetSystemColor(
800         ui::NativeTheme::kColorId_ButtonHoverBackgroundColor, &hoverColor);
801     DCHECK(found);
802     hoverColor_.reset([gfx::SkColorToSRGBNSColor(hoverColor) retain]);
804     [self setBordered:NO];
805     [self setFont:[NSFont labelFontOfSize:kTextFontSize]];
806     [self setButtonType:NSMomentaryChangeButton];
808     base::scoped_nsobject<CustomPaddingImageButtonCell> cell(
809         [[CustomPaddingImageButtonCell alloc]
810             initWithLeftMarginSpacing:kHorizontalSpacing
811                     imageTitleSpacing:imageTitleSpacing]);
812     [cell setLineBreakMode:NSLineBreakByTruncatingTail];
813     [self setCell:cell.get()];
814   }
815   return self;
818 - (void)setHoverState:(HoverState)state {
819   [super setHoverState:state];
820   bool isHighlighted = ([self hoverState] != kHoverStateNone);
822   NSColor* backgroundColor = isHighlighted ? hoverColor_ : backgroundColor_;
823   [[self cell] setBackgroundColor:backgroundColor];
826 -(void)keyDown:(NSEvent*)event {
827   // Since there is no default button in the bubble, it is safe to activate
828   // all buttons on Enter as well, and be consistent with the Windows
829   // implementation.
830   if ([event keyCode] == kVK_Return)
831     [self performClick:self];
832   else
833     [super keyDown:event];
836 - (BOOL)canBecomeKeyView {
837   return YES;
840 @end
842 // A custom view with the given background color.
843 @interface BackgroundColorView : NSView {
844  @private
845   base::scoped_nsobject<NSColor> backgroundColor_;
847 @end
849 @implementation BackgroundColorView
850 - (id)initWithFrame:(NSRect)frameRect
851           withColor:(NSColor*)color {
852   if ((self = [super initWithFrame:frameRect]))
853     backgroundColor_.reset([color retain]);
854   return self;
857 - (void)drawRect:(NSRect)dirtyRect {
858   [backgroundColor_ setFill];
859   NSRectFill(dirtyRect);
860   [super drawRect:dirtyRect];
862 @end
864 // A custom dummy button that is used to clear focus from the bubble's controls.
865 @interface DummyWindowFocusButton : NSButton
866 @end
868 @implementation DummyWindowFocusButton
869 // Ignore accessibility, as this is a placeholder button.
870 - (BOOL)accessibilityIsIgnored {
871   return YES;
874 - (id)accessibilityAttributeValue:(NSString*)attribute {
875   return nil;
878 - (BOOL)canBecomeKeyView {
879   return NO;
882 @end
884 @interface ProfileChooserController ()
885 // Adds an horizontal separator to |container| at |yOffset| and returns the
886 // yOffset corresponding to after the separator.
887 - (CGFloat)addSeparatorToContainer:(NSView*)container
888                          atYOffset:(CGFloat)yOffset;
890 // Builds the right-click profile switcher.
891 - (void)buildFastUserSwitcherViewWithProfiles:(NSMutableArray*)otherProfiles
892                                     atYOffset:(CGFloat)yOffset
893                                   inContainer:(NSView*)container;
895 // Builds the regular profile chooser view.
896 - (void)buildProfileChooserViewWithProfileView:(NSView*)currentProfileView
897                                   tutorialView:(NSView*)tutorialView
898                                      atYOffset:(CGFloat)yOffset
899                                    inContainer:(NSView*)container
900                                    displayLock:(bool)displayLock;
902 // Builds the profile chooser view.
903 - (NSView*)buildProfileChooserView;
905 - (NSView*)buildTutorialViewIfNeededForItem:(const AvatarMenu::Item&)item;
907 // Builds a tutorial card with a title label using |titleMessage|, a content
908 // label using |contentMessage|, a link using |linkMessage|, and a button using
909 // |buttonMessage|. If |stackButton| is YES, places the button above the link.
910 // Otherwise places both on the same row with the link left aligned and button
911 // right aligned. On click, the link would execute |linkAction|, and the button
912 // would execute |buttonAction|. It sets |tutorialMode_| to the given |mode|.
913 - (NSView*)tutorialViewWithMode:(profiles::TutorialMode)mode
914                    titleMessage:(NSString*)titleMessage
915                  contentMessage:(NSString*)contentMessage
916                     linkMessage:(NSString*)linkMessage
917                   buttonMessage:(NSString*)buttonMessage
918                     stackButton:(BOOL)stackButton
919                  hasCloseButton:(BOOL)hasCloseButton
920                      linkAction:(SEL)linkAction
921                    buttonAction:(SEL)buttonAction;
923 // Builds a tutorial card to introduce an upgrade user to the new avatar menu if
924 // needed. |tutorial_shown| indicates if the tutorial has already been shown in
925 // the previous active view. |avatar_item| refers to the current profile.
926 - (NSView*)buildWelcomeUpgradeTutorialView:(const AvatarMenu::Item&)item;
928 // Builds a tutorial card to inform the user about right-click user switching if
929 // needed.
930 - (NSView*)buildRightClickTutorialView;
932 // Builds a tutorial card to have the user confirm the last Chrome signin,
933 // Chrome sync will be delayed until the user either dismisses the tutorial, or
934 // configures sync through the "Settings" link.
935 - (NSView*)buildSigninConfirmationView;
937 // Builds a tutorial card to show the last signin error.
938 - (NSView*)buildSigninErrorView;
940 // Creates the main profile card for the profile |item| at the top of
941 // the bubble.
942 - (NSView*)createCurrentProfileView:(const AvatarMenu::Item&)item;
944 // Creates the possible links for the main profile card with profile |item|.
945 - (NSView*)createCurrentProfileLinksForItem:(const AvatarMenu::Item&)item
946                                        rect:(NSRect)rect;
948 // Creates the disclaimer text for supervised users, telling them that the
949 // manager can view their history etc.
950 - (NSView*)createSupervisedUserDisclaimerView;
952 // Creates a main profile card for the guest user.
953 - (NSView*)createGuestProfileView;
955 // Creates an item for the profile |itemIndex| that is used in the fast profile
956 // switcher in the middle of the bubble.
957 - (NSButton*)createOtherProfileView:(int)itemIndex;
959 // Creates the "Not you" and Lock option buttons.
960 - (NSView*)createOptionsViewWithRect:(NSRect)rect
961                          displayLock:(BOOL)displayLock;
963 // Creates the account management view for the active profile.
964 - (NSView*)createCurrentProfileAccountsView:(NSRect)rect;
966 // Creates the list of accounts for the active profile.
967 - (NSView*)createAccountsListWithRect:(NSRect)rect;
969 // Creates the Gaia sign-in/add account view.
970 - (NSView*)buildGaiaEmbeddedView;
972 // Creates the account removal view.
973 - (NSView*)buildAccountRemovalView;
975 // Create a view that shows various options for an upgrade user who is not
976 // the same person as the currently signed in user.
977 - (NSView*)buildSwitchUserView;
979 // Creates a button with |text|, an icon given by |imageResourceId| and with
980 // |action|.
981 - (NSButton*)hoverButtonWithRect:(NSRect)rect
982                             text:(NSString*)text
983                  imageResourceId:(int)imageResourceId
984                           action:(SEL)action;
986 // Creates a generic link button with |title| and an |action| positioned at
987 // |frameOrigin|.
988 - (NSButton*)linkButtonWithTitle:(NSString*)title
989                      frameOrigin:(NSPoint)frameOrigin
990                           action:(SEL)action;
992 // Creates an email account button with |title| and a remove icon. If
993 // |reauthRequired| is true, the button also displays a warning icon. |tag|
994 // indicates which account the button refers to.
995 - (NSButton*)accountButtonWithRect:(NSRect)rect
996                          accountId:(const std::string&)accountId
997                                tag:(int)tag
998                     reauthRequired:(BOOL)reauthRequired;
1000 - (bool)shouldShowGoIncognito;
1001 @end
1003 @implementation ProfileChooserController
1004 - (profiles::BubbleViewMode) viewMode {
1005   return viewMode_;
1008 - (void)setTutorialMode:(profiles::TutorialMode)tutorialMode {
1009   tutorialMode_ = tutorialMode;
1012 - (IBAction)switchToProfile:(id)sender {
1013   // Check the event flags to see if a new window should be created.
1014   bool alwaysCreate = ui::WindowOpenDispositionFromNSEvent(
1015       [NSApp currentEvent]) == NEW_WINDOW;
1016   avatarMenu_->SwitchToProfile([sender tag], alwaysCreate,
1017                                ProfileMetrics::SWITCH_PROFILE_ICON);
1020 - (IBAction)showUserManager:(id)sender {
1021   UserManager::Show(base::FilePath(),
1022                     profiles::USER_MANAGER_NO_TUTORIAL,
1023                     profiles::USER_MANAGER_SELECT_PROFILE_NO_ACTION);
1024   [self postActionPerformed:
1025       ProfileMetrics::PROFILE_DESKTOP_MENU_OPEN_USER_MANAGER];
1028 - (IBAction)exitGuest:(id)sender {
1029   DCHECK(browser_->profile()->IsGuestSession());
1030   UserManager::Show(base::FilePath(),
1031                     profiles::USER_MANAGER_NO_TUTORIAL,
1032                     profiles::USER_MANAGER_SELECT_PROFILE_NO_ACTION);
1033   profiles::CloseGuestProfileWindows();
1036 - (IBAction)goIncognito:(id)sender {
1037   DCHECK([self shouldShowGoIncognito]);
1038   chrome::NewIncognitoWindow(browser_);
1039   [self postActionPerformed:
1040       ProfileMetrics::PROFILE_DESKTOP_MENU_GO_INCOGNITO];
1043 - (IBAction)showAccountManagement:(id)sender {
1044   [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_ACCOUNT_MANAGEMENT];
1047 - (IBAction)hideAccountManagement:(id)sender {
1048   [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER];
1051 - (IBAction)lockProfile:(id)sender {
1052   profiles::LockProfile(browser_->profile());
1053   [self postActionPerformed:ProfileMetrics::PROFILE_DESKTOP_MENU_LOCK];
1056 - (IBAction)showInlineSigninPage:(id)sender {
1057   [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_GAIA_SIGNIN];
1060 - (IBAction)addAccount:(id)sender {
1061   [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_GAIA_ADD_ACCOUNT];
1062   [self postActionPerformed:ProfileMetrics::PROFILE_DESKTOP_MENU_ADD_ACCT];
1065 - (IBAction)navigateBackFromSigninPage:(id)sender {
1066   std::string primaryAccount = SigninManagerFactory::GetForProfile(
1067       browser_->profile())->GetAuthenticatedAccountId();
1068   bool hasAccountManagement = !primaryAccount.empty() &&
1069       switches::IsEnableAccountConsistency();
1070   [self initMenuContentsWithView:hasAccountManagement ?
1071       profiles::BUBBLE_VIEW_MODE_ACCOUNT_MANAGEMENT :
1072       profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER];
1075 - (IBAction)showAccountRemovalView:(id)sender {
1076   DCHECK(!isGuestSession_);
1078   // Tag is either |kPrimaryProfileTag| for the primary account, or equal to the
1079   // index in |currentProfileAccounts_| for a secondary account.
1080   int tag = [sender tag];
1081   if (tag == kPrimaryProfileTag) {
1082     accountIdToRemove_ = SigninManagerFactory::GetForProfile(
1083         browser_->profile())->GetAuthenticatedAccountId();
1084   } else {
1085     DCHECK(ContainsKey(currentProfileAccounts_, tag));
1086     accountIdToRemove_ = currentProfileAccounts_[tag];
1087   }
1089   [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_ACCOUNT_REMOVAL];
1092 - (IBAction)showAccountReauthenticationView:(id)sender {
1093   DCHECK(!isGuestSession_);
1094   [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_GAIA_REAUTH];
1097 - (IBAction)removeAccount:(id)sender {
1098   DCHECK(!accountIdToRemove_.empty());
1099   ProfileOAuth2TokenServiceFactory::GetForProfile(browser_->profile())
1100       ->RevokeCredentials(accountIdToRemove_);
1101   [self postActionPerformed:ProfileMetrics::PROFILE_DESKTOP_MENU_REMOVE_ACCT];
1102   accountIdToRemove_.clear();
1104   [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_ACCOUNT_MANAGEMENT];
1107 - (IBAction)seeWhatsNew:(id)sender {
1108   UserManager::Show(base::FilePath(),
1109                     profiles::USER_MANAGER_TUTORIAL_OVERVIEW,
1110                     profiles::USER_MANAGER_SELECT_PROFILE_NO_ACTION);
1111   ProfileMetrics::LogProfileNewAvatarMenuUpgrade(
1112       ProfileMetrics::PROFILE_AVATAR_MENU_UPGRADE_WHATS_NEW);
1115 - (IBAction)showSwitchUserView:(id)sender {
1116   [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_SWITCH_USER];
1117   ProfileMetrics::LogProfileNewAvatarMenuUpgrade(
1118       ProfileMetrics::PROFILE_AVATAR_MENU_UPGRADE_NOT_YOU);
1121 - (IBAction)showLearnMorePage:(id)sender {
1122   signin_ui_util::ShowSigninErrorLearnMorePage(browser_->profile());
1125 - (IBAction)configureSyncSettings:(id)sender {
1126   tutorialMode_ = profiles::TUTORIAL_MODE_NONE;
1127   LoginUIServiceFactory::GetForProfile(browser_->profile())->
1128       SyncConfirmationUIClosed(true);
1129   ProfileMetrics::LogProfileNewAvatarMenuSignin(
1130       ProfileMetrics::PROFILE_AVATAR_MENU_SIGNIN_SETTINGS);
1133 - (IBAction)syncSettingsConfirmed:(id)sender {
1134   tutorialMode_ = profiles::TUTORIAL_MODE_NONE;
1135   LoginUIServiceFactory::GetForProfile(browser_->profile())->
1136       SyncConfirmationUIClosed(false);
1137   ProfileMetrics::LogProfileNewAvatarMenuSignin(
1138       ProfileMetrics::PROFILE_AVATAR_MENU_SIGNIN_OK);
1139   [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER];
1142 - (IBAction)disconnectProfile:(id)sender {
1143   chrome::ShowSettings(browser_);
1144   ProfileMetrics::LogProfileNewAvatarMenuNotYou(
1145       ProfileMetrics::PROFILE_AVATAR_MENU_NOT_YOU_DISCONNECT);
1148 - (IBAction)navigateBackFromSwitchUserView:(id)sender {
1149   [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER];
1150   ProfileMetrics::LogProfileNewAvatarMenuNotYou(
1151       ProfileMetrics::PROFILE_AVATAR_MENU_NOT_YOU_BACK);
1154 - (IBAction)dismissTutorial:(id)sender {
1155   // Never shows the upgrade tutorial again if manually closed.
1156   if (tutorialMode_ == profiles::TUTORIAL_MODE_WELCOME_UPGRADE) {
1157     browser_->profile()->GetPrefs()->SetInteger(
1158         prefs::kProfileAvatarTutorialShown,
1159         signin_ui_util::kUpgradeWelcomeTutorialShowMax + 1);
1160   }
1162   if(tutorialMode_ == profiles::TUTORIAL_MODE_RIGHT_CLICK_SWITCHING) {
1163     PrefService* localState = g_browser_process->local_state();
1164     localState->SetBoolean(
1165         prefs::kProfileAvatarRightClickTutorialDismissed, true);
1166   }
1168   tutorialMode_ = profiles::TUTORIAL_MODE_NONE;
1169   [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER];
1172 - (void)windowWillClose:(NSNotification*)notification {
1173   if (tutorialMode_ == profiles::TUTORIAL_MODE_CONFIRM_SIGNIN) {
1174     LoginUIServiceFactory::GetForProfile(browser_->profile())->
1175         SyncConfirmationUIClosed(false);
1176   }
1178   [super windowWillClose:notification];
1181 - (void)moveDown:(id)sender {
1182   [[self window] selectNextKeyView:self];
1185 - (void)moveUp:(id)sender {
1186   [[self window] selectPreviousKeyView:self];
1189 - (void)cleanUpEmbeddedViewContents {
1190   webContents_.reset();
1191   webContentsDelegate_.reset();
1194 - (id)initWithBrowser:(Browser*)browser
1195            anchoredAt:(NSPoint)point
1196              viewMode:(profiles::BubbleViewMode)viewMode
1197          tutorialMode:(profiles::TutorialMode)tutorialMode
1198           serviceType:(signin::GAIAServiceType)serviceType {
1199   base::scoped_nsobject<InfoBubbleWindow> window([[InfoBubbleWindow alloc]
1200       initWithContentRect:ui::kWindowSizeDeterminedLater
1201                 styleMask:NSBorderlessWindowMask
1202                   backing:NSBackingStoreBuffered
1203                     defer:NO]);
1205   if ((self = [super initWithWindow:window
1206                        parentWindow:browser->window()->GetNativeWindow()
1207                          anchoredAt:point])) {
1208     browser_ = browser;
1209     viewMode_ = viewMode;
1210     tutorialMode_ = tutorialMode;
1211     observer_.reset(new ActiveProfileObserverBridge(self, browser_));
1212     serviceType_ = serviceType;
1214     avatarMenu_.reset(new AvatarMenu(
1215         &g_browser_process->profile_manager()->GetProfileInfoCache(),
1216         observer_.get(),
1217         browser_));
1218     avatarMenu_->RebuildMenu();
1220     // Guest profiles do not have a token service.
1221     isGuestSession_ = browser_->profile()->IsGuestSession();
1223     // If view mode is PROFILE_CHOOSER but there is an auth error, force
1224     // ACCOUNT_MANAGEMENT mode.
1225     if (viewMode_ == profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER &&
1226         HasAuthError(browser_->profile()) &&
1227         switches::IsEnableAccountConsistency() &&
1228         avatarMenu_->GetItemAt(avatarMenu_->GetActiveProfileIndex()).
1229             signed_in) {
1230       viewMode_ = profiles::BUBBLE_VIEW_MODE_ACCOUNT_MANAGEMENT;
1231     }
1233     [window accessibilitySetOverrideValue:
1234         l10n_util::GetNSString(IDS_PROFILES_NEW_AVATAR_MENU_ACCESSIBLE_NAME)
1235                              forAttribute:NSAccessibilityTitleAttribute];
1236     [window accessibilitySetOverrideValue:
1237         l10n_util::GetNSString(IDS_PROFILES_NEW_AVATAR_MENU_ACCESSIBLE_NAME)
1238                              forAttribute:NSAccessibilityHelpAttribute];
1240     [[self bubble] setAlignment:info_bubble::kAlignRightEdgeToAnchorEdge];
1241     [[self bubble] setArrowLocation:info_bubble::kNoArrow];
1242     [[self bubble] setBackgroundColor:GetDialogBackgroundColor()];
1243     [self initMenuContentsWithView:viewMode_];
1244   }
1246   return self;
1249 - (void)initMenuContentsWithView:(profiles::BubbleViewMode)viewToDisplay {
1250   if (browser_->profile()->IsSupervised() &&
1251       (viewToDisplay == profiles::BUBBLE_VIEW_MODE_GAIA_ADD_ACCOUNT ||
1252        viewToDisplay == profiles::BUBBLE_VIEW_MODE_ACCOUNT_REMOVAL)) {
1253     LOG(WARNING) << "Supervised user attempted to add/remove account";
1254     return;
1255   }
1256   viewMode_ = viewToDisplay;
1257   NSView* contentView = [[self window] contentView];
1258   [contentView setSubviews:[NSArray array]];
1259   NSView* subView;
1261   switch (viewMode_) {
1262     case profiles::BUBBLE_VIEW_MODE_GAIA_SIGNIN:
1263     case profiles::BUBBLE_VIEW_MODE_GAIA_ADD_ACCOUNT:
1264     case profiles::BUBBLE_VIEW_MODE_GAIA_REAUTH:
1265       subView = [self buildGaiaEmbeddedView];
1266       break;
1267     case profiles::BUBBLE_VIEW_MODE_ACCOUNT_REMOVAL:
1268       subView = [self buildAccountRemovalView];
1269       break;
1270     case profiles::BUBBLE_VIEW_MODE_SWITCH_USER:
1271       subView = [self buildSwitchUserView];
1272       break;
1273     case profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER:
1274     case profiles::BUBBLE_VIEW_MODE_FAST_PROFILE_CHOOSER:
1275     case profiles::BUBBLE_VIEW_MODE_ACCOUNT_MANAGEMENT:
1276       subView = [self buildProfileChooserView];
1277       break;
1278   }
1280   // Clears tutorial mode for all non-profile-chooser views.
1281   if (viewMode_ != profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER)
1282     tutorialMode_ = profiles::TUTORIAL_MODE_NONE;
1284   // Add a dummy, empty element so that we don't initially display any
1285   // focus rings.
1286   NSButton* dummyFocusButton =
1287      [[[DummyWindowFocusButton alloc] initWithFrame:NSZeroRect] autorelease];
1288   [dummyFocusButton setNextKeyView:subView];
1289   [[self window] makeFirstResponder:dummyFocusButton];
1291   [contentView addSubview:subView];
1292   [contentView addSubview:dummyFocusButton];
1293   SetWindowSize([self window],
1294       NSMakeSize(NSWidth([subView frame]), NSHeight([subView frame])));
1297 - (CGFloat)addSeparatorToContainer:(NSView*)container
1298                          atYOffset:(CGFloat)yOffset {
1299   NSBox* separator = [self horizontalSeparatorWithFrame:NSMakeRect(
1300       0, yOffset, kFixedMenuWidth, 0)];
1301   [container addSubview:separator];
1302   return NSMaxY([separator frame]);
1305 // Builds the fast user switcher view in |container| at |yOffset| and populates
1306 // it with the entries for every profile in |otherProfiles|. Returns the new
1307 // yOffset after adding the elements.
1308 - (void)buildFastUserSwitcherViewWithProfiles:(NSMutableArray*)otherProfiles
1309                                     atYOffset:(CGFloat)yOffset
1310                                   inContainer:(NSView*)container {
1311   // Other profiles switcher. The profiles have already been sorted
1312   // by their y-coordinate, so they can be added in the existing order.
1313   for (NSView* otherProfileView in otherProfiles) {
1314    [otherProfileView setFrameOrigin:NSMakePoint(0, yOffset)];
1315    [container addSubview:otherProfileView];
1316    yOffset = NSMaxY([otherProfileView frame]);
1318    yOffset = [self addSeparatorToContainer:container atYOffset: yOffset];
1319   }
1321   [container setFrameSize:NSMakeSize(kFixedMenuWidth, yOffset)];
1324 - (void)buildProfileChooserViewWithProfileView:(NSView*)currentProfileView
1325                                   tutorialView:(NSView*)tutorialView
1326                                      atYOffset:(CGFloat)yOffset
1327                                    inContainer:(NSView*)container
1328                                    displayLock:(bool)displayLock {
1329   // Option buttons.
1330   NSRect rect = NSMakeRect(0, yOffset, kFixedMenuWidth, 0);
1331   NSView* optionsView = [self createOptionsViewWithRect:rect
1332                                             displayLock:displayLock];
1333   [container addSubview:optionsView];
1334   rect.origin.y = NSMaxY([optionsView frame]);
1336   NSBox* separator = [self horizontalSeparatorWithFrame:rect];
1337   [container addSubview:separator];
1338   yOffset = NSMaxY([separator frame]);
1340   // For supervised users, add the disclaimer text.
1341   if (browser_->profile()->IsSupervised()) {
1342     yOffset += kSmallVerticalSpacing;
1343     NSView* disclaimerContainer = [self createSupervisedUserDisclaimerView];
1344     [disclaimerContainer setFrameOrigin:NSMakePoint(0, yOffset)];
1345     [container addSubview:disclaimerContainer];
1346     yOffset = NSMaxY([disclaimerContainer frame]);
1347     yOffset += kSmallVerticalSpacing;
1349     yOffset = [self addSeparatorToContainer:container atYOffset: yOffset];
1350   }
1352   if (viewMode_ == profiles::BUBBLE_VIEW_MODE_ACCOUNT_MANAGEMENT) {
1353     NSView* currentProfileAccountsView = [self createCurrentProfileAccountsView:
1354         NSMakeRect(0, yOffset, kFixedMenuWidth, 0)];
1355     [container addSubview:currentProfileAccountsView];
1356     yOffset = NSMaxY([currentProfileAccountsView frame]);
1358     yOffset = [self addSeparatorToContainer:container atYOffset: yOffset];
1359   }
1361   // Active profile card.
1362   if (currentProfileView) {
1363     yOffset += kVerticalSpacing;
1364     [currentProfileView setFrameOrigin:NSMakePoint(0, yOffset)];
1365     [container addSubview:currentProfileView];
1366     yOffset = NSMaxY([currentProfileView frame]) + kVerticalSpacing;
1367   }
1369   if (tutorialView) {
1370     [tutorialView setFrameOrigin:NSMakePoint(0, yOffset)];
1371     [container addSubview:tutorialView];
1372     yOffset = NSMaxY([tutorialView frame]);
1373     //TODO(mlerman): update UMA stats for the new tutorials.
1374   } else {
1375     tutorialMode_ = profiles::TUTORIAL_MODE_NONE;
1376   }
1378   [container setFrameSize:NSMakeSize(kFixedMenuWidth, yOffset)];
1381 - (NSView*)buildProfileChooserView {
1382   base::scoped_nsobject<NSView> container(
1383       [[NSView alloc] initWithFrame:NSZeroRect]);
1385   NSView* tutorialView = nil;
1386   NSView* currentProfileView = nil;
1387   base::scoped_nsobject<NSMutableArray> otherProfiles(
1388       [[NSMutableArray alloc] init]);
1389   // Local and guest profiles cannot lock their profile.
1390   bool displayLock = false;
1391   bool isFastProfileChooser =
1392       viewMode_ == profiles::BUBBLE_VIEW_MODE_FAST_PROFILE_CHOOSER;
1393   if (isFastProfileChooser) {
1394     // The user is using right-click switching, no need to tell them about it.
1395     PrefService* localState = g_browser_process->local_state();
1396     localState->SetBoolean(
1397         prefs::kProfileAvatarRightClickTutorialDismissed, true);
1398   }
1400   // Loop over the profiles in reverse, so that they are sorted by their
1401   // y-coordinate, and separate them into active and "other" profiles.
1402   for (int i = avatarMenu_->GetNumberOfItems() - 1; i >= 0; --i) {
1403     const AvatarMenu::Item& item = avatarMenu_->GetItemAt(i);
1404     if (item.active) {
1405       if (viewMode_ == profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER) {
1406         tutorialView = [self buildTutorialViewIfNeededForItem:item];
1407       }
1408       currentProfileView = [self createCurrentProfileView:item];
1409       displayLock = item.signed_in &&
1410           profiles::IsLockAvailable(browser_->profile());
1411     } else {
1412       [otherProfiles addObject:[self createOtherProfileView:i]];
1413     }
1414   }
1415   if (!currentProfileView)  // Guest windows don't have an active profile.
1416     currentProfileView = [self createGuestProfileView];
1418   // |yOffset| is the next position at which to draw in |container|
1419   // coordinates. Add a pixel offset so that the bottom option buttons don't
1420   // overlap the bubble's rounded corners.
1421   CGFloat yOffset = 1;
1423   if (isFastProfileChooser) {
1424     [self buildFastUserSwitcherViewWithProfiles:otherProfiles.get()
1425                                       atYOffset:yOffset
1426                                     inContainer:container.get()];
1427   } else {
1428     [self buildProfileChooserViewWithProfileView:currentProfileView
1429                                     tutorialView:tutorialView
1430                                        atYOffset:yOffset
1431                                      inContainer:container.get()
1432                                      displayLock:displayLock];
1433   }
1435   return container.autorelease();
1438 - (NSView*)buildSigninConfirmationView {
1439   ProfileMetrics::LogProfileNewAvatarMenuSignin(
1440       ProfileMetrics::PROFILE_AVATAR_MENU_SIGNIN_VIEW);
1442   NSString* titleMessage = l10n_util::GetNSString(
1443       IDS_PROFILES_CONFIRM_SIGNIN_TUTORIAL_TITLE);
1444   NSString* contentMessage = l10n_util::GetNSString(
1445       IDS_PROFILES_CONFIRM_SIGNIN_TUTORIAL_CONTENT_TEXT);
1446   NSString* linkMessage = l10n_util::GetNSString(
1447       IDS_PROFILES_SYNC_SETTINGS_LINK);
1448   NSString* buttonMessage = l10n_util::GetNSString(
1449       IDS_PROFILES_TUTORIAL_OK_BUTTON);
1450   return [self tutorialViewWithMode:profiles::TUTORIAL_MODE_CONFIRM_SIGNIN
1451                        titleMessage:titleMessage
1452                      contentMessage:contentMessage
1453                         linkMessage:linkMessage
1454                       buttonMessage:buttonMessage
1455                         stackButton:NO
1456                      hasCloseButton:NO
1457                          linkAction:@selector(configureSyncSettings:)
1458                        buttonAction:@selector(syncSettingsConfirmed:)];
1461 - (NSView*)buildSigninErrorView {
1462   NSString* titleMessage = l10n_util::GetNSString(
1463       IDS_PROFILES_ERROR_TUTORIAL_TITLE);
1464   LoginUIService* loginUiService =
1465       LoginUIServiceFactory::GetForProfile(browser_->profile());
1466   NSString* contentMessage =
1467       base::SysUTF16ToNSString(loginUiService->GetLastLoginResult());
1468   NSString* linkMessage = l10n_util::GetNSString(
1469       IDS_PROFILES_PROFILE_TUTORIAL_LEARN_MORE);
1470   return [self tutorialViewWithMode:profiles::TUTORIAL_MODE_CONFIRM_SIGNIN
1471                        titleMessage:titleMessage
1472                      contentMessage:contentMessage
1473                         linkMessage:linkMessage
1474                       buttonMessage:nil
1475                         stackButton:NO
1476                      hasCloseButton:YES
1477                          linkAction:@selector(showLearnMorePage:)
1478                        buttonAction:nil];
1481 - (NSView*)buildWelcomeUpgradeTutorialView:(const AvatarMenu::Item&)item {
1482   ProfileMetrics::LogProfileNewAvatarMenuUpgrade(
1483       ProfileMetrics::PROFILE_AVATAR_MENU_UPGRADE_VIEW);
1485   NSString* titleMessage = l10n_util::GetNSString(
1486       IDS_PROFILES_WELCOME_UPGRADE_TUTORIAL_TITLE);
1487   NSString* contentMessage = l10n_util::GetNSString(
1488       IDS_PROFILES_WELCOME_UPGRADE_TUTORIAL_CONTENT_TEXT);
1489   // For local profiles, the "Not you" link doesn't make sense.
1490   NSString* linkMessage = item.signed_in ?
1491       ElideMessage(
1492           l10n_util::GetStringFUTF16(IDS_PROFILES_NOT_YOU, item.name),
1493           kFixedMenuWidth - 2 * kHorizontalSpacing) :
1494       nil;
1495   NSString* buttonMessage = l10n_util::GetNSString(
1496       IDS_PROFILES_TUTORIAL_WHATS_NEW_BUTTON);
1497   return [self tutorialViewWithMode:profiles::TUTORIAL_MODE_WELCOME_UPGRADE
1498                        titleMessage:titleMessage
1499                      contentMessage:contentMessage
1500                         linkMessage:linkMessage
1501                       buttonMessage:buttonMessage
1502                         stackButton:YES
1503                      hasCloseButton:YES
1504                          linkAction:@selector(showSwitchUserView:)
1505                        buttonAction:@selector(seeWhatsNew:)];
1508 - (NSView*)buildRightClickTutorialView {
1509   NSString* titleMessage = l10n_util::GetNSString(
1510       IDS_PROFILES_RIGHT_CLICK_TUTORIAL_TITLE);
1511   NSString* contentMessage = l10n_util::GetNSString(
1512       IDS_PROFILES_RIGHT_CLICK_TUTORIAL_CONTENT_TEXT);
1513   NSString* buttonMessage = l10n_util::GetNSString(
1514       IDS_PROFILES_TUTORIAL_OK_BUTTON);
1516   return
1517       [self tutorialViewWithMode:profiles::TUTORIAL_MODE_RIGHT_CLICK_SWITCHING
1518                              titleMessage:titleMessage
1519                            contentMessage:contentMessage
1520                               linkMessage:nil
1521                             buttonMessage:buttonMessage
1522                               stackButton:NO
1523                            hasCloseButton:NO
1524                                linkAction:nil
1525                              buttonAction:@selector(dismissTutorial:)];
1528 - (NSView*)buildTutorialViewIfNeededForItem:(const AvatarMenu::Item&)item {
1529   if (tutorialMode_ == profiles::TUTORIAL_MODE_CONFIRM_SIGNIN)
1530     return [self buildSigninConfirmationView];
1532   if (tutorialMode_ == profiles::TUTORIAL_MODE_SHOW_ERROR)
1533     return [self buildSigninErrorView];
1535   if (profiles::ShouldShowWelcomeUpgradeTutorial(
1536       browser_->profile(), tutorialMode_)) {
1537     if (tutorialMode_ != profiles::TUTORIAL_MODE_WELCOME_UPGRADE) {
1538       Profile* profile = browser_->profile();
1539       const int showCount = profile->GetPrefs()->GetInteger(
1540           prefs::kProfileAvatarTutorialShown);
1541       profile->GetPrefs()->SetInteger(
1542           prefs::kProfileAvatarTutorialShown, showCount + 1);
1543     }
1545     return [self buildWelcomeUpgradeTutorialView:item];
1546   }
1548   if (profiles::ShouldShowRightClickTutorial(browser_->profile()))
1549     return [self buildRightClickTutorialView];
1551   return nil;
1554 - (NSView*)tutorialViewWithMode:(profiles::TutorialMode)mode
1555                    titleMessage:(NSString*)titleMessage
1556                  contentMessage:(NSString*)contentMessage
1557                     linkMessage:(NSString*)linkMessage
1558                   buttonMessage:(NSString*)buttonMessage
1559                     stackButton:(BOOL)stackButton
1560                  hasCloseButton:(BOOL)hasCloseButton
1561                      linkAction:(SEL)linkAction
1562                    buttonAction:(SEL)buttonAction {
1563   tutorialMode_ = mode;
1565   NSColor* tutorialBackgroundColor =
1566       gfx::SkColorToSRGBNSColor(profiles::kAvatarTutorialBackgroundColor);
1567   base::scoped_nsobject<NSView> container([[BackgroundColorView alloc]
1568       initWithFrame:NSMakeRect(0, 0, kFixedMenuWidth, 0)
1569           withColor:tutorialBackgroundColor]);
1570   CGFloat availableWidth = kFixedMenuWidth - 2 * kHorizontalSpacing;
1571   CGFloat yOffset = kVerticalSpacing;
1573   // Adds links and buttons at the bottom.
1574   base::scoped_nsobject<NSButton> tutorialOkButton;
1575   if (buttonMessage) {
1576     tutorialOkButton.reset([[HoverButton alloc] initWithFrame:NSZeroRect]);
1577     [tutorialOkButton setTitle:buttonMessage];
1578     [tutorialOkButton setBezelStyle:NSRoundedBezelStyle];
1579     [tutorialOkButton setTarget:self];
1580     [tutorialOkButton setAction:buttonAction];
1581     [tutorialOkButton setAlignment:NSCenterTextAlignment];
1582     [tutorialOkButton sizeToFit];
1583   }
1585   NSButton* learnMoreLink = nil;
1586   if (linkMessage) {
1587     learnMoreLink = [self linkButtonWithTitle:linkMessage
1588                                   frameOrigin:NSZeroPoint
1589                                        action:linkAction];
1590     [[learnMoreLink cell] setTextColor:[NSColor whiteColor]];
1591   }
1593   if (stackButton) {
1594     if (linkMessage) {
1595       [learnMoreLink setFrameOrigin:NSMakePoint(
1596           (kFixedMenuWidth - NSWidth([learnMoreLink frame])) / 2, yOffset)];
1597     }
1598     [tutorialOkButton setFrameSize:NSMakeSize(
1599         availableWidth, NSHeight([tutorialOkButton frame]))];
1600     [tutorialOkButton setFrameOrigin:NSMakePoint(
1601         kHorizontalSpacing,
1602         yOffset + (learnMoreLink ? NSHeight([learnMoreLink frame]) : 0))];
1603   } else {
1604     if (buttonMessage) {
1605       NSSize buttonSize = [tutorialOkButton frame].size;
1606       const CGFloat kTopBottomTextPadding = 6;
1607       const CGFloat kLeftRightTextPadding = 15;
1608       buttonSize.width += 2 * kLeftRightTextPadding;
1609       buttonSize.height += 2 * kTopBottomTextPadding;
1610       [tutorialOkButton setFrameSize:buttonSize];
1611       CGFloat buttonXOffset = kFixedMenuWidth -
1612           NSWidth([tutorialOkButton frame]) - kHorizontalSpacing;
1613       [tutorialOkButton setFrameOrigin:NSMakePoint(buttonXOffset, yOffset)];
1614     }
1616     if (linkMessage) {
1617       CGFloat linkYOffset = yOffset;
1618       if (buttonMessage) {
1619         linkYOffset += (NSHeight([tutorialOkButton frame]) -
1620                         NSHeight([learnMoreLink frame])) / 2;
1621       }
1622       [learnMoreLink setFrameOrigin:NSMakePoint(
1623           kHorizontalSpacing, linkYOffset)];
1624     }
1625   }
1627   if (buttonMessage) {
1628     [container addSubview:tutorialOkButton];
1629     yOffset = NSMaxY([tutorialOkButton frame]);
1630   }
1632   if (linkMessage) {
1633     [container addSubview:learnMoreLink];
1634     yOffset = std::max(NSMaxY([learnMoreLink frame]), yOffset);
1635   }
1637   yOffset += kVerticalSpacing;
1639   // Adds body content.
1640   NSTextField* contentLabel = BuildLabel(
1641       contentMessage,
1642       NSMakePoint(kHorizontalSpacing, yOffset),
1643       gfx::SkColorToSRGBNSColor(profiles::kAvatarTutorialContentTextColor));
1644   [contentLabel setFrameSize:NSMakeSize(availableWidth, 0)];
1645   [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:contentLabel];
1646   [container addSubview:contentLabel];
1647   yOffset = NSMaxY([contentLabel frame]) + kSmallVerticalSpacing;
1649   // Adds title.
1650   NSTextField* titleLabel =
1651       BuildLabel(titleMessage,
1652                  NSMakePoint(kHorizontalSpacing, yOffset),
1653                  [NSColor whiteColor] /* text_color */);
1654   [titleLabel setFont:[NSFont labelFontOfSize:kTitleFontSize]];
1656   if (hasCloseButton) {
1657     base::scoped_nsobject<HoverImageButton> closeButton(
1658         [[HoverImageButton alloc] initWithFrame:NSZeroRect]);
1659     [closeButton setBordered:NO];
1661     ui::ResourceBundle* rb = &ui::ResourceBundle::GetSharedInstance();
1662     NSImage* closeImage = rb->GetNativeImageNamed(IDR_CLOSE_1).ToNSImage();
1663     CGFloat closeImageWidth = [closeImage size].width;
1664     [closeButton setDefaultImage:closeImage];
1665     [closeButton setHoverImage:
1666         rb->GetNativeImageNamed(IDR_CLOSE_1_H).ToNSImage()];
1667     [closeButton setPressedImage:
1668         rb->GetNativeImageNamed(IDR_CLOSE_1_P).ToNSImage()];
1669     [closeButton setTarget:self];
1670     [closeButton setAction:@selector(dismissTutorial:)];
1671     [closeButton setFrameSize:[closeImage size]];
1672     [closeButton setFrameOrigin:NSMakePoint(
1673         kFixedMenuWidth - kHorizontalSpacing - closeImageWidth, yOffset)];
1674     [container addSubview:closeButton];
1676     [titleLabel setFrameSize:NSMakeSize(
1677         availableWidth - closeImageWidth - kHorizontalSpacing, 0)];
1678   } else {
1679     [titleLabel setFrameSize:NSMakeSize(availableWidth, 0)];
1680   }
1682   [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:titleLabel];
1683   [container addSubview:titleLabel];
1684   yOffset = NSMaxY([titleLabel frame]) + kVerticalSpacing;
1686   [container setFrameSize:NSMakeSize(kFixedMenuWidth, yOffset)];
1687   [container setFrameOrigin:NSZeroPoint];
1688   return container.autorelease();
1691 - (NSView*)createCurrentProfileView:(const AvatarMenu::Item&)item {
1692   base::scoped_nsobject<NSView> container([[NSView alloc]
1693       initWithFrame:NSZeroRect]);
1695   CGFloat xOffset = kHorizontalSpacing;
1696   CGFloat yOffset = 0;
1697   CGFloat availableTextWidth = kFixedMenuWidth - 2 * kHorizontalSpacing;
1699   // Profile options. This can be a link to the accounts view, the profile's
1700   // username for signed in users, or a "Sign in" button for local profiles.
1701   SigninManagerBase* signinManager =
1702       SigninManagerFactory::GetForProfile(
1703           browser_->profile()->GetOriginalProfile());
1704   if (!isGuestSession_ && signinManager->IsSigninAllowed()) {
1705     NSView* linksContainer =
1706         [self createCurrentProfileLinksForItem:item
1707                                           rect:NSMakeRect(xOffset, yOffset,
1708                                                           availableTextWidth,
1709                                                           0)];
1710     [container addSubview:linksContainer];
1711     yOffset = NSMaxY([linksContainer frame]);
1712   }
1714   // Profile name, centered.
1715   bool editingAllowed = !isGuestSession_ &&
1716                         !browser_->profile()->IsLegacySupervised();
1717   base::scoped_nsobject<EditableProfileNameButton> profileName(
1718       [[EditableProfileNameButton alloc]
1719           initWithFrame:NSMakeRect(xOffset,
1720                                    yOffset,
1721                                    availableTextWidth,
1722                                    kProfileButtonHeight)
1723                 profile:browser_->profile()
1724             profileName:base::SysUTF16ToNSString(
1725                             profiles::GetAvatarNameForProfile(
1726                                 browser_->profile()->GetPath()))
1727          editingAllowed:editingAllowed
1728          withController:self]);
1730   [container addSubview:profileName];
1731   yOffset = NSMaxY([profileName frame]) + 4;  // Adds a small vertical padding.
1733   // Profile icon, centered.
1734   xOffset = (kFixedMenuWidth - kLargeImageSide) / 2;
1735   base::scoped_nsobject<EditableProfilePhoto> iconView(
1736       [[EditableProfilePhoto alloc]
1737           initWithFrame:NSMakeRect(xOffset, yOffset,
1738                                    kLargeImageSide, kLargeImageSide)
1739              avatarMenu:avatarMenu_.get()
1740             profileIcon:item.icon
1741          editingAllowed:!isGuestSession_
1742          withController:self]);
1744   [container addSubview:iconView];
1745   yOffset = NSMaxY([iconView frame]);
1747   if (browser_->profile()->IsSupervised()) {
1748     base::scoped_nsobject<NSImageView> supervisedIcon(
1749         [[NSImageView alloc] initWithFrame:NSZeroRect]);
1750     int imageId = browser_->profile()->IsChild()
1751         ? IDR_ICON_PROFILES_MENU_CHILD
1752         : IDR_ICON_PROFILES_MENU_LEGACY_SUPERVISED;
1753     ui::ResourceBundle* rb = &ui::ResourceBundle::GetSharedInstance();
1754     [supervisedIcon setImage:rb->GetNativeImageNamed(imageId).ToNSImage()];
1755     NSSize size = [[supervisedIcon image] size];
1756     [supervisedIcon setFrameSize:size];
1757     NSRect parentFrame = [iconView frame];
1758     [supervisedIcon setFrameOrigin:NSMakePoint(NSMaxX(parentFrame) - size.width,
1759                                                NSMinY(parentFrame))];
1760     [container addSubview:supervisedIcon];
1761   }
1763   [container setFrameSize:NSMakeSize(kFixedMenuWidth, yOffset)];
1764   return container.autorelease();
1767 - (NSView*)createCurrentProfileLinksForItem:(const AvatarMenu::Item&)item
1768                                        rect:(NSRect)rect {
1769   base::scoped_nsobject<NSView> container([[NSView alloc] initWithFrame:rect]);
1771   // Don't double-apply the left margin to the sub-views.
1772   rect.origin.x = 0;
1774   // The available links depend on the type of profile that is active.
1775   if (item.signed_in) {
1776     // Signed in profiles with no authentication errors do not have a clickable
1777     // email link.
1778     NSButton* link = nil;
1779     if (switches::IsEnableAccountConsistency()) {
1780       NSString* linkTitle = l10n_util::GetNSString(
1781           viewMode_ == profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER ?
1782               IDS_PROFILES_PROFILE_MANAGE_ACCOUNTS_BUTTON :
1783               IDS_PROFILES_PROFILE_HIDE_MANAGE_ACCOUNTS_BUTTON);
1784       SEL linkSelector =
1785           (viewMode_ == profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER) ?
1786           @selector(showAccountManagement:) : @selector(hideAccountManagement:);
1787       link = [self linkButtonWithTitle:linkTitle
1788                            frameOrigin:rect.origin
1789                                 action:linkSelector];
1790     } else {
1791       link = [self linkButtonWithTitle:base::SysUTF16ToNSString(item.username)
1792                            frameOrigin:rect.origin
1793                                 action:nil];
1794       if (HasAuthError(browser_->profile())) {
1795         [link setImage:ui::ResourceBundle::GetSharedInstance().
1796             GetNativeImageNamed(IDR_ICON_PROFILES_ACCOUNT_BUTTON_ERROR).
1797             ToNSImage()];
1798         [link setImagePosition:NSImageRight];
1799         [link setTarget:self];
1800         [link setAction:@selector(showAccountReauthenticationView:)];
1801         [link setTag:kPrimaryProfileTag];
1802         [[link cell]
1803             accessibilitySetOverrideValue:l10n_util::GetNSStringF(
1804             IDS_PROFILES_ACCOUNT_BUTTON_AUTH_ERROR_ACCESSIBLE_NAME,
1805             item.username)
1806                              forAttribute:NSAccessibilityTitleAttribute];
1807       } else {
1808         [link setEnabled:NO];
1809       }
1810     }
1811     // -linkButtonWithTitle sizeToFit's the link. We can use the height, but
1812     // need to re-stretch the width so that the link can be centered correctly
1813     // in the view.
1814     rect.size.height = [link frame].size.height;
1815     [link setAlignment:NSCenterTextAlignment];
1816     [link setFrame:rect];
1817     [container addSubview:link];
1818     [container setFrameSize:rect.size];
1819   } else {
1820     rect.size.height = kBlueButtonHeight;
1821     NSButton* signinButton = [[BlueLabelButton alloc] initWithFrame:rect];
1823     // Manually elide the button text so that the contents fit inside the bubble
1824     // This is needed because the BlueLabelButton cell resets the style on
1825     // every call to -cellSize, which prevents setting a custom lineBreakMode.
1826     NSString* elidedButtonText = base::SysUTF16ToNSString(gfx::ElideText(
1827         l10n_util::GetStringFUTF16(
1828             IDS_SYNC_START_SYNC_BUTTON_LABEL,
1829             l10n_util::GetStringUTF16(IDS_SHORT_PRODUCT_NAME)),
1830         gfx::FontList(), rect.size.width, gfx::ELIDE_TAIL));
1832     [signinButton setTitle:elidedButtonText];
1833     [signinButton setTarget:self];
1834     [signinButton setAction:@selector(showInlineSigninPage:)];
1835     [container addSubview:signinButton];
1837     // Sign-in promo text.
1838     NSTextField* promo = BuildLabel(
1839         l10n_util::GetNSString(IDS_PROFILES_SIGNIN_PROMO),
1840         NSMakePoint(0, NSMaxY([signinButton frame]) + kVerticalSpacing),
1841         nil);
1842     [promo setFrameSize:NSMakeSize(rect.size.width, 0)];
1843     [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:promo];
1844     [container addSubview:promo];
1846     [container setFrameSize:NSMakeSize(
1847         rect.size.width,
1848         NSMaxY([promo frame]) + 4)];  // Adds a small vertical padding.
1849   }
1851   return container.autorelease();
1854 - (NSView*)createSupervisedUserDisclaimerView {
1855   base::scoped_nsobject<NSView> container(
1856       [[NSView alloc] initWithFrame:NSZeroRect]);
1858   int yOffset = 0;
1859   int availableTextWidth = kFixedMenuWidth - 2 * kHorizontalSpacing;
1861   NSTextField* disclaimer = BuildLabel(
1862       base::SysUTF16ToNSString(avatarMenu_->GetSupervisedUserInformation()),
1863       NSMakePoint(kHorizontalSpacing, yOffset), nil);
1864   [disclaimer setFrameSize:NSMakeSize(availableTextWidth, 0)];
1865   [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:disclaimer];
1866   yOffset = NSMaxY([disclaimer frame]);
1868   [container addSubview:disclaimer];
1869   [container setFrameSize:NSMakeSize(kFixedMenuWidth, yOffset)];
1870   return container.autorelease();
1873 - (NSView*)createGuestProfileView {
1874   gfx::Image guestIcon =
1875       ui::ResourceBundle::GetSharedInstance().GetNativeImageNamed(
1876           profiles::GetPlaceholderAvatarIconResourceID());
1877   AvatarMenu::Item guestItem(std::string::npos, /* menu_index, not used */
1878                              std::string::npos, /* profile_index, not used */
1879                              guestIcon);
1880   guestItem.active = true;
1881   guestItem.name = base::SysNSStringToUTF16(
1882       l10n_util::GetNSString(IDS_PROFILES_GUEST_PROFILE_NAME));
1884   return [self createCurrentProfileView:guestItem];
1887 - (NSButton*)createOtherProfileView:(int)itemIndex {
1888   const AvatarMenu::Item& item = avatarMenu_->GetItemAt(itemIndex);
1890   NSRect rect = NSMakeRect(
1891       0, 0, kFixedMenuWidth, kBlueButtonHeight + kSmallVerticalSpacing);
1892   base::scoped_nsobject<BackgroundColorHoverButton> profileButton(
1893       [[BackgroundColorHoverButton alloc]
1894           initWithFrame:rect
1895       imageTitleSpacing:kImageTitleSpacing
1896         backgroundColor:GetDialogBackgroundColor()]);
1898   NSString* title = base::SysUTF16ToNSString(
1899       profiles::GetProfileSwitcherTextForItem(item));
1900   [profileButton setTitle:title];
1902   // Use the low-res, small default avatars in the fast user switcher, like
1903   // we do in the menu bar.
1904   gfx::Image itemIcon;
1905   bool isRectangle;
1906   AvatarMenu::GetImageForMenuButton(item.profile_path, &itemIcon, &isRectangle);
1908   [profileButton setDefaultImage:CreateProfileImage(
1909       itemIcon, kSmallImageSide).ToNSImage()];
1910   [profileButton setImagePosition:NSImageLeft];
1911   [profileButton setAlignment:NSLeftTextAlignment];
1912   [profileButton setBordered:NO];
1913   [profileButton setTag:itemIndex];
1914   [profileButton setTarget:self];
1915   [profileButton setAction:@selector(switchToProfile:)];
1917   NSSize textSize = [[profileButton title] sizeWithAttributes:@{
1918     NSFontAttributeName : [profileButton font]
1919   }];
1921   CGFloat availableWidth = rect.size.width - kSmallImageSide -
1922                            kImageTitleSpacing - kHorizontalSpacing;
1924   if (std::ceil(textSize.width) > availableWidth)
1925     [profileButton setToolTip:[profileButton title]];
1927   return profileButton.autorelease();
1930 - (NSView*)createOptionsViewWithRect:(NSRect)rect
1931                          displayLock:(BOOL)displayLock {
1932   NSRect viewRect = NSMakeRect(0, 0,
1933                                rect.size.width,
1934                                kBlueButtonHeight + kSmallVerticalSpacing);
1935   base::scoped_nsobject<NSView> container([[NSView alloc] initWithFrame:rect]);
1937   if (displayLock) {
1938     NSButton* lockButton =
1939         [self hoverButtonWithRect:viewRect
1940                              text:l10n_util::GetNSString(
1941                                   IDS_PROFILES_PROFILE_SIGNOUT_BUTTON)
1942                   imageResourceId:IDR_ICON_PROFILES_MENU_LOCK
1943                            action:@selector(lockProfile:)];
1944     [container addSubview:lockButton];
1945     viewRect.origin.y = NSMaxY([lockButton frame]);
1947     NSBox* separator = [self horizontalSeparatorWithFrame:viewRect];
1948     [container addSubview:separator];
1949     viewRect.origin.y = NSMaxY([separator frame]);
1950   }
1952   if ([self shouldShowGoIncognito]) {
1953     NSButton* goIncognitoButton =
1954         [self hoverButtonWithRect:viewRect
1955                              text:l10n_util::GetNSString(
1956                                   IDS_PROFILES_GO_INCOGNITO_BUTTON)
1957                   imageResourceId:IDR_ICON_PROFILES_MENU_INCOGNITO
1958                            action:@selector(goIncognito:)];
1959     viewRect.origin.y = NSMaxY([goIncognitoButton frame]);
1960     [container addSubview:goIncognitoButton];
1962     NSBox* separator = [self horizontalSeparatorWithFrame:viewRect];
1963     [container addSubview:separator];
1964     viewRect.origin.y = NSMaxY([separator frame]);
1965   }
1967   NSString* text = isGuestSession_ ?
1968       l10n_util::GetNSString(IDS_PROFILES_EXIT_GUEST) :
1969       l10n_util::GetNSString(IDS_PROFILES_SWITCH_USERS_BUTTON);
1970   NSButton* switchUsersButton =
1971       [self hoverButtonWithRect:viewRect
1972                            text:text
1973                 imageResourceId:IDR_ICON_PROFILES_MENU_AVATAR
1974                          action:isGuestSession_? @selector(exitGuest:) :
1975                                                  @selector(showUserManager:)];
1976   viewRect.origin.y = NSMaxY([switchUsersButton frame]);
1977   [container addSubview:switchUsersButton];
1979   [container setFrameSize:NSMakeSize(rect.size.width, viewRect.origin.y)];
1980   return container.autorelease();
1983 - (NSView*)createCurrentProfileAccountsView:(NSRect)rect {
1984   const CGFloat kAccountButtonHeight = 34;
1986   const AvatarMenu::Item& item =
1987       avatarMenu_->GetItemAt(avatarMenu_->GetActiveProfileIndex());
1988   DCHECK(item.signed_in);
1990   NSColor* backgroundColor = gfx::SkColorToCalibratedNSColor(
1991       profiles::kAvatarBubbleAccountsBackgroundColor);
1992   base::scoped_nsobject<NSView> container([[BackgroundColorView alloc]
1993       initWithFrame:rect
1994           withColor:backgroundColor]);
1996   rect.origin.y = 0;
1997   if (!browser_->profile()->IsSupervised()) {
1998     // Manually elide the button text so the contents fit inside the bubble.
1999     // This is needed because the BlueLabelButton cell resets the style on
2000     // every call to -cellSize, which prevents setting a custom lineBreakMode.
2001     NSString* elidedButtonText = base::SysUTF16ToNSString(gfx::ElideText(
2002         l10n_util::GetStringFUTF16(
2003             IDS_PROFILES_PROFILE_ADD_ACCOUNT_BUTTON, item.name),
2004         gfx::FontList(), rect.size.width, gfx::ELIDE_TAIL));
2006     NSButton* addAccountsButton =
2007         [self linkButtonWithTitle:elidedButtonText
2008                       frameOrigin:NSMakePoint(
2009             kHorizontalSpacing, kSmallVerticalSpacing)
2010                            action:@selector(addAccount:)];
2011     [container addSubview:addAccountsButton];
2012     rect.origin.y += kAccountButtonHeight;
2013   }
2015   NSView* accountEmails = [self createAccountsListWithRect:NSMakeRect(
2016       0, rect.origin.y, rect.size.width, kAccountButtonHeight)];
2017   [container addSubview:accountEmails];
2019   [container setFrameSize:NSMakeSize(rect.size.width,
2020                                      NSMaxY([accountEmails frame]))];
2021   return container.autorelease();
2024 - (NSView*)createAccountsListWithRect:(NSRect)rect {
2025   base::scoped_nsobject<NSView> container([[NSView alloc] initWithFrame:rect]);
2026   currentProfileAccounts_.clear();
2028   Profile* profile = browser_->profile();
2029   std::string primaryAccount =
2030       SigninManagerFactory::GetForProfile(profile)->GetAuthenticatedAccountId();
2031   DCHECK(!primaryAccount.empty());
2032   std::vector<std::string>accounts =
2033       profiles::GetSecondaryAccountsForProfile(profile, primaryAccount);
2035   // If there is an account with an authentication error, it needs to be
2036   // badged with a warning icon.
2037   std::string errorAccountId = GetAuthErrorAccountId(profile);
2039   rect.origin.y = 0;
2040   for (size_t i = 0; i < accounts.size(); ++i) {
2041     // Save the original email address, as the button text could be elided.
2042     currentProfileAccounts_[i] = accounts[i];
2043     NSButton* accountButton =
2044         [self accountButtonWithRect:rect
2045                          accountId:accounts[i]
2046                                 tag:i
2047                      reauthRequired:errorAccountId == accounts[i]];
2048     [container addSubview:accountButton];
2049     rect.origin.y = NSMaxY([accountButton frame]);
2050   }
2052   // The primary account should always be listed first.
2053   NSButton* accountButton =
2054       [self accountButtonWithRect:rect
2055                         accountId:primaryAccount
2056                               tag:kPrimaryProfileTag
2057                    reauthRequired:errorAccountId == primaryAccount];
2058   [container addSubview:accountButton];
2059   [container setFrameSize:NSMakeSize(NSWidth([container frame]),
2060                                      NSMaxY([accountButton frame]))];
2061   return container.autorelease();
2064 - (NSView*)buildGaiaEmbeddedView {
2065   base::scoped_nsobject<NSView> container(
2066       [[NSView alloc] initWithFrame:NSZeroRect]);
2067   CGFloat yOffset = 0;
2069   GURL url;
2070   int messageId = -1;
2071   switch (viewMode_) {
2072     case profiles::BUBBLE_VIEW_MODE_GAIA_SIGNIN:
2073       url = signin::GetPromoURL(signin_metrics::SOURCE_AVATAR_BUBBLE_SIGN_IN,
2074                                 false /* auto_close */,
2075                                 true /* is_constrained */);
2076       messageId = IDS_PROFILES_GAIA_SIGNIN_TITLE;
2077       break;
2078     case profiles::BUBBLE_VIEW_MODE_GAIA_ADD_ACCOUNT:
2079       url = signin::GetPromoURL(
2080           signin_metrics::SOURCE_AVATAR_BUBBLE_ADD_ACCOUNT,
2081           false /* auto_close */,
2082           true /* is_constrained */);
2083       messageId = IDS_PROFILES_GAIA_ADD_ACCOUNT_TITLE;
2084       break;
2085     case profiles::BUBBLE_VIEW_MODE_GAIA_REAUTH:
2086       DCHECK(HasAuthError(browser_->profile()));
2087       url = signin::GetReauthURL(
2088           browser_->profile(), GetAuthErrorAccountId(browser_->profile()));
2089       messageId = IDS_PROFILES_GAIA_REAUTH_TITLE;
2090       break;
2091     default:
2092       NOTREACHED() << "Called with invalid mode=" << viewMode_;
2093       break;
2094   }
2096   webContents_.reset(content::WebContents::Create(
2097       content::WebContents::CreateParams(browser_->profile())));
2099   webContentsDelegate_.reset(new GaiaWebContentsDelegate());
2100   webContents_->SetDelegate(webContentsDelegate_.get());
2101   webContents_->GetController().LoadURL(url,
2102                                         content::Referrer(),
2103                                         ui::PAGE_TRANSITION_AUTO_TOPLEVEL,
2104                                         std::string());
2105   NSView* webview = webContents_->GetNativeView();
2106   [webview setFrameSize:NSMakeSize(kFixedGaiaViewWidth, kFixedGaiaViewHeight)];
2107   [container addSubview:webview];
2108   content::RenderWidgetHostView* rwhv = webContents_->GetRenderWidgetHostView();
2109   if (rwhv)
2110     rwhv->SetBackgroundColor(profiles::kAvatarBubbleGaiaBackgroundColor);
2111   yOffset = NSMaxY([webview frame]);
2113   // Adds the title card.
2114   NSBox* separator = [self horizontalSeparatorWithFrame:
2115       NSMakeRect(0, yOffset, kFixedGaiaViewWidth, 0)];
2116   [container addSubview:separator];
2117   yOffset = NSMaxY([separator frame]) + kVerticalSpacing;
2119   NSView* titleView = BuildTitleCard(
2120       NSMakeRect(0, yOffset, kFixedGaiaViewWidth, 0),
2121       l10n_util::GetStringUTF16(messageId),
2122       self /* backButtonTarget*/,
2123       @selector(navigateBackFromSigninPage:) /* backButtonAction */);
2124   [container addSubview:titleView];
2125   yOffset = NSMaxY([titleView frame]);
2127   [container setFrameSize:NSMakeSize(kFixedGaiaViewWidth, yOffset)];
2128   return container.autorelease();
2131 - (NSView*)buildAccountRemovalView {
2132   DCHECK(!accountIdToRemove_.empty());
2134   base::scoped_nsobject<NSView> container(
2135       [[NSView alloc] initWithFrame:NSZeroRect]);
2136   CGFloat availableWidth =
2137       kFixedAccountRemovalViewWidth - 2 * kHorizontalSpacing;
2138   CGFloat yOffset = kVerticalSpacing;
2140   const std::string& primaryAccount = SigninManagerFactory::GetForProfile(
2141       browser_->profile())->GetAuthenticatedAccountId();
2142   bool isPrimaryAccount = primaryAccount == accountIdToRemove_;
2144   // Adds "remove account" button at the bottom if needed.
2145   if (!isPrimaryAccount) {
2146     base::scoped_nsobject<NSButton> removeAccountButton(
2147         [[BlueLabelButton alloc] initWithFrame:NSZeroRect]);
2148     [removeAccountButton setTitle:l10n_util::GetNSString(
2149         IDS_PROFILES_ACCOUNT_REMOVAL_BUTTON)];
2150     [removeAccountButton setTarget:self];
2151     [removeAccountButton setAction:@selector(removeAccount:)];
2152     [removeAccountButton sizeToFit];
2153     [removeAccountButton setAlignment:NSCenterTextAlignment];
2154     CGFloat xOffset = (kFixedAccountRemovalViewWidth -
2155         NSWidth([removeAccountButton frame])) / 2;
2156     [removeAccountButton setFrameOrigin:NSMakePoint(xOffset, yOffset)];
2157     [container addSubview:removeAccountButton];
2159     yOffset = NSMaxY([removeAccountButton frame]) + kVerticalSpacing;
2160   }
2162   NSView* contentView;
2163   NSPoint contentFrameOrigin = NSMakePoint(kHorizontalSpacing, yOffset);
2164   if (isPrimaryAccount) {
2165     std::string email = signin_ui_util::GetDisplayEmail(browser_->profile(),
2166                                                         accountIdToRemove_);
2167     std::vector<size_t> offsets;
2168     NSString* contentStr = l10n_util::GetNSStringF(
2169         IDS_PROFILES_PRIMARY_ACCOUNT_REMOVAL_TEXT,
2170         base::UTF8ToUTF16(email), base::string16(), &offsets);
2171     NSString* linkStr = l10n_util::GetNSString(IDS_PROFILES_SETTINGS_LINK);
2172     contentView = BuildFixedWidthTextViewWithLink(self, contentStr, linkStr,
2173         offsets[1], contentFrameOrigin, availableWidth);
2174   } else {
2175     NSString* contentStr =
2176         l10n_util::GetNSString(IDS_PROFILES_ACCOUNT_REMOVAL_TEXT);
2177     NSTextField* contentLabel = BuildLabel(contentStr, contentFrameOrigin, nil);
2178     [contentLabel setFrameSize:NSMakeSize(availableWidth, 0)];
2179     [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:contentLabel];
2180     contentView = contentLabel;
2181   }
2182   [container addSubview:contentView];
2183   yOffset = NSMaxY([contentView frame]) + kVerticalSpacing;
2185   // Adds the title card.
2186   NSBox* separator = [self horizontalSeparatorWithFrame:
2187       NSMakeRect(0, yOffset, kFixedAccountRemovalViewWidth, 0)];
2188   [container addSubview:separator];
2189   yOffset = NSMaxY([separator frame]) + kVerticalSpacing;
2191   NSView* titleView = BuildTitleCard(
2192       NSMakeRect(0, yOffset, kFixedAccountRemovalViewWidth,0),
2193       l10n_util::GetStringUTF16(IDS_PROFILES_ACCOUNT_REMOVAL_TITLE),
2194       self /* backButtonTarget*/,
2195       @selector(showAccountManagement:) /* backButtonAction */);
2196   [container addSubview:titleView];
2197   yOffset = NSMaxY([titleView frame]);
2199   [container setFrameSize:NSMakeSize(kFixedAccountRemovalViewWidth, yOffset)];
2200   return container.autorelease();
2203 - (NSView*)buildSwitchUserView {
2204   ProfileMetrics::LogProfileNewAvatarMenuNotYou(
2205       ProfileMetrics::PROFILE_AVATAR_MENU_NOT_YOU_VIEW);
2206   base::scoped_nsobject<NSView> container(
2207       [[NSView alloc] initWithFrame:NSZeroRect]);
2208   CGFloat availableWidth =
2209       kFixedSwitchUserViewWidth - 2 * kHorizontalSpacing;
2210   CGFloat yOffset = 0;
2211   NSRect viewRect = NSMakeRect(0, yOffset,
2212                                kFixedSwitchUserViewWidth,
2213                                kBlueButtonHeight + kSmallVerticalSpacing);
2215   const AvatarMenu::Item& avatarItem =
2216       avatarMenu_->GetItemAt(avatarMenu_->GetActiveProfileIndex());
2218   // Adds "Disconnect your Google Account" button at the bottom.
2219   NSButton* disconnectButton =
2220       [self hoverButtonWithRect:viewRect
2221                            text:l10n_util::GetNSString(
2222                                     IDS_PROFILES_DISCONNECT_BUTTON)
2223                 imageResourceId:IDR_ICON_PROFILES_MENU_DISCONNECT
2224                          action:@selector(disconnectProfile:)];
2225   [container addSubview:disconnectButton];
2226   yOffset = NSMaxY([disconnectButton frame]);
2228   NSBox* separator = [self horizontalSeparatorWithFrame:
2229       NSMakeRect(0, yOffset, kFixedSwitchUserViewWidth, 0)];
2230   [container addSubview:separator];
2231   yOffset = NSMaxY([separator frame]);
2233   // Adds "Add person" button.
2234   viewRect.origin.y = yOffset;
2235   NSButton* addPersonButton =
2236       [self hoverButtonWithRect:viewRect
2237                            text:l10n_util::GetNSString(
2238                                     IDS_PROFILES_ADD_PERSON_BUTTON)
2239                 imageResourceId:IDR_ICON_PROFILES_MENU_AVATAR
2240                          action:@selector(showUserManager:)];
2241   [container addSubview:addPersonButton];
2242   yOffset = NSMaxY([addPersonButton frame]);
2244   separator = [self horizontalSeparatorWithFrame:
2245       NSMakeRect(0, yOffset, kFixedSwitchUserViewWidth, 0)];
2246   [container addSubview:separator];
2247   yOffset = NSMaxY([separator frame]);
2249   // Adds the content text.
2250   base::string16 elidedName(gfx::ElideText(
2251       avatarItem.name, gfx::FontList(), availableWidth, gfx::ELIDE_TAIL));
2252   NSTextField* contentLabel = BuildLabel(
2253       l10n_util::GetNSStringF(IDS_PROFILES_NOT_YOU_CONTENT_TEXT, elidedName),
2254       NSMakePoint(kHorizontalSpacing, yOffset + kVerticalSpacing),
2255       nil);
2256   [contentLabel setFrameSize:NSMakeSize(availableWidth, 0)];
2257   [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:contentLabel];
2258   [container addSubview:contentLabel];
2259   yOffset = NSMaxY([contentLabel frame]) + kVerticalSpacing;
2261   // Adds the title card.
2262   separator = [self horizontalSeparatorWithFrame:
2263       NSMakeRect(0, yOffset, kFixedSwitchUserViewWidth, 0)];
2264   [container addSubview:separator];
2265   yOffset = NSMaxY([separator frame]) + kVerticalSpacing;
2267   NSView* titleView = BuildTitleCard(
2268       NSMakeRect(0, yOffset, kFixedSwitchUserViewWidth,0),
2269       l10n_util::GetStringFUTF16(IDS_PROFILES_NOT_YOU, avatarItem.name),
2270       self /* backButtonTarget*/,
2271       @selector(navigateBackFromSwitchUserView:) /* backButtonAction */);
2272   [container addSubview:titleView];
2273   yOffset = NSMaxY([titleView frame]);
2275   [container setFrameSize:NSMakeSize(kFixedSwitchUserViewWidth, yOffset)];
2276   return container.autorelease();
2279 // Called when clicked on the settings link.
2280 - (BOOL)textView:(NSTextView*)textView
2281    clickedOnLink:(id)link
2282          atIndex:(NSUInteger)charIndex {
2283   chrome::ShowSettings(browser_);
2284   return YES;
2287 - (NSButton*)hoverButtonWithRect:(NSRect)rect
2288                             text:(NSString*)text
2289                  imageResourceId:(int)imageResourceId
2290                           action:(SEL)action {
2291   base::scoped_nsobject<BackgroundColorHoverButton> button(
2292       [[BackgroundColorHoverButton alloc]
2293           initWithFrame:rect
2294       imageTitleSpacing:kImageTitleSpacing
2295         backgroundColor:GetDialogBackgroundColor()]);
2297   [button setTitle:text];
2298   ui::ResourceBundle* rb = &ui::ResourceBundle::GetSharedInstance();
2299   NSImage* image = rb->GetNativeImageNamed(imageResourceId).ToNSImage();
2300   [button setDefaultImage:image];
2301   [button setHoverImage:image];
2302   [button setPressedImage:image];
2303   [button setImagePosition:NSImageLeft];
2304   [button setAlignment:NSLeftTextAlignment];
2305   [button setBordered:NO];
2306   [button setTarget:self];
2307   [button setAction:action];
2309   return button.autorelease();
2312 - (NSButton*)linkButtonWithTitle:(NSString*)title
2313                      frameOrigin:(NSPoint)frameOrigin
2314                           action:(SEL)action {
2315   base::scoped_nsobject<NSButton> link(
2316       [[HyperlinkButtonCell buttonWithString:title] retain]);
2318   [[link cell] setShouldUnderline:NO];
2319   [[link cell] setTextColor:gfx::SkColorToCalibratedNSColor(
2320       chrome_style::GetLinkColor())];
2321   [link setTitle:title];
2322   [link setBordered:NO];
2323   [link setFont:[NSFont labelFontOfSize:kTextFontSize]];
2324   [link setTarget:self];
2325   [link setAction:action];
2326   [link setFrameOrigin:frameOrigin];
2327   [link sizeToFit];
2329   return link.autorelease();
2332 - (NSButton*)accountButtonWithRect:(NSRect)rect
2333                          accountId:(const std::string&)accountId
2334                                tag:(int)tag
2335                     reauthRequired:(BOOL)reauthRequired {
2336   // Get display email address for account.
2337   std::string email = signin_ui_util::GetDisplayEmail(browser_->profile(),
2338                                                       accountId);
2340   ui::ResourceBundle* rb = &ui::ResourceBundle::GetSharedInstance();
2341   NSImage* deleteImage = rb->GetNativeImageNamed(IDR_CLOSE_1).ToNSImage();
2342   CGFloat deleteImageWidth = [deleteImage size].width;
2343   NSImage* warningImage = reauthRequired ? rb->GetNativeImageNamed(
2344       IDR_ICON_PROFILES_ACCOUNT_BUTTON_ERROR).ToNSImage() : nil;
2345   CGFloat warningImageWidth = [warningImage size].width;
2347   CGFloat availableTextWidth = rect.size.width - kHorizontalSpacing -
2348       warningImageWidth - deleteImageWidth;
2349   if (warningImage)
2350     availableTextWidth -= kHorizontalSpacing;
2352   NSColor* backgroundColor = gfx::SkColorToCalibratedNSColor(
2353       profiles::kAvatarBubbleAccountsBackgroundColor);
2354   base::scoped_nsobject<BackgroundColorHoverButton> button(
2355       [[BackgroundColorHoverButton alloc] initWithFrame:rect
2356                                       imageTitleSpacing:0
2357                                         backgroundColor:backgroundColor]);
2358   [button setTitle:ElideEmail(email, availableTextWidth)];
2359   [button setAlignment:NSLeftTextAlignment];
2360   [button setBordered:NO];
2361   if (reauthRequired) {
2362     [button setDefaultImage:warningImage];
2363     [button setImagePosition:NSImageLeft];
2364     [button setTarget:self];
2365     [button setAction:@selector(showAccountReauthenticationView:)];
2366     [button setTag:tag];
2367   }
2369   // Delete button.
2370   if (!browser_->profile()->IsSupervised()) {
2371     NSRect buttonRect;
2372     NSDivideRect(rect, &buttonRect, &rect,
2373         deleteImageWidth + kHorizontalSpacing, NSMaxXEdge);
2374     buttonRect.origin.y = 0;
2376     base::scoped_nsobject<HoverImageButton> deleteButton(
2377         [[HoverImageButton alloc] initWithFrame:buttonRect]);
2378     [deleteButton setBordered:NO];
2379     [deleteButton setDefaultImage:deleteImage];
2380     [deleteButton setHoverImage:rb->GetNativeImageNamed(
2381         IDR_CLOSE_1_H).ToNSImage()];
2382     [deleteButton setPressedImage:rb->GetNativeImageNamed(
2383         IDR_CLOSE_1_P).ToNSImage()];
2384     [deleteButton setTarget:self];
2385     [deleteButton setAction:@selector(showAccountRemovalView:)];
2386     [deleteButton setTag:tag];
2388     [button addSubview:deleteButton];
2389   }
2391   return button.autorelease();
2394 - (void)postActionPerformed:(ProfileMetrics::ProfileDesktopMenu)action {
2395   ProfileMetrics::LogProfileDesktopMenu(action, serviceType_);
2396   serviceType_ = signin::GAIA_SERVICE_TYPE_NONE;
2399 - (bool)shouldShowGoIncognito {
2400   bool incognitoAvailable =
2401       IncognitoModePrefs::GetAvailability(browser_->profile()->GetPrefs()) !=
2402           IncognitoModePrefs::DISABLED;
2403   return incognitoAvailable && !browser_->profile()->IsGuestSession();
2406 @end