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_menu_bubble_controller.h" 6 7#include "base/mac/bundle_locations.h" 8#include "base/mac/mac_util.h" 9#include "base/strings/sys_string_conversions.h" 10#include "chrome/browser/browser_process.h" 11#include "chrome/browser/profiles/avatar_menu.h" 12#include "chrome/browser/profiles/profile_info_cache.h" 13#include "chrome/browser/profiles/profile_manager.h" 14#include "chrome/browser/profiles/profile_metrics.h" 15#include "chrome/browser/ui/browser.h" 16#include "chrome/browser/ui/browser_window.h" 17#import "chrome/browser/ui/cocoa/info_bubble_view.h" 18#import "chrome/browser/ui/cocoa/info_bubble_window.h" 19#include "grit/generated_resources.h" 20#include "grit/theme_resources.h" 21#import "third_party/google_toolbox_for_mac/src/AppKit/GTMUILocalizerAndLayoutTweaker.h" 22#import "ui/base/cocoa/controls/hyperlink_button_cell.h" 23#import "ui/base/cocoa/cocoa_base_utils.h" 24#include "ui/base/l10n/l10n_util_mac.h" 25#include "ui/base/resource/resource_bundle.h" 26#include "ui/gfx/image/image.h" 27 28@interface AvatarMenuBubbleController (Private) 29- (AvatarMenu*)menu; 30- (NSView*)configureSupervisedUserInformation:(CGFloat)width; 31- (NSButton*)configureNewUserButton:(CGFloat)yOffset 32 updateWidthAdjust:(CGFloat*)widthAdjust; 33- (NSButton*)configureSwitchUserButton:(CGFloat)yOffset 34 updateWidthAdjust:(CGFloat*)widthAdjust; 35- (AvatarMenuItemController*)initAvatarItem:(int)itemIndex 36 updateWidthAdjust:(CGFloat*)widthAdjust 37 setYOffset:(CGFloat)yOffset; 38- (void)setWindowFrame:(CGFloat)yOffset widthAdjust:(CGFloat)width; 39- (void)initMenuContents; 40- (void)initSupervisedUserContents; 41- (void)keyDown:(NSEvent*)theEvent; 42- (void)moveDown:(id)sender; 43- (void)moveUp:(id)sender; 44- (void)insertNewline:(id)sender; 45- (void)highlightNextItemByDelta:(NSInteger)delta; 46- (void)highlightItem:(AvatarMenuItemController*)newItem; 47@end 48 49namespace { 50 51// Constants taken from the Windows/Views implementation at: 52// chrome/browser/ui/views/avatar_menu_bubble_view.cc 53const CGFloat kBubbleMinWidth = 175; 54const CGFloat kBubbleMaxWidth = 800; 55const CGFloat kMaxItemTextWidth = 200; 56 57// Values derived from the XIB. 58const CGFloat kVerticalSpacing = 10.0; 59const CGFloat kLinkSpacing = 15.0; 60const CGFloat kLabelInset = 49.0; 61 62// The offset of the supervised user information label and the "switch user" 63// link. 64const CGFloat kSupervisedUserSpacing = 26.0; 65 66} // namespace 67 68@implementation AvatarMenuBubbleController 69 70- (id)initWithBrowser:(Browser*)parentBrowser 71 anchoredAt:(NSPoint)point { 72 73 // Pass in a NULL observer. Rebuilding while the bubble is open will cause it 74 // to be positioned incorrectly. Since the bubble will be dismissed on losing 75 // key status, it's impossible for the user to edit the information in a 76 // meaningful way such that it would need to be redrawn. 77 AvatarMenu* menu = new AvatarMenu( 78 &g_browser_process->profile_manager()->GetProfileInfoCache(), 79 NULL, parentBrowser); 80 menu->RebuildMenu(); 81 82 if ((self = [self initWithMenu:menu 83 parentWindow:parentBrowser->window()->GetNativeWindow() 84 anchoredAt:point])) { 85 } 86 return self; 87} 88 89- (IBAction)newProfile:(id)sender { 90 menu_->AddNewProfile(ProfileMetrics::ADD_NEW_USER_ICON); 91} 92 93- (IBAction)switchToProfile:(id)sender { 94 // Check the event flags to see if a new window should be crated. 95 bool always_create = ui::WindowOpenDispositionFromNSEvent( 96 [NSApp currentEvent]) == NEW_WINDOW; 97 menu_->SwitchToProfile([sender menuIndex], always_create, 98 ProfileMetrics::SWITCH_PROFILE_ICON); 99} 100 101- (IBAction)editProfile:(id)sender { 102 menu_->EditProfile([sender menuIndex]); 103} 104 105- (IBAction)switchProfile:(id)sender { 106 expanded_ = YES; 107 [self performLayout]; 108} 109 110// Private ///////////////////////////////////////////////////////////////////// 111 112- (id)initWithMenu:(AvatarMenu*)menu 113 parentWindow:(NSWindow*)parent 114 anchoredAt:(NSPoint)point { 115 // Use an arbitrary height because it will reflect the size of the content. 116 NSRect contentRect = NSMakeRect(0, 0, kBubbleMinWidth, 150); 117 // Create an empty window into which content is placed. 118 base::scoped_nsobject<InfoBubbleWindow> window( 119 [[InfoBubbleWindow alloc] initWithContentRect:contentRect 120 styleMask:NSBorderlessWindowMask 121 backing:NSBackingStoreBuffered 122 defer:NO]); 123 if ((self = [super initWithWindow:window 124 parentWindow:parent 125 anchoredAt:point])) { 126 menu_.reset(menu); 127 128 [window accessibilitySetOverrideValue: 129 l10n_util::GetNSString(IDS_PROFILES_BUBBLE_ACCESSIBLE_NAME) 130 forAttribute:NSAccessibilityTitleAttribute]; 131 [window accessibilitySetOverrideValue: 132 l10n_util::GetNSString(IDS_PROFILES_BUBBLE_ACCESSIBLE_DESCRIPTION) 133 forAttribute:NSAccessibilityHelpAttribute]; 134 135 [[self bubble] setArrowLocation:info_bubble::kTopRight]; 136 [self performLayout]; 137 } 138 return self; 139} 140 141- (AvatarMenuItemController*)initAvatarItem:(int)itemIndex 142 updateWidthAdjust:(CGFloat*)widthAdjust 143 setYOffset:(CGFloat)yOffset { 144 ResourceBundle& rb = ResourceBundle::GetSharedInstance(); 145 const AvatarMenu::Item& item = menu_->GetItemAt(itemIndex); 146 // Create the item view controller. Autorelease it because it will be owned 147 // by the |items_| array. 148 AvatarMenuItemController* itemView = 149 [[[AvatarMenuItemController alloc] initWithMenuIndex:itemIndex 150 menuController:self] autorelease]; 151 itemView.iconView.image = item.icon.ToNSImage(); 152 153 // Adjust the name field to fit the string. If it overflows, record by how 154 // much the window needs to grow to accomodate the new size of the field. 155 NSTextField* nameField = itemView.nameField; 156 nameField.stringValue = base::SysUTF16ToNSString(item.name); 157 NSSize delta = [GTMUILocalizerAndLayoutTweaker sizeToFitView:nameField]; 158 if (NSWidth([nameField frame]) > kMaxItemTextWidth) { 159 delta.width -= (NSWidth([nameField frame]) - kMaxItemTextWidth); 160 NSRect frame = [nameField frame]; 161 frame.size.width = kMaxItemTextWidth; 162 [nameField setFrame:frame]; 163 } 164 *widthAdjust = std::max(*widthAdjust, delta.width); 165 166 // Repeat for the sync state/email. 167 NSTextField* emailField = itemView.emailField; 168 emailField.stringValue = base::SysUTF16ToNSString(item.sync_state); 169 delta = [GTMUILocalizerAndLayoutTweaker sizeToFitView:emailField]; 170 if (NSWidth([emailField frame]) > kMaxItemTextWidth) { 171 delta.width -= (NSWidth([emailField frame]) - kMaxItemTextWidth); 172 NSRect frame = [emailField frame]; 173 frame.size.width = kMaxItemTextWidth; 174 [emailField setFrame:frame]; 175 } 176 *widthAdjust = std::max(*widthAdjust, delta.width); 177 178 if (!item.active) { 179 // In the inactive case, hide additional UI. 180 [itemView.activeView setHidden:YES]; 181 [itemView.editButton setHidden:YES]; 182 } else { 183 // Otherwise, set up the edit button and its three interaction states. 184 itemView.activeView.image = 185 rb.GetImageNamed(IDR_PROFILE_SELECTED).ToNSImage(); 186 } 187 188 // Add the item to the content view. 189 [[itemView view] setFrameOrigin:NSMakePoint(0, yOffset)]; 190 191 // Keep track of the view controller. 192 [items_ addObject:itemView]; 193 return itemView; 194} 195 196- (void)setWindowFrame:(CGFloat)yOffset widthAdjust:(CGFloat)width { 197 // Set the window frame, clamping the width at a sensible max. 198 NSRect frame = [[self window] frame]; 199 // Adjust the origin after we have switched from the supervised user menu to 200 // the regular menu. 201 CGFloat newWidth = std::min(kBubbleMinWidth + width, kBubbleMaxWidth); 202 if (expanded_) { 203 frame.origin.x += frame.size.width - newWidth; 204 frame.origin.y += frame.size.height - yOffset; 205 } 206 frame.size.height = yOffset; 207 frame.size.width = newWidth; 208 [[self window] setFrame:frame display:YES]; 209} 210 211- (void)initMenuContents { 212 NSView* contentView = [[self window] contentView]; 213 214 // |yOffset| is the next position at which to draw in contentView coordinates. 215 // Use a little more vertical spacing because the items have padding built- 216 // into the xib, and this gives a little more space to visually match. 217 CGFloat yOffset = kLinkSpacing; 218 CGFloat widthAdjust = 0; 219 220 if (menu_->ShouldShowAddNewProfileLink()) { 221 // Since drawing happens bottom-up, start with the "New User" link. 222 NSButton* newButton = 223 [self configureNewUserButton:yOffset updateWidthAdjust:&widthAdjust]; 224 [contentView addSubview:newButton]; 225 yOffset += NSHeight([newButton frame]) + kVerticalSpacing; 226 227 NSBox* separator = [self horizontalSeparatorWithFrame: 228 NSMakeRect(10, yOffset, NSWidth([contentView frame]) - 20, 0)]; 229 [separator setAutoresizingMask:NSViewWidthSizable]; 230 [contentView addSubview:separator]; 231 232 yOffset += NSHeight([separator frame]); 233 } else { 234 yOffset = 7; 235 } 236 237 // Loop over the profiles in reverse, constructing the menu items. 238 for (int i = menu_->GetNumberOfItems() - 1; i >= 0; --i) { 239 AvatarMenuItemController* itemView = [self initAvatarItem:i 240 updateWidthAdjust:&widthAdjust 241 setYOffset:yOffset]; 242 [contentView addSubview:[itemView view]]; 243 yOffset += NSHeight([[itemView view] frame]); 244 } 245 246 yOffset += kVerticalSpacing * 1.5; 247 [self setWindowFrame:yOffset widthAdjust:widthAdjust]; 248} 249 250- (void)initSupervisedUserContents { 251 NSView* contentView = [[self window] contentView]; 252 253 // |yOffset| is the next position at which to draw in contentView coordinates. 254 // Use a little more vertical spacing because the items have padding built- 255 // into the xib, and this gives a little more space to visually match. 256 CGFloat yOffset = kLinkSpacing; 257 CGFloat widthAdjust = 0; 258 259 // Since drawing happens bottom-up, start with the "Switch User" link. 260 NSButton* newButton = 261 [self configureSwitchUserButton:yOffset updateWidthAdjust:&widthAdjust]; 262 [contentView addSubview:newButton]; 263 yOffset += NSHeight([newButton frame]) + kVerticalSpacing; 264 265 NSBox* separator = [self horizontalSeparatorWithFrame: 266 NSMakeRect(10, yOffset, NSWidth([contentView frame]) - 20, 0)]; 267 [separator setAutoresizingMask:NSViewWidthSizable]; 268 [contentView addSubview:separator]; 269 270 yOffset += NSHeight([separator frame]) + kVerticalSpacing; 271 272 // First init the active profile in order to determine the required width. We 273 // will have to adjust its frame later after adding general information about 274 // supervised users. 275 AvatarMenuItemController* itemView = 276 [self initAvatarItem:menu_->GetActiveProfileIndex() 277 updateWidthAdjust:&widthAdjust 278 setYOffset:yOffset]; 279 280 // Don't increase the width too much (the total size should be at most 281 // |kBubbleMaxWidth|). 282 widthAdjust = std::min(widthAdjust, kBubbleMaxWidth - kBubbleMinWidth); 283 CGFloat newWidth = kBubbleMinWidth + widthAdjust; 284 285 // Add general information about supervised users. 286 NSView* info = [self configureSupervisedUserInformation:newWidth]; 287 [info setFrameOrigin:NSMakePoint(0, yOffset)]; 288 [contentView addSubview:info]; 289 yOffset += NSHeight([info frame]) + kVerticalSpacing; 290 291 separator = [self horizontalSeparatorWithFrame: 292 NSMakeRect(10, yOffset, NSWidth([contentView frame]) - 20, 0)]; 293 [separator setAutoresizingMask:NSViewWidthSizable]; 294 [contentView addSubview:separator]; 295 296 yOffset += NSHeight([separator frame]); 297 298 // Now update the frame of the active profile and add it. 299 NSRect frame = [[itemView view] frame]; 300 frame.origin.y = yOffset; 301 [[itemView view] setFrame:frame]; 302 [contentView addSubview:[itemView view]]; 303 304 yOffset += NSHeight(frame) + kVerticalSpacing * 1.5; 305 [self setWindowFrame:yOffset widthAdjust:widthAdjust]; 306} 307 308- (void)performLayout { 309 NSView* contentView = [[self window] contentView]; 310 311 // Reset the array of controllers and remove all the views. 312 items_.reset([[NSMutableArray alloc] init]); 313 [contentView setSubviews:[NSArray array]]; 314 315 if (menu_->GetSupervisedUserInformation().empty() || expanded_) 316 [self initMenuContents]; 317 else 318 [self initSupervisedUserContents]; 319} 320 321- (NSView*)configureSupervisedUserInformation:(CGFloat)width { 322 base::scoped_nsobject<NSView> container( 323 [[NSView alloc] initWithFrame:NSZeroRect]); 324 325 // Add the limited user icon on the left side of the information TextView. 326 base::scoped_nsobject<NSImageView> iconView( 327 [[NSImageView alloc] initWithFrame:NSMakeRect(5, 0, 16, 16)]); 328 [iconView setImage:menu_->GetSupervisedUserIcon().ToNSImage()]; 329 [container addSubview:iconView]; 330 331 NSString* info = 332 base::SysUTF16ToNSString(menu_->GetSupervisedUserInformation()); 333 NSDictionary* attributes = 334 @{ NSFontAttributeName : [NSFont labelFontOfSize:12] }; 335 base::scoped_nsobject<NSAttributedString> attrString( 336 [[NSAttributedString alloc] initWithString:info attributes:attributes]); 337 base::scoped_nsobject<NSTextView> label( 338 [[NSTextView alloc] initWithFrame:NSMakeRect( 339 kSupervisedUserSpacing, 0, width - kSupervisedUserSpacing - 5, 0)]); 340 [[label textStorage] setAttributedString:attrString]; 341 [label setHorizontallyResizable:NO]; 342 [label setEditable:NO]; 343 [label sizeToFit]; 344 [container addSubview:label]; 345 [container setFrameSize:NSMakeSize(width, NSHeight([label frame]))]; 346 347 // Reposition the limited user icon so that it is on top. 348 [iconView setFrameOrigin:NSMakePoint(5, NSHeight([label frame]) - 16)]; 349 return container.autorelease(); 350} 351 352- (NSButton*)configureNewUserButton:(CGFloat)yOffset 353 updateWidthAdjust:(CGFloat*)widthAdjust { 354 base::scoped_nsobject<NSButton> newButton([[NSButton alloc] initWithFrame: 355 NSMakeRect(kLabelInset, yOffset, kBubbleMinWidth - kLabelInset, 16)]); 356 base::scoped_nsobject<HyperlinkButtonCell> buttonCell( 357 [[HyperlinkButtonCell alloc] initTextCell: 358 l10n_util::GetNSString(IDS_PROFILES_CREATE_NEW_PROFILE_LINK)]); 359 [newButton setCell:buttonCell.get()]; 360 [newButton setFont:[NSFont labelFontOfSize:12.0]]; 361 [newButton setBezelStyle:NSRegularSquareBezelStyle]; 362 [newButton setTarget:self]; 363 [newButton setAction:@selector(newProfile:)]; 364 NSSize delta = [GTMUILocalizerAndLayoutTweaker sizeToFitView:newButton]; 365 if (delta.width > 0) 366 *widthAdjust = std::max(*widthAdjust, delta.width); 367 return newButton.autorelease(); 368} 369 370- (NSButton*)configureSwitchUserButton:(CGFloat)yOffset 371 updateWidthAdjust:(CGFloat*)widthAdjust { 372 base::scoped_nsobject<NSButton> newButton( 373 [[NSButton alloc] initWithFrame:NSMakeRect( 374 kSupervisedUserSpacing, yOffset, kBubbleMinWidth - kLabelInset, 16)]); 375 base::scoped_nsobject<HyperlinkButtonCell> buttonCell( 376 [[HyperlinkButtonCell alloc] initTextCell: 377 l10n_util::GetNSString(IDS_PROFILES_SWITCH_PROFILE_LINK)]); 378 [newButton setCell:buttonCell.get()]; 379 [newButton setFont:[NSFont labelFontOfSize:12.0]]; 380 [newButton setBezelStyle:NSRegularSquareBezelStyle]; 381 [newButton setTarget:self]; 382 [newButton setAction:@selector(switchProfile:)]; 383 NSSize delta = [GTMUILocalizerAndLayoutTweaker sizeToFitView:newButton]; 384 if (delta.width > 0) 385 *widthAdjust = std::max(*widthAdjust, delta.width); 386 return newButton.autorelease(); 387} 388 389- (NSMutableArray*)items { 390 return items_.get(); 391} 392 393- (void)keyDown:(NSEvent*)theEvent { 394 [self interpretKeyEvents:[NSArray arrayWithObject:theEvent]]; 395} 396 397- (void)moveDown:(id)sender { 398 [self highlightNextItemByDelta:-1]; 399} 400 401- (void)moveUp:(id)sender { 402 [self highlightNextItemByDelta:1]; 403} 404 405- (void)insertNewline:(id)sender { 406 for (AvatarMenuItemController* item in items_.get()) { 407 if ([item isHighlighted]) { 408 [self switchToProfile:item]; 409 return; 410 } 411 } 412} 413 414- (void)highlightNextItemByDelta:(NSInteger)delta { 415 NSUInteger count = [items_ count]; 416 if (count == 0) 417 return; 418 419 NSInteger old_index = -1; 420 for (NSUInteger i = 0; i < count; ++i) { 421 if ([[items_ objectAtIndex:i] isHighlighted]) { 422 old_index = i; 423 break; 424 } 425 } 426 427 NSInteger new_index; 428 // If nothing is selected then start at the top if we're going down and start 429 // at the bottom if we're going up. 430 if (old_index == -1) 431 new_index = delta < 0 ? (count - 1) : 0; 432 else 433 new_index = old_index + delta; 434 435 // Cap the index. We don't wrap around to match the behavior of Mac menus. 436 new_index = 437 std::min(std::max(static_cast<NSInteger>(0), new_index), 438 static_cast<NSInteger>(count - 1)); 439 440 [self highlightItem:[items_ objectAtIndex:new_index]]; 441} 442 443- (void)highlightItem:(AvatarMenuItemController*)newItem { 444 AvatarMenuItemController* oldItem = nil; 445 for (AvatarMenuItemController* item in items_.get()) { 446 if ([item isHighlighted]) { 447 oldItem = item; 448 break; 449 } 450 } 451 452 if (oldItem == newItem) 453 return; 454 455 [oldItem setIsHighlighted:NO]; 456 [newItem setIsHighlighted:YES]; 457} 458 459 460@end 461 462// Menu Item Controller //////////////////////////////////////////////////////// 463 464@interface AvatarMenuItemController (Private) 465- (void)animateFromView:(NSView*)outView toView:(NSView*)inView; 466@end 467 468@implementation AvatarMenuItemController 469 470@synthesize menuIndex = menuIndex_; 471@synthesize isHighlighted = isHighlighted_; 472@synthesize iconView = iconView_; 473@synthesize activeView = activeView_; 474@synthesize nameField = nameField_; 475@synthesize emailField = emailField_; 476@synthesize editButton = editButton_; 477 478- (id)initWithMenuIndex:(size_t)menuIndex 479 menuController:(AvatarMenuBubbleController*)controller { 480 if ((self = [super initWithNibName:@"AvatarMenuItem" 481 bundle:base::mac::FrameworkBundle()])) { 482 menuIndex_ = menuIndex; 483 controller_ = controller; 484 [self loadView]; 485 [nameField_ setAutoresizingMask:NSViewNotSizable]; 486 [[nameField_ cell] setLineBreakMode:NSLineBreakByTruncatingTail]; 487 [emailField_ setAutoresizingMask:NSViewNotSizable]; 488 [[emailField_ cell] setLineBreakMode:NSLineBreakByTruncatingTail]; 489 } 490 return self; 491} 492 493- (void)dealloc { 494 static_cast<AvatarMenuItemView*>(self.view).viewController = nil; 495 [linkAnimation_ stopAnimation]; 496 [linkAnimation_ setDelegate:nil]; 497 [super dealloc]; 498} 499 500- (void)awakeFromNib { 501 [GTMUILocalizerAndLayoutTweaker sizeToFitView:self.editButton]; 502 self.editButton.hidden = YES; 503} 504 505- (IBAction)switchToProfile:(id)sender { 506 [controller_ switchToProfile:self]; 507} 508 509- (IBAction)editProfile:(id)sender { 510 [controller_ editProfile:self]; 511} 512 513- (void)highlightForEventType:(NSEventType)type { 514 switch (type) { 515 case NSMouseEntered: 516 [controller_ highlightItem:self]; 517 break; 518 519 case NSMouseExited: 520 [controller_ highlightItem:nil]; 521 break; 522 523 default: 524 NOTREACHED(); 525 }; 526} 527 528- (void)setIsHighlighted:(BOOL)isHighlighted { 529 if (isHighlighted_ == isHighlighted) 530 return; 531 532 isHighlighted_ = isHighlighted; 533 [[self view] setNeedsDisplay:YES]; 534 535 // Cancel any running animation. 536 if (linkAnimation_.get()) { 537 [NSObject cancelPreviousPerformRequestsWithTarget:linkAnimation_ 538 selector:@selector(startAnimation) 539 object:nil]; 540 } 541 542 // Fade the edit link in or out only if this is the active view. 543 if (self.activeView.isHidden) 544 return; 545 546 if (isHighlighted_) { 547 [self animateFromView:self.emailField toView:self.editButton]; 548 } else { 549 // If the edit button is visible or the animation to make it so is 550 // running, stop the animation and fade it back to the email. If not, then 551 // don't run an animation to prevent flickering. 552 if (!self.editButton.isHidden || [linkAnimation_ isAnimating]) { 553 [linkAnimation_ stopAnimation]; 554 linkAnimation_.reset(); 555 [self animateFromView:self.editButton toView:self.emailField]; 556 } 557 } 558} 559 560- (void)animateFromView:(NSView*)outView toView:(NSView*)inView { 561 const NSTimeInterval kAnimationDuration = 0.175; 562 563 NSDictionary* outDict = [NSDictionary dictionaryWithObjectsAndKeys: 564 outView, NSViewAnimationTargetKey, 565 NSViewAnimationFadeOutEffect, NSViewAnimationEffectKey, 566 nil 567 ]; 568 NSDictionary* inDict = [NSDictionary dictionaryWithObjectsAndKeys: 569 inView, NSViewAnimationTargetKey, 570 NSViewAnimationFadeInEffect, NSViewAnimationEffectKey, 571 nil 572 ]; 573 574 linkAnimation_.reset([[NSViewAnimation alloc] initWithViewAnimations: 575 [NSArray arrayWithObjects:outDict, inDict, nil]]); 576 [linkAnimation_ setDelegate:self]; 577 [linkAnimation_ setDuration:kAnimationDuration]; 578 579 [self willStartAnimation:linkAnimation_]; 580 581 [linkAnimation_ performSelector:@selector(startAnimation) 582 withObject:nil 583 afterDelay:0.2]; 584} 585 586- (void)willStartAnimation:(NSAnimation*)animation { 587} 588 589- (void)animationDidEnd:(NSAnimation*)animation { 590 if (animation == linkAnimation_.get()) 591 linkAnimation_.reset(); 592} 593 594- (void)animationDidStop:(NSAnimation*)animation { 595 if (animation == linkAnimation_.get()) 596 linkAnimation_.reset(); 597} 598 599@end 600 601// Profile Switch Button /////////////////////////////////////////////////////// 602 603@implementation AvatarMenuItemView 604 605@synthesize viewController = viewController_; 606 607- (void)awakeFromNib { 608 [self updateTrackingAreas]; 609} 610 611- (void)updateTrackingAreas { 612 if (trackingArea_.get()) 613 [self removeTrackingArea:trackingArea_.get()]; 614 615 trackingArea_.reset( 616 [[CrTrackingArea alloc] initWithRect:[self bounds] 617 options:NSTrackingMouseEnteredAndExited | 618 NSTrackingActiveInKeyWindow 619 owner:self 620 userInfo:nil]); 621 [self addTrackingArea:trackingArea_.get()]; 622 623 [super updateTrackingAreas]; 624} 625 626- (void)mouseEntered:(id)sender { 627 [viewController_ highlightForEventType:[[NSApp currentEvent] type]]; 628 [self setNeedsDisplay:YES]; 629} 630 631- (void)mouseExited:(id)sender { 632 [viewController_ highlightForEventType:[[NSApp currentEvent] type]]; 633 [self setNeedsDisplay:YES]; 634} 635 636- (void)mouseUp:(id)sender { 637 [viewController_ switchToProfile:self]; 638} 639 640- (void)drawRect:(NSRect)dirtyRect { 641 NSColor* backgroundColor = nil; 642 if ([viewController_ isHighlighted]) { 643 backgroundColor = [NSColor colorWithCalibratedRed:223.0/255 644 green:238.0/255 645 blue:246.0/255 646 alpha:1.0]; 647 } else { 648 backgroundColor = [NSColor clearColor]; 649 } 650 651 [backgroundColor set]; 652 [NSBezierPath fillRect:[self bounds]]; 653} 654 655// Make sure the element is focusable for accessibility. 656- (BOOL)canBecomeKeyView { 657 return YES; 658} 659 660- (BOOL)accessibilityIsIgnored { 661 return NO; 662} 663 664- (NSArray*)accessibilityAttributeNames { 665 NSMutableArray* attributes = 666 [[super accessibilityAttributeNames] mutableCopy]; 667 [attributes addObject:NSAccessibilityTitleAttribute]; 668 [attributes addObject:NSAccessibilityEnabledAttribute]; 669 670 return [attributes autorelease]; 671} 672 673- (NSArray*)accessibilityActionNames { 674 NSArray* parentActions = [super accessibilityActionNames]; 675 return [parentActions arrayByAddingObject:NSAccessibilityPressAction]; 676} 677 678- (id)accessibilityAttributeValue:(NSString*)attribute { 679 if ([attribute isEqual:NSAccessibilityRoleAttribute]) 680 return NSAccessibilityButtonRole; 681 682 if ([attribute isEqual:NSAccessibilityRoleDescriptionAttribute]) 683 return NSAccessibilityRoleDescription(NSAccessibilityButtonRole, nil); 684 685 if ([attribute isEqual:NSAccessibilityTitleAttribute]) { 686 return l10n_util::GetNSStringF( 687 IDS_PROFILES_SWITCH_TO_PROFILE_ACCESSIBLE_NAME, 688 base::SysNSStringToUTF16(self.viewController.nameField.stringValue)); 689 } 690 691 if ([attribute isEqual:NSAccessibilityEnabledAttribute]) 692 return [NSNumber numberWithBool:YES]; 693 694 return [super accessibilityAttributeValue:attribute]; 695} 696 697- (void)accessibilityPerformAction:(NSString*)action { 698 if ([action isEqual:NSAccessibilityPressAction]) { 699 [viewController_ switchToProfile:self]; 700 return; 701 } 702 703 [super accessibilityPerformAction:action]; 704} 705 706@end 707 708//////////////////////////////////////////////////////////////////////////////// 709 710@implementation AccessibilityIgnoredImageCell 711- (BOOL)accessibilityIsIgnored { 712 return YES; 713} 714@end 715 716@implementation AccessibilityIgnoredTextFieldCell 717- (BOOL)accessibilityIsIgnored { 718 return YES; 719} 720@end 721