1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #import "chrome/browser/ui/cocoa/profiles/profile_chooser_controller.h"
7 #import <Carbon/Carbon.h> // kVK_Return.
8 #import <Cocoa/Cocoa.h>
10 #include "base/mac/bundle_locations.h"
11 #include "base/prefs/pref_service.h"
12 #include "base/strings/string_util.h"
13 #include "base/strings/sys_string_conversions.h"
14 #include "base/strings/utf_string_conversions.h"
15 #include "chrome/app/chrome_command_ids.h"
16 #include "chrome/browser/browser_process.h"
17 #include "chrome/browser/chrome_notification_types.h"
18 #include "chrome/browser/lifetime/application_lifetime.h"
19 #include "chrome/browser/prefs/incognito_mode_prefs.h"
20 #include "chrome/browser/profiles/avatar_menu.h"
21 #include "chrome/browser/profiles/avatar_menu_observer.h"
22 #include "chrome/browser/profiles/profile_avatar_icon_util.h"
23 #include "chrome/browser/profiles/profile_info_cache.h"
24 #include "chrome/browser/profiles/profile_manager.h"
25 #include "chrome/browser/profiles/profile_metrics.h"
26 #include "chrome/browser/profiles/profile_window.h"
27 #include "chrome/browser/profiles/profiles_state.h"
28 #include "chrome/browser/signin/profile_oauth2_token_service_factory.h"
29 #include "chrome/browser/signin/signin_error_controller_factory.h"
30 #include "chrome/browser/signin/signin_header_helper.h"
31 #include "chrome/browser/signin/signin_manager_factory.h"
32 #include "chrome/browser/signin/signin_promo.h"
33 #include "chrome/browser/signin/signin_ui_util.h"
34 #include "chrome/browser/ui/browser.h"
35 #include "chrome/browser/ui/browser_commands.h"
36 #include "chrome/browser/ui/browser_window.h"
37 #include "chrome/browser/ui/chrome_pages.h"
38 #include "chrome/browser/ui/chrome_style.h"
39 #import "chrome/browser/ui/cocoa/browser_window_utils.h"
40 #import "chrome/browser/ui/cocoa/info_bubble_view.h"
41 #import "chrome/browser/ui/cocoa/info_bubble_window.h"
42 #import "chrome/browser/ui/cocoa/profiles/user_manager_mac.h"
43 #include "chrome/browser/ui/singleton_tabs.h"
44 #include "chrome/browser/ui/user_manager.h"
45 #include "chrome/browser/ui/webui/signin/login_ui_service.h"
46 #include "chrome/browser/ui/webui/signin/login_ui_service_factory.h"
47 #include "chrome/common/pref_names.h"
48 #include "chrome/common/url_constants.h"
49 #include "chrome/grit/chromium_strings.h"
50 #include "chrome/grit/generated_resources.h"
51 #include "components/signin/core/browser/mutable_profile_oauth2_token_service.h"
52 #include "components/signin/core/browser/profile_oauth2_token_service.h"
53 #include "components/signin/core/browser/signin_manager.h"
54 #include "components/signin/core/common/profile_management_switches.h"
55 #include "content/public/browser/native_web_keyboard_event.h"
56 #include "content/public/browser/notification_service.h"
57 #include "content/public/browser/render_widget_host_view.h"
58 #include "content/public/browser/web_contents.h"
59 #include "google_apis/gaia/oauth2_token_service.h"
60 #include "grit/theme_resources.h"
61 #include "skia/ext/skia_utils_mac.h"
62 #import "third_party/google_toolbox_for_mac/src/AppKit/GTMUILocalizerAndLayoutTweaker.h"
63 #import "ui/base/cocoa/cocoa_base_utils.h"
64 #import "ui/base/cocoa/controls/blue_label_button.h"
65 #import "ui/base/cocoa/controls/hyperlink_button_cell.h"
66 #import "ui/base/cocoa/controls/hyperlink_text_view.h"
67 #import "ui/base/cocoa/hover_image_button.h"
68 #include "ui/base/cocoa/window_size_constants.h"
69 #include "ui/base/l10n/l10n_util.h"
70 #include "ui/base/l10n/l10n_util_mac.h"
71 #include "ui/base/resource/resource_bundle.h"
72 #include "ui/gfx/image/image.h"
73 #include "ui/gfx/text_elider.h"
74 #include "ui/native_theme/common_theme.h"
75 #include "ui/native_theme/native_theme.h"
79 // Constants taken from the Windows/Views implementation at:
80 // chrome/browser/ui/views/profile_chooser_view.cc
81 const int kLargeImageSide = 88;
82 const int kSmallImageSide = 32;
83 const CGFloat kFixedMenuWidth = 250;
85 const CGFloat kVerticalSpacing = 16.0;
86 const CGFloat kSmallVerticalSpacing = 10.0;
87 const CGFloat kHorizontalSpacing = 16.0;
88 const CGFloat kTitleFontSize = 15.0;
89 const CGFloat kTextFontSize = 12.0;
90 const CGFloat kProfileButtonHeight = 30;
91 const int kBezelThickness = 3; // Width of the bezel on an NSButton.
92 const int kImageTitleSpacing = 10;
93 const int kBlueButtonHeight = 30;
94 const CGFloat kFocusRingLineWidth = 2;
96 // Fixed size for embedded sign in pages as defined in Gaia.
97 const CGFloat kFixedGaiaViewWidth = 360;
98 const CGFloat kFixedGaiaViewHeight = 440;
100 // Fixed size for the account removal view.
101 const CGFloat kFixedAccountRemovalViewWidth = 280;
103 // Fixed size for the switch user view.
104 const int kFixedSwitchUserViewWidth = 320;
106 // The tag number for the primary account.
107 const int kPrimaryProfileTag = -1;
109 gfx::Image CreateProfileImage(const gfx::Image& icon, int imageSize) {
110 return profiles::GetSizedAvatarIcon(
111 icon, true /* image is a square */, imageSize, imageSize);
114 // Updates the window size and position.
115 void SetWindowSize(NSWindow* window, NSSize size) {
116 NSRect frame = [window frame];
117 frame.origin.x += frame.size.width - size.width;
118 frame.origin.y += frame.size.height - size.height;
120 [window setFrame:frame display:YES];
123 NSString* ElideEmail(const std::string& email, CGFloat width) {
124 const base::string16 elidedEmail = gfx::ElideText(
125 base::UTF8ToUTF16(email), gfx::FontList(), width, gfx::ELIDE_EMAIL);
126 return base::SysUTF16ToNSString(elidedEmail);
129 NSString* ElideMessage(const base::string16& message, CGFloat width) {
130 return base::SysUTF16ToNSString(
131 gfx::ElideText(message, gfx::FontList(), width, gfx::ELIDE_TAIL));
134 // Builds a label with the given |title| anchored at |frame_origin|. Sets the
135 // text color to |text_color| if not null.
136 NSTextField* BuildLabel(NSString* title,
137 NSPoint frame_origin,
138 NSColor* text_color) {
139 base::scoped_nsobject<NSTextField> label(
140 [[NSTextField alloc] initWithFrame:NSZeroRect]);
141 [label setStringValue:title];
142 [label setEditable:NO];
143 [label setAlignment:NSLeftTextAlignment];
144 [label setBezeled:NO];
145 [label setFont:[NSFont labelFontOfSize:kTextFontSize]];
146 [label setDrawsBackground:NO];
147 [label setFrameOrigin:frame_origin];
151 [[label cell] setTextColor:text_color];
153 return label.autorelease();
156 // Builds an NSTextView that has the contents set to the specified |message|,
157 // with a non-underlined |link| inserted at |link_offset|. The view is anchored
158 // at the specified |frame_origin| and has a fixed |frame_width|.
159 NSTextView* BuildFixedWidthTextViewWithLink(
160 id<NSTextViewDelegate> delegate,
164 NSPoint frame_origin,
165 CGFloat frame_width) {
166 base::scoped_nsobject<HyperlinkTextView> text_view(
167 [[HyperlinkTextView alloc] initWithFrame:NSZeroRect]);
168 NSColor* link_color = gfx::SkColorToCalibratedNSColor(
169 chrome_style::GetLinkColor());
170 NSMutableString* finalMessage =
171 [NSMutableString stringWithFormat:@"%@\n", message];
172 [finalMessage insertString:link atIndex:link_offset];
173 // Adds a padding row at the bottom, because |boundingRectWithSize| below cuts
174 // off the last row sometimes.
175 [text_view setMessage:finalMessage
176 withFont:[NSFont labelFontOfSize:kTextFontSize]
177 messageColor:[NSColor blackColor]];
178 [text_view addLinkRange:NSMakeRange(link_offset, [link length])
180 linkColor:link_color];
182 // Removes the underlining from the link.
183 [text_view setLinkTextAttributes:nil];
184 NSTextStorage* text_storage = [text_view textStorage];
185 NSRange link_range = NSMakeRange(link_offset, [link length]);
186 [text_storage addAttribute:NSUnderlineStyleAttributeName
187 value:[NSNumber numberWithInt:NSUnderlineStyleNone]
190 NSRect frame = [[text_view attributedString]
191 boundingRectWithSize:NSMakeSize(frame_width, 0)
192 options:NSStringDrawingUsesLineFragmentOrigin];
193 frame.origin = frame_origin;
194 [text_view setFrame:frame];
195 [text_view setDelegate:delegate];
196 return text_view.autorelease();
199 // Returns the native dialog background color.
200 NSColor* GetDialogBackgroundColor() {
201 return gfx::SkColorToCalibratedNSColor(
202 ui::NativeTheme::instance()->GetSystemColor(
203 ui::NativeTheme::kColorId_DialogBackground));
206 // Builds a title card with one back button right aligned and one label center
208 NSView* BuildTitleCard(NSRect frame_rect,
209 const base::string16& message,
210 id back_button_target,
211 SEL back_button_action) {
212 base::scoped_nsobject<NSView> container(
213 [[NSView alloc] initWithFrame:frame_rect]);
215 base::scoped_nsobject<HoverImageButton> button(
216 [[HoverImageButton alloc] initWithFrame:frame_rect]);
217 [button setBordered:NO];
218 ui::ResourceBundle* rb = &ui::ResourceBundle::GetSharedInstance();
219 [button setDefaultImage:rb->GetNativeImageNamed(IDR_BACK).ToNSImage()];
220 [button setHoverImage:rb->GetNativeImageNamed(IDR_BACK_H).ToNSImage()];
221 [button setPressedImage:rb->GetNativeImageNamed(IDR_BACK_P).ToNSImage()];
222 [button setTarget:back_button_target];
223 [button setAction:back_button_action];
224 [button setFrameSize:NSMakeSize(kProfileButtonHeight, kProfileButtonHeight)];
225 [button setFrameOrigin:NSMakePoint(kHorizontalSpacing, 0)];
227 CGFloat max_label_width = frame_rect.size.width -
228 (kHorizontalSpacing * 2 + kProfileButtonHeight) * 2;
229 NSTextField* title_label = BuildLabel(
230 ElideMessage(message, max_label_width),
232 [title_label setAlignment:NSCenterTextAlignment];
233 [title_label setFont:[NSFont labelFontOfSize:kTitleFontSize]];
234 [title_label sizeToFit];
235 CGFloat x_offset = (frame_rect.size.width - NSWidth([title_label frame])) / 2;
237 (NSHeight([button frame]) - NSHeight([title_label frame])) / 2;
238 [title_label setFrameOrigin:NSMakePoint(x_offset, y_offset)];
240 [container addSubview:button];
241 [container addSubview:title_label];
242 CGFloat height = std::max(NSMaxY([title_label frame]),
243 NSMaxY([button frame])) + kVerticalSpacing;
244 [container setFrameSize:NSMakeSize(NSWidth([container frame]), height)];
246 return container.autorelease();
249 bool HasAuthError(Profile* profile) {
250 const SigninErrorController* error_controller =
251 SigninErrorControllerFactory::GetForProfile(profile);
252 return error_controller && error_controller->HasError();
255 std::string GetAuthErrorAccountId(Profile* profile) {
256 const SigninErrorController* error_controller =
257 SigninErrorControllerFactory::GetForProfile(profile);
258 if (!error_controller)
259 return std::string();
261 return error_controller->error_account_id();
266 // Custom WebContentsDelegate that allows handling of hotkeys and suppresses
268 class GaiaWebContentsDelegate : public content::WebContentsDelegate {
270 GaiaWebContentsDelegate() {}
271 ~GaiaWebContentsDelegate() override {}
274 // content::WebContentsDelegate:
275 bool HandleContextMenu(const content::ContextMenuParams& params) override;
276 void HandleKeyboardEvent(
277 content::WebContents* source,
278 const content::NativeWebKeyboardEvent& event) override;
280 DISALLOW_COPY_AND_ASSIGN(GaiaWebContentsDelegate);
283 bool GaiaWebContentsDelegate::HandleContextMenu(
284 const content::ContextMenuParams& params) {
285 // Suppresses the context menu because some features, such as inspecting
286 // elements, are not appropriate in a bubble.
290 void GaiaWebContentsDelegate::HandleKeyboardEvent(
291 content::WebContents* source,
292 const content::NativeWebKeyboardEvent& event) {
293 if (![BrowserWindowUtils shouldHandleKeyboardEvent:event])
296 int chrome_command_id = [BrowserWindowUtils getCommandId:event];
298 bool is_text_editing_command =
299 (event.modifiers & blink::WebInputEvent::MetaKey) &&
300 (event.windowsKeyCode == ui::VKEY_A ||
301 event.windowsKeyCode == ui::VKEY_V);
303 // TODO(guohui): maybe should add an accelerator for the back button.
304 if (chrome_command_id == IDC_CLOSE_WINDOW || chrome_command_id == IDC_EXIT ||
305 is_text_editing_command) {
306 [[NSApp mainMenu] performKeyEquivalent:event.os_event];
310 // Class that listens to changes to the OAuth2Tokens for the active profile,
311 // changes to the avatar menu model or browser close notifications.
312 class ActiveProfileObserverBridge : public AvatarMenuObserver,
313 public content::NotificationObserver,
314 public OAuth2TokenService::Observer {
316 ActiveProfileObserverBridge(ProfileChooserController* controller,
318 : controller_(controller),
320 token_observer_registered_(false) {
321 registrar_.Add(this, chrome::NOTIFICATION_BROWSER_CLOSING,
322 content::NotificationService::AllSources());
323 if (!browser_->profile()->IsGuestSession())
324 AddTokenServiceObserver();
327 ~ActiveProfileObserverBridge() override { RemoveTokenServiceObserver(); }
330 void AddTokenServiceObserver() {
331 ProfileOAuth2TokenService* oauth2_token_service =
332 ProfileOAuth2TokenServiceFactory::GetForProfile(browser_->profile());
333 DCHECK(oauth2_token_service);
334 oauth2_token_service->AddObserver(this);
335 token_observer_registered_ = true;
338 void RemoveTokenServiceObserver() {
339 if (!token_observer_registered_)
341 DCHECK(browser_->profile());
342 ProfileOAuth2TokenService* oauth2_token_service =
343 ProfileOAuth2TokenServiceFactory::GetForProfile(browser_->profile());
344 DCHECK(oauth2_token_service);
345 oauth2_token_service->RemoveObserver(this);
346 token_observer_registered_ = false;
349 // OAuth2TokenService::Observer:
350 void OnRefreshTokenAvailable(const std::string& account_id) override {
351 // Tokens can only be added by adding an account through the inline flow,
352 // which is started from the account management view. Refresh it to show the
354 profiles::BubbleViewMode viewMode = [controller_ viewMode];
355 if (viewMode == profiles::BUBBLE_VIEW_MODE_ACCOUNT_MANAGEMENT ||
356 viewMode == profiles::BUBBLE_VIEW_MODE_GAIA_ADD_ACCOUNT ||
357 viewMode == profiles::BUBBLE_VIEW_MODE_GAIA_REAUTH) {
358 [controller_ initMenuContentsWithView:
359 switches::IsEnableAccountConsistency() ?
360 profiles::BUBBLE_VIEW_MODE_ACCOUNT_MANAGEMENT :
361 profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER];
365 void OnRefreshTokenRevoked(const std::string& account_id) override {
366 // Tokens can only be removed from the account management view. Refresh it
367 // to show the update.
368 if ([controller_ viewMode] == profiles::BUBBLE_VIEW_MODE_ACCOUNT_MANAGEMENT)
369 [controller_ initMenuContentsWithView:
370 profiles::BUBBLE_VIEW_MODE_ACCOUNT_MANAGEMENT];
373 // AvatarMenuObserver:
374 void OnAvatarMenuChanged(AvatarMenu* avatar_menu) override {
375 profiles::BubbleViewMode viewMode = [controller_ viewMode];
376 if (viewMode == profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER ||
377 viewMode == profiles::BUBBLE_VIEW_MODE_ACCOUNT_MANAGEMENT) {
378 [controller_ initMenuContentsWithView:viewMode];
382 // content::NotificationObserver:
383 void Observe(int type,
384 const content::NotificationSource& source,
385 const content::NotificationDetails& details) override {
386 DCHECK_EQ(chrome::NOTIFICATION_BROWSER_CLOSING, type);
387 if (browser_ == content::Source<Browser>(source).ptr()) {
388 RemoveTokenServiceObserver();
389 // Clean up the bubble's WebContents (used by the Gaia embedded view), to
390 // make sure the guest profile doesn't have any dangling host renderers.
391 // This can happen if Chrome is quit using Command-Q while the bubble is
392 // still open, which won't give the bubble a chance to be closed and
393 // clean up the WebContents itself.
394 [controller_ cleanUpEmbeddedViewContents];
398 ProfileChooserController* controller_; // Weak; owns this.
399 Browser* browser_; // Weak.
400 content::NotificationRegistrar registrar_;
402 // The observer can be removed both when closing the browser, and by just
403 // closing the avatar bubble. However, in the case of closing the browser,
404 // the avatar bubble will also be closed afterwards, resulting in a second
405 // attempt to remove the observer. This ensures the observer is only
407 bool token_observer_registered_;
409 DISALLOW_COPY_AND_ASSIGN(ActiveProfileObserverBridge);
412 // Custom button cell that adds a left padding before the button image, and
413 // a custom spacing between the button image and title.
414 @interface CustomPaddingImageButtonCell : NSButtonCell {
416 // Padding added to the left margin of the button.
417 int leftMarginSpacing_;
418 // Spacing between the cell image and title.
419 int imageTitleSpacing_;
422 - (id)initWithLeftMarginSpacing:(int)leftMarginSpacing
423 imageTitleSpacing:(int)imageTitleSpacing;
426 @implementation CustomPaddingImageButtonCell
427 - (id)initWithLeftMarginSpacing:(int)leftMarginSpacing
428 imageTitleSpacing:(int)imageTitleSpacing {
429 if ((self = [super init])) {
430 leftMarginSpacing_ = leftMarginSpacing;
431 imageTitleSpacing_ = imageTitleSpacing;
436 - (NSRect)drawTitle:(NSAttributedString*)title
437 withFrame:(NSRect)frame
438 inView:(NSView*)controlView {
440 NSDivideRect(frame, &marginRect, &frame, leftMarginSpacing_, NSMinXEdge);
442 // The title frame origin isn't aware of the left margin spacing added
443 // in -drawImage, so it must be added when drawing the title as well.
444 if ([self imagePosition] == NSImageLeft)
445 NSDivideRect(frame, &marginRect, &frame, imageTitleSpacing_, NSMinXEdge);
447 return [super drawTitle:title withFrame:frame inView:controlView];
450 - (void)drawImage:(NSImage*)image
451 withFrame:(NSRect)frame
452 inView:(NSView*)controlView {
453 if ([self imagePosition] == NSImageLeft)
454 frame.origin.x = leftMarginSpacing_;
455 [super drawImage:image withFrame:frame inView:controlView];
459 NSSize buttonSize = [super cellSize];
460 buttonSize.width += leftMarginSpacing_;
461 if ([self imagePosition] == NSImageLeft)
462 buttonSize.width += imageTitleSpacing_;
466 - (NSFocusRingType)focusRingType {
467 // This is taken care of by the custom drawing code.
468 return NSFocusRingTypeNone;
471 - (void)drawWithFrame:(NSRect)frame inView:(NSView *)controlView {
472 [super drawInteriorWithFrame:frame inView:controlView];
475 if ([self showsFirstResponder]) {
476 NSRect focusRingRect =
477 NSInsetRect(frame, kFocusRingLineWidth, kFocusRingLineWidth);
478 // TODO(noms): When we are targetting 10.7, we should change this to use
479 // -drawFocusRingMaskWithFrame instead.
480 [[[NSColor keyboardFocusIndicatorColor] colorWithAlphaComponent:1] set];
481 NSBezierPath* path = [NSBezierPath bezierPathWithRect:focusRingRect];
482 [path setLineWidth:kFocusRingLineWidth];
489 // A custom image view that has a transparent backround.
490 @interface TransparentBackgroundImageView : NSImageView
493 @implementation TransparentBackgroundImageView
494 - (void)drawRect:(NSRect)dirtyRect {
495 NSColor* backgroundColor = [NSColor colorWithCalibratedWhite:1 alpha:0.6f];
496 [backgroundColor setFill];
497 NSRectFillUsingOperation(dirtyRect, NSCompositeSourceAtop);
498 [super drawRect:dirtyRect];
502 @interface CustomCircleImageCell : NSButtonCell
505 @implementation CustomCircleImageCell
506 - (void)drawWithFrame:(NSRect)frame inView:(NSView *)controlView {
507 // Display everything as a circle that spans the entire control.
508 NSBezierPath* path = [NSBezierPath bezierPathWithOvalInRect:frame];
510 [super drawImage:[self image] withFrame:frame inView:controlView];
514 // A custom image control that shows a "Change" button when moused over.
515 @interface EditableProfilePhoto : HoverImageButton {
517 AvatarMenu* avatarMenu_; // Weak; Owned by ProfileChooserController.
518 base::scoped_nsobject<TransparentBackgroundImageView> changePhotoImage_;
519 ProfileChooserController* controller_;
522 - (id)initWithFrame:(NSRect)frameRect
523 avatarMenu:(AvatarMenu*)avatarMenu
524 profileIcon:(const gfx::Image&)profileIcon
525 editingAllowed:(BOOL)editingAllowed
526 withController:(ProfileChooserController*)controller;
528 // Called when the "Change" button is clicked.
529 - (void)editPhoto:(id)sender;
533 @implementation EditableProfilePhoto
534 - (id)initWithFrame:(NSRect)frameRect
535 avatarMenu:(AvatarMenu*)avatarMenu
536 profileIcon:(const gfx::Image&)profileIcon
537 editingAllowed:(BOOL)editingAllowed
538 withController:(ProfileChooserController*)controller {
539 if ((self = [super initWithFrame:frameRect])) {
540 avatarMenu_ = avatarMenu;
541 controller_ = controller;
543 [self setBordered:NO];
545 base::scoped_nsobject<CustomCircleImageCell> cell(
546 [[CustomCircleImageCell alloc] init]);
547 [self setCell:cell.get()];
549 [self setDefaultImage:CreateProfileImage(
550 profileIcon, kLargeImageSide).ToNSImage()];
551 [self setImagePosition:NSImageOnly];
553 NSRect bounds = NSMakeRect(0, 0, kLargeImageSide, kLargeImageSide);
554 if (editingAllowed) {
555 [self setTarget:self];
556 [self setAction:@selector(editPhoto:)];
557 changePhotoImage_.reset([[TransparentBackgroundImageView alloc]
558 initWithFrame:bounds]);
559 [changePhotoImage_ setImage:ui::ResourceBundle::GetSharedInstance().
560 GetNativeImageNamed(IDR_ICON_PROFILES_EDIT_CAMERA).AsNSImage()];
561 [self addSubview:changePhotoImage_];
563 // Hide the image until the button is hovered over.
564 [changePhotoImage_ setHidden:YES];
567 // Set the image cell's accessibility strings to be the same as the
569 [[self cell] accessibilitySetOverrideValue:l10n_util::GetNSString(
571 IDS_PROFILES_NEW_AVATAR_MENU_CHANGE_PHOTO_ACCESSIBLE_NAME :
572 IDS_PROFILES_NEW_AVATAR_MENU_PHOTO_ACCESSIBLE_NAME)
573 forAttribute:NSAccessibilityTitleAttribute];
574 [[self cell] accessibilitySetOverrideValue:
575 editingAllowed ? NSAccessibilityButtonRole : NSAccessibilityImageRole
576 forAttribute:NSAccessibilityRoleAttribute];
577 [[self cell] accessibilitySetOverrideValue:
578 NSAccessibilityRoleDescription(NSAccessibilityButtonRole, nil)
579 forAttribute:NSAccessibilityRoleDescriptionAttribute];
581 // The button and the cell should read the same thing.
582 [self accessibilitySetOverrideValue:l10n_util::GetNSString(
584 IDS_PROFILES_NEW_AVATAR_MENU_CHANGE_PHOTO_ACCESSIBLE_NAME :
585 IDS_PROFILES_NEW_AVATAR_MENU_PHOTO_ACCESSIBLE_NAME)
586 forAttribute:NSAccessibilityTitleAttribute];
587 [self accessibilitySetOverrideValue:NSAccessibilityButtonRole
588 forAttribute:NSAccessibilityRoleAttribute];
589 [self accessibilitySetOverrideValue:
590 NSAccessibilityRoleDescription(NSAccessibilityButtonRole, nil)
591 forAttribute:NSAccessibilityRoleDescriptionAttribute];
596 - (void)editPhoto:(id)sender {
597 avatarMenu_->EditProfile(avatarMenu_->GetActiveProfileIndex());
599 postActionPerformed:ProfileMetrics::PROFILE_DESKTOP_MENU_EDIT_IMAGE];
602 - (void)setHoverState:(HoverState)state {
603 [super setHoverState:state];
604 [changePhotoImage_ setHidden:([self hoverState] == kHoverStateNone)];
607 - (BOOL)canBecomeKeyView {
611 - (BOOL)accessibilityIsIgnored {
615 - (NSArray*)accessibilityActionNames {
616 NSArray* parentActions = [super accessibilityActionNames];
617 return [parentActions arrayByAddingObject:NSAccessibilityPressAction];
620 - (void)accessibilityPerformAction:(NSString*)action {
621 if ([action isEqualToString:NSAccessibilityPressAction]) {
622 avatarMenu_->EditProfile(avatarMenu_->GetActiveProfileIndex());
625 [super accessibilityPerformAction:action];
630 // A custom text control that turns into a textfield for editing when clicked.
631 @interface EditableProfileNameButton : HoverImageButton<NSTextFieldDelegate> {
633 base::scoped_nsobject<NSTextField> profileNameTextField_;
634 Profile* profile_; // Weak.
635 ProfileChooserController* controller_;
638 - (id)initWithFrame:(NSRect)frameRect
639 profile:(Profile*)profile
640 profileName:(NSString*)profileName
641 editingAllowed:(BOOL)editingAllowed
642 withController:(ProfileChooserController*)controller;
644 // Called when the button is clicked.
645 - (void)showEditableView:(id)sender;
647 // Called when enter is pressed in the text field.
648 - (void)saveProfileName;
652 @implementation EditableProfileNameButton
653 - (id)initWithFrame:(NSRect)frameRect
654 profile:(Profile*)profile
655 profileName:(NSString*)profileName
656 editingAllowed:(BOOL)editingAllowed
657 withController:(ProfileChooserController*)controller {
658 if ((self = [super initWithFrame:frameRect])) {
660 controller_ = controller;
662 if (editingAllowed) {
663 // Show an "edit" pencil icon when hovering over. In the default state,
664 // we need to create an empty placeholder of the correct size, so that
665 // the text doesn't jump around when the hovered icon appears.
666 ui::ResourceBundle* rb = &ui::ResourceBundle::GetSharedInstance();
667 NSImage* hoverImage = rb->GetNativeImageNamed(
668 IDR_ICON_PROFILES_EDIT_HOVER).AsNSImage();
670 // In order to center the button title, we need to add a left padding of
671 // the same width as the pencil icon.
672 base::scoped_nsobject<CustomPaddingImageButtonCell> cell(
673 [[CustomPaddingImageButtonCell alloc]
674 initWithLeftMarginSpacing:[hoverImage size].width
675 imageTitleSpacing:0]);
676 [self setCell:cell.get()];
678 NSImage* placeholder = [[NSImage alloc] initWithSize:[hoverImage size]];
679 [self setDefaultImage:placeholder];
680 [self setHoverImage:hoverImage];
681 [self setAlternateImage:
682 rb->GetNativeImageNamed(IDR_ICON_PROFILES_EDIT_PRESSED).AsNSImage()];
683 [self setImagePosition:NSImageRight];
684 [self setTarget:self];
685 [self setAction:@selector(showEditableView:)];
687 // We need to subtract the width of the bezel from the frame rect, so that
688 // the textfield can take the exact same space as the button.
689 frameRect.size.height -= 2 * kBezelThickness;
690 frameRect.origin = NSMakePoint(0, kBezelThickness);
691 profileNameTextField_.reset(
692 [[NSTextField alloc] initWithFrame:frameRect]);
693 [profileNameTextField_ setStringValue:profileName];
694 [profileNameTextField_ setFont:[NSFont labelFontOfSize:kTitleFontSize]];
695 [profileNameTextField_ setEditable:YES];
696 [profileNameTextField_ setDrawsBackground:YES];
697 [profileNameTextField_ setBezeled:YES];
698 [profileNameTextField_ setAlignment:NSCenterTextAlignment];
699 [[profileNameTextField_ cell] setWraps:NO];
700 [[profileNameTextField_ cell] setLineBreakMode:
701 NSLineBreakByTruncatingTail];
702 [[profileNameTextField_ cell] setUsesSingleLineMode:YES];
703 [self addSubview:profileNameTextField_];
704 [profileNameTextField_ setDelegate:self];
706 // Hide the textfield until the user clicks on the button.
707 [profileNameTextField_ setHidden:YES];
709 [[self cell] accessibilitySetOverrideValue:l10n_util::GetNSStringF(
710 IDS_PROFILES_NEW_AVATAR_MENU_EDIT_NAME_ACCESSIBLE_NAME,
711 base::SysNSStringToUTF16(profileName))
712 forAttribute:NSAccessibilityTitleAttribute];
714 NSSize textSize = [profileName sizeWithAttributes:@{
715 NSFontAttributeName : [profileNameTextField_ font]
718 if (textSize.width > frameRect.size.width - [hoverImage size].width * 2)
719 [self setToolTip:profileName];
722 [[self cell] accessibilitySetOverrideValue:NSAccessibilityButtonRole
723 forAttribute:NSAccessibilityRoleAttribute];
725 accessibilitySetOverrideValue:NSAccessibilityRoleDescription(
726 NSAccessibilityButtonRole, nil)
727 forAttribute:NSAccessibilityRoleDescriptionAttribute];
729 [self setBordered:NO];
730 [self setFont:[NSFont labelFontOfSize:kTitleFontSize]];
731 [self setAlignment:NSCenterTextAlignment];
732 [[self cell] setLineBreakMode:NSLineBreakByTruncatingTail];
733 [self setTitle:profileName];
738 - (void)saveProfileName {
739 base::string16 newProfileName =
740 base::SysNSStringToUTF16([profileNameTextField_ stringValue]);
742 // Empty profile names are not allowed, and do nothing.
743 base::TrimWhitespace(newProfileName, base::TRIM_ALL, &newProfileName);
744 if (!newProfileName.empty()) {
745 profiles::UpdateProfileName(profile_, newProfileName);
747 postActionPerformed:ProfileMetrics::PROFILE_DESKTOP_MENU_EDIT_NAME];
748 [profileNameTextField_ setHidden:YES];
752 - (void)showEditableView:(id)sender {
753 [profileNameTextField_ setHidden:NO];
754 [[self window] makeFirstResponder:profileNameTextField_];
757 - (BOOL)canBecomeKeyView {
761 - (BOOL)control:(NSControl*)control
762 textView:(NSTextView*)textView
763 doCommandBySelector:(SEL)commandSelector {
764 if (commandSelector == @selector(insertTab:) ||
765 commandSelector == @selector(insertNewline:)) {
766 [self saveProfileName];
774 // A custom button that allows for setting a background color when hovered over.
775 @interface BackgroundColorHoverButton : HoverImageButton {
777 base::scoped_nsobject<NSColor> backgroundColor_;
778 base::scoped_nsobject<NSColor> hoverColor_;
782 @implementation BackgroundColorHoverButton
784 - (id)initWithFrame:(NSRect)frameRect
785 imageTitleSpacing:(int)imageTitleSpacing
786 backgroundColor:(NSColor*)backgroundColor {
787 if ((self = [super initWithFrame:frameRect])) {
788 backgroundColor_.reset([backgroundColor retain]);
789 // Use a color from the common theme, since this button is not trying to
790 // look like a native control.
792 bool found = ui::CommonThemeGetSystemColor(
793 ui::NativeTheme::kColorId_ButtonHoverBackgroundColor, &hoverColor);
795 hoverColor_.reset([gfx::SkColorToSRGBNSColor(hoverColor) retain]);
797 [self setBordered:NO];
798 [self setFont:[NSFont labelFontOfSize:kTextFontSize]];
799 [self setButtonType:NSMomentaryChangeButton];
801 base::scoped_nsobject<CustomPaddingImageButtonCell> cell(
802 [[CustomPaddingImageButtonCell alloc]
803 initWithLeftMarginSpacing:kHorizontalSpacing
804 imageTitleSpacing:imageTitleSpacing]);
805 [cell setLineBreakMode:NSLineBreakByTruncatingTail];
806 [self setCell:cell.get()];
811 - (void)setHoverState:(HoverState)state {
812 [super setHoverState:state];
813 bool isHighlighted = ([self hoverState] != kHoverStateNone);
815 NSColor* backgroundColor = isHighlighted ? hoverColor_ : backgroundColor_;
816 [[self cell] setBackgroundColor:backgroundColor];
819 -(void)keyDown:(NSEvent*)event {
820 // Since there is no default button in the bubble, it is safe to activate
821 // all buttons on Enter as well, and be consistent with the Windows
823 if ([event keyCode] == kVK_Return)
824 [self performClick:self];
826 [super keyDown:event];
829 - (BOOL)canBecomeKeyView {
835 // A custom view with the given background color.
836 @interface BackgroundColorView : NSView {
838 base::scoped_nsobject<NSColor> backgroundColor_;
842 @implementation BackgroundColorView
843 - (id)initWithFrame:(NSRect)frameRect
844 withColor:(NSColor*)color {
845 if ((self = [super initWithFrame:frameRect]))
846 backgroundColor_.reset([color retain]);
850 - (void)drawRect:(NSRect)dirtyRect {
851 [backgroundColor_ setFill];
852 NSRectFill(dirtyRect);
853 [super drawRect:dirtyRect];
857 // A custom dummy button that is used to clear focus from the bubble's controls.
858 @interface DummyWindowFocusButton : NSButton
861 @implementation DummyWindowFocusButton
862 // Ignore accessibility, as this is a placeholder button.
863 - (BOOL)accessibilityIsIgnored {
867 - (id)accessibilityAttributeValue:(NSString*)attribute {
871 - (BOOL)canBecomeKeyView {
877 @interface ProfileChooserController ()
878 // Adds an horizontal separator to |container| at |yOffset| and returns the
879 // yOffset corresponding to after the separator.
880 - (CGFloat)addSeparatorToContainer:(NSView*)container
881 atYOffset:(CGFloat)yOffset;
883 // Builds the right-click profile switcher.
884 - (void)buildFastUserSwitcherViewWithProfiles:(NSMutableArray*)otherProfiles
885 atYOffset:(CGFloat)yOffset
886 inContainer:(NSView*)container;
888 // Builds the regular profile chooser view.
889 - (void)buildProfileChooserViewWithProfileView:(NSView*)currentProfileView
890 tutorialView:(NSView*)tutorialView
891 atYOffset:(CGFloat)yOffset
892 inContainer:(NSView*)container
893 displayLock:(bool)displayLock;
895 // Builds the profile chooser view.
896 - (NSView*)buildProfileChooserView;
898 - (NSView*)buildTutorialViewIfNeededForItem:(const AvatarMenu::Item&)item;
900 // Builds a tutorial card with a title label using |titleMessage|, a content
901 // label using |contentMessage|, a link using |linkMessage|, and a button using
902 // |buttonMessage|. If |stackButton| is YES, places the button above the link.
903 // Otherwise places both on the same row with the link left aligned and button
904 // right aligned. On click, the link would execute |linkAction|, and the button
905 // would execute |buttonAction|. It sets |tutorialMode_| to the given |mode|.
906 - (NSView*)tutorialViewWithMode:(profiles::TutorialMode)mode
907 titleMessage:(NSString*)titleMessage
908 contentMessage:(NSString*)contentMessage
909 linkMessage:(NSString*)linkMessage
910 buttonMessage:(NSString*)buttonMessage
911 stackButton:(BOOL)stackButton
912 hasCloseButton:(BOOL)hasCloseButton
913 linkAction:(SEL)linkAction
914 buttonAction:(SEL)buttonAction;
916 // Builds a tutorial card to introduce an upgrade user to the new avatar menu if
917 // needed. |tutorial_shown| indicates if the tutorial has already been shown in
918 // the previous active view. |avatar_item| refers to the current profile.
919 - (NSView*)buildWelcomeUpgradeTutorialView:(const AvatarMenu::Item&)item;
921 // Builds a tutorial card to inform the user about right-click user switching if
923 - (NSView*)buildRightClickTutorialView;
925 // Builds a tutorial card to have the user confirm the last Chrome signin,
926 // Chrome sync will be delayed until the user either dismisses the tutorial, or
927 // configures sync through the "Settings" link.
928 - (NSView*)buildSigninConfirmationView;
930 // Builds a tutorial card to show the last signin error.
931 - (NSView*)buildSigninErrorView;
933 // Creates the main profile card for the profile |item| at the top of
935 - (NSView*)createCurrentProfileView:(const AvatarMenu::Item&)item;
937 // Creates the possible links for the main profile card with profile |item|.
938 - (NSView*)createCurrentProfileLinksForItem:(const AvatarMenu::Item&)item
941 // Creates the disclaimer text for supervised users, telling them that the
942 // manager can view their history etc.
943 - (NSView*)createSupervisedUserDisclaimerView;
945 // Creates a main profile card for the guest user.
946 - (NSView*)createGuestProfileView;
948 // Creates an item for the profile |itemIndex| that is used in the fast profile
949 // switcher in the middle of the bubble.
950 - (NSButton*)createOtherProfileView:(int)itemIndex;
952 // Creates the "Not you" and Lock option buttons.
953 - (NSView*)createOptionsViewWithRect:(NSRect)rect
954 displayLock:(BOOL)displayLock;
956 // Creates the account management view for the active profile.
957 - (NSView*)createCurrentProfileAccountsView:(NSRect)rect;
959 // Creates the list of accounts for the active profile.
960 - (NSView*)createAccountsListWithRect:(NSRect)rect;
962 // Creates the Gaia sign-in/add account view.
963 - (NSView*)buildGaiaEmbeddedView;
965 // Creates the account removal view.
966 - (NSView*)buildAccountRemovalView;
968 // Create a view that shows various options for an upgrade user who is not
969 // the same person as the currently signed in user.
970 - (NSView*)buildSwitchUserView;
972 // Creates a button with |text|, an icon given by |imageResourceId| and with
974 - (NSButton*)hoverButtonWithRect:(NSRect)rect
976 imageResourceId:(int)imageResourceId
979 // Creates a generic link button with |title| and an |action| positioned at
981 - (NSButton*)linkButtonWithTitle:(NSString*)title
982 frameOrigin:(NSPoint)frameOrigin
985 // Creates an email account button with |title| and a remove icon. If
986 // |reauthRequired| is true, the button also displays a warning icon. |tag|
987 // indicates which account the button refers to.
988 - (NSButton*)accountButtonWithRect:(NSRect)rect
989 accountId:(const std::string&)accountId
991 reauthRequired:(BOOL)reauthRequired;
993 - (bool)shouldShowGoIncognito;
996 @implementation ProfileChooserController
997 - (profiles::BubbleViewMode) viewMode {
1001 - (void)setTutorialMode:(profiles::TutorialMode)tutorialMode {
1002 tutorialMode_ = tutorialMode;
1005 - (IBAction)switchToProfile:(id)sender {
1006 // Check the event flags to see if a new window should be created.
1007 bool alwaysCreate = ui::WindowOpenDispositionFromNSEvent(
1008 [NSApp currentEvent]) == NEW_WINDOW;
1009 avatarMenu_->SwitchToProfile([sender tag], alwaysCreate,
1010 ProfileMetrics::SWITCH_PROFILE_ICON);
1013 - (IBAction)showUserManager:(id)sender {
1014 UserManager::Show(base::FilePath(),
1015 profiles::USER_MANAGER_NO_TUTORIAL,
1016 profiles::USER_MANAGER_SELECT_PROFILE_NO_ACTION);
1017 [self postActionPerformed:
1018 ProfileMetrics::PROFILE_DESKTOP_MENU_OPEN_USER_MANAGER];
1021 - (IBAction)exitGuest:(id)sender {
1022 DCHECK(browser_->profile()->IsGuestSession());
1023 UserManager::Show(base::FilePath(),
1024 profiles::USER_MANAGER_NO_TUTORIAL,
1025 profiles::USER_MANAGER_SELECT_PROFILE_NO_ACTION);
1026 profiles::CloseGuestProfileWindows();
1029 - (IBAction)goIncognito:(id)sender {
1030 DCHECK([self shouldShowGoIncognito]);
1031 chrome::NewIncognitoWindow(browser_);
1032 [self postActionPerformed:
1033 ProfileMetrics::PROFILE_DESKTOP_MENU_GO_INCOGNITO];
1036 - (IBAction)showAccountManagement:(id)sender {
1037 [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_ACCOUNT_MANAGEMENT];
1040 - (IBAction)hideAccountManagement:(id)sender {
1041 [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER];
1044 - (IBAction)lockProfile:(id)sender {
1045 profiles::LockProfile(browser_->profile());
1046 [self postActionPerformed:ProfileMetrics::PROFILE_DESKTOP_MENU_LOCK];
1049 - (IBAction)showInlineSigninPage:(id)sender {
1050 [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_GAIA_SIGNIN];
1053 - (IBAction)addAccount:(id)sender {
1054 [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_GAIA_ADD_ACCOUNT];
1055 [self postActionPerformed:ProfileMetrics::PROFILE_DESKTOP_MENU_ADD_ACCT];
1058 - (IBAction)navigateBackFromSigninPage:(id)sender {
1059 std::string primaryAccount = SigninManagerFactory::GetForProfile(
1060 browser_->profile())->GetAuthenticatedAccountId();
1061 bool hasAccountManagement = !primaryAccount.empty() &&
1062 switches::IsEnableAccountConsistency();
1063 [self initMenuContentsWithView:hasAccountManagement ?
1064 profiles::BUBBLE_VIEW_MODE_ACCOUNT_MANAGEMENT :
1065 profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER];
1068 - (IBAction)showAccountRemovalView:(id)sender {
1069 DCHECK(!isGuestSession_);
1071 // Tag is either |kPrimaryProfileTag| for the primary account, or equal to the
1072 // index in |currentProfileAccounts_| for a secondary account.
1073 int tag = [sender tag];
1074 if (tag == kPrimaryProfileTag) {
1075 accountIdToRemove_ = SigninManagerFactory::GetForProfile(
1076 browser_->profile())->GetAuthenticatedAccountId();
1078 DCHECK(ContainsKey(currentProfileAccounts_, tag));
1079 accountIdToRemove_ = currentProfileAccounts_[tag];
1082 [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_ACCOUNT_REMOVAL];
1085 - (IBAction)showAccountReauthenticationView:(id)sender {
1086 DCHECK(!isGuestSession_);
1087 [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_GAIA_REAUTH];
1090 - (IBAction)removeAccount:(id)sender {
1091 DCHECK(!accountIdToRemove_.empty());
1092 ProfileOAuth2TokenServiceFactory::GetPlatformSpecificForProfile(
1093 browser_->profile())->RevokeCredentials(accountIdToRemove_);
1094 [self postActionPerformed:ProfileMetrics::PROFILE_DESKTOP_MENU_REMOVE_ACCT];
1095 accountIdToRemove_.clear();
1097 [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_ACCOUNT_MANAGEMENT];
1100 - (IBAction)seeWhatsNew:(id)sender {
1101 UserManager::Show(base::FilePath(),
1102 profiles::USER_MANAGER_TUTORIAL_OVERVIEW,
1103 profiles::USER_MANAGER_SELECT_PROFILE_NO_ACTION);
1104 ProfileMetrics::LogProfileNewAvatarMenuUpgrade(
1105 ProfileMetrics::PROFILE_AVATAR_MENU_UPGRADE_WHATS_NEW);
1108 - (IBAction)showSwitchUserView:(id)sender {
1109 [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_SWITCH_USER];
1110 ProfileMetrics::LogProfileNewAvatarMenuUpgrade(
1111 ProfileMetrics::PROFILE_AVATAR_MENU_UPGRADE_NOT_YOU);
1114 - (IBAction)showLearnMorePage:(id)sender {
1115 signin_ui_util::ShowSigninErrorLearnMorePage(browser_->profile());
1118 - (IBAction)configureSyncSettings:(id)sender {
1119 tutorialMode_ = profiles::TUTORIAL_MODE_NONE;
1120 LoginUIServiceFactory::GetForProfile(browser_->profile())->
1121 SyncConfirmationUIClosed(true);
1122 ProfileMetrics::LogProfileNewAvatarMenuSignin(
1123 ProfileMetrics::PROFILE_AVATAR_MENU_SIGNIN_SETTINGS);
1126 - (IBAction)syncSettingsConfirmed:(id)sender {
1127 tutorialMode_ = profiles::TUTORIAL_MODE_NONE;
1128 LoginUIServiceFactory::GetForProfile(browser_->profile())->
1129 SyncConfirmationUIClosed(false);
1130 ProfileMetrics::LogProfileNewAvatarMenuSignin(
1131 ProfileMetrics::PROFILE_AVATAR_MENU_SIGNIN_OK);
1132 [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER];
1135 - (IBAction)disconnectProfile:(id)sender {
1136 chrome::ShowSettings(browser_);
1137 ProfileMetrics::LogProfileNewAvatarMenuNotYou(
1138 ProfileMetrics::PROFILE_AVATAR_MENU_NOT_YOU_DISCONNECT);
1141 - (IBAction)navigateBackFromSwitchUserView:(id)sender {
1142 [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER];
1143 ProfileMetrics::LogProfileNewAvatarMenuNotYou(
1144 ProfileMetrics::PROFILE_AVATAR_MENU_NOT_YOU_BACK);
1147 - (IBAction)dismissTutorial:(id)sender {
1148 // Never shows the upgrade tutorial again if manually closed.
1149 if (tutorialMode_ == profiles::TUTORIAL_MODE_WELCOME_UPGRADE) {
1150 browser_->profile()->GetPrefs()->SetInteger(
1151 prefs::kProfileAvatarTutorialShown,
1152 signin_ui_util::kUpgradeWelcomeTutorialShowMax + 1);
1155 if(tutorialMode_ == profiles::TUTORIAL_MODE_RIGHT_CLICK_SWITCHING) {
1156 PrefService* localState = g_browser_process->local_state();
1157 localState->SetBoolean(
1158 prefs::kProfileAvatarRightClickTutorialDismissed, true);
1161 tutorialMode_ = profiles::TUTORIAL_MODE_NONE;
1162 [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER];
1165 - (void)windowWillClose:(NSNotification*)notification {
1166 if (tutorialMode_ == profiles::TUTORIAL_MODE_CONFIRM_SIGNIN) {
1167 LoginUIServiceFactory::GetForProfile(browser_->profile())->
1168 SyncConfirmationUIClosed(false);
1171 [super windowWillClose:notification];
1174 - (void)moveDown:(id)sender {
1175 [[self window] selectNextKeyView:self];
1178 - (void)moveUp:(id)sender {
1179 [[self window] selectPreviousKeyView:self];
1182 - (void)cleanUpEmbeddedViewContents {
1183 webContents_.reset();
1184 webContentsDelegate_.reset();
1187 - (id)initWithBrowser:(Browser*)browser
1188 anchoredAt:(NSPoint)point
1189 viewMode:(profiles::BubbleViewMode)viewMode
1190 tutorialMode:(profiles::TutorialMode)tutorialMode
1191 serviceType:(signin::GAIAServiceType)serviceType {
1192 base::scoped_nsobject<InfoBubbleWindow> window([[InfoBubbleWindow alloc]
1193 initWithContentRect:ui::kWindowSizeDeterminedLater
1194 styleMask:NSBorderlessWindowMask
1195 backing:NSBackingStoreBuffered
1198 if ((self = [super initWithWindow:window
1199 parentWindow:browser->window()->GetNativeWindow()
1200 anchoredAt:point])) {
1202 viewMode_ = viewMode;
1203 tutorialMode_ = tutorialMode;
1204 observer_.reset(new ActiveProfileObserverBridge(self, browser_));
1205 serviceType_ = serviceType;
1207 avatarMenu_.reset(new AvatarMenu(
1208 &g_browser_process->profile_manager()->GetProfileInfoCache(),
1211 avatarMenu_->RebuildMenu();
1213 // Guest profiles do not have a token service.
1214 isGuestSession_ = browser_->profile()->IsGuestSession();
1216 // If view mode is PROFILE_CHOOSER but there is an auth error, force
1217 // ACCOUNT_MANAGEMENT mode.
1218 if (viewMode_ == profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER &&
1219 HasAuthError(browser_->profile()) &&
1220 switches::IsEnableAccountConsistency() &&
1221 avatarMenu_->GetItemAt(avatarMenu_->GetActiveProfileIndex()).
1223 viewMode_ = profiles::BUBBLE_VIEW_MODE_ACCOUNT_MANAGEMENT;
1226 [window accessibilitySetOverrideValue:
1227 l10n_util::GetNSString(IDS_PROFILES_NEW_AVATAR_MENU_ACCESSIBLE_NAME)
1228 forAttribute:NSAccessibilityTitleAttribute];
1229 [window accessibilitySetOverrideValue:
1230 l10n_util::GetNSString(IDS_PROFILES_NEW_AVATAR_MENU_ACCESSIBLE_NAME)
1231 forAttribute:NSAccessibilityHelpAttribute];
1233 [[self bubble] setAlignment:info_bubble::kAlignRightEdgeToAnchorEdge];
1234 [[self bubble] setArrowLocation:info_bubble::kNoArrow];
1235 [[self bubble] setBackgroundColor:GetDialogBackgroundColor()];
1236 [self initMenuContentsWithView:viewMode_];
1242 - (void)initMenuContentsWithView:(profiles::BubbleViewMode)viewToDisplay {
1243 if (browser_->profile()->IsSupervised() &&
1244 (viewToDisplay == profiles::BUBBLE_VIEW_MODE_GAIA_ADD_ACCOUNT ||
1245 viewToDisplay == profiles::BUBBLE_VIEW_MODE_ACCOUNT_REMOVAL)) {
1246 LOG(WARNING) << "Supervised user attempted to add/remove account";
1249 viewMode_ = viewToDisplay;
1250 NSView* contentView = [[self window] contentView];
1251 [contentView setSubviews:[NSArray array]];
1254 switch (viewMode_) {
1255 case profiles::BUBBLE_VIEW_MODE_GAIA_SIGNIN:
1256 case profiles::BUBBLE_VIEW_MODE_GAIA_ADD_ACCOUNT:
1257 case profiles::BUBBLE_VIEW_MODE_GAIA_REAUTH:
1258 subView = [self buildGaiaEmbeddedView];
1260 case profiles::BUBBLE_VIEW_MODE_ACCOUNT_REMOVAL:
1261 subView = [self buildAccountRemovalView];
1263 case profiles::BUBBLE_VIEW_MODE_SWITCH_USER:
1264 subView = [self buildSwitchUserView];
1266 case profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER:
1267 case profiles::BUBBLE_VIEW_MODE_FAST_PROFILE_CHOOSER:
1268 case profiles::BUBBLE_VIEW_MODE_ACCOUNT_MANAGEMENT:
1269 subView = [self buildProfileChooserView];
1273 // Clears tutorial mode for all non-profile-chooser views.
1274 if (viewMode_ != profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER)
1275 tutorialMode_ = profiles::TUTORIAL_MODE_NONE;
1277 if (viewMode_ == profiles::BUBBLE_VIEW_MODE_GAIA_SIGNIN ||
1278 viewMode_ == profiles::BUBBLE_VIEW_MODE_GAIA_ADD_ACCOUNT ||
1279 viewMode_ == profiles::BUBBLE_VIEW_MODE_GAIA_REAUTH ||
1280 tutorialMode_ == profiles::TUTORIAL_MODE_CONFIRM_SIGNIN) {
1281 [self setShouldCloseOnResignKey:NO];
1283 [self setShouldCloseOnResignKey:YES];
1286 // Add a dummy, empty element so that we don't initially display any
1288 NSButton* dummyFocusButton =
1289 [[[DummyWindowFocusButton alloc] initWithFrame:NSZeroRect] autorelease];
1290 [dummyFocusButton setNextKeyView:subView];
1291 [[self window] makeFirstResponder:dummyFocusButton];
1293 [contentView addSubview:subView];
1294 [contentView addSubview:dummyFocusButton];
1295 SetWindowSize([self window],
1296 NSMakeSize(NSWidth([subView frame]), NSHeight([subView frame])));
1299 - (CGFloat)addSeparatorToContainer:(NSView*)container
1300 atYOffset:(CGFloat)yOffset {
1301 NSBox* separator = [self horizontalSeparatorWithFrame:NSMakeRect(
1302 0, yOffset, kFixedMenuWidth, 0)];
1303 [container addSubview:separator];
1304 return NSMaxY([separator frame]);
1307 // Builds the fast user switcher view in |container| at |yOffset| and populates
1308 // it with the entries for every profile in |otherProfiles|. Returns the new
1309 // yOffset after adding the elements.
1310 - (void)buildFastUserSwitcherViewWithProfiles:(NSMutableArray*)otherProfiles
1311 atYOffset:(CGFloat)yOffset
1312 inContainer:(NSView*)container {
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) {
1316 [otherProfileView setFrameOrigin:NSMakePoint(0, yOffset)];
1317 [container addSubview:otherProfileView];
1318 yOffset = NSMaxY([otherProfileView frame]);
1320 yOffset = [self addSeparatorToContainer:container atYOffset: yOffset];
1323 [container setFrameSize:NSMakeSize(kFixedMenuWidth, yOffset)];
1326 - (void)buildProfileChooserViewWithProfileView:(NSView*)currentProfileView
1327 tutorialView:(NSView*)tutorialView
1328 atYOffset:(CGFloat)yOffset
1329 inContainer:(NSView*)container
1330 displayLock:(bool)displayLock {
1332 NSRect rect = NSMakeRect(0, yOffset, kFixedMenuWidth, 0);
1333 NSView* optionsView = [self createOptionsViewWithRect:rect
1334 displayLock:displayLock];
1335 [container addSubview:optionsView];
1336 rect.origin.y = NSMaxY([optionsView frame]);
1338 NSBox* separator = [self horizontalSeparatorWithFrame:rect];
1339 [container addSubview:separator];
1340 yOffset = NSMaxY([separator frame]);
1342 // For supervised users, add the disclaimer text.
1343 if (browser_->profile()->IsSupervised()) {
1344 yOffset += kSmallVerticalSpacing;
1345 NSView* disclaimerContainer = [self createSupervisedUserDisclaimerView];
1346 [disclaimerContainer setFrameOrigin:NSMakePoint(0, yOffset)];
1347 [container addSubview:disclaimerContainer];
1348 yOffset = NSMaxY([disclaimerContainer frame]);
1349 yOffset += kSmallVerticalSpacing;
1351 yOffset = [self addSeparatorToContainer:container atYOffset: yOffset];
1354 if (viewMode_ == profiles::BUBBLE_VIEW_MODE_ACCOUNT_MANAGEMENT) {
1355 NSView* currentProfileAccountsView = [self createCurrentProfileAccountsView:
1356 NSMakeRect(0, yOffset, kFixedMenuWidth, 0)];
1357 [container addSubview:currentProfileAccountsView];
1358 yOffset = NSMaxY([currentProfileAccountsView frame]);
1360 yOffset = [self addSeparatorToContainer:container atYOffset: yOffset];
1363 // Active profile card.
1364 if (currentProfileView) {
1365 yOffset += kVerticalSpacing;
1366 [currentProfileView setFrameOrigin:NSMakePoint(0, yOffset)];
1367 [container addSubview:currentProfileView];
1368 yOffset = NSMaxY([currentProfileView frame]) + kVerticalSpacing;
1372 [tutorialView setFrameOrigin:NSMakePoint(0, yOffset)];
1373 [container addSubview:tutorialView];
1374 yOffset = NSMaxY([tutorialView frame]);
1375 //TODO(mlerman): update UMA stats for the new tutorials.
1377 tutorialMode_ = profiles::TUTORIAL_MODE_NONE;
1380 [container setFrameSize:NSMakeSize(kFixedMenuWidth, yOffset)];
1383 - (NSView*)buildProfileChooserView {
1384 base::scoped_nsobject<NSView> container(
1385 [[NSView alloc] initWithFrame:NSZeroRect]);
1387 NSView* tutorialView = nil;
1388 NSView* currentProfileView = nil;
1389 base::scoped_nsobject<NSMutableArray> otherProfiles(
1390 [[NSMutableArray alloc] init]);
1391 // Local and guest profiles cannot lock their profile.
1392 bool displayLock = false;
1393 bool isFastProfileChooser =
1394 viewMode_ == profiles::BUBBLE_VIEW_MODE_FAST_PROFILE_CHOOSER;
1395 if (isFastProfileChooser) {
1396 // The user is using right-click switching, no need to tell them about it.
1397 PrefService* localState = g_browser_process->local_state();
1398 localState->SetBoolean(
1399 prefs::kProfileAvatarRightClickTutorialDismissed, true);
1402 // Loop over the profiles in reverse, so that they are sorted by their
1403 // y-coordinate, and separate them into active and "other" profiles.
1404 for (int i = avatarMenu_->GetNumberOfItems() - 1; i >= 0; --i) {
1405 const AvatarMenu::Item& item = avatarMenu_->GetItemAt(i);
1407 if (viewMode_ == profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER) {
1408 tutorialView = [self buildTutorialViewIfNeededForItem:item];
1410 currentProfileView = [self createCurrentProfileView:item];
1411 displayLock = item.signed_in &&
1412 profiles::IsLockAvailable(browser_->profile());
1414 [otherProfiles addObject:[self createOtherProfileView:i]];
1417 if (!currentProfileView) // Guest windows don't have an active profile.
1418 currentProfileView = [self createGuestProfileView];
1420 // |yOffset| is the next position at which to draw in |container|
1421 // coordinates. Add a pixel offset so that the bottom option buttons don't
1422 // overlap the bubble's rounded corners.
1423 CGFloat yOffset = 1;
1425 if (isFastProfileChooser) {
1426 [self buildFastUserSwitcherViewWithProfiles:otherProfiles.get()
1428 inContainer:container.get()];
1430 [self buildProfileChooserViewWithProfileView:currentProfileView
1431 tutorialView:tutorialView
1433 inContainer:container.get()
1434 displayLock:displayLock];
1437 return container.autorelease();
1440 - (NSView*)buildSigninConfirmationView {
1441 ProfileMetrics::LogProfileNewAvatarMenuSignin(
1442 ProfileMetrics::PROFILE_AVATAR_MENU_SIGNIN_VIEW);
1444 NSString* titleMessage = l10n_util::GetNSString(
1445 IDS_PROFILES_CONFIRM_SIGNIN_TUTORIAL_TITLE);
1446 NSString* contentMessage = l10n_util::GetNSString(
1447 IDS_PROFILES_CONFIRM_SIGNIN_TUTORIAL_CONTENT_TEXT);
1448 NSString* linkMessage = l10n_util::GetNSString(
1449 IDS_PROFILES_SYNC_SETTINGS_LINK);
1450 NSString* buttonMessage = l10n_util::GetNSString(
1451 IDS_PROFILES_TUTORIAL_OK_BUTTON);
1452 return [self tutorialViewWithMode:profiles::TUTORIAL_MODE_CONFIRM_SIGNIN
1453 titleMessage:titleMessage
1454 contentMessage:contentMessage
1455 linkMessage:linkMessage
1456 buttonMessage:buttonMessage
1459 linkAction:@selector(configureSyncSettings:)
1460 buttonAction:@selector(syncSettingsConfirmed:)];
1463 - (NSView*)buildSigninErrorView {
1464 NSString* titleMessage = l10n_util::GetNSString(
1465 IDS_PROFILES_ERROR_TUTORIAL_TITLE);
1466 LoginUIService* loginUiService =
1467 LoginUIServiceFactory::GetForProfile(browser_->profile());
1468 NSString* contentMessage =
1469 base::SysUTF16ToNSString(loginUiService->GetLastLoginResult());
1470 NSString* linkMessage = l10n_util::GetNSString(
1471 IDS_PROFILES_PROFILE_TUTORIAL_LEARN_MORE);
1472 return [self tutorialViewWithMode:profiles::TUTORIAL_MODE_CONFIRM_SIGNIN
1473 titleMessage:titleMessage
1474 contentMessage:contentMessage
1475 linkMessage:linkMessage
1479 linkAction:@selector(showLearnMorePage:)
1483 - (NSView*)buildWelcomeUpgradeTutorialView:(const AvatarMenu::Item&)item {
1484 ProfileMetrics::LogProfileNewAvatarMenuUpgrade(
1485 ProfileMetrics::PROFILE_AVATAR_MENU_UPGRADE_VIEW);
1487 NSString* titleMessage = l10n_util::GetNSString(
1488 IDS_PROFILES_WELCOME_UPGRADE_TUTORIAL_TITLE);
1489 NSString* contentMessage = l10n_util::GetNSString(
1490 IDS_PROFILES_WELCOME_UPGRADE_TUTORIAL_CONTENT_TEXT);
1491 // For local profiles, the "Not you" link doesn't make sense.
1492 NSString* linkMessage = item.signed_in ?
1494 l10n_util::GetStringFUTF16(IDS_PROFILES_NOT_YOU, item.name),
1495 kFixedMenuWidth - 2 * kHorizontalSpacing) :
1497 NSString* buttonMessage = l10n_util::GetNSString(
1498 IDS_PROFILES_TUTORIAL_WHATS_NEW_BUTTON);
1499 return [self tutorialViewWithMode:profiles::TUTORIAL_MODE_WELCOME_UPGRADE
1500 titleMessage:titleMessage
1501 contentMessage:contentMessage
1502 linkMessage:linkMessage
1503 buttonMessage:buttonMessage
1506 linkAction:@selector(showSwitchUserView:)
1507 buttonAction:@selector(seeWhatsNew:)];
1510 - (NSView*)buildRightClickTutorialView {
1511 NSString* titleMessage = l10n_util::GetNSString(
1512 IDS_PROFILES_RIGHT_CLICK_TUTORIAL_TITLE);
1513 NSString* contentMessage = l10n_util::GetNSString(
1514 IDS_PROFILES_RIGHT_CLICK_TUTORIAL_CONTENT_TEXT);
1515 NSString* buttonMessage = l10n_util::GetNSString(
1516 IDS_PROFILES_TUTORIAL_OK_BUTTON);
1519 [self tutorialViewWithMode:profiles::TUTORIAL_MODE_RIGHT_CLICK_SWITCHING
1520 titleMessage:titleMessage
1521 contentMessage:contentMessage
1523 buttonMessage:buttonMessage
1527 buttonAction:@selector(dismissTutorial:)];
1530 - (NSView*)buildTutorialViewIfNeededForItem:(const AvatarMenu::Item&)item {
1531 if (tutorialMode_ == profiles::TUTORIAL_MODE_CONFIRM_SIGNIN)
1532 return [self buildSigninConfirmationView];
1534 if (tutorialMode_ == profiles::TUTORIAL_MODE_SHOW_ERROR)
1535 return [self buildSigninErrorView];
1537 if (profiles::ShouldShowWelcomeUpgradeTutorial(
1538 browser_->profile(), tutorialMode_)) {
1539 if (tutorialMode_ != profiles::TUTORIAL_MODE_WELCOME_UPGRADE) {
1540 Profile* profile = browser_->profile();
1541 const int showCount = profile->GetPrefs()->GetInteger(
1542 prefs::kProfileAvatarTutorialShown);
1543 profile->GetPrefs()->SetInteger(
1544 prefs::kProfileAvatarTutorialShown, showCount + 1);
1547 return [self buildWelcomeUpgradeTutorialView:item];
1550 if (profiles::ShouldShowRightClickTutorial(browser_->profile()))
1551 return [self buildRightClickTutorialView];
1556 - (NSView*)tutorialViewWithMode:(profiles::TutorialMode)mode
1557 titleMessage:(NSString*)titleMessage
1558 contentMessage:(NSString*)contentMessage
1559 linkMessage:(NSString*)linkMessage
1560 buttonMessage:(NSString*)buttonMessage
1561 stackButton:(BOOL)stackButton
1562 hasCloseButton:(BOOL)hasCloseButton
1563 linkAction:(SEL)linkAction
1564 buttonAction:(SEL)buttonAction {
1565 tutorialMode_ = mode;
1567 NSColor* tutorialBackgroundColor =
1568 gfx::SkColorToSRGBNSColor(profiles::kAvatarTutorialBackgroundColor);
1569 base::scoped_nsobject<NSView> container([[BackgroundColorView alloc]
1570 initWithFrame:NSMakeRect(0, 0, kFixedMenuWidth, 0)
1571 withColor:tutorialBackgroundColor]);
1572 CGFloat availableWidth = kFixedMenuWidth - 2 * kHorizontalSpacing;
1573 CGFloat yOffset = kVerticalSpacing;
1575 // Adds links and buttons at the bottom.
1576 base::scoped_nsobject<NSButton> tutorialOkButton;
1577 if (buttonMessage) {
1578 tutorialOkButton.reset([[HoverButton alloc] initWithFrame:NSZeroRect]);
1579 [tutorialOkButton setTitle:buttonMessage];
1580 [tutorialOkButton setBezelStyle:NSRoundedBezelStyle];
1581 [tutorialOkButton setTarget:self];
1582 [tutorialOkButton setAction:buttonAction];
1583 [tutorialOkButton setAlignment:NSCenterTextAlignment];
1584 [tutorialOkButton sizeToFit];
1587 NSButton* learnMoreLink = nil;
1589 learnMoreLink = [self linkButtonWithTitle:linkMessage
1590 frameOrigin:NSZeroPoint
1592 [[learnMoreLink cell] setTextColor:[NSColor whiteColor]];
1597 [learnMoreLink setFrameOrigin:NSMakePoint(
1598 (kFixedMenuWidth - NSWidth([learnMoreLink frame])) / 2, yOffset)];
1600 [tutorialOkButton setFrameSize:NSMakeSize(
1601 availableWidth, NSHeight([tutorialOkButton frame]))];
1602 [tutorialOkButton setFrameOrigin:NSMakePoint(
1604 yOffset + (learnMoreLink ? NSHeight([learnMoreLink frame]) : 0))];
1606 if (buttonMessage) {
1607 NSSize buttonSize = [tutorialOkButton frame].size;
1608 const CGFloat kTopBottomTextPadding = 6;
1609 const CGFloat kLeftRightTextPadding = 15;
1610 buttonSize.width += 2 * kLeftRightTextPadding;
1611 buttonSize.height += 2 * kTopBottomTextPadding;
1612 [tutorialOkButton setFrameSize:buttonSize];
1613 CGFloat buttonXOffset = kFixedMenuWidth -
1614 NSWidth([tutorialOkButton frame]) - kHorizontalSpacing;
1615 [tutorialOkButton setFrameOrigin:NSMakePoint(buttonXOffset, yOffset)];
1619 CGFloat linkYOffset = yOffset;
1620 if (buttonMessage) {
1621 linkYOffset += (NSHeight([tutorialOkButton frame]) -
1622 NSHeight([learnMoreLink frame])) / 2;
1624 [learnMoreLink setFrameOrigin:NSMakePoint(
1625 kHorizontalSpacing, linkYOffset)];
1629 if (buttonMessage) {
1630 [container addSubview:tutorialOkButton];
1631 yOffset = NSMaxY([tutorialOkButton frame]);
1635 [container addSubview:learnMoreLink];
1636 yOffset = std::max(NSMaxY([learnMoreLink frame]), yOffset);
1639 yOffset += kVerticalSpacing;
1641 // Adds body content.
1642 NSTextField* contentLabel = BuildLabel(
1644 NSMakePoint(kHorizontalSpacing, yOffset),
1645 gfx::SkColorToSRGBNSColor(profiles::kAvatarTutorialContentTextColor));
1646 [contentLabel setFrameSize:NSMakeSize(availableWidth, 0)];
1647 [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:contentLabel];
1648 [container addSubview:contentLabel];
1649 yOffset = NSMaxY([contentLabel frame]) + kSmallVerticalSpacing;
1652 NSTextField* titleLabel =
1653 BuildLabel(titleMessage,
1654 NSMakePoint(kHorizontalSpacing, yOffset),
1655 [NSColor whiteColor] /* text_color */);
1656 [titleLabel setFont:[NSFont labelFontOfSize:kTitleFontSize]];
1658 if (hasCloseButton) {
1659 base::scoped_nsobject<HoverImageButton> closeButton(
1660 [[HoverImageButton alloc] initWithFrame:NSZeroRect]);
1661 [closeButton setBordered:NO];
1663 ui::ResourceBundle* rb = &ui::ResourceBundle::GetSharedInstance();
1664 NSImage* closeImage = rb->GetNativeImageNamed(IDR_CLOSE_1).ToNSImage();
1665 CGFloat closeImageWidth = [closeImage size].width;
1666 [closeButton setDefaultImage:closeImage];
1667 [closeButton setHoverImage:
1668 rb->GetNativeImageNamed(IDR_CLOSE_1_H).ToNSImage()];
1669 [closeButton setPressedImage:
1670 rb->GetNativeImageNamed(IDR_CLOSE_1_P).ToNSImage()];
1671 [closeButton setTarget:self];
1672 [closeButton setAction:@selector(dismissTutorial:)];
1673 [closeButton setFrameSize:[closeImage size]];
1674 [closeButton setFrameOrigin:NSMakePoint(
1675 kFixedMenuWidth - kHorizontalSpacing - closeImageWidth, yOffset)];
1676 [container addSubview:closeButton];
1678 [titleLabel setFrameSize:NSMakeSize(
1679 availableWidth - closeImageWidth - kHorizontalSpacing, 0)];
1681 [titleLabel setFrameSize:NSMakeSize(availableWidth, 0)];
1684 [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:titleLabel];
1685 [container addSubview:titleLabel];
1686 yOffset = NSMaxY([titleLabel frame]) + kVerticalSpacing;
1688 [container setFrameSize:NSMakeSize(kFixedMenuWidth, yOffset)];
1689 [container setFrameOrigin:NSZeroPoint];
1690 return container.autorelease();
1693 - (NSView*)createCurrentProfileView:(const AvatarMenu::Item&)item {
1694 base::scoped_nsobject<NSView> container([[NSView alloc]
1695 initWithFrame:NSZeroRect]);
1697 CGFloat xOffset = kHorizontalSpacing;
1698 CGFloat yOffset = 0;
1699 CGFloat availableTextWidth = kFixedMenuWidth - 2 * kHorizontalSpacing;
1701 // Profile options. This can be a link to the accounts view, the profile's
1702 // username for signed in users, or a "Sign in" button for local profiles.
1703 SigninManagerBase* signinManager =
1704 SigninManagerFactory::GetForProfile(
1705 browser_->profile()->GetOriginalProfile());
1706 if (!isGuestSession_ && signinManager->IsSigninAllowed()) {
1707 NSView* linksContainer =
1708 [self createCurrentProfileLinksForItem:item
1709 rect:NSMakeRect(xOffset, yOffset,
1712 [container addSubview:linksContainer];
1713 yOffset = NSMaxY([linksContainer frame]);
1716 // Profile name, centered.
1717 bool editingAllowed = !isGuestSession_ &&
1718 !browser_->profile()->IsLegacySupervised();
1719 base::scoped_nsobject<EditableProfileNameButton> profileName(
1720 [[EditableProfileNameButton alloc]
1721 initWithFrame:NSMakeRect(xOffset,
1724 kProfileButtonHeight)
1725 profile:browser_->profile()
1726 profileName:base::SysUTF16ToNSString(
1727 profiles::GetAvatarNameForProfile(
1728 browser_->profile()->GetPath()))
1729 editingAllowed:editingAllowed
1730 withController:self]);
1732 [container addSubview:profileName];
1733 yOffset = NSMaxY([profileName frame]) + 4; // Adds a small vertical padding.
1735 // Profile icon, centered.
1736 xOffset = (kFixedMenuWidth - kLargeImageSide) / 2;
1737 base::scoped_nsobject<EditableProfilePhoto> iconView(
1738 [[EditableProfilePhoto alloc]
1739 initWithFrame:NSMakeRect(xOffset, yOffset,
1740 kLargeImageSide, kLargeImageSide)
1741 avatarMenu:avatarMenu_.get()
1742 profileIcon:item.icon
1743 editingAllowed:!isGuestSession_
1744 withController:self]);
1746 [container addSubview:iconView];
1747 yOffset = NSMaxY([iconView frame]);
1749 if (browser_->profile()->IsSupervised()) {
1750 base::scoped_nsobject<NSImageView> supervisedIcon(
1751 [[NSImageView alloc] initWithFrame:NSZeroRect]);
1752 int imageId = browser_->profile()->IsChild()
1753 ? IDR_ICON_PROFILES_MENU_CHILD
1754 : IDR_ICON_PROFILES_MENU_LEGACY_SUPERVISED;
1755 ui::ResourceBundle* rb = &ui::ResourceBundle::GetSharedInstance();
1756 [supervisedIcon setImage:rb->GetNativeImageNamed(imageId).ToNSImage()];
1757 NSSize size = [[supervisedIcon image] size];
1758 [supervisedIcon setFrameSize:size];
1759 NSRect parentFrame = [iconView frame];
1760 [supervisedIcon setFrameOrigin:NSMakePoint(NSMaxX(parentFrame) - size.width,
1761 NSMinY(parentFrame))];
1762 [container addSubview:supervisedIcon];
1765 [container setFrameSize:NSMakeSize(kFixedMenuWidth, yOffset)];
1766 return container.autorelease();
1769 - (NSView*)createCurrentProfileLinksForItem:(const AvatarMenu::Item&)item
1771 base::scoped_nsobject<NSView> container([[NSView alloc] initWithFrame:rect]);
1773 // Don't double-apply the left margin to the sub-views.
1776 // The available links depend on the type of profile that is active.
1777 if (item.signed_in) {
1778 // Signed in profiles with no authentication errors do not have a clickable
1780 NSButton* link = nil;
1781 if (switches::IsEnableAccountConsistency()) {
1782 NSString* linkTitle = l10n_util::GetNSString(
1783 viewMode_ == profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER ?
1784 IDS_PROFILES_PROFILE_MANAGE_ACCOUNTS_BUTTON :
1785 IDS_PROFILES_PROFILE_HIDE_MANAGE_ACCOUNTS_BUTTON);
1787 (viewMode_ == profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER) ?
1788 @selector(showAccountManagement:) : @selector(hideAccountManagement:);
1789 link = [self linkButtonWithTitle:linkTitle
1790 frameOrigin:rect.origin
1791 action:linkSelector];
1793 link = [self linkButtonWithTitle:base::SysUTF16ToNSString(item.username)
1794 frameOrigin:rect.origin
1796 if (HasAuthError(browser_->profile())) {
1797 [link setImage:ui::ResourceBundle::GetSharedInstance().
1798 GetNativeImageNamed(IDR_ICON_PROFILES_ACCOUNT_BUTTON_ERROR).
1800 [link setImagePosition:NSImageRight];
1801 [link setTarget:self];
1802 [link setAction:@selector(showAccountReauthenticationView:)];
1803 [link setTag:kPrimaryProfileTag];
1805 accessibilitySetOverrideValue:l10n_util::GetNSStringF(
1806 IDS_PROFILES_ACCOUNT_BUTTON_AUTH_ERROR_ACCESSIBLE_NAME,
1808 forAttribute:NSAccessibilityTitleAttribute];
1810 [link setEnabled:NO];
1813 // -linkButtonWithTitle sizeToFit's the link. We can use the height, but
1814 // need to re-stretch the width so that the link can be centered correctly
1816 rect.size.height = [link frame].size.height;
1817 [link setAlignment:NSCenterTextAlignment];
1818 [link setFrame:rect];
1819 [container addSubview:link];
1820 [container setFrameSize:rect.size];
1822 rect.size.height = kBlueButtonHeight;
1823 NSButton* signinButton = [[BlueLabelButton alloc] initWithFrame:rect];
1825 // Manually elide the button text so that the contents fit inside the bubble
1826 // This is needed because the BlueLabelButton cell resets the style on
1827 // every call to -cellSize, which prevents setting a custom lineBreakMode.
1828 NSString* elidedButtonText = base::SysUTF16ToNSString(gfx::ElideText(
1829 l10n_util::GetStringFUTF16(
1830 IDS_SYNC_START_SYNC_BUTTON_LABEL,
1831 l10n_util::GetStringUTF16(IDS_SHORT_PRODUCT_NAME)),
1832 gfx::FontList(), rect.size.width, gfx::ELIDE_TAIL));
1834 [signinButton setTitle:elidedButtonText];
1835 [signinButton setTarget:self];
1836 [signinButton setAction:@selector(showInlineSigninPage:)];
1837 [container addSubview:signinButton];
1839 // Sign-in promo text.
1840 NSTextField* promo = BuildLabel(
1841 l10n_util::GetNSString(IDS_PROFILES_SIGNIN_PROMO),
1842 NSMakePoint(0, NSMaxY([signinButton frame]) + kVerticalSpacing),
1844 [promo setFrameSize:NSMakeSize(rect.size.width, 0)];
1845 [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:promo];
1846 [container addSubview:promo];
1848 [container setFrameSize:NSMakeSize(
1850 NSMaxY([promo frame]) + 4)]; // Adds a small vertical padding.
1853 return container.autorelease();
1856 - (NSView*)createSupervisedUserDisclaimerView {
1857 base::scoped_nsobject<NSView> container(
1858 [[NSView alloc] initWithFrame:NSZeroRect]);
1861 int availableTextWidth = kFixedMenuWidth - 2 * kHorizontalSpacing;
1863 NSTextField* disclaimer = BuildLabel(
1864 base::SysUTF16ToNSString(avatarMenu_->GetSupervisedUserInformation()),
1865 NSMakePoint(kHorizontalSpacing, yOffset), nil);
1866 [disclaimer setFrameSize:NSMakeSize(availableTextWidth, 0)];
1867 [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:disclaimer];
1868 yOffset = NSMaxY([disclaimer frame]);
1870 [container addSubview:disclaimer];
1871 [container setFrameSize:NSMakeSize(kFixedMenuWidth, yOffset)];
1872 return container.autorelease();
1875 - (NSView*)createGuestProfileView {
1876 gfx::Image guestIcon =
1877 ui::ResourceBundle::GetSharedInstance().GetNativeImageNamed(
1878 profiles::GetPlaceholderAvatarIconResourceID());
1879 AvatarMenu::Item guestItem(std::string::npos, /* menu_index, not used */
1880 std::string::npos, /* profile_index, not used */
1882 guestItem.active = true;
1883 guestItem.name = base::SysNSStringToUTF16(
1884 l10n_util::GetNSString(IDS_PROFILES_GUEST_PROFILE_NAME));
1886 return [self createCurrentProfileView:guestItem];
1889 - (NSButton*)createOtherProfileView:(int)itemIndex {
1890 const AvatarMenu::Item& item = avatarMenu_->GetItemAt(itemIndex);
1892 NSRect rect = NSMakeRect(
1893 0, 0, kFixedMenuWidth, kBlueButtonHeight + kSmallVerticalSpacing);
1894 base::scoped_nsobject<BackgroundColorHoverButton> profileButton(
1895 [[BackgroundColorHoverButton alloc]
1897 imageTitleSpacing:kImageTitleSpacing
1898 backgroundColor:GetDialogBackgroundColor()]);
1900 NSString* title = base::SysUTF16ToNSString(
1901 profiles::GetProfileSwitcherTextForItem(item));
1902 [profileButton setTitle:title];
1904 // Use the low-res, small default avatars in the fast user switcher, like
1905 // we do in the menu bar.
1906 gfx::Image itemIcon;
1908 AvatarMenu::GetImageForMenuButton(item.profile_path, &itemIcon, &isRectangle);
1910 [profileButton setDefaultImage:CreateProfileImage(
1911 itemIcon, kSmallImageSide).ToNSImage()];
1912 [profileButton setImagePosition:NSImageLeft];
1913 [profileButton setAlignment:NSLeftTextAlignment];
1914 [profileButton setBordered:NO];
1915 [profileButton setTag:itemIndex];
1916 [profileButton setTarget:self];
1917 [profileButton setAction:@selector(switchToProfile:)];
1919 NSSize textSize = [[profileButton title] sizeWithAttributes:@{
1920 NSFontAttributeName : [profileButton font]
1923 CGFloat availableWidth = rect.size.width - kSmallImageSide -
1924 kImageTitleSpacing - kHorizontalSpacing;
1926 if (std::ceil(textSize.width) > availableWidth)
1927 [profileButton setToolTip:[profileButton title]];
1929 return profileButton.autorelease();
1932 - (NSView*)createOptionsViewWithRect:(NSRect)rect
1933 displayLock:(BOOL)displayLock {
1934 NSRect viewRect = NSMakeRect(0, 0,
1936 kBlueButtonHeight + kSmallVerticalSpacing);
1937 base::scoped_nsobject<NSView> container([[NSView alloc] initWithFrame:rect]);
1940 NSButton* lockButton =
1941 [self hoverButtonWithRect:viewRect
1942 text:l10n_util::GetNSString(
1943 IDS_PROFILES_PROFILE_SIGNOUT_BUTTON)
1944 imageResourceId:IDR_ICON_PROFILES_MENU_LOCK
1945 action:@selector(lockProfile:)];
1946 [container addSubview:lockButton];
1947 viewRect.origin.y = NSMaxY([lockButton frame]);
1949 NSBox* separator = [self horizontalSeparatorWithFrame:viewRect];
1950 [container addSubview:separator];
1951 viewRect.origin.y = NSMaxY([separator frame]);
1954 if ([self shouldShowGoIncognito]) {
1955 NSButton* goIncognitoButton =
1956 [self hoverButtonWithRect:viewRect
1957 text:l10n_util::GetNSString(
1958 IDS_PROFILES_GO_INCOGNITO_BUTTON)
1959 imageResourceId:IDR_ICON_PROFILES_MENU_INCOGNITO
1960 action:@selector(goIncognito:)];
1961 viewRect.origin.y = NSMaxY([goIncognitoButton frame]);
1962 [container addSubview:goIncognitoButton];
1964 NSBox* separator = [self horizontalSeparatorWithFrame:viewRect];
1965 [container addSubview:separator];
1966 viewRect.origin.y = NSMaxY([separator frame]);
1969 NSString* text = isGuestSession_ ?
1970 l10n_util::GetNSString(IDS_PROFILES_EXIT_GUEST) :
1971 l10n_util::GetNSString(IDS_PROFILES_SWITCH_USERS_BUTTON);
1972 NSButton* switchUsersButton =
1973 [self hoverButtonWithRect:viewRect
1975 imageResourceId:IDR_ICON_PROFILES_MENU_AVATAR
1976 action:isGuestSession_? @selector(exitGuest:) :
1977 @selector(showUserManager:)];
1978 viewRect.origin.y = NSMaxY([switchUsersButton frame]);
1979 [container addSubview:switchUsersButton];
1981 [container setFrameSize:NSMakeSize(rect.size.width, viewRect.origin.y)];
1982 return container.autorelease();
1985 - (NSView*)createCurrentProfileAccountsView:(NSRect)rect {
1986 const CGFloat kAccountButtonHeight = 34;
1988 const AvatarMenu::Item& item =
1989 avatarMenu_->GetItemAt(avatarMenu_->GetActiveProfileIndex());
1990 DCHECK(item.signed_in);
1992 NSColor* backgroundColor = gfx::SkColorToCalibratedNSColor(
1993 profiles::kAvatarBubbleAccountsBackgroundColor);
1994 base::scoped_nsobject<NSView> container([[BackgroundColorView alloc]
1996 withColor:backgroundColor]);
1999 if (!browser_->profile()->IsSupervised()) {
2000 // Manually elide the button text so the contents fit inside the bubble.
2001 // This is needed because the BlueLabelButton cell resets the style on
2002 // every call to -cellSize, which prevents setting a custom lineBreakMode.
2003 NSString* elidedButtonText = base::SysUTF16ToNSString(gfx::ElideText(
2004 l10n_util::GetStringFUTF16(
2005 IDS_PROFILES_PROFILE_ADD_ACCOUNT_BUTTON, item.name),
2006 gfx::FontList(), rect.size.width, gfx::ELIDE_TAIL));
2008 NSButton* addAccountsButton =
2009 [self linkButtonWithTitle:elidedButtonText
2010 frameOrigin:NSMakePoint(
2011 kHorizontalSpacing, kSmallVerticalSpacing)
2012 action:@selector(addAccount:)];
2013 [container addSubview:addAccountsButton];
2014 rect.origin.y += kAccountButtonHeight;
2017 NSView* accountEmails = [self createAccountsListWithRect:NSMakeRect(
2018 0, rect.origin.y, rect.size.width, kAccountButtonHeight)];
2019 [container addSubview:accountEmails];
2021 [container setFrameSize:NSMakeSize(rect.size.width,
2022 NSMaxY([accountEmails frame]))];
2023 return container.autorelease();
2026 - (NSView*)createAccountsListWithRect:(NSRect)rect {
2027 base::scoped_nsobject<NSView> container([[NSView alloc] initWithFrame:rect]);
2028 currentProfileAccounts_.clear();
2030 Profile* profile = browser_->profile();
2031 std::string primaryAccount =
2032 SigninManagerFactory::GetForProfile(profile)->GetAuthenticatedAccountId();
2033 DCHECK(!primaryAccount.empty());
2034 std::vector<std::string>accounts =
2035 profiles::GetSecondaryAccountsForProfile(profile, primaryAccount);
2037 // If there is an account with an authentication error, it needs to be
2038 // badged with a warning icon.
2039 std::string errorAccountId = GetAuthErrorAccountId(profile);
2042 for (size_t i = 0; i < accounts.size(); ++i) {
2043 // Save the original email address, as the button text could be elided.
2044 currentProfileAccounts_[i] = accounts[i];
2045 NSButton* accountButton =
2046 [self accountButtonWithRect:rect
2047 accountId:accounts[i]
2049 reauthRequired:errorAccountId == accounts[i]];
2050 [container addSubview:accountButton];
2051 rect.origin.y = NSMaxY([accountButton frame]);
2054 // The primary account should always be listed first.
2055 NSButton* accountButton =
2056 [self accountButtonWithRect:rect
2057 accountId:primaryAccount
2058 tag:kPrimaryProfileTag
2059 reauthRequired:errorAccountId == primaryAccount];
2060 [container addSubview:accountButton];
2061 [container setFrameSize:NSMakeSize(NSWidth([container frame]),
2062 NSMaxY([accountButton frame]))];
2063 return container.autorelease();
2066 - (NSView*)buildGaiaEmbeddedView {
2067 base::scoped_nsobject<NSView> container(
2068 [[NSView alloc] initWithFrame:NSZeroRect]);
2069 CGFloat yOffset = 0;
2073 switch (viewMode_) {
2074 case profiles::BUBBLE_VIEW_MODE_GAIA_SIGNIN:
2075 url = signin::GetPromoURL(signin_metrics::SOURCE_AVATAR_BUBBLE_SIGN_IN,
2076 false /* auto_close */,
2077 true /* is_constrained */);
2078 messageId = IDS_PROFILES_GAIA_SIGNIN_TITLE;
2080 case profiles::BUBBLE_VIEW_MODE_GAIA_ADD_ACCOUNT:
2081 url = signin::GetPromoURL(
2082 signin_metrics::SOURCE_AVATAR_BUBBLE_ADD_ACCOUNT,
2083 false /* auto_close */,
2084 true /* is_constrained */);
2085 messageId = IDS_PROFILES_GAIA_ADD_ACCOUNT_TITLE;
2087 case profiles::BUBBLE_VIEW_MODE_GAIA_REAUTH:
2088 DCHECK(HasAuthError(browser_->profile()));
2089 url = signin::GetReauthURL(
2090 browser_->profile(), GetAuthErrorAccountId(browser_->profile()));
2091 messageId = IDS_PROFILES_GAIA_REAUTH_TITLE;
2094 NOTREACHED() << "Called with invalid mode=" << viewMode_;
2098 webContents_.reset(content::WebContents::Create(
2099 content::WebContents::CreateParams(browser_->profile())));
2101 webContentsDelegate_.reset(new GaiaWebContentsDelegate());
2102 webContents_->SetDelegate(webContentsDelegate_.get());
2103 webContents_->GetController().LoadURL(url,
2104 content::Referrer(),
2105 ui::PAGE_TRANSITION_AUTO_TOPLEVEL,
2107 NSView* webview = webContents_->GetNativeView();
2108 [webview setFrameSize:NSMakeSize(kFixedGaiaViewWidth, kFixedGaiaViewHeight)];
2109 [container addSubview:webview];
2110 content::RenderWidgetHostView* rwhv = webContents_->GetRenderWidgetHostView();
2112 rwhv->SetBackgroundColor(profiles::kAvatarBubbleGaiaBackgroundColor);
2113 yOffset = NSMaxY([webview frame]);
2115 // Adds the title card.
2116 NSBox* separator = [self horizontalSeparatorWithFrame:
2117 NSMakeRect(0, yOffset, kFixedGaiaViewWidth, 0)];
2118 [container addSubview:separator];
2119 yOffset = NSMaxY([separator frame]) + kVerticalSpacing;
2121 NSView* titleView = BuildTitleCard(
2122 NSMakeRect(0, yOffset, kFixedGaiaViewWidth, 0),
2123 l10n_util::GetStringUTF16(messageId),
2124 self /* backButtonTarget*/,
2125 @selector(navigateBackFromSigninPage:) /* backButtonAction */);
2126 [container addSubview:titleView];
2127 yOffset = NSMaxY([titleView frame]);
2129 [container setFrameSize:NSMakeSize(kFixedGaiaViewWidth, yOffset)];
2130 return container.autorelease();
2133 - (NSView*)buildAccountRemovalView {
2134 DCHECK(!accountIdToRemove_.empty());
2136 base::scoped_nsobject<NSView> container(
2137 [[NSView alloc] initWithFrame:NSZeroRect]);
2138 CGFloat availableWidth =
2139 kFixedAccountRemovalViewWidth - 2 * kHorizontalSpacing;
2140 CGFloat yOffset = kVerticalSpacing;
2142 const std::string& primaryAccount = SigninManagerFactory::GetForProfile(
2143 browser_->profile())->GetAuthenticatedAccountId();
2144 bool isPrimaryAccount = primaryAccount == accountIdToRemove_;
2146 // Adds "remove account" button at the bottom if needed.
2147 if (!isPrimaryAccount) {
2148 base::scoped_nsobject<NSButton> removeAccountButton(
2149 [[BlueLabelButton alloc] initWithFrame:NSZeroRect]);
2150 [removeAccountButton setTitle:l10n_util::GetNSString(
2151 IDS_PROFILES_ACCOUNT_REMOVAL_BUTTON)];
2152 [removeAccountButton setTarget:self];
2153 [removeAccountButton setAction:@selector(removeAccount:)];
2154 [removeAccountButton sizeToFit];
2155 [removeAccountButton setAlignment:NSCenterTextAlignment];
2156 CGFloat xOffset = (kFixedAccountRemovalViewWidth -
2157 NSWidth([removeAccountButton frame])) / 2;
2158 [removeAccountButton setFrameOrigin:NSMakePoint(xOffset, yOffset)];
2159 [container addSubview:removeAccountButton];
2161 yOffset = NSMaxY([removeAccountButton frame]) + kVerticalSpacing;
2164 NSView* contentView;
2165 NSPoint contentFrameOrigin = NSMakePoint(kHorizontalSpacing, yOffset);
2166 if (isPrimaryAccount) {
2167 std::string email = signin_ui_util::GetDisplayEmail(browser_->profile(),
2168 accountIdToRemove_);
2169 std::vector<size_t> offsets;
2170 NSString* contentStr = l10n_util::GetNSStringF(
2171 IDS_PROFILES_PRIMARY_ACCOUNT_REMOVAL_TEXT,
2172 base::UTF8ToUTF16(email), base::string16(), &offsets);
2173 NSString* linkStr = l10n_util::GetNSString(IDS_PROFILES_SETTINGS_LINK);
2174 contentView = BuildFixedWidthTextViewWithLink(self, contentStr, linkStr,
2175 offsets[1], contentFrameOrigin, availableWidth);
2177 NSString* contentStr =
2178 l10n_util::GetNSString(IDS_PROFILES_ACCOUNT_REMOVAL_TEXT);
2179 NSTextField* contentLabel = BuildLabel(contentStr, contentFrameOrigin, nil);
2180 [contentLabel setFrameSize:NSMakeSize(availableWidth, 0)];
2181 [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:contentLabel];
2182 contentView = contentLabel;
2184 [container addSubview:contentView];
2185 yOffset = NSMaxY([contentView frame]) + kVerticalSpacing;
2187 // Adds the title card.
2188 NSBox* separator = [self horizontalSeparatorWithFrame:
2189 NSMakeRect(0, yOffset, kFixedAccountRemovalViewWidth, 0)];
2190 [container addSubview:separator];
2191 yOffset = NSMaxY([separator frame]) + kVerticalSpacing;
2193 NSView* titleView = BuildTitleCard(
2194 NSMakeRect(0, yOffset, kFixedAccountRemovalViewWidth,0),
2195 l10n_util::GetStringUTF16(IDS_PROFILES_ACCOUNT_REMOVAL_TITLE),
2196 self /* backButtonTarget*/,
2197 @selector(showAccountManagement:) /* backButtonAction */);
2198 [container addSubview:titleView];
2199 yOffset = NSMaxY([titleView frame]);
2201 [container setFrameSize:NSMakeSize(kFixedAccountRemovalViewWidth, yOffset)];
2202 return container.autorelease();
2205 - (NSView*)buildSwitchUserView {
2206 ProfileMetrics::LogProfileNewAvatarMenuNotYou(
2207 ProfileMetrics::PROFILE_AVATAR_MENU_NOT_YOU_VIEW);
2208 base::scoped_nsobject<NSView> container(
2209 [[NSView alloc] initWithFrame:NSZeroRect]);
2210 CGFloat availableWidth =
2211 kFixedSwitchUserViewWidth - 2 * kHorizontalSpacing;
2212 CGFloat yOffset = 0;
2213 NSRect viewRect = NSMakeRect(0, yOffset,
2214 kFixedSwitchUserViewWidth,
2215 kBlueButtonHeight + kSmallVerticalSpacing);
2217 const AvatarMenu::Item& avatarItem =
2218 avatarMenu_->GetItemAt(avatarMenu_->GetActiveProfileIndex());
2220 // Adds "Disconnect your Google Account" button at the bottom.
2221 NSButton* disconnectButton =
2222 [self hoverButtonWithRect:viewRect
2223 text:l10n_util::GetNSString(
2224 IDS_PROFILES_DISCONNECT_BUTTON)
2225 imageResourceId:IDR_ICON_PROFILES_MENU_DISCONNECT
2226 action:@selector(disconnectProfile:)];
2227 [container addSubview:disconnectButton];
2228 yOffset = NSMaxY([disconnectButton frame]);
2230 NSBox* separator = [self horizontalSeparatorWithFrame:
2231 NSMakeRect(0, yOffset, kFixedSwitchUserViewWidth, 0)];
2232 [container addSubview:separator];
2233 yOffset = NSMaxY([separator frame]);
2235 // Adds "Add person" button.
2236 viewRect.origin.y = yOffset;
2237 NSButton* addPersonButton =
2238 [self hoverButtonWithRect:viewRect
2239 text:l10n_util::GetNSString(
2240 IDS_PROFILES_ADD_PERSON_BUTTON)
2241 imageResourceId:IDR_ICON_PROFILES_MENU_AVATAR
2242 action:@selector(showUserManager:)];
2243 [container addSubview:addPersonButton];
2244 yOffset = NSMaxY([addPersonButton frame]);
2246 separator = [self horizontalSeparatorWithFrame:
2247 NSMakeRect(0, yOffset, kFixedSwitchUserViewWidth, 0)];
2248 [container addSubview:separator];
2249 yOffset = NSMaxY([separator frame]);
2251 // Adds the content text.
2252 base::string16 elidedName(gfx::ElideText(
2253 avatarItem.name, gfx::FontList(), availableWidth, gfx::ELIDE_TAIL));
2254 NSTextField* contentLabel = BuildLabel(
2255 l10n_util::GetNSStringF(IDS_PROFILES_NOT_YOU_CONTENT_TEXT, elidedName),
2256 NSMakePoint(kHorizontalSpacing, yOffset + kVerticalSpacing),
2258 [contentLabel setFrameSize:NSMakeSize(availableWidth, 0)];
2259 [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:contentLabel];
2260 [container addSubview:contentLabel];
2261 yOffset = NSMaxY([contentLabel frame]) + kVerticalSpacing;
2263 // Adds the title card.
2264 separator = [self horizontalSeparatorWithFrame:
2265 NSMakeRect(0, yOffset, kFixedSwitchUserViewWidth, 0)];
2266 [container addSubview:separator];
2267 yOffset = NSMaxY([separator frame]) + kVerticalSpacing;
2269 NSView* titleView = BuildTitleCard(
2270 NSMakeRect(0, yOffset, kFixedSwitchUserViewWidth,0),
2271 l10n_util::GetStringFUTF16(IDS_PROFILES_NOT_YOU, avatarItem.name),
2272 self /* backButtonTarget*/,
2273 @selector(navigateBackFromSwitchUserView:) /* backButtonAction */);
2274 [container addSubview:titleView];
2275 yOffset = NSMaxY([titleView frame]);
2277 [container setFrameSize:NSMakeSize(kFixedSwitchUserViewWidth, yOffset)];
2278 return container.autorelease();
2281 // Called when clicked on the settings link.
2282 - (BOOL)textView:(NSTextView*)textView
2283 clickedOnLink:(id)link
2284 atIndex:(NSUInteger)charIndex {
2285 chrome::ShowSettings(browser_);
2289 - (NSButton*)hoverButtonWithRect:(NSRect)rect
2290 text:(NSString*)text
2291 imageResourceId:(int)imageResourceId
2292 action:(SEL)action {
2293 base::scoped_nsobject<BackgroundColorHoverButton> button(
2294 [[BackgroundColorHoverButton alloc]
2296 imageTitleSpacing:kImageTitleSpacing
2297 backgroundColor:GetDialogBackgroundColor()]);
2299 [button setTitle:text];
2300 ui::ResourceBundle* rb = &ui::ResourceBundle::GetSharedInstance();
2301 NSImage* image = rb->GetNativeImageNamed(imageResourceId).ToNSImage();
2302 [button setDefaultImage:image];
2303 [button setHoverImage:image];
2304 [button setPressedImage:image];
2305 [button setImagePosition:NSImageLeft];
2306 [button setAlignment:NSLeftTextAlignment];
2307 [button setBordered:NO];
2308 [button setTarget:self];
2309 [button setAction:action];
2311 return button.autorelease();
2314 - (NSButton*)linkButtonWithTitle:(NSString*)title
2315 frameOrigin:(NSPoint)frameOrigin
2316 action:(SEL)action {
2317 base::scoped_nsobject<NSButton> link(
2318 [[HyperlinkButtonCell buttonWithString:title] retain]);
2320 [[link cell] setShouldUnderline:NO];
2321 [[link cell] setTextColor:gfx::SkColorToCalibratedNSColor(
2322 chrome_style::GetLinkColor())];
2323 [link setTitle:title];
2324 [link setBordered:NO];
2325 [link setFont:[NSFont labelFontOfSize:kTextFontSize]];
2326 [link setTarget:self];
2327 [link setAction:action];
2328 [link setFrameOrigin:frameOrigin];
2331 return link.autorelease();
2334 - (NSButton*)accountButtonWithRect:(NSRect)rect
2335 accountId:(const std::string&)accountId
2337 reauthRequired:(BOOL)reauthRequired {
2338 // Get display email address for account.
2339 std::string email = signin_ui_util::GetDisplayEmail(browser_->profile(),
2342 ui::ResourceBundle* rb = &ui::ResourceBundle::GetSharedInstance();
2343 NSImage* deleteImage = rb->GetNativeImageNamed(IDR_CLOSE_1).ToNSImage();
2344 CGFloat deleteImageWidth = [deleteImage size].width;
2345 NSImage* warningImage = reauthRequired ? rb->GetNativeImageNamed(
2346 IDR_ICON_PROFILES_ACCOUNT_BUTTON_ERROR).ToNSImage() : nil;
2347 CGFloat warningImageWidth = [warningImage size].width;
2349 CGFloat availableTextWidth = rect.size.width - kHorizontalSpacing -
2350 warningImageWidth - deleteImageWidth;
2352 availableTextWidth -= kHorizontalSpacing;
2354 NSColor* backgroundColor = gfx::SkColorToCalibratedNSColor(
2355 profiles::kAvatarBubbleAccountsBackgroundColor);
2356 base::scoped_nsobject<BackgroundColorHoverButton> button(
2357 [[BackgroundColorHoverButton alloc] initWithFrame:rect
2359 backgroundColor:backgroundColor]);
2360 [button setTitle:ElideEmail(email, availableTextWidth)];
2361 [button setAlignment:NSLeftTextAlignment];
2362 [button setBordered:NO];
2363 if (reauthRequired) {
2364 [button setDefaultImage:warningImage];
2365 [button setImagePosition:NSImageLeft];
2366 [button setTarget:self];
2367 [button setAction:@selector(showAccountReauthenticationView:)];
2368 [button setTag:tag];
2372 if (!browser_->profile()->IsSupervised()) {
2374 NSDivideRect(rect, &buttonRect, &rect,
2375 deleteImageWidth + kHorizontalSpacing, NSMaxXEdge);
2376 buttonRect.origin.y = 0;
2378 base::scoped_nsobject<HoverImageButton> deleteButton(
2379 [[HoverImageButton alloc] initWithFrame:buttonRect]);
2380 [deleteButton setBordered:NO];
2381 [deleteButton setDefaultImage:deleteImage];
2382 [deleteButton setHoverImage:rb->GetNativeImageNamed(
2383 IDR_CLOSE_1_H).ToNSImage()];
2384 [deleteButton setPressedImage:rb->GetNativeImageNamed(
2385 IDR_CLOSE_1_P).ToNSImage()];
2386 [deleteButton setTarget:self];
2387 [deleteButton setAction:@selector(showAccountRemovalView:)];
2388 [deleteButton setTag:tag];
2390 [button addSubview:deleteButton];
2393 return button.autorelease();
2396 - (void)postActionPerformed:(ProfileMetrics::ProfileDesktopMenu)action {
2397 ProfileMetrics::LogProfileDesktopMenu(action, serviceType_);
2398 serviceType_ = signin::GAIA_SERVICE_TYPE_NONE;
2401 - (bool)shouldShowGoIncognito {
2402 bool incognitoAvailable =
2403 IncognitoModePrefs::GetAvailability(browser_->profile()->GetPrefs()) !=
2404 IncognitoModePrefs::DISABLED;
2405 return incognitoAvailable && !browser_->profile()->IsGuestSession();