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