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