| Index: chrome/browser/ui/cocoa/browser/avatar_menu_bubble_controller.mm
|
| diff --git a/chrome/browser/ui/cocoa/browser/avatar_menu_bubble_controller.mm b/chrome/browser/ui/cocoa/browser/avatar_menu_bubble_controller.mm
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..2a21437a604dc4f9be2c2a6ce2e6abaf780b42a9
|
| --- /dev/null
|
| +++ b/chrome/browser/ui/cocoa/browser/avatar_menu_bubble_controller.mm
|
| @@ -0,0 +1,326 @@
|
| +// Copyright (c) 2011 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_menu_bubble_controller.h"
|
| +
|
| +#include "base/mac/mac_util.h"
|
| +#include "base/sys_string_conversions.h"
|
| +#include "chrome/browser/browser_process.h"
|
| +#include "chrome/browser/profiles/avatar_menu_model.h"
|
| +#include "chrome/browser/profiles/avatar_menu_model_observer.h"
|
| +#include "chrome/browser/profiles/profile_manager.h"
|
| +#include "chrome/browser/profiles/profile_info_cache.h"
|
| +#include "chrome/browser/ui/browser.h"
|
| +#include "chrome/browser/ui/browser_window.h"
|
| +#import "chrome/browser/ui/cocoa/hover_image_button.h"
|
| +#import "chrome/browser/ui/cocoa/hyperlink_button_cell.h"
|
| +#import "chrome/browser/ui/cocoa/info_bubble_view.h"
|
| +#import "chrome/browser/ui/cocoa/info_bubble_window.h"
|
| +#include "grit/generated_resources.h"
|
| +#include "grit/theme_resources.h"
|
| +#import "third_party/GTM/AppKit/GTMUILocalizerAndLayoutTweaker.h"
|
| +#include "ui/base/l10n/l10n_util_mac.h"
|
| +#include "ui/base/resource/resource_bundle.h"
|
| +#include "ui/gfx/image/image.h"
|
| +
|
| +@interface AvatarMenuBubbleController (Private)
|
| +- (AvatarMenuModel*)model;
|
| +- (NSButton*)configureNewUserButton:(CGFloat)yOffset;
|
| +- (void)configureEditButton:(HoverImageButton*)button;
|
| +@end
|
| +
|
| +namespace AvatarMenuInternal {
|
| +
|
| +class Bridge : public AvatarMenuModelObserver {
|
| + public:
|
| + Bridge(AvatarMenuBubbleController* controller) : controller_(controller) {}
|
| +
|
| + // AvatarMenuModelObserver:
|
| + void OnAvatarMenuModelChanged(AvatarMenuModel* model) {
|
| + [controller_ performLayout];
|
| + }
|
| +
|
| + private:
|
| + AvatarMenuBubbleController* controller_;
|
| +};
|
| +
|
| +} // namespace AvatarMenuInternal
|
| +
|
| +namespace {
|
| +
|
| +// Constants taken from the Windows/Views implementation at:
|
| +// chrome/browser/ui/views/avatar_menu_bubble_view.cc
|
| +const CGFloat kBubbleMinWidth = 175;
|
| +const CGFloat kBubbleMaxWidth = 800;
|
| +
|
| +// Values derived from the XIB.
|
| +const CGFloat kVerticalSpacing = 10.0;
|
| +const CGFloat kLinkSpacing = 15.0;
|
| +const CGFloat kLabelInset = 49.0;
|
| +
|
| +} // namespace
|
| +
|
| +@implementation AvatarMenuBubbleController
|
| +
|
| +- (id)initWithBrowser:(Browser*)parentBrowser
|
| + anchoredAt:(NSPoint)point {
|
| +
|
| + AvatarMenuInternal::Bridge* bridge = new AvatarMenuInternal::Bridge(self);
|
| + AvatarMenuModel* model = new AvatarMenuModel(
|
| + &g_browser_process->profile_manager()->GetProfileInfoCache(),
|
| + bridge, parentBrowser);
|
| +
|
| + if ((self = [self initWithModel:model
|
| + bridge:bridge
|
| + parentWindow:parentBrowser->window()->GetNativeHandle()
|
| + anchoredAt:point])) {
|
| + }
|
| + return self;
|
| +}
|
| +
|
| +- (IBAction)newProfile:(id)sender {
|
| + model_->AddNewProfile();
|
| +}
|
| +
|
| +- (IBAction)switchToProfile:(id)sender {
|
| + model_->SwitchToProfile([sender modelIndex]);
|
| +}
|
| +
|
| +- (IBAction)editProfile:(id)sender {
|
| + model_->EditProfile([sender modelIndex]);
|
| +}
|
| +
|
| +// Private /////////////////////////////////////////////////////////////////////
|
| +
|
| +- (id)initWithModel:(AvatarMenuModel*)model
|
| + bridge:(AvatarMenuModelObserver*)bridge
|
| + parentWindow:(NSWindow*)parent
|
| + anchoredAt:(NSPoint)point {
|
| + // Use an arbitrary height because it will reflect the size of the content.
|
| + NSRect contentRect = NSMakeRect(0, 0, kBubbleMinWidth, 150);
|
| + // Create an empty window into which content is placed.
|
| + scoped_nsobject<InfoBubbleWindow> window(
|
| + [[InfoBubbleWindow alloc] initWithContentRect:contentRect
|
| + styleMask:NSBorderlessWindowMask
|
| + backing:NSBackingStoreBuffered
|
| + defer:NO]);
|
| + if ((self = [super initWithWindow:window
|
| + parentWindow:parent
|
| + anchoredAt:point])) {
|
| + bridge_.reset(bridge);
|
| + model_.reset(model);
|
| + [[self bubble] setArrowLocation:info_bubble::kTopRight];
|
| + [self performLayout];
|
| + }
|
| + return self;
|
| +}
|
| +
|
| +- (void)performLayout {
|
| + ResourceBundle& rb = ResourceBundle::GetSharedInstance();
|
| + NSView* contentView = [[self window] contentView];
|
| +
|
| + // Reset the array of controllers and remove all the views.
|
| + items_.reset([[NSMutableArray alloc] init]);
|
| + [contentView setSubviews:[NSArray array]];
|
| +
|
| + // |yOffset| is the next position at which to draw in contentView coordinates.
|
| + // Use a little more vertical spacing because the items have padding built-
|
| + // into the xib, and this gives a little more space to visually match.
|
| + CGFloat yOffset = kLinkSpacing;
|
| +
|
| + // Since drawing happens bottom-up, start with the "New User" link.
|
| + NSButton* newButton = [self configureNewUserButton:yOffset];
|
| + [contentView addSubview:newButton];
|
| + yOffset += NSHeight([newButton frame]) + (kLinkSpacing - kVerticalSpacing);
|
| +
|
| + // Loop over the profiles in reverse, constructing the menu items.
|
| + CGFloat widthAdjust = 0;
|
| + for (int i = model_->GetNumberOfItems() - 1; i >= 0; --i) {
|
| + const AvatarMenuModel::Item& item = model_->GetItemAt(i);
|
| +
|
| + // Create the item view controller. Autorelease it because it will be owned
|
| + // by the |items_| array.
|
| + AvatarMenuItemController* itemView =
|
| + [[[AvatarMenuItemController alloc] initWithModelIndex:item.model_index
|
| + menuController:self] autorelease];
|
| + itemView.iconView.image = item.icon.ToNSImage();
|
| +
|
| + // Adjust the name field to fit the string. If it overflows, record by how
|
| + // much the window needs to grow to accomodate the new size of the field.
|
| + NSTextField* nameField = itemView.nameField;
|
| + nameField.stringValue = base::SysUTF16ToNSString(item.name);
|
| + NSSize delta = [GTMUILocalizerAndLayoutTweaker sizeToFitView:nameField];
|
| + if (delta.width > 0)
|
| + widthAdjust = std::max(widthAdjust, delta.width);
|
| +
|
| + if (!item.active) {
|
| + // In the inactive case, hide additional UI.
|
| + [itemView.activeView setHidden:YES];
|
| + [itemView.editButton setHidden:YES];
|
| + } else {
|
| + // Otherwise, set up the edit button and its three interaction states.
|
| + [self configureEditButton:itemView.editButton];
|
| + itemView.activeView.image =
|
| + rb.GetImageNamed(IDR_PROFILE_SELECTED).ToNSImage();
|
| + }
|
| +
|
| + // Force a highlight of the default state to get the text colored correctly.
|
| + [itemView highlightForEventType:NSLeftMouseUp];
|
| +
|
| + // Add the item to the content view.
|
| + [[itemView view] setFrameOrigin:NSMakePoint(0, yOffset)];
|
| + [contentView addSubview:[itemView view]];
|
| + yOffset += NSHeight([[itemView view] frame]);
|
| +
|
| + // Keep track of the view controller.
|
| + [items_ addObject:itemView];
|
| + }
|
| +
|
| + yOffset += kVerticalSpacing;
|
| +
|
| + // Set the window frame, clamping the width at a sensible max.
|
| + NSRect frame = [[self window] frame];
|
| + frame.size.height = yOffset;
|
| + frame.size.width = kBubbleMinWidth + widthAdjust;
|
| + frame.size.width = std::min(NSWidth(frame), kBubbleMaxWidth);
|
| + [[self window] setFrame:frame display:YES];
|
| +}
|
| +
|
| +- (NSButton*)configureNewUserButton:(CGFloat)yOffset {
|
| + scoped_nsobject<NSButton> newButton(
|
| + [[NSButton alloc] initWithFrame:NSMakeRect(kLabelInset, yOffset,
|
| + 90, 16)]);
|
| + scoped_nsobject<HyperlinkButtonCell> buttonCell(
|
| + [[HyperlinkButtonCell alloc] initTextCell:
|
| + l10n_util::GetNSString(IDS_PROFILES_CREATE_NEW_PROFILE_LINK)]);
|
| + [newButton setCell:buttonCell.get()];
|
| + [newButton setFont:[NSFont labelFontOfSize:12.0]];
|
| + [newButton setBezelStyle:NSRegularSquareBezelStyle];
|
| + [newButton setTarget:self];
|
| + [newButton setAction:@selector(newProfile:)];
|
| + [GTMUILocalizerAndLayoutTweaker sizeToFitView:newButton];
|
| + return [newButton.release() autorelease];
|
| +}
|
| +
|
| +- (void)configureEditButton:(HoverImageButton*)button {
|
| + [button setTrackingEnabled:YES];
|
| +
|
| + ResourceBundle& rb = ResourceBundle::GetSharedInstance();
|
| + [button setDefaultImage:rb.GetImageNamed(IDR_PROFILE_EDIT).ToNSImage()];
|
| + [button setDefaultOpacity:1.0];
|
| +
|
| + [button setHoverImage:rb.GetImageNamed(IDR_PROFILE_EDIT_HOVER).ToNSImage()];
|
| + [button setHoverOpacity:1.0];
|
| +
|
| + [button setPressedImage:
|
| + rb.GetImageNamed(IDR_PROFILE_EDIT_PRESSED).ToNSImage()];
|
| + [button setPressedOpacity:1.0];
|
| +
|
| + [[button cell] setHighlightsBy:NSNoCellMask];
|
| +}
|
| +
|
| +- (NSMutableArray*)items {
|
| + return items_.get();
|
| +}
|
| +
|
| +@end
|
| +
|
| +// Menu Item Controller ////////////////////////////////////////////////////////
|
| +
|
| +@implementation AvatarMenuItemController
|
| +
|
| +@synthesize modelIndex = modelIndex_;
|
| +@synthesize iconView = iconView_;
|
| +@synthesize activeView = activeView_;
|
| +@synthesize nameField = nameField_;
|
| +@synthesize editButton = editButton_;
|
| +
|
| +- (id)initWithModelIndex:(size_t)modelIndex
|
| + menuController:(AvatarMenuBubbleController*)controller {
|
| + if ((self = [super initWithNibName:@"AvatarMenuItem"
|
| + bundle:base::mac::MainAppBundle()])) {
|
| + modelIndex_ = modelIndex;
|
| + controller_ = controller;
|
| + [self loadView];
|
| + }
|
| + return self;
|
| +}
|
| +
|
| +- (IBAction)switchToProfile:(id)sender {
|
| + [controller_ switchToProfile:self];
|
| +}
|
| +
|
| +- (IBAction)editProfile:(id)sender {
|
| + [controller_ editProfile:self];
|
| +}
|
| +
|
| +- (void)highlightForEventType:(NSEventType)type {
|
| + BOOL active = !self.activeView.isHidden;
|
| + NSColor* color = nil;
|
| + switch (type) {
|
| + case NSMouseEntered:
|
| + case NSLeftMouseDown:
|
| + if (active) {
|
| + color = [NSColor colorWithCalibratedRed:0 green:0 blue:0 alpha:1];
|
| + } else {
|
| + color = [NSColor colorWithCalibratedRed:0.25
|
| + green:0.25
|
| + blue:0.25
|
| + alpha:1];
|
| + }
|
| + break;
|
| +
|
| + case NSMouseExited:
|
| + case NSLeftMouseUp:
|
| + if (active) {
|
| + color = [NSColor colorWithCalibratedRed:0.12
|
| + green:0.12
|
| + blue:0.12
|
| + alpha:1];
|
| + } else {
|
| + color = [NSColor colorWithCalibratedRed:0.5
|
| + green:0.5
|
| + blue:0.5
|
| + alpha:1];
|
| + }
|
| + break;
|
| +
|
| + default:
|
| + NOTREACHED();
|
| + };
|
| +
|
| + DCHECK(color);
|
| + self.nameField.textColor = color;
|
| +}
|
| +
|
| +@end
|
| +
|
| +// Profile Switch Button ///////////////////////////////////////////////////////
|
| +
|
| +@implementation SwitchProfileButtonCell
|
| +
|
| +@synthesize viewController = viewController_;
|
| +
|
| +- (void)awakeFromNib {
|
| + // Needed to get entered and exited events.
|
| + self.showsBorderOnlyWhileMouseInside = YES;
|
| +}
|
| +
|
| +- (void)mouseEntered:(id)sender {
|
| + [viewController_ highlightForEventType:[[NSApp currentEvent] type]];
|
| +}
|
| +
|
| +- (void)mouseExited:(id)sender {
|
| + [viewController_ highlightForEventType:[[NSApp currentEvent] type]];
|
| +}
|
| +
|
| +- (void)mouseDown:(id)sender {
|
| + [viewController_ highlightForEventType:[[NSApp currentEvent] type]];
|
| +}
|
| +
|
| +- (void)mouseUp:(id)sender {
|
| + [viewController_ highlightForEventType:[[NSApp currentEvent] type]];
|
| +}
|
| +
|
| +@end
|
|
|