1 // Copyright 2013 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #import <Cocoa/Cocoa.h>
7 #import "chrome/browser/ui/cocoa/browser/profile_chooser_controller.h"
9 #include "base/mac/bundle_locations.h"
10 #include "base/prefs/pref_service.h"
11 #include "base/strings/sys_string_conversions.h"
12 #include "base/strings/utf_string_conversions.h"
13 #include "chrome/browser/browser_process.h"
14 #include "chrome/browser/chrome_notification_types.h"
15 #include "chrome/browser/profiles/avatar_menu.h"
16 #include "chrome/browser/profiles/avatar_menu_observer.h"
17 #include "chrome/browser/profiles/profile_info_cache.h"
18 #include "chrome/browser/profiles/profile_info_util.h"
19 #include "chrome/browser/profiles/profile_manager.h"
20 #include "chrome/browser/profiles/profile_metrics.h"
21 #include "chrome/browser/profiles/profile_window.h"
22 #include "chrome/browser/profiles/profiles_state.h"
23 #include "chrome/browser/signin/profile_oauth2_token_service_factory.h"
24 #include "chrome/browser/signin/signin_manager.h"
25 #include "chrome/browser/signin/signin_manager_factory.h"
26 #include "chrome/browser/signin/signin_promo.h"
27 #include "chrome/browser/ui/browser.h"
28 #include "chrome/browser/ui/browser_dialogs.h"
29 #include "chrome/browser/ui/browser_window.h"
30 #include "chrome/browser/ui/chrome_style.h"
31 #import "chrome/browser/ui/cocoa/info_bubble_view.h"
32 #import "chrome/browser/ui/cocoa/info_bubble_window.h"
33 #import "chrome/browser/ui/cocoa/user_manager_mac.h"
34 #include "chrome/browser/ui/singleton_tabs.h"
35 #include "chrome/common/pref_names.h"
36 #include "chrome/common/profile_management_switches.h"
37 #include "chrome/common/url_constants.h"
38 #include "components/signin/core/browser/mutable_profile_oauth2_token_service.h"
39 #include "components/signin/core/browser/profile_oauth2_token_service.h"
40 #include "content/public/browser/notification_service.h"
41 #include "content/public/browser/web_contents.h"
42 #include "content/public/browser/web_contents_view.h"
43 #include "google_apis/gaia/oauth2_token_service.h"
44 #include "grit/chromium_strings.h"
45 #include "grit/generated_resources.h"
46 #include "grit/theme_resources.h"
47 #include "skia/ext/skia_utils_mac.h"
48 #import "third_party/google_toolbox_for_mac/src/AppKit/GTMUILocalizerAndLayoutTweaker.h"
49 #import "ui/base/cocoa/cocoa_base_utils.h"
50 #import "ui/base/cocoa/controls/blue_label_button.h"
51 #import "ui/base/cocoa/controls/hyperlink_button_cell.h"
52 #import "ui/base/cocoa/hover_image_button.h"
53 #include "ui/base/cocoa/window_size_constants.h"
54 #include "ui/base/l10n/l10n_util.h"
55 #include "ui/base/l10n/l10n_util_mac.h"
56 #include "ui/base/resource/resource_bundle.h"
57 #include "ui/gfx/image/image.h"
58 #include "ui/gfx/text_elider.h"
59 #include "ui/native_theme/native_theme.h"
63 // Constants taken from the Windows/Views implementation at:
64 // chrome/browser/ui/views/profile_chooser_view.cc
65 const int kLargeImageSide = 64;
66 const int kSmallImageSide = 32;
67 const CGFloat kFixedMenuWidth = 250;
69 const CGFloat kVerticalSpacing = 20.0;
70 const CGFloat kSmallVerticalSpacing = 10.0;
71 const CGFloat kHorizontalSpacing = 20.0;
72 const CGFloat kTitleFontSize = 15.0;
73 const CGFloat kTextFontSize = 12.0;
74 const CGFloat kProfileButtonHeight = 30;
75 const int kOverlayHeight = 20; // Height of the "Change" avatar photo overlay.
76 const int kBezelThickness = 3; // Width of the bezel on an NSButton.
77 const int kImageTitleSpacing = 10;
78 const int kBlueButtonHeight = 30;
80 // Minimum size for embedded sign in pages as defined in Gaia.
81 const CGFloat kMinGaiaViewWidth = 320;
82 const CGFloat kMinGaiaViewHeight = 440;
84 // Maximum number of times to show the tutorial in the profile avatar bubble.
85 const int kProfileAvatarTutorialShowMax = 5;
87 gfx::Image CreateProfileImage(const gfx::Image& icon, int imageSize) {
88 return profiles::GetSizedAvatarIconWithBorder(
89 icon, true /* image is a square */,
90 imageSize + profiles::kAvatarIconPadding,
91 imageSize + profiles::kAvatarIconPadding);
94 // Updates the window size and position.
95 void SetWindowSize(NSWindow* window, NSSize size) {
96 NSRect frame = [window frame];
97 frame.origin.x += frame.size.width - size.width;
98 frame.origin.y += frame.size.height - size.height;
100 [window setFrame:frame display:YES];
103 NSString* ElideEmail(const std::string& email, CGFloat width) {
104 base::string16 elidedEmail = gfx::ElideEmail(
105 base::UTF8ToUTF16(email),
106 ui::ResourceBundle::GetSharedInstance().GetFontList(
107 ui::ResourceBundle::BaseFont),
109 return base::SysUTF16ToNSString(elidedEmail);
114 // Class that listens to changes to the OAuth2Tokens for the active profile,
115 // changes to the avatar menu model or browser close notifications.
116 class ActiveProfileObserverBridge : public AvatarMenuObserver,
117 public content::NotificationObserver,
118 public OAuth2TokenService::Observer {
120 ActiveProfileObserverBridge(ProfileChooserController* controller,
122 : controller_(controller),
124 token_observer_registered_(false) {
125 registrar_.Add(this, chrome::NOTIFICATION_BROWSER_CLOSING,
126 content::NotificationService::AllSources());
127 if (!browser_->profile()->IsGuestSession())
128 AddTokenServiceObserver();
131 virtual ~ActiveProfileObserverBridge() {
132 RemoveTokenServiceObserver();
136 void AddTokenServiceObserver() {
137 ProfileOAuth2TokenService* oauth2_token_service =
138 ProfileOAuth2TokenServiceFactory::GetForProfile(browser_->profile());
139 DCHECK(oauth2_token_service);
140 oauth2_token_service->AddObserver(this);
141 token_observer_registered_ = true;
144 void RemoveTokenServiceObserver() {
145 if (!token_observer_registered_)
147 DCHECK(browser_->profile());
148 ProfileOAuth2TokenService* oauth2_token_service =
149 ProfileOAuth2TokenServiceFactory::GetForProfile(browser_->profile());
150 DCHECK(oauth2_token_service);
151 oauth2_token_service->RemoveObserver(this);
152 token_observer_registered_ = false;
155 // OAuth2TokenService::Observer:
156 virtual void OnRefreshTokenAvailable(const std::string& account_id) OVERRIDE {
157 // Tokens can only be added by adding an account through the inline flow,
158 // which is started from the account management view. Refresh it to show the
160 BubbleViewMode viewMode = [controller_ viewMode];
161 if (viewMode == ACCOUNT_MANAGEMENT_VIEW ||
162 viewMode == GAIA_SIGNIN_VIEW ||
163 viewMode == GAIA_ADD_ACCOUNT_VIEW) {
164 [controller_ initMenuContentsWithView:ACCOUNT_MANAGEMENT_VIEW];
168 virtual void OnRefreshTokenRevoked(const std::string& account_id) OVERRIDE {
169 // Tokens can only be removed from the account management view. Refresh it
170 // to show the update.
171 if ([controller_ viewMode] == ACCOUNT_MANAGEMENT_VIEW)
172 [controller_ initMenuContentsWithView:ACCOUNT_MANAGEMENT_VIEW];
175 // AvatarMenuObserver:
176 virtual void OnAvatarMenuChanged(AvatarMenu* avatar_menu) OVERRIDE {
177 // While the bubble is open, the avatar menu can only change from the
178 // profile chooser view by modifying the current profile's photo or name.
179 [controller_ initMenuContentsWithView:PROFILE_CHOOSER_VIEW];
182 // content::NotificationObserver:
183 virtual void Observe(
185 const content::NotificationSource& source,
186 const content::NotificationDetails& details) OVERRIDE {
187 DCHECK_EQ(chrome::NOTIFICATION_BROWSER_CLOSING, type);
188 if (browser_ == content::Source<Browser>(source).ptr()) {
189 RemoveTokenServiceObserver();
190 // Clean up the bubble's WebContents (used by the Gaia embedded view), to
191 // make sure the guest profile doesn't have any dangling host renderers.
192 // This can happen if Chrome is quit using Command-Q while the bubble is
193 // still open, which won't give the bubble a chance to be closed and
194 // clean up the WebContents itself.
195 [controller_ cleanUpEmbeddedViewContents];
199 ProfileChooserController* controller_; // Weak; owns this.
200 Browser* browser_; // Weak.
201 content::NotificationRegistrar registrar_;
203 // The observer can be removed both when closing the browser, and by just
204 // closing the avatar bubble. However, in the case of closing the browser,
205 // the avatar bubble will also be closed afterwards, resulting in a second
206 // attempt to remove the observer. This ensures the observer is only
208 bool token_observer_registered_;
210 DISALLOW_COPY_AND_ASSIGN(ActiveProfileObserverBridge);
213 // A custom button that has a transparent backround.
214 @interface TransparentBackgroundButton : NSButton
217 @implementation TransparentBackgroundButton
218 - (id)initWithFrame:(NSRect)frameRect {
219 if ((self = [super initWithFrame:frameRect])) {
220 [self setBordered:NO];
221 [self setFont:[NSFont labelFontOfSize:kTextFontSize]];
222 [self setButtonType:NSMomentaryChangeButton];
227 - (void)drawRect:(NSRect)dirtyRect {
228 NSColor* backgroundColor = [NSColor colorWithCalibratedWhite:0 alpha:0.5f];
229 [backgroundColor setFill];
230 NSRectFillUsingOperation(dirtyRect, NSCompositeSourceAtop);
231 [super drawRect:dirtyRect];
235 // A custom image control that shows a "Change" button when moused over.
236 @interface EditableProfilePhoto : NSImageView {
238 AvatarMenu* avatarMenu_; // Weak; Owned by ProfileChooserController.
239 base::scoped_nsobject<TransparentBackgroundButton> changePhotoButton_;
240 // Used to display the "Change" button on hover.
241 ui::ScopedCrTrackingArea trackingArea_;
244 - (id)initWithFrame:(NSRect)frameRect
245 avatarMenu:(AvatarMenu*)avatarMenu
246 profileIcon:(const gfx::Image&)profileIcon
247 editingAllowed:(BOOL)editingAllowed;
249 // Called when the "Change" button is clicked.
250 - (void)editPhoto:(id)sender;
252 // When hovering over the profile photo, show the "Change" button.
253 - (void)mouseEntered:(NSEvent*)event;
255 // When hovering away from the profile photo, hide the "Change" button.
256 - (void)mouseExited:(NSEvent*)event;
259 @interface EditableProfilePhoto (Private)
260 // Create the "Change" avatar photo button.
261 - (TransparentBackgroundButton*)changePhotoButtonWithRect:(NSRect)rect;
264 @implementation EditableProfilePhoto
265 - (id)initWithFrame:(NSRect)frameRect
266 avatarMenu:(AvatarMenu*)avatarMenu
267 profileIcon:(const gfx::Image&)profileIcon
268 editingAllowed:(BOOL)editingAllowed {
269 if ((self = [super initWithFrame:frameRect])) {
270 avatarMenu_ = avatarMenu;
271 [self setImage:CreateProfileImage(
272 profileIcon, kLargeImageSide).ToNSImage()];
274 // Add a tracking area so that we can show/hide the button when hovering.
275 trackingArea_.reset([[CrTrackingArea alloc]
276 initWithRect:[self bounds]
277 options:NSTrackingMouseEnteredAndExited | NSTrackingActiveAlways
280 [self addTrackingArea:trackingArea_.get()];
282 if (editingAllowed) {
283 // The avatar photo uses a frame of width profiles::kAvatarIconPadding,
284 // which we must subtract from the button's bounds.
285 changePhotoButton_.reset([self changePhotoButtonWithRect:NSMakeRect(
286 profiles::kAvatarIconPadding, profiles::kAvatarIconPadding,
287 kLargeImageSide - 2 * profiles::kAvatarIconPadding,
289 [self addSubview:changePhotoButton_];
291 // Hide the button until the image is hovered over.
292 [changePhotoButton_ setHidden:YES];
298 - (void)editPhoto:(id)sender {
299 avatarMenu_->EditProfile(avatarMenu_->GetActiveProfileIndex());
302 - (void)mouseEntered:(NSEvent*)event {
303 [changePhotoButton_ setHidden:NO];
306 - (void)mouseExited:(NSEvent*)event {
307 [changePhotoButton_ setHidden:YES];
310 - (TransparentBackgroundButton*)changePhotoButtonWithRect:(NSRect)rect {
311 TransparentBackgroundButton* button =
312 [[TransparentBackgroundButton alloc] initWithFrame:rect];
314 // The button has a centered white text and a transparent background.
315 base::scoped_nsobject<NSMutableParagraphStyle> textStyle(
316 [[NSMutableParagraphStyle alloc] init]);
317 [textStyle setAlignment:NSCenterTextAlignment];
318 NSDictionary* titleAttributes = @{
319 NSParagraphStyleAttributeName : textStyle,
320 NSForegroundColorAttributeName : [NSColor whiteColor]
322 NSString* buttonTitle = l10n_util::GetNSString(
323 IDS_PROFILES_PROFILE_CHANGE_PHOTO_BUTTON);
324 base::scoped_nsobject<NSAttributedString> attributedTitle(
325 [[NSAttributedString alloc] initWithString:buttonTitle
326 attributes:titleAttributes]);
327 [button setAttributedTitle:attributedTitle];
328 [button setTarget:self];
329 [button setAction:@selector(editPhoto:)];
334 // A custom text control that turns into a textfield for editing when clicked.
335 @interface EditableProfileNameButton : HoverImageButton<NSTextFieldDelegate> {
337 base::scoped_nsobject<NSTextField> profileNameTextField_;
338 Profile* profile_; // Weak.
341 - (id)initWithFrame:(NSRect)frameRect
342 profile:(Profile*)profile
343 profileName:(NSString*)profileName
344 editingAllowed:(BOOL)editingAllowed;
346 // Called when the button is clicked.
347 - (void)showEditableView:(id)sender;
349 // Called when the user presses "Enter" in the textfield.
350 - (void)controlTextDidEndEditing:(NSNotification *)obj;
353 @implementation EditableProfileNameButton
354 - (id)initWithFrame:(NSRect)frameRect
355 profile:(Profile*)profile
356 profileName:(NSString*)profileName
357 editingAllowed:(BOOL)editingAllowed {
358 if ((self = [super initWithFrame:frameRect])) {
361 [self setBordered:NO];
362 [self setFont:[NSFont labelFontOfSize:kTitleFontSize]];
363 [self setAlignment:NSLeftTextAlignment];
364 [[self cell] setLineBreakMode:NSLineBreakByTruncatingTail];
365 [self setTitle:profileName];
367 if (editingAllowed) {
368 // Show an "edit" pencil icon when hovering over.
369 ui::ResourceBundle* rb = &ui::ResourceBundle::GetSharedInstance();
371 rb->GetNativeImageNamed(IDR_ICON_PROFILES_EDIT_HOVER).AsNSImage()];
372 [self setAlternateImage:
373 rb->GetNativeImageNamed(IDR_ICON_PROFILES_EDIT_PRESSED).AsNSImage()];
374 [self setImagePosition:NSImageRight];
375 [self setTarget:self];
376 [self setAction:@selector(showEditableView:)];
378 // We need to subtract the width of the bezel from the frame rect, so that
379 // the textfield can take the exact same space as the button.
380 frameRect.size.height -= 2 * kBezelThickness;
381 frameRect.origin = NSMakePoint(0, kBezelThickness);
382 profileNameTextField_.reset(
383 [[NSTextField alloc] initWithFrame:frameRect]);
384 [profileNameTextField_ setStringValue:profileName];
385 [profileNameTextField_ setFont:[NSFont labelFontOfSize:kTitleFontSize]];
386 [profileNameTextField_ setEditable:YES];
387 [profileNameTextField_ setDrawsBackground:YES];
388 [profileNameTextField_ setBezeled:YES];
389 [[profileNameTextField_ cell] setWraps:NO];
390 [[profileNameTextField_ cell] setLineBreakMode:
391 NSLineBreakByTruncatingTail];
392 [profileNameTextField_ setDelegate:self];
393 [self addSubview:profileNameTextField_];
395 // Hide the textfield until the user clicks on the button.
396 [profileNameTextField_ setHidden:YES];
402 // NSTextField objects send an NSNotification to a delegate if
403 // it implements this method:
404 - (void)controlTextDidEndEditing:(NSNotification *)obj {
405 NSString* text = [profileNameTextField_ stringValue];
406 // Empty profile names are not allowed, and are treated as a cancel.
407 if ([text length] > 0) {
408 profiles::UpdateProfileName(profile_, base::SysNSStringToUTF16(text));
409 [self setTitle:text];
411 [profileNameTextField_ setHidden:YES];
412 [profileNameTextField_ resignFirstResponder];
415 - (void)showEditableView:(id)sender {
416 [profileNameTextField_ setHidden:NO];
417 [profileNameTextField_ becomeFirstResponder];
422 // Custom button cell that adds a left padding before the button image, and
423 // a custom spacing between the button image and title.
424 @interface CustomPaddingImageButtonCell : NSButtonCell {
426 // Padding between the left margin of the button and the cell image.
427 int leftMarginSpacing_;
428 // Spacing between the cell image and title.
429 int imageTitleSpacing_;
432 - (id)initWithLeftMarginSpacing:(int)leftMarginSpacing
433 imageTitleSpacing:(int)imageTitleSpacing;
436 @implementation CustomPaddingImageButtonCell
437 - (id)initWithLeftMarginSpacing:(int)leftMarginSpacing
438 imageTitleSpacing:(int)imageTitleSpacing {
439 if ((self = [super init])) {
440 leftMarginSpacing_ = leftMarginSpacing;
441 imageTitleSpacing_ = imageTitleSpacing;
446 - (NSRect)drawTitle:(NSAttributedString*)title
447 withFrame:(NSRect)frame
448 inView:(NSView*)controlView {
449 // The title frame origin isn't aware of the left margin spacing added
450 // in -drawImage, so it must be added when drawing the title as well.
451 frame.origin.x += leftMarginSpacing_ + imageTitleSpacing_;
452 return [super drawTitle:title withFrame:frame inView:controlView];
455 - (void)drawImage:(NSImage*)image
456 withFrame:(NSRect)frame
457 inView:(NSView*)controlView {
458 frame.origin.x = leftMarginSpacing_;
459 [super drawImage:image withFrame:frame inView:controlView];
463 NSSize buttonSize = [super cellSize];
464 buttonSize.width += leftMarginSpacing_ + imageTitleSpacing_;
470 // A custom button that allows for setting a background color when hovered over.
471 @interface BackgroundColorHoverButton : HoverImageButton
474 @implementation BackgroundColorHoverButton
475 - (id)initWithFrame:(NSRect)frameRect {
476 if ((self = [super initWithFrame:frameRect])) {
477 [self setBordered:NO];
478 [self setFont:[NSFont labelFontOfSize:kTextFontSize]];
479 [self setButtonType:NSMomentaryChangeButton];
481 base::scoped_nsobject<CustomPaddingImageButtonCell> cell(
482 [[CustomPaddingImageButtonCell alloc]
483 initWithLeftMarginSpacing:kHorizontalSpacing
484 imageTitleSpacing:kImageTitleSpacing]);
485 [self setCell:cell.get()];
490 - (void)setHoverState:(HoverState)state {
491 [super setHoverState:state];
492 bool isHighlighted = ([self hoverState] != kHoverStateNone);
494 NSColor* backgroundColor = gfx::SkColorToCalibratedNSColor(
495 ui::NativeTheme::instance()->GetSystemColor(isHighlighted?
496 ui::NativeTheme::kColorId_MenuSeparatorColor :
497 ui::NativeTheme::kColorId_DialogBackground));
499 [[self cell] setBackgroundColor:backgroundColor];
505 @interface ProfileChooserController ()
506 // Creates a tutorial card for the profile |avatar_item| if needed.
507 - (NSView*)createTutorialViewIfNeeded:(const AvatarMenu::Item&)item;
509 // Creates the main profile card for the profile |item| at the top of
511 - (NSView*)createCurrentProfileView:(const AvatarMenu::Item&)item;
513 // Creates the possible links for the main profile card with profile |item|.
514 - (NSView*)createCurrentProfileLinksForItem:(const AvatarMenu::Item&)item
515 withXOffset:(CGFloat)xOffset;
517 // Creates a main profile card for the guest user.
518 - (NSView*)createGuestProfileView;
520 // Creates an item for the profile |itemIndex| that is used in the fast profile
521 // switcher in the middle of the bubble.
522 - (NSButton*)createOtherProfileView:(int)itemIndex;
524 // Creates the "Not you" and Lock option buttons.
525 - (NSView*)createOptionsViewWithRect:(NSRect)rect
526 enableLock:(BOOL)enableLock;
528 // Creates the account management view for the active profile.
529 - (NSView*)createCurrentProfileAccountsView:(NSRect)rect;
531 // Creates the list of accounts for the active profile.
532 - (NSView*)createAccountsListWithRect:(NSRect)rect;
534 // Creates the Gaia sign-in/add account view.
535 - (NSView*)createGaiaEmbeddedView;
537 // Creates a button with |text|, an icon given by |imageResourceId| and with
538 // |action|. The icon |alternateImageResourceId| is displayed in the button's
539 // hovered and pressed states.
540 - (NSButton*)hoverButtonWithRect:(NSRect)rect
542 imageResourceId:(int)imageResourceId
543 alternateImageResourceId:(int)alternateImageResourceId
546 // Creates a generic link button with |title| and an |action| positioned at
548 - (NSButton*)linkButtonWithTitle:(NSString*)title
549 frameOrigin:(NSPoint)frameOrigin
552 // Creates an email account button with |title|. If |canBeDeleted| is YES, then
553 // the button is clickable and has a remove icon.
554 - (NSButton*)accountButtonWithRect:(NSRect)rect
555 title:(const std::string&)title
556 canBeDeleted:(BOOL)canBeDeleted;
558 - (NSTextField*)labelWithTitle:(NSString*)title
559 frameOrigin:(NSPoint)frameOrigin;
563 @implementation ProfileChooserController
564 - (BubbleViewMode) viewMode {
568 - (IBAction)switchToProfile:(id)sender {
569 // Check the event flags to see if a new window should be created.
570 bool alwaysCreate = ui::WindowOpenDispositionFromNSEvent(
571 [NSApp currentEvent]) == NEW_WINDOW;
572 avatarMenu_->SwitchToProfile([sender tag], alwaysCreate,
573 ProfileMetrics::SWITCH_PROFILE_ICON);
576 - (IBAction)showUserManager:(id)sender {
577 // Guest users cannot appear in the User Manager, nor display a tutorial.
578 profiles::ShowUserManagerMaybeWithTutorial(
579 isGuestSession_ ? NULL : browser_->profile());
582 - (IBAction)showAccountManagement:(id)sender {
583 [self initMenuContentsWithView:ACCOUNT_MANAGEMENT_VIEW];
586 - (IBAction)lockProfile:(id)sender {
587 profiles::LockProfile(browser_->profile());
590 - (IBAction)showSigninPage:(id)sender {
591 [self initMenuContentsWithView:GAIA_SIGNIN_VIEW];
594 - (IBAction)addAccount:(id)sender {
595 [self initMenuContentsWithView:GAIA_ADD_ACCOUNT_VIEW];
598 - (IBAction)removeAccount:(id)sender {
599 DCHECK(!isGuestSession_);
600 DCHECK_GE([sender tag], 0); // Should not be called for the primary account.
601 DCHECK(ContainsKey(currentProfileAccounts_, [sender tag]));
602 std::string account = currentProfileAccounts_[[sender tag]];
603 ProfileOAuth2TokenServiceFactory::GetPlatformSpecificForProfile(
604 browser_->profile())->RevokeCredentials(account);
607 - (IBAction)openTutorialLearnMoreURL:(id)sender {
608 // TODO(guohui): update |learnMoreUrl| once it is decided.
609 const GURL learnMoreUrl("https://support.google.com/chrome/?hl=en#to");
610 chrome::NavigateParams params(browser_->profile(), learnMoreUrl,
611 content::PAGE_TRANSITION_LINK);
612 params.disposition = NEW_FOREGROUND_TAB;
613 chrome::Navigate(¶ms);
616 - (IBAction)dismissTutorial:(id)sender {
617 // If the user manually dismissed the tutorial, never show it again by setting
618 // the number of times shown to the maximum plus 1, so that later we could
619 // distinguish between the dismiss case and the case when the tutorial is
620 // indeed shown for the maximum number of times.
621 browser_->profile()->GetPrefs()->SetInteger(
622 prefs::kProfileAvatarTutorialShown, kProfileAvatarTutorialShowMax + 1);
623 [self initMenuContentsWithView:PROFILE_CHOOSER_VIEW];
626 - (void)cleanUpEmbeddedViewContents {
627 webContents_.reset();
630 - (id)initWithBrowser:(Browser*)browser
631 anchoredAt:(NSPoint)point
632 withMode:(BubbleViewMode)mode {
633 base::scoped_nsobject<InfoBubbleWindow> window([[InfoBubbleWindow alloc]
634 initWithContentRect:ui::kWindowSizeDeterminedLater
635 styleMask:NSBorderlessWindowMask
636 backing:NSBackingStoreBuffered
639 if ((self = [super initWithWindow:window
640 parentWindow:browser->window()->GetNativeWindow()
641 anchoredAt:point])) {
644 tutorialShowing_ = false;
645 observer_.reset(new ActiveProfileObserverBridge(self, browser_));
647 avatarMenu_.reset(new AvatarMenu(
648 &g_browser_process->profile_manager()->GetProfileInfoCache(),
651 avatarMenu_->RebuildMenu();
653 // Guest profiles do not have a token service.
654 isGuestSession_ = browser_->profile()->IsGuestSession();
656 ui::NativeTheme* nativeTheme = ui::NativeTheme::instance();
657 [[self bubble] setAlignment:info_bubble::kAlignRightEdgeToAnchorEdge];
658 [[self bubble] setArrowLocation:info_bubble::kNoArrow];
659 [[self bubble] setBackgroundColor:
660 gfx::SkColorToCalibratedNSColor(nativeTheme->GetSystemColor(
661 ui::NativeTheme::kColorId_DialogBackground))];
662 [self initMenuContentsWithView:viewMode_];
668 - (void)initMenuContentsWithView:(BubbleViewMode)viewToDisplay {
669 viewMode_ = viewToDisplay;
670 NSView* contentView = [[self window] contentView];
671 [contentView setSubviews:[NSArray array]];
673 if (viewMode_ == GAIA_SIGNIN_VIEW || viewMode_ == GAIA_ADD_ACCOUNT_VIEW) {
674 [contentView addSubview:[self createGaiaEmbeddedView]];
675 SetWindowSize([self window],
676 NSMakeSize(kMinGaiaViewWidth, kMinGaiaViewHeight));
680 NSView* tutorialView = nil;
681 NSView* currentProfileView = nil;
682 base::scoped_nsobject<NSMutableArray> otherProfiles(
683 [[NSMutableArray alloc] init]);
684 // Local and guest profiles cannot lock their profile.
685 bool enableLock = false;
687 // Loop over the profiles in reverse, so that they are sorted by their
688 // y-coordinate, and separate them into active and "other" profiles.
689 for (int i = avatarMenu_->GetNumberOfItems() - 1; i >= 0; --i) {
690 const AvatarMenu::Item& item = avatarMenu_->GetItemAt(i);
692 if (viewMode_ == PROFILE_CHOOSER_VIEW)
693 tutorialView = [self createTutorialViewIfNeeded:item];
694 currentProfileView = [self createCurrentProfileView:item];
695 enableLock = item.signed_in;
697 [otherProfiles addObject:[self createOtherProfileView:i]];
700 if (!currentProfileView) // Guest windows don't have an active profile.
701 currentProfileView = [self createGuestProfileView];
703 // |yOffset| is the next position at which to draw in |contentView|
705 CGFloat yOffset = kSmallVerticalSpacing;
708 NSView* optionsView = [self createOptionsViewWithRect:
709 NSMakeRect(0, yOffset, kFixedMenuWidth, 0)
710 enableLock:enableLock];
711 [contentView addSubview:optionsView];
712 yOffset = NSMaxY([optionsView frame]) + kSmallVerticalSpacing;
714 NSBox* separator = [self separatorWithFrame:
715 NSMakeRect(0, yOffset, kFixedMenuWidth, 0)];
716 [contentView addSubview:separator];
717 yOffset = NSMaxY([separator frame]) + kVerticalSpacing;
719 if (viewToDisplay == PROFILE_CHOOSER_VIEW &&
720 switches::IsFastUserSwitching()) {
721 // Other profiles switcher. The profiles have already been sorted
722 // by their y-coordinate, so they can be added in the existing order.
723 for (NSView *otherProfileView in otherProfiles.get()) {
724 [otherProfileView setFrameOrigin:NSMakePoint(kHorizontalSpacing,
726 [contentView addSubview:otherProfileView];
727 yOffset = NSMaxY([otherProfileView frame]) + kSmallVerticalSpacing;
730 // If we displayed other profiles, ensure the spacing between the last item
731 // and the active profile card is the same as the spacing between the active
732 // profile card and the bottom of the bubble.
733 if ([otherProfiles.get() count] > 0)
734 yOffset += kSmallVerticalSpacing;
735 } else if (viewToDisplay == ACCOUNT_MANAGEMENT_VIEW) {
736 NSView* currentProfileAccountsView = [self createCurrentProfileAccountsView:
737 NSMakeRect(kHorizontalSpacing,
739 kFixedMenuWidth - 2 * kHorizontalSpacing,
741 [contentView addSubview:currentProfileAccountsView];
742 yOffset = NSMaxY([currentProfileAccountsView frame]) + kVerticalSpacing;
744 NSBox* accountsSeparator = [self separatorWithFrame:
745 NSMakeRect(0, yOffset, kFixedMenuWidth, 0)];
746 [contentView addSubview:accountsSeparator];
747 yOffset = NSMaxY([accountsSeparator frame]) + kVerticalSpacing;
750 // Active profile card.
751 if (currentProfileView) {
752 [currentProfileView setFrameOrigin:NSMakePoint(kHorizontalSpacing,
754 [contentView addSubview:currentProfileView];
755 yOffset = NSMaxY([currentProfileView frame]) + kVerticalSpacing;
759 NSBox* accountsSeparator = [self separatorWithFrame:
760 NSMakeRect(0, yOffset, kFixedMenuWidth, 0)];
761 [contentView addSubview:accountsSeparator];
762 yOffset = NSMaxY([accountsSeparator frame]) + kVerticalSpacing;
764 [tutorialView setFrameOrigin:NSMakePoint(kHorizontalSpacing,
766 [contentView addSubview:tutorialView];
767 yOffset = NSMaxY([tutorialView frame]) + kVerticalSpacing;
769 tutorialShowing_ = false;
772 SetWindowSize([self window], NSMakeSize(kFixedMenuWidth, yOffset));
775 - (NSView*)createTutorialViewIfNeeded:(const AvatarMenu::Item&)item {
779 Profile* profile = browser_->profile();
780 const int showCount = profile->GetPrefs()->GetInteger(
781 prefs::kProfileAvatarTutorialShown);
782 // Do not show the tutorial if user has dismissed it.
783 if (showCount > kProfileAvatarTutorialShowMax)
786 if (!tutorialShowing_) {
787 if (showCount == kProfileAvatarTutorialShowMax)
789 profile->GetPrefs()->SetInteger(
790 prefs::kProfileAvatarTutorialShown, showCount + 1);
791 tutorialShowing_ = true;
794 CGFloat availableWidth = kFixedMenuWidth - 2 * kHorizontalSpacing;
795 base::scoped_nsobject<NSView> container([[NSView alloc]
796 initWithFrame:NSMakeRect(0, 0, availableWidth, 0)]);
799 // Adds links and buttons at the bottom.
800 NSButton* learnMoreLink =
801 [self linkButtonWithTitle:l10n_util::GetNSString(IDS_LEARN_MORE)
802 frameOrigin:NSMakePoint(0, yOffset)
803 action:@selector(openTutorialLearnMoreURL:)];
804 [container addSubview:learnMoreLink];
806 base::scoped_nsobject<NSButton> tutorialOkButton([[BlueLabelButton alloc]
807 initWithFrame:NSZeroRect]);
808 [tutorialOkButton setTitle:l10n_util::GetNSString(
809 IDS_PROFILES_SIGNIN_TUTORIAL_OK_BUTTON)];
810 [tutorialOkButton setTarget:self];
811 [tutorialOkButton setAction:@selector(dismissTutorial:)];
812 [tutorialOkButton sizeToFit];
813 [tutorialOkButton setFrameOrigin:NSMakePoint(
814 availableWidth - NSWidth([tutorialOkButton frame]), yOffset)];
815 [container addSubview:tutorialOkButton];
817 yOffset = std::max(NSMaxY([learnMoreLink frame]),
818 NSMaxY([tutorialOkButton frame])) + kVerticalSpacing;
820 // Adds body content consisting of three bulleted lines.
821 const int kTextHorizIndentation = 10;
822 NSTextField* bulletLabel =
823 [self labelWithTitle:l10n_util::GetNSString(
824 IDS_PROFILES_SIGNIN_TUTORIAL_CONTENT_TEXT)
825 frameOrigin:NSMakePoint(kTextHorizIndentation, yOffset)];
826 [bulletLabel setFrameSize:NSMakeSize(availableWidth,
827 NSHeight([bulletLabel frame]))];
828 [container addSubview:bulletLabel];
829 yOffset = NSMaxY([bulletLabel frame]) + kSmallVerticalSpacing;
832 NSTextField* contentHeaderLabel =
833 [self labelWithTitle:l10n_util::GetNSString(
834 IDS_PROFILES_SIGNIN_TUTORIAL_CONTENT_HEADER)
835 frameOrigin:NSMakePoint(0, yOffset)];
836 [contentHeaderLabel setFrameSize:NSMakeSize(availableWidth, 0)];
837 [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:
839 [container addSubview:contentHeaderLabel];
840 yOffset = NSMaxY([contentHeaderLabel frame]) + kSmallVerticalSpacing;
843 NSTextField* titleLabel =
844 [self labelWithTitle:l10n_util::GetNSStringF(
845 IDS_PROFILES_SIGNIN_TUTORIAL_TITLE,
846 profiles::GetAvatarNameForProfile(profile))
847 frameOrigin:NSMakePoint(0, yOffset)];
848 [titleLabel setFont:[NSFont labelFontOfSize:kTitleFontSize]];
849 [[titleLabel cell] setTextColor:
850 gfx::SkColorToCalibratedNSColor(chrome_style::GetLinkColor())];
851 [titleLabel sizeToFit];
852 [titleLabel setFrameSize:
853 NSMakeSize(availableWidth, NSHeight([titleLabel frame]))];
854 [container addSubview:titleLabel];
855 yOffset = NSMaxY([titleLabel frame]);
857 [container setFrameSize:NSMakeSize(kFixedMenuWidth, yOffset)];
858 return container.autorelease();
861 - (NSView*)createCurrentProfileView:(const AvatarMenu::Item&)item {
862 base::scoped_nsobject<NSView> container([[NSView alloc]
863 initWithFrame:NSZeroRect]);
866 base::scoped_nsobject<EditableProfilePhoto> iconView(
867 [[EditableProfilePhoto alloc]
868 initWithFrame:NSMakeRect(0, 0, kLargeImageSide, kLargeImageSide)
869 avatarMenu:avatarMenu_.get()
870 profileIcon:item.icon
871 editingAllowed:!isGuestSession_]);
873 [container addSubview:iconView];
875 CGFloat xOffset = NSMaxX([iconView frame]) + kHorizontalSpacing;
876 CGFloat yOffset = kVerticalSpacing;
877 if (!isGuestSession_ && viewMode_ == PROFILE_CHOOSER_VIEW) {
878 NSView* linksContainer =
879 [self createCurrentProfileLinksForItem:item withXOffset:xOffset];
880 [container addSubview:linksContainer];
881 yOffset = NSMaxY([linksContainer frame]);
885 CGFloat availableTextWidth =
886 kFixedMenuWidth - xOffset - 2 * kHorizontalSpacing;
887 base::scoped_nsobject<EditableProfileNameButton> profileName(
888 [[EditableProfileNameButton alloc]
889 initWithFrame:NSMakeRect(xOffset, yOffset,
891 kProfileButtonHeight)
892 profile:browser_->profile()
893 profileName:base::SysUTF16ToNSString(
894 profiles::GetAvatarNameForProfile(browser_->profile()))
895 editingAllowed:!isGuestSession_]);
897 [container addSubview:profileName];
898 [container setFrameSize:NSMakeSize(kFixedMenuWidth,
899 NSHeight([iconView frame]))];
900 return container.autorelease();
903 - (NSView*)createCurrentProfileLinksForItem:(const AvatarMenu::Item&)item
904 withXOffset:(CGFloat)xOffset {
905 base::scoped_nsobject<NSView> container([[NSView alloc]
906 initWithFrame:NSZeroRect]);
909 NSPoint frameOrigin = NSMakePoint(xOffset, kSmallVerticalSpacing);
910 // The available links depend on the type of profile that is active.
911 if (item.signed_in) {
912 link = [self linkButtonWithTitle:l10n_util::GetNSString(
913 IDS_PROFILES_PROFILE_MANAGE_ACCOUNTS_BUTTON)
914 frameOrigin:frameOrigin
915 action:@selector(showAccountManagement:)];
917 link = [self linkButtonWithTitle:l10n_util::GetNSStringFWithFixup(
918 IDS_SYNC_START_SYNC_BUTTON_LABEL,
919 l10n_util::GetStringUTF16(IDS_SHORT_PRODUCT_NAME))
920 frameOrigin:frameOrigin
921 action:@selector(showSigninPage:)];
924 [container addSubview:link];
925 [container setFrameSize:NSMakeSize(
926 NSMaxX([link frame]), NSMaxY([link frame]) + kSmallVerticalSpacing)];
927 return container.autorelease();
930 - (NSView*)createGuestProfileView {
931 gfx::Image guestIcon =
932 ui::ResourceBundle::GetSharedInstance().GetNativeImageNamed(
934 AvatarMenu::Item guestItem(std::string::npos, /* menu_index, not used */
935 std::string::npos, /* profile_index, not used */
937 guestItem.active = true;
938 guestItem.name = base::SysNSStringToUTF16(
939 l10n_util::GetNSString(IDS_PROFILES_GUEST_PROFILE_NAME));
941 return [self createCurrentProfileView:guestItem];
944 - (NSButton*)createOtherProfileView:(int)itemIndex {
945 const AvatarMenu::Item& item = avatarMenu_->GetItemAt(itemIndex);
946 base::scoped_nsobject<NSButton> profileButton([[NSButton alloc]
947 initWithFrame:NSZeroRect]);
948 base::scoped_nsobject<CustomPaddingImageButtonCell> cell(
949 [[CustomPaddingImageButtonCell alloc]
950 initWithLeftMarginSpacing:0
951 imageTitleSpacing:kImageTitleSpacing]);
952 [profileButton setCell:cell.get()];
954 [[profileButton cell] setLineBreakMode:NSLineBreakByTruncatingTail];
955 [profileButton setTitle:base::SysUTF16ToNSString(item.name)];
956 [profileButton setImage:CreateProfileImage(
957 item.icon, kSmallImageSide).ToNSImage()];
958 [profileButton setImagePosition:NSImageLeft];
959 [profileButton setAlignment:NSLeftTextAlignment];
960 [profileButton setBordered:NO];
961 [profileButton setFont:[NSFont labelFontOfSize:kTitleFontSize]];
962 [profileButton setTag:itemIndex];
963 [profileButton setTarget:self];
964 [profileButton setAction:@selector(switchToProfile:)];
966 // Since the bubble is fixed width, we need to calculate the width available
967 // for the profile name, as longer names will have to be elided.
968 CGFloat availableTextWidth = kFixedMenuWidth - 2 * kHorizontalSpacing;
969 [profileButton sizeToFit];
970 [profileButton setFrameSize:NSMakeSize(availableTextWidth,
971 NSHeight([profileButton frame]))];
973 return profileButton.autorelease();
976 - (NSView*)createOptionsViewWithRect:(NSRect)rect
977 enableLock:(BOOL)enableLock {
978 int widthOfLockButton = enableLock? 2 * kHorizontalSpacing + 12 : 0;
979 NSRect viewRect = NSMakeRect(0, 0,
980 rect.size.width - widthOfLockButton,
982 NSButton* notYouButton =
983 [self hoverButtonWithRect:viewRect
984 text:l10n_util::GetNSStringF(
985 IDS_PROFILES_NOT_YOU_BUTTON,
986 profiles::GetAvatarNameForProfile(browser_->profile()))
987 imageResourceId:IDR_ICON_PROFILES_MENU_AVATAR
988 alternateImageResourceId:IDR_ICON_PROFILES_MENU_AVATAR
989 action:@selector(showUserManager:)];
991 rect.size.height = NSMaxY([notYouButton frame]);
992 base::scoped_nsobject<NSView> container([[NSView alloc]
993 initWithFrame:rect]);
994 [container addSubview:notYouButton];
997 viewRect.origin.x = NSMaxX([notYouButton frame]);
998 viewRect.size.width = widthOfLockButton;
999 NSButton* lockButton =
1000 [self hoverButtonWithRect:viewRect
1002 imageResourceId:IDR_ICON_PROFILES_MENU_LOCK
1003 alternateImageResourceId:IDR_ICON_PROFILES_MENU_LOCK
1004 action:@selector(lockProfile:)];
1005 [container addSubview:lockButton];
1008 return container.autorelease();
1011 - (NSView*)createCurrentProfileAccountsView:(NSRect)rect {
1012 const CGFloat kAccountButtonHeight = 15;
1014 const AvatarMenu::Item& item =
1015 avatarMenu_->GetItemAt(avatarMenu_->GetActiveProfileIndex());
1016 DCHECK(item.signed_in);
1018 base::scoped_nsobject<NSView> container([[NSView alloc] initWithFrame:rect]);
1020 NSRect viewRect = NSMakeRect(0, 0, rect.size.width, kBlueButtonHeight);
1021 base::scoped_nsobject<NSButton> addAccountsButton([[BlueLabelButton alloc]
1022 initWithFrame:viewRect]);
1024 // Manually elide the button text so that the contents fit inside the bubble.
1025 // This is needed because the BlueLabelButton cell resets the style on
1026 // every call to -cellSize, which prevents setting a custom lineBreakMode.
1027 NSString* elidedButtonText = base::SysUTF16ToNSString(gfx::ElideText(
1028 l10n_util::GetStringFUTF16(
1029 IDS_PROFILES_PROFILE_ADD_ACCOUNT_BUTTON, item.name),
1030 ui::ResourceBundle::GetSharedInstance().GetFontList(
1031 ui::ResourceBundle::BaseFont),
1033 gfx::ELIDE_AT_END));
1035 [addAccountsButton setTitle:elidedButtonText];
1036 [addAccountsButton setTarget:self];
1037 [addAccountsButton setAction:@selector(addAccount:)];
1038 [container addSubview:addAccountsButton];
1040 // Update the height of the email account buttons. This is needed so that the
1041 // all the buttons span the entire width of the bubble.
1042 viewRect.origin.y = NSMaxY([addAccountsButton frame]) + kVerticalSpacing;
1043 viewRect.size.height = kAccountButtonHeight;
1045 NSView* accountEmails = [self createAccountsListWithRect:viewRect];
1046 [container addSubview:accountEmails];
1047 [container setFrameSize:NSMakeSize(
1048 NSWidth([container frame]), NSMaxY([accountEmails frame]))];
1049 return container.autorelease();
1052 - (NSView*)createAccountsListWithRect:(NSRect)rect {
1053 base::scoped_nsobject<NSView> container([[NSView alloc] initWithFrame:rect]);
1054 currentProfileAccounts_.clear();
1056 Profile* profile = browser_->profile();
1057 std::string primaryAccount =
1058 SigninManagerFactory::GetForProfile(profile)->GetAuthenticatedUsername();
1059 DCHECK(!primaryAccount.empty());
1060 std::vector<std::string>accounts =
1061 profiles::GetSecondaryAccountsForProfile(profile, primaryAccount);
1064 for (size_t i = 0; i < accounts.size(); ++i) {
1065 // Save the original email address, as the button text could be elided.
1066 currentProfileAccounts_[i] = accounts[i];
1067 NSButton* accountButton = [self accountButtonWithRect:rect
1070 [accountButton setTag:i];
1071 [container addSubview:accountButton];
1072 rect.origin.y = NSMaxY([accountButton frame]) + kSmallVerticalSpacing;
1075 // The primary account should always be listed first. It doesn't need a tag,
1076 // as it cannot be removed.
1077 // TODO(rogerta): we still need to further differentiate the primary account
1078 // from the others in the UI, so more work is likely required here:
1079 // crbug.com/311124.
1080 NSButton* accountButton = [self accountButtonWithRect:rect
1081 title:primaryAccount
1082 canBeDeleted:false];
1083 [container addSubview:accountButton];
1084 [container setFrameSize:NSMakeSize(NSWidth([container frame]),
1085 NSMaxY([accountButton frame]))];
1086 return container.autorelease();
1089 - (NSView*) createGaiaEmbeddedView {
1090 signin::Source source = (viewMode_ == GAIA_SIGNIN_VIEW) ?
1091 signin::SOURCE_AVATAR_BUBBLE_SIGN_IN :
1092 signin::SOURCE_AVATAR_BUBBLE_ADD_ACCOUNT;
1094 webContents_.reset(content::WebContents::Create(
1095 content::WebContents::CreateParams(browser_->profile())));
1096 webContents_->GetController().LoadURL(
1097 signin::GetPromoURL(
1098 source, false /* auto_close */, true /* is_constrained */),
1099 content::Referrer(),
1100 content::PAGE_TRANSITION_AUTO_TOPLEVEL,
1102 NSView* webview = webContents_->GetView()->GetNativeView();
1103 [webview setFrameSize:NSMakeSize(kMinGaiaViewWidth, kMinGaiaViewHeight)];
1107 - (NSButton*)hoverButtonWithRect:(NSRect)rect
1108 text:(NSString*)text
1109 imageResourceId:(int)imageResourceId
1110 alternateImageResourceId:(int)alternateImageResourceId
1111 action:(SEL)action {
1112 base::scoped_nsobject<BackgroundColorHoverButton> button(
1113 [[BackgroundColorHoverButton alloc] initWithFrame:rect]);
1115 [button setTitle:text];
1116 ui::ResourceBundle* rb = &ui::ResourceBundle::GetSharedInstance();
1117 NSImage* alternateImage = rb->GetNativeImageNamed(
1118 alternateImageResourceId).ToNSImage();
1119 [button setDefaultImage:rb->GetNativeImageNamed(imageResourceId).ToNSImage()];
1120 [button setHoverImage:alternateImage];
1121 [button setPressedImage:alternateImage];
1122 [button setImagePosition:NSImageLeft];
1123 [button setAlignment:NSLeftTextAlignment];
1124 [button setBordered:NO];
1125 [button setTarget:self];
1126 [button setAction:action];
1128 return button.autorelease();
1131 - (NSButton*)linkButtonWithTitle:(NSString*)title
1132 frameOrigin:(NSPoint)frameOrigin
1133 action:(SEL)action {
1134 base::scoped_nsobject<NSButton> link(
1135 [[HyperlinkButtonCell buttonWithString:title] retain]);
1137 [[link cell] setShouldUnderline:NO];
1138 [[link cell] setTextColor:gfx::SkColorToCalibratedNSColor(
1139 chrome_style::GetLinkColor())];
1140 [link setTitle:title];
1141 [link setBordered:NO];
1142 [link setFont:[NSFont labelFontOfSize:kTextFontSize]];
1143 [link setTarget:self];
1144 [link setAction:action];
1145 [link setFrameOrigin:frameOrigin];
1148 return link.autorelease();
1151 - (NSButton*)accountButtonWithRect:(NSRect)rect
1152 title:(const std::string&)title
1153 canBeDeleted:(BOOL)canBeDeleted {
1154 base::scoped_nsobject<NSButton> button([[NSButton alloc] initWithFrame:rect]);
1155 [button setTitle:ElideEmail(title, rect.size.width)];
1156 [button setAlignment:NSLeftTextAlignment];
1157 [button setBordered:NO];
1160 [button setImage:ui::ResourceBundle::GetSharedInstance().
1161 GetNativeImageNamed(IDR_CLOSE_1).ToNSImage()];
1162 [button setImagePosition:NSImageRight];
1163 [button setTarget:self];
1164 [button setAction:@selector(removeAccount:)];
1167 return button.autorelease();
1170 - (NSTextField*)labelWithTitle:(NSString*)title
1171 frameOrigin:(NSPoint)frameOrigin {
1172 base::scoped_nsobject<NSTextField> label(
1173 [[NSTextField alloc] initWithFrame:NSZeroRect]);
1174 [label setStringValue:title];
1175 [label setEditable:NO];
1176 [label setAlignment:NSLeftTextAlignment];
1177 [label setBezeled:NO];
1178 [label setFont:[NSFont labelFontOfSize:kTextFontSize]];
1179 [label setFrameOrigin:frameOrigin];
1182 return label.autorelease();