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. 4 5#import <Cocoa/Cocoa.h> 6 7#import "chrome/browser/ui/cocoa/profiles/profile_chooser_controller.h" 8 9#include "base/mac/bundle_locations.h" 10#include "base/prefs/pref_service.h" 11#include "base/strings/sys_string_conversions.h" 12#include "base/strings/utf_string_conversions.h" 13#include "chrome/browser/browser_process.h" 14#include "chrome/browser/chrome_notification_types.h" 15#include "chrome/browser/lifetime/application_lifetime.h" 16#include "chrome/browser/profiles/avatar_menu.h" 17#include "chrome/browser/profiles/avatar_menu_observer.h" 18#include "chrome/browser/profiles/profile_avatar_icon_util.h" 19#include "chrome/browser/profiles/profile_info_cache.h" 20#include "chrome/browser/profiles/profile_manager.h" 21#include "chrome/browser/profiles/profile_metrics.h" 22#include "chrome/browser/profiles/profile_window.h" 23#include "chrome/browser/profiles/profiles_state.h" 24#include "chrome/browser/signin/profile_oauth2_token_service_factory.h" 25#include "chrome/browser/signin/signin_header_helper.h" 26#include "chrome/browser/signin/signin_manager_factory.h" 27#include "chrome/browser/signin/signin_promo.h" 28#include "chrome/browser/ui/browser.h" 29#include "chrome/browser/ui/browser_commands.h" 30#include "chrome/browser/ui/browser_dialogs.h" 31#include "chrome/browser/ui/browser_window.h" 32#include "chrome/browser/ui/chrome_pages.h" 33#include "chrome/browser/ui/chrome_style.h" 34#import "chrome/browser/ui/cocoa/hyperlink_text_view.h" 35#import "chrome/browser/ui/cocoa/info_bubble_view.h" 36#import "chrome/browser/ui/cocoa/info_bubble_window.h" 37#import "chrome/browser/ui/cocoa/profiles/user_manager_mac.h" 38#include "chrome/browser/ui/singleton_tabs.h" 39#include "chrome/common/pref_names.h" 40#include "chrome/common/url_constants.h" 41#include "components/signin/core/common/profile_management_switches.h" 42#include "components/signin/core/browser/mutable_profile_oauth2_token_service.h" 43#include "components/signin/core/browser/profile_oauth2_token_service.h" 44#include "components/signin/core/browser/signin_manager.h" 45#include "content/public/browser/notification_service.h" 46#include "content/public/browser/web_contents.h" 47#include "google_apis/gaia/oauth2_token_service.h" 48#include "grit/chromium_strings.h" 49#include "grit/generated_resources.h" 50#include "grit/theme_resources.h" 51#include "skia/ext/skia_utils_mac.h" 52#import "third_party/google_toolbox_for_mac/src/AppKit/GTMUILocalizerAndLayoutTweaker.h" 53#import "ui/base/cocoa/cocoa_base_utils.h" 54#import "ui/base/cocoa/controls/blue_label_button.h" 55#import "ui/base/cocoa/controls/hyperlink_button_cell.h" 56#import "ui/base/cocoa/hover_image_button.h" 57#include "ui/base/cocoa/window_size_constants.h" 58#include "ui/base/l10n/l10n_util.h" 59#include "ui/base/l10n/l10n_util_mac.h" 60#include "ui/base/resource/resource_bundle.h" 61#include "ui/gfx/image/image.h" 62#include "ui/gfx/text_elider.h" 63#include "ui/native_theme/native_theme.h" 64 65namespace { 66 67// Constants taken from the Windows/Views implementation at: 68// chrome/browser/ui/views/profile_chooser_view.cc 69const int kLargeImageSide = 88; 70const int kSmallImageSide = 32; 71const CGFloat kFixedMenuWidth = 250; 72 73const CGFloat kVerticalSpacing = 16.0; 74const CGFloat kSmallVerticalSpacing = 10.0; 75const CGFloat kHorizontalSpacing = 16.0; 76const CGFloat kTitleFontSize = 15.0; 77const CGFloat kTextFontSize = 12.0; 78const CGFloat kProfileButtonHeight = 30; 79const int kBezelThickness = 3; // Width of the bezel on an NSButton. 80const int kImageTitleSpacing = 10; 81const int kBlueButtonHeight = 30; 82 83// Fixed size for embedded sign in pages as defined in Gaia. 84const CGFloat kFixedGaiaViewWidth = 360; 85const CGFloat kFixedGaiaViewHeight = 400; 86 87// Fixed size for the account removal view. 88const CGFloat kFixedAccountRemovalViewWidth = 280; 89// Fixed size for the end-preview view. 90const int kFixedEndPreviewViewWidth = 280; 91 92// Maximum number of times to show the welcome tutorial in the profile avatar 93// bubble. 94const int kProfileAvatarTutorialShowMax = 1; 95 96// The tag number for the primary account. 97const int kPrimaryProfileTag = -1; 98 99gfx::Image CreateProfileImage(const gfx::Image& icon, int imageSize) { 100 return profiles::GetSizedAvatarIcon( 101 icon, true /* image is a square */, imageSize, imageSize); 102} 103 104// Updates the window size and position. 105void SetWindowSize(NSWindow* window, NSSize size) { 106 NSRect frame = [window frame]; 107 frame.origin.x += frame.size.width - size.width; 108 frame.origin.y += frame.size.height - size.height; 109 frame.size = size; 110 [window setFrame:frame display:YES]; 111} 112 113NSString* ElideEmail(const std::string& email, CGFloat width) { 114 const base::string16 elidedEmail = gfx::ElideText( 115 base::UTF8ToUTF16(email), gfx::FontList(), width, gfx::ELIDE_EMAIL); 116 return base::SysUTF16ToNSString(elidedEmail); 117} 118 119// Builds a label with the given |title| anchored at |frame_origin|. Sets the 120// text color and background color to the given colors if not null. 121NSTextField* BuildLabel(NSString* title, 122 NSPoint frame_origin, 123 NSColor* background_color, 124 NSColor* text_color) { 125 base::scoped_nsobject<NSTextField> label( 126 [[NSTextField alloc] initWithFrame:NSZeroRect]); 127 [label setStringValue:title]; 128 [label setEditable:NO]; 129 [label setAlignment:NSLeftTextAlignment]; 130 [label setBezeled:NO]; 131 [label setFont:[NSFont labelFontOfSize:kTextFontSize]]; 132 [label setFrameOrigin:frame_origin]; 133 [label sizeToFit]; 134 135 if (background_color) 136 [[label cell] setBackgroundColor:background_color]; 137 if (text_color) 138 [[label cell] setTextColor:text_color]; 139 140 return label.autorelease(); 141} 142 143// Builds an NSTextView that has the contents set to the specified |message|, 144// with a non-underlined |link| inserted at |link_offset|. The view is anchored 145// at the specified |frame_origin| and has a fixed |frame_width|. 146NSTextView* BuildFixedWidthTextViewWithLink( 147 id<NSTextViewDelegate> delegate, 148 NSString* message, 149 NSString* link, 150 int link_offset, 151 NSPoint frame_origin, 152 CGFloat frame_width) { 153 base::scoped_nsobject<HyperlinkTextView> text_view( 154 [[HyperlinkTextView alloc] initWithFrame:NSZeroRect]); 155 NSColor* link_color = gfx::SkColorToCalibratedNSColor( 156 chrome_style::GetLinkColor()); 157 // Adds a padding row at the bottom, because |boundingRectWithSize| below cuts 158 // off the last row sometimes. 159 [text_view setMessageAndLink:[NSString stringWithFormat:@"%@\n", message] 160 withLink:link 161 atOffset:link_offset 162 font:[NSFont labelFontOfSize:kTextFontSize] 163 messageColor:[NSColor blackColor] 164 linkColor:link_color]; 165 166 // Removes the underlining from the link. 167 [text_view setLinkTextAttributes:nil]; 168 NSTextStorage* text_storage = [text_view textStorage]; 169 NSRange link_range = NSMakeRange(link_offset, [link length]); 170 [text_storage addAttribute:NSUnderlineStyleAttributeName 171 value:[NSNumber numberWithInt:NSUnderlineStyleNone] 172 range:link_range]; 173 174 NSRect frame = [[text_view attributedString] 175 boundingRectWithSize:NSMakeSize(frame_width, 0) 176 options:NSStringDrawingUsesLineFragmentOrigin]; 177 frame.origin = frame_origin; 178 [text_view setFrame:frame]; 179 [text_view setDelegate:delegate]; 180 return text_view.autorelease(); 181} 182 183// Returns the native dialog background color. 184NSColor* GetDialogBackgroundColor() { 185 return gfx::SkColorToCalibratedNSColor( 186 ui::NativeTheme::instance()->GetSystemColor( 187 ui::NativeTheme::kColorId_DialogBackground)); 188} 189 190// Builds a title card with one back button right aligned and one label center 191// aligned. 192NSView* BuildTitleCard(NSRect frame_rect, 193 int message_id, 194 id back_button_target, 195 SEL back_button_action) { 196 base::scoped_nsobject<NSView> container( 197 [[NSView alloc] initWithFrame:frame_rect]); 198 199 base::scoped_nsobject<HoverImageButton> button( 200 [[HoverImageButton alloc] initWithFrame:frame_rect]); 201 [button setBordered:NO]; 202 ui::ResourceBundle* rb = &ui::ResourceBundle::GetSharedInstance(); 203 [button setDefaultImage:rb->GetNativeImageNamed(IDR_BACK).ToNSImage()]; 204 [button setHoverImage:rb->GetNativeImageNamed(IDR_BACK_H).ToNSImage()]; 205 [button setPressedImage:rb->GetNativeImageNamed(IDR_BACK_P).ToNSImage()]; 206 [button setTarget:back_button_target]; 207 [button setAction:back_button_action]; 208 [button setFrameSize:NSMakeSize(kProfileButtonHeight, kProfileButtonHeight)]; 209 [button setFrameOrigin:NSMakePoint(kHorizontalSpacing, 0)]; 210 211 NSTextField* title_label = 212 BuildLabel(l10n_util::GetNSString(message_id), NSZeroPoint, 213 GetDialogBackgroundColor(), nil /* text_color */); 214 [title_label setAlignment:NSCenterTextAlignment]; 215 [title_label setFont:[NSFont labelFontOfSize:kTitleFontSize]]; 216 [title_label sizeToFit]; 217 CGFloat x_offset = (frame_rect.size.width - NSWidth([title_label frame])) / 2; 218 CGFloat y_offset = 219 (NSHeight([button frame]) - NSHeight([title_label frame])) / 2; 220 [title_label setFrameOrigin:NSMakePoint(x_offset, y_offset)]; 221 222 [container addSubview:button]; 223 [container addSubview:title_label]; 224 CGFloat height = std::max(NSMaxY([title_label frame]), 225 NSMaxY([button frame])) + kSmallVerticalSpacing; 226 [container setFrameSize:NSMakeSize(NSWidth([container frame]), height)]; 227 228 return container.autorelease(); 229} 230 231bool HasAuthError(Profile* profile) { 232 const SigninErrorController* error_controller = 233 profiles::GetSigninErrorController(profile); 234 return error_controller && error_controller->HasError(); 235} 236 237} // namespace 238 239// Class that listens to changes to the OAuth2Tokens for the active profile, 240// changes to the avatar menu model or browser close notifications. 241class ActiveProfileObserverBridge : public AvatarMenuObserver, 242 public content::NotificationObserver, 243 public OAuth2TokenService::Observer { 244 public: 245 ActiveProfileObserverBridge(ProfileChooserController* controller, 246 Browser* browser) 247 : controller_(controller), 248 browser_(browser), 249 token_observer_registered_(false) { 250 registrar_.Add(this, chrome::NOTIFICATION_BROWSER_CLOSING, 251 content::NotificationService::AllSources()); 252 if (!browser_->profile()->IsGuestSession()) 253 AddTokenServiceObserver(); 254 } 255 256 virtual ~ActiveProfileObserverBridge() { 257 RemoveTokenServiceObserver(); 258 } 259 260 private: 261 void AddTokenServiceObserver() { 262 ProfileOAuth2TokenService* oauth2_token_service = 263 ProfileOAuth2TokenServiceFactory::GetForProfile(browser_->profile()); 264 DCHECK(oauth2_token_service); 265 oauth2_token_service->AddObserver(this); 266 token_observer_registered_ = true; 267 } 268 269 void RemoveTokenServiceObserver() { 270 if (!token_observer_registered_) 271 return; 272 DCHECK(browser_->profile()); 273 ProfileOAuth2TokenService* oauth2_token_service = 274 ProfileOAuth2TokenServiceFactory::GetForProfile(browser_->profile()); 275 DCHECK(oauth2_token_service); 276 oauth2_token_service->RemoveObserver(this); 277 token_observer_registered_ = false; 278 } 279 280 // OAuth2TokenService::Observer: 281 virtual void OnRefreshTokenAvailable(const std::string& account_id) OVERRIDE { 282 // Tokens can only be added by adding an account through the inline flow, 283 // which is started from the account management view. Refresh it to show the 284 // update. 285 profiles::BubbleViewMode viewMode = [controller_ viewMode]; 286 if (viewMode == profiles::BUBBLE_VIEW_MODE_ACCOUNT_MANAGEMENT || 287 viewMode == profiles::BUBBLE_VIEW_MODE_GAIA_SIGNIN || 288 viewMode == profiles::BUBBLE_VIEW_MODE_GAIA_ADD_ACCOUNT || 289 viewMode == profiles::BUBBLE_VIEW_MODE_GAIA_REAUTH) { 290 [controller_ initMenuContentsWithView: 291 profiles::BUBBLE_VIEW_MODE_ACCOUNT_MANAGEMENT]; 292 } 293 } 294 295 virtual void OnRefreshTokenRevoked(const std::string& account_id) OVERRIDE { 296 // Tokens can only be removed from the account management view. Refresh it 297 // to show the update. 298 if ([controller_ viewMode] == profiles::BUBBLE_VIEW_MODE_ACCOUNT_MANAGEMENT) 299 [controller_ initMenuContentsWithView: 300 profiles::BUBBLE_VIEW_MODE_ACCOUNT_MANAGEMENT]; 301 } 302 303 // AvatarMenuObserver: 304 virtual void OnAvatarMenuChanged(AvatarMenu* avatar_menu) OVERRIDE { 305 // While the bubble is open, the avatar menu can only change from the 306 // profile chooser view by modifying the current profile's photo or name. 307 [controller_ 308 initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER]; 309 } 310 311 // content::NotificationObserver: 312 virtual void Observe( 313 int type, 314 const content::NotificationSource& source, 315 const content::NotificationDetails& details) OVERRIDE { 316 DCHECK_EQ(chrome::NOTIFICATION_BROWSER_CLOSING, type); 317 if (browser_ == content::Source<Browser>(source).ptr()) { 318 RemoveTokenServiceObserver(); 319 // Clean up the bubble's WebContents (used by the Gaia embedded view), to 320 // make sure the guest profile doesn't have any dangling host renderers. 321 // This can happen if Chrome is quit using Command-Q while the bubble is 322 // still open, which won't give the bubble a chance to be closed and 323 // clean up the WebContents itself. 324 [controller_ cleanUpEmbeddedViewContents]; 325 } 326 } 327 328 ProfileChooserController* controller_; // Weak; owns this. 329 Browser* browser_; // Weak. 330 content::NotificationRegistrar registrar_; 331 332 // The observer can be removed both when closing the browser, and by just 333 // closing the avatar bubble. However, in the case of closing the browser, 334 // the avatar bubble will also be closed afterwards, resulting in a second 335 // attempt to remove the observer. This ensures the observer is only 336 // removed once. 337 bool token_observer_registered_; 338 339 DISALLOW_COPY_AND_ASSIGN(ActiveProfileObserverBridge); 340}; 341 342// Custom button cell that adds a left padding before the button image, and 343// a custom spacing between the button image and title. 344@interface CustomPaddingImageButtonCell : NSButtonCell { 345 @private 346 // Padding added to the left margin of the button. 347 int leftMarginSpacing_; 348 // Spacing between the cell image and title. 349 int imageTitleSpacing_; 350} 351 352- (id)initWithLeftMarginSpacing:(int)leftMarginSpacing 353 imageTitleSpacing:(int)imageTitleSpacing; 354@end 355 356@implementation CustomPaddingImageButtonCell 357- (id)initWithLeftMarginSpacing:(int)leftMarginSpacing 358 imageTitleSpacing:(int)imageTitleSpacing { 359 if ((self = [super init])) { 360 leftMarginSpacing_ = leftMarginSpacing; 361 imageTitleSpacing_ = imageTitleSpacing; 362 } 363 return self; 364} 365 366- (NSRect)drawTitle:(NSAttributedString*)title 367 withFrame:(NSRect)frame 368 inView:(NSView*)controlView { 369 NSRect marginRect; 370 NSDivideRect(frame, &marginRect, &frame, leftMarginSpacing_, NSMinXEdge); 371 372 // The title frame origin isn't aware of the left margin spacing added 373 // in -drawImage, so it must be added when drawing the title as well. 374 if ([self imagePosition] == NSImageLeft) 375 NSDivideRect(frame, &marginRect, &frame, imageTitleSpacing_, NSMinXEdge); 376 377 return [super drawTitle:title withFrame:frame inView:controlView]; 378} 379 380- (void)drawImage:(NSImage*)image 381 withFrame:(NSRect)frame 382 inView:(NSView*)controlView { 383 if ([self imagePosition] == NSImageLeft) 384 frame.origin.x = leftMarginSpacing_; 385 [super drawImage:image withFrame:frame inView:controlView]; 386} 387 388- (NSSize)cellSize { 389 NSSize buttonSize = [super cellSize]; 390 buttonSize.width += leftMarginSpacing_; 391 if ([self imagePosition] == NSImageLeft) 392 buttonSize.width += imageTitleSpacing_; 393 return buttonSize; 394} 395 396@end 397 398// A custom button that has a transparent backround. 399@interface TransparentBackgroundButton : NSButton 400@end 401 402@implementation TransparentBackgroundButton 403- (id)initWithFrame:(NSRect)frameRect { 404 if ((self = [super initWithFrame:frameRect])) { 405 [self setBordered:NO]; 406 [self setFont:[NSFont labelFontOfSize:kTextFontSize]]; 407 [self setButtonType:NSMomentaryChangeButton]; 408 } 409 return self; 410} 411 412- (void)drawRect:(NSRect)dirtyRect { 413 NSColor* backgroundColor = [NSColor colorWithCalibratedWhite:1 alpha:0.6f]; 414 [backgroundColor setFill]; 415 NSRectFillUsingOperation(dirtyRect, NSCompositeSourceAtop); 416 [super drawRect:dirtyRect]; 417} 418@end 419 420// A custom image control that shows a "Change" button when moused over. 421@interface EditableProfilePhoto : NSImageView { 422 @private 423 AvatarMenu* avatarMenu_; // Weak; Owned by ProfileChooserController. 424 base::scoped_nsobject<TransparentBackgroundButton> changePhotoButton_; 425 // Used to display the "Change" button on hover. 426 ui::ScopedCrTrackingArea trackingArea_; 427 ProfileChooserController* controller_; 428} 429 430- (id)initWithFrame:(NSRect)frameRect 431 avatarMenu:(AvatarMenu*)avatarMenu 432 profileIcon:(const gfx::Image&)profileIcon 433 editingAllowed:(BOOL)editingAllowed 434 withController:(ProfileChooserController*)controller; 435 436// Called when the "Change" button is clicked. 437- (void)editPhoto:(id)sender; 438 439// When hovering over the profile photo, show the "Change" button. 440- (void)mouseEntered:(NSEvent*)event; 441 442// When hovering away from the profile photo, hide the "Change" button. 443- (void)mouseExited:(NSEvent*)event; 444@end 445 446@interface EditableProfilePhoto (Private) 447// Create the "Change" avatar photo button. 448- (TransparentBackgroundButton*)changePhotoButtonWithRect:(NSRect)rect; 449@end 450 451@implementation EditableProfilePhoto 452- (id)initWithFrame:(NSRect)frameRect 453 avatarMenu:(AvatarMenu*)avatarMenu 454 profileIcon:(const gfx::Image&)profileIcon 455 editingAllowed:(BOOL)editingAllowed 456 withController:(ProfileChooserController*)controller { 457 if ((self = [super initWithFrame:frameRect])) { 458 avatarMenu_ = avatarMenu; 459 controller_ = controller; 460 [self setImage:CreateProfileImage( 461 profileIcon, kLargeImageSide).ToNSImage()]; 462 463 // Add a tracking area so that we can show/hide the button when hovering. 464 trackingArea_.reset([[CrTrackingArea alloc] 465 initWithRect:[self bounds] 466 options:NSTrackingMouseEnteredAndExited | NSTrackingActiveAlways 467 owner:self 468 userInfo:nil]); 469 [self addTrackingArea:trackingArea_.get()]; 470 471 NSRect bounds = NSMakeRect(0, 0, kLargeImageSide, kLargeImageSide); 472 if (editingAllowed) { 473 changePhotoButton_.reset([self changePhotoButtonWithRect:bounds]); 474 [self addSubview:changePhotoButton_]; 475 476 // Hide the button until the image is hovered over. 477 [changePhotoButton_ setHidden:YES]; 478 } 479 } 480 return self; 481} 482 483- (void)drawRect:(NSRect)dirtyRect { 484 NSRect bounds = [self bounds]; 485 486 // Display the profile picture as a circle. 487 NSBezierPath* path = [NSBezierPath bezierPathWithOvalInRect:bounds]; 488 [path addClip]; 489 [self.image drawAtPoint:bounds.origin 490 fromRect:bounds 491 operation:NSCompositeSourceOver 492 fraction:1.0]; 493 494} 495 496- (void)editPhoto:(id)sender { 497 avatarMenu_->EditProfile(avatarMenu_->GetActiveProfileIndex()); 498 [controller_ 499 postActionPerformed:ProfileMetrics::PROFILE_DESKTOP_MENU_EDIT_IMAGE]; 500} 501 502- (void)mouseEntered:(NSEvent*)event { 503 [changePhotoButton_ setHidden:NO]; 504} 505 506- (void)mouseExited:(NSEvent*)event { 507 [changePhotoButton_ setHidden:YES]; 508} 509 510- (TransparentBackgroundButton*)changePhotoButtonWithRect:(NSRect)rect { 511 TransparentBackgroundButton* button = 512 [[TransparentBackgroundButton alloc] initWithFrame:rect]; 513 [button setImage:ui::ResourceBundle::GetSharedInstance().GetNativeImageNamed( 514 IDR_ICON_PROFILES_EDIT_CAMERA).AsNSImage()]; 515 [button setImagePosition:NSImageOnly]; 516 [button setTarget:self]; 517 [button setAction:@selector(editPhoto:)]; 518 return button; 519} 520@end 521 522// A custom text control that turns into a textfield for editing when clicked. 523@interface EditableProfileNameButton : HoverImageButton { 524 @private 525 base::scoped_nsobject<NSTextField> profileNameTextField_; 526 Profile* profile_; // Weak. 527 ProfileChooserController* controller_; 528} 529 530- (id)initWithFrame:(NSRect)frameRect 531 profile:(Profile*)profile 532 profileName:(NSString*)profileName 533 editingAllowed:(BOOL)editingAllowed 534 withController:(ProfileChooserController*)controller; 535 536// Called when the button is clicked. 537- (void)showEditableView:(id)sender; 538 539// Called when enter is pressed in the text field. 540- (void)saveProfileName:(id)sender; 541 542@end 543 544@implementation EditableProfileNameButton 545- (id)initWithFrame:(NSRect)frameRect 546 profile:(Profile*)profile 547 profileName:(NSString*)profileName 548 editingAllowed:(BOOL)editingAllowed 549 withController:(ProfileChooserController*)controller { 550 if ((self = [super initWithFrame:frameRect])) { 551 profile_ = profile; 552 controller_ = controller; 553 554 if (editingAllowed) { 555 // Show an "edit" pencil icon when hovering over. In the default state, 556 // we need to create an empty placeholder of the correct size, so that 557 // the text doesn't jump around when the hovered icon appears. 558 ui::ResourceBundle* rb = &ui::ResourceBundle::GetSharedInstance(); 559 NSImage* hoverImage = rb->GetNativeImageNamed( 560 IDR_ICON_PROFILES_EDIT_HOVER).AsNSImage(); 561 562 // In order to center the button title, we need to add a left padding of 563 // the same width as the pencil icon. 564 base::scoped_nsobject<CustomPaddingImageButtonCell> cell( 565 [[CustomPaddingImageButtonCell alloc] 566 initWithLeftMarginSpacing:[hoverImage size].width 567 imageTitleSpacing:0]); 568 [self setCell:cell.get()]; 569 570 NSImage* placeholder = [[NSImage alloc] initWithSize:[hoverImage size]]; 571 [self setDefaultImage:placeholder]; 572 [self setHoverImage:hoverImage]; 573 [self setAlternateImage: 574 rb->GetNativeImageNamed(IDR_ICON_PROFILES_EDIT_PRESSED).AsNSImage()]; 575 [self setImagePosition:NSImageRight]; 576 [self setTarget:self]; 577 [self setAction:@selector(showEditableView:)]; 578 579 // We need to subtract the width of the bezel from the frame rect, so that 580 // the textfield can take the exact same space as the button. 581 frameRect.size.height -= 2 * kBezelThickness; 582 frameRect.origin = NSMakePoint(0, kBezelThickness); 583 profileNameTextField_.reset( 584 [[NSTextField alloc] initWithFrame:frameRect]); 585 [profileNameTextField_ setStringValue:profileName]; 586 [profileNameTextField_ setFont:[NSFont labelFontOfSize:kTitleFontSize]]; 587 [profileNameTextField_ setEditable:YES]; 588 [profileNameTextField_ setDrawsBackground:YES]; 589 [profileNameTextField_ setBezeled:YES]; 590 [profileNameTextField_ setAlignment:NSCenterTextAlignment]; 591 [[profileNameTextField_ cell] setWraps:NO]; 592 [[profileNameTextField_ cell] setLineBreakMode: 593 NSLineBreakByTruncatingTail]; 594 [self addSubview:profileNameTextField_]; 595 [profileNameTextField_ setTarget:self]; 596 [profileNameTextField_ setAction:@selector(saveProfileName:)]; 597 598 // Hide the textfield until the user clicks on the button. 599 [profileNameTextField_ setHidden:YES]; 600 } 601 602 [self setBordered:NO]; 603 [self setFont:[NSFont labelFontOfSize:kTitleFontSize]]; 604 [self setAlignment:NSCenterTextAlignment]; 605 [[self cell] setLineBreakMode:NSLineBreakByTruncatingTail]; 606 [self setTitle:profileName]; 607 } 608 return self; 609} 610 611- (void)saveProfileName:(id)sender { 612 NSString* text = [profileNameTextField_ stringValue]; 613 // Empty profile names are not allowed, and are treated as a cancel. 614 if ([text length] > 0) { 615 profiles::UpdateProfileName(profile_, base::SysNSStringToUTF16(text)); 616 [controller_ 617 postActionPerformed:ProfileMetrics::PROFILE_DESKTOP_MENU_EDIT_NAME]; 618 [self setTitle:text]; 619 } 620 [profileNameTextField_ setHidden:YES]; 621} 622 623- (void)showEditableView:(id)sender { 624 [profileNameTextField_ setHidden:NO]; 625 [[self window] makeFirstResponder:profileNameTextField_]; 626} 627 628@end 629 630// A custom button that allows for setting a background color when hovered over. 631@interface BackgroundColorHoverButton : HoverImageButton { 632 @private 633 base::scoped_nsobject<NSColor> backgroundColor_; 634 base::scoped_nsobject<NSColor> hoverColor_; 635} 636@end 637 638@implementation BackgroundColorHoverButton 639 640- (id)initWithFrame:(NSRect)frameRect 641 imageTitleSpacing:(int)imageTitleSpacing 642 backgroundColor:(NSColor*)backgroundColor { 643 if ((self = [super initWithFrame:frameRect])) { 644 backgroundColor_.reset([backgroundColor retain]); 645 hoverColor_.reset([gfx::SkColorToCalibratedNSColor( 646 ui::NativeTheme::instance()->GetSystemColor( 647 ui::NativeTheme::kColorId_ButtonHoverBackgroundColor)) retain]); 648 649 [self setBordered:NO]; 650 [self setFont:[NSFont labelFontOfSize:kTextFontSize]]; 651 [self setButtonType:NSMomentaryChangeButton]; 652 653 base::scoped_nsobject<CustomPaddingImageButtonCell> cell( 654 [[CustomPaddingImageButtonCell alloc] 655 initWithLeftMarginSpacing:kHorizontalSpacing 656 imageTitleSpacing:imageTitleSpacing]); 657 [cell setLineBreakMode:NSLineBreakByTruncatingTail]; 658 [self setCell:cell.get()]; 659 } 660 return self; 661} 662 663- (void)setHoverState:(HoverState)state { 664 [super setHoverState:state]; 665 bool isHighlighted = ([self hoverState] != kHoverStateNone); 666 667 NSColor* backgroundColor = isHighlighted ? hoverColor_ : backgroundColor_; 668 [[self cell] setBackgroundColor:backgroundColor]; 669} 670 671@end 672 673// A custom view with the given background color. 674@interface BackgroundColorView : NSView { 675 @private 676 base::scoped_nsobject<NSColor> backgroundColor_; 677} 678@end 679 680@implementation BackgroundColorView 681- (id)initWithFrame:(NSRect)frameRect 682 withColor:(NSColor*)color { 683 if ((self = [super initWithFrame:frameRect])) 684 backgroundColor_.reset([color retain]); 685 return self; 686} 687 688- (void)drawRect:(NSRect)dirtyRect { 689 [backgroundColor_ setFill]; 690 NSRectFill(dirtyRect); 691 [super drawRect:dirtyRect]; 692} 693@end 694 695@interface ProfileChooserController () 696// Builds the profile chooser view. 697- (NSView*)buildProfileChooserView; 698 699// Builds a tutorial card with a title label using |titleMessageId|, a content 700// label using |contentMessageId|, and a bottom row with a right-aligned link 701// using |linkMessageId|, and a left aligned button using |buttonMessageId|. 702// On click, the link would execute |linkAction|, and the button would execute 703// |buttonAction|. It sets |tutorialMode_| to the given |mode|. 704- (NSView*)tutorialViewWithMode:(profiles::TutorialMode)mode 705 titleMessage:(int)titleMessageId 706 contentMessage:(int)contentMessageId 707 linkMessage:(int)linkMessageId 708 buttonMessage:(int)buttonMessageId 709 linkAction:(SEL)linkAction 710 buttonAction:(SEL)buttonAction; 711 712// Builds a a tutorial card for new profile management preview if needed. if 713// new profile management is not enabled yet, then it prompts the user to try 714// out the feature. Otherwise, it notifies the user that the feature has been 715// enabled if needed. 716- (NSView*)buildPreviewTutorialIfNeeded:(const AvatarMenu::Item&)item; 717 718// Creates the main profile card for the profile |item| at the top of 719// the bubble. 720- (NSView*)createCurrentProfileView:(const AvatarMenu::Item&)item; 721 722// Creates the possible links for the main profile card with profile |item|. 723- (NSView*)createCurrentProfileLinksForItem:(const AvatarMenu::Item&)item 724 rect:(NSRect)rect; 725 726// Creates the disclaimer text for supervised users, telling them that the 727// manager can view their history etc. 728- (NSView*)createSupervisedUserDisclaimerView; 729 730// Creates a main profile card for the guest user. 731- (NSView*)createGuestProfileView; 732 733// Creates an item for the profile |itemIndex| that is used in the fast profile 734// switcher in the middle of the bubble. 735- (NSButton*)createOtherProfileView:(int)itemIndex; 736 737// Creates the "Not you" and Lock option buttons. 738- (NSView*)createOptionsViewWithRect:(NSRect)rect 739 enableLock:(BOOL)enableLock; 740 741// Creates the account management view for the active profile. 742- (NSView*)createCurrentProfileAccountsView:(NSRect)rect; 743 744// Creates the list of accounts for the active profile. 745- (NSView*)createAccountsListWithRect:(NSRect)rect; 746 747// Creates the Gaia sign-in/add account view. 748- (NSView*)buildGaiaEmbeddedView; 749 750// Creates the account removal view. 751- (NSView*)buildAccountRemovalView; 752 753// Creates the end-preview view. 754- (NSView*)buildEndPreviewView; 755 756// Creates a button with |text|, an icon given by |imageResourceId| and with 757// |action|. The icon |alternateImageResourceId| is displayed in the button's 758// hovered and pressed states. 759- (NSButton*)hoverButtonWithRect:(NSRect)rect 760 text:(NSString*)text 761 imageResourceId:(int)imageResourceId 762 alternateImageResourceId:(int)alternateImageResourceId 763 action:(SEL)action; 764 765// Creates a generic link button with |title| and an |action| positioned at 766// |frameOrigin|. 767- (NSButton*)linkButtonWithTitle:(NSString*)title 768 frameOrigin:(NSPoint)frameOrigin 769 action:(SEL)action; 770 771// Creates an email account button with |title| and a remove icon. If 772// |reauthRequired| is true, the button also displays a warning icon. |tag| 773// indicates which account the button refers to. 774- (NSButton*)accountButtonWithRect:(NSRect)rect 775 title:(const std::string&)title 776 tag:(int)tag 777 reauthRequired:(BOOL)reauthRequired; 778@end 779 780@implementation ProfileChooserController 781- (profiles::BubbleViewMode) viewMode { 782 return viewMode_; 783} 784 785- (IBAction)switchToProfile:(id)sender { 786 // Check the event flags to see if a new window should be created. 787 bool alwaysCreate = ui::WindowOpenDispositionFromNSEvent( 788 [NSApp currentEvent]) == NEW_WINDOW; 789 avatarMenu_->SwitchToProfile([sender tag], alwaysCreate, 790 ProfileMetrics::SWITCH_PROFILE_ICON); 791} 792 793- (IBAction)showUserManager:(id)sender { 794 profiles::ShowUserManagerMaybeWithTutorial(browser_->profile()); 795} 796 797- (IBAction)exitGuest:(id)sender { 798 DCHECK(browser_->profile()->IsGuestSession()); 799 [self showUserManager:sender]; 800 profiles::CloseGuestProfileWindows(); 801} 802 803- (IBAction)showAccountManagement:(id)sender { 804 [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_ACCOUNT_MANAGEMENT]; 805} 806 807- (IBAction)hideAccountManagement:(id)sender { 808 [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER]; 809} 810 811- (IBAction)lockProfile:(id)sender { 812 profiles::LockProfile(browser_->profile()); 813 [self postActionPerformed:ProfileMetrics::PROFILE_DESKTOP_MENU_LOCK]; 814} 815 816- (IBAction)showInlineSigninPage:(id)sender { 817 [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_GAIA_SIGNIN]; 818} 819 820- (IBAction)showTabbedSigninPage:(id)sender { 821 chrome::ShowBrowserSignin(browser_, signin::SOURCE_MENU); 822} 823 824- (IBAction)addAccount:(id)sender { 825 [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_GAIA_ADD_ACCOUNT]; 826 [self postActionPerformed:ProfileMetrics::PROFILE_DESKTOP_MENU_ADD_ACCT]; 827} 828 829- (IBAction)navigateBackFromSigninPage:(id)sender { 830 std::string primaryAccount = SigninManagerFactory::GetForProfile( 831 browser_->profile())->GetAuthenticatedUsername(); 832 [self initMenuContentsWithView:primaryAccount.empty() ? 833 profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER : 834 profiles::BUBBLE_VIEW_MODE_ACCOUNT_MANAGEMENT]; 835} 836 837- (IBAction)showAccountRemovalView:(id)sender { 838 DCHECK(!isGuestSession_); 839 840 // Tag is either |kPrimaryProfileTag| for the primary account, or equal to the 841 // index in |currentProfileAccounts_| for a secondary account. 842 int tag = [sender tag]; 843 if (tag == kPrimaryProfileTag) { 844 accountIdToRemove_ = SigninManagerFactory::GetForProfile( 845 browser_->profile())->GetAuthenticatedUsername(); 846 } else { 847 DCHECK(ContainsKey(currentProfileAccounts_, tag)); 848 accountIdToRemove_ = currentProfileAccounts_[tag]; 849 } 850 851 [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_ACCOUNT_REMOVAL]; 852} 853 854- (IBAction)showAccountReauthenticationView:(id)sender { 855 DCHECK(!isGuestSession_); 856 [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_GAIA_REAUTH]; 857} 858 859- (IBAction)removeAccount:(id)sender { 860 DCHECK(!accountIdToRemove_.empty()); 861 ProfileOAuth2TokenServiceFactory::GetPlatformSpecificForProfile( 862 browser_->profile())->RevokeCredentials(accountIdToRemove_); 863 [self postActionPerformed:ProfileMetrics::PROFILE_DESKTOP_MENU_REMOVE_ACCT]; 864 accountIdToRemove_.clear(); 865 866 [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_ACCOUNT_MANAGEMENT]; 867} 868 869- (IBAction)openTutorialLearnMoreURL:(id)sender { 870 ProfileMetrics::LogProfileUpgradeEnrollment( 871 ProfileMetrics::PROFILE_ENROLLMENT_LAUNCH_LEARN_MORE); 872 // TODO(guohui): update |learnMoreUrl| once it is decided. 873 const GURL learnMoreUrl("https://support.google.com/chrome/?hl=en#to"); 874 chrome::NavigateParams params(browser_->profile(), learnMoreUrl, 875 content::PAGE_TRANSITION_LINK); 876 params.disposition = NEW_FOREGROUND_TAB; 877 chrome::Navigate(¶ms); 878} 879 880- (IBAction)enableNewProfileManagementPreview:(id)sender { 881 ProfileMetrics::LogProfileUpgradeEnrollment( 882 ProfileMetrics::PROFILE_ENROLLMENT_ACCEPT_NEW_PROFILE_MGMT); 883 profiles::EnableNewProfileManagementPreview(browser_->profile()); 884} 885 886- (IBAction)dismissTutorial:(id)sender { 887 ProfileMetrics::LogProfileUpgradeEnrollment( 888 ProfileMetrics::PROFILE_ENROLLMENT_CLOSE_WELCOME_CARD); 889 // If the user manually dismissed the tutorial, never show it again by setting 890 // the number of times shown to the maximum plus 1, so that later we could 891 // distinguish between the dismiss case and the case when the tutorial is 892 // indeed shown for the maximum number of times. 893 browser_->profile()->GetPrefs()->SetInteger( 894 prefs::kProfileAvatarTutorialShown, kProfileAvatarTutorialShowMax + 1); 895 [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER]; 896} 897 898- (IBAction)showSendFeedbackTutorial:(id)sender { 899 ProfileMetrics::LogProfileUpgradeEnrollment( 900 ProfileMetrics::PROFILE_ENROLLMENT_SEND_FEEDBACK); 901 tutorialMode_ = profiles::TUTORIAL_MODE_SEND_FEEDBACK; 902 [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER]; 903} 904 905- (IBAction)showEndPreviewView:(id)sender { 906 [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_END_PREVIEW]; 907} 908 909- (IBAction)sendFeedback:(id)sender { 910 chrome::OpenFeedbackDialog(browser_); 911} 912 913- (IBAction)endPreviewAndRelaunch:(id)sender { 914 profiles::DisableNewProfileManagementPreview(browser_->profile()); 915} 916 917- (void)cleanUpEmbeddedViewContents { 918 webContents_.reset(); 919} 920 921- (id)initWithBrowser:(Browser*)browser 922 anchoredAt:(NSPoint)point 923 withMode:(profiles::BubbleViewMode)mode 924 withServiceType:(signin::GAIAServiceType)serviceType { 925 base::scoped_nsobject<InfoBubbleWindow> window([[InfoBubbleWindow alloc] 926 initWithContentRect:ui::kWindowSizeDeterminedLater 927 styleMask:NSBorderlessWindowMask 928 backing:NSBackingStoreBuffered 929 defer:NO]); 930 931 if ((self = [super initWithWindow:window 932 parentWindow:browser->window()->GetNativeWindow() 933 anchoredAt:point])) { 934 browser_ = browser; 935 viewMode_ = mode; 936 tutorialMode_ = profiles::TUTORIAL_MODE_NONE; 937 observer_.reset(new ActiveProfileObserverBridge(self, browser_)); 938 serviceType_ = serviceType; 939 940 avatarMenu_.reset(new AvatarMenu( 941 &g_browser_process->profile_manager()->GetProfileInfoCache(), 942 observer_.get(), 943 browser_)); 944 avatarMenu_->RebuildMenu(); 945 946 // Guest profiles do not have a token service. 947 isGuestSession_ = browser_->profile()->IsGuestSession(); 948 949 // If view mode is PROFILE_CHOOSER but there is an auth error, force 950 // ACCOUNT_MANAGEMENT mode. 951 if (viewMode_ == profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER && 952 HasAuthError(browser_->profile())) { 953 viewMode_ = profiles::BUBBLE_VIEW_MODE_ACCOUNT_MANAGEMENT; 954 } 955 956 [[self bubble] setAlignment:info_bubble::kAlignRightEdgeToAnchorEdge]; 957 [[self bubble] setArrowLocation:info_bubble::kNoArrow]; 958 [[self bubble] setBackgroundColor:GetDialogBackgroundColor()]; 959 [self initMenuContentsWithView:viewMode_]; 960 } 961 962 return self; 963} 964 965- (void)initMenuContentsWithView:(profiles::BubbleViewMode)viewToDisplay { 966 viewMode_ = viewToDisplay; 967 NSView* contentView = [[self window] contentView]; 968 [contentView setSubviews:[NSArray array]]; 969 NSView* subView; 970 971 switch (viewMode_) { 972 case profiles::BUBBLE_VIEW_MODE_GAIA_SIGNIN: 973 case profiles::BUBBLE_VIEW_MODE_GAIA_ADD_ACCOUNT: 974 case profiles::BUBBLE_VIEW_MODE_GAIA_REAUTH: 975 subView = [self buildGaiaEmbeddedView]; 976 break; 977 case profiles::BUBBLE_VIEW_MODE_ACCOUNT_REMOVAL: 978 subView = [self buildAccountRemovalView]; 979 break; 980 case profiles::BUBBLE_VIEW_MODE_END_PREVIEW: 981 subView = [self buildEndPreviewView]; 982 break; 983 case profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER: 984 case profiles::BUBBLE_VIEW_MODE_ACCOUNT_MANAGEMENT: 985 subView = [self buildProfileChooserView]; 986 break; 987 } 988 989 [contentView addSubview:subView]; 990 SetWindowSize([self window], 991 NSMakeSize(NSWidth([subView frame]), NSHeight([subView frame]))); 992} 993 994- (NSView*)buildProfileChooserView { 995 base::scoped_nsobject<NSView> container( 996 [[NSView alloc] initWithFrame:NSZeroRect]); 997 998 NSView* tutorialView = nil; 999 NSView* currentProfileView = nil; 1000 base::scoped_nsobject<NSMutableArray> otherProfiles( 1001 [[NSMutableArray alloc] init]); 1002 // Local and guest profiles cannot lock their profile. 1003 bool enableLock = false; 1004 // Store the most recently displayed tutorial mode 1005 profiles::TutorialMode lastTutorialMode = tutorialMode_; 1006 1007 // Loop over the profiles in reverse, so that they are sorted by their 1008 // y-coordinate, and separate them into active and "other" profiles. 1009 for (int i = avatarMenu_->GetNumberOfItems() - 1; i >= 0; --i) { 1010 const AvatarMenu::Item& item = avatarMenu_->GetItemAt(i); 1011 if (item.active) { 1012 if (viewMode_ == profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER) 1013 tutorialView = [self buildPreviewTutorialIfNeeded:item]; 1014 currentProfileView = [self createCurrentProfileView:item]; 1015 enableLock = item.signed_in; 1016 } else { 1017 [otherProfiles addObject:[self createOtherProfileView:i]]; 1018 } 1019 } 1020 if (!currentProfileView) // Guest windows don't have an active profile. 1021 currentProfileView = [self createGuestProfileView]; 1022 1023 // |yOffset| is the next position at which to draw in |container| 1024 // coordinates. Add a pixel offset so that the bottom option buttons don't 1025 // overlap the bubble's rounded corners. 1026 CGFloat yOffset = 1; 1027 1028 // Option buttons. Only available with the new profile management flag. 1029 if (switches::IsNewProfileManagement()) { 1030 NSRect rect = NSMakeRect(0, yOffset, kFixedMenuWidth, 0); 1031 NSView* optionsView = [self createOptionsViewWithRect:rect 1032 enableLock:enableLock]; 1033 [container addSubview:optionsView]; 1034 rect.origin.y = NSMaxY([optionsView frame]); 1035 1036 NSBox* separator = [self horizontalSeparatorWithFrame:rect]; 1037 [container addSubview:separator]; 1038 yOffset = NSMaxY([separator frame]); 1039 } 1040 1041 if (viewMode_ == profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER && 1042 switches::IsFastUserSwitching()) { 1043 // Other profiles switcher. The profiles have already been sorted 1044 // by their y-coordinate, so they can be added in the existing order. 1045 for (NSView *otherProfileView in otherProfiles.get()) { 1046 [otherProfileView setFrameOrigin:NSMakePoint(0, yOffset)]; 1047 [container addSubview:otherProfileView]; 1048 yOffset = NSMaxY([otherProfileView frame]); 1049 1050 NSBox* separator = [self horizontalSeparatorWithFrame:NSMakeRect( 1051 0, yOffset, kFixedMenuWidth, 0)]; 1052 [container addSubview:separator]; 1053 yOffset = NSMaxY([separator frame]); 1054 } 1055 } 1056 1057 // For supervised users, add the disclaimer text. 1058 if (browser_->profile()->IsSupervised()) { 1059 yOffset += kSmallVerticalSpacing; 1060 NSView* disclaimerContainer = [self createSupervisedUserDisclaimerView]; 1061 [disclaimerContainer setFrameOrigin:NSMakePoint(0, yOffset)]; 1062 [container addSubview:disclaimerContainer]; 1063 yOffset = NSMaxY([disclaimerContainer frame]); 1064 yOffset += kSmallVerticalSpacing; 1065 1066 NSBox* separator = [self horizontalSeparatorWithFrame:NSMakeRect( 1067 0, yOffset, kFixedMenuWidth, 0)]; 1068 [container addSubview:separator]; 1069 yOffset = NSMaxY([separator frame]); 1070 } 1071 1072 if (viewMode_ == profiles::BUBBLE_VIEW_MODE_ACCOUNT_MANAGEMENT) { 1073 NSView* currentProfileAccountsView = [self createCurrentProfileAccountsView: 1074 NSMakeRect(0, yOffset, kFixedMenuWidth, 0)]; 1075 [container addSubview:currentProfileAccountsView]; 1076 yOffset = NSMaxY([currentProfileAccountsView frame]); 1077 1078 NSBox* accountsSeparator = [self horizontalSeparatorWithFrame: 1079 NSMakeRect(0, yOffset, kFixedMenuWidth, 0)]; 1080 [container addSubview:accountsSeparator]; 1081 yOffset = NSMaxY([accountsSeparator frame]); 1082 } 1083 1084 // Active profile card. 1085 if (currentProfileView) { 1086 yOffset += kVerticalSpacing; 1087 [currentProfileView setFrameOrigin:NSMakePoint(0, yOffset)]; 1088 [container addSubview:currentProfileView]; 1089 yOffset = NSMaxY([currentProfileView frame]) + kVerticalSpacing; 1090 } 1091 1092 if (tutorialView) { 1093 [tutorialView setFrameOrigin:NSMakePoint(0, yOffset)]; 1094 [container addSubview:tutorialView]; 1095 yOffset = NSMaxY([tutorialView frame]); 1096 if (!switches::IsNewProfileManagement() && 1097 tutorialMode_ != lastTutorialMode) { 1098 ProfileMetrics::LogProfileUpgradeEnrollment( 1099 ProfileMetrics::PROFILE_ENROLLMENT_SHOW_PREVIEW_PROMO); 1100 } 1101 } else { 1102 tutorialMode_ = profiles::TUTORIAL_MODE_NONE; 1103 } 1104 1105 [container setFrameSize:NSMakeSize(kFixedMenuWidth, yOffset)]; 1106 return container.autorelease(); 1107} 1108 1109- (NSView*)buildPreviewTutorialIfNeeded:(const AvatarMenu::Item&)item { 1110 if (!switches::IsNewProfileManagement()) { 1111 return [self tutorialViewWithMode:profiles::TUTORIAL_MODE_ENABLE_PREVIEW 1112 titleMessage:IDS_PROFILES_PREVIEW_TUTORIAL_TITLE 1113 contentMessage:IDS_PROFILES_PREVIEW_TUTORIAL_CONTENT_TEXT 1114 linkMessage:IDS_PROFILES_PROFILE_TUTORIAL_LEARN_MORE 1115 buttonMessage:IDS_PROFILES_TUTORIAL_TRY_BUTTON 1116 linkAction:@selector(openTutorialLearnMoreURL:) 1117 buttonAction: 1118 @selector(enableNewProfileManagementPreview:)]; 1119 } 1120 1121 if (!switches::IsNewProfileManagementPreviewEnabled()) 1122 return nil; 1123 1124 if (tutorialMode_ == profiles::TUTORIAL_MODE_SEND_FEEDBACK) { 1125 return [self tutorialViewWithMode:profiles::TUTORIAL_MODE_SEND_FEEDBACK 1126 titleMessage:IDS_PROFILES_FEEDBACK_TUTORIAL_TITLE 1127 contentMessage: 1128 IDS_PROFILES_FEEDBACK_TUTORIAL_CONTENT_TEXT 1129 linkMessage:IDS_PROFILES_END_PREVIEW 1130 buttonMessage:IDS_PROFILES_SEND_FEEDBACK_BUTTON 1131 linkAction:@selector(showEndPreviewView:) 1132 buttonAction:@selector(sendFeedback:)]; 1133 } 1134 1135 Profile* profile = browser_->profile(); 1136 const int showCount = profile->GetPrefs()->GetInteger( 1137 prefs::kProfileAvatarTutorialShown); 1138 // Do not show the tutorial if user has dismissed it. 1139 if (showCount > kProfileAvatarTutorialShowMax) 1140 return nil; 1141 1142 if (tutorialMode_ != profiles::TUTORIAL_MODE_WELCOME) { 1143 if (showCount == kProfileAvatarTutorialShowMax) 1144 return nil; 1145 profile->GetPrefs()->SetInteger( 1146 prefs::kProfileAvatarTutorialShown, showCount + 1); 1147 } 1148 1149 return [self tutorialViewWithMode:profiles::TUTORIAL_MODE_WELCOME 1150 titleMessage:IDS_PROFILES_PREVIEW_ENABLED_TUTORIAL_TITLE 1151 contentMessage: 1152 IDS_PROFILES_PREVIEW_ENABLED_TUTORIAL_CONTENT_TEXT 1153 linkMessage:IDS_PROFILES_PROFILE_TUTORIAL_LEARN_MORE 1154 buttonMessage:IDS_PROFILES_TUTORIAL_OK_BUTTON 1155 linkAction:@selector(openTutorialLearnMoreURL:) 1156 buttonAction:@selector(dismissTutorial:)]; 1157} 1158 1159- (NSView*)tutorialViewWithMode:(profiles::TutorialMode)mode 1160 titleMessage:(int)titleMessageId 1161 contentMessage:(int)contentMessageId 1162 linkMessage:(int)linkMessageId 1163 buttonMessage:(int)buttonMessageId 1164 linkAction:(SEL)linkAction 1165 buttonAction:(SEL)buttonAction { 1166 tutorialMode_ = mode; 1167 1168 NSColor* tutorialBackgroundColor = 1169 gfx::SkColorToSRGBNSColor(profiles::kAvatarTutorialBackgroundColor); 1170 base::scoped_nsobject<NSView> container([[BackgroundColorView alloc] 1171 initWithFrame:NSMakeRect(0, 0, kFixedMenuWidth, 0) 1172 withColor:tutorialBackgroundColor]); 1173 CGFloat availableWidth = kFixedMenuWidth - 2 * kHorizontalSpacing; 1174 CGFloat yOffset = kSmallVerticalSpacing; 1175 1176 // Adds links and buttons at the bottom. 1177 base::scoped_nsobject<NSButton> tutorialOkButton([[HoverButton alloc] 1178 initWithFrame:NSZeroRect]); 1179 [tutorialOkButton setTitle:l10n_util::GetNSString( 1180 buttonMessageId)]; 1181 [tutorialOkButton setBezelStyle:NSRoundedBezelStyle]; 1182 [tutorialOkButton setTarget:self]; 1183 [tutorialOkButton setAction:buttonAction]; 1184 [tutorialOkButton sizeToFit]; 1185 NSSize buttonSize = [tutorialOkButton frame].size; 1186 const CGFloat kTopBottomTextPadding = 6; 1187 const CGFloat kLeftRightTextPadding = 15; 1188 buttonSize.width += 2 * kLeftRightTextPadding; 1189 buttonSize.height += 2 * kTopBottomTextPadding; 1190 [tutorialOkButton setFrameSize:buttonSize]; 1191 [tutorialOkButton setAlignment:NSCenterTextAlignment]; 1192 [tutorialOkButton setFrameOrigin:NSMakePoint( 1193 kFixedMenuWidth - NSWidth([tutorialOkButton frame]) - kHorizontalSpacing, 1194 yOffset)]; 1195 [container addSubview:tutorialOkButton]; 1196 1197 NSButton* learnMoreLink = 1198 [self linkButtonWithTitle:l10n_util::GetNSString(linkMessageId) 1199 frameOrigin:NSZeroPoint 1200 action:linkAction]; 1201 [[learnMoreLink cell] setTextColor:[NSColor whiteColor]]; 1202 CGFloat linkYOffset = yOffset + (NSHeight([tutorialOkButton frame]) - 1203 NSHeight([learnMoreLink frame])) / 2; 1204 [learnMoreLink setFrameOrigin:NSMakePoint(kHorizontalSpacing, linkYOffset)]; 1205 [container addSubview:learnMoreLink]; 1206 1207 yOffset = std::max(NSMaxY([learnMoreLink frame]), 1208 NSMaxY([tutorialOkButton frame])) + kVerticalSpacing; 1209 1210 // Adds body content. 1211 NSTextField* contentLabel = BuildLabel( 1212 l10n_util::GetNSString(contentMessageId), 1213 NSMakePoint(kHorizontalSpacing, yOffset), 1214 tutorialBackgroundColor, 1215 gfx::SkColorToSRGBNSColor(profiles::kAvatarTutorialContentTextColor)); 1216 [contentLabel setFrameSize:NSMakeSize(availableWidth, 0)]; 1217 [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:contentLabel]; 1218 [container addSubview:contentLabel]; 1219 yOffset = NSMaxY([contentLabel frame]) + kSmallVerticalSpacing; 1220 1221 // Adds title. 1222 NSTextField* titleLabel = 1223 BuildLabel(l10n_util::GetNSString(titleMessageId), 1224 NSMakePoint(kHorizontalSpacing, yOffset), 1225 tutorialBackgroundColor, 1226 [NSColor whiteColor] /* text_color */); 1227 [titleLabel setFont:[NSFont labelFontOfSize:kTitleFontSize]]; 1228 [titleLabel sizeToFit]; 1229 [titleLabel setFrameSize: 1230 NSMakeSize(availableWidth, NSHeight([titleLabel frame]))]; 1231 [container addSubview:titleLabel]; 1232 yOffset = NSMaxY([titleLabel frame]) + kVerticalSpacing; 1233 1234 [container setFrameSize:NSMakeSize(kFixedMenuWidth, yOffset)]; 1235 1236 // Adds caret at the bottom. 1237 NSImage* caretImage = ui::ResourceBundle::GetSharedInstance(). 1238 GetNativeImageNamed(IDR_ICON_PROFILES_MENU_CARET).AsNSImage(); 1239 base::scoped_nsobject<NSImageView> caretView( 1240 [[NSImageView alloc] initWithFrame:NSMakeRect( 1241 kHorizontalSpacing, 0, caretImage.size.width, 1242 caretImage.size.height)]); 1243 [caretView setImage:caretImage]; 1244 1245 base::scoped_nsobject<NSView> containerWithCaret([[NSView alloc] 1246 initWithFrame:NSMakeRect(0, 0, kFixedMenuWidth, 0)]); 1247 [containerWithCaret addSubview:caretView]; 1248 1249 [container setFrameOrigin:NSMakePoint(0, caretImage.size.height)]; 1250 [containerWithCaret addSubview:container]; 1251 1252 [containerWithCaret setFrameSize: 1253 NSMakeSize(kFixedMenuWidth, NSMaxY([container frame]))]; 1254 return containerWithCaret.autorelease(); 1255} 1256 1257- (NSView*)createCurrentProfileView:(const AvatarMenu::Item&)item { 1258 base::scoped_nsobject<NSView> container([[NSView alloc] 1259 initWithFrame:NSZeroRect]); 1260 1261 CGFloat xOffset = kHorizontalSpacing; 1262 CGFloat yOffset = 0; 1263 CGFloat availableTextWidth = kFixedMenuWidth - 2 * kHorizontalSpacing; 1264 1265 // Profile options. This can be a link to the accounts view, the profile's 1266 // username for signed in users, or a "Sign in" button for local profiles. 1267 SigninManagerBase* signinManager = 1268 SigninManagerFactory::GetForProfile( 1269 browser_->profile()->GetOriginalProfile()); 1270 if (!isGuestSession_ && signinManager->IsSigninAllowed()) { 1271 NSView* linksContainer = 1272 [self createCurrentProfileLinksForItem:item 1273 rect:NSMakeRect(xOffset, yOffset, 1274 availableTextWidth, 1275 0)]; 1276 [container addSubview:linksContainer]; 1277 yOffset = NSMaxY([linksContainer frame]); 1278 } 1279 1280 // Profile name, centered. 1281 bool editingAllowed = !isGuestSession_ && 1282 !browser_->profile()->IsSupervised(); 1283 base::scoped_nsobject<EditableProfileNameButton> profileName( 1284 [[EditableProfileNameButton alloc] 1285 initWithFrame:NSMakeRect(xOffset, yOffset, 1286 availableTextWidth, 1287 kProfileButtonHeight) 1288 profile:browser_->profile() 1289 profileName:base::SysUTF16ToNSString( 1290 profiles::GetAvatarNameForProfile(browser_->profile())) 1291 editingAllowed:editingAllowed 1292 withController:self]); 1293 1294 [container addSubview:profileName]; 1295 yOffset = NSMaxY([profileName frame]); 1296 1297 // Profile icon, centered. 1298 xOffset = (kFixedMenuWidth - kLargeImageSide) / 2; 1299 base::scoped_nsobject<EditableProfilePhoto> iconView( 1300 [[EditableProfilePhoto alloc] 1301 initWithFrame:NSMakeRect(xOffset, yOffset, 1302 kLargeImageSide, kLargeImageSide) 1303 avatarMenu:avatarMenu_.get() 1304 profileIcon:item.icon 1305 editingAllowed:!isGuestSession_ 1306 withController:self]); 1307 1308 [container addSubview:iconView]; 1309 yOffset = NSMaxY([iconView frame]); 1310 1311 if (browser_->profile()->IsSupervised()) { 1312 base::scoped_nsobject<NSImageView> supervisedIcon( 1313 [[NSImageView alloc] initWithFrame:NSZeroRect]); 1314 ui::ResourceBundle* rb = &ui::ResourceBundle::GetSharedInstance(); 1315 [supervisedIcon setImage:rb->GetNativeImageNamed( 1316 IDR_ICON_PROFILES_MENU_SUPERVISED).ToNSImage()]; 1317 NSSize size = [[supervisedIcon image] size]; 1318 [supervisedIcon setFrameSize:size]; 1319 NSRect parentFrame = [iconView frame]; 1320 [supervisedIcon setFrameOrigin:NSMakePoint(NSMaxX(parentFrame) - size.width, 1321 NSMinY(parentFrame))]; 1322 [container addSubview:supervisedIcon]; 1323 } 1324 1325 if (switches::IsNewProfileManagementPreviewEnabled()) { 1326 base::scoped_nsobject<HoverImageButton> questionButton( 1327 [[HoverImageButton alloc] initWithFrame:NSZeroRect]); 1328 [questionButton setBordered:NO]; 1329 ui::ResourceBundle* rb = &ui::ResourceBundle::GetSharedInstance(); 1330 [questionButton setDefaultImage:rb->GetNativeImageNamed( 1331 IDR_ICON_PROFILES_MENU_QUESTION_STABLE).ToNSImage()]; 1332 [questionButton setHoverImage:rb->GetNativeImageNamed( 1333 IDR_ICON_PROFILES_MENU_QUESTION_HOVER).ToNSImage()]; 1334 [questionButton setPressedImage:rb->GetNativeImageNamed( 1335 IDR_ICON_PROFILES_MENU_QUESTION_SELECT).ToNSImage()]; 1336 [questionButton setTarget:self]; 1337 [questionButton setAction:@selector(showSendFeedbackTutorial:)]; 1338 [questionButton sizeToFit]; 1339 const CGFloat size = NSHeight([questionButton frame]) + 2; 1340 [questionButton setFrame: 1341 NSMakeRect(kHorizontalSpacing, yOffset - size, size, size)]; 1342 [container addSubview:questionButton]; 1343 } 1344 1345 [container setFrameSize:NSMakeSize(kFixedMenuWidth, yOffset)]; 1346 return container.autorelease(); 1347} 1348 1349- (NSView*)createCurrentProfileLinksForItem:(const AvatarMenu::Item&)item 1350 rect:(NSRect)rect { 1351 base::scoped_nsobject<NSView> container([[NSView alloc] initWithFrame:rect]); 1352 1353 // Don't double-apply the left margin to the sub-views. 1354 rect.origin.x = 0; 1355 1356 // The available links depend on the type of profile that is active. 1357 NSButton* link; 1358 if (item.signed_in) { 1359 if (switches::IsNewProfileManagement()) { 1360 NSString* linkTitle = l10n_util::GetNSString( 1361 viewMode_ == profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER ? 1362 IDS_PROFILES_PROFILE_MANAGE_ACCOUNTS_BUTTON : 1363 IDS_PROFILES_PROFILE_HIDE_MANAGE_ACCOUNTS_BUTTON); 1364 SEL linkSelector = 1365 (viewMode_ == profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER) ? 1366 @selector(showAccountManagement:) : @selector(hideAccountManagement:); 1367 link = [self linkButtonWithTitle:linkTitle 1368 frameOrigin:rect.origin 1369 action:linkSelector]; 1370 } else { 1371 link = [self linkButtonWithTitle:base::SysUTF16ToNSString(item.sync_state) 1372 frameOrigin:rect.origin 1373 action:nil]; 1374 [[link cell] setTextColor:[NSColor blackColor]]; 1375 } 1376 // -linkButtonWithTitle sizeToFit's the link, so re-stretch it so that it 1377 // can be centered correctly in the view. 1378 rect.size.height = NSMaxY([link frame]); 1379 [link setFrame:rect]; 1380 [link setAlignment:NSCenterTextAlignment]; 1381 } else { 1382 rect.size.height = kBlueButtonHeight; 1383 link = [[BlueLabelButton alloc] initWithFrame:rect]; 1384 1385 // Manually elide the button text so that the contents fit inside the bubble 1386 // This is needed because the BlueLabelButton cell resets the style on 1387 // every call to -cellSize, which prevents setting a custom lineBreakMode. 1388 NSString* elidedButtonText = base::SysUTF16ToNSString(gfx::ElideText( 1389 l10n_util::GetStringFUTF16( 1390 IDS_SYNC_START_SYNC_BUTTON_LABEL, 1391 l10n_util::GetStringUTF16(IDS_SHORT_PRODUCT_NAME)), 1392 gfx::FontList(), rect.size.width, gfx::ELIDE_TAIL)); 1393 1394 [link setTitle:elidedButtonText]; 1395 [link setTarget:self]; 1396 [link setAction:switches::IsNewProfileManagement() ? 1397 @selector(showInlineSigninPage:) : @selector(showTabbedSigninPage:)]; 1398 } 1399 1400 [container addSubview:link]; 1401 [container setFrameSize:rect.size]; 1402 return container.autorelease(); 1403} 1404 1405- (NSView*)createSupervisedUserDisclaimerView { 1406 base::scoped_nsobject<NSView> container( 1407 [[NSView alloc] initWithFrame:NSZeroRect]); 1408 1409 int yOffset = 0; 1410 int availableTextWidth = kFixedMenuWidth - 2 * kHorizontalSpacing; 1411 1412 NSTextField* disclaimer = BuildLabel( 1413 base::SysUTF16ToNSString(avatarMenu_->GetSupervisedUserInformation()), 1414 NSMakePoint(kHorizontalSpacing, yOffset), 1415 nil /* background_color */, 1416 nil /* text_color */); 1417 [disclaimer setFrameSize:NSMakeSize(availableTextWidth, 0)]; 1418 [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:disclaimer]; 1419 yOffset = NSMaxY([disclaimer frame]); 1420 1421 [container addSubview:disclaimer]; 1422 [container setFrameSize:NSMakeSize(kFixedMenuWidth, yOffset)]; 1423 return container.autorelease(); 1424} 1425 1426- (NSView*)createGuestProfileView { 1427 gfx::Image guestIcon = 1428 ui::ResourceBundle::GetSharedInstance().GetNativeImageNamed( 1429 profiles::GetPlaceholderAvatarIconResourceID()); 1430 AvatarMenu::Item guestItem(std::string::npos, /* menu_index, not used */ 1431 std::string::npos, /* profile_index, not used */ 1432 guestIcon); 1433 guestItem.active = true; 1434 guestItem.name = base::SysNSStringToUTF16( 1435 l10n_util::GetNSString(IDS_PROFILES_GUEST_PROFILE_NAME)); 1436 1437 return [self createCurrentProfileView:guestItem]; 1438} 1439 1440- (NSButton*)createOtherProfileView:(int)itemIndex { 1441 const AvatarMenu::Item& item = avatarMenu_->GetItemAt(itemIndex); 1442 1443 NSRect rect = NSMakeRect(0, 0, kFixedMenuWidth, 1444 kBlueButtonHeight + kSmallVerticalSpacing); 1445 base::scoped_nsobject<BackgroundColorHoverButton> profileButton( 1446 [[BackgroundColorHoverButton alloc] 1447 initWithFrame:rect 1448 imageTitleSpacing:kImageTitleSpacing 1449 backgroundColor:GetDialogBackgroundColor()]); 1450 [profileButton setTitle:base::SysUTF16ToNSString(item.name)]; 1451 [profileButton setDefaultImage:CreateProfileImage( 1452 item.icon, kSmallImageSide).ToNSImage()]; 1453 [profileButton setImagePosition:NSImageLeft]; 1454 [profileButton setAlignment:NSLeftTextAlignment]; 1455 [profileButton setBordered:NO]; 1456 [profileButton setTag:itemIndex]; 1457 [profileButton setTarget:self]; 1458 [profileButton setAction:@selector(switchToProfile:)]; 1459 1460 return profileButton.autorelease(); 1461} 1462 1463- (NSView*)createOptionsViewWithRect:(NSRect)rect 1464 enableLock:(BOOL)enableLock { 1465 int widthOfLockButton = enableLock ? 2 * kHorizontalSpacing + 14 : 0; 1466 NSRect viewRect = NSMakeRect(0, 0, 1467 rect.size.width - widthOfLockButton, 1468 kBlueButtonHeight + kVerticalSpacing); 1469 NSString* text = isGuestSession_ ? 1470 l10n_util::GetNSString(IDS_PROFILES_EXIT_GUEST) : 1471 l10n_util::GetNSStringF(IDS_PROFILES_NOT_YOU_BUTTON, 1472 profiles::GetAvatarNameForProfile(browser_->profile())); 1473 NSButton* notYouButton = 1474 [self hoverButtonWithRect:viewRect 1475 text:text 1476 imageResourceId:IDR_ICON_PROFILES_MENU_AVATAR 1477 alternateImageResourceId:IDR_ICON_PROFILES_MENU_AVATAR 1478 action:isGuestSession_? @selector(exitGuest:) : 1479 @selector(showUserManager:)]; 1480 1481 rect.size.height = NSMaxY([notYouButton frame]); 1482 base::scoped_nsobject<NSView> container([[NSView alloc] initWithFrame:rect]); 1483 [container addSubview:notYouButton]; 1484 1485 if (enableLock) { 1486 viewRect.origin.x = NSMaxX([notYouButton frame]); 1487 NSBox* separator = [self verticalSeparatorWithFrame:viewRect]; 1488 [container addSubview:separator]; 1489 1490 viewRect.origin.x = NSMaxX([separator frame]); 1491 viewRect.size.width = widthOfLockButton; 1492 NSButton* lockButton = 1493 [self hoverButtonWithRect:viewRect 1494 text:@"" 1495 imageResourceId:IDR_ICON_PROFILES_MENU_LOCK 1496 alternateImageResourceId:IDR_ICON_PROFILES_MENU_LOCK 1497 action:@selector(lockProfile:)]; 1498 [container addSubview:lockButton]; 1499 } 1500 1501 return container.autorelease(); 1502} 1503 1504- (NSView*)createCurrentProfileAccountsView:(NSRect)rect { 1505 const CGFloat kAccountButtonHeight = 34; 1506 1507 const AvatarMenu::Item& item = 1508 avatarMenu_->GetItemAt(avatarMenu_->GetActiveProfileIndex()); 1509 DCHECK(item.signed_in); 1510 1511 NSColor* backgroundColor = gfx::SkColorToCalibratedNSColor( 1512 profiles::kAvatarBubbleAccountsBackgroundColor); 1513 base::scoped_nsobject<NSView> container([[BackgroundColorView alloc] 1514 initWithFrame:rect 1515 withColor:backgroundColor]); 1516 1517 // Manually elide the button text so that the contents fit inside the bubble. 1518 // This is needed because the BlueLabelButton cell resets the style on 1519 // every call to -cellSize, which prevents setting a custom lineBreakMode. 1520 NSString* elidedButtonText = base::SysUTF16ToNSString(gfx::ElideText( 1521 l10n_util::GetStringFUTF16( 1522 IDS_PROFILES_PROFILE_ADD_ACCOUNT_BUTTON, item.name), 1523 gfx::FontList(), rect.size.width, gfx::ELIDE_TAIL)); 1524 1525 NSButton* addAccountsButton = 1526 [self linkButtonWithTitle:elidedButtonText 1527 frameOrigin:NSMakePoint( 1528 kHorizontalSpacing, kSmallVerticalSpacing) 1529 action:@selector(addAccount:)]; 1530 [container addSubview:addAccountsButton]; 1531 1532 NSView* accountEmails = [self createAccountsListWithRect:NSMakeRect( 1533 0, kAccountButtonHeight, rect.size.width, kAccountButtonHeight)]; 1534 [container addSubview:accountEmails]; 1535 1536 [container setFrameSize:NSMakeSize(rect.size.width, 1537 NSMaxY([accountEmails frame]))]; 1538 return container.autorelease(); 1539} 1540 1541- (NSView*)createAccountsListWithRect:(NSRect)rect { 1542 base::scoped_nsobject<NSView> container([[NSView alloc] initWithFrame:rect]); 1543 currentProfileAccounts_.clear(); 1544 1545 Profile* profile = browser_->profile(); 1546 std::string primaryAccount = 1547 SigninManagerFactory::GetForProfile(profile)->GetAuthenticatedUsername(); 1548 DCHECK(!primaryAccount.empty()); 1549 std::vector<std::string>accounts = 1550 profiles::GetSecondaryAccountsForProfile(profile, primaryAccount); 1551 1552 // If there is an account with an authentication error, it needs to be 1553 // badged with a warning icon. 1554 const SigninErrorController* errorController = 1555 profiles::GetSigninErrorController(profile); 1556 std::string errorAccountId = 1557 errorController ? errorController->error_account_id() : std::string(); 1558 1559 rect.origin.y = 0; 1560 for (size_t i = 0; i < accounts.size(); ++i) { 1561 // Save the original email address, as the button text could be elided. 1562 currentProfileAccounts_[i] = accounts[i]; 1563 NSButton* accountButton = 1564 [self accountButtonWithRect:rect 1565 title:accounts[i] 1566 tag:i 1567 reauthRequired:errorAccountId == accounts[i]]; 1568 [container addSubview:accountButton]; 1569 rect.origin.y = NSMaxY([accountButton frame]); 1570 } 1571 1572 // The primary account should always be listed first. 1573 NSButton* accountButton = 1574 [self accountButtonWithRect:rect 1575 title:primaryAccount 1576 tag:kPrimaryProfileTag 1577 reauthRequired:errorAccountId == primaryAccount]; 1578 [container addSubview:accountButton]; 1579 [container setFrameSize:NSMakeSize(NSWidth([container frame]), 1580 NSMaxY([accountButton frame]))]; 1581 return container.autorelease(); 1582} 1583 1584- (NSView*)buildGaiaEmbeddedView { 1585 base::scoped_nsobject<NSView> container( 1586 [[NSView alloc] initWithFrame:NSZeroRect]); 1587 CGFloat yOffset = 0; 1588 1589 GURL url; 1590 int messageId = -1; 1591 SigninErrorController* errorController = NULL; 1592 switch (viewMode_) { 1593 case profiles::BUBBLE_VIEW_MODE_GAIA_SIGNIN: 1594 url = signin::GetPromoURL(signin::SOURCE_AVATAR_BUBBLE_SIGN_IN, 1595 false /* auto_close */, 1596 true /* is_constrained */); 1597 messageId = IDS_PROFILES_GAIA_SIGNIN_TITLE; 1598 break; 1599 case profiles::BUBBLE_VIEW_MODE_GAIA_ADD_ACCOUNT: 1600 url = signin::GetPromoURL(signin::SOURCE_AVATAR_BUBBLE_ADD_ACCOUNT, 1601 false /* auto_close */, 1602 true /* is_constrained */); 1603 messageId = IDS_PROFILES_GAIA_ADD_ACCOUNT_TITLE; 1604 break; 1605 case profiles::BUBBLE_VIEW_MODE_GAIA_REAUTH: 1606 DCHECK(HasAuthError(browser_->profile())); 1607 errorController = profiles::GetSigninErrorController(browser_->profile()); 1608 url = signin::GetReauthURL( 1609 browser_->profile(), 1610 errorController ? errorController->error_username() : std::string()); 1611 messageId = IDS_PROFILES_GAIA_REAUTH_TITLE; 1612 break; 1613 default: 1614 NOTREACHED() << "Called with invalid mode=" << viewMode_; 1615 break; 1616 } 1617 1618 webContents_.reset(content::WebContents::Create( 1619 content::WebContents::CreateParams(browser_->profile()))); 1620 webContents_->GetController().LoadURL(url, 1621 content::Referrer(), 1622 content::PAGE_TRANSITION_AUTO_TOPLEVEL, 1623 std::string()); 1624 NSView* webview = webContents_->GetNativeView(); 1625 [webview setFrameSize:NSMakeSize(kFixedGaiaViewWidth, kFixedGaiaViewHeight)]; 1626 [container addSubview:webview]; 1627 yOffset = NSMaxY([webview frame]); 1628 1629 // Adds the title card. 1630 NSBox* separator = [self horizontalSeparatorWithFrame: 1631 NSMakeRect(0, yOffset, kFixedGaiaViewWidth, 0)]; 1632 [container addSubview:separator]; 1633 yOffset = NSMaxY([separator frame]) + kSmallVerticalSpacing; 1634 1635 NSView* titleView = BuildTitleCard( 1636 NSMakeRect(0, yOffset, kFixedGaiaViewWidth, 0), 1637 messageId, 1638 self /* backButtonTarget*/, 1639 @selector(navigateBackFromSigninPage:) /* backButtonAction */); 1640 [container addSubview:titleView]; 1641 yOffset = NSMaxY([titleView frame]); 1642 1643 [container setFrameSize:NSMakeSize(kFixedGaiaViewWidth, yOffset)]; 1644 return container.autorelease(); 1645} 1646 1647- (NSView*)buildAccountRemovalView { 1648 DCHECK(!accountIdToRemove_.empty()); 1649 1650 base::scoped_nsobject<NSView> container( 1651 [[NSView alloc] initWithFrame:NSZeroRect]); 1652 CGFloat availableWidth = 1653 kFixedAccountRemovalViewWidth - 2 * kHorizontalSpacing; 1654 CGFloat yOffset = kVerticalSpacing; 1655 1656 const std::string& primaryAccount = SigninManagerFactory::GetForProfile( 1657 browser_->profile())->GetAuthenticatedUsername(); 1658 bool isPrimaryAccount = primaryAccount == accountIdToRemove_; 1659 1660 // Adds "remove account" button at the bottom if needed. 1661 if (!isPrimaryAccount) { 1662 base::scoped_nsobject<NSButton> removeAccountButton( 1663 [[BlueLabelButton alloc] initWithFrame:NSZeroRect]); 1664 [removeAccountButton setTitle:l10n_util::GetNSString( 1665 IDS_PROFILES_ACCOUNT_REMOVAL_BUTTON)]; 1666 [removeAccountButton setTarget:self]; 1667 [removeAccountButton setAction:@selector(removeAccount:)]; 1668 [removeAccountButton sizeToFit]; 1669 [removeAccountButton setAlignment:NSCenterTextAlignment]; 1670 CGFloat xOffset = (kFixedAccountRemovalViewWidth - 1671 NSWidth([removeAccountButton frame])) / 2; 1672 [removeAccountButton setFrameOrigin:NSMakePoint(xOffset, yOffset)]; 1673 [container addSubview:removeAccountButton]; 1674 1675 yOffset = NSMaxY([removeAccountButton frame]) + kVerticalSpacing; 1676 } 1677 1678 NSView* contentView; 1679 NSPoint contentFrameOrigin = NSMakePoint(kHorizontalSpacing, yOffset); 1680 if (isPrimaryAccount) { 1681 std::vector<size_t> offsets; 1682 NSString* contentStr = l10n_util::GetNSStringF( 1683 IDS_PROFILES_PRIMARY_ACCOUNT_REMOVAL_TEXT, 1684 base::UTF8ToUTF16(accountIdToRemove_), base::string16(), &offsets); 1685 NSString* linkStr = l10n_util::GetNSString(IDS_PROFILES_SETTINGS_LINK); 1686 contentView = BuildFixedWidthTextViewWithLink(self, contentStr, linkStr, 1687 offsets[1], contentFrameOrigin, availableWidth); 1688 } else { 1689 NSString* contentStr = 1690 l10n_util::GetNSString(IDS_PROFILES_ACCOUNT_REMOVAL_TEXT); 1691 NSTextField* contentLabel = BuildLabel(contentStr, contentFrameOrigin, 1692 GetDialogBackgroundColor(), nil /* text_color */); 1693 [contentLabel setFrameSize:NSMakeSize(availableWidth, 0)]; 1694 [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:contentLabel]; 1695 contentView = contentLabel; 1696 } 1697 [container addSubview:contentView]; 1698 yOffset = NSMaxY([contentView frame]) + kVerticalSpacing; 1699 1700 // Adds the title card. 1701 NSBox* separator = [self horizontalSeparatorWithFrame: 1702 NSMakeRect(0, yOffset, kFixedAccountRemovalViewWidth, 0)]; 1703 [container addSubview:separator]; 1704 yOffset = NSMaxY([separator frame]) + kSmallVerticalSpacing; 1705 1706 NSView* titleView = BuildTitleCard( 1707 NSMakeRect(0, yOffset, kFixedAccountRemovalViewWidth,0), 1708 IDS_PROFILES_ACCOUNT_REMOVAL_TITLE, 1709 self /* backButtonTarget*/, 1710 @selector(showAccountManagement:) /* backButtonAction */); 1711 [container addSubview:titleView]; 1712 yOffset = NSMaxY([titleView frame]); 1713 1714 [container setFrameSize:NSMakeSize(kFixedAccountRemovalViewWidth, yOffset)]; 1715 return container.autorelease(); 1716} 1717 1718- (NSView*)buildEndPreviewView { 1719 base::scoped_nsobject<NSView> container( 1720 [[NSView alloc] initWithFrame:NSZeroRect]); 1721 CGFloat availableWidth = 1722 kFixedEndPreviewViewWidth - 2 * kHorizontalSpacing; 1723 CGFloat yOffset = kVerticalSpacing; 1724 1725 // Adds the "end preview and relaunch" button at the bottom. 1726 base::scoped_nsobject<NSButton> endPreviewAndRelaunchButton( 1727 [[BlueLabelButton alloc] initWithFrame:NSZeroRect]); 1728 [endPreviewAndRelaunchButton setTitle:l10n_util::GetNSString( 1729 IDS_PROFILES_END_PREVIEW_AND_RELAUNCH)]; 1730 [endPreviewAndRelaunchButton setTarget:self]; 1731 [endPreviewAndRelaunchButton setAction:@selector(endPreviewAndRelaunch:)]; 1732 [endPreviewAndRelaunchButton sizeToFit]; 1733 [endPreviewAndRelaunchButton setAlignment:NSCenterTextAlignment]; 1734 CGFloat xOffset = (kFixedEndPreviewViewWidth - 1735 NSWidth([endPreviewAndRelaunchButton frame])) / 2; 1736 [endPreviewAndRelaunchButton setFrameOrigin:NSMakePoint(xOffset, yOffset)]; 1737 [container addSubview:endPreviewAndRelaunchButton]; 1738 yOffset = NSMaxY([endPreviewAndRelaunchButton frame]) + kVerticalSpacing; 1739 1740 // Adds the main text label. 1741 NSPoint contentFrameOrigin = NSMakePoint(kHorizontalSpacing, yOffset); 1742 NSString* contentStr = 1743 l10n_util::GetNSString(IDS_PROFILES_END_PREVIEW_TEXT); 1744 NSTextField* contentLabel = BuildLabel(contentStr, contentFrameOrigin, 1745 GetDialogBackgroundColor(), nil /* text_color */); 1746 [contentLabel setFrameSize:NSMakeSize(availableWidth, 0)]; 1747 [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:contentLabel]; 1748 [container addSubview:contentLabel]; 1749 yOffset = NSMaxY([contentLabel frame]) + kVerticalSpacing; 1750 1751 // Adds the title card. 1752 NSBox* separator = [self horizontalSeparatorWithFrame: 1753 NSMakeRect(0, yOffset, kFixedEndPreviewViewWidth, 0)]; 1754 [container addSubview:separator]; 1755 yOffset = NSMaxY([separator frame]) + kSmallVerticalSpacing; 1756 1757 NSView* titleView = BuildTitleCard( 1758 NSMakeRect(0, yOffset, kFixedEndPreviewViewWidth, 0), 1759 IDS_PROFILES_END_PREVIEW, 1760 self /* backButtonTarget*/, 1761 @selector(showSendFeedbackTutorial:) /* backButtonAction */); 1762 [container addSubview:titleView]; 1763 yOffset = NSMaxY([titleView frame]); 1764 1765 [container setFrameSize:NSMakeSize(kFixedEndPreviewViewWidth, yOffset)]; 1766 return container.autorelease(); 1767} 1768 1769// Called when clicked on the settings link. 1770- (BOOL)textView:(NSTextView*)textView 1771 clickedOnLink:(id)link 1772 atIndex:(NSUInteger)charIndex { 1773 chrome::ShowSettings(browser_); 1774 return YES; 1775} 1776 1777- (NSButton*)hoverButtonWithRect:(NSRect)rect 1778 text:(NSString*)text 1779 imageResourceId:(int)imageResourceId 1780 alternateImageResourceId:(int)alternateImageResourceId 1781 action:(SEL)action { 1782 base::scoped_nsobject<BackgroundColorHoverButton> button( 1783 [[BackgroundColorHoverButton alloc] 1784 initWithFrame:rect 1785 imageTitleSpacing:kImageTitleSpacing 1786 backgroundColor:GetDialogBackgroundColor()]); 1787 1788 [button setTitle:text]; 1789 ui::ResourceBundle* rb = &ui::ResourceBundle::GetSharedInstance(); 1790 NSImage* alternateImage = rb->GetNativeImageNamed( 1791 alternateImageResourceId).ToNSImage(); 1792 [button setDefaultImage:rb->GetNativeImageNamed(imageResourceId).ToNSImage()]; 1793 [button setHoverImage:alternateImage]; 1794 [button setPressedImage:alternateImage]; 1795 [button setImagePosition:NSImageLeft]; 1796 [button setAlignment:NSLeftTextAlignment]; 1797 [button setBordered:NO]; 1798 [button setTarget:self]; 1799 [button setAction:action]; 1800 1801 return button.autorelease(); 1802} 1803 1804- (NSButton*)linkButtonWithTitle:(NSString*)title 1805 frameOrigin:(NSPoint)frameOrigin 1806 action:(SEL)action { 1807 base::scoped_nsobject<NSButton> link( 1808 [[HyperlinkButtonCell buttonWithString:title] retain]); 1809 1810 [[link cell] setShouldUnderline:NO]; 1811 [[link cell] setTextColor:gfx::SkColorToCalibratedNSColor( 1812 chrome_style::GetLinkColor())]; 1813 [link setTitle:title]; 1814 [link setBordered:NO]; 1815 [link setFont:[NSFont labelFontOfSize:kTextFontSize]]; 1816 [link setTarget:self]; 1817 [link setAction:action]; 1818 [link setFrameOrigin:frameOrigin]; 1819 [link sizeToFit]; 1820 1821 return link.autorelease(); 1822} 1823 1824- (NSButton*)accountButtonWithRect:(NSRect)rect 1825 title:(const std::string&)title 1826 tag:(int)tag 1827 reauthRequired:(BOOL)reauthRequired { 1828 ui::ResourceBundle* rb = &ui::ResourceBundle::GetSharedInstance(); 1829 NSImage* deleteImage = rb->GetNativeImageNamed(IDR_CLOSE_1).ToNSImage(); 1830 CGFloat deleteImageWidth = [deleteImage size].width; 1831 NSImage* warningImage = reauthRequired ? rb->GetNativeImageNamed( 1832 IDR_ICON_PROFILES_ACCOUNT_BUTTON_ERROR).ToNSImage() : nil; 1833 CGFloat warningImageWidth = [warningImage size].width; 1834 1835 CGFloat availableTextWidth = rect.size.width - kHorizontalSpacing - 1836 warningImageWidth - deleteImageWidth; 1837 if (warningImage) 1838 availableTextWidth -= kHorizontalSpacing; 1839 1840 NSColor* backgroundColor = gfx::SkColorToCalibratedNSColor( 1841 profiles::kAvatarBubbleAccountsBackgroundColor); 1842 base::scoped_nsobject<BackgroundColorHoverButton> button( 1843 [[BackgroundColorHoverButton alloc] initWithFrame:rect 1844 imageTitleSpacing:0 1845 backgroundColor:backgroundColor]); 1846 [button setTitle:ElideEmail(title, availableTextWidth)]; 1847 [button setAlignment:NSLeftTextAlignment]; 1848 [button setBordered:NO]; 1849 if (reauthRequired) { 1850 [button setDefaultImage:warningImage]; 1851 [button setImagePosition:NSImageLeft]; 1852 [button setTarget:self]; 1853 [button setAction:@selector(showAccountReauthenticationView:)]; 1854 [button setTag:tag]; 1855 } 1856 1857 // Delete button. 1858 NSRect buttonRect; 1859 NSDivideRect(rect, &buttonRect, &rect, 1860 deleteImageWidth + kHorizontalSpacing, NSMaxXEdge); 1861 buttonRect.origin.y = 0; 1862 1863 base::scoped_nsobject<HoverImageButton> deleteButton( 1864 [[HoverImageButton alloc] initWithFrame:buttonRect]); 1865 [deleteButton setBordered:NO]; 1866 [deleteButton setDefaultImage:deleteImage]; 1867 [deleteButton setHoverImage:rb->GetNativeImageNamed( 1868 IDR_CLOSE_1_H).ToNSImage()]; 1869 [deleteButton setPressedImage:rb->GetNativeImageNamed( 1870 IDR_CLOSE_1_P).ToNSImage()]; 1871 [deleteButton setTarget:self]; 1872 [deleteButton setAction:@selector(showAccountRemovalView:)]; 1873 [deleteButton setTag:tag]; 1874 1875 [button addSubview:deleteButton]; 1876 1877 return button.autorelease(); 1878} 1879 1880- (void)postActionPerformed:(ProfileMetrics::ProfileDesktopMenu)action { 1881 ProfileMetrics::LogProfileDesktopMenu(action, serviceType_); 1882 serviceType_ = signin::GAIA_SERVICE_TYPE_NONE; 1883} 1884 1885@end 1886