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>
7 #import "chrome/browser/ui/cocoa/profiles/profile_chooser_controller.h"
9 #include "base/mac/bundle_locations.h"
10 #include "base/prefs/pref_service.h"
11 #include "base/strings/sys_string_conversions.h"
12 #include "base/strings/utf_string_conversions.h"
13 #include "chrome/browser/browser_process.h"
14 #include "chrome/browser/chrome_notification_types.h"
15 #include "chrome/browser/lifetime/application_lifetime.h"
16 #include "chrome/browser/prefs/incognito_mode_prefs.h"
17 #include "chrome/browser/profiles/avatar_menu.h"
18 #include "chrome/browser/profiles/avatar_menu_observer.h"
19 #include "chrome/browser/profiles/profile_avatar_icon_util.h"
20 #include "chrome/browser/profiles/profile_info_cache.h"
21 #include "chrome/browser/profiles/profile_manager.h"
22 #include "chrome/browser/profiles/profile_metrics.h"
23 #include "chrome/browser/profiles/profile_window.h"
24 #include "chrome/browser/profiles/profiles_state.h"
25 #include "chrome/browser/signin/profile_oauth2_token_service_factory.h"
26 #include "chrome/browser/signin/signin_header_helper.h"
27 #include "chrome/browser/signin/signin_manager_factory.h"
28 #include "chrome/browser/signin/signin_promo.h"
29 #include "chrome/browser/signin/signin_ui_util.h"
30 #include "chrome/browser/ui/browser.h"
31 #include "chrome/browser/ui/browser_commands.h"
32 #include "chrome/browser/ui/browser_dialogs.h"
33 #include "chrome/browser/ui/browser_window.h"
34 #include "chrome/browser/ui/chrome_pages.h"
35 #include "chrome/browser/ui/chrome_style.h"
36 #include "chrome/browser/ui/webui/signin/login_ui_service.h"
37 #include "chrome/browser/ui/webui/signin/login_ui_service_factory.h"
38 #import "chrome/browser/ui/cocoa/hyperlink_text_view.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/common/pref_names.h"
44 #include "chrome/common/url_constants.h"
45 #include "components/signin/core/common/profile_management_switches.h"
46 #include "components/signin/core/browser/mutable_profile_oauth2_token_service.h"
47 #include "components/signin/core/browser/profile_oauth2_token_service.h"
48 #include "components/signin/core/browser/signin_manager.h"
49 #include "content/public/browser/notification_service.h"
50 #include "content/public/browser/web_contents.h"
51 #include "google_apis/gaia/oauth2_token_service.h"
52 #include "grit/chromium_strings.h"
53 #include "grit/generated_resources.h"
54 #include "grit/theme_resources.h"
55 #include "skia/ext/skia_utils_mac.h"
56 #import "third_party/google_toolbox_for_mac/src/AppKit/GTMUILocalizerAndLayoutTweaker.h"
57 #import "ui/base/cocoa/cocoa_base_utils.h"
58 #import "ui/base/cocoa/controls/blue_label_button.h"
59 #import "ui/base/cocoa/controls/hyperlink_button_cell.h"
60 #import "ui/base/cocoa/hover_image_button.h"
61 #include "ui/base/cocoa/window_size_constants.h"
62 #include "ui/base/l10n/l10n_util.h"
63 #include "ui/base/l10n/l10n_util_mac.h"
64 #include "ui/base/resource/resource_bundle.h"
65 #include "ui/gfx/image/image.h"
66 #include "ui/gfx/text_elider.h"
67 #include "ui/native_theme/common_theme.h"
68 #include "ui/native_theme/native_theme.h"
72 // Constants taken from the Windows/Views implementation at:
73 // chrome/browser/ui/views/profile_chooser_view.cc
74 const int kLargeImageSide = 88;
75 const int kSmallImageSide = 32;
76 const CGFloat kFixedMenuWidth = 250;
78 const CGFloat kVerticalSpacing = 16.0;
79 const CGFloat kSmallVerticalSpacing = 10.0;
80 const CGFloat kHorizontalSpacing = 16.0;
81 const CGFloat kTitleFontSize = 15.0;
82 const CGFloat kTextFontSize = 12.0;
83 const CGFloat kProfileButtonHeight = 30;
84 const int kBezelThickness = 3; // Width of the bezel on an NSButton.
85 const int kImageTitleSpacing = 10;
86 const int kBlueButtonHeight = 30;
87 const CGFloat kFocusRingLineWidth = 2;
89 // Fixed size for embedded sign in pages as defined in Gaia.
90 const CGFloat kFixedGaiaViewWidth = 360;
91 const CGFloat kFixedGaiaViewHeight = 440;
93 // Fixed size for the account removal view.
94 const CGFloat kFixedAccountRemovalViewWidth = 280;
96 // Fixed size for the switch user view.
97 const int kFixedSwitchUserViewWidth = 320;
99 // The tag number for the primary account.
100 const int kPrimaryProfileTag = -1;
102 gfx::Image CreateProfileImage(const gfx::Image& icon, int imageSize) {
103 return profiles::GetSizedAvatarIcon(
104 icon, true /* image is a square */, imageSize, imageSize);
107 // Updates the window size and position.
108 void SetWindowSize(NSWindow* window, NSSize size) {
109 NSRect frame = [window frame];
110 frame.origin.x += frame.size.width - size.width;
111 frame.origin.y += frame.size.height - size.height;
113 [window setFrame:frame display:YES];
116 NSString* ElideEmail(const std::string& email, CGFloat width) {
117 const base::string16 elidedEmail = gfx::ElideText(
118 base::UTF8ToUTF16(email), gfx::FontList(), width, gfx::ELIDE_EMAIL);
119 return base::SysUTF16ToNSString(elidedEmail);
122 NSString* ElideMessage(const base::string16& message, CGFloat width) {
123 return base::SysUTF16ToNSString(
124 gfx::ElideText(message, gfx::FontList(), width, gfx::ELIDE_TAIL));
127 // Builds a label with the given |title| anchored at |frame_origin|. Sets the
128 // text color to |text_color| if not null.
129 NSTextField* BuildLabel(NSString* title,
130 NSPoint frame_origin,
131 NSColor* text_color) {
132 base::scoped_nsobject<NSTextField> label(
133 [[NSTextField alloc] initWithFrame:NSZeroRect]);
134 [label setStringValue:title];
135 [label setEditable:NO];
136 [label setAlignment:NSLeftTextAlignment];
137 [label setBezeled:NO];
138 [label setFont:[NSFont labelFontOfSize:kTextFontSize]];
139 [label setDrawsBackground:NO];
140 [label setFrameOrigin:frame_origin];
144 [[label cell] setTextColor:text_color];
146 return label.autorelease();
149 // Builds an NSTextView that has the contents set to the specified |message|,
150 // with a non-underlined |link| inserted at |link_offset|. The view is anchored
151 // at the specified |frame_origin| and has a fixed |frame_width|.
152 NSTextView* BuildFixedWidthTextViewWithLink(
153 id<NSTextViewDelegate> delegate,
157 NSPoint frame_origin,
158 CGFloat frame_width) {
159 base::scoped_nsobject<HyperlinkTextView> text_view(
160 [[HyperlinkTextView alloc] initWithFrame:NSZeroRect]);
161 NSColor* link_color = gfx::SkColorToCalibratedNSColor(
162 chrome_style::GetLinkColor());
163 // Adds a padding row at the bottom, because |boundingRectWithSize| below cuts
164 // off the last row sometimes.
165 [text_view setMessageAndLink:[NSString stringWithFormat:@"%@\n", message]
168 font:[NSFont labelFontOfSize:kTextFontSize]
169 messageColor:[NSColor blackColor]
170 linkColor:link_color];
172 // Removes the underlining from the link.
173 [text_view setLinkTextAttributes:nil];
174 NSTextStorage* text_storage = [text_view textStorage];
175 NSRange link_range = NSMakeRange(link_offset, [link length]);
176 [text_storage addAttribute:NSUnderlineStyleAttributeName
177 value:[NSNumber numberWithInt:NSUnderlineStyleNone]
180 NSRect frame = [[text_view attributedString]
181 boundingRectWithSize:NSMakeSize(frame_width, 0)
182 options:NSStringDrawingUsesLineFragmentOrigin];
183 frame.origin = frame_origin;
184 [text_view setFrame:frame];
185 [text_view setDelegate:delegate];
186 return text_view.autorelease();
189 // Returns the native dialog background color.
190 NSColor* GetDialogBackgroundColor() {
191 return gfx::SkColorToCalibratedNSColor(
192 ui::NativeTheme::instance()->GetSystemColor(
193 ui::NativeTheme::kColorId_DialogBackground));
196 // Builds a title card with one back button right aligned and one label center
198 NSView* BuildTitleCard(NSRect frame_rect,
199 const base::string16& message,
200 id back_button_target,
201 SEL back_button_action) {
202 base::scoped_nsobject<NSView> container(
203 [[NSView alloc] initWithFrame:frame_rect]);
205 base::scoped_nsobject<HoverImageButton> button(
206 [[HoverImageButton alloc] initWithFrame:frame_rect]);
207 [button setBordered:NO];
208 ui::ResourceBundle* rb = &ui::ResourceBundle::GetSharedInstance();
209 [button setDefaultImage:rb->GetNativeImageNamed(IDR_BACK).ToNSImage()];
210 [button setHoverImage:rb->GetNativeImageNamed(IDR_BACK_H).ToNSImage()];
211 [button setPressedImage:rb->GetNativeImageNamed(IDR_BACK_P).ToNSImage()];
212 [button setTarget:back_button_target];
213 [button setAction:back_button_action];
214 [button setFrameSize:NSMakeSize(kProfileButtonHeight, kProfileButtonHeight)];
215 [button setFrameOrigin:NSMakePoint(kHorizontalSpacing, 0)];
217 CGFloat max_label_width = frame_rect.size.width -
218 (kHorizontalSpacing * 2 + kProfileButtonHeight) * 2;
219 NSTextField* title_label = BuildLabel(
220 ElideMessage(message, max_label_width),
222 [title_label setAlignment:NSCenterTextAlignment];
223 [title_label setFont:[NSFont labelFontOfSize:kTitleFontSize]];
224 [title_label sizeToFit];
225 CGFloat x_offset = (frame_rect.size.width - NSWidth([title_label frame])) / 2;
227 (NSHeight([button frame]) - NSHeight([title_label frame])) / 2;
228 [title_label setFrameOrigin:NSMakePoint(x_offset, y_offset)];
230 [container addSubview:button];
231 [container addSubview:title_label];
232 CGFloat height = std::max(NSMaxY([title_label frame]),
233 NSMaxY([button frame])) + kVerticalSpacing;
234 [container setFrameSize:NSMakeSize(NSWidth([container frame]), height)];
236 return container.autorelease();
239 bool HasAuthError(Profile* profile) {
240 const SigninErrorController* error_controller =
241 profiles::GetSigninErrorController(profile);
242 return error_controller && error_controller->HasError();
247 // Class that listens to changes to the OAuth2Tokens for the active profile,
248 // changes to the avatar menu model or browser close notifications.
249 class ActiveProfileObserverBridge : public AvatarMenuObserver,
250 public content::NotificationObserver,
251 public OAuth2TokenService::Observer {
253 ActiveProfileObserverBridge(ProfileChooserController* controller,
255 : controller_(controller),
257 token_observer_registered_(false) {
258 registrar_.Add(this, chrome::NOTIFICATION_BROWSER_CLOSING,
259 content::NotificationService::AllSources());
260 if (!browser_->profile()->IsGuestSession())
261 AddTokenServiceObserver();
264 virtual ~ActiveProfileObserverBridge() {
265 RemoveTokenServiceObserver();
269 void AddTokenServiceObserver() {
270 ProfileOAuth2TokenService* oauth2_token_service =
271 ProfileOAuth2TokenServiceFactory::GetForProfile(browser_->profile());
272 DCHECK(oauth2_token_service);
273 oauth2_token_service->AddObserver(this);
274 token_observer_registered_ = true;
277 void RemoveTokenServiceObserver() {
278 if (!token_observer_registered_)
280 DCHECK(browser_->profile());
281 ProfileOAuth2TokenService* oauth2_token_service =
282 ProfileOAuth2TokenServiceFactory::GetForProfile(browser_->profile());
283 DCHECK(oauth2_token_service);
284 oauth2_token_service->RemoveObserver(this);
285 token_observer_registered_ = false;
288 // OAuth2TokenService::Observer:
289 virtual void OnRefreshTokenAvailable(const std::string& account_id) OVERRIDE {
290 // Tokens can only be added by adding an account through the inline flow,
291 // which is started from the account management view. Refresh it to show the
293 profiles::BubbleViewMode viewMode = [controller_ viewMode];
294 if (viewMode == profiles::BUBBLE_VIEW_MODE_ACCOUNT_MANAGEMENT ||
295 viewMode == profiles::BUBBLE_VIEW_MODE_GAIA_ADD_ACCOUNT ||
296 viewMode == profiles::BUBBLE_VIEW_MODE_GAIA_REAUTH) {
297 [controller_ initMenuContentsWithView:
298 switches::IsEnableAccountConsistency() ?
299 profiles::BUBBLE_VIEW_MODE_ACCOUNT_MANAGEMENT :
300 profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER];
304 virtual void OnRefreshTokenRevoked(const std::string& account_id) OVERRIDE {
305 // Tokens can only be removed from the account management view. Refresh it
306 // to show the update.
307 if ([controller_ viewMode] == profiles::BUBBLE_VIEW_MODE_ACCOUNT_MANAGEMENT)
308 [controller_ initMenuContentsWithView:
309 profiles::BUBBLE_VIEW_MODE_ACCOUNT_MANAGEMENT];
312 // AvatarMenuObserver:
313 virtual void OnAvatarMenuChanged(AvatarMenu* avatar_menu) OVERRIDE {
314 // Do not refresh the avatar menu if the user is on a signin related view.
315 profiles::BubbleViewMode viewMode = [controller_ viewMode];
316 if (viewMode == profiles::BUBBLE_VIEW_MODE_GAIA_SIGNIN ||
317 viewMode == profiles::BUBBLE_VIEW_MODE_GAIA_ADD_ACCOUNT ||
318 viewMode == profiles::BUBBLE_VIEW_MODE_GAIA_REAUTH) {
322 // While the bubble is open, the avatar menu can only change from the
323 // profile chooser view by modifying the current profile's photo or name.
325 initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER];
328 // content::NotificationObserver:
329 virtual void Observe(
331 const content::NotificationSource& source,
332 const content::NotificationDetails& details) OVERRIDE {
333 DCHECK_EQ(chrome::NOTIFICATION_BROWSER_CLOSING, type);
334 if (browser_ == content::Source<Browser>(source).ptr()) {
335 RemoveTokenServiceObserver();
336 // Clean up the bubble's WebContents (used by the Gaia embedded view), to
337 // make sure the guest profile doesn't have any dangling host renderers.
338 // This can happen if Chrome is quit using Command-Q while the bubble is
339 // still open, which won't give the bubble a chance to be closed and
340 // clean up the WebContents itself.
341 [controller_ cleanUpEmbeddedViewContents];
345 ProfileChooserController* controller_; // Weak; owns this.
346 Browser* browser_; // Weak.
347 content::NotificationRegistrar registrar_;
349 // The observer can be removed both when closing the browser, and by just
350 // closing the avatar bubble. However, in the case of closing the browser,
351 // the avatar bubble will also be closed afterwards, resulting in a second
352 // attempt to remove the observer. This ensures the observer is only
354 bool token_observer_registered_;
356 DISALLOW_COPY_AND_ASSIGN(ActiveProfileObserverBridge);
359 // Custom button cell that adds a left padding before the button image, and
360 // a custom spacing between the button image and title.
361 @interface CustomPaddingImageButtonCell : NSButtonCell {
363 // Padding added to the left margin of the button.
364 int leftMarginSpacing_;
365 // Spacing between the cell image and title.
366 int imageTitleSpacing_;
369 - (id)initWithLeftMarginSpacing:(int)leftMarginSpacing
370 imageTitleSpacing:(int)imageTitleSpacing;
373 @implementation CustomPaddingImageButtonCell
374 - (id)initWithLeftMarginSpacing:(int)leftMarginSpacing
375 imageTitleSpacing:(int)imageTitleSpacing {
376 if ((self = [super init])) {
377 leftMarginSpacing_ = leftMarginSpacing;
378 imageTitleSpacing_ = imageTitleSpacing;
383 - (NSRect)drawTitle:(NSAttributedString*)title
384 withFrame:(NSRect)frame
385 inView:(NSView*)controlView {
387 NSDivideRect(frame, &marginRect, &frame, leftMarginSpacing_, NSMinXEdge);
389 // The title frame origin isn't aware of the left margin spacing added
390 // in -drawImage, so it must be added when drawing the title as well.
391 if ([self imagePosition] == NSImageLeft)
392 NSDivideRect(frame, &marginRect, &frame, imageTitleSpacing_, NSMinXEdge);
394 return [super drawTitle:title withFrame:frame inView:controlView];
397 - (void)drawImage:(NSImage*)image
398 withFrame:(NSRect)frame
399 inView:(NSView*)controlView {
400 if ([self imagePosition] == NSImageLeft)
401 frame.origin.x = leftMarginSpacing_;
402 [super drawImage:image withFrame:frame inView:controlView];
406 NSSize buttonSize = [super cellSize];
407 buttonSize.width += leftMarginSpacing_;
408 if ([self imagePosition] == NSImageLeft)
409 buttonSize.width += imageTitleSpacing_;
413 - (NSFocusRingType)focusRingType {
414 // This is taken care of by the custom drawing code.
415 return NSFocusRingTypeNone;
418 - (void)drawWithFrame:(NSRect)frame inView:(NSView *)controlView {
419 [super drawInteriorWithFrame:frame inView:controlView];
422 if ([self showsFirstResponder]) {
423 NSRect focusRingRect =
424 NSInsetRect(frame, kFocusRingLineWidth, kFocusRingLineWidth);
425 // TODO(noms): When we are targetting 10.7, we should change this to use
426 // -drawFocusRingMaskWithFrame instead.
427 [[[NSColor keyboardFocusIndicatorColor] colorWithAlphaComponent:1] set];
428 NSBezierPath* path = [NSBezierPath bezierPathWithRect:focusRingRect];
429 [path setLineWidth:kFocusRingLineWidth];
436 // A custom image view that has a transparent backround.
437 @interface TransparentBackgroundImageView : NSImageView
440 @implementation TransparentBackgroundImageView
441 - (void)drawRect:(NSRect)dirtyRect {
442 NSColor* backgroundColor = [NSColor colorWithCalibratedWhite:1 alpha:0.6f];
443 [backgroundColor setFill];
444 NSRectFillUsingOperation(dirtyRect, NSCompositeSourceAtop);
445 [super drawRect:dirtyRect];
449 @interface CustomCircleImageCell : NSButtonCell
452 @implementation CustomCircleImageCell
453 - (void)drawWithFrame:(NSRect)frame inView:(NSView *)controlView {
454 // Display everything as a circle that spans the entire control.
455 NSBezierPath* path = [NSBezierPath bezierPathWithOvalInRect:frame];
458 [super drawImage:[self image] withFrame:frame inView:controlView];
461 if ([self showsFirstResponder]) {
462 [[[NSColor keyboardFocusIndicatorColor] colorWithAlphaComponent:1] set];
463 [path setLineWidth:kFocusRingLineWidth];
469 // A custom image control that shows a "Change" button when moused over.
470 @interface EditableProfilePhoto : HoverImageButton {
472 AvatarMenu* avatarMenu_; // Weak; Owned by ProfileChooserController.
473 base::scoped_nsobject<TransparentBackgroundImageView> changePhotoImage_;
474 ProfileChooserController* controller_;
477 - (id)initWithFrame:(NSRect)frameRect
478 avatarMenu:(AvatarMenu*)avatarMenu
479 profileIcon:(const gfx::Image&)profileIcon
480 editingAllowed:(BOOL)editingAllowed
481 withController:(ProfileChooserController*)controller;
483 // Called when the "Change" button is clicked.
484 - (void)editPhoto:(id)sender;
488 @implementation EditableProfilePhoto
489 - (id)initWithFrame:(NSRect)frameRect
490 avatarMenu:(AvatarMenu*)avatarMenu
491 profileIcon:(const gfx::Image&)profileIcon
492 editingAllowed:(BOOL)editingAllowed
493 withController:(ProfileChooserController*)controller {
494 if ((self = [super initWithFrame:frameRect])) {
495 avatarMenu_ = avatarMenu;
496 controller_ = controller;
498 [self setBordered:NO];
500 base::scoped_nsobject<CustomCircleImageCell> cell(
501 [[CustomCircleImageCell alloc] init]);
502 [self setCell:cell.get()];
504 [self setDefaultImage:CreateProfileImage(
505 profileIcon, kLargeImageSide).ToNSImage()];
506 [self setImagePosition:NSImageOnly];
508 NSRect bounds = NSMakeRect(0, 0, kLargeImageSide, kLargeImageSide);
509 if (editingAllowed) {
510 [self setTarget:self];
511 [self setAction:@selector(editPhoto:)];
512 changePhotoImage_.reset([[TransparentBackgroundImageView alloc]
513 initWithFrame:bounds]);
514 [changePhotoImage_ setImage:ui::ResourceBundle::GetSharedInstance().
515 GetNativeImageNamed(IDR_ICON_PROFILES_EDIT_CAMERA).AsNSImage()];
516 [self addSubview:changePhotoImage_];
518 // Hide the image until the button is hovered over.
519 [changePhotoImage_ setHidden:YES];
522 // Set the image cell's accessibility strings to be the same as the
524 [[self cell] accessibilitySetOverrideValue:l10n_util::GetNSString(
526 IDS_PROFILES_NEW_AVATAR_MENU_CHANGE_PHOTO_ACCESSIBLE_NAME :
527 IDS_PROFILES_NEW_AVATAR_MENU_PHOTO_ACCESSIBLE_NAME)
528 forAttribute:NSAccessibilityTitleAttribute];
529 [[self cell] accessibilitySetOverrideValue:
530 editingAllowed ? NSAccessibilityButtonRole : NSAccessibilityImageRole
531 forAttribute:NSAccessibilityRoleAttribute];
532 [[self cell] accessibilitySetOverrideValue:
533 NSAccessibilityRoleDescription(NSAccessibilityButtonRole, nil)
534 forAttribute:NSAccessibilityRoleDescriptionAttribute];
536 // The button and the cell should read the same thing.
537 [self accessibilitySetOverrideValue:l10n_util::GetNSString(
539 IDS_PROFILES_NEW_AVATAR_MENU_CHANGE_PHOTO_ACCESSIBLE_NAME :
540 IDS_PROFILES_NEW_AVATAR_MENU_PHOTO_ACCESSIBLE_NAME)
541 forAttribute:NSAccessibilityTitleAttribute];
542 [self accessibilitySetOverrideValue:NSAccessibilityButtonRole
543 forAttribute:NSAccessibilityRoleAttribute];
544 [self accessibilitySetOverrideValue:
545 NSAccessibilityRoleDescription(NSAccessibilityButtonRole, nil)
546 forAttribute:NSAccessibilityRoleDescriptionAttribute];
551 - (void)editPhoto:(id)sender {
552 avatarMenu_->EditProfile(avatarMenu_->GetActiveProfileIndex());
554 postActionPerformed:ProfileMetrics::PROFILE_DESKTOP_MENU_EDIT_IMAGE];
557 - (void)setHoverState:(HoverState)state {
558 [super setHoverState:state];
559 [changePhotoImage_ setHidden:([self hoverState] == kHoverStateNone)];
562 - (BOOL)accessibilityIsIgnored {
566 - (NSArray*)accessibilityActionNames {
567 NSArray* parentActions = [super accessibilityActionNames];
568 return [parentActions arrayByAddingObject:NSAccessibilityPressAction];
571 - (void)accessibilityPerformAction:(NSString*)action {
572 if ([action isEqualToString:NSAccessibilityPressAction]) {
573 avatarMenu_->EditProfile(avatarMenu_->GetActiveProfileIndex());
576 [super accessibilityPerformAction:action];
581 // A custom text control that turns into a textfield for editing when clicked.
582 @interface EditableProfileNameButton : HoverImageButton {
584 base::scoped_nsobject<NSTextField> profileNameTextField_;
585 Profile* profile_; // Weak.
586 ProfileChooserController* controller_;
589 - (id)initWithFrame:(NSRect)frameRect
590 profile:(Profile*)profile
591 profileName:(NSString*)profileName
592 editingAllowed:(BOOL)editingAllowed
593 withController:(ProfileChooserController*)controller;
595 // Called when the button is clicked.
596 - (void)showEditableView:(id)sender;
598 // Called when enter is pressed in the text field.
599 - (void)saveProfileName:(id)sender;
603 @implementation EditableProfileNameButton
604 - (id)initWithFrame:(NSRect)frameRect
605 profile:(Profile*)profile
606 profileName:(NSString*)profileName
607 editingAllowed:(BOOL)editingAllowed
608 withController:(ProfileChooserController*)controller {
609 if ((self = [super initWithFrame:frameRect])) {
611 controller_ = controller;
613 if (editingAllowed) {
614 // Show an "edit" pencil icon when hovering over. In the default state,
615 // we need to create an empty placeholder of the correct size, so that
616 // the text doesn't jump around when the hovered icon appears.
617 ui::ResourceBundle* rb = &ui::ResourceBundle::GetSharedInstance();
618 NSImage* hoverImage = rb->GetNativeImageNamed(
619 IDR_ICON_PROFILES_EDIT_HOVER).AsNSImage();
621 // In order to center the button title, we need to add a left padding of
622 // the same width as the pencil icon.
623 base::scoped_nsobject<CustomPaddingImageButtonCell> cell(
624 [[CustomPaddingImageButtonCell alloc]
625 initWithLeftMarginSpacing:[hoverImage size].width
626 imageTitleSpacing:0]);
627 [self setCell:cell.get()];
629 NSImage* placeholder = [[NSImage alloc] initWithSize:[hoverImage size]];
630 [self setDefaultImage:placeholder];
631 [self setHoverImage:hoverImage];
632 [self setAlternateImage:
633 rb->GetNativeImageNamed(IDR_ICON_PROFILES_EDIT_PRESSED).AsNSImage()];
634 [self setImagePosition:NSImageRight];
635 [self setTarget:self];
636 [self setAction:@selector(showEditableView:)];
638 // We need to subtract the width of the bezel from the frame rect, so that
639 // the textfield can take the exact same space as the button.
640 frameRect.size.height -= 2 * kBezelThickness;
641 frameRect.origin = NSMakePoint(0, kBezelThickness);
642 profileNameTextField_.reset(
643 [[NSTextField alloc] initWithFrame:frameRect]);
644 [profileNameTextField_ setStringValue:profileName];
645 [profileNameTextField_ setFont:[NSFont labelFontOfSize:kTitleFontSize]];
646 [profileNameTextField_ setEditable:YES];
647 [profileNameTextField_ setDrawsBackground:YES];
648 [profileNameTextField_ setBezeled:YES];
649 [profileNameTextField_ setAlignment:NSCenterTextAlignment];
650 [[profileNameTextField_ cell] setWraps:NO];
651 [[profileNameTextField_ cell] setLineBreakMode:
652 NSLineBreakByTruncatingTail];
653 [self addSubview:profileNameTextField_];
654 [profileNameTextField_ setTarget:self];
655 [profileNameTextField_ setAction:@selector(saveProfileName:)];
657 // Hide the textfield until the user clicks on the button.
658 [profileNameTextField_ setHidden:YES];
661 [[self cell] accessibilitySetOverrideValue:NSAccessibilityButtonRole
662 forAttribute:NSAccessibilityRoleAttribute];
663 [[self cell] accessibilitySetOverrideValue:
664 NSAccessibilityRoleDescription(NSAccessibilityButtonRole, nil)
665 forAttribute:NSAccessibilityRoleDescriptionAttribute];
667 [self setBordered:NO];
668 [self setFont:[NSFont labelFontOfSize:kTitleFontSize]];
669 [self setAlignment:NSCenterTextAlignment];
670 [[self cell] setLineBreakMode:NSLineBreakByTruncatingTail];
671 [self setTitle:profileName];
676 - (void)saveProfileName:(id)sender {
677 base::string16 newProfileName =
678 base::SysNSStringToUTF16([profileNameTextField_ stringValue]);
680 // Empty profile names are not allowed, and are treated as a cancel.
681 base::TrimWhitespace(newProfileName, base::TRIM_ALL, &newProfileName);
682 if (!newProfileName.empty()) {
683 profiles::UpdateProfileName(profile_, newProfileName);
685 postActionPerformed:ProfileMetrics::PROFILE_DESKTOP_MENU_EDIT_NAME];
686 [self setTitle:base::SysUTF16ToNSString(newProfileName)];
688 // Since the text is empty and not allowed, revert it from the textbox.
689 [profileNameTextField_ setStringValue:[self title]];
691 [profileNameTextField_ setHidden:YES];
694 - (void)showEditableView:(id)sender {
695 [profileNameTextField_ setHidden:NO];
696 [[self window] makeFirstResponder:profileNameTextField_];
701 // A custom button that allows for setting a background color when hovered over.
702 @interface BackgroundColorHoverButton : HoverImageButton {
704 base::scoped_nsobject<NSColor> backgroundColor_;
705 base::scoped_nsobject<NSColor> hoverColor_;
709 @implementation BackgroundColorHoverButton
711 - (id)initWithFrame:(NSRect)frameRect
712 imageTitleSpacing:(int)imageTitleSpacing
713 backgroundColor:(NSColor*)backgroundColor {
714 if ((self = [super initWithFrame:frameRect])) {
715 backgroundColor_.reset([backgroundColor retain]);
716 // Use a color from the common theme, since this button is not trying to
717 // look like a native control.
719 bool found = ui::CommonThemeGetSystemColor(
720 ui::NativeTheme::kColorId_ButtonHoverBackgroundColor, &hoverColor);
722 hoverColor_.reset([gfx::SkColorToSRGBNSColor(hoverColor) retain]);
724 [self setBordered:NO];
725 [self setFont:[NSFont labelFontOfSize:kTextFontSize]];
726 [self setButtonType:NSMomentaryChangeButton];
728 base::scoped_nsobject<CustomPaddingImageButtonCell> cell(
729 [[CustomPaddingImageButtonCell alloc]
730 initWithLeftMarginSpacing:kHorizontalSpacing
731 imageTitleSpacing:imageTitleSpacing]);
732 [cell setLineBreakMode:NSLineBreakByTruncatingTail];
733 [self setCell:cell.get()];
738 - (void)setHoverState:(HoverState)state {
739 [super setHoverState:state];
740 bool isHighlighted = ([self hoverState] != kHoverStateNone);
742 NSColor* backgroundColor = isHighlighted ? hoverColor_ : backgroundColor_;
743 [[self cell] setBackgroundColor:backgroundColor];
748 // A custom view with the given background color.
749 @interface BackgroundColorView : NSView {
751 base::scoped_nsobject<NSColor> backgroundColor_;
755 @implementation BackgroundColorView
756 - (id)initWithFrame:(NSRect)frameRect
757 withColor:(NSColor*)color {
758 if ((self = [super initWithFrame:frameRect]))
759 backgroundColor_.reset([color retain]);
763 - (void)drawRect:(NSRect)dirtyRect {
764 [backgroundColor_ setFill];
765 NSRectFill(dirtyRect);
766 [super drawRect:dirtyRect];
770 // A custom dummy button that is used to clear focus from the bubble's controls.
771 @interface DummyWindowFocusButton : NSButton
774 @implementation DummyWindowFocusButton
775 // Ignore accessibility, as this is a placeholder button.
776 - (BOOL)accessibilityIsIgnored {
780 - (id)accessibilityAttributeValue:(NSString*)attribute {
784 - (BOOL)canBecomeKeyView {
790 @interface ProfileChooserController ()
791 // Builds the profile chooser view.
792 - (NSView*)buildProfileChooserView;
794 // Builds a tutorial card with a title label using |titleMessage|, a content
795 // label using |contentMessage|, a link using |linkMessage|, and a button using
796 // |buttonMessage|. If |stackButton| is YES, places the button above the link.
797 // Otherwise places both on the same row with the link left aligned and button
798 // right aligned. On click, the link would execute |linkAction|, and the button
799 // would execute |buttonAction|. It sets |tutorialMode_| to the given |mode|.
800 - (NSView*)tutorialViewWithMode:(profiles::TutorialMode)mode
801 titleMessage:(NSString*)titleMessage
802 contentMessage:(NSString*)contentMessage
803 linkMessage:(NSString*)linkMessage
804 buttonMessage:(NSString*)buttonMessage
805 stackButton:(BOOL)stackButton
806 hasCloseButton:(BOOL)hasCloseButton
807 linkAction:(SEL)linkAction
808 buttonAction:(SEL)buttonAction;
810 // Builds a tutorial card to introduce an upgrade user to the new avatar menu if
811 // needed. |tutorial_shown| indicates if the tutorial has already been shown in
812 // the previous active view. |avatar_item| refers to the current profile.
813 - (NSView*)buildWelcomeUpgradeTutorialViewIfNeeded;
815 // Builds a tutorial card to have the user confirm the last Chrome signin,
816 // Chrome sync will be delayed until the user either dismisses the tutorial, or
817 // configures sync through the "Settings" link.
818 - (NSView*)buildSigninConfirmationView;
820 // Builds a tutorial card to show the last signin error.
821 - (NSView*)buildSigninErrorView;
823 // Creates the main profile card for the profile |item| at the top of
825 - (NSView*)createCurrentProfileView:(const AvatarMenu::Item&)item;
827 // Creates the possible links for the main profile card with profile |item|.
828 - (NSView*)createCurrentProfileLinksForItem:(const AvatarMenu::Item&)item
831 // Creates the disclaimer text for supervised users, telling them that the
832 // manager can view their history etc.
833 - (NSView*)createSupervisedUserDisclaimerView;
835 // Creates a main profile card for the guest user.
836 - (NSView*)createGuestProfileView;
838 // Creates an item for the profile |itemIndex| that is used in the fast profile
839 // switcher in the middle of the bubble.
840 - (NSButton*)createOtherProfileView:(int)itemIndex;
842 // Creates the "Not you" and Lock option buttons.
843 - (NSView*)createOptionsViewWithRect:(NSRect)rect
844 enableLock:(BOOL)enableLock;
846 // Creates the account management view for the active profile.
847 - (NSView*)createCurrentProfileAccountsView:(NSRect)rect;
849 // Creates the list of accounts for the active profile.
850 - (NSView*)createAccountsListWithRect:(NSRect)rect;
852 // Creates the Gaia sign-in/add account view.
853 - (NSView*)buildGaiaEmbeddedView;
855 // Creates the account removal view.
856 - (NSView*)buildAccountRemovalView;
858 // Create a view that shows various options for an upgrade user who is not
859 // the same person as the currently signed in user.
860 - (NSView*)buildSwitchUserView;
862 // Creates a button with |text|, an icon given by |imageResourceId| and with
864 - (NSButton*)hoverButtonWithRect:(NSRect)rect
866 imageResourceId:(int)imageResourceId
869 // Creates a generic link button with |title| and an |action| positioned at
871 - (NSButton*)linkButtonWithTitle:(NSString*)title
872 frameOrigin:(NSPoint)frameOrigin
875 // Creates an email account button with |title| and a remove icon. If
876 // |reauthRequired| is true, the button also displays a warning icon. |tag|
877 // indicates which account the button refers to.
878 - (NSButton*)accountButtonWithRect:(NSRect)rect
879 title:(const std::string&)title
881 reauthRequired:(BOOL)reauthRequired;
883 - (bool)shouldShowGoIncognito;
886 @implementation ProfileChooserController
887 - (profiles::BubbleViewMode) viewMode {
891 - (void)setTutorialMode:(profiles::TutorialMode)tutorialMode {
892 tutorialMode_ = tutorialMode;
895 - (IBAction)switchToProfile:(id)sender {
896 // Check the event flags to see if a new window should be created.
897 bool alwaysCreate = ui::WindowOpenDispositionFromNSEvent(
898 [NSApp currentEvent]) == NEW_WINDOW;
899 avatarMenu_->SwitchToProfile([sender tag], alwaysCreate,
900 ProfileMetrics::SWITCH_PROFILE_ICON);
903 - (IBAction)showUserManager:(id)sender {
904 chrome::ShowUserManager(browser_->profile()->GetPath());
905 [self postActionPerformed:
906 ProfileMetrics::PROFILE_DESKTOP_MENU_OPEN_USER_MANAGER];
909 - (IBAction)exitGuest:(id)sender {
910 DCHECK(browser_->profile()->IsGuestSession());
911 chrome::ShowUserManager(base::FilePath());
912 profiles::CloseGuestProfileWindows();
915 - (IBAction)goIncognito:(id)sender {
916 DCHECK([self shouldShowGoIncognito]);
917 chrome::NewIncognitoWindow(browser_);
920 - (IBAction)showAccountManagement:(id)sender {
921 [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_ACCOUNT_MANAGEMENT];
924 - (IBAction)hideAccountManagement:(id)sender {
925 [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER];
928 - (IBAction)lockProfile:(id)sender {
929 profiles::LockProfile(browser_->profile());
930 [self postActionPerformed:ProfileMetrics::PROFILE_DESKTOP_MENU_LOCK];
933 - (IBAction)showInlineSigninPage:(id)sender {
934 [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_GAIA_SIGNIN];
937 - (IBAction)addAccount:(id)sender {
938 [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_GAIA_ADD_ACCOUNT];
939 [self postActionPerformed:ProfileMetrics::PROFILE_DESKTOP_MENU_ADD_ACCT];
942 - (IBAction)navigateBackFromSigninPage:(id)sender {
943 std::string primaryAccount = SigninManagerFactory::GetForProfile(
944 browser_->profile())->GetAuthenticatedUsername();
945 bool hasAccountManagement = !primaryAccount.empty() &&
946 switches::IsEnableAccountConsistency();
947 [self initMenuContentsWithView:hasAccountManagement ?
948 profiles::BUBBLE_VIEW_MODE_ACCOUNT_MANAGEMENT :
949 profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER];
952 - (IBAction)showAccountRemovalView:(id)sender {
953 DCHECK(!isGuestSession_);
955 // Tag is either |kPrimaryProfileTag| for the primary account, or equal to the
956 // index in |currentProfileAccounts_| for a secondary account.
957 int tag = [sender tag];
958 if (tag == kPrimaryProfileTag) {
959 accountIdToRemove_ = SigninManagerFactory::GetForProfile(
960 browser_->profile())->GetAuthenticatedUsername();
962 DCHECK(ContainsKey(currentProfileAccounts_, tag));
963 accountIdToRemove_ = currentProfileAccounts_[tag];
966 [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_ACCOUNT_REMOVAL];
969 - (IBAction)showAccountReauthenticationView:(id)sender {
970 DCHECK(!isGuestSession_);
971 [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_GAIA_REAUTH];
974 - (IBAction)removeAccount:(id)sender {
975 DCHECK(!accountIdToRemove_.empty());
976 ProfileOAuth2TokenServiceFactory::GetPlatformSpecificForProfile(
977 browser_->profile())->RevokeCredentials(accountIdToRemove_);
978 [self postActionPerformed:ProfileMetrics::PROFILE_DESKTOP_MENU_REMOVE_ACCT];
979 accountIdToRemove_.clear();
981 [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_ACCOUNT_MANAGEMENT];
984 - (IBAction)seeWhatsNew:(id)sender {
985 chrome::ShowUserManagerWithTutorial(
986 profiles::USER_MANAGER_TUTORIAL_OVERVIEW);
987 ProfileMetrics::LogProfileNewAvatarMenuUpgrade(
988 ProfileMetrics::PROFILE_AVATAR_MENU_UPGRADE_WHATS_NEW);
991 - (IBAction)showSwitchUserView:(id)sender {
992 [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_SWITCH_USER];
993 ProfileMetrics::LogProfileNewAvatarMenuUpgrade(
994 ProfileMetrics::PROFILE_AVATAR_MENU_UPGRADE_NOT_YOU);
997 - (IBAction)showLearnMorePage:(id)sender {
998 signin_ui_util::ShowSigninErrorLearnMorePage(browser_->profile());
1001 - (IBAction)configureSyncSettings:(id)sender {
1002 tutorialMode_ = profiles::TUTORIAL_MODE_NONE;
1003 LoginUIServiceFactory::GetForProfile(browser_->profile())->
1004 SyncConfirmationUIClosed(true);
1005 ProfileMetrics::LogProfileNewAvatarMenuSignin(
1006 ProfileMetrics::PROFILE_AVATAR_MENU_SIGNIN_SETTINGS);
1009 - (IBAction)syncSettingsConfirmed:(id)sender {
1010 tutorialMode_ = profiles::TUTORIAL_MODE_NONE;
1011 LoginUIServiceFactory::GetForProfile(browser_->profile())->
1012 SyncConfirmationUIClosed(false);
1013 ProfileMetrics::LogProfileNewAvatarMenuSignin(
1014 ProfileMetrics::PROFILE_AVATAR_MENU_SIGNIN_OK);
1015 [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER];
1018 - (IBAction)disconnectProfile:(id)sender {
1019 chrome::ShowSettings(browser_);
1020 ProfileMetrics::LogProfileNewAvatarMenuNotYou(
1021 ProfileMetrics::PROFILE_AVATAR_MENU_NOT_YOU_DISCONNECT);
1024 - (IBAction)navigateBackFromSwitchUserView:(id)sender {
1025 [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER];
1026 ProfileMetrics::LogProfileNewAvatarMenuNotYou(
1027 ProfileMetrics::PROFILE_AVATAR_MENU_NOT_YOU_BACK);
1030 - (IBAction)dismissTutorial:(id)sender {
1031 // Never shows the upgrade tutorial again if manually closed.
1032 if (tutorialMode_ == profiles::TUTORIAL_MODE_WELCOME_UPGRADE) {
1033 browser_->profile()->GetPrefs()->SetInteger(
1034 prefs::kProfileAvatarTutorialShown,
1035 signin_ui_util::kUpgradeWelcomeTutorialShowMax + 1);
1038 tutorialMode_ = profiles::TUTORIAL_MODE_NONE;
1039 [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER];
1042 - (void)windowWillClose:(NSNotification*)notification {
1043 if (tutorialMode_ == profiles::TUTORIAL_MODE_CONFIRM_SIGNIN) {
1044 LoginUIServiceFactory::GetForProfile(browser_->profile())->
1045 SyncConfirmationUIClosed(false);
1048 [super windowWillClose:notification];
1051 - (void)cleanUpEmbeddedViewContents {
1052 webContents_.reset();
1055 - (id)initWithBrowser:(Browser*)browser
1056 anchoredAt:(NSPoint)point
1057 viewMode:(profiles::BubbleViewMode)viewMode
1058 tutorialMode:(profiles::TutorialMode)tutorialMode
1059 serviceType:(signin::GAIAServiceType)serviceType {
1060 base::scoped_nsobject<InfoBubbleWindow> window([[InfoBubbleWindow alloc]
1061 initWithContentRect:ui::kWindowSizeDeterminedLater
1062 styleMask:NSBorderlessWindowMask
1063 backing:NSBackingStoreBuffered
1066 if ((self = [super initWithWindow:window
1067 parentWindow:browser->window()->GetNativeWindow()
1068 anchoredAt:point])) {
1070 viewMode_ = viewMode;
1071 tutorialMode_ = tutorialMode;
1072 observer_.reset(new ActiveProfileObserverBridge(self, browser_));
1073 serviceType_ = serviceType;
1075 avatarMenu_.reset(new AvatarMenu(
1076 &g_browser_process->profile_manager()->GetProfileInfoCache(),
1079 avatarMenu_->RebuildMenu();
1081 // Guest profiles do not have a token service.
1082 isGuestSession_ = browser_->profile()->IsGuestSession();
1084 // If view mode is PROFILE_CHOOSER but there is an auth error, force
1085 // ACCOUNT_MANAGEMENT mode.
1086 if (viewMode_ == profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER &&
1087 HasAuthError(browser_->profile()) &&
1088 switches::IsEnableAccountConsistency() &&
1089 avatarMenu_->GetItemAt(avatarMenu_->GetActiveProfileIndex()).
1091 viewMode_ = profiles::BUBBLE_VIEW_MODE_ACCOUNT_MANAGEMENT;
1094 [window accessibilitySetOverrideValue:
1095 l10n_util::GetNSString(IDS_PROFILES_NEW_AVATAR_MENU_ACCESSIBLE_NAME)
1096 forAttribute:NSAccessibilityTitleAttribute];
1097 [window accessibilitySetOverrideValue:
1098 l10n_util::GetNSString(IDS_PROFILES_NEW_AVATAR_MENU_ACCESSIBLE_NAME)
1099 forAttribute:NSAccessibilityHelpAttribute];
1101 [[self bubble] setAlignment:info_bubble::kAlignRightEdgeToAnchorEdge];
1102 [[self bubble] setArrowLocation:info_bubble::kNoArrow];
1103 [[self bubble] setBackgroundColor:GetDialogBackgroundColor()];
1104 [self initMenuContentsWithView:viewMode_];
1110 - (void)initMenuContentsWithView:(profiles::BubbleViewMode)viewToDisplay {
1111 if (browser_->profile()->IsSupervised() &&
1112 (viewToDisplay == profiles::BUBBLE_VIEW_MODE_GAIA_ADD_ACCOUNT ||
1113 viewToDisplay == profiles::BUBBLE_VIEW_MODE_ACCOUNT_REMOVAL)) {
1114 LOG(WARNING) << "Supervised user attempted to add/remove account";
1117 viewMode_ = viewToDisplay;
1118 NSView* contentView = [[self window] contentView];
1119 [contentView setSubviews:[NSArray array]];
1122 switch (viewMode_) {
1123 case profiles::BUBBLE_VIEW_MODE_GAIA_SIGNIN:
1124 case profiles::BUBBLE_VIEW_MODE_GAIA_ADD_ACCOUNT:
1125 case profiles::BUBBLE_VIEW_MODE_GAIA_REAUTH:
1126 subView = [self buildGaiaEmbeddedView];
1128 case profiles::BUBBLE_VIEW_MODE_ACCOUNT_REMOVAL:
1129 subView = [self buildAccountRemovalView];
1131 case profiles::BUBBLE_VIEW_MODE_SWITCH_USER:
1132 subView = [self buildSwitchUserView];
1134 case profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER:
1135 case profiles::BUBBLE_VIEW_MODE_ACCOUNT_MANAGEMENT:
1136 subView = [self buildProfileChooserView];
1140 // Clears tutorial mode for all non-profile-chooser views.
1141 if (viewMode_ != profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER)
1142 tutorialMode_ = profiles::TUTORIAL_MODE_NONE;
1144 // Add a dummy, empty element so that we don't initially display any
1146 NSButton* dummyFocusButton =
1147 [[[DummyWindowFocusButton alloc] initWithFrame:NSZeroRect] autorelease];
1148 [dummyFocusButton setNextKeyView:subView];
1149 [[self window] makeFirstResponder:dummyFocusButton];
1151 [contentView addSubview:subView];
1152 [contentView addSubview:dummyFocusButton];
1153 SetWindowSize([self window],
1154 NSMakeSize(NSWidth([subView frame]), NSHeight([subView frame])));
1157 - (NSView*)buildProfileChooserView {
1158 base::scoped_nsobject<NSView> container(
1159 [[NSView alloc] initWithFrame:NSZeroRect]);
1161 NSView* tutorialView = nil;
1162 NSView* currentProfileView = nil;
1163 base::scoped_nsobject<NSMutableArray> otherProfiles(
1164 [[NSMutableArray alloc] init]);
1165 // Local and guest profiles cannot lock their profile.
1166 bool enableLock = false;
1168 // Loop over the profiles in reverse, so that they are sorted by their
1169 // y-coordinate, and separate them into active and "other" profiles.
1170 for (int i = avatarMenu_->GetNumberOfItems() - 1; i >= 0; --i) {
1171 const AvatarMenu::Item& item = avatarMenu_->GetItemAt(i);
1173 if (viewMode_ == profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER) {
1174 switch (tutorialMode_) {
1175 case profiles::TUTORIAL_MODE_NONE:
1176 case profiles::TUTORIAL_MODE_WELCOME_UPGRADE:
1178 [self buildWelcomeUpgradeTutorialViewIfNeeded];
1180 case profiles::TUTORIAL_MODE_CONFIRM_SIGNIN:
1181 tutorialView = [self buildSigninConfirmationView];
1183 case profiles::TUTORIAL_MODE_SHOW_ERROR:
1184 tutorialView = [self buildSigninErrorView];
1187 currentProfileView = [self createCurrentProfileView:item];
1188 enableLock = switches::IsNewProfileManagement() && item.signed_in;
1190 [otherProfiles addObject:[self createOtherProfileView:i]];
1193 if (!currentProfileView) // Guest windows don't have an active profile.
1194 currentProfileView = [self createGuestProfileView];
1196 // |yOffset| is the next position at which to draw in |container|
1197 // coordinates. Add a pixel offset so that the bottom option buttons don't
1198 // overlap the bubble's rounded corners.
1199 CGFloat yOffset = 1;
1202 NSRect rect = NSMakeRect(0, yOffset, kFixedMenuWidth, 0);
1203 NSView* optionsView = [self createOptionsViewWithRect:rect
1204 enableLock:enableLock];
1205 [container addSubview:optionsView];
1206 rect.origin.y = NSMaxY([optionsView frame]);
1208 NSBox* separator = [self horizontalSeparatorWithFrame:rect];
1209 [container addSubview:separator];
1210 yOffset = NSMaxY([separator frame]);
1212 if (viewMode_ == profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER &&
1213 switches::IsFastUserSwitching()) {
1214 // Other profiles switcher. The profiles have already been sorted
1215 // by their y-coordinate, so they can be added in the existing order.
1216 for (NSView *otherProfileView in otherProfiles.get()) {
1217 [otherProfileView setFrameOrigin:NSMakePoint(0, yOffset)];
1218 [container addSubview:otherProfileView];
1219 yOffset = NSMaxY([otherProfileView frame]);
1221 NSBox* separator = [self horizontalSeparatorWithFrame:NSMakeRect(
1222 0, yOffset, kFixedMenuWidth, 0)];
1223 [container addSubview:separator];
1224 yOffset = NSMaxY([separator frame]);
1228 // For supervised users, add the disclaimer text.
1229 if (browser_->profile()->IsSupervised()) {
1230 yOffset += kSmallVerticalSpacing;
1231 NSView* disclaimerContainer = [self createSupervisedUserDisclaimerView];
1232 [disclaimerContainer setFrameOrigin:NSMakePoint(0, yOffset)];
1233 [container addSubview:disclaimerContainer];
1234 yOffset = NSMaxY([disclaimerContainer frame]);
1235 yOffset += kSmallVerticalSpacing;
1237 NSBox* separator = [self horizontalSeparatorWithFrame:NSMakeRect(
1238 0, yOffset, kFixedMenuWidth, 0)];
1239 [container addSubview:separator];
1240 yOffset = NSMaxY([separator frame]);
1243 if (viewMode_ == profiles::BUBBLE_VIEW_MODE_ACCOUNT_MANAGEMENT) {
1244 NSView* currentProfileAccountsView = [self createCurrentProfileAccountsView:
1245 NSMakeRect(0, yOffset, kFixedMenuWidth, 0)];
1246 [container addSubview:currentProfileAccountsView];
1247 yOffset = NSMaxY([currentProfileAccountsView frame]);
1249 NSBox* accountsSeparator = [self horizontalSeparatorWithFrame:
1250 NSMakeRect(0, yOffset, kFixedMenuWidth, 0)];
1251 [container addSubview:accountsSeparator];
1252 yOffset = NSMaxY([accountsSeparator frame]);
1255 // Active profile card.
1256 if (currentProfileView) {
1257 yOffset += kVerticalSpacing;
1258 [currentProfileView setFrameOrigin:NSMakePoint(0, yOffset)];
1259 [container addSubview:currentProfileView];
1260 yOffset = NSMaxY([currentProfileView frame]) + kVerticalSpacing;
1264 [tutorialView setFrameOrigin:NSMakePoint(0, yOffset)];
1265 [container addSubview:tutorialView];
1266 yOffset = NSMaxY([tutorialView frame]);
1267 //TODO(mlerman): update UMA stats for the new tutorials.
1269 tutorialMode_ = profiles::TUTORIAL_MODE_NONE;
1272 [container setFrameSize:NSMakeSize(kFixedMenuWidth, yOffset)];
1273 return container.autorelease();
1276 - (NSView*)buildSigninConfirmationView {
1277 ProfileMetrics::LogProfileNewAvatarMenuSignin(
1278 ProfileMetrics::PROFILE_AVATAR_MENU_SIGNIN_VIEW);
1280 NSString* titleMessage = l10n_util::GetNSString(
1281 IDS_PROFILES_CONFIRM_SIGNIN_TUTORIAL_TITLE);
1282 NSString* contentMessage = l10n_util::GetNSString(
1283 IDS_PROFILES_CONFIRM_SIGNIN_TUTORIAL_CONTENT_TEXT);
1284 NSString* linkMessage = l10n_util::GetNSString(
1285 IDS_PROFILES_SYNC_SETTINGS_LINK);
1286 NSString* buttonMessage = l10n_util::GetNSString(
1287 IDS_PROFILES_TUTORIAL_OK_BUTTON);
1288 return [self tutorialViewWithMode:profiles::TUTORIAL_MODE_CONFIRM_SIGNIN
1289 titleMessage:titleMessage
1290 contentMessage:contentMessage
1291 linkMessage:linkMessage
1292 buttonMessage:buttonMessage
1295 linkAction:@selector(configureSyncSettings:)
1296 buttonAction:@selector(syncSettingsConfirmed:)];
1299 - (NSView*)buildSigninErrorView {
1300 NSString* titleMessage = l10n_util::GetNSString(
1301 IDS_PROFILES_ERROR_TUTORIAL_TITLE);
1302 LoginUIService* loginUiService =
1303 LoginUIServiceFactory::GetForProfile(browser_->profile());
1304 NSString* contentMessage =
1305 base::SysUTF16ToNSString(loginUiService->GetLastLoginResult());
1306 NSString* linkMessage = l10n_util::GetNSString(
1307 IDS_PROFILES_PROFILE_TUTORIAL_LEARN_MORE);
1308 return [self tutorialViewWithMode:profiles::TUTORIAL_MODE_CONFIRM_SIGNIN
1309 titleMessage:titleMessage
1310 contentMessage:contentMessage
1311 linkMessage:linkMessage
1315 linkAction:@selector(showLearnMorePage:)
1319 - (NSView*)buildWelcomeUpgradeTutorialViewIfNeeded {
1320 Profile* profile = browser_->profile();
1321 const AvatarMenu::Item& avatarItem =
1322 avatarMenu_->GetItemAt(avatarMenu_->GetActiveProfileIndex());
1324 const int showCount = profile->GetPrefs()->GetInteger(
1325 prefs::kProfileAvatarTutorialShown);
1326 // Do not show the tutorial if user has dismissed it.
1327 if (showCount > signin_ui_util::kUpgradeWelcomeTutorialShowMax)
1330 if (tutorialMode_ != profiles::TUTORIAL_MODE_WELCOME_UPGRADE) {
1331 if (showCount == signin_ui_util::kUpgradeWelcomeTutorialShowMax)
1333 profile->GetPrefs()->SetInteger(
1334 prefs::kProfileAvatarTutorialShown, showCount + 1);
1337 ProfileMetrics::LogProfileNewAvatarMenuUpgrade(
1338 ProfileMetrics::PROFILE_AVATAR_MENU_UPGRADE_VIEW);
1340 NSString* titleMessage = l10n_util::GetNSString(
1341 IDS_PROFILES_WELCOME_UPGRADE_TUTORIAL_TITLE);
1342 NSString* contentMessage = l10n_util::GetNSString(
1343 IDS_PROFILES_WELCOME_UPGRADE_TUTORIAL_CONTENT_TEXT);
1344 // For local profiles, the "Not you" link doesn't make sense.
1345 NSString* linkMessage = avatarItem.signed_in ?
1347 l10n_util::GetStringFUTF16(IDS_PROFILES_NOT_YOU, avatarItem.name),
1348 kFixedMenuWidth - 2 * kHorizontalSpacing) :
1350 NSString* buttonMessage = l10n_util::GetNSString(
1351 IDS_PROFILES_TUTORIAL_WHATS_NEW_BUTTON);
1352 return [self tutorialViewWithMode:profiles::TUTORIAL_MODE_WELCOME_UPGRADE
1353 titleMessage:titleMessage
1354 contentMessage:contentMessage
1355 linkMessage:linkMessage
1356 buttonMessage:buttonMessage
1359 linkAction:@selector(showSwitchUserView:)
1360 buttonAction:@selector(seeWhatsNew:)];
1363 - (NSView*)tutorialViewWithMode:(profiles::TutorialMode)mode
1364 titleMessage:(NSString*)titleMessage
1365 contentMessage:(NSString*)contentMessage
1366 linkMessage:(NSString*)linkMessage
1367 buttonMessage:(NSString*)buttonMessage
1368 stackButton:(BOOL)stackButton
1369 hasCloseButton:(BOOL)hasCloseButton
1370 linkAction:(SEL)linkAction
1371 buttonAction:(SEL)buttonAction {
1372 tutorialMode_ = mode;
1374 NSColor* tutorialBackgroundColor =
1375 gfx::SkColorToSRGBNSColor(profiles::kAvatarTutorialBackgroundColor);
1376 base::scoped_nsobject<NSView> container([[BackgroundColorView alloc]
1377 initWithFrame:NSMakeRect(0, 0, kFixedMenuWidth, 0)
1378 withColor:tutorialBackgroundColor]);
1379 CGFloat availableWidth = kFixedMenuWidth - 2 * kHorizontalSpacing;
1380 CGFloat yOffset = kVerticalSpacing;
1382 // Adds links and buttons at the bottom.
1383 base::scoped_nsobject<NSButton> tutorialOkButton;
1384 if (buttonMessage) {
1385 tutorialOkButton.reset([[HoverButton alloc] initWithFrame:NSZeroRect]);
1386 [tutorialOkButton setTitle:buttonMessage];
1387 [tutorialOkButton setBezelStyle:NSRoundedBezelStyle];
1388 [tutorialOkButton setTarget:self];
1389 [tutorialOkButton setAction:buttonAction];
1390 [tutorialOkButton setAlignment:NSCenterTextAlignment];
1391 [tutorialOkButton sizeToFit];
1394 NSButton* learnMoreLink = nil;
1396 learnMoreLink = [self linkButtonWithTitle:linkMessage
1397 frameOrigin:NSZeroPoint
1399 [[learnMoreLink cell] setTextColor:[NSColor whiteColor]];
1404 [learnMoreLink setFrameOrigin:NSMakePoint(
1405 (kFixedMenuWidth - NSWidth([learnMoreLink frame])) / 2, yOffset)];
1407 [tutorialOkButton setFrameSize:NSMakeSize(
1408 availableWidth, NSHeight([tutorialOkButton frame]))];
1409 [tutorialOkButton setFrameOrigin:NSMakePoint(
1411 yOffset + (learnMoreLink ? NSHeight([learnMoreLink frame]) : 0))];
1413 if (buttonMessage) {
1414 NSSize buttonSize = [tutorialOkButton frame].size;
1415 const CGFloat kTopBottomTextPadding = 6;
1416 const CGFloat kLeftRightTextPadding = 15;
1417 buttonSize.width += 2 * kLeftRightTextPadding;
1418 buttonSize.height += 2 * kTopBottomTextPadding;
1419 [tutorialOkButton setFrameSize:buttonSize];
1420 CGFloat buttonXOffset = kFixedMenuWidth -
1421 NSWidth([tutorialOkButton frame]) - kHorizontalSpacing;
1422 [tutorialOkButton setFrameOrigin:NSMakePoint(buttonXOffset, yOffset)];
1426 CGFloat linkYOffset = yOffset;
1427 if (buttonMessage) {
1428 linkYOffset += (NSHeight([tutorialOkButton frame]) -
1429 NSHeight([learnMoreLink frame])) / 2;
1431 [learnMoreLink setFrameOrigin:NSMakePoint(
1432 kHorizontalSpacing, linkYOffset)];
1436 if (buttonMessage) {
1437 [container addSubview:tutorialOkButton];
1438 yOffset = NSMaxY([tutorialOkButton frame]);
1442 [container addSubview:learnMoreLink];
1443 yOffset = std::max(NSMaxY([learnMoreLink frame]), yOffset);
1446 yOffset += kVerticalSpacing;
1448 // Adds body content.
1449 NSTextField* contentLabel = BuildLabel(
1451 NSMakePoint(kHorizontalSpacing, yOffset),
1452 gfx::SkColorToSRGBNSColor(profiles::kAvatarTutorialContentTextColor));
1453 [contentLabel setFrameSize:NSMakeSize(availableWidth, 0)];
1454 [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:contentLabel];
1455 [container addSubview:contentLabel];
1456 yOffset = NSMaxY([contentLabel frame]) + kSmallVerticalSpacing;
1459 NSTextField* titleLabel =
1460 BuildLabel(titleMessage,
1461 NSMakePoint(kHorizontalSpacing, yOffset),
1462 [NSColor whiteColor] /* text_color */);
1463 [titleLabel setFont:[NSFont labelFontOfSize:kTitleFontSize]];
1465 if (hasCloseButton) {
1466 base::scoped_nsobject<HoverImageButton> closeButton(
1467 [[HoverImageButton alloc] initWithFrame:NSZeroRect]);
1468 [closeButton setBordered:NO];
1470 ui::ResourceBundle* rb = &ui::ResourceBundle::GetSharedInstance();
1471 NSImage* closeImage = rb->GetNativeImageNamed(IDR_CLOSE_1).ToNSImage();
1472 CGFloat closeImageWidth = [closeImage size].width;
1473 [closeButton setDefaultImage:closeImage];
1474 [closeButton setHoverImage:
1475 rb->GetNativeImageNamed(IDR_CLOSE_1_H).ToNSImage()];
1476 [closeButton setPressedImage:
1477 rb->GetNativeImageNamed(IDR_CLOSE_1_P).ToNSImage()];
1478 [closeButton setTarget:self];
1479 [closeButton setAction:@selector(dismissTutorial:)];
1480 [closeButton setFrameSize:[closeImage size]];
1481 [closeButton setFrameOrigin:NSMakePoint(
1482 kFixedMenuWidth - kHorizontalSpacing - closeImageWidth, yOffset)];
1483 [container addSubview:closeButton];
1485 [titleLabel setFrameSize:NSMakeSize(
1486 availableWidth - closeImageWidth - kHorizontalSpacing, 0)];
1488 [titleLabel setFrameSize:NSMakeSize(availableWidth, 0)];
1491 [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:titleLabel];
1492 [container addSubview:titleLabel];
1493 yOffset = NSMaxY([titleLabel frame]) + kVerticalSpacing;
1495 [container setFrameSize:NSMakeSize(kFixedMenuWidth, yOffset)];
1496 [container setFrameOrigin:NSZeroPoint];
1497 return container.autorelease();
1500 - (NSView*)createCurrentProfileView:(const AvatarMenu::Item&)item {
1501 base::scoped_nsobject<NSView> container([[NSView alloc]
1502 initWithFrame:NSZeroRect]);
1504 CGFloat xOffset = kHorizontalSpacing;
1505 CGFloat yOffset = 0;
1506 CGFloat availableTextWidth = kFixedMenuWidth - 2 * kHorizontalSpacing;
1508 // Profile options. This can be a link to the accounts view, the profile's
1509 // username for signed in users, or a "Sign in" button for local profiles.
1510 SigninManagerBase* signinManager =
1511 SigninManagerFactory::GetForProfile(
1512 browser_->profile()->GetOriginalProfile());
1513 if (!isGuestSession_ && signinManager->IsSigninAllowed()) {
1514 NSView* linksContainer =
1515 [self createCurrentProfileLinksForItem:item
1516 rect:NSMakeRect(xOffset, yOffset,
1519 [container addSubview:linksContainer];
1520 yOffset = NSMaxY([linksContainer frame]);
1523 // Profile name, centered.
1524 bool editingAllowed = !isGuestSession_ &&
1525 !browser_->profile()->IsSupervised();
1526 base::scoped_nsobject<EditableProfileNameButton> profileName(
1527 [[EditableProfileNameButton alloc]
1528 initWithFrame:NSMakeRect(xOffset,
1531 kProfileButtonHeight)
1532 profile:browser_->profile()
1533 profileName:base::SysUTF16ToNSString(
1534 profiles::GetAvatarNameForProfile(
1535 browser_->profile()->GetPath()))
1536 editingAllowed:editingAllowed
1537 withController:self]);
1539 [container addSubview:profileName];
1540 yOffset = NSMaxY([profileName frame]) + 4; // Adds a small vertical padding.
1542 // Profile icon, centered.
1543 xOffset = (kFixedMenuWidth - kLargeImageSide) / 2;
1544 base::scoped_nsobject<EditableProfilePhoto> iconView(
1545 [[EditableProfilePhoto alloc]
1546 initWithFrame:NSMakeRect(xOffset, yOffset,
1547 kLargeImageSide, kLargeImageSide)
1548 avatarMenu:avatarMenu_.get()
1549 profileIcon:item.icon
1550 editingAllowed:!isGuestSession_
1551 withController:self]);
1553 [container addSubview:iconView];
1554 yOffset = NSMaxY([iconView frame]);
1556 if (browser_->profile()->IsSupervised()) {
1557 base::scoped_nsobject<NSImageView> supervisedIcon(
1558 [[NSImageView alloc] initWithFrame:NSZeroRect]);
1559 ui::ResourceBundle* rb = &ui::ResourceBundle::GetSharedInstance();
1560 [supervisedIcon setImage:rb->GetNativeImageNamed(
1561 IDR_ICON_PROFILES_MENU_SUPERVISED).ToNSImage()];
1562 NSSize size = [[supervisedIcon image] size];
1563 [supervisedIcon setFrameSize:size];
1564 NSRect parentFrame = [iconView frame];
1565 [supervisedIcon setFrameOrigin:NSMakePoint(NSMaxX(parentFrame) - size.width,
1566 NSMinY(parentFrame))];
1567 [container addSubview:supervisedIcon];
1570 [container setFrameSize:NSMakeSize(kFixedMenuWidth, yOffset)];
1571 return container.autorelease();
1574 - (NSView*)createCurrentProfileLinksForItem:(const AvatarMenu::Item&)item
1576 base::scoped_nsobject<NSView> container([[NSView alloc] initWithFrame:rect]);
1578 // Don't double-apply the left margin to the sub-views.
1581 // The available links depend on the type of profile that is active.
1582 if (item.signed_in) {
1583 rect.size.height = kBlueButtonHeight / 2;
1584 // Signed in profiles with no authentication errors do not have a clickable
1586 NSButton* link = nil;
1587 if (switches::IsEnableAccountConsistency()) {
1588 NSString* linkTitle = l10n_util::GetNSString(
1589 viewMode_ == profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER ?
1590 IDS_PROFILES_PROFILE_MANAGE_ACCOUNTS_BUTTON :
1591 IDS_PROFILES_PROFILE_HIDE_MANAGE_ACCOUNTS_BUTTON);
1593 (viewMode_ == profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER) ?
1594 @selector(showAccountManagement:) : @selector(hideAccountManagement:);
1595 link = [self linkButtonWithTitle:linkTitle
1596 frameOrigin:rect.origin
1597 action:linkSelector];
1599 link = [self linkButtonWithTitle:base::SysUTF16ToNSString(item.sync_state)
1600 frameOrigin:rect.origin
1602 if (HasAuthError(browser_->profile())) {
1603 [link setImage:ui::ResourceBundle::GetSharedInstance().
1604 GetNativeImageNamed(IDR_ICON_PROFILES_ACCOUNT_BUTTON_ERROR).
1606 [link setImagePosition:NSImageRight];
1607 [link setTarget:self];
1608 [link setAction:@selector(showAccountReauthenticationView:)];
1609 [link setTag:kPrimaryProfileTag];
1611 accessibilitySetOverrideValue:l10n_util::GetNSStringF(
1612 IDS_PROFILES_ACCOUNT_BUTTON_AUTH_ERROR_ACCESSIBLE_NAME,
1614 forAttribute:NSAccessibilityTitleAttribute];
1616 [link setEnabled:NO];
1619 // -linkButtonWithTitle sizeToFit's the link, so re-stretch it so that it
1620 // can be centered correctly in the view.
1621 [link setAlignment:NSCenterTextAlignment];
1622 [link setFrame:rect];
1623 [container addSubview:link];
1624 [container setFrameSize:rect.size];
1626 rect.size.height = kBlueButtonHeight;
1627 NSButton* signinButton = [[BlueLabelButton alloc] initWithFrame:rect];
1629 // Manually elide the button text so that the contents fit inside the bubble
1630 // This is needed because the BlueLabelButton cell resets the style on
1631 // every call to -cellSize, which prevents setting a custom lineBreakMode.
1632 NSString* elidedButtonText = base::SysUTF16ToNSString(gfx::ElideText(
1633 l10n_util::GetStringFUTF16(
1634 IDS_SYNC_START_SYNC_BUTTON_LABEL,
1635 l10n_util::GetStringUTF16(IDS_SHORT_PRODUCT_NAME)),
1636 gfx::FontList(), rect.size.width, gfx::ELIDE_TAIL));
1638 [signinButton setTitle:elidedButtonText];
1639 [signinButton setTarget:self];
1640 [signinButton setAction:@selector(showInlineSigninPage:)];
1641 [container addSubview:signinButton];
1643 // Sign-in promo text.
1644 NSTextField* promo = BuildLabel(
1645 l10n_util::GetNSString(IDS_PROFILES_SIGNIN_PROMO),
1646 NSMakePoint(0, NSMaxY([signinButton frame]) + kVerticalSpacing),
1648 [promo setFrameSize:NSMakeSize(rect.size.width, 0)];
1649 [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:promo];
1650 [container addSubview:promo];
1652 [container setFrameSize:NSMakeSize(
1654 NSMaxY([promo frame]) + 4)]; // Adds a small vertical padding.
1657 return container.autorelease();
1660 - (NSView*)createSupervisedUserDisclaimerView {
1661 base::scoped_nsobject<NSView> container(
1662 [[NSView alloc] initWithFrame:NSZeroRect]);
1665 int availableTextWidth = kFixedMenuWidth - 2 * kHorizontalSpacing;
1667 NSTextField* disclaimer = BuildLabel(
1668 base::SysUTF16ToNSString(avatarMenu_->GetSupervisedUserInformation()),
1669 NSMakePoint(kHorizontalSpacing, yOffset), nil);
1670 [disclaimer setFrameSize:NSMakeSize(availableTextWidth, 0)];
1671 [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:disclaimer];
1672 yOffset = NSMaxY([disclaimer frame]);
1674 [container addSubview:disclaimer];
1675 [container setFrameSize:NSMakeSize(kFixedMenuWidth, yOffset)];
1676 return container.autorelease();
1679 - (NSView*)createGuestProfileView {
1680 gfx::Image guestIcon =
1681 ui::ResourceBundle::GetSharedInstance().GetNativeImageNamed(
1682 profiles::GetPlaceholderAvatarIconResourceID());
1683 AvatarMenu::Item guestItem(std::string::npos, /* menu_index, not used */
1684 std::string::npos, /* profile_index, not used */
1686 guestItem.active = true;
1687 guestItem.name = base::SysNSStringToUTF16(
1688 l10n_util::GetNSString(IDS_PROFILES_GUEST_PROFILE_NAME));
1690 return [self createCurrentProfileView:guestItem];
1693 - (NSButton*)createOtherProfileView:(int)itemIndex {
1694 const AvatarMenu::Item& item = avatarMenu_->GetItemAt(itemIndex);
1696 NSRect rect = NSMakeRect(
1697 0, 0, kFixedMenuWidth, kBlueButtonHeight + kSmallVerticalSpacing);
1698 base::scoped_nsobject<BackgroundColorHoverButton> profileButton(
1699 [[BackgroundColorHoverButton alloc]
1701 imageTitleSpacing:kImageTitleSpacing
1702 backgroundColor:GetDialogBackgroundColor()]);
1703 [profileButton setTitle:base::SysUTF16ToNSString(item.name)];
1704 [profileButton setDefaultImage:CreateProfileImage(
1705 item.icon, kSmallImageSide).ToNSImage()];
1706 [profileButton setImagePosition:NSImageLeft];
1707 [profileButton setAlignment:NSLeftTextAlignment];
1708 [profileButton setBordered:NO];
1709 [profileButton setTag:itemIndex];
1710 [profileButton setTarget:self];
1711 [profileButton setAction:@selector(switchToProfile:)];
1713 return profileButton.autorelease();
1716 - (NSView*)createOptionsViewWithRect:(NSRect)rect
1717 enableLock:(BOOL)enableLock {
1718 NSRect viewRect = NSMakeRect(0, 0,
1720 kBlueButtonHeight + kSmallVerticalSpacing);
1721 base::scoped_nsobject<NSView> container([[NSView alloc] initWithFrame:rect]);
1724 NSButton* lockButton =
1725 [self hoverButtonWithRect:viewRect
1726 text:l10n_util::GetNSString(
1727 IDS_PROFILES_PROFILE_SIGNOUT_BUTTON)
1728 imageResourceId:IDR_ICON_PROFILES_MENU_LOCK
1729 action:@selector(lockProfile:)];
1730 [container addSubview:lockButton];
1731 viewRect.origin.y = NSMaxY([lockButton frame]);
1733 NSBox* separator = [self horizontalSeparatorWithFrame:viewRect];
1734 [container addSubview:separator];
1735 viewRect.origin.y = NSMaxY([separator frame]);
1738 if ([self shouldShowGoIncognito]) {
1739 NSButton* goIncognitoButton =
1740 [self hoverButtonWithRect:viewRect
1741 text:l10n_util::GetNSString(
1742 IDS_PROFILES_GO_INCOGNITO_BUTTON)
1743 imageResourceId:IDR_ICON_PROFILES_MENU_INCOGNITO
1744 action:@selector(goIncognito:)];
1745 viewRect.origin.y = NSMaxY([goIncognitoButton frame]);
1746 [container addSubview:goIncognitoButton];
1748 NSBox* separator = [self horizontalSeparatorWithFrame:viewRect];
1749 [container addSubview:separator];
1750 viewRect.origin.y = NSMaxY([separator frame]);
1753 NSString* text = isGuestSession_ ?
1754 l10n_util::GetNSString(IDS_PROFILES_EXIT_GUEST) :
1755 l10n_util::GetNSString(IDS_PROFILES_SWITCH_USERS_BUTTON);
1756 NSButton* switchUsersButton =
1757 [self hoverButtonWithRect:viewRect
1759 imageResourceId:IDR_ICON_PROFILES_MENU_AVATAR
1760 action:isGuestSession_? @selector(exitGuest:) :
1761 @selector(showUserManager:)];
1762 viewRect.origin.y = NSMaxY([switchUsersButton frame]);
1763 [container addSubview:switchUsersButton];
1765 [container setFrameSize:NSMakeSize(rect.size.width, viewRect.origin.y)];
1766 return container.autorelease();
1769 - (NSView*)createCurrentProfileAccountsView:(NSRect)rect {
1770 const CGFloat kAccountButtonHeight = 34;
1772 const AvatarMenu::Item& item =
1773 avatarMenu_->GetItemAt(avatarMenu_->GetActiveProfileIndex());
1774 DCHECK(item.signed_in);
1776 NSColor* backgroundColor = gfx::SkColorToCalibratedNSColor(
1777 profiles::kAvatarBubbleAccountsBackgroundColor);
1778 base::scoped_nsobject<NSView> container([[BackgroundColorView alloc]
1780 withColor:backgroundColor]);
1783 if (!browser_->profile()->IsSupervised()) {
1784 // Manually elide the button text so the contents fit inside the bubble.
1785 // This is needed because the BlueLabelButton cell resets the style on
1786 // every call to -cellSize, which prevents setting a custom lineBreakMode.
1787 NSString* elidedButtonText = base::SysUTF16ToNSString(gfx::ElideText(
1788 l10n_util::GetStringFUTF16(
1789 IDS_PROFILES_PROFILE_ADD_ACCOUNT_BUTTON, item.name),
1790 gfx::FontList(), rect.size.width, gfx::ELIDE_TAIL));
1792 NSButton* addAccountsButton =
1793 [self linkButtonWithTitle:elidedButtonText
1794 frameOrigin:NSMakePoint(
1795 kHorizontalSpacing, kSmallVerticalSpacing)
1796 action:@selector(addAccount:)];
1797 [container addSubview:addAccountsButton];
1798 rect.origin.y += kAccountButtonHeight;
1801 NSView* accountEmails = [self createAccountsListWithRect:NSMakeRect(
1802 0, rect.origin.y, rect.size.width, kAccountButtonHeight)];
1803 [container addSubview:accountEmails];
1805 [container setFrameSize:NSMakeSize(rect.size.width,
1806 NSMaxY([accountEmails frame]))];
1807 return container.autorelease();
1810 - (NSView*)createAccountsListWithRect:(NSRect)rect {
1811 base::scoped_nsobject<NSView> container([[NSView alloc] initWithFrame:rect]);
1812 currentProfileAccounts_.clear();
1814 Profile* profile = browser_->profile();
1815 std::string primaryAccount =
1816 SigninManagerFactory::GetForProfile(profile)->GetAuthenticatedUsername();
1817 DCHECK(!primaryAccount.empty());
1818 std::vector<std::string>accounts =
1819 profiles::GetSecondaryAccountsForProfile(profile, primaryAccount);
1821 // If there is an account with an authentication error, it needs to be
1822 // badged with a warning icon.
1823 const SigninErrorController* errorController =
1824 profiles::GetSigninErrorController(profile);
1825 std::string errorAccountId =
1826 errorController ? errorController->error_account_id() : std::string();
1829 for (size_t i = 0; i < accounts.size(); ++i) {
1830 // Save the original email address, as the button text could be elided.
1831 currentProfileAccounts_[i] = accounts[i];
1832 NSButton* accountButton =
1833 [self accountButtonWithRect:rect
1836 reauthRequired:errorAccountId == accounts[i]];
1837 [container addSubview:accountButton];
1838 rect.origin.y = NSMaxY([accountButton frame]);
1841 // The primary account should always be listed first.
1842 NSButton* accountButton =
1843 [self accountButtonWithRect:rect
1844 title:primaryAccount
1845 tag:kPrimaryProfileTag
1846 reauthRequired:errorAccountId == primaryAccount];
1847 [container addSubview:accountButton];
1848 [container setFrameSize:NSMakeSize(NSWidth([container frame]),
1849 NSMaxY([accountButton frame]))];
1850 return container.autorelease();
1853 - (NSView*)buildGaiaEmbeddedView {
1854 base::scoped_nsobject<NSView> container(
1855 [[NSView alloc] initWithFrame:NSZeroRect]);
1856 CGFloat yOffset = 0;
1860 SigninErrorController* errorController = NULL;
1861 switch (viewMode_) {
1862 case profiles::BUBBLE_VIEW_MODE_GAIA_SIGNIN:
1863 url = signin::GetPromoURL(signin::SOURCE_AVATAR_BUBBLE_SIGN_IN,
1864 false /* auto_close */,
1865 true /* is_constrained */);
1866 messageId = IDS_PROFILES_GAIA_SIGNIN_TITLE;
1868 case profiles::BUBBLE_VIEW_MODE_GAIA_ADD_ACCOUNT:
1869 url = signin::GetPromoURL(signin::SOURCE_AVATAR_BUBBLE_ADD_ACCOUNT,
1870 false /* auto_close */,
1871 true /* is_constrained */);
1872 messageId = IDS_PROFILES_GAIA_ADD_ACCOUNT_TITLE;
1874 case profiles::BUBBLE_VIEW_MODE_GAIA_REAUTH:
1875 DCHECK(HasAuthError(browser_->profile()));
1876 errorController = profiles::GetSigninErrorController(browser_->profile());
1877 url = signin::GetReauthURL(
1878 browser_->profile(),
1879 errorController ? errorController->error_username() : std::string());
1880 messageId = IDS_PROFILES_GAIA_REAUTH_TITLE;
1883 NOTREACHED() << "Called with invalid mode=" << viewMode_;
1887 webContents_.reset(content::WebContents::Create(
1888 content::WebContents::CreateParams(browser_->profile())));
1889 webContents_->GetController().LoadURL(url,
1890 content::Referrer(),
1891 content::PAGE_TRANSITION_AUTO_TOPLEVEL,
1893 NSView* webview = webContents_->GetNativeView();
1894 [webview setFrameSize:NSMakeSize(kFixedGaiaViewWidth, kFixedGaiaViewHeight)];
1895 [container addSubview:webview];
1896 yOffset = NSMaxY([webview frame]);
1898 // Adds the title card.
1899 NSBox* separator = [self horizontalSeparatorWithFrame:
1900 NSMakeRect(0, yOffset, kFixedGaiaViewWidth, 0)];
1901 [container addSubview:separator];
1902 yOffset = NSMaxY([separator frame]) + kVerticalSpacing;
1904 NSView* titleView = BuildTitleCard(
1905 NSMakeRect(0, yOffset, kFixedGaiaViewWidth, 0),
1906 l10n_util::GetStringUTF16(messageId),
1907 self /* backButtonTarget*/,
1908 @selector(navigateBackFromSigninPage:) /* backButtonAction */);
1909 [container addSubview:titleView];
1910 yOffset = NSMaxY([titleView frame]);
1912 [container setFrameSize:NSMakeSize(kFixedGaiaViewWidth, yOffset)];
1913 return container.autorelease();
1916 - (NSView*)buildAccountRemovalView {
1917 DCHECK(!accountIdToRemove_.empty());
1919 base::scoped_nsobject<NSView> container(
1920 [[NSView alloc] initWithFrame:NSZeroRect]);
1921 CGFloat availableWidth =
1922 kFixedAccountRemovalViewWidth - 2 * kHorizontalSpacing;
1923 CGFloat yOffset = kVerticalSpacing;
1925 const std::string& primaryAccount = SigninManagerFactory::GetForProfile(
1926 browser_->profile())->GetAuthenticatedUsername();
1927 bool isPrimaryAccount = primaryAccount == accountIdToRemove_;
1929 // Adds "remove account" button at the bottom if needed.
1930 if (!isPrimaryAccount) {
1931 base::scoped_nsobject<NSButton> removeAccountButton(
1932 [[BlueLabelButton alloc] initWithFrame:NSZeroRect]);
1933 [removeAccountButton setTitle:l10n_util::GetNSString(
1934 IDS_PROFILES_ACCOUNT_REMOVAL_BUTTON)];
1935 [removeAccountButton setTarget:self];
1936 [removeAccountButton setAction:@selector(removeAccount:)];
1937 [removeAccountButton sizeToFit];
1938 [removeAccountButton setAlignment:NSCenterTextAlignment];
1939 CGFloat xOffset = (kFixedAccountRemovalViewWidth -
1940 NSWidth([removeAccountButton frame])) / 2;
1941 [removeAccountButton setFrameOrigin:NSMakePoint(xOffset, yOffset)];
1942 [container addSubview:removeAccountButton];
1944 yOffset = NSMaxY([removeAccountButton frame]) + kVerticalSpacing;
1947 NSView* contentView;
1948 NSPoint contentFrameOrigin = NSMakePoint(kHorizontalSpacing, yOffset);
1949 if (isPrimaryAccount) {
1950 std::vector<size_t> offsets;
1951 NSString* contentStr = l10n_util::GetNSStringF(
1952 IDS_PROFILES_PRIMARY_ACCOUNT_REMOVAL_TEXT,
1953 base::UTF8ToUTF16(accountIdToRemove_), base::string16(), &offsets);
1954 NSString* linkStr = l10n_util::GetNSString(IDS_PROFILES_SETTINGS_LINK);
1955 contentView = BuildFixedWidthTextViewWithLink(self, contentStr, linkStr,
1956 offsets[1], contentFrameOrigin, availableWidth);
1958 NSString* contentStr =
1959 l10n_util::GetNSString(IDS_PROFILES_ACCOUNT_REMOVAL_TEXT);
1960 NSTextField* contentLabel = BuildLabel(contentStr, contentFrameOrigin, nil);
1961 [contentLabel setFrameSize:NSMakeSize(availableWidth, 0)];
1962 [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:contentLabel];
1963 contentView = contentLabel;
1965 [container addSubview:contentView];
1966 yOffset = NSMaxY([contentView frame]) + kVerticalSpacing;
1968 // Adds the title card.
1969 NSBox* separator = [self horizontalSeparatorWithFrame:
1970 NSMakeRect(0, yOffset, kFixedAccountRemovalViewWidth, 0)];
1971 [container addSubview:separator];
1972 yOffset = NSMaxY([separator frame]) + kVerticalSpacing;
1974 NSView* titleView = BuildTitleCard(
1975 NSMakeRect(0, yOffset, kFixedAccountRemovalViewWidth,0),
1976 l10n_util::GetStringUTF16(IDS_PROFILES_ACCOUNT_REMOVAL_TITLE),
1977 self /* backButtonTarget*/,
1978 @selector(showAccountManagement:) /* backButtonAction */);
1979 [container addSubview:titleView];
1980 yOffset = NSMaxY([titleView frame]);
1982 [container setFrameSize:NSMakeSize(kFixedAccountRemovalViewWidth, yOffset)];
1983 return container.autorelease();
1986 - (NSView*)buildSwitchUserView {
1987 ProfileMetrics::LogProfileNewAvatarMenuNotYou(
1988 ProfileMetrics::PROFILE_AVATAR_MENU_NOT_YOU_VIEW);
1989 base::scoped_nsobject<NSView> container(
1990 [[NSView alloc] initWithFrame:NSZeroRect]);
1991 CGFloat availableWidth =
1992 kFixedSwitchUserViewWidth - 2 * kHorizontalSpacing;
1993 CGFloat yOffset = 0;
1994 NSRect viewRect = NSMakeRect(0, yOffset,
1995 kFixedSwitchUserViewWidth,
1996 kBlueButtonHeight + kSmallVerticalSpacing);
1998 const AvatarMenu::Item& avatarItem =
1999 avatarMenu_->GetItemAt(avatarMenu_->GetActiveProfileIndex());
2001 // Adds "Disconnect your Google Account" button at the bottom.
2002 NSButton* disconnectButton =
2003 [self hoverButtonWithRect:viewRect
2004 text:l10n_util::GetNSString(
2005 IDS_PROFILES_DISCONNECT_BUTTON)
2006 imageResourceId:IDR_ICON_PROFILES_MENU_DISCONNECT
2007 action:@selector(disconnectProfile:)];
2008 [container addSubview:disconnectButton];
2009 yOffset = NSMaxY([disconnectButton frame]);
2011 NSBox* separator = [self horizontalSeparatorWithFrame:
2012 NSMakeRect(0, yOffset, kFixedSwitchUserViewWidth, 0)];
2013 [container addSubview:separator];
2014 yOffset = NSMaxY([separator frame]);
2016 // Adds "Add person" button.
2017 viewRect.origin.y = yOffset;
2018 NSButton* addPersonButton =
2019 [self hoverButtonWithRect:viewRect
2020 text:l10n_util::GetNSString(
2021 IDS_PROFILES_ADD_PERSON_BUTTON)
2022 imageResourceId:IDR_ICON_PROFILES_MENU_AVATAR
2023 action:@selector(showUserManager:)];
2024 [container addSubview:addPersonButton];
2025 yOffset = NSMaxY([addPersonButton frame]);
2027 separator = [self horizontalSeparatorWithFrame:
2028 NSMakeRect(0, yOffset, kFixedSwitchUserViewWidth, 0)];
2029 [container addSubview:separator];
2030 yOffset = NSMaxY([separator frame]);
2032 // Adds the content text.
2033 base::string16 elidedName(gfx::ElideText(
2034 avatarItem.name, gfx::FontList(), availableWidth, gfx::ELIDE_TAIL));
2035 NSTextField* contentLabel = BuildLabel(
2036 l10n_util::GetNSStringF(IDS_PROFILES_NOT_YOU_CONTENT_TEXT, elidedName),
2037 NSMakePoint(kHorizontalSpacing, yOffset + kVerticalSpacing),
2039 [contentLabel setFrameSize:NSMakeSize(availableWidth, 0)];
2040 [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:contentLabel];
2041 [container addSubview:contentLabel];
2042 yOffset = NSMaxY([contentLabel frame]) + kVerticalSpacing;
2044 // Adds the title card.
2045 separator = [self horizontalSeparatorWithFrame:
2046 NSMakeRect(0, yOffset, kFixedSwitchUserViewWidth, 0)];
2047 [container addSubview:separator];
2048 yOffset = NSMaxY([separator frame]) + kVerticalSpacing;
2050 NSView* titleView = BuildTitleCard(
2051 NSMakeRect(0, yOffset, kFixedSwitchUserViewWidth,0),
2052 l10n_util::GetStringFUTF16(IDS_PROFILES_NOT_YOU, avatarItem.name),
2053 self /* backButtonTarget*/,
2054 @selector(navigateBackFromSwitchUserView:) /* backButtonAction */);
2055 [container addSubview:titleView];
2056 yOffset = NSMaxY([titleView frame]);
2058 [container setFrameSize:NSMakeSize(kFixedSwitchUserViewWidth, yOffset)];
2059 return container.autorelease();
2062 // Called when clicked on the settings link.
2063 - (BOOL)textView:(NSTextView*)textView
2064 clickedOnLink:(id)link
2065 atIndex:(NSUInteger)charIndex {
2066 chrome::ShowSettings(browser_);
2070 - (NSButton*)hoverButtonWithRect:(NSRect)rect
2071 text:(NSString*)text
2072 imageResourceId:(int)imageResourceId
2073 action:(SEL)action {
2074 base::scoped_nsobject<BackgroundColorHoverButton> button(
2075 [[BackgroundColorHoverButton alloc]
2077 imageTitleSpacing:kImageTitleSpacing
2078 backgroundColor:GetDialogBackgroundColor()]);
2080 [button setTitle:text];
2081 ui::ResourceBundle* rb = &ui::ResourceBundle::GetSharedInstance();
2082 NSImage* image = rb->GetNativeImageNamed(imageResourceId).ToNSImage();
2083 [button setDefaultImage:image];
2084 [button setHoverImage:image];
2085 [button setPressedImage:image];
2086 [button setImagePosition:NSImageLeft];
2087 [button setAlignment:NSLeftTextAlignment];
2088 [button setBordered:NO];
2089 [button setTarget:self];
2090 [button setAction:action];
2092 return button.autorelease();
2095 - (NSButton*)linkButtonWithTitle:(NSString*)title
2096 frameOrigin:(NSPoint)frameOrigin
2097 action:(SEL)action {
2098 base::scoped_nsobject<NSButton> link(
2099 [[HyperlinkButtonCell buttonWithString:title] retain]);
2101 [[link cell] setShouldUnderline:NO];
2102 [[link cell] setTextColor:gfx::SkColorToCalibratedNSColor(
2103 chrome_style::GetLinkColor())];
2104 [link setTitle:title];
2105 [link setBordered:NO];
2106 [link setFont:[NSFont labelFontOfSize:kTextFontSize]];
2107 [link setTarget:self];
2108 [link setAction:action];
2109 [link setFrameOrigin:frameOrigin];
2112 return link.autorelease();
2115 - (NSButton*)accountButtonWithRect:(NSRect)rect
2116 title:(const std::string&)title
2118 reauthRequired:(BOOL)reauthRequired {
2119 ui::ResourceBundle* rb = &ui::ResourceBundle::GetSharedInstance();
2120 NSImage* deleteImage = rb->GetNativeImageNamed(IDR_CLOSE_1).ToNSImage();
2121 CGFloat deleteImageWidth = [deleteImage size].width;
2122 NSImage* warningImage = reauthRequired ? rb->GetNativeImageNamed(
2123 IDR_ICON_PROFILES_ACCOUNT_BUTTON_ERROR).ToNSImage() : nil;
2124 CGFloat warningImageWidth = [warningImage size].width;
2126 CGFloat availableTextWidth = rect.size.width - kHorizontalSpacing -
2127 warningImageWidth - deleteImageWidth;
2129 availableTextWidth -= kHorizontalSpacing;
2131 NSColor* backgroundColor = gfx::SkColorToCalibratedNSColor(
2132 profiles::kAvatarBubbleAccountsBackgroundColor);
2133 base::scoped_nsobject<BackgroundColorHoverButton> button(
2134 [[BackgroundColorHoverButton alloc] initWithFrame:rect
2136 backgroundColor:backgroundColor]);
2137 [button setTitle:ElideEmail(title, availableTextWidth)];
2138 [button setAlignment:NSLeftTextAlignment];
2139 [button setBordered:NO];
2140 if (reauthRequired) {
2141 [button setDefaultImage:warningImage];
2142 [button setImagePosition:NSImageLeft];
2143 [button setTarget:self];
2144 [button setAction:@selector(showAccountReauthenticationView:)];
2145 [button setTag:tag];
2149 if (!browser_->profile()->IsSupervised()) {
2151 NSDivideRect(rect, &buttonRect, &rect,
2152 deleteImageWidth + kHorizontalSpacing, NSMaxXEdge);
2153 buttonRect.origin.y = 0;
2155 base::scoped_nsobject<HoverImageButton> deleteButton(
2156 [[HoverImageButton alloc] initWithFrame:buttonRect]);
2157 [deleteButton setBordered:NO];
2158 [deleteButton setDefaultImage:deleteImage];
2159 [deleteButton setHoverImage:rb->GetNativeImageNamed(
2160 IDR_CLOSE_1_H).ToNSImage()];
2161 [deleteButton setPressedImage:rb->GetNativeImageNamed(
2162 IDR_CLOSE_1_P).ToNSImage()];
2163 [deleteButton setTarget:self];
2164 [deleteButton setAction:@selector(showAccountRemovalView:)];
2165 [deleteButton setTag:tag];
2167 [button addSubview:deleteButton];
2170 return button.autorelease();
2173 - (void)postActionPerformed:(ProfileMetrics::ProfileDesktopMenu)action {
2174 ProfileMetrics::LogProfileDesktopMenu(action, serviceType_);
2175 serviceType_ = signin::GAIA_SERVICE_TYPE_NONE;
2178 - (bool)shouldShowGoIncognito {
2179 bool incognitoAvailable =
2180 IncognitoModePrefs::GetAvailability(browser_->profile()->GetPrefs()) !=
2181 IncognitoModePrefs::DISABLED;
2182 return incognitoAvailable && !browser_->profile()->IsGuestSession();