Check USB device path access when prompting users to select a device.
[chromium-blink-merge.git] / chrome / browser / ui / cocoa / profiles / profile_chooser_controller.mm
blob549b13b8542aeebef8661612803cec4fc10f2462
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 <Cocoa/Cocoa.h>
6 #import <Carbon/Carbon.h>  // kVK_Return.
8 #import "chrome/browser/ui/cocoa/profiles/profile_chooser_controller.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/profile_oauth2_token_service_factory.h"
29 #include "chrome/browser/signin/signin_header_helper.h"
30 #include "chrome/browser/signin/signin_manager_factory.h"
31 #include "chrome/browser/signin/signin_promo.h"
32 #include "chrome/browser/signin/signin_ui_util.h"
33 #include "chrome/browser/ui/browser.h"
34 #include "chrome/browser/ui/browser_commands.h"
35 #include "chrome/browser/ui/browser_window.h"
36 #include "chrome/browser/ui/chrome_pages.h"
37 #include "chrome/browser/ui/chrome_style.h"
38 #import "chrome/browser/ui/cocoa/browser_window_utils.h"
39 #import "chrome/browser/ui/cocoa/info_bubble_view.h"
40 #import "chrome/browser/ui/cocoa/info_bubble_window.h"
41 #import "chrome/browser/ui/cocoa/profiles/user_manager_mac.h"
42 #include "chrome/browser/ui/singleton_tabs.h"
43 #include "chrome/browser/ui/user_manager.h"
44 #include "chrome/browser/ui/webui/signin/login_ui_service.h"
45 #include "chrome/browser/ui/webui/signin/login_ui_service_factory.h"
46 #include "chrome/common/pref_names.h"
47 #include "chrome/common/url_constants.h"
48 #include "chrome/grit/chromium_strings.h"
49 #include "chrome/grit/generated_resources.h"
50 #include "components/signin/core/browser/mutable_profile_oauth2_token_service.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       profiles::GetSigninErrorController(profile);
251   return error_controller && error_controller->HasError();
254 }  // namespace
256 // Custom WebContentsDelegate that allows handling of hotkeys and suppresses
257 // the context menu.
258 class GaiaWebContentsDelegate : public content::WebContentsDelegate {
259  public:
260   GaiaWebContentsDelegate() {}
261   ~GaiaWebContentsDelegate() override {}
263  private:
264   // content::WebContentsDelegate:
265   bool HandleContextMenu(const content::ContextMenuParams& params) override;
266   void HandleKeyboardEvent(
267       content::WebContents* source,
268       const content::NativeWebKeyboardEvent& event) override;
270   DISALLOW_COPY_AND_ASSIGN(GaiaWebContentsDelegate);
273 bool GaiaWebContentsDelegate::HandleContextMenu(
274     const content::ContextMenuParams& params) {
275   // Suppresses the context menu because some features, such as inspecting
276   // elements, are not appropriate in a bubble.
277   return true;
280 void GaiaWebContentsDelegate::HandleKeyboardEvent(
281     content::WebContents* source,
282     const content::NativeWebKeyboardEvent& event) {
283   if (![BrowserWindowUtils shouldHandleKeyboardEvent:event])
284     return;
286   int chrome_command_id = [BrowserWindowUtils getCommandId:event];
288   bool is_text_editing_command =
289       (event.modifiers & blink::WebInputEvent::MetaKey) &&
290       (event.windowsKeyCode == ui::VKEY_A ||
291        event.windowsKeyCode == ui::VKEY_V);
293   // TODO(guohui): maybe should add an accelerator for the back button.
294   if (chrome_command_id == IDC_CLOSE_WINDOW || chrome_command_id == IDC_EXIT ||
295       is_text_editing_command) {
296     [[NSApp mainMenu] performKeyEquivalent:event.os_event];
297   }
300 // Class that listens to changes to the OAuth2Tokens for the active profile,
301 // changes to the avatar menu model or browser close notifications.
302 class ActiveProfileObserverBridge : public AvatarMenuObserver,
303                                     public content::NotificationObserver,
304                                     public OAuth2TokenService::Observer {
305  public:
306   ActiveProfileObserverBridge(ProfileChooserController* controller,
307                               Browser* browser)
308       : controller_(controller),
309         browser_(browser),
310         token_observer_registered_(false) {
311     registrar_.Add(this, chrome::NOTIFICATION_BROWSER_CLOSING,
312                    content::NotificationService::AllSources());
313     if (!browser_->profile()->IsGuestSession())
314       AddTokenServiceObserver();
315   }
317   ~ActiveProfileObserverBridge() override { RemoveTokenServiceObserver(); }
319  private:
320   void AddTokenServiceObserver() {
321     ProfileOAuth2TokenService* oauth2_token_service =
322         ProfileOAuth2TokenServiceFactory::GetForProfile(browser_->profile());
323     DCHECK(oauth2_token_service);
324     oauth2_token_service->AddObserver(this);
325     token_observer_registered_ = true;
326   }
328   void RemoveTokenServiceObserver() {
329     if (!token_observer_registered_)
330       return;
331     DCHECK(browser_->profile());
332     ProfileOAuth2TokenService* oauth2_token_service =
333         ProfileOAuth2TokenServiceFactory::GetForProfile(browser_->profile());
334     DCHECK(oauth2_token_service);
335     oauth2_token_service->RemoveObserver(this);
336     token_observer_registered_ = false;
337   }
339   // OAuth2TokenService::Observer:
340   void OnRefreshTokenAvailable(const std::string& account_id) override {
341     // Tokens can only be added by adding an account through the inline flow,
342     // which is started from the account management view. Refresh it to show the
343     // update.
344     profiles::BubbleViewMode viewMode = [controller_ viewMode];
345     if (viewMode == profiles::BUBBLE_VIEW_MODE_ACCOUNT_MANAGEMENT ||
346         viewMode == profiles::BUBBLE_VIEW_MODE_GAIA_ADD_ACCOUNT ||
347         viewMode == profiles::BUBBLE_VIEW_MODE_GAIA_REAUTH) {
348       [controller_ initMenuContentsWithView:
349           switches::IsEnableAccountConsistency() ?
350               profiles::BUBBLE_VIEW_MODE_ACCOUNT_MANAGEMENT :
351               profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER];
352     }
353   }
355   void OnRefreshTokenRevoked(const std::string& account_id) override {
356     // Tokens can only be removed from the account management view. Refresh it
357     // to show the update.
358     if ([controller_ viewMode] == profiles::BUBBLE_VIEW_MODE_ACCOUNT_MANAGEMENT)
359       [controller_ initMenuContentsWithView:
360           profiles::BUBBLE_VIEW_MODE_ACCOUNT_MANAGEMENT];
361   }
363   // AvatarMenuObserver:
364   void OnAvatarMenuChanged(AvatarMenu* avatar_menu) override {
365     profiles::BubbleViewMode viewMode = [controller_ viewMode];
366     if (viewMode == profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER ||
367         viewMode == profiles::BUBBLE_VIEW_MODE_ACCOUNT_MANAGEMENT) {
368       [controller_ initMenuContentsWithView:viewMode];
369     }
370   }
372   // content::NotificationObserver:
373   void Observe(int type,
374                const content::NotificationSource& source,
375                const content::NotificationDetails& details) override {
376     DCHECK_EQ(chrome::NOTIFICATION_BROWSER_CLOSING, type);
377     if (browser_ == content::Source<Browser>(source).ptr()) {
378       RemoveTokenServiceObserver();
379       // Clean up the bubble's WebContents (used by the Gaia embedded view), to
380       // make sure the guest profile doesn't have any dangling host renderers.
381       // This can happen if Chrome is quit using Command-Q while the bubble is
382       // still open, which won't give the bubble a chance to be closed and
383       // clean up the WebContents itself.
384       [controller_ cleanUpEmbeddedViewContents];
385     }
386   }
388   ProfileChooserController* controller_;  // Weak; owns this.
389   Browser* browser_;  // Weak.
390   content::NotificationRegistrar registrar_;
392   // The observer can be removed both when closing the browser, and by just
393   // closing the avatar bubble. However, in the case of closing the browser,
394   // the avatar bubble will also be closed afterwards, resulting in a second
395   // attempt to remove the observer. This ensures the observer is only
396   // removed once.
397   bool token_observer_registered_;
399   DISALLOW_COPY_AND_ASSIGN(ActiveProfileObserverBridge);
402 // Custom button cell that adds a left padding before the button image, and
403 // a custom spacing between the button image and title.
404 @interface CustomPaddingImageButtonCell : NSButtonCell {
405  @private
406   // Padding added to the left margin of the button.
407   int leftMarginSpacing_;
408   // Spacing between the cell image and title.
409   int imageTitleSpacing_;
412 - (id)initWithLeftMarginSpacing:(int)leftMarginSpacing
413               imageTitleSpacing:(int)imageTitleSpacing;
414 @end
416 @implementation CustomPaddingImageButtonCell
417 - (id)initWithLeftMarginSpacing:(int)leftMarginSpacing
418               imageTitleSpacing:(int)imageTitleSpacing {
419   if ((self = [super init])) {
420     leftMarginSpacing_ = leftMarginSpacing;
421     imageTitleSpacing_ = imageTitleSpacing;
422   }
423   return self;
426 - (NSRect)drawTitle:(NSAttributedString*)title
427           withFrame:(NSRect)frame
428              inView:(NSView*)controlView {
429   NSRect marginRect;
430   NSDivideRect(frame, &marginRect, &frame, leftMarginSpacing_, NSMinXEdge);
432   // The title frame origin isn't aware of the left margin spacing added
433   // in -drawImage, so it must be added when drawing the title as well.
434   if ([self imagePosition] == NSImageLeft)
435     NSDivideRect(frame, &marginRect, &frame, imageTitleSpacing_, NSMinXEdge);
437   return [super drawTitle:title withFrame:frame inView:controlView];
440 - (void)drawImage:(NSImage*)image
441         withFrame:(NSRect)frame
442            inView:(NSView*)controlView {
443   if ([self imagePosition] == NSImageLeft)
444     frame.origin.x = leftMarginSpacing_;
445   [super drawImage:image withFrame:frame inView:controlView];
448 - (NSSize)cellSize {
449   NSSize buttonSize = [super cellSize];
450   buttonSize.width += leftMarginSpacing_;
451   if ([self imagePosition] == NSImageLeft)
452     buttonSize.width += imageTitleSpacing_;
453   return buttonSize;
456 - (NSFocusRingType)focusRingType {
457   // This is taken care of by the custom drawing code.
458   return NSFocusRingTypeNone;
461 - (void)drawWithFrame:(NSRect)frame inView:(NSView *)controlView {
462   [super drawInteriorWithFrame:frame inView:controlView];
464   // Focus ring.
465   if ([self showsFirstResponder]) {
466     NSRect focusRingRect =
467         NSInsetRect(frame, kFocusRingLineWidth, kFocusRingLineWidth);
468     // TODO(noms): When we are targetting 10.7, we should change this to use
469     // -drawFocusRingMaskWithFrame instead.
470     [[[NSColor keyboardFocusIndicatorColor] colorWithAlphaComponent:1] set];
471     NSBezierPath* path = [NSBezierPath bezierPathWithRect:focusRingRect];
472     [path setLineWidth:kFocusRingLineWidth];
473     [path stroke];
474   }
477 @end
479 // A custom image view that has a transparent backround.
480 @interface TransparentBackgroundImageView : NSImageView
481 @end
483 @implementation TransparentBackgroundImageView
484 - (void)drawRect:(NSRect)dirtyRect {
485   NSColor* backgroundColor = [NSColor colorWithCalibratedWhite:1 alpha:0.6f];
486   [backgroundColor setFill];
487   NSRectFillUsingOperation(dirtyRect, NSCompositeSourceAtop);
488   [super drawRect:dirtyRect];
490 @end
492 @interface CustomCircleImageCell : NSButtonCell
493 @end
495 @implementation CustomCircleImageCell
496 - (void)drawWithFrame:(NSRect)frame inView:(NSView *)controlView {
497   // Display everything as a circle that spans the entire control.
498   NSBezierPath* path = [NSBezierPath bezierPathWithOvalInRect:frame];
499   [path addClip];
500   [super drawImage:[self image] withFrame:frame inView:controlView];
502 @end
504 // A custom image control that shows a "Change" button when moused over.
505 @interface EditableProfilePhoto : HoverImageButton {
506  @private
507   AvatarMenu* avatarMenu_;  // Weak; Owned by ProfileChooserController.
508   base::scoped_nsobject<TransparentBackgroundImageView> changePhotoImage_;
509   ProfileChooserController* controller_;
512 - (id)initWithFrame:(NSRect)frameRect
513          avatarMenu:(AvatarMenu*)avatarMenu
514         profileIcon:(const gfx::Image&)profileIcon
515      editingAllowed:(BOOL)editingAllowed
516      withController:(ProfileChooserController*)controller;
518 // Called when the "Change" button is clicked.
519 - (void)editPhoto:(id)sender;
521 @end
523 @implementation EditableProfilePhoto
524 - (id)initWithFrame:(NSRect)frameRect
525          avatarMenu:(AvatarMenu*)avatarMenu
526         profileIcon:(const gfx::Image&)profileIcon
527      editingAllowed:(BOOL)editingAllowed
528      withController:(ProfileChooserController*)controller {
529   if ((self = [super initWithFrame:frameRect])) {
530     avatarMenu_ = avatarMenu;
531     controller_ = controller;
533     [self setBordered:NO];
535     base::scoped_nsobject<CustomCircleImageCell> cell(
536         [[CustomCircleImageCell alloc] init]);
537     [self setCell:cell.get()];
539     [self setDefaultImage:CreateProfileImage(
540         profileIcon, kLargeImageSide).ToNSImage()];
541     [self setImagePosition:NSImageOnly];
543     NSRect bounds = NSMakeRect(0, 0, kLargeImageSide, kLargeImageSide);
544     if (editingAllowed) {
545       [self setTarget:self];
546       [self setAction:@selector(editPhoto:)];
547       changePhotoImage_.reset([[TransparentBackgroundImageView alloc]
548           initWithFrame:bounds]);
549       [changePhotoImage_ setImage:ui::ResourceBundle::GetSharedInstance().
550           GetNativeImageNamed(IDR_ICON_PROFILES_EDIT_CAMERA).AsNSImage()];
551       [self addSubview:changePhotoImage_];
553       // Hide the image until the button is hovered over.
554       [changePhotoImage_ setHidden:YES];
555     }
557     // Set the image cell's accessibility strings to be the same as the
558     // button's strings.
559     [[self cell] accessibilitySetOverrideValue:l10n_util::GetNSString(
560         editingAllowed ?
561         IDS_PROFILES_NEW_AVATAR_MENU_CHANGE_PHOTO_ACCESSIBLE_NAME :
562         IDS_PROFILES_NEW_AVATAR_MENU_PHOTO_ACCESSIBLE_NAME)
563                                   forAttribute:NSAccessibilityTitleAttribute];
564     [[self cell] accessibilitySetOverrideValue:
565         editingAllowed ? NSAccessibilityButtonRole : NSAccessibilityImageRole
566                                   forAttribute:NSAccessibilityRoleAttribute];
567     [[self cell] accessibilitySetOverrideValue:
568         NSAccessibilityRoleDescription(NSAccessibilityButtonRole, nil)
569             forAttribute:NSAccessibilityRoleDescriptionAttribute];
571     // The button and the cell should read the same thing.
572     [self accessibilitySetOverrideValue:l10n_util::GetNSString(
573         editingAllowed ?
574         IDS_PROFILES_NEW_AVATAR_MENU_CHANGE_PHOTO_ACCESSIBLE_NAME :
575         IDS_PROFILES_NEW_AVATAR_MENU_PHOTO_ACCESSIBLE_NAME)
576                                   forAttribute:NSAccessibilityTitleAttribute];
577     [self accessibilitySetOverrideValue:NSAccessibilityButtonRole
578                                   forAttribute:NSAccessibilityRoleAttribute];
579     [self accessibilitySetOverrideValue:
580         NSAccessibilityRoleDescription(NSAccessibilityButtonRole, nil)
581             forAttribute:NSAccessibilityRoleDescriptionAttribute];
582   }
583   return self;
586 - (void)editPhoto:(id)sender {
587   avatarMenu_->EditProfile(avatarMenu_->GetActiveProfileIndex());
588   [controller_
589       postActionPerformed:ProfileMetrics::PROFILE_DESKTOP_MENU_EDIT_IMAGE];
592 - (void)setHoverState:(HoverState)state {
593   [super setHoverState:state];
594   [changePhotoImage_ setHidden:([self hoverState] == kHoverStateNone)];
597 - (BOOL)canBecomeKeyView {
598   return false;
601 - (BOOL)accessibilityIsIgnored {
602   return NO;
605 - (NSArray*)accessibilityActionNames {
606   NSArray* parentActions = [super accessibilityActionNames];
607   return [parentActions arrayByAddingObject:NSAccessibilityPressAction];
610 - (void)accessibilityPerformAction:(NSString*)action {
611   if ([action isEqualToString:NSAccessibilityPressAction]) {
612     avatarMenu_->EditProfile(avatarMenu_->GetActiveProfileIndex());
613   }
615   [super accessibilityPerformAction:action];
618 @end
620 // A custom text control that turns into a textfield for editing when clicked.
621 @interface EditableProfileNameButton : HoverImageButton<NSTextFieldDelegate> {
622  @private
623   base::scoped_nsobject<NSTextField> profileNameTextField_;
624   Profile* profile_;  // Weak.
625   ProfileChooserController* controller_;
628 - (id)initWithFrame:(NSRect)frameRect
629             profile:(Profile*)profile
630         profileName:(NSString*)profileName
631      editingAllowed:(BOOL)editingAllowed
632      withController:(ProfileChooserController*)controller;
634 // Called when the button is clicked.
635 - (void)showEditableView:(id)sender;
637 // Called when enter is pressed in the text field.
638 - (void)saveProfileName;
640 @end
642 @implementation EditableProfileNameButton
643 - (id)initWithFrame:(NSRect)frameRect
644             profile:(Profile*)profile
645         profileName:(NSString*)profileName
646      editingAllowed:(BOOL)editingAllowed
647      withController:(ProfileChooserController*)controller {
648   if ((self = [super initWithFrame:frameRect])) {
649     profile_ = profile;
650     controller_ = controller;
652     if (editingAllowed) {
653       // Show an "edit" pencil icon when hovering over. In the default state,
654       // we need to create an empty placeholder of the correct size, so that
655       // the text doesn't jump around when the hovered icon appears.
656       ui::ResourceBundle* rb = &ui::ResourceBundle::GetSharedInstance();
657       NSImage* hoverImage = rb->GetNativeImageNamed(
658           IDR_ICON_PROFILES_EDIT_HOVER).AsNSImage();
660       // In order to center the button title, we need to add a left padding of
661       // the same width as the pencil icon.
662       base::scoped_nsobject<CustomPaddingImageButtonCell> cell(
663           [[CustomPaddingImageButtonCell alloc]
664               initWithLeftMarginSpacing:[hoverImage size].width
665                       imageTitleSpacing:0]);
666       [self setCell:cell.get()];
668       NSImage* placeholder = [[NSImage alloc] initWithSize:[hoverImage size]];
669       [self setDefaultImage:placeholder];
670       [self setHoverImage:hoverImage];
671       [self setAlternateImage:
672           rb->GetNativeImageNamed(IDR_ICON_PROFILES_EDIT_PRESSED).AsNSImage()];
673       [self setImagePosition:NSImageRight];
674       [self setTarget:self];
675       [self setAction:@selector(showEditableView:)];
677       // We need to subtract the width of the bezel from the frame rect, so that
678       // the textfield can take the exact same space as the button.
679       frameRect.size.height -= 2 * kBezelThickness;
680       frameRect.origin = NSMakePoint(0, kBezelThickness);
681       profileNameTextField_.reset(
682           [[NSTextField alloc] initWithFrame:frameRect]);
683       [profileNameTextField_ setStringValue:profileName];
684       [profileNameTextField_ setFont:[NSFont labelFontOfSize:kTitleFontSize]];
685       [profileNameTextField_ setEditable:YES];
686       [profileNameTextField_ setDrawsBackground:YES];
687       [profileNameTextField_ setBezeled:YES];
688       [profileNameTextField_ setAlignment:NSCenterTextAlignment];
689       [[profileNameTextField_ cell] setWraps:NO];
690       [[profileNameTextField_ cell] setLineBreakMode:
691           NSLineBreakByTruncatingTail];
692       [[profileNameTextField_ cell] setUsesSingleLineMode:YES];
693       [self addSubview:profileNameTextField_];
694       [profileNameTextField_ setDelegate:self];
696       // Hide the textfield until the user clicks on the button.
697       [profileNameTextField_ setHidden:YES];
699       [[self cell] accessibilitySetOverrideValue:l10n_util::GetNSStringF(
700           IDS_PROFILES_NEW_AVATAR_MENU_EDIT_NAME_ACCESSIBLE_NAME,
701           base::SysNSStringToUTF16(profileName))
702                                     forAttribute:NSAccessibilityTitleAttribute];
704       NSSize textSize = [profileName sizeWithAttributes:@{
705         NSFontAttributeName : [profileNameTextField_ font]
706       }];
708       if (textSize.width > frameRect.size.width - [hoverImage size].width * 2)
709         [self setToolTip:profileName];
710     }
712     [[self cell] accessibilitySetOverrideValue:NSAccessibilityButtonRole
713                                   forAttribute:NSAccessibilityRoleAttribute];
714     [[self cell]
715         accessibilitySetOverrideValue:NSAccessibilityRoleDescription(
716                                           NSAccessibilityButtonRole, nil)
717                          forAttribute:NSAccessibilityRoleDescriptionAttribute];
719     [self setBordered:NO];
720     [self setFont:[NSFont labelFontOfSize:kTitleFontSize]];
721     [self setAlignment:NSCenterTextAlignment];
722     [[self cell] setLineBreakMode:NSLineBreakByTruncatingTail];
723     [self setTitle:profileName];
724   }
725   return self;
728 - (void)saveProfileName {
729   base::string16 newProfileName =
730       base::SysNSStringToUTF16([profileNameTextField_ stringValue]);
732   // Empty profile names are not allowed, and do nothing.
733   base::TrimWhitespace(newProfileName, base::TRIM_ALL, &newProfileName);
734   if (!newProfileName.empty()) {
735     profiles::UpdateProfileName(profile_, newProfileName);
736     [controller_
737         postActionPerformed:ProfileMetrics::PROFILE_DESKTOP_MENU_EDIT_NAME];
738     [profileNameTextField_ setHidden:YES];
739   }
742 - (void)showEditableView:(id)sender {
743   [profileNameTextField_ setHidden:NO];
744   [[self window] makeFirstResponder:profileNameTextField_];
747 - (BOOL)canBecomeKeyView {
748   return false;
751 - (BOOL)control:(NSControl*)control
752                textView:(NSTextView*)textView
753     doCommandBySelector:(SEL)commandSelector {
754   if (commandSelector == @selector(insertTab:) ||
755       commandSelector == @selector(insertNewline:)) {
756     [self saveProfileName];
757     return YES;
758   }
759   return NO;
762 @end
764 // A custom button that allows for setting a background color when hovered over.
765 @interface BackgroundColorHoverButton : HoverImageButton {
766  @private
767   base::scoped_nsobject<NSColor> backgroundColor_;
768   base::scoped_nsobject<NSColor> hoverColor_;
770 @end
772 @implementation BackgroundColorHoverButton
774 - (id)initWithFrame:(NSRect)frameRect
775   imageTitleSpacing:(int)imageTitleSpacing
776     backgroundColor:(NSColor*)backgroundColor {
777   if ((self = [super initWithFrame:frameRect])) {
778     backgroundColor_.reset([backgroundColor retain]);
779     // Use a color from the common theme, since this button is not trying to
780     // look like a native control.
781     SkColor hoverColor;
782     bool found = ui::CommonThemeGetSystemColor(
783         ui::NativeTheme::kColorId_ButtonHoverBackgroundColor, &hoverColor);
784     DCHECK(found);
785     hoverColor_.reset([gfx::SkColorToSRGBNSColor(hoverColor) retain]);
787     [self setBordered:NO];
788     [self setFont:[NSFont labelFontOfSize:kTextFontSize]];
789     [self setButtonType:NSMomentaryChangeButton];
791     base::scoped_nsobject<CustomPaddingImageButtonCell> cell(
792         [[CustomPaddingImageButtonCell alloc]
793             initWithLeftMarginSpacing:kHorizontalSpacing
794                     imageTitleSpacing:imageTitleSpacing]);
795     [cell setLineBreakMode:NSLineBreakByTruncatingTail];
796     [self setCell:cell.get()];
797   }
798   return self;
801 - (void)setHoverState:(HoverState)state {
802   [super setHoverState:state];
803   bool isHighlighted = ([self hoverState] != kHoverStateNone);
805   NSColor* backgroundColor = isHighlighted ? hoverColor_ : backgroundColor_;
806   [[self cell] setBackgroundColor:backgroundColor];
809 -(void)keyDown:(NSEvent*)event {
810   // Since there is no default button in the bubble, it is safe to activate
811   // all buttons on Enter as well, and be consistent with the Windows
812   // implementation.
813   if ([event keyCode] == kVK_Return)
814     [self performClick:self];
815   else
816     [super keyDown:event];
819 - (BOOL)canBecomeKeyView {
820   return YES;
823 @end
825 // A custom view with the given background color.
826 @interface BackgroundColorView : NSView {
827  @private
828   base::scoped_nsobject<NSColor> backgroundColor_;
830 @end
832 @implementation BackgroundColorView
833 - (id)initWithFrame:(NSRect)frameRect
834           withColor:(NSColor*)color {
835   if ((self = [super initWithFrame:frameRect]))
836     backgroundColor_.reset([color retain]);
837   return self;
840 - (void)drawRect:(NSRect)dirtyRect {
841   [backgroundColor_ setFill];
842   NSRectFill(dirtyRect);
843   [super drawRect:dirtyRect];
845 @end
847 // A custom dummy button that is used to clear focus from the bubble's controls.
848 @interface DummyWindowFocusButton : NSButton
849 @end
851 @implementation DummyWindowFocusButton
852 // Ignore accessibility, as this is a placeholder button.
853 - (BOOL)accessibilityIsIgnored {
854   return YES;
857 - (id)accessibilityAttributeValue:(NSString*)attribute {
858   return nil;
861 - (BOOL)canBecomeKeyView {
862   return NO;
865 @end
867 @interface ProfileChooserController ()
868 // Builds the profile chooser view.
869 - (NSView*)buildProfileChooserView;
871 // Builds a tutorial card with a title label using |titleMessage|, a content
872 // label using |contentMessage|, a link using |linkMessage|, and a button using
873 // |buttonMessage|. If |stackButton| is YES, places the button above the link.
874 // Otherwise places both on the same row with the link left aligned and button
875 // right aligned. On click, the link would execute |linkAction|, and the button
876 // would execute |buttonAction|. It sets |tutorialMode_| to the given |mode|.
877 - (NSView*)tutorialViewWithMode:(profiles::TutorialMode)mode
878                    titleMessage:(NSString*)titleMessage
879                  contentMessage:(NSString*)contentMessage
880                     linkMessage:(NSString*)linkMessage
881                   buttonMessage:(NSString*)buttonMessage
882                     stackButton:(BOOL)stackButton
883                  hasCloseButton:(BOOL)hasCloseButton
884                      linkAction:(SEL)linkAction
885                    buttonAction:(SEL)buttonAction;
887 // Builds a tutorial card to introduce an upgrade user to the new avatar menu if
888 // needed. |tutorial_shown| indicates if the tutorial has already been shown in
889 // the previous active view. |avatar_item| refers to the current profile.
890 - (NSView*)buildWelcomeUpgradeTutorialViewIfNeeded;
892 // Builds a tutorial card to have the user confirm the last Chrome signin,
893 // Chrome sync will be delayed until the user either dismisses the tutorial, or
894 // configures sync through the "Settings" link.
895 - (NSView*)buildSigninConfirmationView;
897 // Builds a tutorial card to show the last signin error.
898 - (NSView*)buildSigninErrorView;
900 // Creates the main profile card for the profile |item| at the top of
901 // the bubble.
902 - (NSView*)createCurrentProfileView:(const AvatarMenu::Item&)item;
904 // Creates the possible links for the main profile card with profile |item|.
905 - (NSView*)createCurrentProfileLinksForItem:(const AvatarMenu::Item&)item
906                                        rect:(NSRect)rect;
908 // Creates the disclaimer text for supervised users, telling them that the
909 // manager can view their history etc.
910 - (NSView*)createSupervisedUserDisclaimerView;
912 // Creates a main profile card for the guest user.
913 - (NSView*)createGuestProfileView;
915 // Creates an item for the profile |itemIndex| that is used in the fast profile
916 // switcher in the middle of the bubble.
917 - (NSButton*)createOtherProfileView:(int)itemIndex;
919 // Creates the "Not you" and Lock option buttons.
920 - (NSView*)createOptionsViewWithRect:(NSRect)rect
921                          displayLock:(BOOL)displayLock;
923 // Creates the account management view for the active profile.
924 - (NSView*)createCurrentProfileAccountsView:(NSRect)rect;
926 // Creates the list of accounts for the active profile.
927 - (NSView*)createAccountsListWithRect:(NSRect)rect;
929 // Creates the Gaia sign-in/add account view.
930 - (NSView*)buildGaiaEmbeddedView;
932 // Creates the account removal view.
933 - (NSView*)buildAccountRemovalView;
935 // Create a view that shows various options for an upgrade user who is not
936 // the same person as the currently signed in user.
937 - (NSView*)buildSwitchUserView;
939 // Creates a button with |text|, an icon given by |imageResourceId| and with
940 // |action|.
941 - (NSButton*)hoverButtonWithRect:(NSRect)rect
942                             text:(NSString*)text
943                  imageResourceId:(int)imageResourceId
944                           action:(SEL)action;
946 // Creates a generic link button with |title| and an |action| positioned at
947 // |frameOrigin|.
948 - (NSButton*)linkButtonWithTitle:(NSString*)title
949                      frameOrigin:(NSPoint)frameOrigin
950                           action:(SEL)action;
952 // Creates an email account button with |title| and a remove icon. If
953 // |reauthRequired| is true, the button also displays a warning icon. |tag|
954 // indicates which account the button refers to.
955 - (NSButton*)accountButtonWithRect:(NSRect)rect
956                          accountId:(const std::string&)accountId
957                                tag:(int)tag
958                     reauthRequired:(BOOL)reauthRequired;
960 - (bool)shouldShowGoIncognito;
961 @end
963 @implementation ProfileChooserController
964 - (profiles::BubbleViewMode) viewMode {
965   return viewMode_;
968 - (void)setTutorialMode:(profiles::TutorialMode)tutorialMode {
969   tutorialMode_ = tutorialMode;
972 - (IBAction)switchToProfile:(id)sender {
973   // Check the event flags to see if a new window should be created.
974   bool alwaysCreate = ui::WindowOpenDispositionFromNSEvent(
975       [NSApp currentEvent]) == NEW_WINDOW;
976   avatarMenu_->SwitchToProfile([sender tag], alwaysCreate,
977                                ProfileMetrics::SWITCH_PROFILE_ICON);
980 - (IBAction)showUserManager:(id)sender {
981   UserManager::Show(base::FilePath(),
982                     profiles::USER_MANAGER_NO_TUTORIAL,
983                     profiles::USER_MANAGER_SELECT_PROFILE_NO_ACTION);
984   [self postActionPerformed:
985       ProfileMetrics::PROFILE_DESKTOP_MENU_OPEN_USER_MANAGER];
988 - (IBAction)exitGuest:(id)sender {
989   DCHECK(browser_->profile()->IsGuestSession());
990   UserManager::Show(base::FilePath(),
991                     profiles::USER_MANAGER_NO_TUTORIAL,
992                     profiles::USER_MANAGER_SELECT_PROFILE_NO_ACTION);
993   profiles::CloseGuestProfileWindows();
996 - (IBAction)goIncognito:(id)sender {
997   DCHECK([self shouldShowGoIncognito]);
998   chrome::NewIncognitoWindow(browser_);
999   [self postActionPerformed:
1000       ProfileMetrics::PROFILE_DESKTOP_MENU_GO_INCOGNITO];
1003 - (IBAction)showAccountManagement:(id)sender {
1004   [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_ACCOUNT_MANAGEMENT];
1007 - (IBAction)hideAccountManagement:(id)sender {
1008   [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER];
1011 - (IBAction)lockProfile:(id)sender {
1012   profiles::LockProfile(browser_->profile());
1013   [self postActionPerformed:ProfileMetrics::PROFILE_DESKTOP_MENU_LOCK];
1016 - (IBAction)showInlineSigninPage:(id)sender {
1017   [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_GAIA_SIGNIN];
1020 - (IBAction)addAccount:(id)sender {
1021   [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_GAIA_ADD_ACCOUNT];
1022   [self postActionPerformed:ProfileMetrics::PROFILE_DESKTOP_MENU_ADD_ACCT];
1025 - (IBAction)navigateBackFromSigninPage:(id)sender {
1026   std::string primaryAccount = SigninManagerFactory::GetForProfile(
1027       browser_->profile())->GetAuthenticatedAccountId();
1028   bool hasAccountManagement = !primaryAccount.empty() &&
1029       switches::IsEnableAccountConsistency();
1030   [self initMenuContentsWithView:hasAccountManagement ?
1031       profiles::BUBBLE_VIEW_MODE_ACCOUNT_MANAGEMENT :
1032       profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER];
1035 - (IBAction)showAccountRemovalView:(id)sender {
1036   DCHECK(!isGuestSession_);
1038   // Tag is either |kPrimaryProfileTag| for the primary account, or equal to the
1039   // index in |currentProfileAccounts_| for a secondary account.
1040   int tag = [sender tag];
1041   if (tag == kPrimaryProfileTag) {
1042     accountIdToRemove_ = SigninManagerFactory::GetForProfile(
1043         browser_->profile())->GetAuthenticatedAccountId();
1044   } else {
1045     DCHECK(ContainsKey(currentProfileAccounts_, tag));
1046     accountIdToRemove_ = currentProfileAccounts_[tag];
1047   }
1049   [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_ACCOUNT_REMOVAL];
1052 - (IBAction)showAccountReauthenticationView:(id)sender {
1053   DCHECK(!isGuestSession_);
1054   [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_GAIA_REAUTH];
1057 - (IBAction)removeAccount:(id)sender {
1058   DCHECK(!accountIdToRemove_.empty());
1059   ProfileOAuth2TokenServiceFactory::GetPlatformSpecificForProfile(
1060       browser_->profile())->RevokeCredentials(accountIdToRemove_);
1061   [self postActionPerformed:ProfileMetrics::PROFILE_DESKTOP_MENU_REMOVE_ACCT];
1062   accountIdToRemove_.clear();
1064   [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_ACCOUNT_MANAGEMENT];
1067 - (IBAction)seeWhatsNew:(id)sender {
1068   UserManager::Show(base::FilePath(),
1069                     profiles::USER_MANAGER_TUTORIAL_OVERVIEW,
1070                     profiles::USER_MANAGER_SELECT_PROFILE_NO_ACTION);
1071   ProfileMetrics::LogProfileNewAvatarMenuUpgrade(
1072       ProfileMetrics::PROFILE_AVATAR_MENU_UPGRADE_WHATS_NEW);
1075 - (IBAction)showSwitchUserView:(id)sender {
1076   [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_SWITCH_USER];
1077   ProfileMetrics::LogProfileNewAvatarMenuUpgrade(
1078       ProfileMetrics::PROFILE_AVATAR_MENU_UPGRADE_NOT_YOU);
1081 - (IBAction)showLearnMorePage:(id)sender {
1082   signin_ui_util::ShowSigninErrorLearnMorePage(browser_->profile());
1085 - (IBAction)configureSyncSettings:(id)sender {
1086   tutorialMode_ = profiles::TUTORIAL_MODE_NONE;
1087   LoginUIServiceFactory::GetForProfile(browser_->profile())->
1088       SyncConfirmationUIClosed(true);
1089   ProfileMetrics::LogProfileNewAvatarMenuSignin(
1090       ProfileMetrics::PROFILE_AVATAR_MENU_SIGNIN_SETTINGS);
1093 - (IBAction)syncSettingsConfirmed:(id)sender {
1094   tutorialMode_ = profiles::TUTORIAL_MODE_NONE;
1095   LoginUIServiceFactory::GetForProfile(browser_->profile())->
1096       SyncConfirmationUIClosed(false);
1097   ProfileMetrics::LogProfileNewAvatarMenuSignin(
1098       ProfileMetrics::PROFILE_AVATAR_MENU_SIGNIN_OK);
1099   [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER];
1102 - (IBAction)disconnectProfile:(id)sender {
1103   chrome::ShowSettings(browser_);
1104   ProfileMetrics::LogProfileNewAvatarMenuNotYou(
1105       ProfileMetrics::PROFILE_AVATAR_MENU_NOT_YOU_DISCONNECT);
1108 - (IBAction)navigateBackFromSwitchUserView:(id)sender {
1109   [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER];
1110   ProfileMetrics::LogProfileNewAvatarMenuNotYou(
1111       ProfileMetrics::PROFILE_AVATAR_MENU_NOT_YOU_BACK);
1114 - (IBAction)dismissTutorial:(id)sender {
1115   // Never shows the upgrade tutorial again if manually closed.
1116   if (tutorialMode_ == profiles::TUTORIAL_MODE_WELCOME_UPGRADE) {
1117     browser_->profile()->GetPrefs()->SetInteger(
1118         prefs::kProfileAvatarTutorialShown,
1119         signin_ui_util::kUpgradeWelcomeTutorialShowMax + 1);
1120   }
1122   tutorialMode_ = profiles::TUTORIAL_MODE_NONE;
1123   [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER];
1126 - (void)windowWillClose:(NSNotification*)notification {
1127   if (tutorialMode_ == profiles::TUTORIAL_MODE_CONFIRM_SIGNIN) {
1128     LoginUIServiceFactory::GetForProfile(browser_->profile())->
1129         SyncConfirmationUIClosed(false);
1130   }
1132   [super windowWillClose:notification];
1135 - (void)moveDown:(id)sender {
1136   [[self window] selectNextKeyView:self];
1139 - (void)moveUp:(id)sender {
1140   [[self window] selectPreviousKeyView:self];
1143 - (void)cleanUpEmbeddedViewContents {
1144   webContents_.reset();
1145   webContentsDelegate_.reset();
1148 - (id)initWithBrowser:(Browser*)browser
1149            anchoredAt:(NSPoint)point
1150              viewMode:(profiles::BubbleViewMode)viewMode
1151          tutorialMode:(profiles::TutorialMode)tutorialMode
1152           serviceType:(signin::GAIAServiceType)serviceType {
1153   base::scoped_nsobject<InfoBubbleWindow> window([[InfoBubbleWindow alloc]
1154       initWithContentRect:ui::kWindowSizeDeterminedLater
1155                 styleMask:NSBorderlessWindowMask
1156                   backing:NSBackingStoreBuffered
1157                     defer:NO]);
1159   if ((self = [super initWithWindow:window
1160                        parentWindow:browser->window()->GetNativeWindow()
1161                          anchoredAt:point])) {
1162     browser_ = browser;
1163     viewMode_ = viewMode;
1164     tutorialMode_ = tutorialMode;
1165     observer_.reset(new ActiveProfileObserverBridge(self, browser_));
1166     serviceType_ = serviceType;
1168     avatarMenu_.reset(new AvatarMenu(
1169         &g_browser_process->profile_manager()->GetProfileInfoCache(),
1170         observer_.get(),
1171         browser_));
1172     avatarMenu_->RebuildMenu();
1174     // Guest profiles do not have a token service.
1175     isGuestSession_ = browser_->profile()->IsGuestSession();
1177     // If view mode is PROFILE_CHOOSER but there is an auth error, force
1178     // ACCOUNT_MANAGEMENT mode.
1179     if (viewMode_ == profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER &&
1180         HasAuthError(browser_->profile()) &&
1181         switches::IsEnableAccountConsistency() &&
1182         avatarMenu_->GetItemAt(avatarMenu_->GetActiveProfileIndex()).
1183             signed_in) {
1184       viewMode_ = profiles::BUBBLE_VIEW_MODE_ACCOUNT_MANAGEMENT;
1185     }
1187     [window accessibilitySetOverrideValue:
1188         l10n_util::GetNSString(IDS_PROFILES_NEW_AVATAR_MENU_ACCESSIBLE_NAME)
1189                              forAttribute:NSAccessibilityTitleAttribute];
1190     [window accessibilitySetOverrideValue:
1191         l10n_util::GetNSString(IDS_PROFILES_NEW_AVATAR_MENU_ACCESSIBLE_NAME)
1192                              forAttribute:NSAccessibilityHelpAttribute];
1194     [[self bubble] setAlignment:info_bubble::kAlignRightEdgeToAnchorEdge];
1195     [[self bubble] setArrowLocation:info_bubble::kNoArrow];
1196     [[self bubble] setBackgroundColor:GetDialogBackgroundColor()];
1197     [self initMenuContentsWithView:viewMode_];
1198   }
1200   return self;
1203 - (void)initMenuContentsWithView:(profiles::BubbleViewMode)viewToDisplay {
1204   if (browser_->profile()->IsSupervised() &&
1205       (viewToDisplay == profiles::BUBBLE_VIEW_MODE_GAIA_ADD_ACCOUNT ||
1206        viewToDisplay == profiles::BUBBLE_VIEW_MODE_ACCOUNT_REMOVAL)) {
1207     LOG(WARNING) << "Supervised user attempted to add/remove account";
1208     return;
1209   }
1210   viewMode_ = viewToDisplay;
1211   NSView* contentView = [[self window] contentView];
1212   [contentView setSubviews:[NSArray array]];
1213   NSView* subView;
1215   switch (viewMode_) {
1216     case profiles::BUBBLE_VIEW_MODE_GAIA_SIGNIN:
1217     case profiles::BUBBLE_VIEW_MODE_GAIA_ADD_ACCOUNT:
1218     case profiles::BUBBLE_VIEW_MODE_GAIA_REAUTH:
1219       subView = [self buildGaiaEmbeddedView];
1220       break;
1221     case profiles::BUBBLE_VIEW_MODE_ACCOUNT_REMOVAL:
1222       subView = [self buildAccountRemovalView];
1223       break;
1224     case profiles::BUBBLE_VIEW_MODE_SWITCH_USER:
1225       subView = [self buildSwitchUserView];
1226       break;
1227     case profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER:
1228     case profiles::BUBBLE_VIEW_MODE_FAST_PROFILE_CHOOSER:
1229     case profiles::BUBBLE_VIEW_MODE_ACCOUNT_MANAGEMENT:
1230       subView = [self buildProfileChooserView];
1231       break;
1232   }
1234   // Clears tutorial mode for all non-profile-chooser views.
1235   if (viewMode_ != profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER)
1236     tutorialMode_ = profiles::TUTORIAL_MODE_NONE;
1238   // Add a dummy, empty element so that we don't initially display any
1239   // focus rings.
1240   NSButton* dummyFocusButton =
1241      [[[DummyWindowFocusButton alloc] initWithFrame:NSZeroRect] autorelease];
1242   [dummyFocusButton setNextKeyView:subView];
1243   [[self window] makeFirstResponder:dummyFocusButton];
1245   [contentView addSubview:subView];
1246   [contentView addSubview:dummyFocusButton];
1247   SetWindowSize([self window],
1248       NSMakeSize(NSWidth([subView frame]), NSHeight([subView frame])));
1251 - (NSView*)buildProfileChooserView {
1252   base::scoped_nsobject<NSView> container(
1253       [[NSView alloc] initWithFrame:NSZeroRect]);
1255   NSView* tutorialView = nil;
1256   NSView* currentProfileView = nil;
1257   base::scoped_nsobject<NSMutableArray> otherProfiles(
1258       [[NSMutableArray alloc] init]);
1259   // Local and guest profiles cannot lock their profile.
1260   bool displayLock = false;
1261   bool isFastProfileChooser =
1262       viewMode_ == profiles::BUBBLE_VIEW_MODE_FAST_PROFILE_CHOOSER;
1264   // Loop over the profiles in reverse, so that they are sorted by their
1265   // y-coordinate, and separate them into active and "other" profiles.
1266   for (int i = avatarMenu_->GetNumberOfItems() - 1; i >= 0; --i) {
1267     const AvatarMenu::Item& item = avatarMenu_->GetItemAt(i);
1268     if (item.active) {
1269       if (viewMode_ == profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER) {
1270         switch (tutorialMode_) {
1271           case profiles::TUTORIAL_MODE_NONE:
1272           case profiles::TUTORIAL_MODE_WELCOME_UPGRADE:
1273             tutorialView =
1274                 [self buildWelcomeUpgradeTutorialViewIfNeeded];
1275             break;
1276           case profiles::TUTORIAL_MODE_CONFIRM_SIGNIN:
1277             tutorialView = [self buildSigninConfirmationView];
1278             break;
1279           case profiles::TUTORIAL_MODE_SHOW_ERROR:
1280             tutorialView = [self buildSigninErrorView];
1281         }
1282       }
1283       currentProfileView = [self createCurrentProfileView:item];
1284       displayLock = item.signed_in &&
1285           profiles::IsLockAvailable(browser_->profile());
1286     } else {
1287       [otherProfiles addObject:[self createOtherProfileView:i]];
1288     }
1289   }
1290   if (!currentProfileView)  // Guest windows don't have an active profile.
1291     currentProfileView = [self createGuestProfileView];
1293   // |yOffset| is the next position at which to draw in |container|
1294   // coordinates. Add a pixel offset so that the bottom option buttons don't
1295   // overlap the bubble's rounded corners.
1296   CGFloat yOffset = 1;
1298   if (!isFastProfileChooser) {
1299     // Option buttons.
1300     NSRect rect = NSMakeRect(0, yOffset, kFixedMenuWidth, 0);
1301     NSView* optionsView = [self createOptionsViewWithRect:rect
1302                                               displayLock:displayLock];
1303     [container addSubview:optionsView];
1304     rect.origin.y = NSMaxY([optionsView frame]);
1306     NSBox* separator = [self horizontalSeparatorWithFrame:rect];
1307     [container addSubview:separator];
1308     yOffset = NSMaxY([separator frame]);
1309   }
1311   if ((viewMode_ == profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER &&
1312        switches::IsFastUserSwitching()) || isFastProfileChooser) {
1313     // Other profiles switcher. The profiles have already been sorted
1314     // by their y-coordinate, so they can be added in the existing order.
1315     for (NSView *otherProfileView in otherProfiles.get()) {
1316       [otherProfileView setFrameOrigin:NSMakePoint(0, yOffset)];
1317       [container addSubview:otherProfileView];
1318       yOffset = NSMaxY([otherProfileView frame]);
1320       NSBox* separator = [self horizontalSeparatorWithFrame:NSMakeRect(
1321           0, yOffset, kFixedMenuWidth, 0)];
1322       [container addSubview:separator];
1323       yOffset = NSMaxY([separator frame]);
1324     }
1325   }
1327   // For supervised users, add the disclaimer text.
1328   if (browser_->profile()->IsSupervised()) {
1329     yOffset += kSmallVerticalSpacing;
1330     NSView* disclaimerContainer = [self createSupervisedUserDisclaimerView];
1331     [disclaimerContainer setFrameOrigin:NSMakePoint(0, yOffset)];
1332     [container addSubview:disclaimerContainer];
1333     yOffset = NSMaxY([disclaimerContainer frame]);
1334     yOffset += kSmallVerticalSpacing;
1336     NSBox* separator = [self horizontalSeparatorWithFrame:NSMakeRect(
1337         0, yOffset, kFixedMenuWidth, 0)];
1338     [container addSubview:separator];
1339     yOffset = NSMaxY([separator frame]);
1340   }
1342   if (viewMode_ == profiles::BUBBLE_VIEW_MODE_ACCOUNT_MANAGEMENT) {
1343     NSView* currentProfileAccountsView = [self createCurrentProfileAccountsView:
1344         NSMakeRect(0, yOffset, kFixedMenuWidth, 0)];
1345     [container addSubview:currentProfileAccountsView];
1346     yOffset = NSMaxY([currentProfileAccountsView frame]);
1348     NSBox* accountsSeparator = [self horizontalSeparatorWithFrame:
1349         NSMakeRect(0, yOffset, kFixedMenuWidth, 0)];
1350     [container addSubview:accountsSeparator];
1351     yOffset = NSMaxY([accountsSeparator frame]);
1352   }
1354   // Active profile card.
1355   if (!isFastProfileChooser && currentProfileView) {
1356     yOffset += kVerticalSpacing;
1357     [currentProfileView setFrameOrigin:NSMakePoint(0, yOffset)];
1358     [container addSubview:currentProfileView];
1359     yOffset = NSMaxY([currentProfileView frame]) + kVerticalSpacing;
1360   }
1362   if (!isFastProfileChooser && tutorialView) {
1363     [tutorialView setFrameOrigin:NSMakePoint(0, yOffset)];
1364     [container addSubview:tutorialView];
1365     yOffset = NSMaxY([tutorialView frame]);
1366     //TODO(mlerman): update UMA stats for the new tutorials.
1367   } else {
1368     tutorialMode_ = profiles::TUTORIAL_MODE_NONE;
1369   }
1371   [container setFrameSize:NSMakeSize(kFixedMenuWidth, yOffset)];
1372   return container.autorelease();
1375 - (NSView*)buildSigninConfirmationView {
1376   ProfileMetrics::LogProfileNewAvatarMenuSignin(
1377       ProfileMetrics::PROFILE_AVATAR_MENU_SIGNIN_VIEW);
1379   NSString* titleMessage = l10n_util::GetNSString(
1380       IDS_PROFILES_CONFIRM_SIGNIN_TUTORIAL_TITLE);
1381   NSString* contentMessage = l10n_util::GetNSString(
1382       IDS_PROFILES_CONFIRM_SIGNIN_TUTORIAL_CONTENT_TEXT);
1383   NSString* linkMessage = l10n_util::GetNSString(
1384       IDS_PROFILES_SYNC_SETTINGS_LINK);
1385   NSString* buttonMessage = l10n_util::GetNSString(
1386       IDS_PROFILES_TUTORIAL_OK_BUTTON);
1387   return [self tutorialViewWithMode:profiles::TUTORIAL_MODE_CONFIRM_SIGNIN
1388                        titleMessage:titleMessage
1389                      contentMessage:contentMessage
1390                         linkMessage:linkMessage
1391                       buttonMessage:buttonMessage
1392                         stackButton:NO
1393                      hasCloseButton:NO
1394                          linkAction:@selector(configureSyncSettings:)
1395                        buttonAction:@selector(syncSettingsConfirmed:)];
1398 - (NSView*)buildSigninErrorView {
1399   NSString* titleMessage = l10n_util::GetNSString(
1400       IDS_PROFILES_ERROR_TUTORIAL_TITLE);
1401   LoginUIService* loginUiService =
1402       LoginUIServiceFactory::GetForProfile(browser_->profile());
1403   NSString* contentMessage =
1404       base::SysUTF16ToNSString(loginUiService->GetLastLoginResult());
1405   NSString* linkMessage = l10n_util::GetNSString(
1406       IDS_PROFILES_PROFILE_TUTORIAL_LEARN_MORE);
1407   return [self tutorialViewWithMode:profiles::TUTORIAL_MODE_CONFIRM_SIGNIN
1408                        titleMessage:titleMessage
1409                      contentMessage:contentMessage
1410                         linkMessage:linkMessage
1411                       buttonMessage:nil
1412                         stackButton:NO
1413                      hasCloseButton:YES
1414                          linkAction:@selector(showLearnMorePage:)
1415                        buttonAction:nil];
1418 - (NSView*)buildWelcomeUpgradeTutorialViewIfNeeded {
1419   Profile* profile = browser_->profile();
1420   const AvatarMenu::Item& avatarItem =
1421       avatarMenu_->GetItemAt(avatarMenu_->GetActiveProfileIndex());
1423   const int showCount = profile->GetPrefs()->GetInteger(
1424       prefs::kProfileAvatarTutorialShown);
1425   // Do not show the tutorial if user has dismissed it.
1426   if (showCount > signin_ui_util::kUpgradeWelcomeTutorialShowMax)
1427     return nil;
1429   if (tutorialMode_ != profiles::TUTORIAL_MODE_WELCOME_UPGRADE) {
1430     if (showCount == signin_ui_util::kUpgradeWelcomeTutorialShowMax)
1431       return nil;
1432     profile->GetPrefs()->SetInteger(
1433         prefs::kProfileAvatarTutorialShown, showCount + 1);
1434   }
1436   ProfileMetrics::LogProfileNewAvatarMenuUpgrade(
1437       ProfileMetrics::PROFILE_AVATAR_MENU_UPGRADE_VIEW);
1439   NSString* titleMessage = l10n_util::GetNSString(
1440       IDS_PROFILES_WELCOME_UPGRADE_TUTORIAL_TITLE);
1441   NSString* contentMessage = l10n_util::GetNSString(
1442       IDS_PROFILES_WELCOME_UPGRADE_TUTORIAL_CONTENT_TEXT);
1443   // For local profiles, the "Not you" link doesn't make sense.
1444   NSString* linkMessage = avatarItem.signed_in ?
1445       ElideMessage(
1446           l10n_util::GetStringFUTF16(IDS_PROFILES_NOT_YOU, avatarItem.name),
1447           kFixedMenuWidth - 2 * kHorizontalSpacing) :
1448       nil;
1449   NSString* buttonMessage = l10n_util::GetNSString(
1450       IDS_PROFILES_TUTORIAL_WHATS_NEW_BUTTON);
1451   return [self tutorialViewWithMode:profiles::TUTORIAL_MODE_WELCOME_UPGRADE
1452                        titleMessage:titleMessage
1453                      contentMessage:contentMessage
1454                         linkMessage:linkMessage
1455                       buttonMessage:buttonMessage
1456                         stackButton:YES
1457                      hasCloseButton:YES
1458                          linkAction:@selector(showSwitchUserView:)
1459                        buttonAction:@selector(seeWhatsNew:)];
1462 - (NSView*)tutorialViewWithMode:(profiles::TutorialMode)mode
1463                    titleMessage:(NSString*)titleMessage
1464                  contentMessage:(NSString*)contentMessage
1465                     linkMessage:(NSString*)linkMessage
1466                   buttonMessage:(NSString*)buttonMessage
1467                     stackButton:(BOOL)stackButton
1468                  hasCloseButton:(BOOL)hasCloseButton
1469                      linkAction:(SEL)linkAction
1470                    buttonAction:(SEL)buttonAction {
1471   tutorialMode_ = mode;
1473   NSColor* tutorialBackgroundColor =
1474       gfx::SkColorToSRGBNSColor(profiles::kAvatarTutorialBackgroundColor);
1475   base::scoped_nsobject<NSView> container([[BackgroundColorView alloc]
1476       initWithFrame:NSMakeRect(0, 0, kFixedMenuWidth, 0)
1477           withColor:tutorialBackgroundColor]);
1478   CGFloat availableWidth = kFixedMenuWidth - 2 * kHorizontalSpacing;
1479   CGFloat yOffset = kVerticalSpacing;
1481   // Adds links and buttons at the bottom.
1482   base::scoped_nsobject<NSButton> tutorialOkButton;
1483   if (buttonMessage) {
1484     tutorialOkButton.reset([[HoverButton alloc] initWithFrame:NSZeroRect]);
1485     [tutorialOkButton setTitle:buttonMessage];
1486     [tutorialOkButton setBezelStyle:NSRoundedBezelStyle];
1487     [tutorialOkButton setTarget:self];
1488     [tutorialOkButton setAction:buttonAction];
1489     [tutorialOkButton setAlignment:NSCenterTextAlignment];
1490     [tutorialOkButton sizeToFit];
1491   }
1493   NSButton* learnMoreLink = nil;
1494   if (linkMessage) {
1495     learnMoreLink = [self linkButtonWithTitle:linkMessage
1496                                   frameOrigin:NSZeroPoint
1497                                        action:linkAction];
1498     [[learnMoreLink cell] setTextColor:[NSColor whiteColor]];
1499   }
1501   if (stackButton) {
1502     if (linkMessage) {
1503       [learnMoreLink setFrameOrigin:NSMakePoint(
1504           (kFixedMenuWidth - NSWidth([learnMoreLink frame])) / 2, yOffset)];
1505     }
1506     [tutorialOkButton setFrameSize:NSMakeSize(
1507         availableWidth, NSHeight([tutorialOkButton frame]))];
1508     [tutorialOkButton setFrameOrigin:NSMakePoint(
1509         kHorizontalSpacing,
1510         yOffset + (learnMoreLink ? NSHeight([learnMoreLink frame]) : 0))];
1511   } else {
1512     if (buttonMessage) {
1513       NSSize buttonSize = [tutorialOkButton frame].size;
1514       const CGFloat kTopBottomTextPadding = 6;
1515       const CGFloat kLeftRightTextPadding = 15;
1516       buttonSize.width += 2 * kLeftRightTextPadding;
1517       buttonSize.height += 2 * kTopBottomTextPadding;
1518       [tutorialOkButton setFrameSize:buttonSize];
1519       CGFloat buttonXOffset = kFixedMenuWidth -
1520           NSWidth([tutorialOkButton frame]) - kHorizontalSpacing;
1521       [tutorialOkButton setFrameOrigin:NSMakePoint(buttonXOffset, yOffset)];
1522     }
1524     if (linkMessage) {
1525       CGFloat linkYOffset = yOffset;
1526       if (buttonMessage) {
1527         linkYOffset += (NSHeight([tutorialOkButton frame]) -
1528                         NSHeight([learnMoreLink frame])) / 2;
1529       }
1530       [learnMoreLink setFrameOrigin:NSMakePoint(
1531           kHorizontalSpacing, linkYOffset)];
1532     }
1533   }
1535   if (buttonMessage) {
1536     [container addSubview:tutorialOkButton];
1537     yOffset = NSMaxY([tutorialOkButton frame]);
1538   }
1540   if (linkMessage) {
1541     [container addSubview:learnMoreLink];
1542     yOffset = std::max(NSMaxY([learnMoreLink frame]), yOffset);
1543   }
1545   yOffset += kVerticalSpacing;
1547   // Adds body content.
1548   NSTextField* contentLabel = BuildLabel(
1549       contentMessage,
1550       NSMakePoint(kHorizontalSpacing, yOffset),
1551       gfx::SkColorToSRGBNSColor(profiles::kAvatarTutorialContentTextColor));
1552   [contentLabel setFrameSize:NSMakeSize(availableWidth, 0)];
1553   [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:contentLabel];
1554   [container addSubview:contentLabel];
1555   yOffset = NSMaxY([contentLabel frame]) + kSmallVerticalSpacing;
1557   // Adds title.
1558   NSTextField* titleLabel =
1559       BuildLabel(titleMessage,
1560                  NSMakePoint(kHorizontalSpacing, yOffset),
1561                  [NSColor whiteColor] /* text_color */);
1562   [titleLabel setFont:[NSFont labelFontOfSize:kTitleFontSize]];
1564   if (hasCloseButton) {
1565     base::scoped_nsobject<HoverImageButton> closeButton(
1566         [[HoverImageButton alloc] initWithFrame:NSZeroRect]);
1567     [closeButton setBordered:NO];
1569     ui::ResourceBundle* rb = &ui::ResourceBundle::GetSharedInstance();
1570     NSImage* closeImage = rb->GetNativeImageNamed(IDR_CLOSE_1).ToNSImage();
1571     CGFloat closeImageWidth = [closeImage size].width;
1572     [closeButton setDefaultImage:closeImage];
1573     [closeButton setHoverImage:
1574         rb->GetNativeImageNamed(IDR_CLOSE_1_H).ToNSImage()];
1575     [closeButton setPressedImage:
1576         rb->GetNativeImageNamed(IDR_CLOSE_1_P).ToNSImage()];
1577     [closeButton setTarget:self];
1578     [closeButton setAction:@selector(dismissTutorial:)];
1579     [closeButton setFrameSize:[closeImage size]];
1580     [closeButton setFrameOrigin:NSMakePoint(
1581         kFixedMenuWidth - kHorizontalSpacing - closeImageWidth, yOffset)];
1582     [container addSubview:closeButton];
1584     [titleLabel setFrameSize:NSMakeSize(
1585         availableWidth - closeImageWidth - kHorizontalSpacing, 0)];
1586   } else {
1587     [titleLabel setFrameSize:NSMakeSize(availableWidth, 0)];
1588   }
1590   [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:titleLabel];
1591   [container addSubview:titleLabel];
1592   yOffset = NSMaxY([titleLabel frame]) + kVerticalSpacing;
1594   [container setFrameSize:NSMakeSize(kFixedMenuWidth, yOffset)];
1595   [container setFrameOrigin:NSZeroPoint];
1596   return container.autorelease();
1599 - (NSView*)createCurrentProfileView:(const AvatarMenu::Item&)item {
1600   base::scoped_nsobject<NSView> container([[NSView alloc]
1601       initWithFrame:NSZeroRect]);
1603   CGFloat xOffset = kHorizontalSpacing;
1604   CGFloat yOffset = 0;
1605   CGFloat availableTextWidth = kFixedMenuWidth - 2 * kHorizontalSpacing;
1607   // Profile options. This can be a link to the accounts view, the profile's
1608   // username for signed in users, or a "Sign in" button for local profiles.
1609   SigninManagerBase* signinManager =
1610       SigninManagerFactory::GetForProfile(
1611           browser_->profile()->GetOriginalProfile());
1612   if (!isGuestSession_ && signinManager->IsSigninAllowed()) {
1613     NSView* linksContainer =
1614         [self createCurrentProfileLinksForItem:item
1615                                           rect:NSMakeRect(xOffset, yOffset,
1616                                                           availableTextWidth,
1617                                                           0)];
1618     [container addSubview:linksContainer];
1619     yOffset = NSMaxY([linksContainer frame]);
1620   }
1622   // Profile name, centered.
1623   bool editingAllowed = !isGuestSession_ &&
1624                         !browser_->profile()->IsLegacySupervised();
1625   base::scoped_nsobject<EditableProfileNameButton> profileName(
1626       [[EditableProfileNameButton alloc]
1627           initWithFrame:NSMakeRect(xOffset,
1628                                    yOffset,
1629                                    availableTextWidth,
1630                                    kProfileButtonHeight)
1631                 profile:browser_->profile()
1632             profileName:base::SysUTF16ToNSString(
1633                             profiles::GetAvatarNameForProfile(
1634                                 browser_->profile()->GetPath()))
1635          editingAllowed:editingAllowed
1636          withController:self]);
1638   [container addSubview:profileName];
1639   yOffset = NSMaxY([profileName frame]) + 4;  // Adds a small vertical padding.
1641   // Profile icon, centered.
1642   xOffset = (kFixedMenuWidth - kLargeImageSide) / 2;
1643   base::scoped_nsobject<EditableProfilePhoto> iconView(
1644       [[EditableProfilePhoto alloc]
1645           initWithFrame:NSMakeRect(xOffset, yOffset,
1646                                    kLargeImageSide, kLargeImageSide)
1647              avatarMenu:avatarMenu_.get()
1648             profileIcon:item.icon
1649          editingAllowed:!isGuestSession_
1650          withController:self]);
1652   [container addSubview:iconView];
1653   yOffset = NSMaxY([iconView frame]);
1655   if (browser_->profile()->IsSupervised()) {
1656     base::scoped_nsobject<NSImageView> supervisedIcon(
1657         [[NSImageView alloc] initWithFrame:NSZeroRect]);
1658     int imageId = browser_->profile()->IsChild()
1659         ? IDR_ICON_PROFILES_MENU_CHILD
1660         : IDR_ICON_PROFILES_MENU_LEGACY_SUPERVISED;
1661     ui::ResourceBundle* rb = &ui::ResourceBundle::GetSharedInstance();
1662     [supervisedIcon setImage:rb->GetNativeImageNamed(imageId).ToNSImage()];
1663     NSSize size = [[supervisedIcon image] size];
1664     [supervisedIcon setFrameSize:size];
1665     NSRect parentFrame = [iconView frame];
1666     [supervisedIcon setFrameOrigin:NSMakePoint(NSMaxX(parentFrame) - size.width,
1667                                                NSMinY(parentFrame))];
1668     [container addSubview:supervisedIcon];
1669   }
1671   [container setFrameSize:NSMakeSize(kFixedMenuWidth, yOffset)];
1672   return container.autorelease();
1675 - (NSView*)createCurrentProfileLinksForItem:(const AvatarMenu::Item&)item
1676                                        rect:(NSRect)rect {
1677   base::scoped_nsobject<NSView> container([[NSView alloc] initWithFrame:rect]);
1679   // Don't double-apply the left margin to the sub-views.
1680   rect.origin.x = 0;
1682   // The available links depend on the type of profile that is active.
1683   if (item.signed_in) {
1684     // Signed in profiles with no authentication errors do not have a clickable
1685     // email link.
1686     NSButton* link = nil;
1687     if (switches::IsEnableAccountConsistency()) {
1688       NSString* linkTitle = l10n_util::GetNSString(
1689           viewMode_ == profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER ?
1690               IDS_PROFILES_PROFILE_MANAGE_ACCOUNTS_BUTTON :
1691               IDS_PROFILES_PROFILE_HIDE_MANAGE_ACCOUNTS_BUTTON);
1692       SEL linkSelector =
1693           (viewMode_ == profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER) ?
1694           @selector(showAccountManagement:) : @selector(hideAccountManagement:);
1695       link = [self linkButtonWithTitle:linkTitle
1696                            frameOrigin:rect.origin
1697                                 action:linkSelector];
1698     } else {
1699       link = [self linkButtonWithTitle:base::SysUTF16ToNSString(item.sync_state)
1700                            frameOrigin:rect.origin
1701                                 action:nil];
1702       if (HasAuthError(browser_->profile())) {
1703         [link setImage:ui::ResourceBundle::GetSharedInstance().
1704             GetNativeImageNamed(IDR_ICON_PROFILES_ACCOUNT_BUTTON_ERROR).
1705             ToNSImage()];
1706         [link setImagePosition:NSImageRight];
1707         [link setTarget:self];
1708         [link setAction:@selector(showAccountReauthenticationView:)];
1709         [link setTag:kPrimaryProfileTag];
1710         [[link cell]
1711             accessibilitySetOverrideValue:l10n_util::GetNSStringF(
1712             IDS_PROFILES_ACCOUNT_BUTTON_AUTH_ERROR_ACCESSIBLE_NAME,
1713             item.sync_state)
1714                              forAttribute:NSAccessibilityTitleAttribute];
1715       } else {
1716         [link setEnabled:NO];
1717       }
1718     }
1719     // -linkButtonWithTitle sizeToFit's the link. We can use the height, but
1720     // need to re-stretch the width so that the link can be centered correctly
1721     // in the view.
1722     rect.size.height = [link frame].size.height;
1723     [link setAlignment:NSCenterTextAlignment];
1724     [link setFrame:rect];
1725     [container addSubview:link];
1726     [container setFrameSize:rect.size];
1727   } else {
1728     rect.size.height = kBlueButtonHeight;
1729     NSButton* signinButton = [[BlueLabelButton alloc] initWithFrame:rect];
1731     // Manually elide the button text so that the contents fit inside the bubble
1732     // This is needed because the BlueLabelButton cell resets the style on
1733     // every call to -cellSize, which prevents setting a custom lineBreakMode.
1734     NSString* elidedButtonText = base::SysUTF16ToNSString(gfx::ElideText(
1735         l10n_util::GetStringFUTF16(
1736             IDS_SYNC_START_SYNC_BUTTON_LABEL,
1737             l10n_util::GetStringUTF16(IDS_SHORT_PRODUCT_NAME)),
1738         gfx::FontList(), rect.size.width, gfx::ELIDE_TAIL));
1740     [signinButton setTitle:elidedButtonText];
1741     [signinButton setTarget:self];
1742     [signinButton setAction:@selector(showInlineSigninPage:)];
1743     [container addSubview:signinButton];
1745     // Sign-in promo text.
1746     NSTextField* promo = BuildLabel(
1747         l10n_util::GetNSString(IDS_PROFILES_SIGNIN_PROMO),
1748         NSMakePoint(0, NSMaxY([signinButton frame]) + kVerticalSpacing),
1749         nil);
1750     [promo setFrameSize:NSMakeSize(rect.size.width, 0)];
1751     [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:promo];
1752     [container addSubview:promo];
1754     [container setFrameSize:NSMakeSize(
1755         rect.size.width,
1756         NSMaxY([promo frame]) + 4)];  // Adds a small vertical padding.
1757   }
1759   return container.autorelease();
1762 - (NSView*)createSupervisedUserDisclaimerView {
1763   base::scoped_nsobject<NSView> container(
1764       [[NSView alloc] initWithFrame:NSZeroRect]);
1766   int yOffset = 0;
1767   int availableTextWidth = kFixedMenuWidth - 2 * kHorizontalSpacing;
1769   NSTextField* disclaimer = BuildLabel(
1770       base::SysUTF16ToNSString(avatarMenu_->GetSupervisedUserInformation()),
1771       NSMakePoint(kHorizontalSpacing, yOffset), nil);
1772   [disclaimer setFrameSize:NSMakeSize(availableTextWidth, 0)];
1773   [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:disclaimer];
1774   yOffset = NSMaxY([disclaimer frame]);
1776   [container addSubview:disclaimer];
1777   [container setFrameSize:NSMakeSize(kFixedMenuWidth, yOffset)];
1778   return container.autorelease();
1781 - (NSView*)createGuestProfileView {
1782   gfx::Image guestIcon =
1783       ui::ResourceBundle::GetSharedInstance().GetNativeImageNamed(
1784           profiles::GetPlaceholderAvatarIconResourceID());
1785   AvatarMenu::Item guestItem(std::string::npos, /* menu_index, not used */
1786                              std::string::npos, /* profile_index, not used */
1787                              guestIcon);
1788   guestItem.active = true;
1789   guestItem.name = base::SysNSStringToUTF16(
1790       l10n_util::GetNSString(IDS_PROFILES_GUEST_PROFILE_NAME));
1792   return [self createCurrentProfileView:guestItem];
1795 - (NSButton*)createOtherProfileView:(int)itemIndex {
1796   const AvatarMenu::Item& item = avatarMenu_->GetItemAt(itemIndex);
1798   NSRect rect = NSMakeRect(
1799       0, 0, kFixedMenuWidth, kBlueButtonHeight + kSmallVerticalSpacing);
1800   base::scoped_nsobject<BackgroundColorHoverButton> profileButton(
1801       [[BackgroundColorHoverButton alloc]
1802           initWithFrame:rect
1803       imageTitleSpacing:kImageTitleSpacing
1804         backgroundColor:GetDialogBackgroundColor()]);
1805   [profileButton setTitle:base::SysUTF16ToNSString(item.name)];
1807   // Use the low-res, small default avatars in the fast user switcher, like
1808   // we do in the menu bar.
1809   gfx::Image itemIcon;
1810   bool isRectangle;
1811   AvatarMenu::GetImageForMenuButton(item.profile_path, &itemIcon, &isRectangle);
1813   [profileButton setDefaultImage:CreateProfileImage(
1814       itemIcon, kSmallImageSide).ToNSImage()];
1815   [profileButton setImagePosition:NSImageLeft];
1816   [profileButton setAlignment:NSLeftTextAlignment];
1817   [profileButton setBordered:NO];
1818   [profileButton setTag:itemIndex];
1819   [profileButton setTarget:self];
1820   [profileButton setAction:@selector(switchToProfile:)];
1822   NSSize textSize = [[profileButton title] sizeWithAttributes:@{
1823     NSFontAttributeName : [profileButton font]
1824   }];
1826   CGFloat availableWidth = rect.size.width - kSmallImageSide -
1827                            kImageTitleSpacing - kHorizontalSpacing;
1829   if (std::ceil(textSize.width) > availableWidth)
1830     [profileButton setToolTip:[profileButton title]];
1832   return profileButton.autorelease();
1835 - (NSView*)createOptionsViewWithRect:(NSRect)rect
1836                          displayLock:(BOOL)displayLock {
1837   NSRect viewRect = NSMakeRect(0, 0,
1838                                rect.size.width,
1839                                kBlueButtonHeight + kSmallVerticalSpacing);
1840   base::scoped_nsobject<NSView> container([[NSView alloc] initWithFrame:rect]);
1842   if (displayLock) {
1843     NSButton* lockButton =
1844         [self hoverButtonWithRect:viewRect
1845                              text:l10n_util::GetNSString(
1846                                   IDS_PROFILES_PROFILE_SIGNOUT_BUTTON)
1847                   imageResourceId:IDR_ICON_PROFILES_MENU_LOCK
1848                            action:@selector(lockProfile:)];
1849     [container addSubview:lockButton];
1850     viewRect.origin.y = NSMaxY([lockButton frame]);
1852     NSBox* separator = [self horizontalSeparatorWithFrame:viewRect];
1853     [container addSubview:separator];
1854     viewRect.origin.y = NSMaxY([separator frame]);
1855   }
1857   if ([self shouldShowGoIncognito]) {
1858     NSButton* goIncognitoButton =
1859         [self hoverButtonWithRect:viewRect
1860                              text:l10n_util::GetNSString(
1861                                   IDS_PROFILES_GO_INCOGNITO_BUTTON)
1862                   imageResourceId:IDR_ICON_PROFILES_MENU_INCOGNITO
1863                            action:@selector(goIncognito:)];
1864     viewRect.origin.y = NSMaxY([goIncognitoButton frame]);
1865     [container addSubview:goIncognitoButton];
1867     NSBox* separator = [self horizontalSeparatorWithFrame:viewRect];
1868     [container addSubview:separator];
1869     viewRect.origin.y = NSMaxY([separator frame]);
1870   }
1872   NSString* text = isGuestSession_ ?
1873       l10n_util::GetNSString(IDS_PROFILES_EXIT_GUEST) :
1874       l10n_util::GetNSString(IDS_PROFILES_SWITCH_USERS_BUTTON);
1875   NSButton* switchUsersButton =
1876       [self hoverButtonWithRect:viewRect
1877                            text:text
1878                 imageResourceId:IDR_ICON_PROFILES_MENU_AVATAR
1879                          action:isGuestSession_? @selector(exitGuest:) :
1880                                                  @selector(showUserManager:)];
1881   viewRect.origin.y = NSMaxY([switchUsersButton frame]);
1882   [container addSubview:switchUsersButton];
1884   [container setFrameSize:NSMakeSize(rect.size.width, viewRect.origin.y)];
1885   return container.autorelease();
1888 - (NSView*)createCurrentProfileAccountsView:(NSRect)rect {
1889   const CGFloat kAccountButtonHeight = 34;
1891   const AvatarMenu::Item& item =
1892       avatarMenu_->GetItemAt(avatarMenu_->GetActiveProfileIndex());
1893   DCHECK(item.signed_in);
1895   NSColor* backgroundColor = gfx::SkColorToCalibratedNSColor(
1896       profiles::kAvatarBubbleAccountsBackgroundColor);
1897   base::scoped_nsobject<NSView> container([[BackgroundColorView alloc]
1898       initWithFrame:rect
1899           withColor:backgroundColor]);
1901   rect.origin.y = 0;
1902   if (!browser_->profile()->IsSupervised()) {
1903     // Manually elide the button text so the contents fit inside the bubble.
1904     // This is needed because the BlueLabelButton cell resets the style on
1905     // every call to -cellSize, which prevents setting a custom lineBreakMode.
1906     NSString* elidedButtonText = base::SysUTF16ToNSString(gfx::ElideText(
1907         l10n_util::GetStringFUTF16(
1908             IDS_PROFILES_PROFILE_ADD_ACCOUNT_BUTTON, item.name),
1909         gfx::FontList(), rect.size.width, gfx::ELIDE_TAIL));
1911     NSButton* addAccountsButton =
1912         [self linkButtonWithTitle:elidedButtonText
1913                       frameOrigin:NSMakePoint(
1914             kHorizontalSpacing, kSmallVerticalSpacing)
1915                            action:@selector(addAccount:)];
1916     [container addSubview:addAccountsButton];
1917     rect.origin.y += kAccountButtonHeight;
1918   }
1920   NSView* accountEmails = [self createAccountsListWithRect:NSMakeRect(
1921       0, rect.origin.y, rect.size.width, kAccountButtonHeight)];
1922   [container addSubview:accountEmails];
1924   [container setFrameSize:NSMakeSize(rect.size.width,
1925                                      NSMaxY([accountEmails frame]))];
1926   return container.autorelease();
1929 - (NSView*)createAccountsListWithRect:(NSRect)rect {
1930   base::scoped_nsobject<NSView> container([[NSView alloc] initWithFrame:rect]);
1931   currentProfileAccounts_.clear();
1933   Profile* profile = browser_->profile();
1934   std::string primaryAccount =
1935       SigninManagerFactory::GetForProfile(profile)->GetAuthenticatedAccountId();
1936   DCHECK(!primaryAccount.empty());
1937   std::vector<std::string>accounts =
1938       profiles::GetSecondaryAccountsForProfile(profile, primaryAccount);
1940   // If there is an account with an authentication error, it needs to be
1941   // badged with a warning icon.
1942   const SigninErrorController* errorController =
1943       profiles::GetSigninErrorController(profile);
1944   std::string errorAccountId =
1945       errorController ? errorController->error_account_id() : std::string();
1947   rect.origin.y = 0;
1948   for (size_t i = 0; i < accounts.size(); ++i) {
1949     // Save the original email address, as the button text could be elided.
1950     currentProfileAccounts_[i] = accounts[i];
1951     NSButton* accountButton =
1952         [self accountButtonWithRect:rect
1953                          accountId:accounts[i]
1954                                 tag:i
1955                      reauthRequired:errorAccountId == accounts[i]];
1956     [container addSubview:accountButton];
1957     rect.origin.y = NSMaxY([accountButton frame]);
1958   }
1960   // The primary account should always be listed first.
1961   NSButton* accountButton =
1962       [self accountButtonWithRect:rect
1963                         accountId:primaryAccount
1964                               tag:kPrimaryProfileTag
1965                    reauthRequired:errorAccountId == primaryAccount];
1966   [container addSubview:accountButton];
1967   [container setFrameSize:NSMakeSize(NSWidth([container frame]),
1968                                      NSMaxY([accountButton frame]))];
1969   return container.autorelease();
1972 - (NSView*)buildGaiaEmbeddedView {
1973   base::scoped_nsobject<NSView> container(
1974       [[NSView alloc] initWithFrame:NSZeroRect]);
1975   CGFloat yOffset = 0;
1977   GURL url;
1978   int messageId = -1;
1979   SigninErrorController* errorController = NULL;
1980   switch (viewMode_) {
1981     case profiles::BUBBLE_VIEW_MODE_GAIA_SIGNIN:
1982       url = signin::GetPromoURL(signin_metrics::SOURCE_AVATAR_BUBBLE_SIGN_IN,
1983                                 false /* auto_close */,
1984                                 true /* is_constrained */);
1985       messageId = IDS_PROFILES_GAIA_SIGNIN_TITLE;
1986       break;
1987     case profiles::BUBBLE_VIEW_MODE_GAIA_ADD_ACCOUNT:
1988       url = signin::GetPromoURL(
1989           signin_metrics::SOURCE_AVATAR_BUBBLE_ADD_ACCOUNT,
1990           false /* auto_close */,
1991           true /* is_constrained */);
1992       messageId = IDS_PROFILES_GAIA_ADD_ACCOUNT_TITLE;
1993       break;
1994     case profiles::BUBBLE_VIEW_MODE_GAIA_REAUTH:
1995       DCHECK(HasAuthError(browser_->profile()));
1996       errorController = profiles::GetSigninErrorController(browser_->profile());
1997       url = signin::GetReauthURL(
1998           browser_->profile(),
1999           errorController ? errorController->error_username() : std::string());
2000       messageId = IDS_PROFILES_GAIA_REAUTH_TITLE;
2001       break;
2002     default:
2003       NOTREACHED() << "Called with invalid mode=" << viewMode_;
2004       break;
2005   }
2007   webContents_.reset(content::WebContents::Create(
2008       content::WebContents::CreateParams(browser_->profile())));
2010   webContentsDelegate_.reset(new GaiaWebContentsDelegate());
2011   webContents_->SetDelegate(webContentsDelegate_.get());
2012   webContents_->GetController().LoadURL(url,
2013                                         content::Referrer(),
2014                                         ui::PAGE_TRANSITION_AUTO_TOPLEVEL,
2015                                         std::string());
2016   NSView* webview = webContents_->GetNativeView();
2017   [webview setFrameSize:NSMakeSize(kFixedGaiaViewWidth, kFixedGaiaViewHeight)];
2018   [container addSubview:webview];
2019   content::RenderWidgetHostView* rwhv = webContents_->GetRenderWidgetHostView();
2020   if (rwhv)
2021     rwhv->SetBackgroundColor(profiles::kAvatarBubbleGaiaBackgroundColor);
2022   yOffset = NSMaxY([webview frame]);
2024   // Adds the title card.
2025   NSBox* separator = [self horizontalSeparatorWithFrame:
2026       NSMakeRect(0, yOffset, kFixedGaiaViewWidth, 0)];
2027   [container addSubview:separator];
2028   yOffset = NSMaxY([separator frame]) + kVerticalSpacing;
2030   NSView* titleView = BuildTitleCard(
2031       NSMakeRect(0, yOffset, kFixedGaiaViewWidth, 0),
2032       l10n_util::GetStringUTF16(messageId),
2033       self /* backButtonTarget*/,
2034       @selector(navigateBackFromSigninPage:) /* backButtonAction */);
2035   [container addSubview:titleView];
2036   yOffset = NSMaxY([titleView frame]);
2038   [container setFrameSize:NSMakeSize(kFixedGaiaViewWidth, yOffset)];
2039   return container.autorelease();
2042 - (NSView*)buildAccountRemovalView {
2043   DCHECK(!accountIdToRemove_.empty());
2045   base::scoped_nsobject<NSView> container(
2046       [[NSView alloc] initWithFrame:NSZeroRect]);
2047   CGFloat availableWidth =
2048       kFixedAccountRemovalViewWidth - 2 * kHorizontalSpacing;
2049   CGFloat yOffset = kVerticalSpacing;
2051   const std::string& primaryAccount = SigninManagerFactory::GetForProfile(
2052       browser_->profile())->GetAuthenticatedAccountId();
2053   bool isPrimaryAccount = primaryAccount == accountIdToRemove_;
2055   // Adds "remove account" button at the bottom if needed.
2056   if (!isPrimaryAccount) {
2057     base::scoped_nsobject<NSButton> removeAccountButton(
2058         [[BlueLabelButton alloc] initWithFrame:NSZeroRect]);
2059     [removeAccountButton setTitle:l10n_util::GetNSString(
2060         IDS_PROFILES_ACCOUNT_REMOVAL_BUTTON)];
2061     [removeAccountButton setTarget:self];
2062     [removeAccountButton setAction:@selector(removeAccount:)];
2063     [removeAccountButton sizeToFit];
2064     [removeAccountButton setAlignment:NSCenterTextAlignment];
2065     CGFloat xOffset = (kFixedAccountRemovalViewWidth -
2066         NSWidth([removeAccountButton frame])) / 2;
2067     [removeAccountButton setFrameOrigin:NSMakePoint(xOffset, yOffset)];
2068     [container addSubview:removeAccountButton];
2070     yOffset = NSMaxY([removeAccountButton frame]) + kVerticalSpacing;
2071   }
2073   NSView* contentView;
2074   NSPoint contentFrameOrigin = NSMakePoint(kHorizontalSpacing, yOffset);
2075   if (isPrimaryAccount) {
2076     std::string email = signin_ui_util::GetDisplayEmail(browser_->profile(),
2077                                                         accountIdToRemove_);
2078     std::vector<size_t> offsets;
2079     NSString* contentStr = l10n_util::GetNSStringF(
2080         IDS_PROFILES_PRIMARY_ACCOUNT_REMOVAL_TEXT,
2081         base::UTF8ToUTF16(email), base::string16(), &offsets);
2082     NSString* linkStr = l10n_util::GetNSString(IDS_PROFILES_SETTINGS_LINK);
2083     contentView = BuildFixedWidthTextViewWithLink(self, contentStr, linkStr,
2084         offsets[1], contentFrameOrigin, availableWidth);
2085   } else {
2086     NSString* contentStr =
2087         l10n_util::GetNSString(IDS_PROFILES_ACCOUNT_REMOVAL_TEXT);
2088     NSTextField* contentLabel = BuildLabel(contentStr, contentFrameOrigin, nil);
2089     [contentLabel setFrameSize:NSMakeSize(availableWidth, 0)];
2090     [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:contentLabel];
2091     contentView = contentLabel;
2092   }
2093   [container addSubview:contentView];
2094   yOffset = NSMaxY([contentView frame]) + kVerticalSpacing;
2096   // Adds the title card.
2097   NSBox* separator = [self horizontalSeparatorWithFrame:
2098       NSMakeRect(0, yOffset, kFixedAccountRemovalViewWidth, 0)];
2099   [container addSubview:separator];
2100   yOffset = NSMaxY([separator frame]) + kVerticalSpacing;
2102   NSView* titleView = BuildTitleCard(
2103       NSMakeRect(0, yOffset, kFixedAccountRemovalViewWidth,0),
2104       l10n_util::GetStringUTF16(IDS_PROFILES_ACCOUNT_REMOVAL_TITLE),
2105       self /* backButtonTarget*/,
2106       @selector(showAccountManagement:) /* backButtonAction */);
2107   [container addSubview:titleView];
2108   yOffset = NSMaxY([titleView frame]);
2110   [container setFrameSize:NSMakeSize(kFixedAccountRemovalViewWidth, yOffset)];
2111   return container.autorelease();
2114 - (NSView*)buildSwitchUserView {
2115   ProfileMetrics::LogProfileNewAvatarMenuNotYou(
2116       ProfileMetrics::PROFILE_AVATAR_MENU_NOT_YOU_VIEW);
2117   base::scoped_nsobject<NSView> container(
2118       [[NSView alloc] initWithFrame:NSZeroRect]);
2119   CGFloat availableWidth =
2120       kFixedSwitchUserViewWidth - 2 * kHorizontalSpacing;
2121   CGFloat yOffset = 0;
2122   NSRect viewRect = NSMakeRect(0, yOffset,
2123                                kFixedSwitchUserViewWidth,
2124                                kBlueButtonHeight + kSmallVerticalSpacing);
2126   const AvatarMenu::Item& avatarItem =
2127       avatarMenu_->GetItemAt(avatarMenu_->GetActiveProfileIndex());
2129   // Adds "Disconnect your Google Account" button at the bottom.
2130   NSButton* disconnectButton =
2131       [self hoverButtonWithRect:viewRect
2132                            text:l10n_util::GetNSString(
2133                                     IDS_PROFILES_DISCONNECT_BUTTON)
2134                 imageResourceId:IDR_ICON_PROFILES_MENU_DISCONNECT
2135                          action:@selector(disconnectProfile:)];
2136   [container addSubview:disconnectButton];
2137   yOffset = NSMaxY([disconnectButton frame]);
2139   NSBox* separator = [self horizontalSeparatorWithFrame:
2140       NSMakeRect(0, yOffset, kFixedSwitchUserViewWidth, 0)];
2141   [container addSubview:separator];
2142   yOffset = NSMaxY([separator frame]);
2144   // Adds "Add person" button.
2145   viewRect.origin.y = yOffset;
2146   NSButton* addPersonButton =
2147       [self hoverButtonWithRect:viewRect
2148                            text:l10n_util::GetNSString(
2149                                     IDS_PROFILES_ADD_PERSON_BUTTON)
2150                 imageResourceId:IDR_ICON_PROFILES_MENU_AVATAR
2151                          action:@selector(showUserManager:)];
2152   [container addSubview:addPersonButton];
2153   yOffset = NSMaxY([addPersonButton frame]);
2155   separator = [self horizontalSeparatorWithFrame:
2156       NSMakeRect(0, yOffset, kFixedSwitchUserViewWidth, 0)];
2157   [container addSubview:separator];
2158   yOffset = NSMaxY([separator frame]);
2160   // Adds the content text.
2161   base::string16 elidedName(gfx::ElideText(
2162       avatarItem.name, gfx::FontList(), availableWidth, gfx::ELIDE_TAIL));
2163   NSTextField* contentLabel = BuildLabel(
2164       l10n_util::GetNSStringF(IDS_PROFILES_NOT_YOU_CONTENT_TEXT, elidedName),
2165       NSMakePoint(kHorizontalSpacing, yOffset + kVerticalSpacing),
2166       nil);
2167   [contentLabel setFrameSize:NSMakeSize(availableWidth, 0)];
2168   [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:contentLabel];
2169   [container addSubview:contentLabel];
2170   yOffset = NSMaxY([contentLabel frame]) + kVerticalSpacing;
2172   // Adds the title card.
2173   separator = [self horizontalSeparatorWithFrame:
2174       NSMakeRect(0, yOffset, kFixedSwitchUserViewWidth, 0)];
2175   [container addSubview:separator];
2176   yOffset = NSMaxY([separator frame]) + kVerticalSpacing;
2178   NSView* titleView = BuildTitleCard(
2179       NSMakeRect(0, yOffset, kFixedSwitchUserViewWidth,0),
2180       l10n_util::GetStringFUTF16(IDS_PROFILES_NOT_YOU, avatarItem.name),
2181       self /* backButtonTarget*/,
2182       @selector(navigateBackFromSwitchUserView:) /* backButtonAction */);
2183   [container addSubview:titleView];
2184   yOffset = NSMaxY([titleView frame]);
2186   [container setFrameSize:NSMakeSize(kFixedSwitchUserViewWidth, yOffset)];
2187   return container.autorelease();
2190 // Called when clicked on the settings link.
2191 - (BOOL)textView:(NSTextView*)textView
2192    clickedOnLink:(id)link
2193          atIndex:(NSUInteger)charIndex {
2194   chrome::ShowSettings(browser_);
2195   return YES;
2198 - (NSButton*)hoverButtonWithRect:(NSRect)rect
2199                             text:(NSString*)text
2200                  imageResourceId:(int)imageResourceId
2201                           action:(SEL)action {
2202   base::scoped_nsobject<BackgroundColorHoverButton> button(
2203       [[BackgroundColorHoverButton alloc]
2204           initWithFrame:rect
2205       imageTitleSpacing:kImageTitleSpacing
2206         backgroundColor:GetDialogBackgroundColor()]);
2208   [button setTitle:text];
2209   ui::ResourceBundle* rb = &ui::ResourceBundle::GetSharedInstance();
2210   NSImage* image = rb->GetNativeImageNamed(imageResourceId).ToNSImage();
2211   [button setDefaultImage:image];
2212   [button setHoverImage:image];
2213   [button setPressedImage:image];
2214   [button setImagePosition:NSImageLeft];
2215   [button setAlignment:NSLeftTextAlignment];
2216   [button setBordered:NO];
2217   [button setTarget:self];
2218   [button setAction:action];
2220   return button.autorelease();
2223 - (NSButton*)linkButtonWithTitle:(NSString*)title
2224                      frameOrigin:(NSPoint)frameOrigin
2225                           action:(SEL)action {
2226   base::scoped_nsobject<NSButton> link(
2227       [[HyperlinkButtonCell buttonWithString:title] retain]);
2229   [[link cell] setShouldUnderline:NO];
2230   [[link cell] setTextColor:gfx::SkColorToCalibratedNSColor(
2231       chrome_style::GetLinkColor())];
2232   [link setTitle:title];
2233   [link setBordered:NO];
2234   [link setFont:[NSFont labelFontOfSize:kTextFontSize]];
2235   [link setTarget:self];
2236   [link setAction:action];
2237   [link setFrameOrigin:frameOrigin];
2238   [link sizeToFit];
2240   return link.autorelease();
2243 - (NSButton*)accountButtonWithRect:(NSRect)rect
2244                          accountId:(const std::string&)accountId
2245                                tag:(int)tag
2246                     reauthRequired:(BOOL)reauthRequired {
2247   // Get display email address for account.
2248   std::string email = signin_ui_util::GetDisplayEmail(browser_->profile(),
2249                                                       accountId);
2251   ui::ResourceBundle* rb = &ui::ResourceBundle::GetSharedInstance();
2252   NSImage* deleteImage = rb->GetNativeImageNamed(IDR_CLOSE_1).ToNSImage();
2253   CGFloat deleteImageWidth = [deleteImage size].width;
2254   NSImage* warningImage = reauthRequired ? rb->GetNativeImageNamed(
2255       IDR_ICON_PROFILES_ACCOUNT_BUTTON_ERROR).ToNSImage() : nil;
2256   CGFloat warningImageWidth = [warningImage size].width;
2258   CGFloat availableTextWidth = rect.size.width - kHorizontalSpacing -
2259       warningImageWidth - deleteImageWidth;
2260   if (warningImage)
2261     availableTextWidth -= kHorizontalSpacing;
2263   NSColor* backgroundColor = gfx::SkColorToCalibratedNSColor(
2264       profiles::kAvatarBubbleAccountsBackgroundColor);
2265   base::scoped_nsobject<BackgroundColorHoverButton> button(
2266       [[BackgroundColorHoverButton alloc] initWithFrame:rect
2267                                       imageTitleSpacing:0
2268                                         backgroundColor:backgroundColor]);
2269   [button setTitle:ElideEmail(email, availableTextWidth)];
2270   [button setAlignment:NSLeftTextAlignment];
2271   [button setBordered:NO];
2272   if (reauthRequired) {
2273     [button setDefaultImage:warningImage];
2274     [button setImagePosition:NSImageLeft];
2275     [button setTarget:self];
2276     [button setAction:@selector(showAccountReauthenticationView:)];
2277     [button setTag:tag];
2278   }
2280   // Delete button.
2281   if (!browser_->profile()->IsSupervised()) {
2282     NSRect buttonRect;
2283     NSDivideRect(rect, &buttonRect, &rect,
2284         deleteImageWidth + kHorizontalSpacing, NSMaxXEdge);
2285     buttonRect.origin.y = 0;
2287     base::scoped_nsobject<HoverImageButton> deleteButton(
2288         [[HoverImageButton alloc] initWithFrame:buttonRect]);
2289     [deleteButton setBordered:NO];
2290     [deleteButton setDefaultImage:deleteImage];
2291     [deleteButton setHoverImage:rb->GetNativeImageNamed(
2292         IDR_CLOSE_1_H).ToNSImage()];
2293     [deleteButton setPressedImage:rb->GetNativeImageNamed(
2294         IDR_CLOSE_1_P).ToNSImage()];
2295     [deleteButton setTarget:self];
2296     [deleteButton setAction:@selector(showAccountRemovalView:)];
2297     [deleteButton setTag:tag];
2299     [button addSubview:deleteButton];
2300   }
2302   return button.autorelease();
2305 - (void)postActionPerformed:(ProfileMetrics::ProfileDesktopMenu)action {
2306   ProfileMetrics::LogProfileDesktopMenu(action, serviceType_);
2307   serviceType_ = signin::GAIA_SERVICE_TYPE_NONE;
2310 - (bool)shouldShowGoIncognito {
2311   bool incognitoAvailable =
2312       IncognitoModePrefs::GetAvailability(browser_->profile()->GetPrefs()) !=
2313           IncognitoModePrefs::DISABLED;
2314   return incognitoAvailable && !browser_->profile()->IsGuestSession();
2317 @end