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;
88 // Fixed size for embedded sign in pages as defined in Gaia.
89 const CGFloat kFixedGaiaViewWidth = 360;
90 const CGFloat kFixedGaiaViewHeight = 440;
92 // Fixed size for the account removal view.
93 const CGFloat kFixedAccountRemovalViewWidth = 280;
95 // Fixed size for the switch user view.
96 const int kFixedSwitchUserViewWidth = 280;
98 // The tag number for the primary account.
99 const int kPrimaryProfileTag = -1;
101 gfx::Image CreateProfileImage(const gfx::Image& icon, int imageSize) {
102 return profiles::GetSizedAvatarIcon(
103 icon, true /* image is a square */, imageSize, imageSize);
106 // Updates the window size and position.
107 void SetWindowSize(NSWindow* window, NSSize size) {
108 NSRect frame = [window frame];
109 frame.origin.x += frame.size.width - size.width;
110 frame.origin.y += frame.size.height - size.height;
112 [window setFrame:frame display:YES];
115 NSString* ElideEmail(const std::string& email, CGFloat width) {
116 const base::string16 elidedEmail = gfx::ElideText(
117 base::UTF8ToUTF16(email), gfx::FontList(), width, gfx::ELIDE_EMAIL);
118 return base::SysUTF16ToNSString(elidedEmail);
121 NSString* ElideMessage(const base::string16& message, CGFloat width) {
122 return base::SysUTF16ToNSString(
123 gfx::ElideText(message, gfx::FontList(), width, gfx::ELIDE_TAIL));
126 // Builds a label with the given |title| anchored at |frame_origin|. Sets the
127 // text color to |text_color| if not null.
128 NSTextField* BuildLabel(NSString* title,
129 NSPoint frame_origin,
130 NSColor* text_color) {
131 base::scoped_nsobject<NSTextField> label(
132 [[NSTextField alloc] initWithFrame:NSZeroRect]);
133 [label setStringValue:title];
134 [label setEditable:NO];
135 [label setAlignment:NSLeftTextAlignment];
136 [label setBezeled:NO];
137 [label setFont:[NSFont labelFontOfSize:kTextFontSize]];
138 [label setDrawsBackground:NO];
139 [label setFrameOrigin:frame_origin];
143 [[label cell] setTextColor:text_color];
145 return label.autorelease();
148 // Builds an NSTextView that has the contents set to the specified |message|,
149 // with a non-underlined |link| inserted at |link_offset|. The view is anchored
150 // at the specified |frame_origin| and has a fixed |frame_width|.
151 NSTextView* BuildFixedWidthTextViewWithLink(
152 id<NSTextViewDelegate> delegate,
156 NSPoint frame_origin,
157 CGFloat frame_width) {
158 base::scoped_nsobject<HyperlinkTextView> text_view(
159 [[HyperlinkTextView alloc] initWithFrame:NSZeroRect]);
160 NSColor* link_color = gfx::SkColorToCalibratedNSColor(
161 chrome_style::GetLinkColor());
162 // Adds a padding row at the bottom, because |boundingRectWithSize| below cuts
163 // off the last row sometimes.
164 [text_view setMessageAndLink:[NSString stringWithFormat:@"%@\n", message]
167 font:[NSFont labelFontOfSize:kTextFontSize]
168 messageColor:[NSColor blackColor]
169 linkColor:link_color];
171 // Removes the underlining from the link.
172 [text_view setLinkTextAttributes:nil];
173 NSTextStorage* text_storage = [text_view textStorage];
174 NSRange link_range = NSMakeRange(link_offset, [link length]);
175 [text_storage addAttribute:NSUnderlineStyleAttributeName
176 value:[NSNumber numberWithInt:NSUnderlineStyleNone]
179 NSRect frame = [[text_view attributedString]
180 boundingRectWithSize:NSMakeSize(frame_width, 0)
181 options:NSStringDrawingUsesLineFragmentOrigin];
182 frame.origin = frame_origin;
183 [text_view setFrame:frame];
184 [text_view setDelegate:delegate];
185 return text_view.autorelease();
188 // Returns the native dialog background color.
189 NSColor* GetDialogBackgroundColor() {
190 return gfx::SkColorToCalibratedNSColor(
191 ui::NativeTheme::instance()->GetSystemColor(
192 ui::NativeTheme::kColorId_DialogBackground));
195 // Builds a title card with one back button right aligned and one label center
197 NSView* BuildTitleCard(NSRect frame_rect,
198 const base::string16& message,
199 id back_button_target,
200 SEL back_button_action) {
201 base::scoped_nsobject<NSView> container(
202 [[NSView alloc] initWithFrame:frame_rect]);
204 base::scoped_nsobject<HoverImageButton> button(
205 [[HoverImageButton alloc] initWithFrame:frame_rect]);
206 [button setBordered:NO];
207 ui::ResourceBundle* rb = &ui::ResourceBundle::GetSharedInstance();
208 [button setDefaultImage:rb->GetNativeImageNamed(IDR_BACK).ToNSImage()];
209 [button setHoverImage:rb->GetNativeImageNamed(IDR_BACK_H).ToNSImage()];
210 [button setPressedImage:rb->GetNativeImageNamed(IDR_BACK_P).ToNSImage()];
211 [button setTarget:back_button_target];
212 [button setAction:back_button_action];
213 [button setFrameSize:NSMakeSize(kProfileButtonHeight, kProfileButtonHeight)];
214 [button setFrameOrigin:NSMakePoint(kHorizontalSpacing, 0)];
216 CGFloat max_label_width = frame_rect.size.width -
217 (kHorizontalSpacing * 2 + kProfileButtonHeight) * 2;
218 NSTextField* title_label = BuildLabel(
219 ElideMessage(message, max_label_width),
221 [title_label setAlignment:NSCenterTextAlignment];
222 [title_label setFont:[NSFont labelFontOfSize:kTitleFontSize]];
223 [title_label sizeToFit];
224 CGFloat x_offset = (frame_rect.size.width - NSWidth([title_label frame])) / 2;
226 (NSHeight([button frame]) - NSHeight([title_label frame])) / 2;
227 [title_label setFrameOrigin:NSMakePoint(x_offset, y_offset)];
229 [container addSubview:button];
230 [container addSubview:title_label];
231 CGFloat height = std::max(NSMaxY([title_label frame]),
232 NSMaxY([button frame])) + kSmallVerticalSpacing;
233 [container setFrameSize:NSMakeSize(NSWidth([container frame]), height)];
235 return container.autorelease();
238 bool HasAuthError(Profile* profile) {
239 const SigninErrorController* error_controller =
240 profiles::GetSigninErrorController(profile);
241 return error_controller && error_controller->HasError();
246 // Class that listens to changes to the OAuth2Tokens for the active profile,
247 // changes to the avatar menu model or browser close notifications.
248 class ActiveProfileObserverBridge : public AvatarMenuObserver,
249 public content::NotificationObserver,
250 public OAuth2TokenService::Observer {
252 ActiveProfileObserverBridge(ProfileChooserController* controller,
254 : controller_(controller),
256 token_observer_registered_(false) {
257 registrar_.Add(this, chrome::NOTIFICATION_BROWSER_CLOSING,
258 content::NotificationService::AllSources());
259 if (!browser_->profile()->IsGuestSession())
260 AddTokenServiceObserver();
263 virtual ~ActiveProfileObserverBridge() {
264 RemoveTokenServiceObserver();
268 void AddTokenServiceObserver() {
269 ProfileOAuth2TokenService* oauth2_token_service =
270 ProfileOAuth2TokenServiceFactory::GetForProfile(browser_->profile());
271 DCHECK(oauth2_token_service);
272 oauth2_token_service->AddObserver(this);
273 token_observer_registered_ = true;
276 void RemoveTokenServiceObserver() {
277 if (!token_observer_registered_)
279 DCHECK(browser_->profile());
280 ProfileOAuth2TokenService* oauth2_token_service =
281 ProfileOAuth2TokenServiceFactory::GetForProfile(browser_->profile());
282 DCHECK(oauth2_token_service);
283 oauth2_token_service->RemoveObserver(this);
284 token_observer_registered_ = false;
287 // OAuth2TokenService::Observer:
288 virtual void OnRefreshTokenAvailable(const std::string& account_id) OVERRIDE {
289 // Tokens can only be added by adding an account through the inline flow,
290 // which is started from the account management view. Refresh it to show the
292 profiles::BubbleViewMode viewMode = [controller_ viewMode];
293 if (viewMode == profiles::BUBBLE_VIEW_MODE_ACCOUNT_MANAGEMENT ||
294 viewMode == profiles::BUBBLE_VIEW_MODE_GAIA_ADD_ACCOUNT ||
295 viewMode == profiles::BUBBLE_VIEW_MODE_GAIA_REAUTH) {
296 [controller_ initMenuContentsWithView:
297 switches::IsEnableAccountConsistency() ?
298 profiles::BUBBLE_VIEW_MODE_ACCOUNT_MANAGEMENT :
299 profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER];
303 virtual void OnRefreshTokenRevoked(const std::string& account_id) OVERRIDE {
304 // Tokens can only be removed from the account management view. Refresh it
305 // to show the update.
306 if ([controller_ viewMode] == profiles::BUBBLE_VIEW_MODE_ACCOUNT_MANAGEMENT)
307 [controller_ initMenuContentsWithView:
308 profiles::BUBBLE_VIEW_MODE_ACCOUNT_MANAGEMENT];
311 // AvatarMenuObserver:
312 virtual void OnAvatarMenuChanged(AvatarMenu* avatar_menu) OVERRIDE {
313 // Do not refresh the avatar menu if the user is on a signin related view.
314 profiles::BubbleViewMode viewMode = [controller_ viewMode];
315 if (viewMode == profiles::BUBBLE_VIEW_MODE_GAIA_SIGNIN ||
316 viewMode == profiles::BUBBLE_VIEW_MODE_GAIA_ADD_ACCOUNT ||
317 viewMode == profiles::BUBBLE_VIEW_MODE_GAIA_REAUTH) {
321 // While the bubble is open, the avatar menu can only change from the
322 // profile chooser view by modifying the current profile's photo or name.
324 initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER];
327 // content::NotificationObserver:
328 virtual void Observe(
330 const content::NotificationSource& source,
331 const content::NotificationDetails& details) OVERRIDE {
332 DCHECK_EQ(chrome::NOTIFICATION_BROWSER_CLOSING, type);
333 if (browser_ == content::Source<Browser>(source).ptr()) {
334 RemoveTokenServiceObserver();
335 // Clean up the bubble's WebContents (used by the Gaia embedded view), to
336 // make sure the guest profile doesn't have any dangling host renderers.
337 // This can happen if Chrome is quit using Command-Q while the bubble is
338 // still open, which won't give the bubble a chance to be closed and
339 // clean up the WebContents itself.
340 [controller_ cleanUpEmbeddedViewContents];
344 ProfileChooserController* controller_; // Weak; owns this.
345 Browser* browser_; // Weak.
346 content::NotificationRegistrar registrar_;
348 // The observer can be removed both when closing the browser, and by just
349 // closing the avatar bubble. However, in the case of closing the browser,
350 // the avatar bubble will also be closed afterwards, resulting in a second
351 // attempt to remove the observer. This ensures the observer is only
353 bool token_observer_registered_;
355 DISALLOW_COPY_AND_ASSIGN(ActiveProfileObserverBridge);
358 // Custom button cell that adds a left padding before the button image, and
359 // a custom spacing between the button image and title.
360 @interface CustomPaddingImageButtonCell : NSButtonCell {
362 // Padding added to the left margin of the button.
363 int leftMarginSpacing_;
364 // Spacing between the cell image and title.
365 int imageTitleSpacing_;
368 - (id)initWithLeftMarginSpacing:(int)leftMarginSpacing
369 imageTitleSpacing:(int)imageTitleSpacing;
372 @implementation CustomPaddingImageButtonCell
373 - (id)initWithLeftMarginSpacing:(int)leftMarginSpacing
374 imageTitleSpacing:(int)imageTitleSpacing {
375 if ((self = [super init])) {
376 leftMarginSpacing_ = leftMarginSpacing;
377 imageTitleSpacing_ = imageTitleSpacing;
382 - (NSRect)drawTitle:(NSAttributedString*)title
383 withFrame:(NSRect)frame
384 inView:(NSView*)controlView {
386 NSDivideRect(frame, &marginRect, &frame, leftMarginSpacing_, NSMinXEdge);
388 // The title frame origin isn't aware of the left margin spacing added
389 // in -drawImage, so it must be added when drawing the title as well.
390 if ([self imagePosition] == NSImageLeft)
391 NSDivideRect(frame, &marginRect, &frame, imageTitleSpacing_, NSMinXEdge);
393 return [super drawTitle:title withFrame:frame inView:controlView];
396 - (void)drawImage:(NSImage*)image
397 withFrame:(NSRect)frame
398 inView:(NSView*)controlView {
399 if ([self imagePosition] == NSImageLeft)
400 frame.origin.x = leftMarginSpacing_;
401 [super drawImage:image withFrame:frame inView:controlView];
405 NSSize buttonSize = [super cellSize];
406 buttonSize.width += leftMarginSpacing_;
407 if ([self imagePosition] == NSImageLeft)
408 buttonSize.width += imageTitleSpacing_;
414 // A custom button that has a transparent backround.
415 @interface TransparentBackgroundButton : NSButton
418 @implementation TransparentBackgroundButton
419 - (id)initWithFrame:(NSRect)frameRect {
420 if ((self = [super initWithFrame:frameRect])) {
421 [self setBordered:NO];
422 [self setFont:[NSFont labelFontOfSize:kTextFontSize]];
423 [self setButtonType:NSMomentaryChangeButton];
428 - (void)drawRect:(NSRect)dirtyRect {
429 NSColor* backgroundColor = [NSColor colorWithCalibratedWhite:1 alpha:0.6f];
430 [backgroundColor setFill];
431 NSRectFillUsingOperation(dirtyRect, NSCompositeSourceAtop);
432 [super drawRect:dirtyRect];
436 // A custom image control that shows a "Change" button when moused over.
437 @interface EditableProfilePhoto : NSImageView {
439 AvatarMenu* avatarMenu_; // Weak; Owned by ProfileChooserController.
440 base::scoped_nsobject<TransparentBackgroundButton> changePhotoButton_;
441 // Used to display the "Change" button on hover.
442 ui::ScopedCrTrackingArea trackingArea_;
443 ProfileChooserController* controller_;
446 - (id)initWithFrame:(NSRect)frameRect
447 avatarMenu:(AvatarMenu*)avatarMenu
448 profileIcon:(const gfx::Image&)profileIcon
449 editingAllowed:(BOOL)editingAllowed
450 withController:(ProfileChooserController*)controller;
452 // Called when the "Change" button is clicked.
453 - (void)editPhoto:(id)sender;
455 // When hovering over the profile photo, show the "Change" button.
456 - (void)mouseEntered:(NSEvent*)event;
458 // When hovering away from the profile photo, hide the "Change" button.
459 - (void)mouseExited:(NSEvent*)event;
462 @interface EditableProfilePhoto (Private)
463 // Create the "Change" avatar photo button.
464 - (TransparentBackgroundButton*)changePhotoButtonWithRect:(NSRect)rect;
467 @implementation EditableProfilePhoto
468 - (id)initWithFrame:(NSRect)frameRect
469 avatarMenu:(AvatarMenu*)avatarMenu
470 profileIcon:(const gfx::Image&)profileIcon
471 editingAllowed:(BOOL)editingAllowed
472 withController:(ProfileChooserController*)controller {
473 if ((self = [super initWithFrame:frameRect])) {
474 avatarMenu_ = avatarMenu;
475 controller_ = controller;
476 [self setImage:CreateProfileImage(
477 profileIcon, kLargeImageSide).ToNSImage()];
479 // Add a tracking area so that we can show/hide the button when hovering.
480 trackingArea_.reset([[CrTrackingArea alloc]
481 initWithRect:[self bounds]
482 options:NSTrackingMouseEnteredAndExited | NSTrackingActiveAlways
485 [self addTrackingArea:trackingArea_.get()];
487 NSRect bounds = NSMakeRect(0, 0, kLargeImageSide, kLargeImageSide);
488 if (editingAllowed) {
489 changePhotoButton_.reset([self changePhotoButtonWithRect:bounds]);
490 [self addSubview:changePhotoButton_];
492 // Hide the button until the image is hovered over.
493 [changePhotoButton_ setHidden:YES];
496 // Set the image cell's accessibility strings to be the same as the
498 [[self cell] accessibilitySetOverrideValue:l10n_util::GetNSString(
500 IDS_PROFILES_NEW_AVATAR_MENU_CHANGE_PHOTO_ACCESSIBLE_NAME :
501 IDS_PROFILES_NEW_AVATAR_MENU_PHOTO_ACCESSIBLE_NAME)
502 forAttribute:NSAccessibilityTitleAttribute];
503 [[self cell] accessibilitySetOverrideValue:
504 editingAllowed ? NSAccessibilityButtonRole : NSAccessibilityImageRole
505 forAttribute:NSAccessibilityRoleAttribute];
506 [[self cell] accessibilitySetOverrideValue:
507 NSAccessibilityRoleDescription(NSAccessibilityButtonRole, nil)
508 forAttribute:NSAccessibilityRoleDescriptionAttribute];
510 // The button and the cell should read the same thing.
511 [self accessibilitySetOverrideValue:l10n_util::GetNSString(
513 IDS_PROFILES_NEW_AVATAR_MENU_CHANGE_PHOTO_ACCESSIBLE_NAME :
514 IDS_PROFILES_NEW_AVATAR_MENU_PHOTO_ACCESSIBLE_NAME)
515 forAttribute:NSAccessibilityTitleAttribute];
516 [self accessibilitySetOverrideValue:NSAccessibilityButtonRole
517 forAttribute:NSAccessibilityRoleAttribute];
518 [self accessibilitySetOverrideValue:
519 NSAccessibilityRoleDescription(NSAccessibilityButtonRole, nil)
520 forAttribute:NSAccessibilityRoleDescriptionAttribute];
525 - (void)drawRect:(NSRect)dirtyRect {
526 NSRect bounds = [self bounds];
528 // Display the profile picture as a circle.
529 NSBezierPath* path = [NSBezierPath bezierPathWithOvalInRect:bounds];
531 [self.image drawAtPoint:bounds.origin
533 operation:NSCompositeSourceOver
538 - (void)editPhoto:(id)sender {
539 avatarMenu_->EditProfile(avatarMenu_->GetActiveProfileIndex());
541 postActionPerformed:ProfileMetrics::PROFILE_DESKTOP_MENU_EDIT_IMAGE];
544 - (void)mouseEntered:(NSEvent*)event {
545 [changePhotoButton_ setHidden:NO];
548 - (void)mouseExited:(NSEvent*)event {
549 [changePhotoButton_ setHidden:YES];
552 // Make sure the element is focusable for accessibility.
553 - (BOOL)canBecomeKeyView {
557 - (BOOL)accessibilityIsIgnored {
561 - (NSArray*)accessibilityActionNames {
562 NSArray* parentActions = [super accessibilityActionNames];
563 return [parentActions arrayByAddingObject:NSAccessibilityPressAction];
566 - (void)accessibilityPerformAction:(NSString*)action {
567 if ([action isEqualToString:NSAccessibilityPressAction]) {
568 avatarMenu_->EditProfile(avatarMenu_->GetActiveProfileIndex());
571 [super accessibilityPerformAction:action];
574 - (TransparentBackgroundButton*)changePhotoButtonWithRect:(NSRect)rect {
575 TransparentBackgroundButton* button =
576 [[TransparentBackgroundButton alloc] initWithFrame:rect];
577 [button setImage:ui::ResourceBundle::GetSharedInstance().GetNativeImageNamed(
578 IDR_ICON_PROFILES_EDIT_CAMERA).AsNSImage()];
579 [button setImagePosition:NSImageOnly];
580 [button setTarget:self];
581 [button setAction:@selector(editPhoto:)];
586 // A custom text control that turns into a textfield for editing when clicked.
587 @interface EditableProfileNameButton : HoverImageButton {
589 base::scoped_nsobject<NSTextField> profileNameTextField_;
590 Profile* profile_; // Weak.
591 ProfileChooserController* controller_;
594 - (id)initWithFrame:(NSRect)frameRect
595 profile:(Profile*)profile
596 profileName:(NSString*)profileName
597 editingAllowed:(BOOL)editingAllowed
598 withController:(ProfileChooserController*)controller;
600 // Called when the button is clicked.
601 - (void)showEditableView:(id)sender;
603 // Called when enter is pressed in the text field.
604 - (void)saveProfileName:(id)sender;
608 @implementation EditableProfileNameButton
609 - (id)initWithFrame:(NSRect)frameRect
610 profile:(Profile*)profile
611 profileName:(NSString*)profileName
612 editingAllowed:(BOOL)editingAllowed
613 withController:(ProfileChooserController*)controller {
614 if ((self = [super initWithFrame:frameRect])) {
616 controller_ = controller;
618 if (editingAllowed) {
619 // Show an "edit" pencil icon when hovering over. In the default state,
620 // we need to create an empty placeholder of the correct size, so that
621 // the text doesn't jump around when the hovered icon appears.
622 ui::ResourceBundle* rb = &ui::ResourceBundle::GetSharedInstance();
623 NSImage* hoverImage = rb->GetNativeImageNamed(
624 IDR_ICON_PROFILES_EDIT_HOVER).AsNSImage();
626 // In order to center the button title, we need to add a left padding of
627 // the same width as the pencil icon.
628 base::scoped_nsobject<CustomPaddingImageButtonCell> cell(
629 [[CustomPaddingImageButtonCell alloc]
630 initWithLeftMarginSpacing:[hoverImage size].width
631 imageTitleSpacing:0]);
632 [self setCell:cell.get()];
634 NSImage* placeholder = [[NSImage alloc] initWithSize:[hoverImage size]];
635 [self setDefaultImage:placeholder];
636 [self setHoverImage:hoverImage];
637 [self setAlternateImage:
638 rb->GetNativeImageNamed(IDR_ICON_PROFILES_EDIT_PRESSED).AsNSImage()];
639 [self setImagePosition:NSImageRight];
640 [self setTarget:self];
641 [self setAction:@selector(showEditableView:)];
643 // We need to subtract the width of the bezel from the frame rect, so that
644 // the textfield can take the exact same space as the button.
645 frameRect.size.height -= 2 * kBezelThickness;
646 frameRect.origin = NSMakePoint(0, kBezelThickness);
647 profileNameTextField_.reset(
648 [[NSTextField alloc] initWithFrame:frameRect]);
649 [profileNameTextField_ setStringValue:profileName];
650 [profileNameTextField_ setFont:[NSFont labelFontOfSize:kTitleFontSize]];
651 [profileNameTextField_ setEditable:YES];
652 [profileNameTextField_ setDrawsBackground:YES];
653 [profileNameTextField_ setBezeled:YES];
654 [profileNameTextField_ setAlignment:NSCenterTextAlignment];
655 [[profileNameTextField_ cell] setWraps:NO];
656 [[profileNameTextField_ cell] setLineBreakMode:
657 NSLineBreakByTruncatingTail];
658 [self addSubview:profileNameTextField_];
659 [profileNameTextField_ setTarget:self];
660 [profileNameTextField_ setAction:@selector(saveProfileName:)];
662 // Hide the textfield until the user clicks on the button.
663 [profileNameTextField_ setHidden:YES];
666 [[self cell] accessibilitySetOverrideValue:NSAccessibilityButtonRole
667 forAttribute:NSAccessibilityRoleAttribute];
668 [[self cell] accessibilitySetOverrideValue:
669 NSAccessibilityRoleDescription(NSAccessibilityButtonRole, nil)
670 forAttribute:NSAccessibilityRoleDescriptionAttribute];
672 [self setBordered:NO];
673 [self setFont:[NSFont labelFontOfSize:kTitleFontSize]];
674 [self setAlignment:NSCenterTextAlignment];
675 [[self cell] setLineBreakMode:NSLineBreakByTruncatingTail];
676 [self setTitle:profileName];
681 - (void)saveProfileName:(id)sender {
682 base::string16 newProfileName =
683 base::SysNSStringToUTF16([profileNameTextField_ stringValue]);
685 // Empty profile names are not allowed, and are treated as a cancel.
686 base::TrimWhitespace(newProfileName, base::TRIM_ALL, &newProfileName);
687 if (!newProfileName.empty()) {
688 profiles::UpdateProfileName(profile_, newProfileName);
690 postActionPerformed:ProfileMetrics::PROFILE_DESKTOP_MENU_EDIT_NAME];
691 [self setTitle:base::SysUTF16ToNSString(newProfileName)];
693 // Since the text is empty and not allowed, revert it from the textbox.
694 [profileNameTextField_ setStringValue:[self title]];
696 [profileNameTextField_ setHidden:YES];
699 - (void)showEditableView:(id)sender {
700 [profileNameTextField_ setHidden:NO];
701 [[self window] makeFirstResponder:profileNameTextField_];
706 // A custom button that allows for setting a background color when hovered over.
707 @interface BackgroundColorHoverButton : HoverImageButton {
709 base::scoped_nsobject<NSColor> backgroundColor_;
710 base::scoped_nsobject<NSColor> hoverColor_;
714 @implementation BackgroundColorHoverButton
716 - (id)initWithFrame:(NSRect)frameRect
717 imageTitleSpacing:(int)imageTitleSpacing
718 backgroundColor:(NSColor*)backgroundColor {
719 if ((self = [super initWithFrame:frameRect])) {
720 backgroundColor_.reset([backgroundColor retain]);
721 // Use a color from the common theme, since this button is not trying to
722 // look like a native control.
724 bool found = ui::CommonThemeGetSystemColor(
725 ui::NativeTheme::kColorId_ButtonHoverBackgroundColor, &hoverColor);
727 hoverColor_.reset([gfx::SkColorToSRGBNSColor(hoverColor) retain]);
729 [self setBordered:NO];
730 [self setFont:[NSFont labelFontOfSize:kTextFontSize]];
731 [self setButtonType:NSMomentaryChangeButton];
733 base::scoped_nsobject<CustomPaddingImageButtonCell> cell(
734 [[CustomPaddingImageButtonCell alloc]
735 initWithLeftMarginSpacing:kHorizontalSpacing
736 imageTitleSpacing:imageTitleSpacing]);
737 [cell setLineBreakMode:NSLineBreakByTruncatingTail];
738 [self setCell:cell.get()];
743 - (void)setHoverState:(HoverState)state {
744 [super setHoverState:state];
745 bool isHighlighted = ([self hoverState] != kHoverStateNone);
747 NSColor* backgroundColor = isHighlighted ? hoverColor_ : backgroundColor_;
748 [[self cell] setBackgroundColor:backgroundColor];
753 // A custom view with the given background color.
754 @interface BackgroundColorView : NSView {
756 base::scoped_nsobject<NSColor> backgroundColor_;
760 @implementation BackgroundColorView
761 - (id)initWithFrame:(NSRect)frameRect
762 withColor:(NSColor*)color {
763 if ((self = [super initWithFrame:frameRect]))
764 backgroundColor_.reset([color retain]);
768 - (void)drawRect:(NSRect)dirtyRect {
769 [backgroundColor_ setFill];
770 NSRectFill(dirtyRect);
771 [super drawRect:dirtyRect];
775 @interface ProfileChooserController ()
776 // Builds the profile chooser view.
777 - (NSView*)buildProfileChooserView;
779 // Builds a tutorial card with a title label using |titleMessage|, a content
780 // label using |contentMessage|, a link using |linkMessage|, and a button using
781 // |buttonMessage|. If |stackButton| is YES, places the button above the link.
782 // Otherwise places both on the same row with the link left aligned and button
783 // right aligned. On click, the link would execute |linkAction|, and the button
784 // would execute |buttonAction|. It sets |tutorialMode_| to the given |mode|.
785 - (NSView*)tutorialViewWithMode:(profiles::TutorialMode)mode
786 titleMessage:(NSString*)titleMessage
787 contentMessage:(NSString*)contentMessage
788 linkMessage:(NSString*)linkMessage
789 buttonMessage:(NSString*)buttonMessage
790 stackButton:(BOOL)stackButton
791 hasCloseButton:(BOOL)hasCloseButton
792 linkAction:(SEL)linkAction
793 buttonAction:(SEL)buttonAction;
795 // Builds a tutorial card to introduce an upgrade user to the new avatar menu if
796 // needed. |tutorial_shown| indicates if the tutorial has already been shown in
797 // the previous active view. |avatar_item| refers to the current profile.
798 - (NSView*)buildWelcomeUpgradeTutorialViewIfNeeded;
800 // Builds a tutorial card to have the user confirm the last Chrome signin,
801 // Chrome sync will be delayed until the user either dismisses the tutorial, or
802 // configures sync through the "Settings" link.
803 - (NSView*)buildSigninConfirmationView;
805 // Builds a tutorial card to show the last signin error.
806 - (NSView*)buildSigninErrorView;
808 // Creates the main profile card for the profile |item| at the top of
810 - (NSView*)createCurrentProfileView:(const AvatarMenu::Item&)item;
812 // Creates the possible links for the main profile card with profile |item|.
813 - (NSView*)createCurrentProfileLinksForItem:(const AvatarMenu::Item&)item
816 // Creates the disclaimer text for supervised users, telling them that the
817 // manager can view their history etc.
818 - (NSView*)createSupervisedUserDisclaimerView;
820 // Creates a main profile card for the guest user.
821 - (NSView*)createGuestProfileView;
823 // Creates an item for the profile |itemIndex| that is used in the fast profile
824 // switcher in the middle of the bubble.
825 - (NSButton*)createOtherProfileView:(int)itemIndex;
827 // Creates the "Not you" and Lock option buttons.
828 - (NSView*)createOptionsViewWithRect:(NSRect)rect
829 enableLock:(BOOL)enableLock;
831 // Creates the account management view for the active profile.
832 - (NSView*)createCurrentProfileAccountsView:(NSRect)rect;
834 // Creates the list of accounts for the active profile.
835 - (NSView*)createAccountsListWithRect:(NSRect)rect;
837 // Creates the Gaia sign-in/add account view.
838 - (NSView*)buildGaiaEmbeddedView;
840 // Creates the account removal view.
841 - (NSView*)buildAccountRemovalView;
843 // Create a view that shows various options for an upgrade user who is not
844 // the same person as the currently signed in user.
845 - (NSView*)buildSwitchUserView;
847 // Creates a button with |text|, an icon given by |imageResourceId| and with
849 - (NSButton*)hoverButtonWithRect:(NSRect)rect
851 imageResourceId:(int)imageResourceId
854 // Creates a generic link button with |title| and an |action| positioned at
856 - (NSButton*)linkButtonWithTitle:(NSString*)title
857 frameOrigin:(NSPoint)frameOrigin
860 // Creates an email account button with |title| and a remove icon. If
861 // |reauthRequired| is true, the button also displays a warning icon. |tag|
862 // indicates which account the button refers to.
863 - (NSButton*)accountButtonWithRect:(NSRect)rect
864 title:(const std::string&)title
866 reauthRequired:(BOOL)reauthRequired;
868 - (bool)shouldShowGoIncognito;
871 @implementation ProfileChooserController
872 - (profiles::BubbleViewMode) viewMode {
876 - (void)setTutorialMode:(profiles::TutorialMode)tutorialMode {
877 tutorialMode_ = tutorialMode;
880 - (IBAction)switchToProfile:(id)sender {
881 // Check the event flags to see if a new window should be created.
882 bool alwaysCreate = ui::WindowOpenDispositionFromNSEvent(
883 [NSApp currentEvent]) == NEW_WINDOW;
884 avatarMenu_->SwitchToProfile([sender tag], alwaysCreate,
885 ProfileMetrics::SWITCH_PROFILE_ICON);
888 - (IBAction)showUserManager:(id)sender {
889 chrome::ShowUserManager(browser_->profile()->GetPath());
890 [self postActionPerformed:
891 ProfileMetrics::PROFILE_DESKTOP_MENU_OPEN_USER_MANAGER];
894 - (IBAction)exitGuest:(id)sender {
895 DCHECK(browser_->profile()->IsGuestSession());
896 chrome::ShowUserManager(base::FilePath());
897 profiles::CloseGuestProfileWindows();
900 - (IBAction)goIncognito:(id)sender {
901 DCHECK([self shouldShowGoIncognito]);
902 chrome::NewIncognitoWindow(browser_);
905 - (IBAction)showAccountManagement:(id)sender {
906 [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_ACCOUNT_MANAGEMENT];
909 - (IBAction)hideAccountManagement:(id)sender {
910 [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER];
913 - (IBAction)lockProfile:(id)sender {
914 profiles::LockProfile(browser_->profile());
915 [self postActionPerformed:ProfileMetrics::PROFILE_DESKTOP_MENU_LOCK];
918 - (IBAction)showInlineSigninPage:(id)sender {
919 [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_GAIA_SIGNIN];
922 - (IBAction)addAccount:(id)sender {
923 [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_GAIA_ADD_ACCOUNT];
924 [self postActionPerformed:ProfileMetrics::PROFILE_DESKTOP_MENU_ADD_ACCT];
927 - (IBAction)navigateBackFromSigninPage:(id)sender {
928 std::string primaryAccount = SigninManagerFactory::GetForProfile(
929 browser_->profile())->GetAuthenticatedUsername();
930 bool hasAccountManagement = !primaryAccount.empty() &&
931 switches::IsEnableAccountConsistency();
932 [self initMenuContentsWithView:hasAccountManagement ?
933 profiles::BUBBLE_VIEW_MODE_ACCOUNT_MANAGEMENT :
934 profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER];
937 - (IBAction)showAccountRemovalView:(id)sender {
938 DCHECK(!isGuestSession_);
940 // Tag is either |kPrimaryProfileTag| for the primary account, or equal to the
941 // index in |currentProfileAccounts_| for a secondary account.
942 int tag = [sender tag];
943 if (tag == kPrimaryProfileTag) {
944 accountIdToRemove_ = SigninManagerFactory::GetForProfile(
945 browser_->profile())->GetAuthenticatedUsername();
947 DCHECK(ContainsKey(currentProfileAccounts_, tag));
948 accountIdToRemove_ = currentProfileAccounts_[tag];
951 [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_ACCOUNT_REMOVAL];
954 - (IBAction)showAccountReauthenticationView:(id)sender {
955 DCHECK(!isGuestSession_);
956 [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_GAIA_REAUTH];
959 - (IBAction)removeAccount:(id)sender {
960 DCHECK(!accountIdToRemove_.empty());
961 ProfileOAuth2TokenServiceFactory::GetPlatformSpecificForProfile(
962 browser_->profile())->RevokeCredentials(accountIdToRemove_);
963 [self postActionPerformed:ProfileMetrics::PROFILE_DESKTOP_MENU_REMOVE_ACCT];
964 accountIdToRemove_.clear();
966 [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_ACCOUNT_MANAGEMENT];
969 - (IBAction)seeWhatsNew:(id)sender {
970 chrome::ShowUserManagerWithTutorial(
971 profiles::USER_MANAGER_TUTORIAL_OVERVIEW);
972 ProfileMetrics::LogProfileNewAvatarMenuUpgrade(
973 ProfileMetrics::PROFILE_AVATAR_MENU_UPGRADE_WHATS_NEW);
976 - (IBAction)showSwitchUserView:(id)sender {
977 [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_SWITCH_USER];
978 ProfileMetrics::LogProfileNewAvatarMenuUpgrade(
979 ProfileMetrics::PROFILE_AVATAR_MENU_UPGRADE_NOT_YOU);
982 - (IBAction)showLearnMorePage:(id)sender {
983 signin_ui_util::ShowSigninErrorLearnMorePage(browser_->profile());
986 - (IBAction)configureSyncSettings:(id)sender {
987 tutorialMode_ = profiles::TUTORIAL_MODE_NONE;
988 LoginUIServiceFactory::GetForProfile(browser_->profile())->
989 SyncConfirmationUIClosed(true);
990 ProfileMetrics::LogProfileNewAvatarMenuSignin(
991 ProfileMetrics::PROFILE_AVATAR_MENU_SIGNIN_SETTINGS);
994 - (IBAction)syncSettingsConfirmed:(id)sender {
995 tutorialMode_ = profiles::TUTORIAL_MODE_NONE;
996 LoginUIServiceFactory::GetForProfile(browser_->profile())->
997 SyncConfirmationUIClosed(false);
998 ProfileMetrics::LogProfileNewAvatarMenuSignin(
999 ProfileMetrics::PROFILE_AVATAR_MENU_SIGNIN_OK);
1000 [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER];
1003 - (IBAction)disconnectProfile:(id)sender {
1004 chrome::ShowSettings(browser_);
1005 ProfileMetrics::LogProfileNewAvatarMenuNotYou(
1006 ProfileMetrics::PROFILE_AVATAR_MENU_NOT_YOU_DISCONNECT);
1009 - (IBAction)navigateBackFromSwitchUserView:(id)sender {
1010 [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER];
1011 ProfileMetrics::LogProfileNewAvatarMenuNotYou(
1012 ProfileMetrics::PROFILE_AVATAR_MENU_NOT_YOU_BACK);
1015 - (IBAction)dismissTutorial:(id)sender {
1016 // Never shows the upgrade tutorial again if manually closed.
1017 if (tutorialMode_ == profiles::TUTORIAL_MODE_WELCOME_UPGRADE) {
1018 browser_->profile()->GetPrefs()->SetInteger(
1019 prefs::kProfileAvatarTutorialShown,
1020 signin_ui_util::kUpgradeWelcomeTutorialShowMax + 1);
1023 tutorialMode_ = profiles::TUTORIAL_MODE_NONE;
1024 [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER];
1027 - (void)windowWillClose:(NSNotification*)notification {
1028 if (tutorialMode_ == profiles::TUTORIAL_MODE_CONFIRM_SIGNIN) {
1029 LoginUIServiceFactory::GetForProfile(browser_->profile())->
1030 SyncConfirmationUIClosed(false);
1033 [super windowWillClose:notification];
1036 - (void)cleanUpEmbeddedViewContents {
1037 webContents_.reset();
1040 - (id)initWithBrowser:(Browser*)browser
1041 anchoredAt:(NSPoint)point
1042 viewMode:(profiles::BubbleViewMode)viewMode
1043 tutorialMode:(profiles::TutorialMode)tutorialMode
1044 serviceType:(signin::GAIAServiceType)serviceType {
1045 base::scoped_nsobject<InfoBubbleWindow> window([[InfoBubbleWindow alloc]
1046 initWithContentRect:ui::kWindowSizeDeterminedLater
1047 styleMask:NSBorderlessWindowMask
1048 backing:NSBackingStoreBuffered
1051 if ((self = [super initWithWindow:window
1052 parentWindow:browser->window()->GetNativeWindow()
1053 anchoredAt:point])) {
1055 viewMode_ = viewMode;
1056 tutorialMode_ = tutorialMode;
1057 observer_.reset(new ActiveProfileObserverBridge(self, browser_));
1058 serviceType_ = serviceType;
1060 avatarMenu_.reset(new AvatarMenu(
1061 &g_browser_process->profile_manager()->GetProfileInfoCache(),
1064 avatarMenu_->RebuildMenu();
1066 // Guest profiles do not have a token service.
1067 isGuestSession_ = browser_->profile()->IsGuestSession();
1069 // If view mode is PROFILE_CHOOSER but there is an auth error, force
1070 // ACCOUNT_MANAGEMENT mode.
1071 if (viewMode_ == profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER &&
1072 HasAuthError(browser_->profile()) &&
1073 switches::IsEnableAccountConsistency() &&
1074 avatarMenu_->GetItemAt(avatarMenu_->GetActiveProfileIndex()).
1076 viewMode_ = profiles::BUBBLE_VIEW_MODE_ACCOUNT_MANAGEMENT;
1079 [window accessibilitySetOverrideValue:
1080 l10n_util::GetNSString(IDS_PROFILES_NEW_AVATAR_MENU_ACCESSIBLE_NAME)
1081 forAttribute:NSAccessibilityTitleAttribute];
1082 [window accessibilitySetOverrideValue:
1083 l10n_util::GetNSString(IDS_PROFILES_NEW_AVATAR_MENU_ACCESSIBLE_NAME)
1084 forAttribute:NSAccessibilityHelpAttribute];
1086 [[self bubble] setAlignment:info_bubble::kAlignRightEdgeToAnchorEdge];
1087 [[self bubble] setArrowLocation:info_bubble::kNoArrow];
1088 [[self bubble] setBackgroundColor:GetDialogBackgroundColor()];
1089 [self initMenuContentsWithView:viewMode_];
1095 - (void)initMenuContentsWithView:(profiles::BubbleViewMode)viewToDisplay {
1096 if (browser_->profile()->IsSupervised() &&
1097 (viewToDisplay == profiles::BUBBLE_VIEW_MODE_GAIA_ADD_ACCOUNT ||
1098 viewToDisplay == profiles::BUBBLE_VIEW_MODE_ACCOUNT_REMOVAL)) {
1099 LOG(WARNING) << "Supervised user attempted to add/remove account";
1102 viewMode_ = viewToDisplay;
1103 NSView* contentView = [[self window] contentView];
1104 [contentView setSubviews:[NSArray array]];
1107 switch (viewMode_) {
1108 case profiles::BUBBLE_VIEW_MODE_GAIA_SIGNIN:
1109 case profiles::BUBBLE_VIEW_MODE_GAIA_ADD_ACCOUNT:
1110 case profiles::BUBBLE_VIEW_MODE_GAIA_REAUTH:
1111 subView = [self buildGaiaEmbeddedView];
1113 case profiles::BUBBLE_VIEW_MODE_ACCOUNT_REMOVAL:
1114 subView = [self buildAccountRemovalView];
1116 case profiles::BUBBLE_VIEW_MODE_SWITCH_USER:
1117 subView = [self buildSwitchUserView];
1119 case profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER:
1120 case profiles::BUBBLE_VIEW_MODE_ACCOUNT_MANAGEMENT:
1121 subView = [self buildProfileChooserView];
1125 // Clears tutorial mode for all non-profile-chooser views.
1126 if (viewMode_ != profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER)
1127 tutorialMode_ = profiles::TUTORIAL_MODE_NONE;
1129 [contentView addSubview:subView];
1130 SetWindowSize([self window],
1131 NSMakeSize(NSWidth([subView frame]), NSHeight([subView frame])));
1134 - (NSView*)buildProfileChooserView {
1135 base::scoped_nsobject<NSView> container(
1136 [[NSView alloc] initWithFrame:NSZeroRect]);
1138 NSView* tutorialView = nil;
1139 NSView* currentProfileView = nil;
1140 base::scoped_nsobject<NSMutableArray> otherProfiles(
1141 [[NSMutableArray alloc] init]);
1142 // Local and guest profiles cannot lock their profile.
1143 bool enableLock = false;
1145 // Loop over the profiles in reverse, so that they are sorted by their
1146 // y-coordinate, and separate them into active and "other" profiles.
1147 for (int i = avatarMenu_->GetNumberOfItems() - 1; i >= 0; --i) {
1148 const AvatarMenu::Item& item = avatarMenu_->GetItemAt(i);
1150 if (viewMode_ == profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER) {
1151 switch (tutorialMode_) {
1152 case profiles::TUTORIAL_MODE_NONE:
1153 case profiles::TUTORIAL_MODE_WELCOME_UPGRADE:
1155 [self buildWelcomeUpgradeTutorialViewIfNeeded];
1157 case profiles::TUTORIAL_MODE_CONFIRM_SIGNIN:
1158 tutorialView = [self buildSigninConfirmationView];
1160 case profiles::TUTORIAL_MODE_SHOW_ERROR:
1161 tutorialView = [self buildSigninErrorView];
1164 currentProfileView = [self createCurrentProfileView:item];
1165 enableLock = switches::IsNewProfileManagement() && item.signed_in;
1167 [otherProfiles addObject:[self createOtherProfileView:i]];
1170 if (!currentProfileView) // Guest windows don't have an active profile.
1171 currentProfileView = [self createGuestProfileView];
1173 // |yOffset| is the next position at which to draw in |container|
1174 // coordinates. Add a pixel offset so that the bottom option buttons don't
1175 // overlap the bubble's rounded corners.
1176 CGFloat yOffset = 1;
1179 NSRect rect = NSMakeRect(0, yOffset, kFixedMenuWidth, 0);
1180 NSView* optionsView = [self createOptionsViewWithRect:rect
1181 enableLock:enableLock];
1182 [container addSubview:optionsView];
1183 rect.origin.y = NSMaxY([optionsView frame]);
1185 NSBox* separator = [self horizontalSeparatorWithFrame:rect];
1186 [container addSubview:separator];
1187 yOffset = NSMaxY([separator frame]);
1189 if (viewMode_ == profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER &&
1190 switches::IsFastUserSwitching()) {
1191 // Other profiles switcher. The profiles have already been sorted
1192 // by their y-coordinate, so they can be added in the existing order.
1193 for (NSView *otherProfileView in otherProfiles.get()) {
1194 [otherProfileView setFrameOrigin:NSMakePoint(0, yOffset)];
1195 [container addSubview:otherProfileView];
1196 yOffset = NSMaxY([otherProfileView frame]);
1198 NSBox* separator = [self horizontalSeparatorWithFrame:NSMakeRect(
1199 0, yOffset, kFixedMenuWidth, 0)];
1200 [container addSubview:separator];
1201 yOffset = NSMaxY([separator frame]);
1205 // For supervised users, add the disclaimer text.
1206 if (browser_->profile()->IsSupervised()) {
1207 yOffset += kSmallVerticalSpacing;
1208 NSView* disclaimerContainer = [self createSupervisedUserDisclaimerView];
1209 [disclaimerContainer setFrameOrigin:NSMakePoint(0, yOffset)];
1210 [container addSubview:disclaimerContainer];
1211 yOffset = NSMaxY([disclaimerContainer frame]);
1212 yOffset += kSmallVerticalSpacing;
1214 NSBox* separator = [self horizontalSeparatorWithFrame:NSMakeRect(
1215 0, yOffset, kFixedMenuWidth, 0)];
1216 [container addSubview:separator];
1217 yOffset = NSMaxY([separator frame]);
1220 if (viewMode_ == profiles::BUBBLE_VIEW_MODE_ACCOUNT_MANAGEMENT) {
1221 NSView* currentProfileAccountsView = [self createCurrentProfileAccountsView:
1222 NSMakeRect(0, yOffset, kFixedMenuWidth, 0)];
1223 [container addSubview:currentProfileAccountsView];
1224 yOffset = NSMaxY([currentProfileAccountsView frame]);
1226 NSBox* accountsSeparator = [self horizontalSeparatorWithFrame:
1227 NSMakeRect(0, yOffset, kFixedMenuWidth, 0)];
1228 [container addSubview:accountsSeparator];
1229 yOffset = NSMaxY([accountsSeparator frame]);
1232 // Active profile card.
1233 if (currentProfileView) {
1234 yOffset += kVerticalSpacing;
1235 [currentProfileView setFrameOrigin:NSMakePoint(0, yOffset)];
1236 [container addSubview:currentProfileView];
1237 yOffset = NSMaxY([currentProfileView frame]) + kVerticalSpacing;
1241 [tutorialView setFrameOrigin:NSMakePoint(0, yOffset)];
1242 [container addSubview:tutorialView];
1243 yOffset = NSMaxY([tutorialView frame]);
1244 //TODO(mlerman): update UMA stats for the new tutorials.
1246 tutorialMode_ = profiles::TUTORIAL_MODE_NONE;
1249 [container setFrameSize:NSMakeSize(kFixedMenuWidth, yOffset)];
1250 return container.autorelease();
1253 - (NSView*)buildSigninConfirmationView {
1254 ProfileMetrics::LogProfileNewAvatarMenuSignin(
1255 ProfileMetrics::PROFILE_AVATAR_MENU_SIGNIN_VIEW);
1257 NSString* titleMessage = l10n_util::GetNSString(
1258 IDS_PROFILES_CONFIRM_SIGNIN_TUTORIAL_TITLE);
1259 NSString* contentMessage = l10n_util::GetNSString(
1260 IDS_PROFILES_CONFIRM_SIGNIN_TUTORIAL_CONTENT_TEXT);
1261 NSString* linkMessage = l10n_util::GetNSString(
1262 IDS_PROFILES_SYNC_SETTINGS_LINK);
1263 NSString* buttonMessage = l10n_util::GetNSString(
1264 IDS_PROFILES_TUTORIAL_OK_BUTTON);
1265 return [self tutorialViewWithMode:profiles::TUTORIAL_MODE_CONFIRM_SIGNIN
1266 titleMessage:titleMessage
1267 contentMessage:contentMessage
1268 linkMessage:linkMessage
1269 buttonMessage:buttonMessage
1272 linkAction:@selector(configureSyncSettings:)
1273 buttonAction:@selector(syncSettingsConfirmed:)];
1276 - (NSView*)buildSigninErrorView {
1277 NSString* titleMessage = l10n_util::GetNSString(
1278 IDS_PROFILES_ERROR_TUTORIAL_TITLE);
1279 LoginUIService* loginUiService =
1280 LoginUIServiceFactory::GetForProfile(browser_->profile());
1281 NSString* contentMessage =
1282 base::SysUTF16ToNSString(loginUiService->GetLastLoginResult());
1283 NSString* linkMessage = l10n_util::GetNSString(
1284 IDS_PROFILES_PROFILE_TUTORIAL_LEARN_MORE);
1285 return [self tutorialViewWithMode:profiles::TUTORIAL_MODE_CONFIRM_SIGNIN
1286 titleMessage:titleMessage
1287 contentMessage:contentMessage
1288 linkMessage:linkMessage
1292 linkAction:@selector(showLearnMorePage:)
1296 - (NSView*)buildWelcomeUpgradeTutorialViewIfNeeded {
1297 Profile* profile = browser_->profile();
1298 const AvatarMenu::Item& avatarItem =
1299 avatarMenu_->GetItemAt(avatarMenu_->GetActiveProfileIndex());
1301 const int showCount = profile->GetPrefs()->GetInteger(
1302 prefs::kProfileAvatarTutorialShown);
1303 // Do not show the tutorial if user has dismissed it.
1304 if (showCount > signin_ui_util::kUpgradeWelcomeTutorialShowMax)
1307 if (tutorialMode_ != profiles::TUTORIAL_MODE_WELCOME_UPGRADE) {
1308 if (showCount == signin_ui_util::kUpgradeWelcomeTutorialShowMax)
1310 profile->GetPrefs()->SetInteger(
1311 prefs::kProfileAvatarTutorialShown, showCount + 1);
1314 ProfileMetrics::LogProfileNewAvatarMenuUpgrade(
1315 ProfileMetrics::PROFILE_AVATAR_MENU_UPGRADE_VIEW);
1317 NSString* titleMessage = l10n_util::GetNSString(
1318 IDS_PROFILES_WELCOME_UPGRADE_TUTORIAL_TITLE);
1319 NSString* contentMessage = l10n_util::GetNSString(
1320 IDS_PROFILES_WELCOME_UPGRADE_TUTORIAL_CONTENT_TEXT);
1321 // For local profiles, the "Not you" link doesn't make sense.
1322 NSString* linkMessage = avatarItem.signed_in ?
1324 l10n_util::GetStringFUTF16(IDS_PROFILES_NOT_YOU, avatarItem.name),
1325 kFixedMenuWidth - 2 * kHorizontalSpacing) :
1327 NSString* buttonMessage = l10n_util::GetNSString(
1328 IDS_PROFILES_TUTORIAL_WHATS_NEW_BUTTON);
1329 return [self tutorialViewWithMode:profiles::TUTORIAL_MODE_WELCOME_UPGRADE
1330 titleMessage:titleMessage
1331 contentMessage:contentMessage
1332 linkMessage:linkMessage
1333 buttonMessage:buttonMessage
1336 linkAction:@selector(showSwitchUserView:)
1337 buttonAction:@selector(seeWhatsNew:)];
1340 - (NSView*)tutorialViewWithMode:(profiles::TutorialMode)mode
1341 titleMessage:(NSString*)titleMessage
1342 contentMessage:(NSString*)contentMessage
1343 linkMessage:(NSString*)linkMessage
1344 buttonMessage:(NSString*)buttonMessage
1345 stackButton:(BOOL)stackButton
1346 hasCloseButton:(BOOL)hasCloseButton
1347 linkAction:(SEL)linkAction
1348 buttonAction:(SEL)buttonAction {
1349 tutorialMode_ = mode;
1351 NSColor* tutorialBackgroundColor =
1352 gfx::SkColorToSRGBNSColor(profiles::kAvatarTutorialBackgroundColor);
1353 base::scoped_nsobject<NSView> container([[BackgroundColorView alloc]
1354 initWithFrame:NSMakeRect(0, 0, kFixedMenuWidth, 0)
1355 withColor:tutorialBackgroundColor]);
1356 CGFloat availableWidth = kFixedMenuWidth - 2 * kHorizontalSpacing;
1357 CGFloat yOffset = kVerticalSpacing;
1359 // Adds links and buttons at the bottom.
1360 base::scoped_nsobject<NSButton> tutorialOkButton;
1361 if (buttonMessage) {
1362 tutorialOkButton.reset([[HoverButton alloc] initWithFrame:NSZeroRect]);
1363 [tutorialOkButton setTitle:buttonMessage];
1364 [tutorialOkButton setBezelStyle:NSRoundedBezelStyle];
1365 [tutorialOkButton setTarget:self];
1366 [tutorialOkButton setAction:buttonAction];
1367 [tutorialOkButton setAlignment:NSCenterTextAlignment];
1368 [tutorialOkButton sizeToFit];
1371 NSButton* learnMoreLink = nil;
1373 learnMoreLink = [self linkButtonWithTitle:linkMessage
1374 frameOrigin:NSZeroPoint
1376 [[learnMoreLink cell] setTextColor:[NSColor whiteColor]];
1381 [learnMoreLink setFrameOrigin:NSMakePoint(
1382 (kFixedMenuWidth - NSWidth([learnMoreLink frame])) / 2, yOffset)];
1384 [tutorialOkButton setFrameSize:NSMakeSize(
1385 availableWidth, NSHeight([tutorialOkButton frame]))];
1386 [tutorialOkButton setFrameOrigin:NSMakePoint(
1388 yOffset + (learnMoreLink ? NSHeight([learnMoreLink frame]) : 0))];
1390 if (buttonMessage) {
1391 NSSize buttonSize = [tutorialOkButton frame].size;
1392 const CGFloat kTopBottomTextPadding = 6;
1393 const CGFloat kLeftRightTextPadding = 15;
1394 buttonSize.width += 2 * kLeftRightTextPadding;
1395 buttonSize.height += 2 * kTopBottomTextPadding;
1396 [tutorialOkButton setFrameSize:buttonSize];
1397 CGFloat buttonXOffset = kFixedMenuWidth -
1398 NSWidth([tutorialOkButton frame]) - kHorizontalSpacing;
1399 [tutorialOkButton setFrameOrigin:NSMakePoint(buttonXOffset, yOffset)];
1403 CGFloat linkYOffset = yOffset;
1404 if (buttonMessage) {
1405 linkYOffset += (NSHeight([tutorialOkButton frame]) -
1406 NSHeight([learnMoreLink frame])) / 2;
1408 [learnMoreLink setFrameOrigin:NSMakePoint(
1409 kHorizontalSpacing, linkYOffset)];
1413 if (buttonMessage) {
1414 [container addSubview:tutorialOkButton];
1415 yOffset = NSMaxY([tutorialOkButton frame]);
1419 [container addSubview:learnMoreLink];
1420 yOffset = std::max(NSMaxY([learnMoreLink frame]), yOffset);
1423 yOffset += kVerticalSpacing;
1425 // Adds body content.
1426 NSTextField* contentLabel = BuildLabel(
1428 NSMakePoint(kHorizontalSpacing, yOffset),
1429 gfx::SkColorToSRGBNSColor(profiles::kAvatarTutorialContentTextColor));
1430 [contentLabel setFrameSize:NSMakeSize(availableWidth, 0)];
1431 [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:contentLabel];
1432 [container addSubview:contentLabel];
1433 yOffset = NSMaxY([contentLabel frame]) + kSmallVerticalSpacing;
1436 NSTextField* titleLabel =
1437 BuildLabel(titleMessage,
1438 NSMakePoint(kHorizontalSpacing, yOffset),
1439 [NSColor whiteColor] /* text_color */);
1440 [titleLabel setFont:[NSFont labelFontOfSize:kTitleFontSize]];
1442 if (hasCloseButton) {
1443 base::scoped_nsobject<HoverImageButton> closeButton(
1444 [[HoverImageButton alloc] initWithFrame:NSZeroRect]);
1445 [closeButton setBordered:NO];
1447 ui::ResourceBundle* rb = &ui::ResourceBundle::GetSharedInstance();
1448 NSImage* closeImage = rb->GetNativeImageNamed(IDR_CLOSE_1).ToNSImage();
1449 CGFloat closeImageWidth = [closeImage size].width;
1450 [closeButton setDefaultImage:closeImage];
1451 [closeButton setHoverImage:
1452 rb->GetNativeImageNamed(IDR_CLOSE_1_H).ToNSImage()];
1453 [closeButton setPressedImage:
1454 rb->GetNativeImageNamed(IDR_CLOSE_1_P).ToNSImage()];
1455 [closeButton setTarget:self];
1456 [closeButton setAction:@selector(dismissTutorial:)];
1457 [closeButton setFrameSize:[closeImage size]];
1458 [closeButton setFrameOrigin:NSMakePoint(
1459 kFixedMenuWidth - kHorizontalSpacing - closeImageWidth, yOffset)];
1460 [container addSubview:closeButton];
1462 [titleLabel setFrameSize:NSMakeSize(
1463 availableWidth - closeImageWidth - kHorizontalSpacing, 0)];
1465 [titleLabel setFrameSize:NSMakeSize(availableWidth, 0)];
1468 [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:titleLabel];
1469 [container addSubview:titleLabel];
1470 yOffset = NSMaxY([titleLabel frame]) + kVerticalSpacing;
1472 [container setFrameSize:NSMakeSize(kFixedMenuWidth, yOffset)];
1473 [container setFrameOrigin:NSZeroPoint];
1474 return container.autorelease();
1477 - (NSView*)createCurrentProfileView:(const AvatarMenu::Item&)item {
1478 base::scoped_nsobject<NSView> container([[NSView alloc]
1479 initWithFrame:NSZeroRect]);
1481 CGFloat xOffset = kHorizontalSpacing;
1482 CGFloat yOffset = 0;
1483 CGFloat availableTextWidth = kFixedMenuWidth - 2 * kHorizontalSpacing;
1485 // Profile options. This can be a link to the accounts view, the profile's
1486 // username for signed in users, or a "Sign in" button for local profiles.
1487 SigninManagerBase* signinManager =
1488 SigninManagerFactory::GetForProfile(
1489 browser_->profile()->GetOriginalProfile());
1490 if (!isGuestSession_ && signinManager->IsSigninAllowed()) {
1491 NSView* linksContainer =
1492 [self createCurrentProfileLinksForItem:item
1493 rect:NSMakeRect(xOffset, yOffset,
1496 [container addSubview:linksContainer];
1497 yOffset = NSMaxY([linksContainer frame]);
1500 // Profile name, centered.
1501 bool editingAllowed = !isGuestSession_ &&
1502 !browser_->profile()->IsSupervised();
1503 base::scoped_nsobject<EditableProfileNameButton> profileName(
1504 [[EditableProfileNameButton alloc]
1505 initWithFrame:NSMakeRect(xOffset,
1508 kProfileButtonHeight)
1509 profile:browser_->profile()
1510 profileName:base::SysUTF16ToNSString(
1511 profiles::GetAvatarNameForProfile(
1512 browser_->profile()->GetPath()))
1513 editingAllowed:editingAllowed
1514 withController:self]);
1516 [container addSubview:profileName];
1517 yOffset = NSMaxY([profileName frame]) + 4; // Adds a small vertical padding.
1519 // Profile icon, centered.
1520 xOffset = (kFixedMenuWidth - kLargeImageSide) / 2;
1521 base::scoped_nsobject<EditableProfilePhoto> iconView(
1522 [[EditableProfilePhoto alloc]
1523 initWithFrame:NSMakeRect(xOffset, yOffset,
1524 kLargeImageSide, kLargeImageSide)
1525 avatarMenu:avatarMenu_.get()
1526 profileIcon:item.icon
1527 editingAllowed:!isGuestSession_
1528 withController:self]);
1530 [container addSubview:iconView];
1531 yOffset = NSMaxY([iconView frame]);
1533 if (browser_->profile()->IsSupervised()) {
1534 base::scoped_nsobject<NSImageView> supervisedIcon(
1535 [[NSImageView alloc] initWithFrame:NSZeroRect]);
1536 ui::ResourceBundle* rb = &ui::ResourceBundle::GetSharedInstance();
1537 [supervisedIcon setImage:rb->GetNativeImageNamed(
1538 IDR_ICON_PROFILES_MENU_SUPERVISED).ToNSImage()];
1539 NSSize size = [[supervisedIcon image] size];
1540 [supervisedIcon setFrameSize:size];
1541 NSRect parentFrame = [iconView frame];
1542 [supervisedIcon setFrameOrigin:NSMakePoint(NSMaxX(parentFrame) - size.width,
1543 NSMinY(parentFrame))];
1544 [container addSubview:supervisedIcon];
1547 [container setFrameSize:NSMakeSize(kFixedMenuWidth, yOffset)];
1548 return container.autorelease();
1551 - (NSView*)createCurrentProfileLinksForItem:(const AvatarMenu::Item&)item
1553 base::scoped_nsobject<NSView> container([[NSView alloc] initWithFrame:rect]);
1555 // Don't double-apply the left margin to the sub-views.
1558 // The available links depend on the type of profile that is active.
1559 if (item.signed_in) {
1560 rect.size.height = kBlueButtonHeight / 2;
1561 // Signed in profiles with no authentication errors do not have a clickable
1563 NSButton* link = nil;
1564 if (switches::IsEnableAccountConsistency()) {
1565 NSString* linkTitle = l10n_util::GetNSString(
1566 viewMode_ == profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER ?
1567 IDS_PROFILES_PROFILE_MANAGE_ACCOUNTS_BUTTON :
1568 IDS_PROFILES_PROFILE_HIDE_MANAGE_ACCOUNTS_BUTTON);
1570 (viewMode_ == profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER) ?
1571 @selector(showAccountManagement:) : @selector(hideAccountManagement:);
1572 link = [self linkButtonWithTitle:linkTitle
1573 frameOrigin:rect.origin
1574 action:linkSelector];
1576 link = [self linkButtonWithTitle:base::SysUTF16ToNSString(item.sync_state)
1577 frameOrigin:rect.origin
1579 if (HasAuthError(browser_->profile())) {
1580 [link setImage:ui::ResourceBundle::GetSharedInstance().
1581 GetNativeImageNamed(IDR_ICON_PROFILES_ACCOUNT_BUTTON_ERROR).
1583 [link setImagePosition:NSImageRight];
1584 [link setTarget:self];
1585 [link setAction:@selector(showAccountReauthenticationView:)];
1586 [link setTag:kPrimaryProfileTag];
1588 accessibilitySetOverrideValue:l10n_util::GetNSStringF(
1589 IDS_PROFILES_ACCOUNT_BUTTON_AUTH_ERROR_ACCESSIBLE_NAME,
1591 forAttribute:NSAccessibilityTitleAttribute];
1593 [link setEnabled:NO];
1596 // -linkButtonWithTitle sizeToFit's the link, so re-stretch it so that it
1597 // can be centered correctly in the view.
1598 [link setAlignment:NSCenterTextAlignment];
1599 [link setFrame:rect];
1600 [container addSubview:link];
1601 [container setFrameSize:rect.size];
1603 rect.size.height = kBlueButtonHeight;
1604 NSButton* signinButton = [[BlueLabelButton alloc] initWithFrame:rect];
1606 // Manually elide the button text so that the contents fit inside the bubble
1607 // This is needed because the BlueLabelButton cell resets the style on
1608 // every call to -cellSize, which prevents setting a custom lineBreakMode.
1609 NSString* elidedButtonText = base::SysUTF16ToNSString(gfx::ElideText(
1610 l10n_util::GetStringFUTF16(
1611 IDS_SYNC_START_SYNC_BUTTON_LABEL,
1612 l10n_util::GetStringUTF16(IDS_SHORT_PRODUCT_NAME)),
1613 gfx::FontList(), rect.size.width, gfx::ELIDE_TAIL));
1615 [signinButton setTitle:elidedButtonText];
1616 [signinButton setTarget:self];
1617 [signinButton setAction:@selector(showInlineSigninPage:)];
1618 [container addSubview:signinButton];
1620 // Sign-in promo text.
1621 NSTextField* promo = BuildLabel(
1622 l10n_util::GetNSString(IDS_PROFILES_SIGNIN_PROMO),
1623 NSMakePoint(0, NSMaxY([signinButton frame]) + kVerticalSpacing),
1625 [promo setFrameSize:NSMakeSize(rect.size.width, 0)];
1626 [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:promo];
1627 [container addSubview:promo];
1629 [container setFrameSize:NSMakeSize(
1631 NSMaxY([promo frame]) + 4)]; // Adds a small vertical padding.
1634 return container.autorelease();
1637 - (NSView*)createSupervisedUserDisclaimerView {
1638 base::scoped_nsobject<NSView> container(
1639 [[NSView alloc] initWithFrame:NSZeroRect]);
1642 int availableTextWidth = kFixedMenuWidth - 2 * kHorizontalSpacing;
1644 NSTextField* disclaimer = BuildLabel(
1645 base::SysUTF16ToNSString(avatarMenu_->GetSupervisedUserInformation()),
1646 NSMakePoint(kHorizontalSpacing, yOffset), nil);
1647 [disclaimer setFrameSize:NSMakeSize(availableTextWidth, 0)];
1648 [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:disclaimer];
1649 yOffset = NSMaxY([disclaimer frame]);
1651 [container addSubview:disclaimer];
1652 [container setFrameSize:NSMakeSize(kFixedMenuWidth, yOffset)];
1653 return container.autorelease();
1656 - (NSView*)createGuestProfileView {
1657 gfx::Image guestIcon =
1658 ui::ResourceBundle::GetSharedInstance().GetNativeImageNamed(
1659 profiles::GetPlaceholderAvatarIconResourceID());
1660 AvatarMenu::Item guestItem(std::string::npos, /* menu_index, not used */
1661 std::string::npos, /* profile_index, not used */
1663 guestItem.active = true;
1664 guestItem.name = base::SysNSStringToUTF16(
1665 l10n_util::GetNSString(IDS_PROFILES_GUEST_PROFILE_NAME));
1667 return [self createCurrentProfileView:guestItem];
1670 - (NSButton*)createOtherProfileView:(int)itemIndex {
1671 const AvatarMenu::Item& item = avatarMenu_->GetItemAt(itemIndex);
1673 NSRect rect = NSMakeRect(0, 0, kFixedMenuWidth, kBlueButtonHeight);
1674 base::scoped_nsobject<BackgroundColorHoverButton> profileButton(
1675 [[BackgroundColorHoverButton alloc]
1677 imageTitleSpacing:kImageTitleSpacing
1678 backgroundColor:GetDialogBackgroundColor()]);
1679 [profileButton setTitle:base::SysUTF16ToNSString(item.name)];
1680 [profileButton setDefaultImage:CreateProfileImage(
1681 item.icon, kSmallImageSide).ToNSImage()];
1682 [profileButton setImagePosition:NSImageLeft];
1683 [profileButton setAlignment:NSLeftTextAlignment];
1684 [profileButton setBordered:NO];
1685 [profileButton setTag:itemIndex];
1686 [profileButton setTarget:self];
1687 [profileButton setAction:@selector(switchToProfile:)];
1689 return profileButton.autorelease();
1692 - (NSView*)createOptionsViewWithRect:(NSRect)rect
1693 enableLock:(BOOL)enableLock {
1694 NSRect viewRect = NSMakeRect(0, 0,
1696 kBlueButtonHeight + kSmallVerticalSpacing);
1697 base::scoped_nsobject<NSView> container([[NSView alloc] initWithFrame:rect]);
1700 NSButton* lockButton =
1701 [self hoverButtonWithRect:viewRect
1702 text:l10n_util::GetNSString(
1703 IDS_PROFILES_PROFILE_SIGNOUT_BUTTON)
1704 imageResourceId:IDR_ICON_PROFILES_MENU_LOCK
1705 action:@selector(lockProfile:)];
1706 [container addSubview:lockButton];
1707 viewRect.origin.y = NSMaxY([lockButton frame]);
1709 NSBox* separator = [self horizontalSeparatorWithFrame:viewRect];
1710 [container addSubview:separator];
1711 viewRect.origin.y = NSMaxY([separator frame]);
1714 if ([self shouldShowGoIncognito]) {
1715 NSButton* goIncognitoButton =
1716 [self hoverButtonWithRect:viewRect
1717 text:l10n_util::GetNSString(
1718 IDS_PROFILES_GO_INCOGNITO_BUTTON)
1719 imageResourceId:IDR_ICON_PROFILES_MENU_INCOGNITO
1720 action:@selector(goIncognito:)];
1721 viewRect.origin.y = NSMaxY([goIncognitoButton frame]);
1722 [container addSubview:goIncognitoButton];
1724 NSBox* separator = [self horizontalSeparatorWithFrame:viewRect];
1725 [container addSubview:separator];
1726 viewRect.origin.y = NSMaxY([separator frame]);
1729 NSString* text = isGuestSession_ ?
1730 l10n_util::GetNSString(IDS_PROFILES_EXIT_GUEST) :
1731 l10n_util::GetNSString(IDS_PROFILES_SWITCH_USERS_BUTTON);
1732 NSButton* switchUsersButton =
1733 [self hoverButtonWithRect:viewRect
1735 imageResourceId:IDR_ICON_PROFILES_MENU_AVATAR
1736 action:isGuestSession_? @selector(exitGuest:) :
1737 @selector(showUserManager:)];
1738 viewRect.origin.y = NSMaxY([switchUsersButton frame]);
1739 [container addSubview:switchUsersButton];
1741 [container setFrameSize:NSMakeSize(rect.size.width, viewRect.origin.y)];
1742 return container.autorelease();
1745 - (NSView*)createCurrentProfileAccountsView:(NSRect)rect {
1746 const CGFloat kAccountButtonHeight = 34;
1748 const AvatarMenu::Item& item =
1749 avatarMenu_->GetItemAt(avatarMenu_->GetActiveProfileIndex());
1750 DCHECK(item.signed_in);
1752 NSColor* backgroundColor = gfx::SkColorToCalibratedNSColor(
1753 profiles::kAvatarBubbleAccountsBackgroundColor);
1754 base::scoped_nsobject<NSView> container([[BackgroundColorView alloc]
1756 withColor:backgroundColor]);
1759 if (!browser_->profile()->IsSupervised()) {
1760 // Manually elide the button text so the contents fit inside the bubble.
1761 // This is needed because the BlueLabelButton cell resets the style on
1762 // every call to -cellSize, which prevents setting a custom lineBreakMode.
1763 NSString* elidedButtonText = base::SysUTF16ToNSString(gfx::ElideText(
1764 l10n_util::GetStringFUTF16(
1765 IDS_PROFILES_PROFILE_ADD_ACCOUNT_BUTTON, item.name),
1766 gfx::FontList(), rect.size.width, gfx::ELIDE_TAIL));
1768 NSButton* addAccountsButton =
1769 [self linkButtonWithTitle:elidedButtonText
1770 frameOrigin:NSMakePoint(
1771 kHorizontalSpacing, kSmallVerticalSpacing)
1772 action:@selector(addAccount:)];
1773 [container addSubview:addAccountsButton];
1774 rect.origin.y += kAccountButtonHeight;
1777 NSView* accountEmails = [self createAccountsListWithRect:NSMakeRect(
1778 0, rect.origin.y, rect.size.width, kAccountButtonHeight)];
1779 [container addSubview:accountEmails];
1781 [container setFrameSize:NSMakeSize(rect.size.width,
1782 NSMaxY([accountEmails frame]))];
1783 return container.autorelease();
1786 - (NSView*)createAccountsListWithRect:(NSRect)rect {
1787 base::scoped_nsobject<NSView> container([[NSView alloc] initWithFrame:rect]);
1788 currentProfileAccounts_.clear();
1790 Profile* profile = browser_->profile();
1791 std::string primaryAccount =
1792 SigninManagerFactory::GetForProfile(profile)->GetAuthenticatedUsername();
1793 DCHECK(!primaryAccount.empty());
1794 std::vector<std::string>accounts =
1795 profiles::GetSecondaryAccountsForProfile(profile, primaryAccount);
1797 // If there is an account with an authentication error, it needs to be
1798 // badged with a warning icon.
1799 const SigninErrorController* errorController =
1800 profiles::GetSigninErrorController(profile);
1801 std::string errorAccountId =
1802 errorController ? errorController->error_account_id() : std::string();
1805 for (size_t i = 0; i < accounts.size(); ++i) {
1806 // Save the original email address, as the button text could be elided.
1807 currentProfileAccounts_[i] = accounts[i];
1808 NSButton* accountButton =
1809 [self accountButtonWithRect:rect
1812 reauthRequired:errorAccountId == accounts[i]];
1813 [container addSubview:accountButton];
1814 rect.origin.y = NSMaxY([accountButton frame]);
1817 // The primary account should always be listed first.
1818 NSButton* accountButton =
1819 [self accountButtonWithRect:rect
1820 title:primaryAccount
1821 tag:kPrimaryProfileTag
1822 reauthRequired:errorAccountId == primaryAccount];
1823 [container addSubview:accountButton];
1824 [container setFrameSize:NSMakeSize(NSWidth([container frame]),
1825 NSMaxY([accountButton frame]))];
1826 return container.autorelease();
1829 - (NSView*)buildGaiaEmbeddedView {
1830 base::scoped_nsobject<NSView> container(
1831 [[NSView alloc] initWithFrame:NSZeroRect]);
1832 CGFloat yOffset = 0;
1836 SigninErrorController* errorController = NULL;
1837 switch (viewMode_) {
1838 case profiles::BUBBLE_VIEW_MODE_GAIA_SIGNIN:
1839 url = signin::GetPromoURL(signin::SOURCE_AVATAR_BUBBLE_SIGN_IN,
1840 false /* auto_close */,
1841 true /* is_constrained */);
1842 messageId = IDS_PROFILES_GAIA_SIGNIN_TITLE;
1844 case profiles::BUBBLE_VIEW_MODE_GAIA_ADD_ACCOUNT:
1845 url = signin::GetPromoURL(signin::SOURCE_AVATAR_BUBBLE_ADD_ACCOUNT,
1846 false /* auto_close */,
1847 true /* is_constrained */);
1848 messageId = IDS_PROFILES_GAIA_ADD_ACCOUNT_TITLE;
1850 case profiles::BUBBLE_VIEW_MODE_GAIA_REAUTH:
1851 DCHECK(HasAuthError(browser_->profile()));
1852 errorController = profiles::GetSigninErrorController(browser_->profile());
1853 url = signin::GetReauthURL(
1854 browser_->profile(),
1855 errorController ? errorController->error_username() : std::string());
1856 messageId = IDS_PROFILES_GAIA_REAUTH_TITLE;
1859 NOTREACHED() << "Called with invalid mode=" << viewMode_;
1863 webContents_.reset(content::WebContents::Create(
1864 content::WebContents::CreateParams(browser_->profile())));
1865 webContents_->GetController().LoadURL(url,
1866 content::Referrer(),
1867 content::PAGE_TRANSITION_AUTO_TOPLEVEL,
1869 NSView* webview = webContents_->GetNativeView();
1870 [webview setFrameSize:NSMakeSize(kFixedGaiaViewWidth, kFixedGaiaViewHeight)];
1871 [container addSubview:webview];
1872 yOffset = NSMaxY([webview frame]);
1874 // Adds the title card.
1875 NSBox* separator = [self horizontalSeparatorWithFrame:
1876 NSMakeRect(0, yOffset, kFixedGaiaViewWidth, 0)];
1877 [container addSubview:separator];
1878 yOffset = NSMaxY([separator frame]) + kSmallVerticalSpacing;
1880 NSView* titleView = BuildTitleCard(
1881 NSMakeRect(0, yOffset, kFixedGaiaViewWidth, 0),
1882 l10n_util::GetStringUTF16(messageId),
1883 self /* backButtonTarget*/,
1884 @selector(navigateBackFromSigninPage:) /* backButtonAction */);
1885 [container addSubview:titleView];
1886 yOffset = NSMaxY([titleView frame]);
1888 [container setFrameSize:NSMakeSize(kFixedGaiaViewWidth, yOffset)];
1889 return container.autorelease();
1892 - (NSView*)buildAccountRemovalView {
1893 DCHECK(!accountIdToRemove_.empty());
1895 base::scoped_nsobject<NSView> container(
1896 [[NSView alloc] initWithFrame:NSZeroRect]);
1897 CGFloat availableWidth =
1898 kFixedAccountRemovalViewWidth - 2 * kHorizontalSpacing;
1899 CGFloat yOffset = kVerticalSpacing;
1901 const std::string& primaryAccount = SigninManagerFactory::GetForProfile(
1902 browser_->profile())->GetAuthenticatedUsername();
1903 bool isPrimaryAccount = primaryAccount == accountIdToRemove_;
1905 // Adds "remove account" button at the bottom if needed.
1906 if (!isPrimaryAccount) {
1907 base::scoped_nsobject<NSButton> removeAccountButton(
1908 [[BlueLabelButton alloc] initWithFrame:NSZeroRect]);
1909 [removeAccountButton setTitle:l10n_util::GetNSString(
1910 IDS_PROFILES_ACCOUNT_REMOVAL_BUTTON)];
1911 [removeAccountButton setTarget:self];
1912 [removeAccountButton setAction:@selector(removeAccount:)];
1913 [removeAccountButton sizeToFit];
1914 [removeAccountButton setAlignment:NSCenterTextAlignment];
1915 CGFloat xOffset = (kFixedAccountRemovalViewWidth -
1916 NSWidth([removeAccountButton frame])) / 2;
1917 [removeAccountButton setFrameOrigin:NSMakePoint(xOffset, yOffset)];
1918 [container addSubview:removeAccountButton];
1920 yOffset = NSMaxY([removeAccountButton frame]) + kVerticalSpacing;
1923 NSView* contentView;
1924 NSPoint contentFrameOrigin = NSMakePoint(kHorizontalSpacing, yOffset);
1925 if (isPrimaryAccount) {
1926 std::vector<size_t> offsets;
1927 NSString* contentStr = l10n_util::GetNSStringF(
1928 IDS_PROFILES_PRIMARY_ACCOUNT_REMOVAL_TEXT,
1929 base::UTF8ToUTF16(accountIdToRemove_), base::string16(), &offsets);
1930 NSString* linkStr = l10n_util::GetNSString(IDS_PROFILES_SETTINGS_LINK);
1931 contentView = BuildFixedWidthTextViewWithLink(self, contentStr, linkStr,
1932 offsets[1], contentFrameOrigin, availableWidth);
1934 NSString* contentStr =
1935 l10n_util::GetNSString(IDS_PROFILES_ACCOUNT_REMOVAL_TEXT);
1936 NSTextField* contentLabel = BuildLabel(contentStr, contentFrameOrigin, nil);
1937 [contentLabel setFrameSize:NSMakeSize(availableWidth, 0)];
1938 [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:contentLabel];
1939 contentView = contentLabel;
1941 [container addSubview:contentView];
1942 yOffset = NSMaxY([contentView frame]) + kVerticalSpacing;
1944 // Adds the title card.
1945 NSBox* separator = [self horizontalSeparatorWithFrame:
1946 NSMakeRect(0, yOffset, kFixedAccountRemovalViewWidth, 0)];
1947 [container addSubview:separator];
1948 yOffset = NSMaxY([separator frame]) + kSmallVerticalSpacing;
1950 NSView* titleView = BuildTitleCard(
1951 NSMakeRect(0, yOffset, kFixedAccountRemovalViewWidth,0),
1952 l10n_util::GetStringUTF16(IDS_PROFILES_ACCOUNT_REMOVAL_TITLE),
1953 self /* backButtonTarget*/,
1954 @selector(showAccountManagement:) /* backButtonAction */);
1955 [container addSubview:titleView];
1956 yOffset = NSMaxY([titleView frame]);
1958 [container setFrameSize:NSMakeSize(kFixedAccountRemovalViewWidth, yOffset)];
1959 return container.autorelease();
1962 - (NSView*)buildSwitchUserView {
1963 ProfileMetrics::LogProfileNewAvatarMenuNotYou(
1964 ProfileMetrics::PROFILE_AVATAR_MENU_NOT_YOU_VIEW);
1965 base::scoped_nsobject<NSView> container(
1966 [[NSView alloc] initWithFrame:NSZeroRect]);
1967 CGFloat availableWidth =
1968 kFixedSwitchUserViewWidth - 2 * kHorizontalSpacing;
1969 CGFloat yOffset = 0;
1970 NSRect viewRect = NSMakeRect(0, yOffset,
1971 kFixedSwitchUserViewWidth,
1972 kBlueButtonHeight + kSmallVerticalSpacing);
1974 const AvatarMenu::Item& avatarItem =
1975 avatarMenu_->GetItemAt(avatarMenu_->GetActiveProfileIndex());
1977 // Adds "Disconnect your Google Account" button at the bottom.
1978 NSButton* disconnectButton =
1979 [self hoverButtonWithRect:viewRect
1980 text:l10n_util::GetNSString(
1981 IDS_PROFILES_DISCONNECT_BUTTON)
1982 imageResourceId:IDR_ICON_PROFILES_MENU_DISCONNECT
1983 action:@selector(disconnectProfile:)];
1984 [container addSubview:disconnectButton];
1985 yOffset = NSMaxY([disconnectButton frame]);
1987 NSBox* separator = [self horizontalSeparatorWithFrame:
1988 NSMakeRect(0, yOffset, kFixedMenuWidth, 0)];
1989 [container addSubview:separator];
1990 yOffset = NSMaxY([separator frame]);
1992 // Adds "Add person" button.
1993 viewRect.origin.y = yOffset;
1994 NSButton* addPersonButton =
1995 [self hoverButtonWithRect:viewRect
1996 text:l10n_util::GetNSString(
1997 IDS_PROFILES_ADD_PERSON_BUTTON)
1998 imageResourceId:IDR_ICON_PROFILES_MENU_AVATAR
1999 action:@selector(showUserManager:)];
2000 [container addSubview:addPersonButton];
2001 yOffset = NSMaxY([addPersonButton frame]);
2003 separator = [self horizontalSeparatorWithFrame:
2004 NSMakeRect(0, yOffset, kFixedMenuWidth, 0)];
2005 [container addSubview:separator];
2006 yOffset = NSMaxY([separator frame]);
2008 // Adds the content text.
2009 base::string16 elidedName(gfx::ElideText(
2010 avatarItem.name, gfx::FontList(), availableWidth, gfx::ELIDE_TAIL));
2011 NSTextField* contentLabel = BuildLabel(
2012 l10n_util::GetNSStringF(IDS_PROFILES_NOT_YOU_CONTENT_TEXT, elidedName),
2013 NSMakePoint(kHorizontalSpacing, yOffset + kVerticalSpacing),
2015 [contentLabel setFrameSize:NSMakeSize(availableWidth, 0)];
2016 [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:contentLabel];
2017 [container addSubview:contentLabel];
2018 yOffset = NSMaxY([contentLabel frame]) + kVerticalSpacing;
2020 // Adds the title card.
2021 separator = [self horizontalSeparatorWithFrame:
2022 NSMakeRect(0, yOffset, kFixedSwitchUserViewWidth, 0)];
2023 [container addSubview:separator];
2024 yOffset = NSMaxY([separator frame]) + kSmallVerticalSpacing;
2026 NSView* titleView = BuildTitleCard(
2027 NSMakeRect(0, yOffset, kFixedSwitchUserViewWidth,0),
2028 l10n_util::GetStringFUTF16(IDS_PROFILES_NOT_YOU, avatarItem.name),
2029 self /* backButtonTarget*/,
2030 @selector(navigateBackFromSwitchUserView:) /* backButtonAction */);
2031 [container addSubview:titleView];
2032 yOffset = NSMaxY([titleView frame]);
2034 [container setFrameSize:NSMakeSize(kFixedAccountRemovalViewWidth, yOffset)];
2035 return container.autorelease();
2038 // Called when clicked on the settings link.
2039 - (BOOL)textView:(NSTextView*)textView
2040 clickedOnLink:(id)link
2041 atIndex:(NSUInteger)charIndex {
2042 chrome::ShowSettings(browser_);
2046 - (NSButton*)hoverButtonWithRect:(NSRect)rect
2047 text:(NSString*)text
2048 imageResourceId:(int)imageResourceId
2049 action:(SEL)action {
2050 base::scoped_nsobject<BackgroundColorHoverButton> button(
2051 [[BackgroundColorHoverButton alloc]
2053 imageTitleSpacing:kImageTitleSpacing
2054 backgroundColor:GetDialogBackgroundColor()]);
2056 [button setTitle:text];
2057 ui::ResourceBundle* rb = &ui::ResourceBundle::GetSharedInstance();
2058 NSImage* image = rb->GetNativeImageNamed(imageResourceId).ToNSImage();
2059 [button setDefaultImage:image];
2060 [button setHoverImage:image];
2061 [button setPressedImage:image];
2062 [button setImagePosition:NSImageLeft];
2063 [button setAlignment:NSLeftTextAlignment];
2064 [button setBordered:NO];
2065 [button setTarget:self];
2066 [button setAction:action];
2068 return button.autorelease();
2071 - (NSButton*)linkButtonWithTitle:(NSString*)title
2072 frameOrigin:(NSPoint)frameOrigin
2073 action:(SEL)action {
2074 base::scoped_nsobject<NSButton> link(
2075 [[HyperlinkButtonCell buttonWithString:title] retain]);
2077 [[link cell] setShouldUnderline:NO];
2078 [[link cell] setTextColor:gfx::SkColorToCalibratedNSColor(
2079 chrome_style::GetLinkColor())];
2080 [link setTitle:title];
2081 [link setBordered:NO];
2082 [link setFont:[NSFont labelFontOfSize:kTextFontSize]];
2083 [link setTarget:self];
2084 [link setAction:action];
2085 [link setFrameOrigin:frameOrigin];
2088 return link.autorelease();
2091 - (NSButton*)accountButtonWithRect:(NSRect)rect
2092 title:(const std::string&)title
2094 reauthRequired:(BOOL)reauthRequired {
2095 ui::ResourceBundle* rb = &ui::ResourceBundle::GetSharedInstance();
2096 NSImage* deleteImage = rb->GetNativeImageNamed(IDR_CLOSE_1).ToNSImage();
2097 CGFloat deleteImageWidth = [deleteImage size].width;
2098 NSImage* warningImage = reauthRequired ? rb->GetNativeImageNamed(
2099 IDR_ICON_PROFILES_ACCOUNT_BUTTON_ERROR).ToNSImage() : nil;
2100 CGFloat warningImageWidth = [warningImage size].width;
2102 CGFloat availableTextWidth = rect.size.width - kHorizontalSpacing -
2103 warningImageWidth - deleteImageWidth;
2105 availableTextWidth -= kHorizontalSpacing;
2107 NSColor* backgroundColor = gfx::SkColorToCalibratedNSColor(
2108 profiles::kAvatarBubbleAccountsBackgroundColor);
2109 base::scoped_nsobject<BackgroundColorHoverButton> button(
2110 [[BackgroundColorHoverButton alloc] initWithFrame:rect
2112 backgroundColor:backgroundColor]);
2113 [button setTitle:ElideEmail(title, availableTextWidth)];
2114 [button setAlignment:NSLeftTextAlignment];
2115 [button setBordered:NO];
2116 if (reauthRequired) {
2117 [button setDefaultImage:warningImage];
2118 [button setImagePosition:NSImageLeft];
2119 [button setTarget:self];
2120 [button setAction:@selector(showAccountReauthenticationView:)];
2121 [button setTag:tag];
2125 if (!browser_->profile()->IsSupervised()) {
2127 NSDivideRect(rect, &buttonRect, &rect,
2128 deleteImageWidth + kHorizontalSpacing, NSMaxXEdge);
2129 buttonRect.origin.y = 0;
2131 base::scoped_nsobject<HoverImageButton> deleteButton(
2132 [[HoverImageButton alloc] initWithFrame:buttonRect]);
2133 [deleteButton setBordered:NO];
2134 [deleteButton setDefaultImage:deleteImage];
2135 [deleteButton setHoverImage:rb->GetNativeImageNamed(
2136 IDR_CLOSE_1_H).ToNSImage()];
2137 [deleteButton setPressedImage:rb->GetNativeImageNamed(
2138 IDR_CLOSE_1_P).ToNSImage()];
2139 [deleteButton setTarget:self];
2140 [deleteButton setAction:@selector(showAccountRemovalView:)];
2141 [deleteButton setTag:tag];
2143 [button addSubview:deleteButton];
2146 return button.autorelease();
2149 - (void)postActionPerformed:(ProfileMetrics::ProfileDesktopMenu)action {
2150 ProfileMetrics::LogProfileDesktopMenu(action, serviceType_);
2151 serviceType_ = signin::GAIA_SERVICE_TYPE_NONE;
2154 - (bool)shouldShowGoIncognito {
2155 bool incognitoAvailable =
2156 IncognitoModePrefs::GetAvailability(browser_->profile()->GetPrefs()) !=
2157 IncognitoModePrefs::DISABLED;
2158 return incognitoAvailable && !browser_->profile()->IsGuestSession();