| // Copyright 2014 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #import <Cocoa/Cocoa.h> |
| |
| #import "chrome/browser/ui/cocoa/profiles/profile_chooser_controller.h" |
| |
| #include "base/mac/bundle_locations.h" |
| #include "base/prefs/pref_service.h" |
| #include "base/strings/sys_string_conversions.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "chrome/browser/browser_process.h" |
| #include "chrome/browser/chrome_notification_types.h" |
| #include "chrome/browser/lifetime/application_lifetime.h" |
| #include "chrome/browser/profiles/avatar_menu.h" |
| #include "chrome/browser/profiles/avatar_menu_observer.h" |
| #include "chrome/browser/profiles/profile_avatar_icon_util.h" |
| #include "chrome/browser/profiles/profile_info_cache.h" |
| #include "chrome/browser/profiles/profile_manager.h" |
| #include "chrome/browser/profiles/profile_metrics.h" |
| #include "chrome/browser/profiles/profile_window.h" |
| #include "chrome/browser/profiles/profiles_state.h" |
| #include "chrome/browser/signin/profile_oauth2_token_service_factory.h" |
| #include "chrome/browser/signin/signin_header_helper.h" |
| #include "chrome/browser/signin/signin_manager_factory.h" |
| #include "chrome/browser/signin/signin_promo.h" |
| #include "chrome/browser/ui/browser.h" |
| #include "chrome/browser/ui/browser_commands.h" |
| #include "chrome/browser/ui/browser_dialogs.h" |
| #include "chrome/browser/ui/browser_window.h" |
| #include "chrome/browser/ui/chrome_pages.h" |
| #include "chrome/browser/ui/chrome_style.h" |
| #import "chrome/browser/ui/cocoa/hyperlink_text_view.h" |
| #import "chrome/browser/ui/cocoa/info_bubble_view.h" |
| #import "chrome/browser/ui/cocoa/info_bubble_window.h" |
| #import "chrome/browser/ui/cocoa/profiles/user_manager_mac.h" |
| #include "chrome/browser/ui/singleton_tabs.h" |
| #include "chrome/common/pref_names.h" |
| #include "chrome/common/url_constants.h" |
| #include "components/signin/core/common/profile_management_switches.h" |
| #include "components/signin/core/browser/mutable_profile_oauth2_token_service.h" |
| #include "components/signin/core/browser/profile_oauth2_token_service.h" |
| #include "components/signin/core/browser/signin_manager.h" |
| #include "content/public/browser/notification_service.h" |
| #include "content/public/browser/web_contents.h" |
| #include "google_apis/gaia/oauth2_token_service.h" |
| #include "grit/chromium_strings.h" |
| #include "grit/generated_resources.h" |
| #include "grit/theme_resources.h" |
| #include "skia/ext/skia_utils_mac.h" |
| #import "third_party/google_toolbox_for_mac/src/AppKit/GTMUILocalizerAndLayoutTweaker.h" |
| #import "ui/base/cocoa/cocoa_base_utils.h" |
| #import "ui/base/cocoa/controls/blue_label_button.h" |
| #import "ui/base/cocoa/controls/hyperlink_button_cell.h" |
| #import "ui/base/cocoa/hover_image_button.h" |
| #include "ui/base/cocoa/window_size_constants.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/base/l10n/l10n_util_mac.h" |
| #include "ui/base/resource/resource_bundle.h" |
| #include "ui/gfx/image/image.h" |
| #include "ui/gfx/text_elider.h" |
| #include "ui/native_theme/native_theme.h" |
| |
| namespace { |
| |
| // Constants taken from the Windows/Views implementation at: |
| // chrome/browser/ui/views/profile_chooser_view.cc |
| const int kLargeImageSide = 88; |
| const int kSmallImageSide = 32; |
| const CGFloat kFixedMenuWidth = 250; |
| |
| const CGFloat kVerticalSpacing = 16.0; |
| const CGFloat kSmallVerticalSpacing = 10.0; |
| const CGFloat kHorizontalSpacing = 16.0; |
| const CGFloat kTitleFontSize = 15.0; |
| const CGFloat kTextFontSize = 12.0; |
| const CGFloat kProfileButtonHeight = 30; |
| const int kBezelThickness = 3; // Width of the bezel on an NSButton. |
| const int kImageTitleSpacing = 10; |
| const int kBlueButtonHeight = 30; |
| |
| // Fixed size for embedded sign in pages as defined in Gaia. |
| const CGFloat kFixedGaiaViewWidth = 360; |
| const CGFloat kFixedGaiaViewHeight = 400; |
| |
| // Fixed size for the account removal view. |
| const CGFloat kFixedAccountRemovalViewWidth = 280; |
| // Fixed size for the end-preview view. |
| const int kFixedEndPreviewViewWidth = 280; |
| |
| // Maximum number of times to show the welcome tutorial in the profile avatar |
| // bubble. |
| const int kProfileAvatarTutorialShowMax = 1; |
| |
| // The tag number for the primary account. |
| const int kPrimaryProfileTag = -1; |
| |
| gfx::Image CreateProfileImage(const gfx::Image& icon, int imageSize) { |
| return profiles::GetSizedAvatarIcon( |
| icon, true /* image is a square */, imageSize, imageSize); |
| } |
| |
| // Updates the window size and position. |
| void SetWindowSize(NSWindow* window, NSSize size) { |
| NSRect frame = [window frame]; |
| frame.origin.x += frame.size.width - size.width; |
| frame.origin.y += frame.size.height - size.height; |
| frame.size = size; |
| [window setFrame:frame display:YES]; |
| } |
| |
| NSString* ElideEmail(const std::string& email, CGFloat width) { |
| const base::string16 elidedEmail = gfx::ElideText( |
| base::UTF8ToUTF16(email), gfx::FontList(), width, gfx::ELIDE_EMAIL); |
| return base::SysUTF16ToNSString(elidedEmail); |
| } |
| |
| // Builds a label with the given |title| anchored at |frame_origin|. Sets the |
| // text color and background color to the given colors if not null. |
| NSTextField* BuildLabel(NSString* title, |
| NSPoint frame_origin, |
| NSColor* background_color, |
| NSColor* text_color) { |
| base::scoped_nsobject<NSTextField> label( |
| [[NSTextField alloc] initWithFrame:NSZeroRect]); |
| [label setStringValue:title]; |
| [label setEditable:NO]; |
| [label setAlignment:NSLeftTextAlignment]; |
| [label setBezeled:NO]; |
| [label setFont:[NSFont labelFontOfSize:kTextFontSize]]; |
| [label setFrameOrigin:frame_origin]; |
| [label sizeToFit]; |
| |
| if (background_color) |
| [[label cell] setBackgroundColor:background_color]; |
| if (text_color) |
| [[label cell] setTextColor:text_color]; |
| |
| return label.autorelease(); |
| } |
| |
| // Builds an NSTextView that has the contents set to the specified |message|, |
| // with a non-underlined |link| inserted at |link_offset|. The view is anchored |
| // at the specified |frame_origin| and has a fixed |frame_width|. |
| NSTextView* BuildFixedWidthTextViewWithLink( |
| id<NSTextViewDelegate> delegate, |
| NSString* message, |
| NSString* link, |
| int link_offset, |
| NSPoint frame_origin, |
| CGFloat frame_width) { |
| base::scoped_nsobject<HyperlinkTextView> text_view( |
| [[HyperlinkTextView alloc] initWithFrame:NSZeroRect]); |
| NSColor* link_color = gfx::SkColorToCalibratedNSColor( |
| chrome_style::GetLinkColor()); |
| // Adds a padding row at the bottom, because |boundingRectWithSize| below cuts |
| // off the last row sometimes. |
| [text_view setMessageAndLink:[NSString stringWithFormat:@"%@\n", message] |
| withLink:link |
| atOffset:link_offset |
| font:[NSFont labelFontOfSize:kTextFontSize] |
| messageColor:[NSColor blackColor] |
| linkColor:link_color]; |
| |
| // Removes the underlining from the link. |
| [text_view setLinkTextAttributes:nil]; |
| NSTextStorage* text_storage = [text_view textStorage]; |
| NSRange link_range = NSMakeRange(link_offset, [link length]); |
| [text_storage addAttribute:NSUnderlineStyleAttributeName |
| value:[NSNumber numberWithInt:NSUnderlineStyleNone] |
| range:link_range]; |
| |
| NSRect frame = [[text_view attributedString] |
| boundingRectWithSize:NSMakeSize(frame_width, 0) |
| options:NSStringDrawingUsesLineFragmentOrigin]; |
| frame.origin = frame_origin; |
| [text_view setFrame:frame]; |
| [text_view setDelegate:delegate]; |
| return text_view.autorelease(); |
| } |
| |
| // Returns the native dialog background color. |
| NSColor* GetDialogBackgroundColor() { |
| return gfx::SkColorToCalibratedNSColor( |
| ui::NativeTheme::instance()->GetSystemColor( |
| ui::NativeTheme::kColorId_DialogBackground)); |
| } |
| |
| // Builds a title card with one back button right aligned and one label center |
| // aligned. |
| NSView* BuildTitleCard(NSRect frame_rect, |
| int message_id, |
| id back_button_target, |
| SEL back_button_action) { |
| base::scoped_nsobject<NSView> container( |
| [[NSView alloc] initWithFrame:frame_rect]); |
| |
| base::scoped_nsobject<HoverImageButton> button( |
| [[HoverImageButton alloc] initWithFrame:frame_rect]); |
| [button setBordered:NO]; |
| ui::ResourceBundle* rb = &ui::ResourceBundle::GetSharedInstance(); |
| [button setDefaultImage:rb->GetNativeImageNamed(IDR_BACK).ToNSImage()]; |
| [button setHoverImage:rb->GetNativeImageNamed(IDR_BACK_H).ToNSImage()]; |
| [button setPressedImage:rb->GetNativeImageNamed(IDR_BACK_P).ToNSImage()]; |
| [button setTarget:back_button_target]; |
| [button setAction:back_button_action]; |
| [button setFrameSize:NSMakeSize(kProfileButtonHeight, kProfileButtonHeight)]; |
| [button setFrameOrigin:NSMakePoint(kHorizontalSpacing, 0)]; |
| |
| NSTextField* title_label = |
| BuildLabel(l10n_util::GetNSString(message_id), NSZeroPoint, |
| GetDialogBackgroundColor(), nil /* text_color */); |
| [title_label setAlignment:NSCenterTextAlignment]; |
| [title_label setFont:[NSFont labelFontOfSize:kTitleFontSize]]; |
| [title_label sizeToFit]; |
| CGFloat x_offset = (frame_rect.size.width - NSWidth([title_label frame])) / 2; |
| CGFloat y_offset = |
| (NSHeight([button frame]) - NSHeight([title_label frame])) / 2; |
| [title_label setFrameOrigin:NSMakePoint(x_offset, y_offset)]; |
| |
| [container addSubview:button]; |
| [container addSubview:title_label]; |
| CGFloat height = std::max(NSMaxY([title_label frame]), |
| NSMaxY([button frame])) + kSmallVerticalSpacing; |
| [container setFrameSize:NSMakeSize(NSWidth([container frame]), height)]; |
| |
| return container.autorelease(); |
| } |
| |
| bool HasAuthError(Profile* profile) { |
| const SigninErrorController* error_controller = |
| profiles::GetSigninErrorController(profile); |
| return error_controller && error_controller->HasError(); |
| } |
| |
| } // namespace |
| |
| // Class that listens to changes to the OAuth2Tokens for the active profile, |
| // changes to the avatar menu model or browser close notifications. |
| class ActiveProfileObserverBridge : public AvatarMenuObserver, |
| public content::NotificationObserver, |
| public OAuth2TokenService::Observer { |
| public: |
| ActiveProfileObserverBridge(ProfileChooserController* controller, |
| Browser* browser) |
| : controller_(controller), |
| browser_(browser), |
| token_observer_registered_(false) { |
| registrar_.Add(this, chrome::NOTIFICATION_BROWSER_CLOSING, |
| content::NotificationService::AllSources()); |
| if (!browser_->profile()->IsGuestSession()) |
| AddTokenServiceObserver(); |
| } |
| |
| virtual ~ActiveProfileObserverBridge() { |
| RemoveTokenServiceObserver(); |
| } |
| |
| private: |
| void AddTokenServiceObserver() { |
| ProfileOAuth2TokenService* oauth2_token_service = |
| ProfileOAuth2TokenServiceFactory::GetForProfile(browser_->profile()); |
| DCHECK(oauth2_token_service); |
| oauth2_token_service->AddObserver(this); |
| token_observer_registered_ = true; |
| } |
| |
| void RemoveTokenServiceObserver() { |
| if (!token_observer_registered_) |
| return; |
| DCHECK(browser_->profile()); |
| ProfileOAuth2TokenService* oauth2_token_service = |
| ProfileOAuth2TokenServiceFactory::GetForProfile(browser_->profile()); |
| DCHECK(oauth2_token_service); |
| oauth2_token_service->RemoveObserver(this); |
| token_observer_registered_ = false; |
| } |
| |
| // OAuth2TokenService::Observer: |
| virtual void OnRefreshTokenAvailable(const std::string& account_id) OVERRIDE { |
| // Tokens can only be added by adding an account through the inline flow, |
| // which is started from the account management view. Refresh it to show the |
| // update. |
| profiles::BubbleViewMode viewMode = [controller_ viewMode]; |
| if (viewMode == profiles::BUBBLE_VIEW_MODE_ACCOUNT_MANAGEMENT || |
| viewMode == profiles::BUBBLE_VIEW_MODE_GAIA_SIGNIN || |
| viewMode == profiles::BUBBLE_VIEW_MODE_GAIA_ADD_ACCOUNT || |
| viewMode == profiles::BUBBLE_VIEW_MODE_GAIA_REAUTH) { |
| [controller_ initMenuContentsWithView: |
| profiles::BUBBLE_VIEW_MODE_ACCOUNT_MANAGEMENT]; |
| } |
| } |
| |
| virtual void OnRefreshTokenRevoked(const std::string& account_id) OVERRIDE { |
| // Tokens can only be removed from the account management view. Refresh it |
| // to show the update. |
| if ([controller_ viewMode] == profiles::BUBBLE_VIEW_MODE_ACCOUNT_MANAGEMENT) |
| [controller_ initMenuContentsWithView: |
| profiles::BUBBLE_VIEW_MODE_ACCOUNT_MANAGEMENT]; |
| } |
| |
| // AvatarMenuObserver: |
| virtual void OnAvatarMenuChanged(AvatarMenu* avatar_menu) OVERRIDE { |
| // While the bubble is open, the avatar menu can only change from the |
| // profile chooser view by modifying the current profile's photo or name. |
| [controller_ |
| initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER]; |
| } |
| |
| // content::NotificationObserver: |
| virtual void Observe( |
| int type, |
| const content::NotificationSource& source, |
| const content::NotificationDetails& details) OVERRIDE { |
| DCHECK_EQ(chrome::NOTIFICATION_BROWSER_CLOSING, type); |
| if (browser_ == content::Source<Browser>(source).ptr()) { |
| RemoveTokenServiceObserver(); |
| // Clean up the bubble's WebContents (used by the Gaia embedded view), to |
| // make sure the guest profile doesn't have any dangling host renderers. |
| // This can happen if Chrome is quit using Command-Q while the bubble is |
| // still open, which won't give the bubble a chance to be closed and |
| // clean up the WebContents itself. |
| [controller_ cleanUpEmbeddedViewContents]; |
| } |
| } |
| |
| ProfileChooserController* controller_; // Weak; owns this. |
| Browser* browser_; // Weak. |
| content::NotificationRegistrar registrar_; |
| |
| // The observer can be removed both when closing the browser, and by just |
| // closing the avatar bubble. However, in the case of closing the browser, |
| // the avatar bubble will also be closed afterwards, resulting in a second |
| // attempt to remove the observer. This ensures the observer is only |
| // removed once. |
| bool token_observer_registered_; |
| |
| DISALLOW_COPY_AND_ASSIGN(ActiveProfileObserverBridge); |
| }; |
| |
| // Custom button cell that adds a left padding before the button image, and |
| // a custom spacing between the button image and title. |
| @interface CustomPaddingImageButtonCell : NSButtonCell { |
| @private |
| // Padding added to the left margin of the button. |
| int leftMarginSpacing_; |
| // Spacing between the cell image and title. |
| int imageTitleSpacing_; |
| } |
| |
| - (id)initWithLeftMarginSpacing:(int)leftMarginSpacing |
| imageTitleSpacing:(int)imageTitleSpacing; |
| @end |
| |
| @implementation CustomPaddingImageButtonCell |
| - (id)initWithLeftMarginSpacing:(int)leftMarginSpacing |
| imageTitleSpacing:(int)imageTitleSpacing { |
| if ((self = [super init])) { |
| leftMarginSpacing_ = leftMarginSpacing; |
| imageTitleSpacing_ = imageTitleSpacing; |
| } |
| return self; |
| } |
| |
| - (NSRect)drawTitle:(NSAttributedString*)title |
| withFrame:(NSRect)frame |
| inView:(NSView*)controlView { |
| NSRect marginRect; |
| NSDivideRect(frame, &marginRect, &frame, leftMarginSpacing_, NSMinXEdge); |
| |
| // The title frame origin isn't aware of the left margin spacing added |
| // in -drawImage, so it must be added when drawing the title as well. |
| if ([self imagePosition] == NSImageLeft) |
| NSDivideRect(frame, &marginRect, &frame, imageTitleSpacing_, NSMinXEdge); |
| |
| return [super drawTitle:title withFrame:frame inView:controlView]; |
| } |
| |
| - (void)drawImage:(NSImage*)image |
| withFrame:(NSRect)frame |
| inView:(NSView*)controlView { |
| if ([self imagePosition] == NSImageLeft) |
| frame.origin.x = leftMarginSpacing_; |
| [super drawImage:image withFrame:frame inView:controlView]; |
| } |
| |
| - (NSSize)cellSize { |
| NSSize buttonSize = [super cellSize]; |
| buttonSize.width += leftMarginSpacing_; |
| if ([self imagePosition] == NSImageLeft) |
| buttonSize.width += imageTitleSpacing_; |
| return buttonSize; |
| } |
| |
| @end |
| |
| // A custom button that has a transparent backround. |
| @interface TransparentBackgroundButton : NSButton |
| @end |
| |
| @implementation TransparentBackgroundButton |
| - (id)initWithFrame:(NSRect)frameRect { |
| if ((self = [super initWithFrame:frameRect])) { |
| [self setBordered:NO]; |
| [self setFont:[NSFont labelFontOfSize:kTextFontSize]]; |
| [self setButtonType:NSMomentaryChangeButton]; |
| } |
| return self; |
| } |
| |
| - (void)drawRect:(NSRect)dirtyRect { |
| NSColor* backgroundColor = [NSColor colorWithCalibratedWhite:1 alpha:0.6f]; |
| [backgroundColor setFill]; |
| NSRectFillUsingOperation(dirtyRect, NSCompositeSourceAtop); |
| [super drawRect:dirtyRect]; |
| } |
| @end |
| |
| // A custom image control that shows a "Change" button when moused over. |
| @interface EditableProfilePhoto : NSImageView { |
| @private |
| AvatarMenu* avatarMenu_; // Weak; Owned by ProfileChooserController. |
| base::scoped_nsobject<TransparentBackgroundButton> changePhotoButton_; |
| // Used to display the "Change" button on hover. |
| ui::ScopedCrTrackingArea trackingArea_; |
| ProfileChooserController* controller_; |
| } |
| |
| - (id)initWithFrame:(NSRect)frameRect |
| avatarMenu:(AvatarMenu*)avatarMenu |
| profileIcon:(const gfx::Image&)profileIcon |
| editingAllowed:(BOOL)editingAllowed |
| withController:(ProfileChooserController*)controller; |
| |
| // Called when the "Change" button is clicked. |
| - (void)editPhoto:(id)sender; |
| |
| // When hovering over the profile photo, show the "Change" button. |
| - (void)mouseEntered:(NSEvent*)event; |
| |
| // When hovering away from the profile photo, hide the "Change" button. |
| - (void)mouseExited:(NSEvent*)event; |
| @end |
| |
| @interface EditableProfilePhoto (Private) |
| // Create the "Change" avatar photo button. |
| - (TransparentBackgroundButton*)changePhotoButtonWithRect:(NSRect)rect; |
| @end |
| |
| @implementation EditableProfilePhoto |
| - (id)initWithFrame:(NSRect)frameRect |
| avatarMenu:(AvatarMenu*)avatarMenu |
| profileIcon:(const gfx::Image&)profileIcon |
| editingAllowed:(BOOL)editingAllowed |
| withController:(ProfileChooserController*)controller { |
| if ((self = [super initWithFrame:frameRect])) { |
| avatarMenu_ = avatarMenu; |
| controller_ = controller; |
| [self setImage:CreateProfileImage( |
| profileIcon, kLargeImageSide).ToNSImage()]; |
| |
| // Add a tracking area so that we can show/hide the button when hovering. |
| trackingArea_.reset([[CrTrackingArea alloc] |
| initWithRect:[self bounds] |
| options:NSTrackingMouseEnteredAndExited | NSTrackingActiveAlways |
| owner:self |
| userInfo:nil]); |
| [self addTrackingArea:trackingArea_.get()]; |
| |
| NSRect bounds = NSMakeRect(0, 0, kLargeImageSide, kLargeImageSide); |
| if (editingAllowed) { |
| changePhotoButton_.reset([self changePhotoButtonWithRect:bounds]); |
| [self addSubview:changePhotoButton_]; |
| |
| // Hide the button until the image is hovered over. |
| [changePhotoButton_ setHidden:YES]; |
| } |
| } |
| return self; |
| } |
| |
| - (void)drawRect:(NSRect)dirtyRect { |
| NSRect bounds = [self bounds]; |
| |
| // Display the profile picture as a circle. |
| NSBezierPath* path = [NSBezierPath bezierPathWithOvalInRect:bounds]; |
| [path addClip]; |
| [self.image drawAtPoint:bounds.origin |
| fromRect:bounds |
| operation:NSCompositeSourceOver |
| fraction:1.0]; |
| |
| } |
| |
| - (void)editPhoto:(id)sender { |
| avatarMenu_->EditProfile(avatarMenu_->GetActiveProfileIndex()); |
| [controller_ |
| postActionPerformed:ProfileMetrics::PROFILE_DESKTOP_MENU_EDIT_IMAGE]; |
| } |
| |
| - (void)mouseEntered:(NSEvent*)event { |
| [changePhotoButton_ setHidden:NO]; |
| } |
| |
| - (void)mouseExited:(NSEvent*)event { |
| [changePhotoButton_ setHidden:YES]; |
| } |
| |
| - (TransparentBackgroundButton*)changePhotoButtonWithRect:(NSRect)rect { |
| TransparentBackgroundButton* button = |
| [[TransparentBackgroundButton alloc] initWithFrame:rect]; |
| [button setImage:ui::ResourceBundle::GetSharedInstance().GetNativeImageNamed( |
| IDR_ICON_PROFILES_EDIT_CAMERA).AsNSImage()]; |
| [button setImagePosition:NSImageOnly]; |
| [button setTarget:self]; |
| [button setAction:@selector(editPhoto:)]; |
| return button; |
| } |
| @end |
| |
| // A custom text control that turns into a textfield for editing when clicked. |
| @interface EditableProfileNameButton : HoverImageButton { |
| @private |
| base::scoped_nsobject<NSTextField> profileNameTextField_; |
| Profile* profile_; // Weak. |
| ProfileChooserController* controller_; |
| } |
| |
| - (id)initWithFrame:(NSRect)frameRect |
| profile:(Profile*)profile |
| profileName:(NSString*)profileName |
| editingAllowed:(BOOL)editingAllowed |
| withController:(ProfileChooserController*)controller; |
| |
| // Called when the button is clicked. |
| - (void)showEditableView:(id)sender; |
| |
| // Called when enter is pressed in the text field. |
| - (void)saveProfileName:(id)sender; |
| |
| @end |
| |
| @implementation EditableProfileNameButton |
| - (id)initWithFrame:(NSRect)frameRect |
| profile:(Profile*)profile |
| profileName:(NSString*)profileName |
| editingAllowed:(BOOL)editingAllowed |
| withController:(ProfileChooserController*)controller { |
| if ((self = [super initWithFrame:frameRect])) { |
| profile_ = profile; |
| controller_ = controller; |
| |
| if (editingAllowed) { |
| // Show an "edit" pencil icon when hovering over. In the default state, |
| // we need to create an empty placeholder of the correct size, so that |
| // the text doesn't jump around when the hovered icon appears. |
| ui::ResourceBundle* rb = &ui::ResourceBundle::GetSharedInstance(); |
| NSImage* hoverImage = rb->GetNativeImageNamed( |
| IDR_ICON_PROFILES_EDIT_HOVER).AsNSImage(); |
| |
| // In order to center the button title, we need to add a left padding of |
| // the same width as the pencil icon. |
| base::scoped_nsobject<CustomPaddingImageButtonCell> cell( |
| [[CustomPaddingImageButtonCell alloc] |
| initWithLeftMarginSpacing:[hoverImage size].width |
| imageTitleSpacing:0]); |
| [self setCell:cell.get()]; |
| |
| NSImage* placeholder = [[NSImage alloc] initWithSize:[hoverImage size]]; |
| [self setDefaultImage:placeholder]; |
| [self setHoverImage:hoverImage]; |
| [self setAlternateImage: |
| rb->GetNativeImageNamed(IDR_ICON_PROFILES_EDIT_PRESSED).AsNSImage()]; |
| [self setImagePosition:NSImageRight]; |
| [self setTarget:self]; |
| [self setAction:@selector(showEditableView:)]; |
| |
| // We need to subtract the width of the bezel from the frame rect, so that |
| // the textfield can take the exact same space as the button. |
| frameRect.size.height -= 2 * kBezelThickness; |
| frameRect.origin = NSMakePoint(0, kBezelThickness); |
| profileNameTextField_.reset( |
| [[NSTextField alloc] initWithFrame:frameRect]); |
| [profileNameTextField_ setStringValue:profileName]; |
| [profileNameTextField_ setFont:[NSFont labelFontOfSize:kTitleFontSize]]; |
| [profileNameTextField_ setEditable:YES]; |
| [profileNameTextField_ setDrawsBackground:YES]; |
| [profileNameTextField_ setBezeled:YES]; |
| [profileNameTextField_ setAlignment:NSCenterTextAlignment]; |
| [[profileNameTextField_ cell] setWraps:NO]; |
| [[profileNameTextField_ cell] setLineBreakMode: |
| NSLineBreakByTruncatingTail]; |
| [self addSubview:profileNameTextField_]; |
| [profileNameTextField_ setTarget:self]; |
| [profileNameTextField_ setAction:@selector(saveProfileName:)]; |
| |
| // Hide the textfield until the user clicks on the button. |
| [profileNameTextField_ setHidden:YES]; |
| } |
| |
| [self setBordered:NO]; |
| [self setFont:[NSFont labelFontOfSize:kTitleFontSize]]; |
| [self setAlignment:NSCenterTextAlignment]; |
| [[self cell] setLineBreakMode:NSLineBreakByTruncatingTail]; |
| [self setTitle:profileName]; |
| } |
| return self; |
| } |
| |
| - (void)saveProfileName:(id)sender { |
| NSString* text = [profileNameTextField_ stringValue]; |
| // Empty profile names are not allowed, and are treated as a cancel. |
| if ([text length] > 0) { |
| profiles::UpdateProfileName(profile_, base::SysNSStringToUTF16(text)); |
| [controller_ |
| postActionPerformed:ProfileMetrics::PROFILE_DESKTOP_MENU_EDIT_NAME]; |
| [self setTitle:text]; |
| } |
| [profileNameTextField_ setHidden:YES]; |
| } |
| |
| - (void)showEditableView:(id)sender { |
| [profileNameTextField_ setHidden:NO]; |
| [[self window] makeFirstResponder:profileNameTextField_]; |
| } |
| |
| @end |
| |
| // A custom button that allows for setting a background color when hovered over. |
| @interface BackgroundColorHoverButton : HoverImageButton { |
| @private |
| base::scoped_nsobject<NSColor> backgroundColor_; |
| base::scoped_nsobject<NSColor> hoverColor_; |
| } |
| @end |
| |
| @implementation BackgroundColorHoverButton |
| |
| - (id)initWithFrame:(NSRect)frameRect |
| imageTitleSpacing:(int)imageTitleSpacing |
| backgroundColor:(NSColor*)backgroundColor { |
| if ((self = [super initWithFrame:frameRect])) { |
| backgroundColor_.reset([backgroundColor retain]); |
| hoverColor_.reset([gfx::SkColorToCalibratedNSColor( |
| ui::NativeTheme::instance()->GetSystemColor( |
| ui::NativeTheme::kColorId_ButtonHoverBackgroundColor)) retain]); |
| |
| [self setBordered:NO]; |
| [self setFont:[NSFont labelFontOfSize:kTextFontSize]]; |
| [self setButtonType:NSMomentaryChangeButton]; |
| |
| base::scoped_nsobject<CustomPaddingImageButtonCell> cell( |
| [[CustomPaddingImageButtonCell alloc] |
| initWithLeftMarginSpacing:kHorizontalSpacing |
| imageTitleSpacing:imageTitleSpacing]); |
| [cell setLineBreakMode:NSLineBreakByTruncatingTail]; |
| [self setCell:cell.get()]; |
| } |
| return self; |
| } |
| |
| - (void)setHoverState:(HoverState)state { |
| [super setHoverState:state]; |
| bool isHighlighted = ([self hoverState] != kHoverStateNone); |
| |
| NSColor* backgroundColor = isHighlighted ? hoverColor_ : backgroundColor_; |
| [[self cell] setBackgroundColor:backgroundColor]; |
| } |
| |
| @end |
| |
| // A custom view with the given background color. |
| @interface BackgroundColorView : NSView { |
| @private |
| base::scoped_nsobject<NSColor> backgroundColor_; |
| } |
| @end |
| |
| @implementation BackgroundColorView |
| - (id)initWithFrame:(NSRect)frameRect |
| withColor:(NSColor*)color { |
| if ((self = [super initWithFrame:frameRect])) |
| backgroundColor_.reset([color retain]); |
| return self; |
| } |
| |
| - (void)drawRect:(NSRect)dirtyRect { |
| [backgroundColor_ setFill]; |
| NSRectFill(dirtyRect); |
| [super drawRect:dirtyRect]; |
| } |
| @end |
| |
| @interface ProfileChooserController () |
| // Builds the profile chooser view. |
| - (NSView*)buildProfileChooserView; |
| |
| // Builds a tutorial card with a title label using |titleMessageId|, a content |
| // label using |contentMessageId|, and a bottom row with a right-aligned link |
| // using |linkMessageId|, and a left aligned button using |buttonMessageId|. |
| // On click, the link would execute |linkAction|, and the button would execute |
| // |buttonAction|. It sets |tutorialMode_| to the given |mode|. |
| - (NSView*)tutorialViewWithMode:(profiles::TutorialMode)mode |
| titleMessage:(int)titleMessageId |
| contentMessage:(int)contentMessageId |
| linkMessage:(int)linkMessageId |
| buttonMessage:(int)buttonMessageId |
| linkAction:(SEL)linkAction |
| buttonAction:(SEL)buttonAction; |
| |
| // Builds a a tutorial card for new profile management preview if needed. if |
| // new profile management is not enabled yet, then it prompts the user to try |
| // out the feature. Otherwise, it notifies the user that the feature has been |
| // enabled if needed. |
| - (NSView*)buildPreviewTutorialIfNeeded:(const AvatarMenu::Item&)item; |
| |
| // Creates the main profile card for the profile |item| at the top of |
| // the bubble. |
| - (NSView*)createCurrentProfileView:(const AvatarMenu::Item&)item; |
| |
| // Creates the possible links for the main profile card with profile |item|. |
| - (NSView*)createCurrentProfileLinksForItem:(const AvatarMenu::Item&)item |
| rect:(NSRect)rect; |
| |
| // Creates the disclaimer text for supervised users, telling them that the |
| // manager can view their history etc. |
| - (NSView*)createSupervisedUserDisclaimerView; |
| |
| // Creates a main profile card for the guest user. |
| - (NSView*)createGuestProfileView; |
| |
| // Creates an item for the profile |itemIndex| that is used in the fast profile |
| // switcher in the middle of the bubble. |
| - (NSButton*)createOtherProfileView:(int)itemIndex; |
| |
| // Creates the "Not you" and Lock option buttons. |
| - (NSView*)createOptionsViewWithRect:(NSRect)rect |
| enableLock:(BOOL)enableLock; |
| |
| // Creates the account management view for the active profile. |
| - (NSView*)createCurrentProfileAccountsView:(NSRect)rect; |
| |
| // Creates the list of accounts for the active profile. |
| - (NSView*)createAccountsListWithRect:(NSRect)rect; |
| |
| // Creates the Gaia sign-in/add account view. |
| - (NSView*)buildGaiaEmbeddedView; |
| |
| // Creates the account removal view. |
| - (NSView*)buildAccountRemovalView; |
| |
| // Creates the end-preview view. |
| - (NSView*)buildEndPreviewView; |
| |
| // Creates a button with |text|, an icon given by |imageResourceId| and with |
| // |action|. The icon |alternateImageResourceId| is displayed in the button's |
| // hovered and pressed states. |
| - (NSButton*)hoverButtonWithRect:(NSRect)rect |
| text:(NSString*)text |
| imageResourceId:(int)imageResourceId |
| alternateImageResourceId:(int)alternateImageResourceId |
| action:(SEL)action; |
| |
| // Creates a generic link button with |title| and an |action| positioned at |
| // |frameOrigin|. |
| - (NSButton*)linkButtonWithTitle:(NSString*)title |
| frameOrigin:(NSPoint)frameOrigin |
| action:(SEL)action; |
| |
| // Creates an email account button with |title| and a remove icon. If |
| // |reauthRequired| is true, the button also displays a warning icon. |tag| |
| // indicates which account the button refers to. |
| - (NSButton*)accountButtonWithRect:(NSRect)rect |
| title:(const std::string&)title |
| tag:(int)tag |
| reauthRequired:(BOOL)reauthRequired; |
| @end |
| |
| @implementation ProfileChooserController |
| - (profiles::BubbleViewMode) viewMode { |
| return viewMode_; |
| } |
| |
| - (IBAction)switchToProfile:(id)sender { |
| // Check the event flags to see if a new window should be created. |
| bool alwaysCreate = ui::WindowOpenDispositionFromNSEvent( |
| [NSApp currentEvent]) == NEW_WINDOW; |
| avatarMenu_->SwitchToProfile([sender tag], alwaysCreate, |
| ProfileMetrics::SWITCH_PROFILE_ICON); |
| } |
| |
| - (IBAction)showUserManager:(id)sender { |
| profiles::ShowUserManagerMaybeWithTutorial(browser_->profile()); |
| } |
| |
| - (IBAction)exitGuest:(id)sender { |
| DCHECK(browser_->profile()->IsGuestSession()); |
| [self showUserManager:sender]; |
| profiles::CloseGuestProfileWindows(); |
| } |
| |
| - (IBAction)showAccountManagement:(id)sender { |
| [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_ACCOUNT_MANAGEMENT]; |
| } |
| |
| - (IBAction)hideAccountManagement:(id)sender { |
| [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER]; |
| } |
| |
| - (IBAction)lockProfile:(id)sender { |
| profiles::LockProfile(browser_->profile()); |
| [self postActionPerformed:ProfileMetrics::PROFILE_DESKTOP_MENU_LOCK]; |
| } |
| |
| - (IBAction)showInlineSigninPage:(id)sender { |
| [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_GAIA_SIGNIN]; |
| } |
| |
| - (IBAction)showTabbedSigninPage:(id)sender { |
| chrome::ShowBrowserSignin(browser_, signin::SOURCE_MENU); |
| } |
| |
| - (IBAction)addAccount:(id)sender { |
| [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_GAIA_ADD_ACCOUNT]; |
| [self postActionPerformed:ProfileMetrics::PROFILE_DESKTOP_MENU_ADD_ACCT]; |
| } |
| |
| - (IBAction)navigateBackFromSigninPage:(id)sender { |
| std::string primaryAccount = SigninManagerFactory::GetForProfile( |
| browser_->profile())->GetAuthenticatedUsername(); |
| [self initMenuContentsWithView:primaryAccount.empty() ? |
| profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER : |
| profiles::BUBBLE_VIEW_MODE_ACCOUNT_MANAGEMENT]; |
| } |
| |
| - (IBAction)showAccountRemovalView:(id)sender { |
| DCHECK(!isGuestSession_); |
| |
| // Tag is either |kPrimaryProfileTag| for the primary account, or equal to the |
| // index in |currentProfileAccounts_| for a secondary account. |
| int tag = [sender tag]; |
| if (tag == kPrimaryProfileTag) { |
| accountIdToRemove_ = SigninManagerFactory::GetForProfile( |
| browser_->profile())->GetAuthenticatedUsername(); |
| } else { |
| DCHECK(ContainsKey(currentProfileAccounts_, tag)); |
| accountIdToRemove_ = currentProfileAccounts_[tag]; |
| } |
| |
| [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_ACCOUNT_REMOVAL]; |
| } |
| |
| - (IBAction)showAccountReauthenticationView:(id)sender { |
| DCHECK(!isGuestSession_); |
| [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_GAIA_REAUTH]; |
| } |
| |
| - (IBAction)removeAccount:(id)sender { |
| DCHECK(!accountIdToRemove_.empty()); |
| ProfileOAuth2TokenServiceFactory::GetPlatformSpecificForProfile( |
| browser_->profile())->RevokeCredentials(accountIdToRemove_); |
| [self postActionPerformed:ProfileMetrics::PROFILE_DESKTOP_MENU_REMOVE_ACCT]; |
| accountIdToRemove_.clear(); |
| |
| [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_ACCOUNT_MANAGEMENT]; |
| } |
| |
| - (IBAction)openTutorialLearnMoreURL:(id)sender { |
| ProfileMetrics::LogProfileUpgradeEnrollment( |
| ProfileMetrics::PROFILE_ENROLLMENT_LAUNCH_LEARN_MORE); |
| // TODO(guohui): update |learnMoreUrl| once it is decided. |
| const GURL learnMoreUrl("https://support.google.com/chrome/?hl=en#to"); |
| chrome::NavigateParams params(browser_->profile(), learnMoreUrl, |
| content::PAGE_TRANSITION_LINK); |
| params.disposition = NEW_FOREGROUND_TAB; |
| chrome::Navigate(¶ms); |
| } |
| |
| - (IBAction)enableNewProfileManagementPreview:(id)sender { |
| ProfileMetrics::LogProfileUpgradeEnrollment( |
| ProfileMetrics::PROFILE_ENROLLMENT_ACCEPT_NEW_PROFILE_MGMT); |
| profiles::EnableNewProfileManagementPreview(browser_->profile()); |
| } |
| |
| - (IBAction)dismissTutorial:(id)sender { |
| ProfileMetrics::LogProfileUpgradeEnrollment( |
| ProfileMetrics::PROFILE_ENROLLMENT_CLOSE_WELCOME_CARD); |
| // If the user manually dismissed the tutorial, never show it again by setting |
| // the number of times shown to the maximum plus 1, so that later we could |
| // distinguish between the dismiss case and the case when the tutorial is |
| // indeed shown for the maximum number of times. |
| browser_->profile()->GetPrefs()->SetInteger( |
| prefs::kProfileAvatarTutorialShown, kProfileAvatarTutorialShowMax + 1); |
| [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER]; |
| } |
| |
| - (IBAction)showSendFeedbackTutorial:(id)sender { |
| ProfileMetrics::LogProfileUpgradeEnrollment( |
| ProfileMetrics::PROFILE_ENROLLMENT_SEND_FEEDBACK); |
| tutorialMode_ = profiles::TUTORIAL_MODE_SEND_FEEDBACK; |
| [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER]; |
| } |
| |
| - (IBAction)showEndPreviewView:(id)sender { |
| [self initMenuContentsWithView:profiles::BUBBLE_VIEW_MODE_END_PREVIEW]; |
| } |
| |
| - (IBAction)sendFeedback:(id)sender { |
| chrome::OpenFeedbackDialog(browser_); |
| } |
| |
| - (IBAction)endPreviewAndRelaunch:(id)sender { |
| profiles::DisableNewProfileManagementPreview(browser_->profile()); |
| } |
| |
| - (void)cleanUpEmbeddedViewContents { |
| webContents_.reset(); |
| } |
| |
| - (id)initWithBrowser:(Browser*)browser |
| anchoredAt:(NSPoint)point |
| withMode:(profiles::BubbleViewMode)mode |
| withServiceType:(signin::GAIAServiceType)serviceType { |
| base::scoped_nsobject<InfoBubbleWindow> window([[InfoBubbleWindow alloc] |
| initWithContentRect:ui::kWindowSizeDeterminedLater |
| styleMask:NSBorderlessWindowMask |
| backing:NSBackingStoreBuffered |
| defer:NO]); |
| |
| if ((self = [super initWithWindow:window |
| parentWindow:browser->window()->GetNativeWindow() |
| anchoredAt:point])) { |
| browser_ = browser; |
| viewMode_ = mode; |
| tutorialMode_ = profiles::TUTORIAL_MODE_NONE; |
| observer_.reset(new ActiveProfileObserverBridge(self, browser_)); |
| serviceType_ = serviceType; |
| |
| avatarMenu_.reset(new AvatarMenu( |
| &g_browser_process->profile_manager()->GetProfileInfoCache(), |
| observer_.get(), |
| browser_)); |
| avatarMenu_->RebuildMenu(); |
| |
| // Guest profiles do not have a token service. |
| isGuestSession_ = browser_->profile()->IsGuestSession(); |
| |
| // If view mode is PROFILE_CHOOSER but there is an auth error, force |
| // ACCOUNT_MANAGEMENT mode. |
| if (viewMode_ == profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER && |
| HasAuthError(browser_->profile())) { |
| viewMode_ = profiles::BUBBLE_VIEW_MODE_ACCOUNT_MANAGEMENT; |
| } |
| |
| [[self bubble] setAlignment:info_bubble::kAlignRightEdgeToAnchorEdge]; |
| [[self bubble] setArrowLocation:info_bubble::kNoArrow]; |
| [[self bubble] setBackgroundColor:GetDialogBackgroundColor()]; |
| [self initMenuContentsWithView:viewMode_]; |
| } |
| |
| return self; |
| } |
| |
| - (void)initMenuContentsWithView:(profiles::BubbleViewMode)viewToDisplay { |
| if (browser_->profile()->IsSupervised() && |
| (viewToDisplay == profiles::BUBBLE_VIEW_MODE_GAIA_ADD_ACCOUNT || |
| viewToDisplay == profiles::BUBBLE_VIEW_MODE_ACCOUNT_REMOVAL)) { |
| LOG(WARNING) << "Supervised user attempted to add/remove account"; |
| return; |
| } |
| viewMode_ = viewToDisplay; |
| NSView* contentView = [[self window] contentView]; |
| [contentView setSubviews:[NSArray array]]; |
| NSView* subView; |
| |
| switch (viewMode_) { |
| case profiles::BUBBLE_VIEW_MODE_GAIA_SIGNIN: |
| case profiles::BUBBLE_VIEW_MODE_GAIA_ADD_ACCOUNT: |
| case profiles::BUBBLE_VIEW_MODE_GAIA_REAUTH: |
| subView = [self buildGaiaEmbeddedView]; |
| break; |
| case profiles::BUBBLE_VIEW_MODE_ACCOUNT_REMOVAL: |
| subView = [self buildAccountRemovalView]; |
| break; |
| case profiles::BUBBLE_VIEW_MODE_END_PREVIEW: |
| subView = [self buildEndPreviewView]; |
| break; |
| case profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER: |
| case profiles::BUBBLE_VIEW_MODE_ACCOUNT_MANAGEMENT: |
| subView = [self buildProfileChooserView]; |
| break; |
| } |
| |
| [contentView addSubview:subView]; |
| SetWindowSize([self window], |
| NSMakeSize(NSWidth([subView frame]), NSHeight([subView frame]))); |
| } |
| |
| - (NSView*)buildProfileChooserView { |
| base::scoped_nsobject<NSView> container( |
| [[NSView alloc] initWithFrame:NSZeroRect]); |
| |
| NSView* tutorialView = nil; |
| NSView* currentProfileView = nil; |
| base::scoped_nsobject<NSMutableArray> otherProfiles( |
| [[NSMutableArray alloc] init]); |
| // Local and guest profiles cannot lock their profile. |
| bool enableLock = false; |
| // Store the most recently displayed tutorial mode |
| profiles::TutorialMode lastTutorialMode = tutorialMode_; |
| |
| // Loop over the profiles in reverse, so that they are sorted by their |
| // y-coordinate, and separate them into active and "other" profiles. |
| for (int i = avatarMenu_->GetNumberOfItems() - 1; i >= 0; --i) { |
| const AvatarMenu::Item& item = avatarMenu_->GetItemAt(i); |
| if (item.active) { |
| if (viewMode_ == profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER) |
| tutorialView = [self buildPreviewTutorialIfNeeded:item]; |
| currentProfileView = [self createCurrentProfileView:item]; |
| enableLock = item.signed_in; |
| } else { |
| [otherProfiles addObject:[self createOtherProfileView:i]]; |
| } |
| } |
| if (!currentProfileView) // Guest windows don't have an active profile. |
| currentProfileView = [self createGuestProfileView]; |
| |
| // |yOffset| is the next position at which to draw in |container| |
| // coordinates. Add a pixel offset so that the bottom option buttons don't |
| // overlap the bubble's rounded corners. |
| CGFloat yOffset = 1; |
| |
| // Option buttons. Only available with the new profile management flag. |
| if (switches::IsNewProfileManagement()) { |
| NSRect rect = NSMakeRect(0, yOffset, kFixedMenuWidth, 0); |
| NSView* optionsView = [self createOptionsViewWithRect:rect |
| enableLock:enableLock]; |
| [container addSubview:optionsView]; |
| rect.origin.y = NSMaxY([optionsView frame]); |
| |
| NSBox* separator = [self horizontalSeparatorWithFrame:rect]; |
| [container addSubview:separator]; |
| yOffset = NSMaxY([separator frame]); |
| } |
| |
| if (viewMode_ == profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER && |
| switches::IsFastUserSwitching()) { |
| // Other profiles switcher. The profiles have already been sorted |
| // by their y-coordinate, so they can be added in the existing order. |
| for (NSView *otherProfileView in otherProfiles.get()) { |
| [otherProfileView setFrameOrigin:NSMakePoint(0, yOffset)]; |
| [container addSubview:otherProfileView]; |
| yOffset = NSMaxY([otherProfileView frame]); |
| |
| NSBox* separator = [self horizontalSeparatorWithFrame:NSMakeRect( |
| 0, yOffset, kFixedMenuWidth, 0)]; |
| [container addSubview:separator]; |
| yOffset = NSMaxY([separator frame]); |
| } |
| } |
| |
| // For supervised users, add the disclaimer text. |
| if (browser_->profile()->IsSupervised()) { |
| yOffset += kSmallVerticalSpacing; |
| NSView* disclaimerContainer = [self createSupervisedUserDisclaimerView]; |
| [disclaimerContainer setFrameOrigin:NSMakePoint(0, yOffset)]; |
| [container addSubview:disclaimerContainer]; |
| yOffset = NSMaxY([disclaimerContainer frame]); |
| yOffset += kSmallVerticalSpacing; |
| |
| NSBox* separator = [self horizontalSeparatorWithFrame:NSMakeRect( |
| 0, yOffset, kFixedMenuWidth, 0)]; |
| [container addSubview:separator]; |
| yOffset = NSMaxY([separator frame]); |
| } |
| |
| if (viewMode_ == profiles::BUBBLE_VIEW_MODE_ACCOUNT_MANAGEMENT) { |
| NSView* currentProfileAccountsView = [self createCurrentProfileAccountsView: |
| NSMakeRect(0, yOffset, kFixedMenuWidth, 0)]; |
| [container addSubview:currentProfileAccountsView]; |
| yOffset = NSMaxY([currentProfileAccountsView frame]); |
| |
| NSBox* accountsSeparator = [self horizontalSeparatorWithFrame: |
| NSMakeRect(0, yOffset, kFixedMenuWidth, 0)]; |
| [container addSubview:accountsSeparator]; |
| yOffset = NSMaxY([accountsSeparator frame]); |
| } |
| |
| // Active profile card. |
| if (currentProfileView) { |
| yOffset += kVerticalSpacing; |
| [currentProfileView setFrameOrigin:NSMakePoint(0, yOffset)]; |
| [container addSubview:currentProfileView]; |
| yOffset = NSMaxY([currentProfileView frame]) + kVerticalSpacing; |
| } |
| |
| if (tutorialView) { |
| [tutorialView setFrameOrigin:NSMakePoint(0, yOffset)]; |
| [container addSubview:tutorialView]; |
| yOffset = NSMaxY([tutorialView frame]); |
| if (!switches::IsNewProfileManagement() && |
| tutorialMode_ != lastTutorialMode) { |
| ProfileMetrics::LogProfileUpgradeEnrollment( |
| ProfileMetrics::PROFILE_ENROLLMENT_SHOW_PREVIEW_PROMO); |
| } |
| } else { |
| tutorialMode_ = profiles::TUTORIAL_MODE_NONE; |
| } |
| |
| [container setFrameSize:NSMakeSize(kFixedMenuWidth, yOffset)]; |
| return container.autorelease(); |
| } |
| |
| - (NSView*)buildPreviewTutorialIfNeeded:(const AvatarMenu::Item&)item { |
| if (!switches::IsNewProfileManagement()) { |
| return [self tutorialViewWithMode:profiles::TUTORIAL_MODE_ENABLE_PREVIEW |
| titleMessage:IDS_PROFILES_PREVIEW_TUTORIAL_TITLE |
| contentMessage:IDS_PROFILES_PREVIEW_TUTORIAL_CONTENT_TEXT |
| linkMessage:IDS_PROFILES_PROFILE_TUTORIAL_LEARN_MORE |
| buttonMessage:IDS_PROFILES_TUTORIAL_TRY_BUTTON |
| linkAction:@selector(openTutorialLearnMoreURL:) |
| buttonAction: |
| @selector(enableNewProfileManagementPreview:)]; |
| } |
| |
| if (!switches::IsNewProfileManagementPreviewEnabled()) |
| return nil; |
| |
| if (tutorialMode_ == profiles::TUTORIAL_MODE_SEND_FEEDBACK) { |
| return [self tutorialViewWithMode:profiles::TUTORIAL_MODE_SEND_FEEDBACK |
| titleMessage:IDS_PROFILES_FEEDBACK_TUTORIAL_TITLE |
| contentMessage: |
| IDS_PROFILES_FEEDBACK_TUTORIAL_CONTENT_TEXT |
| linkMessage:IDS_PROFILES_END_PREVIEW |
| buttonMessage:IDS_PROFILES_SEND_FEEDBACK_BUTTON |
| linkAction:@selector(showEndPreviewView:) |
| buttonAction:@selector(sendFeedback:)]; |
| } |
| |
| Profile* profile = browser_->profile(); |
| const int showCount = profile->GetPrefs()->GetInteger( |
| prefs::kProfileAvatarTutorialShown); |
| // Do not show the tutorial if user has dismissed it. |
| if (showCount > kProfileAvatarTutorialShowMax) |
| return nil; |
| |
| if (tutorialMode_ != profiles::TUTORIAL_MODE_WELCOME) { |
| if (showCount == kProfileAvatarTutorialShowMax) |
| return nil; |
| profile->GetPrefs()->SetInteger( |
| prefs::kProfileAvatarTutorialShown, showCount + 1); |
| } |
| |
| return [self tutorialViewWithMode:profiles::TUTORIAL_MODE_WELCOME |
| titleMessage:IDS_PROFILES_PREVIEW_ENABLED_TUTORIAL_TITLE |
| contentMessage: |
| IDS_PROFILES_PREVIEW_ENABLED_TUTORIAL_CONTENT_TEXT |
| linkMessage:IDS_PROFILES_PROFILE_TUTORIAL_LEARN_MORE |
| buttonMessage:IDS_PROFILES_TUTORIAL_OK_BUTTON |
| linkAction:@selector(openTutorialLearnMoreURL:) |
| buttonAction:@selector(dismissTutorial:)]; |
| } |
| |
| - (NSView*)tutorialViewWithMode:(profiles::TutorialMode)mode |
| titleMessage:(int)titleMessageId |
| contentMessage:(int)contentMessageId |
| linkMessage:(int)linkMessageId |
| buttonMessage:(int)buttonMessageId |
| linkAction:(SEL)linkAction |
| buttonAction:(SEL)buttonAction { |
| tutorialMode_ = mode; |
| |
| NSColor* tutorialBackgroundColor = |
| gfx::SkColorToSRGBNSColor(profiles::kAvatarTutorialBackgroundColor); |
| base::scoped_nsobject<NSView> container([[BackgroundColorView alloc] |
| initWithFrame:NSMakeRect(0, 0, kFixedMenuWidth, 0) |
| withColor:tutorialBackgroundColor]); |
| CGFloat availableWidth = kFixedMenuWidth - 2 * kHorizontalSpacing; |
| CGFloat yOffset = kSmallVerticalSpacing; |
| |
| // Adds links and buttons at the bottom. |
| base::scoped_nsobject<NSButton> tutorialOkButton([[HoverButton alloc] |
| initWithFrame:NSZeroRect]); |
| [tutorialOkButton setTitle:l10n_util::GetNSString( |
| buttonMessageId)]; |
| [tutorialOkButton setBezelStyle:NSRoundedBezelStyle]; |
| [tutorialOkButton setTarget:self]; |
| [tutorialOkButton setAction:buttonAction]; |
| [tutorialOkButton sizeToFit]; |
| NSSize buttonSize = [tutorialOkButton frame].size; |
| const CGFloat kTopBottomTextPadding = 6; |
| const CGFloat kLeftRightTextPadding = 15; |
| buttonSize.width += 2 * kLeftRightTextPadding; |
| buttonSize.height += 2 * kTopBottomTextPadding; |
| [tutorialOkButton setFrameSize:buttonSize]; |
| [tutorialOkButton setAlignment:NSCenterTextAlignment]; |
| [tutorialOkButton setFrameOrigin:NSMakePoint( |
| kFixedMenuWidth - NSWidth([tutorialOkButton frame]) - kHorizontalSpacing, |
| yOffset)]; |
| [container addSubview:tutorialOkButton]; |
| |
| NSButton* learnMoreLink = |
| [self linkButtonWithTitle:l10n_util::GetNSString(linkMessageId) |
| frameOrigin:NSZeroPoint |
| action:linkAction]; |
| [[learnMoreLink cell] setTextColor:[NSColor whiteColor]]; |
| CGFloat linkYOffset = yOffset + (NSHeight([tutorialOkButton frame]) - |
| NSHeight([learnMoreLink frame])) / 2; |
| [learnMoreLink setFrameOrigin:NSMakePoint(kHorizontalSpacing, linkYOffset)]; |
| [container addSubview:learnMoreLink]; |
| |
| yOffset = std::max(NSMaxY([learnMoreLink frame]), |
| NSMaxY([tutorialOkButton frame])) + kVerticalSpacing; |
| |
| // Adds body content. |
| NSTextField* contentLabel = BuildLabel( |
| l10n_util::GetNSString(contentMessageId), |
| NSMakePoint(kHorizontalSpacing, yOffset), |
| tutorialBackgroundColor, |
| gfx::SkColorToSRGBNSColor(profiles::kAvatarTutorialContentTextColor)); |
| [contentLabel setFrameSize:NSMakeSize(availableWidth, 0)]; |
| [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:contentLabel]; |
| [container addSubview:contentLabel]; |
| yOffset = NSMaxY([contentLabel frame]) + kSmallVerticalSpacing; |
| |
| // Adds title. |
| NSTextField* titleLabel = |
| BuildLabel(l10n_util::GetNSString(titleMessageId), |
| NSMakePoint(kHorizontalSpacing, yOffset), |
| tutorialBackgroundColor, |
| [NSColor whiteColor] /* text_color */); |
| [titleLabel setFont:[NSFont labelFontOfSize:kTitleFontSize]]; |
| [titleLabel sizeToFit]; |
| [titleLabel setFrameSize: |
| NSMakeSize(availableWidth, NSHeight([titleLabel frame]))]; |
| [container addSubview:titleLabel]; |
| yOffset = NSMaxY([titleLabel frame]) + kVerticalSpacing; |
| |
| [container setFrameSize:NSMakeSize(kFixedMenuWidth, yOffset)]; |
| |
| // Adds caret at the bottom. |
| NSImage* caretImage = ui::ResourceBundle::GetSharedInstance(). |
| GetNativeImageNamed(IDR_ICON_PROFILES_MENU_CARET).AsNSImage(); |
| base::scoped_nsobject<NSImageView> caretView( |
| [[NSImageView alloc] initWithFrame:NSMakeRect( |
| kHorizontalSpacing, 0, caretImage.size.width, |
| caretImage.size.height)]); |
| [caretView setImage:caretImage]; |
| |
| base::scoped_nsobject<NSView> containerWithCaret([[NSView alloc] |
| initWithFrame:NSMakeRect(0, 0, kFixedMenuWidth, 0)]); |
| [containerWithCaret addSubview:caretView]; |
| |
| [container setFrameOrigin:NSMakePoint(0, caretImage.size.height)]; |
| [containerWithCaret addSubview:container]; |
| |
| [containerWithCaret setFrameSize: |
| NSMakeSize(kFixedMenuWidth, NSMaxY([container frame]))]; |
| return containerWithCaret.autorelease(); |
| } |
| |
| - (NSView*)createCurrentProfileView:(const AvatarMenu::Item&)item { |
| base::scoped_nsobject<NSView> container([[NSView alloc] |
| initWithFrame:NSZeroRect]); |
| |
| CGFloat xOffset = kHorizontalSpacing; |
| CGFloat yOffset = 0; |
| CGFloat availableTextWidth = kFixedMenuWidth - 2 * kHorizontalSpacing; |
| |
| // Profile options. This can be a link to the accounts view, the profile's |
| // username for signed in users, or a "Sign in" button for local profiles. |
| SigninManagerBase* signinManager = |
| SigninManagerFactory::GetForProfile( |
| browser_->profile()->GetOriginalProfile()); |
| if (!isGuestSession_ && signinManager->IsSigninAllowed()) { |
| NSView* linksContainer = |
| [self createCurrentProfileLinksForItem:item |
| rect:NSMakeRect(xOffset, yOffset, |
| availableTextWidth, |
| 0)]; |
| [container addSubview:linksContainer]; |
| yOffset = NSMaxY([linksContainer frame]); |
| } |
| |
| // Profile name, centered. |
| bool editingAllowed = !isGuestSession_ && |
| !browser_->profile()->IsSupervised(); |
| base::scoped_nsobject<EditableProfileNameButton> profileName( |
| [[EditableProfileNameButton alloc] |
| initWithFrame:NSMakeRect(xOffset, |
| yOffset, |
| availableTextWidth, |
| kProfileButtonHeight) |
| profile:browser_->profile() |
| profileName:base::SysUTF16ToNSString( |
| profiles::GetAvatarNameForProfile( |
| browser_->profile()->GetPath())) |
| editingAllowed:editingAllowed |
| withController:self]); |
| |
| [container addSubview:profileName]; |
| yOffset = NSMaxY([profileName frame]); |
| |
| // Profile icon, centered. |
| xOffset = (kFixedMenuWidth - kLargeImageSide) / 2; |
| base::scoped_nsobject<EditableProfilePhoto> iconView( |
| [[EditableProfilePhoto alloc] |
| initWithFrame:NSMakeRect(xOffset, yOffset, |
| kLargeImageSide, kLargeImageSide) |
| avatarMenu:avatarMenu_.get() |
| profileIcon:item.icon |
| editingAllowed:!isGuestSession_ |
| withController:self]); |
| |
| [container addSubview:iconView]; |
| yOffset = NSMaxY([iconView frame]); |
| |
| if (browser_->profile()->IsSupervised()) { |
| base::scoped_nsobject<NSImageView> supervisedIcon( |
| [[NSImageView alloc] initWithFrame:NSZeroRect]); |
| ui::ResourceBundle* rb = &ui::ResourceBundle::GetSharedInstance(); |
| [supervisedIcon setImage:rb->GetNativeImageNamed( |
| IDR_ICON_PROFILES_MENU_SUPERVISED).ToNSImage()]; |
| NSSize size = [[supervisedIcon image] size]; |
| [supervisedIcon setFrameSize:size]; |
| NSRect parentFrame = [iconView frame]; |
| [supervisedIcon setFrameOrigin:NSMakePoint(NSMaxX(parentFrame) - size.width, |
| NSMinY(parentFrame))]; |
| [container addSubview:supervisedIcon]; |
| } |
| |
| if (switches::IsNewProfileManagementPreviewEnabled()) { |
| base::scoped_nsobject<HoverImageButton> questionButton( |
| [[HoverImageButton alloc] initWithFrame:NSZeroRect]); |
| [questionButton setBordered:NO]; |
| ui::ResourceBundle* rb = &ui::ResourceBundle::GetSharedInstance(); |
| [questionButton setDefaultImage:rb->GetNativeImageNamed( |
| IDR_ICON_PROFILES_MENU_QUESTION_STABLE).ToNSImage()]; |
| [questionButton setHoverImage:rb->GetNativeImageNamed( |
| IDR_ICON_PROFILES_MENU_QUESTION_HOVER).ToNSImage()]; |
| [questionButton setPressedImage:rb->GetNativeImageNamed( |
| IDR_ICON_PROFILES_MENU_QUESTION_SELECT).ToNSImage()]; |
| [questionButton setTarget:self]; |
| [questionButton setAction:@selector(showSendFeedbackTutorial:)]; |
| [questionButton sizeToFit]; |
| const CGFloat size = NSHeight([questionButton frame]) + 2; |
| [questionButton setFrame: |
| NSMakeRect(kHorizontalSpacing, yOffset - size, size, size)]; |
| [container addSubview:questionButton]; |
| } |
| |
| [container setFrameSize:NSMakeSize(kFixedMenuWidth, yOffset)]; |
| return container.autorelease(); |
| } |
| |
| - (NSView*)createCurrentProfileLinksForItem:(const AvatarMenu::Item&)item |
| rect:(NSRect)rect { |
| base::scoped_nsobject<NSView> container([[NSView alloc] initWithFrame:rect]); |
| |
| // Don't double-apply the left margin to the sub-views. |
| rect.origin.x = 0; |
| |
| // The available links depend on the type of profile that is active. |
| NSButton* link; |
| if (item.signed_in) { |
| if (switches::IsNewProfileManagement()) { |
| NSString* linkTitle = l10n_util::GetNSString( |
| viewMode_ == profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER ? |
| IDS_PROFILES_PROFILE_MANAGE_ACCOUNTS_BUTTON : |
| IDS_PROFILES_PROFILE_HIDE_MANAGE_ACCOUNTS_BUTTON); |
| SEL linkSelector = |
| (viewMode_ == profiles::BUBBLE_VIEW_MODE_PROFILE_CHOOSER) ? |
| @selector(showAccountManagement:) : @selector(hideAccountManagement:); |
| link = [self linkButtonWithTitle:linkTitle |
| frameOrigin:rect.origin |
| action:linkSelector]; |
| } else { |
| link = [self linkButtonWithTitle:base::SysUTF16ToNSString(item.sync_state) |
| frameOrigin:rect.origin |
| action:nil]; |
| [[link cell] setTextColor:[NSColor blackColor]]; |
| } |
| // -linkButtonWithTitle sizeToFit's the link, so re-stretch it so that it |
| // can be centered correctly in the view. |
| rect.size.height = NSMaxY([link frame]); |
| [link setFrame:rect]; |
| [link setAlignment:NSCenterTextAlignment]; |
| } else { |
| rect.size.height = kBlueButtonHeight; |
| link = [[BlueLabelButton alloc] initWithFrame:rect]; |
| |
| // Manually elide the button text so that the contents fit inside the bubble |
| // This is needed because the BlueLabelButton cell resets the style on |
| // every call to -cellSize, which prevents setting a custom lineBreakMode. |
| NSString* elidedButtonText = base::SysUTF16ToNSString(gfx::ElideText( |
| l10n_util::GetStringFUTF16( |
| IDS_SYNC_START_SYNC_BUTTON_LABEL, |
| l10n_util::GetStringUTF16(IDS_SHORT_PRODUCT_NAME)), |
| gfx::FontList(), rect.size.width, gfx::ELIDE_TAIL)); |
| |
| [link setTitle:elidedButtonText]; |
| [link setTarget:self]; |
| [link setAction:switches::IsNewProfileManagement() ? |
| @selector(showInlineSigninPage:) : @selector(showTabbedSigninPage:)]; |
| } |
| |
| [container addSubview:link]; |
| [container setFrameSize:rect.size]; |
| return container.autorelease(); |
| } |
| |
| - (NSView*)createSupervisedUserDisclaimerView { |
| base::scoped_nsobject<NSView> container( |
| [[NSView alloc] initWithFrame:NSZeroRect]); |
| |
| int yOffset = 0; |
| int availableTextWidth = kFixedMenuWidth - 2 * kHorizontalSpacing; |
| |
| NSTextField* disclaimer = BuildLabel( |
| base::SysUTF16ToNSString(avatarMenu_->GetSupervisedUserInformation()), |
| NSMakePoint(kHorizontalSpacing, yOffset), |
| nil /* background_color */, |
| nil /* text_color */); |
| [disclaimer setFrameSize:NSMakeSize(availableTextWidth, 0)]; |
| [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:disclaimer]; |
| yOffset = NSMaxY([disclaimer frame]); |
| |
| [container addSubview:disclaimer]; |
| [container setFrameSize:NSMakeSize(kFixedMenuWidth, yOffset)]; |
| return container.autorelease(); |
| } |
| |
| - (NSView*)createGuestProfileView { |
| gfx::Image guestIcon = |
| ui::ResourceBundle::GetSharedInstance().GetNativeImageNamed( |
| profiles::GetPlaceholderAvatarIconResourceID()); |
| AvatarMenu::Item guestItem(std::string::npos, /* menu_index, not used */ |
| std::string::npos, /* profile_index, not used */ |
| guestIcon); |
| guestItem.active = true; |
| guestItem.name = base::SysNSStringToUTF16( |
| l10n_util::GetNSString(IDS_PROFILES_GUEST_PROFILE_NAME)); |
| |
| return [self createCurrentProfileView:guestItem]; |
| } |
| |
| - (NSButton*)createOtherProfileView:(int)itemIndex { |
| const AvatarMenu::Item& item = avatarMenu_->GetItemAt(itemIndex); |
| |
| NSRect rect = NSMakeRect(0, 0, kFixedMenuWidth, |
| kBlueButtonHeight + kSmallVerticalSpacing); |
| base::scoped_nsobject<BackgroundColorHoverButton> profileButton( |
| [[BackgroundColorHoverButton alloc] |
| initWithFrame:rect |
| imageTitleSpacing:kImageTitleSpacing |
| backgroundColor:GetDialogBackgroundColor()]); |
| [profileButton setTitle:base::SysUTF16ToNSString(item.name)]; |
| [profileButton setDefaultImage:CreateProfileImage( |
| item.icon, kSmallImageSide).ToNSImage()]; |
| [profileButton setImagePosition:NSImageLeft]; |
| [profileButton setAlignment:NSLeftTextAlignment]; |
| [profileButton setBordered:NO]; |
| [profileButton setTag:itemIndex]; |
| [profileButton setTarget:self]; |
| [profileButton setAction:@selector(switchToProfile:)]; |
| |
| return profileButton.autorelease(); |
| } |
| |
| - (NSView*)createOptionsViewWithRect:(NSRect)rect |
| enableLock:(BOOL)enableLock { |
| int widthOfLockButton = enableLock ? 2 * kHorizontalSpacing + 14 : 0; |
| NSRect viewRect = NSMakeRect(0, 0, |
| rect.size.width - widthOfLockButton, |
| kBlueButtonHeight + kVerticalSpacing); |
| NSString* text = isGuestSession_ ? |
| l10n_util::GetNSString(IDS_PROFILES_EXIT_GUEST) : |
| l10n_util::GetNSStringF(IDS_PROFILES_NOT_YOU_BUTTON, |
| profiles::GetAvatarNameForProfile( |
| browser_->profile()->GetPath())); |
| NSButton* notYouButton = |
| [self hoverButtonWithRect:viewRect |
| text:text |
| imageResourceId:IDR_ICON_PROFILES_MENU_AVATAR |
| alternateImageResourceId:IDR_ICON_PROFILES_MENU_AVATAR |
| action:isGuestSession_? @selector(exitGuest:) : |
| @selector(showUserManager:)]; |
| |
| rect.size.height = NSMaxY([notYouButton frame]); |
| base::scoped_nsobject<NSView> container([[NSView alloc] initWithFrame:rect]); |
| [container addSubview:notYouButton]; |
| |
| if (enableLock) { |
| viewRect.origin.x = NSMaxX([notYouButton frame]); |
| NSBox* separator = [self verticalSeparatorWithFrame:viewRect]; |
| [container addSubview:separator]; |
| |
| viewRect.origin.x = NSMaxX([separator frame]); |
| viewRect.size.width = widthOfLockButton; |
| NSButton* lockButton = |
| [self hoverButtonWithRect:viewRect |
| text:@"" |
| imageResourceId:IDR_ICON_PROFILES_MENU_LOCK |
| alternateImageResourceId:IDR_ICON_PROFILES_MENU_LOCK |
| action:@selector(lockProfile:)]; |
| [container addSubview:lockButton]; |
| } |
| |
| return container.autorelease(); |
| } |
| |
| - (NSView*)createCurrentProfileAccountsView:(NSRect)rect { |
| const CGFloat kAccountButtonHeight = 34; |
| |
| const AvatarMenu::Item& item = |
| avatarMenu_->GetItemAt(avatarMenu_->GetActiveProfileIndex()); |
| DCHECK(item.signed_in); |
| |
| NSColor* backgroundColor = gfx::SkColorToCalibratedNSColor( |
| profiles::kAvatarBubbleAccountsBackgroundColor); |
| base::scoped_nsobject<NSView> container([[BackgroundColorView alloc] |
| initWithFrame:rect |
| withColor:backgroundColor]); |
| |
| rect.origin.y = 0; |
| if (!browser_->profile()->IsSupervised()) { |
| // Manually elide the button text so the contents fit inside the bubble. |
| // This is needed because the BlueLabelButton cell resets the style on |
| // every call to -cellSize, which prevents setting a custom lineBreakMode. |
| NSString* elidedButtonText = base::SysUTF16ToNSString(gfx::ElideText( |
| l10n_util::GetStringFUTF16( |
| IDS_PROFILES_PROFILE_ADD_ACCOUNT_BUTTON, item.name), |
| gfx::FontList(), rect.size.width, gfx::ELIDE_TAIL)); |
| |
| NSButton* addAccountsButton = |
| [self linkButtonWithTitle:elidedButtonText |
| frameOrigin:NSMakePoint( |
| kHorizontalSpacing, kSmallVerticalSpacing) |
| action:@selector(addAccount:)]; |
| [container addSubview:addAccountsButton]; |
| rect.origin.y += kAccountButtonHeight; |
| } |
| |
| NSView* accountEmails = [self createAccountsListWithRect:NSMakeRect( |
| 0, rect.origin.y, rect.size.width, kAccountButtonHeight)]; |
| [container addSubview:accountEmails]; |
| |
| [container setFrameSize:NSMakeSize(rect.size.width, |
| NSMaxY([accountEmails frame]))]; |
| return container.autorelease(); |
| } |
| |
| - (NSView*)createAccountsListWithRect:(NSRect)rect { |
| base::scoped_nsobject<NSView> container([[NSView alloc] initWithFrame:rect]); |
| currentProfileAccounts_.clear(); |
| |
| Profile* profile = browser_->profile(); |
| std::string primaryAccount = |
| SigninManagerFactory::GetForProfile(profile)->GetAuthenticatedUsername(); |
| DCHECK(!primaryAccount.empty()); |
| std::vector<std::string>accounts = |
| profiles::GetSecondaryAccountsForProfile(profile, primaryAccount); |
| |
| // If there is an account with an authentication error, it needs to be |
| // badged with a warning icon. |
| const SigninErrorController* errorController = |
| profiles::GetSigninErrorController(profile); |
| std::string errorAccountId = |
| errorController ? errorController->error_account_id() : std::string(); |
| |
| rect.origin.y = 0; |
| for (size_t i = 0; i < accounts.size(); ++i) { |
| // Save the original email address, as the button text could be elided. |
| currentProfileAccounts_[i] = accounts[i]; |
| NSButton* accountButton = |
| [self accountButtonWithRect:rect |
| title:accounts[i] |
| tag:i |
| reauthRequired:errorAccountId == accounts[i]]; |
| [container addSubview:accountButton]; |
| rect.origin.y = NSMaxY([accountButton frame]); |
| } |
| |
| // The primary account should always be listed first. |
| NSButton* accountButton = |
| [self accountButtonWithRect:rect |
| title:primaryAccount |
| tag:kPrimaryProfileTag |
| reauthRequired:errorAccountId == primaryAccount]; |
| [container addSubview:accountButton]; |
| [container setFrameSize:NSMakeSize(NSWidth([container frame]), |
| NSMaxY([accountButton frame]))]; |
| return container.autorelease(); |
| } |
| |
| - (NSView*)buildGaiaEmbeddedView { |
| base::scoped_nsobject<NSView> container( |
| [[NSView alloc] initWithFrame:NSZeroRect]); |
| CGFloat yOffset = 0; |
| |
| GURL url; |
| int messageId = -1; |
| SigninErrorController* errorController = NULL; |
| switch (viewMode_) { |
| case profiles::BUBBLE_VIEW_MODE_GAIA_SIGNIN: |
| url = signin::GetPromoURL(signin::SOURCE_AVATAR_BUBBLE_SIGN_IN, |
| false /* auto_close */, |
| true /* is_constrained */); |
| messageId = IDS_PROFILES_GAIA_SIGNIN_TITLE; |
| break; |
| case profiles::BUBBLE_VIEW_MODE_GAIA_ADD_ACCOUNT: |
| url = signin::GetPromoURL(signin::SOURCE_AVATAR_BUBBLE_ADD_ACCOUNT, |
| false /* auto_close */, |
| true /* is_constrained */); |
| messageId = IDS_PROFILES_GAIA_ADD_ACCOUNT_TITLE; |
| break; |
| case profiles::BUBBLE_VIEW_MODE_GAIA_REAUTH: |
| DCHECK(HasAuthError(browser_->profile())); |
| errorController = profiles::GetSigninErrorController(browser_->profile()); |
| url = signin::GetReauthURL( |
| browser_->profile(), |
| errorController ? errorController->error_username() : std::string()); |
| messageId = IDS_PROFILES_GAIA_REAUTH_TITLE; |
| break; |
| default: |
| NOTREACHED() << "Called with invalid mode=" << viewMode_; |
| break; |
| } |
| |
| webContents_.reset(content::WebContents::Create( |
| content::WebContents::CreateParams(browser_->profile()))); |
| webContents_->GetController().LoadURL(url, |
| content::Referrer(), |
| content::PAGE_TRANSITION_AUTO_TOPLEVEL, |
| std::string()); |
| NSView* webview = webContents_->GetNativeView(); |
| [webview setFrameSize:NSMakeSize(kFixedGaiaViewWidth, kFixedGaiaViewHeight)]; |
| [container addSubview:webview]; |
| yOffset = NSMaxY([webview frame]); |
| |
| // Adds the title card. |
| NSBox* separator = [self horizontalSeparatorWithFrame: |
| NSMakeRect(0, yOffset, kFixedGaiaViewWidth, 0)]; |
| [container addSubview:separator]; |
| yOffset = NSMaxY([separator frame]) + kSmallVerticalSpacing; |
| |
| NSView* titleView = BuildTitleCard( |
| NSMakeRect(0, yOffset, kFixedGaiaViewWidth, 0), |
| messageId, |
| self /* backButtonTarget*/, |
| @selector(navigateBackFromSigninPage:) /* backButtonAction */); |
| [container addSubview:titleView]; |
| yOffset = NSMaxY([titleView frame]); |
| |
| [container setFrameSize:NSMakeSize(kFixedGaiaViewWidth, yOffset)]; |
| return container.autorelease(); |
| } |
| |
| - (NSView*)buildAccountRemovalView { |
| DCHECK(!accountIdToRemove_.empty()); |
| |
| base::scoped_nsobject<NSView> container( |
| [[NSView alloc] initWithFrame:NSZeroRect]); |
| CGFloat availableWidth = |
| kFixedAccountRemovalViewWidth - 2 * kHorizontalSpacing; |
| CGFloat yOffset = kVerticalSpacing; |
| |
| const std::string& primaryAccount = SigninManagerFactory::GetForProfile( |
| browser_->profile())->GetAuthenticatedUsername(); |
| bool isPrimaryAccount = primaryAccount == accountIdToRemove_; |
| |
| // Adds "remove account" button at the bottom if needed. |
| if (!isPrimaryAccount) { |
| base::scoped_nsobject<NSButton> removeAccountButton( |
| [[BlueLabelButton alloc] initWithFrame:NSZeroRect]); |
| [removeAccountButton setTitle:l10n_util::GetNSString( |
| IDS_PROFILES_ACCOUNT_REMOVAL_BUTTON)]; |
| [removeAccountButton setTarget:self]; |
| [removeAccountButton setAction:@selector(removeAccount:)]; |
| [removeAccountButton sizeToFit]; |
| [removeAccountButton setAlignment:NSCenterTextAlignment]; |
| CGFloat xOffset = (kFixedAccountRemovalViewWidth - |
| NSWidth([removeAccountButton frame])) / 2; |
| [removeAccountButton setFrameOrigin:NSMakePoint(xOffset, yOffset)]; |
| [container addSubview:removeAccountButton]; |
| |
| yOffset = NSMaxY([removeAccountButton frame]) + kVerticalSpacing; |
| } |
| |
| NSView* contentView; |
| NSPoint contentFrameOrigin = NSMakePoint(kHorizontalSpacing, yOffset); |
| if (isPrimaryAccount) { |
| std::vector<size_t> offsets; |
| NSString* contentStr = l10n_util::GetNSStringF( |
| IDS_PROFILES_PRIMARY_ACCOUNT_REMOVAL_TEXT, |
| base::UTF8ToUTF16(accountIdToRemove_), base::string16(), &offsets); |
| NSString* linkStr = l10n_util::GetNSString(IDS_PROFILES_SETTINGS_LINK); |
| contentView = BuildFixedWidthTextViewWithLink(self, contentStr, linkStr, |
| offsets[1], contentFrameOrigin, availableWidth); |
| } else { |
| NSString* contentStr = |
| l10n_util::GetNSString(IDS_PROFILES_ACCOUNT_REMOVAL_TEXT); |
| NSTextField* contentLabel = BuildLabel(contentStr, contentFrameOrigin, |
| GetDialogBackgroundColor(), nil /* text_color */); |
| [contentLabel setFrameSize:NSMakeSize(availableWidth, 0)]; |
| [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:contentLabel]; |
| contentView = contentLabel; |
| } |
| [container addSubview:contentView]; |
| yOffset = NSMaxY([contentView frame]) + kVerticalSpacing; |
| |
| // Adds the title card. |
| NSBox* separator = [self horizontalSeparatorWithFrame: |
| NSMakeRect(0, yOffset, kFixedAccountRemovalViewWidth, 0)]; |
| [container addSubview:separator]; |
| yOffset = NSMaxY([separator frame]) + kSmallVerticalSpacing; |
| |
| NSView* titleView = BuildTitleCard( |
| NSMakeRect(0, yOffset, kFixedAccountRemovalViewWidth,0), |
| IDS_PROFILES_ACCOUNT_REMOVAL_TITLE, |
| self /* backButtonTarget*/, |
| @selector(showAccountManagement:) /* backButtonAction */); |
| [container addSubview:titleView]; |
| yOffset = NSMaxY([titleView frame]); |
| |
| [container setFrameSize:NSMakeSize(kFixedAccountRemovalViewWidth, yOffset)]; |
| return container.autorelease(); |
| } |
| |
| - (NSView*)buildEndPreviewView { |
| base::scoped_nsobject<NSView> container( |
| [[NSView alloc] initWithFrame:NSZeroRect]); |
| CGFloat availableWidth = |
| kFixedEndPreviewViewWidth - 2 * kHorizontalSpacing; |
| CGFloat yOffset = kVerticalSpacing; |
| |
| // Adds the "end preview and relaunch" button at the bottom. |
| base::scoped_nsobject<NSButton> endPreviewAndRelaunchButton( |
| [[BlueLabelButton alloc] initWithFrame:NSZeroRect]); |
| [endPreviewAndRelaunchButton setTitle:l10n_util::GetNSString( |
| IDS_PROFILES_END_PREVIEW_AND_RELAUNCH)]; |
| [endPreviewAndRelaunchButton setTarget:self]; |
| [endPreviewAndRelaunchButton setAction:@selector(endPreviewAndRelaunch:)]; |
| [endPreviewAndRelaunchButton sizeToFit]; |
| [endPreviewAndRelaunchButton setAlignment:NSCenterTextAlignment]; |
| CGFloat xOffset = (kFixedEndPreviewViewWidth - |
| NSWidth([endPreviewAndRelaunchButton frame])) / 2; |
| [endPreviewAndRelaunchButton setFrameOrigin:NSMakePoint(xOffset, yOffset)]; |
| [container addSubview:endPreviewAndRelaunchButton]; |
| yOffset = NSMaxY([endPreviewAndRelaunchButton frame]) + kVerticalSpacing; |
| |
| // Adds the main text label. |
| NSPoint contentFrameOrigin = NSMakePoint(kHorizontalSpacing, yOffset); |
| NSString* contentStr = |
| l10n_util::GetNSString(IDS_PROFILES_END_PREVIEW_TEXT); |
| NSTextField* contentLabel = BuildLabel(contentStr, contentFrameOrigin, |
| GetDialogBackgroundColor(), nil /* text_color */); |
| [contentLabel setFrameSize:NSMakeSize(availableWidth, 0)]; |
| [GTMUILocalizerAndLayoutTweaker sizeToFitFixedWidthTextField:contentLabel]; |
| [container addSubview:contentLabel]; |
| yOffset = NSMaxY([contentLabel frame]) + kVerticalSpacing; |
| |
| // Adds the title card. |
| NSBox* separator = [self horizontalSeparatorWithFrame: |
| NSMakeRect(0, yOffset, kFixedEndPreviewViewWidth, 0)]; |
| [container addSubview:separator]; |
| yOffset = NSMaxY([separator frame]) + kSmallVerticalSpacing; |
| |
| NSView* titleView = BuildTitleCard( |
| NSMakeRect(0, yOffset, kFixedEndPreviewViewWidth, 0), |
| IDS_PROFILES_END_PREVIEW, |
| self /* backButtonTarget*/, |
| @selector(showSendFeedbackTutorial:) /* backButtonAction */); |
| [container addSubview:titleView]; |
| yOffset = NSMaxY([titleView frame]); |
| |
| [container setFrameSize:NSMakeSize(kFixedEndPreviewViewWidth, yOffset)]; |
| return container.autorelease(); |
| } |
| |
| // Called when clicked on the settings link. |
| - (BOOL)textView:(NSTextView*)textView |
| clickedOnLink:(id)link |
| atIndex:(NSUInteger)charIndex { |
| chrome::ShowSettings(browser_); |
| return YES; |
| } |
| |
| - (NSButton*)hoverButtonWithRect:(NSRect)rect |
| text:(NSString*)text |
| imageResourceId:(int)imageResourceId |
| alternateImageResourceId:(int)alternateImageResourceId |
| action:(SEL)action { |
| base::scoped_nsobject<BackgroundColorHoverButton> button( |
| [[BackgroundColorHoverButton alloc] |
| initWithFrame:rect |
| imageTitleSpacing:kImageTitleSpacing |
| backgroundColor:GetDialogBackgroundColor()]); |
| |
| [button setTitle:text]; |
| ui::ResourceBundle* rb = &ui::ResourceBundle::GetSharedInstance(); |
| NSImage* alternateImage = rb->GetNativeImageNamed( |
| alternateImageResourceId).ToNSImage(); |
| [button setDefaultImage:rb->GetNativeImageNamed(imageResourceId).ToNSImage()]; |
| [button setHoverImage:alternateImage]; |
| [button setPressedImage:alternateImage]; |
| [button setImagePosition:NSImageLeft]; |
| [button setAlignment:NSLeftTextAlignment]; |
| [button setBordered:NO]; |
| [button setTarget:self]; |
| [button setAction:action]; |
| |
| return button.autorelease(); |
| } |
| |
| - (NSButton*)linkButtonWithTitle:(NSString*)title |
| frameOrigin:(NSPoint)frameOrigin |
| action:(SEL)action { |
| base::scoped_nsobject<NSButton> link( |
| [[HyperlinkButtonCell buttonWithString:title] retain]); |
| |
| [[link cell] setShouldUnderline:NO]; |
| [[link cell] setTextColor:gfx::SkColorToCalibratedNSColor( |
| chrome_style::GetLinkColor())]; |
| [link setTitle:title]; |
| [link setBordered:NO]; |
| [link setFont:[NSFont labelFontOfSize:kTextFontSize]]; |
| [link setTarget:self]; |
| [link setAction:action]; |
| [link setFrameOrigin:frameOrigin]; |
| [link sizeToFit]; |
| |
| return link.autorelease(); |
| } |
| |
| - (NSButton*)accountButtonWithRect:(NSRect)rect |
| title:(const std::string&)title |
| tag:(int)tag |
| reauthRequired:(BOOL)reauthRequired { |
| ui::ResourceBundle* rb = &ui::ResourceBundle::GetSharedInstance(); |
| NSImage* deleteImage = rb->GetNativeImageNamed(IDR_CLOSE_1).ToNSImage(); |
| CGFloat deleteImageWidth = [deleteImage size].width; |
| NSImage* warningImage = reauthRequired ? rb->GetNativeImageNamed( |
| IDR_ICON_PROFILES_ACCOUNT_BUTTON_ERROR).ToNSImage() : nil; |
| CGFloat warningImageWidth = [warningImage size].width; |
| |
| CGFloat availableTextWidth = rect.size.width - kHorizontalSpacing - |
| warningImageWidth - deleteImageWidth; |
| if (warningImage) |
| availableTextWidth -= kHorizontalSpacing; |
| |
| NSColor* backgroundColor = gfx::SkColorToCalibratedNSColor( |
| profiles::kAvatarBubbleAccountsBackgroundColor); |
| base::scoped_nsobject<BackgroundColorHoverButton> button( |
| [[BackgroundColorHoverButton alloc] initWithFrame:rect |
| imageTitleSpacing:0 |
| backgroundColor:backgroundColor]); |
| [button setTitle:ElideEmail(title, availableTextWidth)]; |
| [button setAlignment:NSLeftTextAlignment]; |
| [button setBordered:NO]; |
| if (reauthRequired) { |
| [button setDefaultImage:warningImage]; |
| [button setImagePosition:NSImageLeft]; |
| [button setTarget:self]; |
| [button setAction:@selector(showAccountReauthenticationView:)]; |
| [button setTag:tag]; |
| } |
| |
| // Delete button. |
| if (!browser_->profile()->IsSupervised()) { |
| NSRect buttonRect; |
| NSDivideRect(rect, &buttonRect, &rect, |
| deleteImageWidth + kHorizontalSpacing, NSMaxXEdge); |
| buttonRect.origin.y = 0; |
| |
| base::scoped_nsobject<HoverImageButton> deleteButton( |
| [[HoverImageButton alloc] initWithFrame:buttonRect]); |
| [deleteButton setBordered:NO]; |
| [deleteButton setDefaultImage:deleteImage]; |
| [deleteButton setHoverImage:rb->GetNativeImageNamed( |
| IDR_CLOSE_1_H).ToNSImage()]; |
| [deleteButton setPressedImage:rb->GetNativeImageNamed( |
| IDR_CLOSE_1_P).ToNSImage()]; |
| [deleteButton setTarget:self]; |
| [deleteButton setAction:@selector(showAccountRemovalView:)]; |
| [deleteButton setTag:tag]; |
| |
| [button addSubview:deleteButton]; |
| } |
| |
| return button.autorelease(); |
| } |
| |
| - (void)postActionPerformed:(ProfileMetrics::ProfileDesktopMenu)action { |
| ProfileMetrics::LogProfileDesktopMenu(action, serviceType_); |
| serviceType_ = signin::GAIA_SERVICE_TYPE_NONE; |
| } |
| |
| @end |