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 "chrome/browser/ui/cocoa/profiles/avatar_button_controller.h" 6 7#include "base/mac/foundation_util.h" 8#include "base/strings/sys_string_conversions.h" 9#include "chrome/browser/profiles/profiles_state.h" 10#include "chrome/browser/themes/theme_service.h" 11#include "chrome/browser/themes/theme_service_factory.h" 12#include "chrome/browser/ui/browser.h" 13#include "chrome/browser/ui/browser_window.h" 14#import "chrome/browser/ui/cocoa/browser_window_controller.h" 15#include "components/signin/core/browser/signin_error_controller.h" 16#include "grit/generated_resources.h" 17#include "grit/theme_resources.h" 18#import "ui/base/cocoa/appkit_utils.h" 19#import "ui/base/cocoa/hover_image_button.h" 20#include "ui/base/l10n/l10n_util_mac.h" 21#include "ui/base/nine_image_painter_factory.h" 22#include "ui/base/resource/resource_bundle.h" 23#include "ui/gfx/image/image_skia_operations.h" 24#include "ui/gfx/image/image_skia_util_mac.h" 25#include "ui/gfx/text_elider.h" 26 27namespace { 28 29const CGFloat kButtonPadding = 12; 30const CGFloat kButtonDefaultPadding = 5; 31const CGFloat kButtonHeight = 27; 32const CGFloat kButtonTitleImageSpacing = 10; 33const CGFloat kMaxButtonContentWidth = 100; 34 35const ui::NinePartImageIds kNormalBorderImageIds = 36 IMAGE_GRID(IDR_AVATAR_MAC_BUTTON_NORMAL); 37const ui::NinePartImageIds kHoverBorderImageIds = 38 IMAGE_GRID(IDR_AVATAR_MAC_BUTTON_HOVER); 39const ui::NinePartImageIds kPressedBorderImageIds = 40 IMAGE_GRID(IDR_AVATAR_MAC_BUTTON_PRESSED); 41const ui::NinePartImageIds kThemedBorderImageIds = 42 IMAGE_GRID(IDR_AVATAR_THEMED_MAC_BUTTON_NORMAL); 43 44NSImage* GetImageFromResourceID(int resourceId) { 45 return ui::ResourceBundle::GetSharedInstance().GetNativeImageNamed( 46 resourceId).ToNSImage(); 47} 48 49} // namespace 50 51// Button cell with a custom border given by a set of nine-patch image grids. 52@interface CustomThemeButtonCell : NSButtonCell { 53 @private 54 BOOL isThemedWindow_; 55 base::scoped_nsobject<NSImage> authenticationErrorImage_; 56} 57- (void)setIsThemedWindow:(BOOL)isThemedWindow; 58- (void)setHasError:(BOOL)hasError; 59 60@end 61 62@implementation CustomThemeButtonCell 63- (id)initWithThemedWindow:(BOOL)isThemedWindow { 64 if ((self = [super init])) { 65 isThemedWindow_ = isThemedWindow; 66 authenticationErrorImage_.reset(); 67 } 68 return self; 69} 70 71- (NSSize)cellSize { 72 NSSize buttonSize = [super cellSize]; 73 CGFloat errorWidth = [authenticationErrorImage_ size].width; 74 buttonSize.width += 2 * (kButtonPadding - kButtonDefaultPadding) + errorWidth; 75 buttonSize.height = kButtonHeight; 76 return buttonSize; 77} 78 79- (NSRect)drawTitle:(NSAttributedString*)title 80 withFrame:(NSRect)frame 81 inView:(NSView*)controlView { 82 frame.origin.x = kButtonPadding; 83 84 // If there's an auth error, draw a warning icon before the cell image. 85 if (authenticationErrorImage_) { 86 NSSize imageSize = [authenticationErrorImage_ size]; 87 NSRect rect = NSMakeRect( 88 frame.size.width - imageSize.width, 89 (kButtonHeight - imageSize.height) / 2, 90 imageSize.width, 91 imageSize.height); 92 [authenticationErrorImage_ drawInRect:rect 93 fromRect:NSZeroRect 94 operation:NSCompositeSourceOver 95 fraction:1.0 96 respectFlipped:YES 97 hints:nil]; 98 // Padding between the title and the error image. 99 frame.size.width -= kButtonTitleImageSpacing; 100 } 101 102 // Padding between the title (or error image, if it exists) and the 103 // button's drop down image. 104 frame.size.width -= kButtonTitleImageSpacing; 105 return [super drawTitle:title withFrame:frame inView:controlView]; 106} 107 108- (void)drawImage:(NSImage*)image 109 withFrame:(NSRect)frame 110 inView:(NSView*)controlView { 111 // For the x-offset, we need to undo the default padding and apply the 112 // new one. For the y-offset, increasing the button height means we need 113 // to move the image a little down to align it nicely with the text; this 114 // was chosen by visual inspection. 115 frame = NSOffsetRect(frame, kButtonDefaultPadding - kButtonPadding, 2); 116 [super drawImage:image withFrame:frame inView:controlView]; 117} 118 119- (void)drawBezelWithFrame:(NSRect)frame 120 inView:(NSView*)controlView { 121 HoverState hoverState = 122 [base::mac::ObjCCastStrict<HoverImageButton>(controlView) hoverState]; 123 ui::NinePartImageIds imageIds = kNormalBorderImageIds; 124 if (isThemedWindow_) 125 imageIds = kThemedBorderImageIds; 126 127 if (hoverState == kHoverStateMouseDown) 128 imageIds = kPressedBorderImageIds; 129 else if (hoverState == kHoverStateMouseOver) 130 imageIds = kHoverBorderImageIds; 131 ui::DrawNinePartImage(frame, imageIds, NSCompositeSourceOver, 1.0, true); 132} 133 134- (void)setIsThemedWindow:(BOOL)isThemedWindow { 135 isThemedWindow_ = isThemedWindow; 136} 137 138- (void)setHasError:(BOOL)hasError { 139 if (hasError) { 140 authenticationErrorImage_.reset( 141 [ui::ResourceBundle::GetSharedInstance().GetImageNamed( 142 IDR_ICON_PROFILES_AVATAR_BUTTON_ERROR).ToNSImage() retain]); 143 } else { 144 authenticationErrorImage_.reset(); 145 } 146} 147 148@end 149 150@interface AvatarButtonController (Private) 151- (base::string16)getElidedAvatarName; 152- (void)updateAvatarButtonAndLayoutParent:(BOOL)layoutParent; 153- (void)updateErrorStatus:(BOOL)hasError; 154- (void)dealloc; 155- (void)themeDidChangeNotification:(NSNotification*)aNotification; 156@end 157 158@implementation AvatarButtonController 159 160- (id)initWithBrowser:(Browser*)browser { 161 if ((self = [super initWithBrowser:browser])) { 162 ThemeService* themeService = 163 ThemeServiceFactory::GetForProfile(browser->profile()); 164 isThemedWindow_ = !themeService->UsingSystemTheme(); 165 166 HoverImageButton* hoverButton = 167 [[HoverImageButton alloc] initWithFrame:NSZeroRect]; 168 [hoverButton setDefaultImage:GetImageFromResourceID( 169 IDR_AVATAR_MAC_BUTTON_DROPARROW)]; 170 [hoverButton setHoverImage:GetImageFromResourceID( 171 IDR_AVATAR_MAC_BUTTON_DROPARROW_HOVER)]; 172 [hoverButton setPressedImage:GetImageFromResourceID( 173 IDR_AVATAR_MAC_BUTTON_DROPARROW_PRESSED)]; 174 175 button_.reset(hoverButton); 176 base::scoped_nsobject<CustomThemeButtonCell> cell( 177 [[CustomThemeButtonCell alloc] initWithThemedWindow:isThemedWindow_]); 178 SigninErrorController* errorController = 179 profiles::GetSigninErrorController(browser->profile()); 180 if (errorController) 181 [cell setHasError:errorController->HasError()]; 182 [button_ setCell:cell.get()]; 183 [self setView:button_]; 184 185 [button_ setBezelStyle:NSShadowlessSquareBezelStyle]; 186 [button_ setButtonType:NSMomentaryChangeButton]; 187 [button_ setBordered:YES]; 188 // This is a workaround for an issue in the HoverImageButton where the 189 // button is initially sized incorrectly unless a default image is provided. 190 [button_ setImage:GetImageFromResourceID(IDR_AVATAR_MAC_BUTTON_DROPARROW)]; 191 [button_ setImagePosition:NSImageRight]; 192 [button_ setAutoresizingMask:NSViewMinXMargin | NSViewMinYMargin]; 193 [button_ setTarget:self]; 194 [button_ setAction:@selector(buttonClicked:)]; 195 196 [self updateAvatarButtonAndLayoutParent:NO]; 197 198 NSNotificationCenter* center = [NSNotificationCenter defaultCenter]; 199 [center addObserver:self 200 selector:@selector(themeDidChangeNotification:) 201 name:kBrowserThemeDidChangeNotification 202 object:nil]; 203 } 204 return self; 205} 206 207- (void)dealloc { 208 [[NSNotificationCenter defaultCenter] removeObserver:self]; 209 [super dealloc]; 210} 211 212- (void)themeDidChangeNotification:(NSNotification*)aNotification { 213 // Redraw the button if the window has switched between themed and native. 214 ThemeService* themeService = 215 ThemeServiceFactory::GetForProfile(browser_->profile()); 216 BOOL updatedIsThemedWindow = !themeService->UsingSystemTheme(); 217 if (isThemedWindow_ != updatedIsThemedWindow) { 218 isThemedWindow_ = updatedIsThemedWindow; 219 [[button_ cell] setIsThemedWindow:isThemedWindow_]; 220 [self updateAvatarButtonAndLayoutParent:YES]; 221 } 222} 223 224- (base::string16)getElidedAvatarName { 225 base::string16 name = profiles::GetAvatarNameForProfile(browser_->profile()); 226 int maxTextWidth = kMaxButtonContentWidth - [[button_ image] size].width; 227 return gfx::ElideText(name, gfx::FontList(gfx::Font([button_ font])), 228 maxTextWidth, gfx::ELIDE_TAIL); 229} 230 231- (void)updateAvatarButtonAndLayoutParent:(BOOL)layoutParent { 232 // The button text has a black foreground and a white drop shadow for regular 233 // windows, and a light text with a dark drop shadow for guest windows 234 // which are themed with a dark background. 235 base::scoped_nsobject<NSShadow> shadow([[NSShadow alloc] init]); 236 [shadow setShadowOffset:NSMakeSize(0, -1)]; 237 [shadow setShadowBlurRadius:0]; 238 239 NSColor* foregroundColor; 240 if (browser_->profile()->IsGuestSession()) { 241 foregroundColor = [NSColor colorWithCalibratedWhite:1.0 alpha:0.9]; 242 [shadow setShadowColor:[NSColor colorWithCalibratedWhite:0.0 alpha:0.4]]; 243 } else if (!isThemedWindow_) { 244 foregroundColor = [NSColor blackColor]; 245 [shadow setShadowColor:[NSColor colorWithCalibratedWhite:1.0 alpha:0.7]]; 246 } else { 247 foregroundColor = [NSColor blackColor]; 248 [shadow setShadowColor:[NSColor colorWithCalibratedWhite:1.0 alpha:0.4]]; 249 } 250 251 base::string16 profileName = [self getElidedAvatarName]; 252 NSString* buttonTitle = nil; 253 if (browser_->profile()->IsSupervised()) { 254 // Add the "supervised" label after eliding the profile name, so the label 255 // will not get elided, but will instead enlarge the button. 256 buttonTitle = l10n_util::GetNSStringF(IDS_MANAGED_USER_NEW_AVATAR_LABEL, 257 profileName); 258 } else { 259 buttonTitle = base::SysUTF16ToNSString(profileName); 260 } 261 262 base::scoped_nsobject<NSMutableParagraphStyle> paragraphStyle( 263 [[NSMutableParagraphStyle alloc] init]); 264 [paragraphStyle setAlignment:NSLeftTextAlignment]; 265 266 base::scoped_nsobject<NSAttributedString> attributedTitle( 267 [[NSAttributedString alloc] 268 initWithString:buttonTitle 269 attributes:@{ NSShadowAttributeName : shadow.get(), 270 NSForegroundColorAttributeName : foregroundColor, 271 NSParagraphStyleAttributeName : paragraphStyle }]); 272 [button_ setAttributedTitle:attributedTitle]; 273 [button_ sizeToFit]; 274 275 if (layoutParent) { 276 // Because the width of the button might have changed, the parent browser 277 // frame needs to recalculate the button bounds and redraw it. 278 [[BrowserWindowController 279 browserWindowControllerForWindow:browser_->window()->GetNativeWindow()] 280 layoutSubviews]; 281 } 282} 283 284- (void)updateErrorStatus:(BOOL)hasError { 285 [[button_ cell] setHasError:hasError]; 286 [self updateAvatarButtonAndLayoutParent:YES]; 287} 288 289@end 290