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