1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #import "chrome/browser/ui/cocoa/profiles/profile_chooser_controller.h"
7 #import <Carbon/Carbon.h> // kVK_Return.
8 #import <Cocoa/Cocoa.h>
10 #include "base/mac/bundle_locations.h"
11 #include "base/prefs/pref_service.h"
12 #include "base/strings/string_util.h"
13 #include "base/strings/sys_string_conversions.h"
14 #include "base/strings/utf_string_conversions.h"
15 #include "chrome/app/chrome_command_ids.h"
16 #include "chrome/browser/browser_process.h"
17 #include "chrome/browser/chrome_notification_types.h"
18 #include "chrome/browser/lifetime/application_lifetime.h"
19 #include "chrome/browser/prefs/incognito_mode_prefs.h"
20 #include "chrome/browser/profiles/avatar_menu.h"
21 #include "chrome/browser/profiles/avatar_menu_observer.h"
22 #include "chrome/browser/profiles/profile_avatar_icon_util.h"
23 #include "chrome/browser/profiles/profile_info_cache.h"
24 #include "chrome/browser/profiles/profile_manager.h"
25 #include "chrome/browser/profiles/profile_metrics.h"
26 #include "chrome/browser/profiles/profile_window.h"
27 #include "chrome/browser/profiles/profiles_state.h"
28 #include "chrome/browser/signin/chrome_signin_helper.h"
29 #include "chrome/browser/signin/profile_oauth2_token_service_factory.h"
30 #include "chrome/browser/signin/signin_error_controller_factory.h"
31 #include "chrome/browser/signin/signin_manager_factory.h"
32 #include "chrome/browser/signin/signin_promo.h"
33 #include "chrome/browser/signin/signin_ui_util.h"
34 #include "chrome/browser/ui/browser.h"
35 #include "chrome/browser/ui/browser_commands.h"
36 #include "chrome/browser/ui/browser_window.h"
37 #include "chrome/browser/ui/chrome_pages.h"
38 #include "chrome/browser/ui/chrome_style.h"
39 #import "chrome/browser/ui/cocoa/browser_window_utils.h"
40 #import "chrome/browser/ui/cocoa/info_bubble_view.h"
41 #import "chrome/browser/ui/cocoa/info_bubble_window.h"
42 #import "chrome/browser/ui/cocoa/profiles/user_manager_mac.h"
43 #include "chrome/browser/ui/singleton_tabs.h"
44 #include "chrome/browser/ui/user_manager.h"
45 #include "chrome/browser/ui/webui/signin/login_ui_service.h"
46 #include "chrome/browser/ui/webui/signin/login_ui_service_factory.h"
47 #include "chrome/common/pref_names.h"
48 #include "chrome/common/url_constants.h"
49 #include "chrome/grit/chromium_strings.h"
50 #include "chrome/grit/generated_resources.h"
51 #include "components/signin/core/browser/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 SigninErrorControllerFactory::GetForProfile(profile);
251 return error_controller && error_controller->HasError();
254 std::string GetAuthErrorAccountId(Profile* profile) {
255 const SigninErrorController* error_controller =
256 SigninErrorControllerFactory::GetForProfile(profile);
257 if (!error_controller)
258 return std::string();
260 return error_controller->error_account_id();
265 // Custom WebContentsDelegate that allows handling of hotkeys and suppresses
267 class GaiaWebContentsDelegate : public content::WebContentsDelegate {
269 GaiaWebContentsDelegate() {}
270 ~GaiaWebContentsDelegate() override {}
273 // content::WebContentsDelegate:
274 bool HandleContextMenu(const content::ContextMenuParams& params) override;
275 void HandleKeyboardEvent(
276 content::WebContents* source,
277 const content::NativeWebKeyboardEvent& event) override;
279 DISALLOW_COPY_AND_ASSIGN(GaiaWebContentsDelegate);
282 bool GaiaWebContentsDelegate::HandleContextMenu(
283 const content::ContextMenuParams& params) {
284 // Suppresses the context menu because some features, such as inspecting
285 // elements, are not appropriate in a bubble.
289 void GaiaWebContentsDelegate::HandleKeyboardEvent(
290 content::WebContents* source,
291 const content::NativeWebKeyboardEvent& event) {
292 if (![BrowserWindowUtils shouldHandleKeyboardEvent:event])
295 int chrome_command_id = [BrowserWindowUtils getCommandId:event];
297 bool is_text_editing_command =
298 (event.modifiers & blink::WebInputEvent::MetaKey) &&
299 (event.windowsKeyCode == ui::VKEY_A ||
300 event.windowsKeyCode == ui::VKEY_V);
302 // TODO(guohui): maybe should add an accelerator for the back button.
303 if (chrome_command_id == IDC_CLOSE_WINDOW || chrome_command_id == IDC_EXIT ||
304 is_text_editing_command) {
305 [[NSApp mainMenu] performKeyEquivalent:event.os_event];
309 // Class that listens to changes to the OAuth2Tokens for the active profile,
310 // changes to the avatar menu model or browser close notifications.
311 class ActiveProfileObserverBridge : public AvatarMenuObserver,
312 public content::NotificationObserver,
313 public OAuth2TokenService::Observer {
315 ActiveProfileObserverBridge(ProfileChooserController* controller,
317 : controller_(controller),
319 token_observer_registered_(false) {
320 registrar_.Add(this, chrome::NOTIFICATION_BROWSER_CLOSING,
321 content::NotificationService::AllSources());
322 if (!browser_->profile()->IsGuestSession())
323 AddTokenServiceObserver();
326 ~ActiveProfileObserverBridge() override { RemoveTokenServiceObserver(); }
329 void AddTokenServiceObserver() {
330 ProfileOAuth2TokenService* oauth2_token_service =
331 ProfileOAuth2TokenServiceFactory::GetForProfile(browser_->profile());
332 DCHECK(oauth2_token_service);
333 oauth2_token_service->AddObserver(this);
334 token_observer_registered_ = true;
337 void RemoveTokenServiceObserver() {
338 if (!token_observer_registered_)
340 DCHECK(browser_->profile());
341 ProfileOAuth2TokenService* oauth2_token_service =
342 ProfileOAuth2TokenServiceFactory::GetForProfile(browser_->profile());
343 DCHECK(oauth2_token_service);
344 oauth2_token_service->RemoveObserver(this);
345 token_observer_registered_ = false;
348 // OAuth2TokenService::Observer:
349 void OnRefreshTokenAvailable(const std::string& account_id) override {
350 // Tokens can only be added by adding an account through the inline flow,
351 // which is started from the account management view. Refresh it to show the
353 profiles::BubbleViewMode viewMode = [controller_ viewMode];
354 if (viewMode == profiles::BUBBLE_VIEW_MODE_ACCOUNT_MANAGEMENT ||
355 viewMode == profiles::BUBBLE_VIEW_MODE_GAIA_ADD_ACCOUNT ||
356 viewMode == profiles::BUBBLE_VIEW_MODE_GAIA_REAUTH) {
357 [controller_ initMenuContentsWithView:
358 switches::IsEnableAccountConsistency() ?
359 profiles::BUBBLE_VIEW_MODE_ACCOUNT_MANAGEMENT :
360 profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER];
364 void OnRefreshTokenRevoked(const std::string& account_id) override {
365 // Tokens can only be removed from the account management view. Refresh it
366 // to show the update.
367 if ([controller_ viewMode] == profiles::BUBBLE_VIEW_MODE_ACCOUNT_MANAGEMENT)
368 [controller_ initMenuContentsWithView:
369 profiles::BUBBLE_VIEW_MODE_ACCOUNT_MANAGEMENT];
372 // AvatarMenuObserver:
373 void OnAvatarMenuChanged(AvatarMenu* avatar_menu) override {
374 profiles::BubbleViewMode viewMode = [controller_ viewMode];
375 if (viewMode == profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER ||
376 viewMode == profiles::BUBBLE_VIEW_MODE_ACCOUNT_MANAGEMENT) {
377 [controller_ initMenuContentsWithView:viewMode];
381 // content::NotificationObserver:
382 void Observe(int type,
383 const content::NotificationSource& source,
384 const content::NotificationDetails& details) override {
385 DCHECK_EQ(chrome::NOTIFICATION_BROWSER_CLOSING, type);
386 if (browser_ == content::Source<Browser>(source).ptr()) {
387 RemoveTokenServiceObserver();
388 // Clean up the bubble's WebContents (used by the Gaia embedded view), to
389 // make sure the guest profile doesn't have any dangling host renderers.
390 // This can happen if Chrome is quit using Command-Q while the bubble is
391 // still open, which won't give the bubble a chance to be closed and
392 // clean up the WebContents itself.
393 [controller_ cleanUpEmbeddedViewContents];
397 ProfileChooserController* controller_; // Weak; owns this.
398 Browser* browser_; // Weak.
399 content::NotificationRegistrar registrar_;
401 // The observer can be removed both when closing the browser, and by just
402 // closing the avatar bubble. However, in the case of closing the browser,
403 // the avatar bubble will also be closed afterwards, resulting in a second
404 // attempt to remove the observer. This ensures the observer is only
406 bool token_observer_registered_;
408 DISALLOW_COPY_AND_ASSIGN(ActiveProfileObserverBridge);
411 // Custom button cell that adds a left padding before the button image, and
412 // a custom spacing between the button image and title.
413 @interface CustomPaddingImageButtonCell : NSButtonCell {
415 // Padding added to the left margin of the button.
416 int leftMarginSpacing_;
417 // Spacing between the cell image and title.
418 int imageTitleSpacing_;
421 - (id)initWithLeftMarginSpacing:(int)leftMarginSpacing
422 imageTitleSpacing:(int)imageTitleSpacing;
425 @implementation CustomPaddingImageButtonCell
426 - (id)initWithLeftMarginSpacing:(int)leftMarginSpacing
427 imageTitleSpacing:(int)imageTitleSpacing {
428 if ((self = [super init])) {
429 leftMarginSpacing_ = leftMarginSpacing;
430 imageTitleSpacing_ = imageTitleSpacing;
435 - (NSRect)drawTitle:(NSAttributedString*)title
436 withFrame:(NSRect)frame
437 inView:(NSView*)controlView {
439 NSDivideRect(frame, &marginRect, &frame, leftMarginSpacing_, NSMinXEdge);
441 // The title frame origin isn't aware of the left margin spacing added
442 // in -drawImage, so it must be added when drawing the title as well.
443 if ([self imagePosition] == NSImageLeft)
444 NSDivideRect(frame, &marginRect, &frame, imageTitleSpacing_, NSMinXEdge);
446 return [super drawTitle:title withFrame:frame inView:controlView];
449 - (void)drawImage:(NSImage*)image
450 withFrame:(NSRect)frame
451 inView:(NSView*)controlView {
452 if ([self imagePosition] == NSImageLeft)
453 frame.origin.x = leftMarginSpacing_;
454 [super drawImage:image withFrame:frame inView:controlView];
458 NSSize buttonSize = [super cellSize];
459 buttonSize.width += leftMarginSpacing_;
460 if ([self imagePosition] == NSImageLeft)
461 buttonSize.width += imageTitleSpacing_;
465 - (NSFocusRingType)focusRingType {
466 // This is taken care of by the custom drawing code.
467 return NSFocusRingTypeNone;
470 - (void)drawWithFrame:(NSRect)frame inView:(NSView *)controlView {
471 [super drawInteriorWithFrame:frame inView:controlView];
474 if ([self showsFirstResponder]) {
475 NSRect focusRingRect =
476 NSInsetRect(frame, kFocusRingLineWidth, kFocusRingLineWidth);
477 // TODO(noms): When we are targetting 10.7, we should change this to use
478 // -drawFocusRingMaskWithFrame instead.
479 [[[NSColor keyboardFocusIndicatorColor] colorWithAlphaComponent:1] set];
480 NSBezierPath* path = [NSBezierPath bezierPathWithRect:focusRingRect];
481 [path setLineWidth:kFocusRingLineWidth];
488 // A custom image view that has a transparent backround.
489 @interface TransparentBackgroundImageView : NSImageView
492 @implementation TransparentBackgroundImageView
493 - (void)drawRect:(NSRect)dirtyRect {
494 NSColor* backgroundColor = [NSColor colorWithCalibratedWhite:1 alpha:0.6f];
495 [backgroundColor setFill];
496 NSRectFillUsingOperation(dirtyRect, NSCompositeSourceAtop);
497 [super drawRect:dirtyRect];
501 @interface CustomCircleImageCell : NSButtonCell
504 @implementation CustomCircleImageCell
505 - (void)drawWithFrame:(NSRect)frame inView:(NSView *)controlView {
506 // Display everything as a circle that spans the entire control.
507 NSBezierPath* path = [NSBezierPath bezierPathWithOvalInRect:frame];
509 [super drawImage:[self image] withFrame:frame inView:controlView];
513 // A custom image control that shows a "Change" button when moused over.
514 @interface EditableProfilePhoto : HoverImageButton {
516 AvatarMenu* avatarMenu_; // Weak; Owned by ProfileChooserController.
517 base::scoped_nsobject<TransparentBackgroundImageView> changePhotoImage_;
518 ProfileChooserController* controller_;
521 - (id)initWithFrame:(NSRect)frameRect
522 avatarMenu:(AvatarMenu*)avatarMenu
523 profileIcon:(const gfx::Image&)profileIcon
524 editingAllowed:(BOOL)editingAllowed
525 withController:(ProfileChooserController*)controller;
527 // Called when the "Change" button is clicked.
528 - (void)editPhoto:(id)sender;
532 @implementation EditableProfilePhoto
533 - (id)initWithFrame:(NSRect)frameRect
534 avatarMenu:(AvatarMenu*)avatarMenu
535 profileIcon:(const gfx::Image&)profileIcon
536 editingAllowed:(BOOL)editingAllowed
537 withController:(ProfileChooserController*)controller {
538 if ((self = [super initWithFrame:frameRect])) {
539 avatarMenu_ = avatarMenu;
540 controller_ = controller;
542 [self setBordered:NO];
544 base::scoped_nsobject<CustomCircleImageCell> cell(
545 [[CustomCircleImageCell alloc] init]);
546 [self setCell:cell.get()];
548 [self setDefaultImage:CreateProfileImage(
549 profileIcon, kLargeImageSide).ToNSImage()];
550 [self setImagePosition:NSImageOnly];
552 NSRect bounds = NSMakeRect(0, 0, kLargeImageSide, kLargeImageSide);
553 if (editingAllowed) {
554 [self setTarget:self];
555 [self setAction:@selector(editPhoto:)];
556 changePhotoImage_.reset([[TransparentBackgroundImageView alloc]
557 initWithFrame:bounds]);
558 [changePhotoImage_ setImage:ui::ResourceBundle::GetSharedInstance().
559 GetNativeImageNamed(IDR_ICON_PROFILES_EDIT_CAMERA).AsNSImage()];
560 [self addSubview:changePhotoImage_];
562 // Hide the image until the button is hovered over.
563 [changePhotoImage_ setHidden:YES];
566 // Set the image cell's accessibility strings to be the same as the
568 [[self cell] accessibilitySetOverrideValue:l10n_util::GetNSString(
570 IDS_PROFILES_NEW_AVATAR_MENU_CHANGE_PHOTO_ACCESSIBLE_NAME :
571 IDS_PROFILES_NEW_AVATAR_MENU_PHOTO_ACCESSIBLE_NAME)
572 forAttribute:NSAccessibilityTitleAttribute];
573 [[self cell] accessibilitySetOverrideValue:
574 editingAllowed ? NSAccessibilityButtonRole : NSAccessibilityImageRole
575 forAttribute:NSAccessibilityRoleAttribute];
576 [[self cell] accessibilitySetOverrideValue:
577 NSAccessibilityRoleDescription(NSAccessibilityButtonRole, nil)
578 forAttribute:NSAccessibilityRoleDescriptionAttribute];
580 // The button and the cell should read the same thing.
581 [self accessibilitySetOverrideValue:l10n_util::GetNSString(
583 IDS_PROFILES_NEW_AVATAR_MENU_CHANGE_PHOTO_ACCESSIBLE_NAME :
584 IDS_PROFILES_NEW_AVATAR_MENU_PHOTO_ACCESSIBLE_NAME)
585 forAttribute:NSAccessibilityTitleAttribute];
586 [self accessibilitySetOverrideValue:NSAccessibilityButtonRole
587 forAttribute:NSAccessibilityRoleAttribute];
588 [self accessibilitySetOverrideValue:
589 NSAccessibilityRoleDescription(NSAccessibilityButtonRole, nil)
590 forAttribute:NSAccessibilityRoleDescriptionAttribute];
595 - (void)editPhoto:(id)sender {
596 avatarMenu_->EditProfile(avatarMenu_->GetActiveProfileIndex());
598 postActionPerformed:ProfileMetrics::PROFILE_DESKTOP_MENU_EDIT_IMAGE];
601 - (void)setHoverState:(HoverState)state {
602 [super setHoverState:state];
603 [changePhotoImage_ setHidden:([self hoverState] == kHoverStateNone)];
606 - (BOOL)canBecomeKeyView {
610 - (BOOL)accessibilityIsIgnored {
614 - (NSArray*)accessibilityActionNames {
615 NSArray* parentActions = [super accessibilityActionNames];
616 return [parentActions arrayByAddingObject:NSAccessibilityPressAction];
619 - (void)accessibilityPerformAction:(NSString*)action {
620 if ([action isEqualToString:NSAccessibilityPressAction]) {
621 avatarMenu_->EditProfile(avatarMenu_->GetActiveProfileIndex());
624 [super accessibilityPerformAction:action];
629 // A custom text control that turns into a textfield for editing when clicked.
630 @interface EditableProfileNameButton : HoverImageButton<NSTextFieldDelegate> {
632 base::scoped_nsobject<NSTextField> profileNameTextField_;
633 Profile* profile_; // Weak.
634 ProfileChooserController* controller_;
637 - (id)initWithFrame:(NSRect)frameRect
638 profile:(Profile*)profile
639 profileName:(NSString*)profileName
640 editingAllowed:(BOOL)editingAllowed
641 withController:(ProfileChooserController*)controller;
643 // Called when the button is clicked.
644 - (void)showEditableView:(id)sender;
646 // Called when enter is pressed in the text field.
647 - (void)saveProfileName;
651 @implementation EditableProfileNameButton
652 - (id)initWithFrame:(NSRect)frameRect
653 profile:(Profile*)profile
654 profileName:(NSString*)profileName
655 editingAllowed:(BOOL)editingAllowed
656 withController:(ProfileChooserController*)controller {
657 if ((self = [super initWithFrame:frameRect])) {
659 controller_ = controller;
661 CGFloat availableWidth = frameRect.size.width;
662 NSSize textSize = [profileName sizeWithAttributes:@{
663 NSFontAttributeName : [self font]
666 if (editingAllowed) {
667 // Show an "edit" pencil icon when hovering over. In the default state,
668 // we need to create an empty placeholder of the correct size, so that
669 // the text doesn't jump around when the hovered icon appears.
670 ui::ResourceBundle* rb = &ui::ResourceBundle::GetSharedInstance();
671 NSImage* hoverImage = rb->GetNativeImageNamed(
672 IDR_ICON_PROFILES_EDIT_HOVER).AsNSImage();
674 // In order to center the button title, we need to add a left padding of
675 // the same width as the pencil icon.
676 base::scoped_nsobject<CustomPaddingImageButtonCell> cell(
677 [[CustomPaddingImageButtonCell alloc]
678 initWithLeftMarginSpacing:[hoverImage size].width
679 imageTitleSpacing:0]);
680 [self setCell:cell.get()];
682 NSImage* placeholder = [[NSImage alloc] initWithSize:[hoverImage size]];
683 [self setDefaultImage:placeholder];
684 [self setHoverImage:hoverImage];
685 [self setAlternateImage:
686 rb->GetNativeImageNamed(IDR_ICON_PROFILES_EDIT_PRESSED).AsNSImage()];
687 [self setImagePosition:NSImageRight];
688 [self setTarget:self];
689 [self setAction:@selector(showEditableView:)];
691 // We need to subtract the width of the bezel from the frame rect, so that
692 // the textfield can take the exact same space as the button.
693 frameRect.size.height -= 2 * kBezelThickness;
694 frameRect.origin = NSMakePoint(0, kBezelThickness);
695 profileNameTextField_.reset(
696 [[NSTextField alloc] initWithFrame:frameRect]);
697 [profileNameTextField_ setStringValue:profileName];
698 [profileNameTextField_ setFont:[NSFont labelFontOfSize:kTitleFontSize]];
699 [profileNameTextField_ setEditable:YES];
700 [profileNameTextField_ setDrawsBackground:YES];
701 [profileNameTextField_ setBezeled:YES];
702 [profileNameTextField_ setAlignment:NSCenterTextAlignment];
703 [[profileNameTextField_ cell] setWraps:NO];
704 [[profileNameTextField_ cell] setLineBreakMode:
705 NSLineBreakByTruncatingTail];
706 [[profileNameTextField_ cell] setUsesSingleLineMode:YES];
707 [self addSubview:profileNameTextField_];
708 [profileNameTextField_ setDelegate:self];
710 // Hide the textfield until the user clicks on the button.
711 [profileNameTextField_ setHidden:YES];
713 [[self cell] accessibilitySetOverrideValue:l10n_util::GetNSStringF(
714 IDS_PROFILES_NEW_AVATAR_MENU_EDIT_NAME_ACCESSIBLE_NAME,
715 base::SysNSStringToUTF16(profileName))
716 forAttribute:NSAccessibilityTitleAttribute];
718 // Recompute the available width for the name since the icon takes space.
719 availableWidth -= [hoverImage size].width * 2;
720 // The profileNameTextField_ might size the text differently.
721 textSize = [profileName sizeWithAttributes:@{
722 NSFontAttributeName : [profileNameTextField_ font]
726 if (textSize.width > availableWidth)
727 [self setToolTip:profileName];
729 [[self cell] accessibilitySetOverrideValue:NSAccessibilityButtonRole
730 forAttribute:NSAccessibilityRoleAttribute];
732 accessibilitySetOverrideValue:NSAccessibilityRoleDescription(
733 NSAccessibilityButtonRole, nil)
734 forAttribute:NSAccessibilityRoleDescriptionAttribute];
736 [self setBordered:NO];
737 [self setFont:[NSFont labelFontOfSize:kTitleFontSize]];
738 [self setAlignment:NSCenterTextAlignment];
739 [[self cell] setLineBreakMode:NSLineBreakByTruncatingTail];
740 [self setTitle:profileName];
745 - (void)saveProfileName {
746 base::string16 newProfileName =
747 base::SysNSStringToUTF16([profileNameTextField_ stringValue]);
749 // Empty profile names are not allowed, and do nothing.
750 base::TrimWhitespace(newProfileName, base::TRIM_ALL, &newProfileName);
751 if (!newProfileName.empty()) {
752 profiles::UpdateProfileName(profile_, newProfileName);
754 postActionPerformed:ProfileMetrics::PROFILE_DESKTOP_MENU_EDIT_NAME];
755 [profileNameTextField_ setHidden:YES];
759 - (void)showEditableView:(id)sender {
760 [profileNameTextField_ setHidden:NO];
761 [[self window] makeFirstResponder:profileNameTextField_];
764 - (BOOL)canBecomeKeyView {
768 - (BOOL)control:(NSControl*)control
769 textView:(NSTextView*)textView
770 doCommandBySelector:(SEL)commandSelector {
771 if (commandSelector == @selector(insertTab:) ||
772 commandSelector == @selector(insertNewline:)) {
773 [self saveProfileName];
781 // A custom button that allows for setting a background color when hovered over.
782 @interface BackgroundColorHoverButton : HoverImageButton {
784 base::scoped_nsobject<NSColor> backgroundColor_;
785 base::scoped_nsobject<NSColor> hoverColor_;
789 @implementation BackgroundColorHoverButton
791 - (id)initWithFrame:(NSRect)frameRect
792 imageTitleSpacing:(int)imageTitleSpacing
793 backgroundColor:(NSColor*)backgroundColor {
794 if ((self = [super initWithFrame:frameRect])) {
795 backgroundColor_.reset([backgroundColor retain]);
796 // Use a color from the common theme, since this button is not trying to
797 // look like a native control.
799 bool found = ui::CommonThemeGetSystemColor(
800 ui::NativeTheme::kColorId_ButtonHoverBackgroundColor, &hoverColor);
802 hoverColor_.reset([gfx::SkColorToSRGBNSColor(hoverColor) retain]);
804 [self setBordered:NO];
805 [self setFont:[NSFont labelFontOfSize:kTextFontSize]];
806 [self setButtonType:NSMomentaryChangeButton];
808 base::scoped_nsobject<CustomPaddingImageButtonCell> cell(
809 [[CustomPaddingImageButtonCell alloc]
810 initWithLeftMarginSpacing:kHorizontalSpacing
811 imageTitleSpacing:imageTitleSpacing]);
812 [cell setLineBreakMode:NSLineBreakByTruncatingTail];
813 [self setCell:cell.get()];
818 - (void)setHoverState:(HoverState)state {
819 [super setHoverState:state];
820 bool isHighlighted = ([self hoverState] != kHoverStateNone);
822 NSColor* backgroundColor = isHighlighted ? hoverColor_ : backgroundColor_;
823 [[self cell] setBackgroundColor:backgroundColor];
826 -(void)keyDown:(NSEvent*)event {
827 // Since there is no default button in the bubble, it is safe to activate
828 // all buttons on Enter as well, and be consistent with the Windows
830 if ([event keyCode] == kVK_Return)
831 [self performClick:self];
833 [super keyDown:event];
836 - (BOOL)canBecomeKeyView {
842 // A custom view with the given background color.
843 @interface BackgroundColorView : NSView {
845 base::scoped_nsobject<NSColor> backgroundColor_;
849 @implementation BackgroundColorView
850 - (id)initWithFrame:(NSRect)frameRect
851 withColor:(NSColor*)color {
852 if ((self = [super initWithFrame:frameRect]))
853 backgroundColor_.reset([color retain]);
857 - (void)drawRect:(NSRect)dirtyRect {
858 [backgroundColor_ setFill];
859 NSRectFill(dirtyRect);
860 [super drawRect:dirtyRect];
864 // A custom dummy button that is used to clear focus from the bubble's controls.
865 @interface DummyWindowFocusButton : NSButton
868 @implementation DummyWindowFocusButton
869 // Ignore accessibility, as this is a placeholder button.
870 - (BOOL)accessibilityIsIgnored {
874 - (id)accessibilityAttributeValue:(NSString*)attribute {
878 - (BOOL)canBecomeKeyView {
884 @interface ProfileChooserController ()
885 // Adds an horizontal separator to |container| at |yOffset| and returns the
886 // yOffset corresponding to after the separator.
887 - (CGFloat)addSeparatorToContainer:(NSView*)container
888 atYOffset:(CGFloat)yOffset;
890 // Builds the right-click profile switcher.
891 - (void)buildFastUserSwitcherViewWithProfiles:(NSMutableArray*)otherProfiles
892 atYOffset:(CGFloat)yOffset
893 inContainer:(NSView*)container;
895 // Builds the regular profile chooser view.
896 - (void)buildProfileChooserViewWithProfileView:(NSView*)currentProfileView
897 tutorialView:(NSView*)tutorialView
898 atYOffset:(CGFloat)yOffset
899 inContainer:(NSView*)container
900 displayLock:(bool)displayLock;
902 // Builds the profile chooser view.
903 - (NSView*)buildProfileChooserView;
905 - (NSView*)buildTutorialViewIfNeededForItem:(const AvatarMenu::Item&)item;
907 // Builds a tutorial card with a title label using |titleMessage|, a content
908 // label using |contentMessage|, a link using |linkMessage|, and a button using
909 // |buttonMessage|. If |stackButton| is YES, places the button above the link.
910 // Otherwise places both on the same row with the link left aligned and button
911 // right aligned. On click, the link would execute |linkAction|, and the button
912 // would execute |buttonAction|. It sets |tutorialMode_| to the given |mode|.
913 - (NSView*)tutorialViewWithMode:(profiles::TutorialMode)mode
914 titleMessage:(NSString*)titleMessage
915 contentMessage:(NSString*)contentMessage
916 linkMessage:(NSString*)linkMessage
917 buttonMessage:(NSString*)buttonMessage
918 stackButton:(BOOL)stackButton
919 hasCloseButton:(BOOL)hasCloseButton
920 linkAction:(SEL)linkAction
921 buttonAction:(SEL)buttonAction;
923 // Builds a tutorial card to introduce an upgrade user to the new avatar menu if
924 // needed. |tutorial_shown| indicates if the tutorial has already been shown in
925 // the previous active view. |avatar_item| refers to the current profile.
926 - (NSView*)buildWelcomeUpgradeTutorialView:(const AvatarMenu::Item&)item;
928 // Builds a tutorial card to inform the user about right-click user switching if
930 - (NSView*)buildRightClickTutorialView;
932 // Builds a tutorial card to have the user confirm the last Chrome signin,
933 // Chrome sync will be delayed until the user either dismisses the tutorial, or
934 // configures sync through the "Settings" link.
935 - (NSView*)buildSigninConfirmationView;
937 // Builds a tutorial card to show the last signin error.
938 - (NSView*)buildSigninErrorView;
940 // Creates the main profile card for the profile |item| at the top of
942 - (NSView*)createCurrentProfileView:(const AvatarMenu::Item&)item;
944 // Creates the possible links for the main profile card with profile |item|.
945 - (NSView*)createCurrentProfileLinksForItem:(const AvatarMenu::Item&)item
948 // Creates the disclaimer text for supervised users, telling them that the
949 // manager can view their history etc.
950 - (NSView*)createSupervisedUserDisclaimerView;
952 // Creates a main profile card for the guest user.
953 - (NSView*)createGuestProfileView;
955 // Creates an item for the profile |itemIndex| that is used in the fast profile
956 // switcher in the middle of the bubble.
957 - (NSButton*)createOtherProfileView:(int)itemIndex;
959 // Creates the "Not you" and Lock option buttons.
960 - (NSView*)createOptionsViewWithRect:(NSRect)rect
961 displayLock:(BOOL)displayLock;
963 // Creates the account management view for the active profile.
964 - (NSView*)createCurrentProfileAccountsView:(NSRect)rect;
966 // Creates the list of accounts for the active profile.
967 - (NSView*)createAccountsListWithRect:(NSRect)rect;
969 // Creates the Gaia sign-in/add account view.
970 - (NSView*)buildGaiaEmbeddedView;
972 // Creates the account removal view.
973 - (NSView*)buildAccountRemovalView;
975 // Create a view that shows various options for an upgrade user who is not
976 // the same person as the currently signed in user.
977 - (NSView*)buildSwitchUserView;
979 // Creates a button with |text|, an icon given by |imageResourceId| and with
981 - (NSButton*)hoverButtonWithRect:(NSRect)rect
983 imageResourceId:(int)imageResourceId
986 // Creates a generic link button with |title| and an |action| positioned at
988 - (NSButton*)linkButtonWithTitle:(NSString*)title
989 frameOrigin:(NSPoint)frameOrigin
992 // Creates an email account button with |title| and a remove icon. If
993 // |reauthRequired| is true, the button also displays a warning icon. |tag|
994 // indicates which account the button refers to.
995 - (NSButton*)accountButtonWithRect:(NSRect)rect
996 accountId:(const std::string&)accountId
998 reauthRequired:(BOOL)reauthRequired;
1000 - (bool)shouldShowGoIncognito;
1003 @implementation ProfileChooserController
1004 - (profiles::BubbleViewMode) viewMode {
1008 - (void)setTutorialMode:(profiles::TutorialMode)tutorialMode {
1009 tutorialMode_ = tutorialMode;
1012 - (IBAction)switchToProfile:(id)sender {
1013 // Check the event flags to see if a new window should be created.
1014 bool alwaysCreate = ui::WindowOpenDispositionFromNSEvent(
1015 [NSApp currentEvent]) == NEW_WINDOW;
1016 avatarMenu_->SwitchToProfile([sender tag], alwaysCreate,
1017 ProfileMetrics::SWITCH_PROFILE_ICON);
1020 - (IBAction)showUserManager:(id)sender {
1021 UserManager::Show(base::FilePath(),
1022 profiles::USER_MANAGER_NO_TUTORIAL,
1023 profiles::USER_MANAGER_SELECT_PROFILE_NO_ACTION);
1024 [self postActionPerformed:
1025 ProfileMetrics::PROFILE_DESKTOP_MENU_OPEN_USER_MANAGER];
1028 - (IBAction)exitGuest:(id)sender {
1029 DCHECK(browser_->profile()->IsGuestSession());
1030 UserManager::Show(base::FilePath(),
1031 profiles::USER_MANAGER_NO_TUTORIAL,
1032 profiles::USER_MANAGER_SELECT_PROFILE_NO_ACTION);
1033 profiles::CloseGuestProfileWindows();
1036 - (IBAction)goIncognito:(id)sender {
1037 DCHECK([self shouldShowGoIncognito]);
1038 chrome::NewIncognitoWindow(browser_);
1039 [self postActionPerformed:
1040 ProfileMetrics::PROFILE_DESKTOP_MENU_GO_INCOGNITO];
1043 - (IBAction)showAccountManagement:(id)sender {
1044 [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_ACCOUNT_MANAGEMENT];
1047 - (IBAction)hideAccountManagement:(id)sender {
1048 [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER];
1051 - (IBAction)lockProfile:(id)sender {
1052 profiles::LockProfile(browser_->profile());
1053 [self postActionPerformed:ProfileMetrics::PROFILE_DESKTOP_MENU_LOCK];
1056 - (IBAction)showInlineSigninPage:(id)sender {
1057 [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_GAIA_SIGNIN];
1060 - (IBAction)addAccount:(id)sender {
1061 [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_GAIA_ADD_ACCOUNT];
1062 [self postActionPerformed:ProfileMetrics::PROFILE_DESKTOP_MENU_ADD_ACCT];
1065 - (IBAction)navigateBackFromSigninPage:(id)sender {
1066 std::string primaryAccount = SigninManagerFactory::GetForProfile(
1067 browser_->profile())->GetAuthenticatedAccountId();
1068 bool hasAccountManagement = !primaryAccount.empty() &&
1069 switches::IsEnableAccountConsistency();
1070 [self initMenuContentsWithView:hasAccountManagement ?
1071 profiles::BUBBLE_VIEW_MODE_ACCOUNT_MANAGEMENT :
1072 profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER];
1075 - (IBAction)showAccountRemovalView:(id)sender {
1076 DCHECK(!isGuestSession_);
1078 // Tag is either |kPrimaryProfileTag| for the primary account, or equal to the
1079 // index in |currentProfileAccounts_| for a secondary account.
1080 int tag = [sender tag];
1081 if (tag == kPrimaryProfileTag) {
1082 accountIdToRemove_ = SigninManagerFactory::GetForProfile(
1083 browser_->profile())->GetAuthenticatedAccountId();
1085 DCHECK(ContainsKey(currentProfileAccounts_, tag));
1086 accountIdToRemove_ = currentProfileAccounts_[tag];
1089 [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_ACCOUNT_REMOVAL];
1092 - (IBAction)showAccountReauthenticationView:(id)sender {
1093 DCHECK(!isGuestSession_);
1094 [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_GAIA_REAUTH];
1097 - (IBAction)removeAccount:(id)sender {
1098 DCHECK(!accountIdToRemove_.empty());
1099 ProfileOAuth2TokenServiceFactory::GetForProfile(browser_->profile())
1100 ->RevokeCredentials(accountIdToRemove_);
1101 [self postActionPerformed:ProfileMetrics::PROFILE_DESKTOP_MENU_REMOVE_ACCT];
1102 accountIdToRemove_.clear();
1104 [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_ACCOUNT_MANAGEMENT];
1107 - (IBAction)seeWhatsNew:(id)sender {
1108 UserManager::Show(base::FilePath(),
1109 profiles::USER_MANAGER_TUTORIAL_OVERVIEW,
1110 profiles::USER_MANAGER_SELECT_PROFILE_NO_ACTION);
1111 ProfileMetrics::LogProfileNewAvatarMenuUpgrade(
1112 ProfileMetrics::PROFILE_AVATAR_MENU_UPGRADE_WHATS_NEW);
1115 - (IBAction)showSwitchUserView:(id)sender {
1116 [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_SWITCH_USER];
1117 ProfileMetrics::LogProfileNewAvatarMenuUpgrade(
1118 ProfileMetrics::PROFILE_AVATAR_MENU_UPGRADE_NOT_YOU);
1121 - (IBAction)showLearnMorePage:(id)sender {
1122 signin_ui_util::ShowSigninErrorLearnMorePage(browser_->profile());
1125 - (IBAction)configureSyncSettings:(id)sender {
1126 tutorialMode_ = profiles::TUTORIAL_MODE_NONE;
1127 LoginUIServiceFactory::GetForProfile(browser_->profile())->
1128 SyncConfirmationUIClosed(true);
1129 ProfileMetrics::LogProfileNewAvatarMenuSignin(
1130 ProfileMetrics::PROFILE_AVATAR_MENU_SIGNIN_SETTINGS);
1133 - (IBAction)syncSettingsConfirmed:(id)sender {
1134 tutorialMode_ = profiles::TUTORIAL_MODE_NONE;
1135 LoginUIServiceFactory::GetForProfile(browser_->profile())->
1136 SyncConfirmationUIClosed(false);
1137 ProfileMetrics::LogProfileNewAvatarMenuSignin(
1138 ProfileMetrics::PROFILE_AVATAR_MENU_SIGNIN_OK);
1139 [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER];
1142 - (IBAction)disconnectProfile:(id)sender {
1143 chrome::ShowSettings(browser_);
1144 ProfileMetrics::LogProfileNewAvatarMenuNotYou(
1145 ProfileMetrics::PROFILE_AVATAR_MENU_NOT_YOU_DISCONNECT);
1148 - (IBAction)navigateBackFromSwitchUserView:(id)sender {
1149 [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER];
1150 ProfileMetrics::LogProfileNewAvatarMenuNotYou(
1151 ProfileMetrics::PROFILE_AVATAR_MENU_NOT_YOU_BACK);
1154 - (IBAction)dismissTutorial:(id)sender {
1155 // Never shows the upgrade tutorial again if manually closed.
1156 if (tutorialMode_ == profiles::TUTORIAL_MODE_WELCOME_UPGRADE) {
1157 browser_->profile()->GetPrefs()->SetInteger(
1158 prefs::kProfileAvatarTutorialShown,
1159 signin_ui_util::kUpgradeWelcomeTutorialShowMax + 1);
1162 if(tutorialMode_ == profiles::TUTORIAL_MODE_RIGHT_CLICK_SWITCHING) {
1163 PrefService* localState = g_browser_process->local_state();
1164 localState->SetBoolean(
1165 prefs::kProfileAvatarRightClickTutorialDismissed, true);
1168 tutorialMode_ = profiles::TUTORIAL_MODE_NONE;
1169 [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER];
1172 - (void)windowWillClose:(NSNotification*)notification {
1173 if (tutorialMode_ == profiles::TUTORIAL_MODE_CONFIRM_SIGNIN) {
1174 LoginUIServiceFactory::GetForProfile(browser_->profile())->
1175 SyncConfirmationUIClosed(false);
1178 [super windowWillClose:notification];
1181 - (void)moveDown:(id)sender {
1182 [[self window] selectNextKeyView:self];
1185 - (void)moveUp:(id)sender {
1186 [[self window] selectPreviousKeyView:self];
1189 - (void)cleanUpEmbeddedViewContents {
1190 webContents_.reset();
1191 webContentsDelegate_.reset();
1194 - (id)initWithBrowser:(Browser*)browser
1195 anchoredAt:(NSPoint)point
1196 viewMode:(profiles::BubbleViewMode)viewMode
1197 tutorialMode:(profiles::TutorialMode)tutorialMode
1198 serviceType:(signin::GAIAServiceType)serviceType {
1199 base::scoped_nsobject<InfoBubbleWindow> window([[InfoBubbleWindow alloc]
1200 initWithContentRect:ui::kWindowSizeDeterminedLater
1201 styleMask:NSBorderlessWindowMask
1202 backing:NSBackingStoreBuffered
1205 if ((self = [super initWithWindow:window
1206 parentWindow:browser->window()->GetNativeWindow()
1207 anchoredAt:point])) {
1209 viewMode_ = viewMode;
1210 tutorialMode_ = tutorialMode;
1211 observer_.reset(new ActiveProfileObserverBridge(self, browser_));
1212 serviceType_ = serviceType;
1214 avatarMenu_.reset(new AvatarMenu(
1215 &g_browser_process->profile_manager()->GetProfileInfoCache(),
1218 avatarMenu_->RebuildMenu();
1220 // Guest profiles do not have a token service.
1221 isGuestSession_ = browser_->profile()->IsGuestSession();
1223 // If view mode is PROFILE_CHOOSER but there is an auth error, force
1224 // ACCOUNT_MANAGEMENT mode.
1225 if (viewMode_ == profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER &&
1226 HasAuthError(browser_->profile()) &&
1227 switches::IsEnableAccountConsistency() &&
1228 avatarMenu_->GetItemAt(avatarMenu_->GetActiveProfileIndex()).
1230 viewMode_ = profiles::BUBBLE_VIEW_MODE_ACCOUNT_MANAGEMENT;
1233 [window accessibilitySetOverrideValue:
1234 l10n_util::GetNSString(IDS_PROFILES_NEW_AVATAR_MENU_ACCESSIBLE_NAME)
1235 forAttribute:NSAccessibilityTitleAttribute];
1236 [window accessibilitySetOverrideValue:
1237 l10n_util::GetNSString(IDS_PROFILES_NEW_AVATAR_MENU_ACCESSIBLE_NAME)
1238 forAttribute:NSAccessibilityHelpAttribute];
1240 [[self bubble] setAlignment:info_bubble::kAlignRightEdgeToAnchorEdge];
1241 [[self bubble] setArrowLocation:info_bubble::kNoArrow];
1242 [[self bubble] setBackgroundColor:GetDialogBackgroundColor()];
1243 [self initMenuContentsWithView:viewMode_];
1249 - (void)initMenuContentsWithView:(profiles::BubbleViewMode)viewToDisplay {
1250 if (browser_->profile()->IsSupervised() &&
1251 (viewToDisplay == profiles::BUBBLE_VIEW_MODE_GAIA_ADD_ACCOUNT ||
1252 viewToDisplay == profiles::BUBBLE_VIEW_MODE_ACCOUNT_REMOVAL)) {
1253 LOG(WARNING) << "Supervised user attempted to add/remove account";
1256 viewMode_ = viewToDisplay;
1257 NSView* contentView = [[self window] contentView];
1258 [contentView setSubviews:[NSArray array]];
1261 switch (viewMode_) {
1262 case profiles::BUBBLE_VIEW_MODE_GAIA_SIGNIN:
1263 case profiles::BUBBLE_VIEW_MODE_GAIA_ADD_ACCOUNT:
1264 case profiles::BUBBLE_VIEW_MODE_GAIA_REAUTH:
1265 subView = [self buildGaiaEmbeddedView];
1267 case profiles::BUBBLE_VIEW_MODE_ACCOUNT_REMOVAL:
1268 subView = [self buildAccountRemovalView];
1270 case profiles::BUBBLE_VIEW_MODE_SWITCH_USER:
1271 subView = [self buildSwitchUserView];
1273 case profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER:
1274 case profiles::BUBBLE_VIEW_MODE_FAST_PROFILE_CHOOSER:
1275 case profiles::BUBBLE_VIEW_MODE_ACCOUNT_MANAGEMENT:
1276 subView = [self buildProfileChooserView];
1280 // Clears tutorial mode for all non-profile-chooser views.
1281 if (viewMode_ != profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER)
1282 tutorialMode_ = profiles::TUTORIAL_MODE_NONE;
1284 // Add a dummy, empty element so that we don't initially display any
1286 NSButton* dummyFocusButton =
1287 [[[DummyWindowFocusButton alloc] initWithFrame:NSZeroRect] autorelease];
1288 [dummyFocusButton setNextKeyView:subView];
1289 [[self window] makeFirstResponder:dummyFocusButton];
1291 [contentView addSubview:subView];
1292 [contentView addSubview:dummyFocusButton];
1293 SetWindowSize([self window],
1294 NSMakeSize(NSWidth([subView frame]), NSHeight([subView frame])));
1297 - (CGFloat)addSeparatorToContainer:(NSView*)container
1298 atYOffset:(CGFloat)yOffset {
1299 NSBox* separator = [self horizontalSeparatorWithFrame:NSMakeRect(
1300 0, yOffset, kFixedMenuWidth, 0)];
1301 [container addSubview:separator];
1302 return NSMaxY([separator frame]);
1305 // Builds the fast user switcher view in |container| at |yOffset| and populates
1306 // it with the entries for every profile in |otherProfiles|. Returns the new
1307 // yOffset after adding the elements.
1308 - (void)buildFastUserSwitcherViewWithProfiles:(NSMutableArray*)otherProfiles
1309 atYOffset:(CGFloat)yOffset
1310 inContainer:(NSView*)container {
1311 // Other profiles switcher. The profiles have already been sorted
1312 // by their y-coordinate, so they can be added in the existing order.
1313 for (NSView* otherProfileView in otherProfiles) {
1314 [otherProfileView setFrameOrigin:NSMakePoint(0, yOffset)];
1315 [container addSubview:otherProfileView];
1316 yOffset = NSMaxY([otherProfileView frame]);
1318 yOffset = [self addSeparatorToContainer:container atYOffset: yOffset];
1321 [container setFrameSize:NSMakeSize(kFixedMenuWidth, yOffset)];
1324 - (void)buildProfileChooserViewWithProfileView:(NSView*)currentProfileView
1325 tutorialView:(NSView*)tutorialView
1326 atYOffset:(CGFloat)yOffset
1327 inContainer:(NSView*)container
1328 displayLock:(bool)displayLock {
1330 NSRect rect = NSMakeRect(0, yOffset, kFixedMenuWidth, 0);
1331 NSView* optionsView = [self createOptionsViewWithRect:rect
1332 displayLock:displayLock];
1333 [container addSubview:optionsView];
1334 rect.origin.y = NSMaxY([optionsView frame]);
1336 NSBox* separator = [self horizontalSeparatorWithFrame:rect];
1337 [container addSubview:separator];
1338 yOffset = NSMaxY([separator frame]);
1340 // For supervised users, add the disclaimer text.
1341 if (browser_->profile()->IsSupervised()) {
1342 yOffset += kSmallVerticalSpacing;
1343 NSView* disclaimerContainer = [self createSupervisedUserDisclaimerView];
1344 [disclaimerContainer setFrameOrigin:NSMakePoint(0, yOffset)];
1345 [container addSubview:disclaimerContainer];
1346 yOffset = NSMaxY([disclaimerContainer frame]);
1347 yOffset += kSmallVerticalSpacing;
1349 yOffset = [self addSeparatorToContainer:container atYOffset: yOffset];
1352 if (viewMode_ == profiles::BUBBLE_VIEW_MODE_ACCOUNT_MANAGEMENT) {
1353 NSView* currentProfileAccountsView = [self createCurrentProfileAccountsView:
1354 NSMakeRect(0, yOffset, kFixedMenuWidth, 0)];
1355 [container addSubview:currentProfileAccountsView];
1356 yOffset = NSMaxY([currentProfileAccountsView frame]);
1358 yOffset = [self addSeparatorToContainer:container atYOffset: yOffset];
1361 // Active profile card.
1362 if (currentProfileView) {
1363 yOffset += kVerticalSpacing;
1364 [currentProfileView setFrameOrigin:NSMakePoint(0, yOffset)];
1365 [container addSubview:currentProfileView];
1366 yOffset = NSMaxY([currentProfileView frame]) + kVerticalSpacing;
1370 [tutorialView setFrameOrigin:NSMakePoint(0, yOffset)];
1371 [container addSubview:tutorialView];
1372 yOffset = NSMaxY([tutorialView frame]);
1373 //TODO(mlerman): update UMA stats for the new tutorials.
1375 tutorialMode_ = profiles::TUTORIAL_MODE_NONE;
1378 [container setFrameSize:NSMakeSize(kFixedMenuWidth, yOffset)];
1381 - (NSView*)buildProfileChooserView {
1382 base::scoped_nsobject<NSView> container(
1383 [[NSView alloc] initWithFrame:NSZeroRect]);
1385 NSView* tutorialView = nil;
1386 NSView* currentProfileView = nil;
1387 base::scoped_nsobject<NSMutableArray> otherProfiles(
1388 [[NSMutableArray alloc] init]);
1389 // Local and guest profiles cannot lock their profile.
1390 bool displayLock = false;
1391 bool isFastProfileChooser =
1392 viewMode_ == profiles::BUBBLE_VIEW_MODE_FAST_PROFILE_CHOOSER;
1393 if (isFastProfileChooser) {
1394 // The user is using right-click switching, no need to tell them about it.
1395 PrefService* localState = g_browser_process->local_state();
1396 localState->SetBoolean(
1397 prefs::kProfileAvatarRightClickTutorialDismissed, true);
1400 // Loop over the profiles in reverse, so that they are sorted by their
1401 // y-coordinate, and separate them into active and "other" profiles.
1402 for (int i = avatarMenu_->GetNumberOfItems() - 1; i >= 0; --i) {
1403 const AvatarMenu::Item& item = avatarMenu_->GetItemAt(i);
1405 if (viewMode_ == profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER) {
1406 tutorialView = [self buildTutorialViewIfNeededForItem:item];
1408 currentProfileView = [self createCurrentProfileView:item];
1409 displayLock = item.signed_in &&
1410 profiles::IsLockAvailable(browser_->profile());
1412 [otherProfiles addObject:[self createOtherProfileView:i]];
1415 if (!currentProfileView) // Guest windows don't have an active profile.
1416 currentProfileView = [self createGuestProfileView];
1418 // |yOffset| is the next position at which to draw in |container|
1419 // coordinates. Add a pixel offset so that the bottom option buttons don't
1420 // overlap the bubble's rounded corners.
1421 CGFloat yOffset = 1;
1423 if (isFastProfileChooser) {
1424 [self buildFastUserSwitcherViewWithProfiles:otherProfiles.get()
1426 inContainer:container.get()];
1428 [self buildProfileChooserViewWithProfileView:currentProfileView
1429 tutorialView:tutorialView
1431 inContainer:container.get()
1432 displayLock:displayLock];
1435 return container.autorelease();
1438 - (NSView*)buildSigninConfirmationView {
1439 ProfileMetrics::LogProfileNewAvatarMenuSignin(
1440 ProfileMetrics::PROFILE_AVATAR_MENU_SIGNIN_VIEW);
1442 NSString* titleMessage = l10n_util::GetNSString(
1443 IDS_PROFILES_CONFIRM_SIGNIN_TUTORIAL_TITLE);
1444 NSString* contentMessage = l10n_util::GetNSString(
1445 IDS_PROFILES_CONFIRM_SIGNIN_TUTORIAL_CONTENT_TEXT);
1446 NSString* linkMessage = l10n_util::GetNSString(
1447 IDS_PROFILES_SYNC_SETTINGS_LINK);
1448 NSString* buttonMessage = l10n_util::GetNSString(
1449 IDS_PROFILES_TUTORIAL_OK_BUTTON);
1450 return [self tutorialViewWithMode:profiles::TUTORIAL_MODE_CONFIRM_SIGNIN
1451 titleMessage:titleMessage
1452 contentMessage:contentMessage
1453 linkMessage:linkMessage
1454 buttonMessage:buttonMessage
1457 linkAction:@selector(configureSyncSettings:)
1458 buttonAction:@selector(syncSettingsConfirmed:)];
1461 - (NSView*)buildSigninErrorView {
1462 NSString* titleMessage = l10n_util::GetNSString(
1463 IDS_PROFILES_ERROR_TUTORIAL_TITLE);
1464 LoginUIService* loginUiService =
1465 LoginUIServiceFactory::GetForProfile(browser_->profile());
1466 NSString* contentMessage =
1467 base::SysUTF16ToNSString(loginUiService->GetLastLoginResult());
1468 NSString* linkMessage = l10n_util::GetNSString(
1469 IDS_PROFILES_PROFILE_TUTORIAL_LEARN_MORE);
1470 return [self tutorialViewWithMode:profiles::TUTORIAL_MODE_CONFIRM_SIGNIN
1471 titleMessage:titleMessage
1472 contentMessage:contentMessage
1473 linkMessage:linkMessage
1477 linkAction:@selector(showLearnMorePage:)
1481 - (NSView*)buildWelcomeUpgradeTutorialView:(const AvatarMenu::Item&)item {
1482 ProfileMetrics::LogProfileNewAvatarMenuUpgrade(
1483 ProfileMetrics::PROFILE_AVATAR_MENU_UPGRADE_VIEW);
1485 NSString* titleMessage = l10n_util::GetNSString(
1486 IDS_PROFILES_WELCOME_UPGRADE_TUTORIAL_TITLE);
1487 NSString* contentMessage = l10n_util::GetNSString(
1488 IDS_PROFILES_WELCOME_UPGRADE_TUTORIAL_CONTENT_TEXT);
1489 // For local profiles, the "Not you" link doesn't make sense.
1490 NSString* linkMessage = item.signed_in ?
1492 l10n_util::GetStringFUTF16(IDS_PROFILES_NOT_YOU, item.name),
1493 kFixedMenuWidth - 2 * kHorizontalSpacing) :
1495 NSString* buttonMessage = l10n_util::GetNSString(
1496 IDS_PROFILES_TUTORIAL_WHATS_NEW_BUTTON);
1497 return [self tutorialViewWithMode:profiles::TUTORIAL_MODE_WELCOME_UPGRADE
1498 titleMessage:titleMessage
1499 contentMessage:contentMessage
1500 linkMessage:linkMessage
1501 buttonMessage:buttonMessage
1504 linkAction:@selector(showSwitchUserView:)
1505 buttonAction:@selector(seeWhatsNew:)];
1508 - (NSView*)buildRightClickTutorialView {
1509 NSString* titleMessage = l10n_util::GetNSString(
1510 IDS_PROFILES_RIGHT_CLICK_TUTORIAL_TITLE);
1511 NSString* contentMessage = l10n_util::GetNSString(
1512 IDS_PROFILES_RIGHT_CLICK_TUTORIAL_CONTENT_TEXT);
1513 NSString* buttonMessage = l10n_util::GetNSString(
1514 IDS_PROFILES_TUTORIAL_OK_BUTTON);
1517 [self tutorialViewWithMode:profiles::TUTORIAL_MODE_RIGHT_CLICK_SWITCHING
1518 titleMessage:titleMessage
1519 contentMessage:contentMessage
1521 buttonMessage:buttonMessage
1525 buttonAction:@selector(dismissTutorial:)];
1528 - (NSView*)buildTutorialViewIfNeededForItem:(const AvatarMenu::Item&)item {
1529 if (tutorialMode_ == profiles::TUTORIAL_MODE_CONFIRM_SIGNIN)
1530 return [self buildSigninConfirmationView];
1532 if (tutorialMode_ == profiles::TUTORIAL_MODE_SHOW_ERROR)
1533 return [self buildSigninErrorView];
1535 if (profiles::ShouldShowWelcomeUpgradeTutorial(
1536 browser_->profile(), tutorialMode_)) {
1537 if (tutorialMode_ != profiles::TUTORIAL_MODE_WELCOME_UPGRADE) {
1538 Profile* profile = browser_->profile();
1539 const int showCount = profile->GetPrefs()->GetInteger(
1540 prefs::kProfileAvatarTutorialShown);
1541 profile->GetPrefs()->SetInteger(
1542 prefs::kProfileAvatarTutorialShown, showCount + 1);
1545 return [self buildWelcomeUpgradeTutorialView:item];
1548 if (profiles::ShouldShowRightClickTutorial(browser_->profile()))
1549 return [self buildRightClickTutorialView];
1554 - (NSView*)tutorialViewWithMode:(profiles::TutorialMode)mode
1555 titleMessage:(NSString*)titleMessage
1556 contentMessage:(NSString*)contentMessage
1557 linkMessage:(NSString*)linkMessage
1558 buttonMessage:(NSString*)buttonMessage
1559 stackButton:(BOOL)stackButton
1560 hasCloseButton:(BOOL)hasCloseButton
1561 linkAction:(SEL)linkAction
1562 buttonAction:(SEL)buttonAction {
1563 tutorialMode_ = mode;
1565 NSColor* tutorialBackgroundColor =
1566 gfx::SkColorToSRGBNSColor(profiles::kAvatarTutorialBackgroundColor);
1567 base::scoped_nsobject<NSView> container([[BackgroundColorView alloc]
1568 initWithFrame:NSMakeRect(0, 0, kFixedMenuWidth, 0)
1569 withColor:tutorialBackgroundColor]);
1570 CGFloat availableWidth = kFixedMenuWidth - 2 * kHorizontalSpacing;
1571 CGFloat yOffset = kVerticalSpacing;
1573 // Adds links and buttons at the bottom.
1574 base::scoped_nsobject<NSButton> tutorialOkButton;
1575 if (buttonMessage) {
1576 tutorialOkButton.reset([[HoverButton alloc] initWithFrame:NSZeroRect]);
1577 [tutorialOkButton setTitle:buttonMessage];
1578 [tutorialOkButton setBezelStyle:NSRoundedBezelStyle];
1579 [tutorialOkButton setTarget:self];
1580 [tutorialOkButton setAction:buttonAction];
1581 [tutorialOkButton setAlignment:NSCenterTextAlignment];
1582 [tutorialOkButton sizeToFit];
1585 NSButton* learnMoreLink = nil;
1587 learnMoreLink = [self linkButtonWithTitle:linkMessage
1588 frameOrigin:NSZeroPoint
1590 [[learnMoreLink cell] setTextColor:[NSColor whiteColor]];
1595 [learnMoreLink setFrameOrigin:NSMakePoint(
1596 (kFixedMenuWidth - NSWidth([learnMoreLink frame])) / 2, yOffset)];
1598 [tutorialOkButton setFrameSize:NSMakeSize(
1599 availableWidth, NSHeight([tutorialOkButton frame]))];
1600 [tutorialOkButton setFrameOrigin:NSMakePoint(
1602 yOffset + (learnMoreLink ? NSHeight([learnMoreLink frame]) : 0))];
1604 if (buttonMessage) {
1605 NSSize buttonSize = [tutorialOkButton frame].size;
1606 const CGFloat kTopBottomTextPadding = 6;
1607 const CGFloat kLeftRightTextPadding = 15;
1608 buttonSize.width += 2 * kLeftRightTextPadding;
1609 buttonSize.height += 2 * kTopBottomTextPadding;
1610 [tutorialOkButton setFrameSize:buttonSize];
1611 CGFloat buttonXOffset = kFixedMenuWidth -
1612 NSWidth([tutorialOkButton frame]) - kHorizontalSpacing;
1613 [tutorialOkButton setFrameOrigin:NSMakePoint(buttonXOffset, yOffset)];
1617 CGFloat linkYOffset = yOffset;
1618 if (buttonMessage) {
1619 linkYOffset += (NSHeight([tutorialOkButton frame]) -
1620 NSHeight([learnMoreLink frame])) / 2;
1622 [learnMoreLink setFrameOrigin:NSMakePoint(
1623 kHorizontalSpacing, linkYOffset)];
1627 if (buttonMessage) {
1628 [container addSubview:tutorialOkButton];
1629 yOffset = NSMaxY([tutorialOkButton frame]);
1633 [container addSubview:learnMoreLink];
1634 yOffset = std::max(NSMaxY([learnMoreLink frame]), yOffset);
1637 yOffset += kVerticalSpacing;
1639 // Adds body content.
1640 NSTextField* contentLabel = BuildLabel(
1642 NSMakePoint(kHorizontalSpacing, yOffset),
1643 gfx::SkColorToSRGBNSColor(profiles::kAvatarTutorialContentTextColor));
1644 [contentLabel setFrameSize:NSMakeSize(availableWidth, 0)];
1645 [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:contentLabel];
1646 [container addSubview:contentLabel];
1647 yOffset = NSMaxY([contentLabel frame]) + kSmallVerticalSpacing;
1650 NSTextField* titleLabel =
1651 BuildLabel(titleMessage,
1652 NSMakePoint(kHorizontalSpacing, yOffset),
1653 [NSColor whiteColor] /* text_color */);
1654 [titleLabel setFont:[NSFont labelFontOfSize:kTitleFontSize]];
1656 if (hasCloseButton) {
1657 base::scoped_nsobject<HoverImageButton> closeButton(
1658 [[HoverImageButton alloc] initWithFrame:NSZeroRect]);
1659 [closeButton setBordered:NO];
1661 ui::ResourceBundle* rb = &ui::ResourceBundle::GetSharedInstance();
1662 NSImage* closeImage = rb->GetNativeImageNamed(IDR_CLOSE_1).ToNSImage();
1663 CGFloat closeImageWidth = [closeImage size].width;
1664 [closeButton setDefaultImage:closeImage];
1665 [closeButton setHoverImage:
1666 rb->GetNativeImageNamed(IDR_CLOSE_1_H).ToNSImage()];
1667 [closeButton setPressedImage:
1668 rb->GetNativeImageNamed(IDR_CLOSE_1_P).ToNSImage()];
1669 [closeButton setTarget:self];
1670 [closeButton setAction:@selector(dismissTutorial:)];
1671 [closeButton setFrameSize:[closeImage size]];
1672 [closeButton setFrameOrigin:NSMakePoint(
1673 kFixedMenuWidth - kHorizontalSpacing - closeImageWidth, yOffset)];
1674 [container addSubview:closeButton];
1676 [titleLabel setFrameSize:NSMakeSize(
1677 availableWidth - closeImageWidth - kHorizontalSpacing, 0)];
1679 [titleLabel setFrameSize:NSMakeSize(availableWidth, 0)];
1682 [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:titleLabel];
1683 [container addSubview:titleLabel];
1684 yOffset = NSMaxY([titleLabel frame]) + kVerticalSpacing;
1686 [container setFrameSize:NSMakeSize(kFixedMenuWidth, yOffset)];
1687 [container setFrameOrigin:NSZeroPoint];
1688 return container.autorelease();
1691 - (NSView*)createCurrentProfileView:(const AvatarMenu::Item&)item {
1692 base::scoped_nsobject<NSView> container([[NSView alloc]
1693 initWithFrame:NSZeroRect]);
1695 CGFloat xOffset = kHorizontalSpacing;
1696 CGFloat yOffset = 0;
1697 CGFloat availableTextWidth = kFixedMenuWidth - 2 * kHorizontalSpacing;
1699 // Profile options. This can be a link to the accounts view, the profile's
1700 // username for signed in users, or a "Sign in" button for local profiles.
1701 SigninManagerBase* signinManager =
1702 SigninManagerFactory::GetForProfile(
1703 browser_->profile()->GetOriginalProfile());
1704 if (!isGuestSession_ && signinManager->IsSigninAllowed()) {
1705 NSView* linksContainer =
1706 [self createCurrentProfileLinksForItem:item
1707 rect:NSMakeRect(xOffset, yOffset,
1710 [container addSubview:linksContainer];
1711 yOffset = NSMaxY([linksContainer frame]);
1714 // Profile name, centered.
1715 bool editingAllowed = !isGuestSession_ &&
1716 !browser_->profile()->IsLegacySupervised();
1717 base::scoped_nsobject<EditableProfileNameButton> profileName(
1718 [[EditableProfileNameButton alloc]
1719 initWithFrame:NSMakeRect(xOffset,
1722 kProfileButtonHeight)
1723 profile:browser_->profile()
1724 profileName:base::SysUTF16ToNSString(
1725 profiles::GetAvatarNameForProfile(
1726 browser_->profile()->GetPath()))
1727 editingAllowed:editingAllowed
1728 withController:self]);
1730 [container addSubview:profileName];
1731 yOffset = NSMaxY([profileName frame]) + 4; // Adds a small vertical padding.
1733 // Profile icon, centered.
1734 xOffset = (kFixedMenuWidth - kLargeImageSide) / 2;
1735 base::scoped_nsobject<EditableProfilePhoto> iconView(
1736 [[EditableProfilePhoto alloc]
1737 initWithFrame:NSMakeRect(xOffset, yOffset,
1738 kLargeImageSide, kLargeImageSide)
1739 avatarMenu:avatarMenu_.get()
1740 profileIcon:item.icon
1741 editingAllowed:!isGuestSession_
1742 withController:self]);
1744 [container addSubview:iconView];
1745 yOffset = NSMaxY([iconView frame]);
1747 if (browser_->profile()->IsSupervised()) {
1748 base::scoped_nsobject<NSImageView> supervisedIcon(
1749 [[NSImageView alloc] initWithFrame:NSZeroRect]);
1750 int imageId = browser_->profile()->IsChild()
1751 ? IDR_ICON_PROFILES_MENU_CHILD
1752 : IDR_ICON_PROFILES_MENU_LEGACY_SUPERVISED;
1753 ui::ResourceBundle* rb = &ui::ResourceBundle::GetSharedInstance();
1754 [supervisedIcon setImage:rb->GetNativeImageNamed(imageId).ToNSImage()];
1755 NSSize size = [[supervisedIcon image] size];
1756 [supervisedIcon setFrameSize:size];
1757 NSRect parentFrame = [iconView frame];
1758 [supervisedIcon setFrameOrigin:NSMakePoint(NSMaxX(parentFrame) - size.width,
1759 NSMinY(parentFrame))];
1760 [container addSubview:supervisedIcon];
1763 [container setFrameSize:NSMakeSize(kFixedMenuWidth, yOffset)];
1764 return container.autorelease();
1767 - (NSView*)createCurrentProfileLinksForItem:(const AvatarMenu::Item&)item
1769 base::scoped_nsobject<NSView> container([[NSView alloc] initWithFrame:rect]);
1771 // Don't double-apply the left margin to the sub-views.
1774 // The available links depend on the type of profile that is active.
1775 if (item.signed_in) {
1776 // Signed in profiles with no authentication errors do not have a clickable
1778 NSButton* link = nil;
1779 if (switches::IsEnableAccountConsistency()) {
1780 NSString* linkTitle = l10n_util::GetNSString(
1781 viewMode_ == profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER ?
1782 IDS_PROFILES_PROFILE_MANAGE_ACCOUNTS_BUTTON :
1783 IDS_PROFILES_PROFILE_HIDE_MANAGE_ACCOUNTS_BUTTON);
1785 (viewMode_ == profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER) ?
1786 @selector(showAccountManagement:) : @selector(hideAccountManagement:);
1787 link = [self linkButtonWithTitle:linkTitle
1788 frameOrigin:rect.origin
1789 action:linkSelector];
1791 link = [self linkButtonWithTitle:base::SysUTF16ToNSString(item.username)
1792 frameOrigin:rect.origin
1794 if (HasAuthError(browser_->profile())) {
1795 [link setImage:ui::ResourceBundle::GetSharedInstance().
1796 GetNativeImageNamed(IDR_ICON_PROFILES_ACCOUNT_BUTTON_ERROR).
1798 [link setImagePosition:NSImageRight];
1799 [link setTarget:self];
1800 [link setAction:@selector(showAccountReauthenticationView:)];
1801 [link setTag:kPrimaryProfileTag];
1803 accessibilitySetOverrideValue:l10n_util::GetNSStringF(
1804 IDS_PROFILES_ACCOUNT_BUTTON_AUTH_ERROR_ACCESSIBLE_NAME,
1806 forAttribute:NSAccessibilityTitleAttribute];
1808 [link setEnabled:NO];
1811 // -linkButtonWithTitle sizeToFit's the link. We can use the height, but
1812 // need to re-stretch the width so that the link can be centered correctly
1814 rect.size.height = [link frame].size.height;
1815 [link setAlignment:NSCenterTextAlignment];
1816 [link setFrame:rect];
1817 [container addSubview:link];
1818 [container setFrameSize:rect.size];
1820 rect.size.height = kBlueButtonHeight;
1821 NSButton* signinButton = [[BlueLabelButton alloc] initWithFrame:rect];
1823 // Manually elide the button text so that the contents fit inside the bubble
1824 // This is needed because the BlueLabelButton cell resets the style on
1825 // every call to -cellSize, which prevents setting a custom lineBreakMode.
1826 NSString* elidedButtonText = base::SysUTF16ToNSString(gfx::ElideText(
1827 l10n_util::GetStringFUTF16(
1828 IDS_SYNC_START_SYNC_BUTTON_LABEL,
1829 l10n_util::GetStringUTF16(IDS_SHORT_PRODUCT_NAME)),
1830 gfx::FontList(), rect.size.width, gfx::ELIDE_TAIL));
1832 [signinButton setTitle:elidedButtonText];
1833 [signinButton setTarget:self];
1834 [signinButton setAction:@selector(showInlineSigninPage:)];
1835 [container addSubview:signinButton];
1837 // Sign-in promo text.
1838 NSTextField* promo = BuildLabel(
1839 l10n_util::GetNSString(IDS_PROFILES_SIGNIN_PROMO),
1840 NSMakePoint(0, NSMaxY([signinButton frame]) + kVerticalSpacing),
1842 [promo setFrameSize:NSMakeSize(rect.size.width, 0)];
1843 [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:promo];
1844 [container addSubview:promo];
1846 [container setFrameSize:NSMakeSize(
1848 NSMaxY([promo frame]) + 4)]; // Adds a small vertical padding.
1851 return container.autorelease();
1854 - (NSView*)createSupervisedUserDisclaimerView {
1855 base::scoped_nsobject<NSView> container(
1856 [[NSView alloc] initWithFrame:NSZeroRect]);
1859 int availableTextWidth = kFixedMenuWidth - 2 * kHorizontalSpacing;
1861 NSTextField* disclaimer = BuildLabel(
1862 base::SysUTF16ToNSString(avatarMenu_->GetSupervisedUserInformation()),
1863 NSMakePoint(kHorizontalSpacing, yOffset), nil);
1864 [disclaimer setFrameSize:NSMakeSize(availableTextWidth, 0)];
1865 [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:disclaimer];
1866 yOffset = NSMaxY([disclaimer frame]);
1868 [container addSubview:disclaimer];
1869 [container setFrameSize:NSMakeSize(kFixedMenuWidth, yOffset)];
1870 return container.autorelease();
1873 - (NSView*)createGuestProfileView {
1874 gfx::Image guestIcon =
1875 ui::ResourceBundle::GetSharedInstance().GetNativeImageNamed(
1876 profiles::GetPlaceholderAvatarIconResourceID());
1877 AvatarMenu::Item guestItem(std::string::npos, /* menu_index, not used */
1878 std::string::npos, /* profile_index, not used */
1880 guestItem.active = true;
1881 guestItem.name = base::SysNSStringToUTF16(
1882 l10n_util::GetNSString(IDS_PROFILES_GUEST_PROFILE_NAME));
1884 return [self createCurrentProfileView:guestItem];
1887 - (NSButton*)createOtherProfileView:(int)itemIndex {
1888 const AvatarMenu::Item& item = avatarMenu_->GetItemAt(itemIndex);
1890 NSRect rect = NSMakeRect(
1891 0, 0, kFixedMenuWidth, kBlueButtonHeight + kSmallVerticalSpacing);
1892 base::scoped_nsobject<BackgroundColorHoverButton> profileButton(
1893 [[BackgroundColorHoverButton alloc]
1895 imageTitleSpacing:kImageTitleSpacing
1896 backgroundColor:GetDialogBackgroundColor()]);
1898 NSString* title = base::SysUTF16ToNSString(
1899 profiles::GetProfileSwitcherTextForItem(item));
1900 [profileButton setTitle:title];
1902 // Use the low-res, small default avatars in the fast user switcher, like
1903 // we do in the menu bar.
1904 gfx::Image itemIcon;
1906 AvatarMenu::GetImageForMenuButton(item.profile_path, &itemIcon, &isRectangle);
1908 [profileButton setDefaultImage:CreateProfileImage(
1909 itemIcon, kSmallImageSide).ToNSImage()];
1910 [profileButton setImagePosition:NSImageLeft];
1911 [profileButton setAlignment:NSLeftTextAlignment];
1912 [profileButton setBordered:NO];
1913 [profileButton setTag:itemIndex];
1914 [profileButton setTarget:self];
1915 [profileButton setAction:@selector(switchToProfile:)];
1917 NSSize textSize = [[profileButton title] sizeWithAttributes:@{
1918 NSFontAttributeName : [profileButton font]
1921 CGFloat availableWidth = rect.size.width - kSmallImageSide -
1922 kImageTitleSpacing - kHorizontalSpacing;
1924 if (std::ceil(textSize.width) > availableWidth)
1925 [profileButton setToolTip:[profileButton title]];
1927 return profileButton.autorelease();
1930 - (NSView*)createOptionsViewWithRect:(NSRect)rect
1931 displayLock:(BOOL)displayLock {
1932 NSRect viewRect = NSMakeRect(0, 0,
1934 kBlueButtonHeight + kSmallVerticalSpacing);
1935 base::scoped_nsobject<NSView> container([[NSView alloc] initWithFrame:rect]);
1938 NSButton* lockButton =
1939 [self hoverButtonWithRect:viewRect
1940 text:l10n_util::GetNSString(
1941 IDS_PROFILES_PROFILE_SIGNOUT_BUTTON)
1942 imageResourceId:IDR_ICON_PROFILES_MENU_LOCK
1943 action:@selector(lockProfile:)];
1944 [container addSubview:lockButton];
1945 viewRect.origin.y = NSMaxY([lockButton frame]);
1947 NSBox* separator = [self horizontalSeparatorWithFrame:viewRect];
1948 [container addSubview:separator];
1949 viewRect.origin.y = NSMaxY([separator frame]);
1952 if ([self shouldShowGoIncognito]) {
1953 NSButton* goIncognitoButton =
1954 [self hoverButtonWithRect:viewRect
1955 text:l10n_util::GetNSString(
1956 IDS_PROFILES_GO_INCOGNITO_BUTTON)
1957 imageResourceId:IDR_ICON_PROFILES_MENU_INCOGNITO
1958 action:@selector(goIncognito:)];
1959 viewRect.origin.y = NSMaxY([goIncognitoButton frame]);
1960 [container addSubview:goIncognitoButton];
1962 NSBox* separator = [self horizontalSeparatorWithFrame:viewRect];
1963 [container addSubview:separator];
1964 viewRect.origin.y = NSMaxY([separator frame]);
1967 NSString* text = isGuestSession_ ?
1968 l10n_util::GetNSString(IDS_PROFILES_EXIT_GUEST) :
1969 l10n_util::GetNSString(IDS_PROFILES_SWITCH_USERS_BUTTON);
1970 NSButton* switchUsersButton =
1971 [self hoverButtonWithRect:viewRect
1973 imageResourceId:IDR_ICON_PROFILES_MENU_AVATAR
1974 action:isGuestSession_? @selector(exitGuest:) :
1975 @selector(showUserManager:)];
1976 viewRect.origin.y = NSMaxY([switchUsersButton frame]);
1977 [container addSubview:switchUsersButton];
1979 [container setFrameSize:NSMakeSize(rect.size.width, viewRect.origin.y)];
1980 return container.autorelease();
1983 - (NSView*)createCurrentProfileAccountsView:(NSRect)rect {
1984 const CGFloat kAccountButtonHeight = 34;
1986 const AvatarMenu::Item& item =
1987 avatarMenu_->GetItemAt(avatarMenu_->GetActiveProfileIndex());
1988 DCHECK(item.signed_in);
1990 NSColor* backgroundColor = gfx::SkColorToCalibratedNSColor(
1991 profiles::kAvatarBubbleAccountsBackgroundColor);
1992 base::scoped_nsobject<NSView> container([[BackgroundColorView alloc]
1994 withColor:backgroundColor]);
1997 if (!browser_->profile()->IsSupervised()) {
1998 // Manually elide the button text so the contents fit inside the bubble.
1999 // This is needed because the BlueLabelButton cell resets the style on
2000 // every call to -cellSize, which prevents setting a custom lineBreakMode.
2001 NSString* elidedButtonText = base::SysUTF16ToNSString(gfx::ElideText(
2002 l10n_util::GetStringFUTF16(
2003 IDS_PROFILES_PROFILE_ADD_ACCOUNT_BUTTON, item.name),
2004 gfx::FontList(), rect.size.width, gfx::ELIDE_TAIL));
2006 NSButton* addAccountsButton =
2007 [self linkButtonWithTitle:elidedButtonText
2008 frameOrigin:NSMakePoint(
2009 kHorizontalSpacing, kSmallVerticalSpacing)
2010 action:@selector(addAccount:)];
2011 [container addSubview:addAccountsButton];
2012 rect.origin.y += kAccountButtonHeight;
2015 NSView* accountEmails = [self createAccountsListWithRect:NSMakeRect(
2016 0, rect.origin.y, rect.size.width, kAccountButtonHeight)];
2017 [container addSubview:accountEmails];
2019 [container setFrameSize:NSMakeSize(rect.size.width,
2020 NSMaxY([accountEmails frame]))];
2021 return container.autorelease();
2024 - (NSView*)createAccountsListWithRect:(NSRect)rect {
2025 base::scoped_nsobject<NSView> container([[NSView alloc] initWithFrame:rect]);
2026 currentProfileAccounts_.clear();
2028 Profile* profile = browser_->profile();
2029 std::string primaryAccount =
2030 SigninManagerFactory::GetForProfile(profile)->GetAuthenticatedAccountId();
2031 DCHECK(!primaryAccount.empty());
2032 std::vector<std::string>accounts =
2033 profiles::GetSecondaryAccountsForProfile(profile, primaryAccount);
2035 // If there is an account with an authentication error, it needs to be
2036 // badged with a warning icon.
2037 std::string errorAccountId = GetAuthErrorAccountId(profile);
2040 for (size_t i = 0; i < accounts.size(); ++i) {
2041 // Save the original email address, as the button text could be elided.
2042 currentProfileAccounts_[i] = accounts[i];
2043 NSButton* accountButton =
2044 [self accountButtonWithRect:rect
2045 accountId:accounts[i]
2047 reauthRequired:errorAccountId == accounts[i]];
2048 [container addSubview:accountButton];
2049 rect.origin.y = NSMaxY([accountButton frame]);
2052 // The primary account should always be listed first.
2053 NSButton* accountButton =
2054 [self accountButtonWithRect:rect
2055 accountId:primaryAccount
2056 tag:kPrimaryProfileTag
2057 reauthRequired:errorAccountId == primaryAccount];
2058 [container addSubview:accountButton];
2059 [container setFrameSize:NSMakeSize(NSWidth([container frame]),
2060 NSMaxY([accountButton frame]))];
2061 return container.autorelease();
2064 - (NSView*)buildGaiaEmbeddedView {
2065 base::scoped_nsobject<NSView> container(
2066 [[NSView alloc] initWithFrame:NSZeroRect]);
2067 CGFloat yOffset = 0;
2071 switch (viewMode_) {
2072 case profiles::BUBBLE_VIEW_MODE_GAIA_SIGNIN:
2073 url = signin::GetPromoURL(signin_metrics::SOURCE_AVATAR_BUBBLE_SIGN_IN,
2074 false /* auto_close */,
2075 true /* is_constrained */);
2076 messageId = IDS_PROFILES_GAIA_SIGNIN_TITLE;
2078 case profiles::BUBBLE_VIEW_MODE_GAIA_ADD_ACCOUNT:
2079 url = signin::GetPromoURL(
2080 signin_metrics::SOURCE_AVATAR_BUBBLE_ADD_ACCOUNT,
2081 false /* auto_close */,
2082 true /* is_constrained */);
2083 messageId = IDS_PROFILES_GAIA_ADD_ACCOUNT_TITLE;
2085 case profiles::BUBBLE_VIEW_MODE_GAIA_REAUTH:
2086 DCHECK(HasAuthError(browser_->profile()));
2087 url = signin::GetReauthURL(
2088 browser_->profile(), GetAuthErrorAccountId(browser_->profile()));
2089 messageId = IDS_PROFILES_GAIA_REAUTH_TITLE;
2092 NOTREACHED() << "Called with invalid mode=" << viewMode_;
2096 webContents_.reset(content::WebContents::Create(
2097 content::WebContents::CreateParams(browser_->profile())));
2099 webContentsDelegate_.reset(new GaiaWebContentsDelegate());
2100 webContents_->SetDelegate(webContentsDelegate_.get());
2101 webContents_->GetController().LoadURL(url,
2102 content::Referrer(),
2103 ui::PAGE_TRANSITION_AUTO_TOPLEVEL,
2105 NSView* webview = webContents_->GetNativeView();
2106 [webview setFrameSize:NSMakeSize(kFixedGaiaViewWidth, kFixedGaiaViewHeight)];
2107 [container addSubview:webview];
2108 content::RenderWidgetHostView* rwhv = webContents_->GetRenderWidgetHostView();
2110 rwhv->SetBackgroundColor(profiles::kAvatarBubbleGaiaBackgroundColor);
2111 yOffset = NSMaxY([webview frame]);
2113 // Adds the title card.
2114 NSBox* separator = [self horizontalSeparatorWithFrame:
2115 NSMakeRect(0, yOffset, kFixedGaiaViewWidth, 0)];
2116 [container addSubview:separator];
2117 yOffset = NSMaxY([separator frame]) + kVerticalSpacing;
2119 NSView* titleView = BuildTitleCard(
2120 NSMakeRect(0, yOffset, kFixedGaiaViewWidth, 0),
2121 l10n_util::GetStringUTF16(messageId),
2122 self /* backButtonTarget*/,
2123 @selector(navigateBackFromSigninPage:) /* backButtonAction */);
2124 [container addSubview:titleView];
2125 yOffset = NSMaxY([titleView frame]);
2127 [container setFrameSize:NSMakeSize(kFixedGaiaViewWidth, yOffset)];
2128 return container.autorelease();
2131 - (NSView*)buildAccountRemovalView {
2132 DCHECK(!accountIdToRemove_.empty());
2134 base::scoped_nsobject<NSView> container(
2135 [[NSView alloc] initWithFrame:NSZeroRect]);
2136 CGFloat availableWidth =
2137 kFixedAccountRemovalViewWidth - 2 * kHorizontalSpacing;
2138 CGFloat yOffset = kVerticalSpacing;
2140 const std::string& primaryAccount = SigninManagerFactory::GetForProfile(
2141 browser_->profile())->GetAuthenticatedAccountId();
2142 bool isPrimaryAccount = primaryAccount == accountIdToRemove_;
2144 // Adds "remove account" button at the bottom if needed.
2145 if (!isPrimaryAccount) {
2146 base::scoped_nsobject<NSButton> removeAccountButton(
2147 [[BlueLabelButton alloc] initWithFrame:NSZeroRect]);
2148 [removeAccountButton setTitle:l10n_util::GetNSString(
2149 IDS_PROFILES_ACCOUNT_REMOVAL_BUTTON)];
2150 [removeAccountButton setTarget:self];
2151 [removeAccountButton setAction:@selector(removeAccount:)];
2152 [removeAccountButton sizeToFit];
2153 [removeAccountButton setAlignment:NSCenterTextAlignment];
2154 CGFloat xOffset = (kFixedAccountRemovalViewWidth -
2155 NSWidth([removeAccountButton frame])) / 2;
2156 [removeAccountButton setFrameOrigin:NSMakePoint(xOffset, yOffset)];
2157 [container addSubview:removeAccountButton];
2159 yOffset = NSMaxY([removeAccountButton frame]) + kVerticalSpacing;
2162 NSView* contentView;
2163 NSPoint contentFrameOrigin = NSMakePoint(kHorizontalSpacing, yOffset);
2164 if (isPrimaryAccount) {
2165 std::string email = signin_ui_util::GetDisplayEmail(browser_->profile(),
2166 accountIdToRemove_);
2167 std::vector<size_t> offsets;
2168 NSString* contentStr = l10n_util::GetNSStringF(
2169 IDS_PROFILES_PRIMARY_ACCOUNT_REMOVAL_TEXT,
2170 base::UTF8ToUTF16(email), base::string16(), &offsets);
2171 NSString* linkStr = l10n_util::GetNSString(IDS_PROFILES_SETTINGS_LINK);
2172 contentView = BuildFixedWidthTextViewWithLink(self, contentStr, linkStr,
2173 offsets[1], contentFrameOrigin, availableWidth);
2175 NSString* contentStr =
2176 l10n_util::GetNSString(IDS_PROFILES_ACCOUNT_REMOVAL_TEXT);
2177 NSTextField* contentLabel = BuildLabel(contentStr, contentFrameOrigin, nil);
2178 [contentLabel setFrameSize:NSMakeSize(availableWidth, 0)];
2179 [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:contentLabel];
2180 contentView = contentLabel;
2182 [container addSubview:contentView];
2183 yOffset = NSMaxY([contentView frame]) + kVerticalSpacing;
2185 // Adds the title card.
2186 NSBox* separator = [self horizontalSeparatorWithFrame:
2187 NSMakeRect(0, yOffset, kFixedAccountRemovalViewWidth, 0)];
2188 [container addSubview:separator];
2189 yOffset = NSMaxY([separator frame]) + kVerticalSpacing;
2191 NSView* titleView = BuildTitleCard(
2192 NSMakeRect(0, yOffset, kFixedAccountRemovalViewWidth,0),
2193 l10n_util::GetStringUTF16(IDS_PROFILES_ACCOUNT_REMOVAL_TITLE),
2194 self /* backButtonTarget*/,
2195 @selector(showAccountManagement:) /* backButtonAction */);
2196 [container addSubview:titleView];
2197 yOffset = NSMaxY([titleView frame]);
2199 [container setFrameSize:NSMakeSize(kFixedAccountRemovalViewWidth, yOffset)];
2200 return container.autorelease();
2203 - (NSView*)buildSwitchUserView {
2204 ProfileMetrics::LogProfileNewAvatarMenuNotYou(
2205 ProfileMetrics::PROFILE_AVATAR_MENU_NOT_YOU_VIEW);
2206 base::scoped_nsobject<NSView> container(
2207 [[NSView alloc] initWithFrame:NSZeroRect]);
2208 CGFloat availableWidth =
2209 kFixedSwitchUserViewWidth - 2 * kHorizontalSpacing;
2210 CGFloat yOffset = 0;
2211 NSRect viewRect = NSMakeRect(0, yOffset,
2212 kFixedSwitchUserViewWidth,
2213 kBlueButtonHeight + kSmallVerticalSpacing);
2215 const AvatarMenu::Item& avatarItem =
2216 avatarMenu_->GetItemAt(avatarMenu_->GetActiveProfileIndex());
2218 // Adds "Disconnect your Google Account" button at the bottom.
2219 NSButton* disconnectButton =
2220 [self hoverButtonWithRect:viewRect
2221 text:l10n_util::GetNSString(
2222 IDS_PROFILES_DISCONNECT_BUTTON)
2223 imageResourceId:IDR_ICON_PROFILES_MENU_DISCONNECT
2224 action:@selector(disconnectProfile:)];
2225 [container addSubview:disconnectButton];
2226 yOffset = NSMaxY([disconnectButton frame]);
2228 NSBox* separator = [self horizontalSeparatorWithFrame:
2229 NSMakeRect(0, yOffset, kFixedSwitchUserViewWidth, 0)];
2230 [container addSubview:separator];
2231 yOffset = NSMaxY([separator frame]);
2233 // Adds "Add person" button.
2234 viewRect.origin.y = yOffset;
2235 NSButton* addPersonButton =
2236 [self hoverButtonWithRect:viewRect
2237 text:l10n_util::GetNSString(
2238 IDS_PROFILES_ADD_PERSON_BUTTON)
2239 imageResourceId:IDR_ICON_PROFILES_MENU_AVATAR
2240 action:@selector(showUserManager:)];
2241 [container addSubview:addPersonButton];
2242 yOffset = NSMaxY([addPersonButton frame]);
2244 separator = [self horizontalSeparatorWithFrame:
2245 NSMakeRect(0, yOffset, kFixedSwitchUserViewWidth, 0)];
2246 [container addSubview:separator];
2247 yOffset = NSMaxY([separator frame]);
2249 // Adds the content text.
2250 base::string16 elidedName(gfx::ElideText(
2251 avatarItem.name, gfx::FontList(), availableWidth, gfx::ELIDE_TAIL));
2252 NSTextField* contentLabel = BuildLabel(
2253 l10n_util::GetNSStringF(IDS_PROFILES_NOT_YOU_CONTENT_TEXT, elidedName),
2254 NSMakePoint(kHorizontalSpacing, yOffset + kVerticalSpacing),
2256 [contentLabel setFrameSize:NSMakeSize(availableWidth, 0)];
2257 [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:contentLabel];
2258 [container addSubview:contentLabel];
2259 yOffset = NSMaxY([contentLabel frame]) + kVerticalSpacing;
2261 // Adds the title card.
2262 separator = [self horizontalSeparatorWithFrame:
2263 NSMakeRect(0, yOffset, kFixedSwitchUserViewWidth, 0)];
2264 [container addSubview:separator];
2265 yOffset = NSMaxY([separator frame]) + kVerticalSpacing;
2267 NSView* titleView = BuildTitleCard(
2268 NSMakeRect(0, yOffset, kFixedSwitchUserViewWidth,0),
2269 l10n_util::GetStringFUTF16(IDS_PROFILES_NOT_YOU, avatarItem.name),
2270 self /* backButtonTarget*/,
2271 @selector(navigateBackFromSwitchUserView:) /* backButtonAction */);
2272 [container addSubview:titleView];
2273 yOffset = NSMaxY([titleView frame]);
2275 [container setFrameSize:NSMakeSize(kFixedSwitchUserViewWidth, yOffset)];
2276 return container.autorelease();
2279 // Called when clicked on the settings link.
2280 - (BOOL)textView:(NSTextView*)textView
2281 clickedOnLink:(id)link
2282 atIndex:(NSUInteger)charIndex {
2283 chrome::ShowSettings(browser_);
2287 - (NSButton*)hoverButtonWithRect:(NSRect)rect
2288 text:(NSString*)text
2289 imageResourceId:(int)imageResourceId
2290 action:(SEL)action {
2291 base::scoped_nsobject<BackgroundColorHoverButton> button(
2292 [[BackgroundColorHoverButton alloc]
2294 imageTitleSpacing:kImageTitleSpacing
2295 backgroundColor:GetDialogBackgroundColor()]);
2297 [button setTitle:text];
2298 ui::ResourceBundle* rb = &ui::ResourceBundle::GetSharedInstance();
2299 NSImage* image = rb->GetNativeImageNamed(imageResourceId).ToNSImage();
2300 [button setDefaultImage:image];
2301 [button setHoverImage:image];
2302 [button setPressedImage:image];
2303 [button setImagePosition:NSImageLeft];
2304 [button setAlignment:NSLeftTextAlignment];
2305 [button setBordered:NO];
2306 [button setTarget:self];
2307 [button setAction:action];
2309 return button.autorelease();
2312 - (NSButton*)linkButtonWithTitle:(NSString*)title
2313 frameOrigin:(NSPoint)frameOrigin
2314 action:(SEL)action {
2315 base::scoped_nsobject<NSButton> link(
2316 [[HyperlinkButtonCell buttonWithString:title] retain]);
2318 [[link cell] setShouldUnderline:NO];
2319 [[link cell] setTextColor:gfx::SkColorToCalibratedNSColor(
2320 chrome_style::GetLinkColor())];
2321 [link setTitle:title];
2322 [link setBordered:NO];
2323 [link setFont:[NSFont labelFontOfSize:kTextFontSize]];
2324 [link setTarget:self];
2325 [link setAction:action];
2326 [link setFrameOrigin:frameOrigin];
2329 return link.autorelease();
2332 - (NSButton*)accountButtonWithRect:(NSRect)rect
2333 accountId:(const std::string&)accountId
2335 reauthRequired:(BOOL)reauthRequired {
2336 // Get display email address for account.
2337 std::string email = signin_ui_util::GetDisplayEmail(browser_->profile(),
2340 ui::ResourceBundle* rb = &ui::ResourceBundle::GetSharedInstance();
2341 NSImage* deleteImage = rb->GetNativeImageNamed(IDR_CLOSE_1).ToNSImage();
2342 CGFloat deleteImageWidth = [deleteImage size].width;
2343 NSImage* warningImage = reauthRequired ? rb->GetNativeImageNamed(
2344 IDR_ICON_PROFILES_ACCOUNT_BUTTON_ERROR).ToNSImage() : nil;
2345 CGFloat warningImageWidth = [warningImage size].width;
2347 CGFloat availableTextWidth = rect.size.width - kHorizontalSpacing -
2348 warningImageWidth - deleteImageWidth;
2350 availableTextWidth -= kHorizontalSpacing;
2352 NSColor* backgroundColor = gfx::SkColorToCalibratedNSColor(
2353 profiles::kAvatarBubbleAccountsBackgroundColor);
2354 base::scoped_nsobject<BackgroundColorHoverButton> button(
2355 [[BackgroundColorHoverButton alloc] initWithFrame:rect
2357 backgroundColor:backgroundColor]);
2358 [button setTitle:ElideEmail(email, availableTextWidth)];
2359 [button setAlignment:NSLeftTextAlignment];
2360 [button setBordered:NO];
2361 if (reauthRequired) {
2362 [button setDefaultImage:warningImage];
2363 [button setImagePosition:NSImageLeft];
2364 [button setTarget:self];
2365 [button setAction:@selector(showAccountReauthenticationView:)];
2366 [button setTag:tag];
2370 if (!browser_->profile()->IsSupervised()) {
2372 NSDivideRect(rect, &buttonRect, &rect,
2373 deleteImageWidth + kHorizontalSpacing, NSMaxXEdge);
2374 buttonRect.origin.y = 0;
2376 base::scoped_nsobject<HoverImageButton> deleteButton(
2377 [[HoverImageButton alloc] initWithFrame:buttonRect]);
2378 [deleteButton setBordered:NO];
2379 [deleteButton setDefaultImage:deleteImage];
2380 [deleteButton setHoverImage:rb->GetNativeImageNamed(
2381 IDR_CLOSE_1_H).ToNSImage()];
2382 [deleteButton setPressedImage:rb->GetNativeImageNamed(
2383 IDR_CLOSE_1_P).ToNSImage()];
2384 [deleteButton setTarget:self];
2385 [deleteButton setAction:@selector(showAccountRemovalView:)];
2386 [deleteButton setTag:tag];
2388 [button addSubview:deleteButton];
2391 return button.autorelease();
2394 - (void)postActionPerformed:(ProfileMetrics::ProfileDesktopMenu)action {
2395 ProfileMetrics::LogProfileDesktopMenu(action, serviceType_);
2396 serviceType_ = signin::GAIA_SERVICE_TYPE_NONE;
2399 - (bool)shouldShowGoIncognito {
2400 bool incognitoAvailable =
2401 IncognitoModePrefs::GetAvailability(browser_->profile()->GetPrefs()) !=
2402 IncognitoModePrefs::DISABLED;
2403 return incognitoAvailable && !browser_->profile()->IsGuestSession();