blob: 6c37962102ecdc8347feafe77c23a435f1c22087 [file] [log] [blame]
// Copyright (c) 2012 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 "chrome/browser/ui/cocoa/browser/avatar_button_controller.h"
#include "base/strings/sys_string_conversions.h"
#include "chrome/app/chrome_command_ids.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/chrome_notification_types.h"
#include "chrome/browser/command_updater.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_info_cache.h"
#include "chrome/browser/profiles/profile_info_util.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/profiles/profile_metrics.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_commands.h"
#include "chrome/browser/ui/browser_window.h"
#import "chrome/browser/ui/cocoa/browser/avatar_label_button.h"
#import "chrome/browser/ui/cocoa/browser/avatar_menu_bubble_controller.h"
#import "chrome/browser/ui/cocoa/browser_window_controller.h"
#include "content/public/browser/notification_service.h"
#include "grit/generated_resources.h"
#include "grit/theme_resources.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/scoped_ns_graphics_context_save_gstate_mac.h"
namespace {
// Space between the avatar icon and the avatar menu bubble.
const CGFloat kMenuYOffsetAdjust = 1.0;
// Space between the avatar label and the left edge of the container containing
// the label and the icon.
const CGFloat kAvatarSpacing = 4;
// Space between the bottom of the avatar icon and the bottom of the avatar
// label.
const CGFloat kAvatarLabelBottomSpacing = 3;
// Space between the right edge of the avatar label and the right edge of the
// avatar icon.
const CGFloat kAvatarLabelRightSpacing = 2;
} // namespace
@interface AvatarButtonController (Private)
- (void)setButtonEnabled:(BOOL)flag;
- (IBAction)buttonClicked:(id)sender;
- (void)bubbleWillClose:(NSNotification*)notif;
- (NSImage*)compositeImageWithShadow:(NSImage*)image;
- (void)updateAvatar;
- (void)addOrRemoveButtonIfNecessary;
@end
// Declare a 10.7+ private API.
// NSThemeFrame < NSTitledFrame < NSFrameView < NSView.
@interface NSView (NSThemeFrame)
- (void)_tileTitlebarAndRedisplay:(BOOL)redisplay;
@end
namespace AvatarButtonControllerInternal {
class Observer : public content::NotificationObserver {
public:
Observer(AvatarButtonController* button) : button_(button) {
registrar_.Add(this, chrome::NOTIFICATION_PROFILE_CACHED_INFO_CHANGED,
content::NotificationService::AllSources());
}
// NotificationObserver:
virtual void Observe(int type,
const content::NotificationSource& source,
const content::NotificationDetails& details) OVERRIDE {
switch (type) {
case chrome::NOTIFICATION_PROFILE_CACHED_INFO_CHANGED:
[button_ updateAvatar];
[button_ addOrRemoveButtonIfNecessary];
break;
default:
NOTREACHED();
break;
}
}
private:
content::NotificationRegistrar registrar_;
AvatarButtonController* button_; // Weak; owns this.
};
} // namespace AvatarButtonControllerInternal
////////////////////////////////////////////////////////////////////////////////
@implementation AvatarButtonController
- (id)initWithBrowser:(Browser*)browser {
if ((self = [super init])) {
browser_ = browser;
base::scoped_nsobject<NSView> container(
[[NSView alloc] initWithFrame:NSMakeRect(
0, 0, profiles::kAvatarIconWidth, profiles::kAvatarIconHeight)]);
[self setView:container];
button_.reset([[NSButton alloc] initWithFrame:NSMakeRect(
0, 0, profiles::kAvatarIconWidth, profiles::kAvatarIconHeight)]);
NSButtonCell* cell = [button_ cell];
[button_ setButtonType:NSMomentaryLightButton];
[button_ setImagePosition:NSImageOnly];
[cell setImageScaling:NSImageScaleProportionallyDown];
[cell setImagePosition:NSImageBelow];
// AppKit sets a title for some reason when using |-setImagePosition:|.
[button_ setTitle:nil];
[cell setImageDimsWhenDisabled:NO];
[cell setHighlightsBy:NSContentsCellMask];
[cell setShowsStateBy:NSContentsCellMask];
[button_ setBordered:NO];
[button_ setTarget:self];
[button_ setAction:@selector(buttonClicked:)];
[cell accessibilitySetOverrideValue:NSAccessibilityButtonRole
forAttribute:NSAccessibilityRoleAttribute];
[cell accessibilitySetOverrideValue:
NSAccessibilityRoleDescription(NSAccessibilityButtonRole, nil)
forAttribute:NSAccessibilityRoleDescriptionAttribute];
[cell accessibilitySetOverrideValue:
l10n_util::GetNSString(IDS_PROFILES_BUBBLE_ACCESSIBLE_NAME)
forAttribute:NSAccessibilityTitleAttribute];
[cell accessibilitySetOverrideValue:
l10n_util::GetNSString(IDS_PROFILES_BUBBLE_ACCESSIBLE_DESCRIPTION)
forAttribute:NSAccessibilityHelpAttribute];
[cell accessibilitySetOverrideValue:
l10n_util::GetNSString(IDS_PROFILES_BUBBLE_ACCESSIBLE_DESCRIPTION)
forAttribute:NSAccessibilityDescriptionAttribute];
Profile* profile = browser_->profile();
if (profile->IsOffTheRecord()) {
ResourceBundle& bundle = ResourceBundle::GetSharedInstance();
NSImage* otrIcon = bundle.GetNativeImageNamed(IDR_OTR_ICON).ToNSImage();
[self setImage:[self compositeImageWithShadow:otrIcon]];
[self setButtonEnabled:NO];
} else {
[self setButtonEnabled:YES];
observer_.reset(new AvatarButtonControllerInternal::Observer(self));
[self updateAvatar];
// Managed users cannot enter incognito mode, so we only need to check
// it in this code path.
if (profile->IsManaged()) {
// Initialize the avatar label button.
CGFloat extraWidth =
profiles::kAvatarIconWidth + kAvatarLabelRightSpacing;
NSRect frame = NSMakeRect(
kAvatarSpacing, kAvatarLabelBottomSpacing, extraWidth, 0);
labelButton_.reset([[AvatarLabelButton alloc] initWithFrame:frame]);
[labelButton_ setTarget:self];
[labelButton_ setAction:@selector(buttonClicked:)];
[[self view] addSubview:labelButton_];
// Resize the container and reposition the avatar button.
NSSize textSize = [[labelButton_ cell] labelTextSize];
[container setFrameSize:
NSMakeSize([labelButton_ frame].size.width + kAvatarSpacing,
profiles::kAvatarIconHeight)];
[button_
setFrameOrigin:NSMakePoint(kAvatarSpacing + textSize.width, 0)];
}
}
[[self view] addSubview:button_];
}
return self;
}
- (void)dealloc {
[[NSNotificationCenter defaultCenter]
removeObserver:self
name:NSWindowWillCloseNotification
object:[menuController_ window]];
[super dealloc];
}
- (NSButton*)buttonView {
return button_.get();
}
- (NSButton*)labelButtonView {
return labelButton_.get();
}
- (void)setImage:(NSImage*)image {
[button_ setImage:image];
}
- (void)showAvatarBubble:(NSView*)anchor {
if (menuController_)
return;
DCHECK(chrome::IsCommandEnabled(browser_, IDC_SHOW_AVATAR_MENU));
NSWindowController* wc =
[browser_->window()->GetNativeWindow() windowController];
if ([wc isKindOfClass:[BrowserWindowController class]]) {
[static_cast<BrowserWindowController*>(wc)
lockBarVisibilityForOwner:self withAnimation:NO delay:NO];
}
NSPoint point = NSMakePoint(NSMidX([anchor bounds]),
NSMaxY([anchor bounds]) - kMenuYOffsetAdjust);
point = [anchor convertPoint:point toView:nil];
point = [[anchor window] convertBaseToScreen:point];
// |menu| will automatically release itself on close.
menuController_ = [[AvatarMenuBubbleController alloc] initWithBrowser:browser_
anchoredAt:point];
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(bubbleWillClose:)
name:NSWindowWillCloseNotification
object:[menuController_ window]];
[menuController_ showWindow:self];
ProfileMetrics::LogProfileOpenMethod(ProfileMetrics::ICON_AVATAR_BUBBLE);
}
// Private /////////////////////////////////////////////////////////////////////
- (void)setButtonEnabled:(BOOL)flag {
[button_ setEnabled:flag];
}
- (IBAction)buttonClicked:(id)sender {
DCHECK(sender == button_.get() || sender == labelButton_.get());
[self showAvatarBubble:button_];
}
- (void)bubbleWillClose:(NSNotification*)notif {
NSWindowController* wc =
[browser_->window()->GetNativeWindow() windowController];
if ([wc isKindOfClass:[BrowserWindowController class]]) {
[static_cast<BrowserWindowController*>(wc)
releaseBarVisibilityForOwner:self withAnimation:YES delay:NO];
}
menuController_ = nil;
}
// This will take in an original image and redraw it with a shadow.
- (NSImage*)compositeImageWithShadow:(NSImage*)image {
gfx::ScopedNSGraphicsContextSaveGState scopedGState;
base::scoped_nsobject<NSImage> destination(
[[NSImage alloc] initWithSize:[image size]]);
NSRect destRect = NSZeroRect;
destRect.size = [destination size];
[destination lockFocus];
base::scoped_nsobject<NSShadow> shadow([[NSShadow alloc] init]);
[shadow.get() setShadowColor:[NSColor colorWithCalibratedWhite:0.0
alpha:0.75]];
[shadow.get() setShadowOffset:NSZeroSize];
[shadow.get() setShadowBlurRadius:3.0];
[shadow.get() set];
[image drawInRect:destRect
fromRect:NSZeroRect
operation:NSCompositeSourceOver
fraction:1.0
respectFlipped:YES
hints:nil];
[destination unlockFocus];
return destination.autorelease();
}
// Updates the avatar information from the profile cache.
- (void)updateAvatar {
ProfileInfoCache& cache =
g_browser_process->profile_manager()->GetProfileInfoCache();
size_t index =
cache.GetIndexOfProfileWithPath(browser_->profile()->GetPath());
if (index == std::string::npos)
return;
BOOL is_gaia_picture =
cache.IsUsingGAIAPictureOfProfileAtIndex(index) &&
cache.GetGAIAPictureOfProfileAtIndex(index);
gfx::Image icon = profiles::GetAvatarIconForTitleBar(
cache.GetAvatarIconOfProfileAtIndex(index), is_gaia_picture,
profiles::kAvatarIconWidth, profiles::kAvatarIconHeight);
[self setImage:icon.ToNSImage()];
const string16& name = cache.GetNameOfProfileAtIndex(index);
NSString* nsName = base::SysUTF16ToNSString(name);
[button_ setToolTip:nsName];
[[button_ cell]
accessibilitySetOverrideValue:nsName
forAttribute:NSAccessibilityValueAttribute];
}
// If the second-to-last profile was removed or a second profile was added,
// show or hide the avatar button from the window frame.
- (void)addOrRemoveButtonIfNecessary {
if (browser_->profile()->IsOffTheRecord())
return;
NSWindowController* wc =
[browser_->window()->GetNativeWindow() windowController];
if (![wc isKindOfClass:[BrowserWindowController class]])
return;
size_t count = g_browser_process->profile_manager()->GetNumberOfProfiles();
[self.view setHidden:count < 2];
[static_cast<BrowserWindowController*>(wc) layoutSubviews];
// If the avatar is being added or removed, then the Lion fullscreen button
// needs to be adjusted. Since the fullscreen button is positioned by
// FramedBrowserWindow using private APIs, the easiest way to update the
// position of the button is through this private API. Resizing the window
// also works, but invoking |-display| does not.
NSView* themeFrame = [[[wc window] contentView] superview];
if ([themeFrame respondsToSelector:@selector(_tileTitlebarAndRedisplay:)])
[themeFrame _tileTitlebarAndRedisplay:YES];
}
// Testing /////////////////////////////////////////////////////////////////////
- (AvatarMenuBubbleController*)menuController {
return menuController_;
}
@end