Commit dc84b46c authored by sail@chromium.org's avatar sail@chromium.org

Cocoa: Support keyboard navigation in avatar menu

Added support for up arrow, down arrow, and enter key.

BUG=75782
TEST=


Review URL: http://codereview.chromium.org/8332008

git-svn-id: svn://svn.chromium.org/chrome/trunk/src@106991 0039d316-1c4b-4281-b951-d872f2087c98
parent d9a49f86
...@@ -58,6 +58,9 @@ class Browser; ...@@ -58,6 +58,9 @@ class Browser;
// The AvatarMenuModel::item.model_index field. // The AvatarMenuModel::item.model_index field.
size_t modelIndex_; size_t modelIndex_;
// Tracks whether this item is currently highlighted.
BOOL isHighlighted_;
// Instance variables that back the outlets. // Instance variables that back the outlets.
__weak NSImageView* iconView_; __weak NSImageView* iconView_;
__weak NSImageView* activeView_; __weak NSImageView* activeView_;
...@@ -69,6 +72,7 @@ class Browser; ...@@ -69,6 +72,7 @@ class Browser;
__weak NSButton* editButton_; __weak NSButton* editButton_;
} }
@property(readonly, nonatomic) size_t modelIndex; @property(readonly, nonatomic) size_t modelIndex;
@property(assign, nonatomic) BOOL isHighlighted;
@property(assign, nonatomic) IBOutlet NSImageView* iconView; @property(assign, nonatomic) IBOutlet NSImageView* iconView;
@property(assign, nonatomic) IBOutlet NSImageView* activeView; @property(assign, nonatomic) IBOutlet NSImageView* activeView;
@property(assign, nonatomic) IBOutlet NSTextField* nameField; @property(assign, nonatomic) IBOutlet NSTextField* nameField;
...@@ -102,9 +106,6 @@ class Browser; ...@@ -102,9 +106,6 @@ class Browser;
// Used to highlight the background on hover. // Used to highlight the background on hover.
ScopedCrTrackingArea trackingArea_; ScopedCrTrackingArea trackingArea_;
// Whether the mouse is inside the bounds of this view.
BOOL mouseInside_;
} }
@property(assign, nonatomic) IBOutlet AvatarMenuItemController* viewController; @property(assign, nonatomic) IBOutlet AvatarMenuItemController* viewController;
@end @end
......
...@@ -27,6 +27,12 @@ ...@@ -27,6 +27,12 @@
@interface AvatarMenuBubbleController (Private) @interface AvatarMenuBubbleController (Private)
- (AvatarMenuModel*)model; - (AvatarMenuModel*)model;
- (NSButton*)configureNewUserButton:(CGFloat)yOffset; - (NSButton*)configureNewUserButton:(CGFloat)yOffset;
- (void)keyDown:(NSEvent*)theEvent;
- (void)moveDown:(id)sender;
- (void)moveUp:(id)sender;
- (void)insertNewline:(id)sender;
- (void)highlightNextItemByDelta:(NSInteger)delta;
- (void)highlightItem:(AvatarMenuItemController*)newItem;
@end @end
namespace AvatarMenuInternal { namespace AvatarMenuInternal {
...@@ -227,6 +233,72 @@ const CGFloat kLabelInset = 49.0; ...@@ -227,6 +233,72 @@ const CGFloat kLabelInset = 49.0;
return items_.get(); return items_.get();
} }
- (void)keyDown:(NSEvent*)theEvent {
[self interpretKeyEvents:[NSArray arrayWithObject:theEvent]];
}
- (void)moveDown:(id)sender {
[self highlightNextItemByDelta:-1];
}
- (void)moveUp:(id)sender {
[self highlightNextItemByDelta:1];
}
- (void)insertNewline:(id)sender {
for (AvatarMenuItemController* item in items_.get()) {
if ([item isHighlighted]) {
[self switchToProfile:item];
return;
}
}
}
- (void)highlightNextItemByDelta:(NSInteger)delta {
NSUInteger count = [items_ count];
if (count == 0)
return;
NSInteger old_index = -1;
for (NSUInteger i = 0; i < count; ++i) {
if ([[items_ objectAtIndex:i] isHighlighted]) {
old_index = i;
break;
}
}
NSInteger new_index;
// If nothing is selected then start at the top if we're going down and start
// at the bottom if we're going up.
if (old_index == -1)
new_index = delta < 0 ? (count - 1) : 0;
else
new_index = old_index + delta;
// Cap the index. We don't wrap around to match the behavior of Mac menus.
new_index =
std::min(std::max(0, new_index), static_cast<NSInteger>(count - 1));
[self highlightItem:[items_ objectAtIndex:new_index]];
}
- (void)highlightItem:(AvatarMenuItemController*)newItem {
AvatarMenuItemController* oldItem = nil;
for (AvatarMenuItemController* item in items_.get()) {
if ([item isHighlighted]) {
oldItem = item;
break;
}
}
if (oldItem == newItem)
return;
[oldItem setIsHighlighted:NO];
[newItem setIsHighlighted:YES];
}
@end @end
// Menu Item Controller //////////////////////////////////////////////////////// // Menu Item Controller ////////////////////////////////////////////////////////
...@@ -238,6 +310,7 @@ const CGFloat kLabelInset = 49.0; ...@@ -238,6 +310,7 @@ const CGFloat kLabelInset = 49.0;
@implementation AvatarMenuItemController @implementation AvatarMenuItemController
@synthesize modelIndex = modelIndex_; @synthesize modelIndex = modelIndex_;
@synthesize isHighlighted = isHighlighted_;
@synthesize iconView = iconView_; @synthesize iconView = iconView_;
@synthesize activeView = activeView_; @synthesize activeView = activeView_;
@synthesize nameField = nameField_; @synthesize nameField = nameField_;
...@@ -274,16 +347,13 @@ const CGFloat kLabelInset = 49.0; ...@@ -274,16 +347,13 @@ const CGFloat kLabelInset = 49.0;
} }
- (void)highlightForEventType:(NSEventType)type { - (void)highlightForEventType:(NSEventType)type {
BOOL active = !self.activeView.isHidden;
switch (type) { switch (type) {
case NSMouseEntered: case NSMouseEntered:
if (active) [controller_ highlightItem:self];
[self animateFromView:self.emailField toView:self.editButton];
break; break;
case NSMouseExited: case NSMouseExited:
if (active) [controller_ highlightItem:nil];
[self animateFromView:self.editButton toView:self.emailField];
break; break;
default: default:
...@@ -291,6 +361,21 @@ const CGFloat kLabelInset = 49.0; ...@@ -291,6 +361,21 @@ const CGFloat kLabelInset = 49.0;
}; };
} }
- (void)setIsHighlighted:(BOOL)isHighlighted {
if (isHighlighted_ == isHighlighted)
return;
isHighlighted_ = isHighlighted;
[[self view] setNeedsDisplay:YES];
if (!self.activeView.isHidden) {
if (isHighlighted_)
[self animateFromView:self.emailField toView:self.editButton];
else
[self animateFromView:self.editButton toView:self.emailField];
}
}
- (void)animateFromView:(NSView*)outView toView:(NSView*)inView { - (void)animateFromView:(NSView*)outView toView:(NSView*)inView {
const NSTimeInterval kAnimationDuration = 0.175; const NSTimeInterval kAnimationDuration = 0.175;
...@@ -345,13 +430,11 @@ const CGFloat kLabelInset = 49.0; ...@@ -345,13 +430,11 @@ const CGFloat kLabelInset = 49.0;
- (void)mouseEntered:(id)sender { - (void)mouseEntered:(id)sender {
[viewController_ highlightForEventType:[[NSApp currentEvent] type]]; [viewController_ highlightForEventType:[[NSApp currentEvent] type]];
mouseInside_ = YES;
[self setNeedsDisplay:YES]; [self setNeedsDisplay:YES];
} }
- (void)mouseExited:(id)sender { - (void)mouseExited:(id)sender {
[viewController_ highlightForEventType:[[NSApp currentEvent] type]]; [viewController_ highlightForEventType:[[NSApp currentEvent] type]];
mouseInside_ = NO;
[self setNeedsDisplay:YES]; [self setNeedsDisplay:YES];
} }
...@@ -361,7 +444,7 @@ const CGFloat kLabelInset = 49.0; ...@@ -361,7 +444,7 @@ const CGFloat kLabelInset = 49.0;
- (void)drawRect:(NSRect)dirtyRect { - (void)drawRect:(NSRect)dirtyRect {
NSColor* backgroundColor = nil; NSColor* backgroundColor = nil;
if (mouseInside_) { if ([viewController_ isHighlighted]) {
backgroundColor = [NSColor colorWithCalibratedRed:223.0/255 backgroundColor = [NSColor colorWithCalibratedRed:223.0/255
green:238.0/255 green:238.0/255
blue:246.0/255 blue:246.0/255
......
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
#include "chrome/test/base/testing_browser_process.h" #include "chrome/test/base/testing_browser_process.h"
#include "chrome/test/base/testing_profile_manager.h" #include "chrome/test/base/testing_profile_manager.h"
#include "testing/gtest_mac.h" #include "testing/gtest_mac.h"
#include "ui/base/test/cocoa_test_event_utils.h"
class FakeBridge : public AvatarMenuModelObserver { class FakeBridge : public AvatarMenuModelObserver {
public: public:
...@@ -51,6 +52,14 @@ class AvatarMenuBubbleControllerTest : public CocoaTest { ...@@ -51,6 +52,14 @@ class AvatarMenuBubbleControllerTest : public CocoaTest {
AvatarMenuModel* model() { return model_; } AvatarMenuModel* model() { return model_; }
FakeBridge* bridge() { return bridge_; } FakeBridge* bridge() { return bridge_; }
AvatarMenuItemController* GetHighlightedItem() {
for (AvatarMenuItemController* item in [controller() items]) {
if ([item isHighlighted])
return item;
}
return nil;
}
private: private:
TestingProfileManager manager_; TestingProfileManager manager_;
...@@ -178,26 +187,62 @@ TEST_F(AvatarMenuBubbleControllerTest, HighlightForEventType) { ...@@ -178,26 +187,62 @@ TEST_F(AvatarMenuBubbleControllerTest, HighlightForEventType) {
NSView* emailField = [item emailField]; NSView* emailField = [item emailField];
// The edit link remains hidden. // The edit link remains hidden.
[item highlightForEventType:NSMouseEntered]; [item setIsHighlighted:YES];
EXPECT_TRUE(editButton.isHidden); EXPECT_TRUE(editButton.isHidden);
EXPECT_FALSE(emailField.isHidden); EXPECT_FALSE(emailField.isHidden);
[item highlightForEventType:NSMouseExited]; [item setIsHighlighted:NO];
EXPECT_TRUE(editButton.isHidden); EXPECT_TRUE(editButton.isHidden);
EXPECT_FALSE(emailField.isHidden); EXPECT_FALSE(emailField.isHidden);
// Make the item "active" and re-test. // Make the item "active" and re-test.
[[item activeView] setHidden:NO]; [[item activeView] setHidden:NO];
[item highlightForEventType:NSMouseEntered]; [item setIsHighlighted:YES];
[item runMessagePump]; [item runMessagePump];
EXPECT_FALSE(editButton.isHidden); EXPECT_FALSE(editButton.isHidden);
EXPECT_TRUE(emailField.isHidden); EXPECT_TRUE(emailField.isHidden);
[item highlightForEventType:NSMouseExited]; [item setIsHighlighted:NO];
[item runMessagePump]; [item runMessagePump];
EXPECT_TRUE(editButton.isHidden); EXPECT_TRUE(editButton.isHidden);
EXPECT_FALSE(emailField.isHidden); EXPECT_FALSE(emailField.isHidden);
} }
TEST_F(AvatarMenuBubbleControllerTest, DownArrow) {
EXPECT_NSEQ(nil, GetHighlightedItem());
NSEvent* event =
cocoa_test_event_utils::KeyEventWithCharacter(NSDownArrowFunctionKey);
// Going down with no item selected should start the selection at the first
// item.
[controller() keyDown:event];
EXPECT_EQ([[controller() items] objectAtIndex:1], GetHighlightedItem());
[controller() keyDown:event];
EXPECT_EQ([[controller() items] objectAtIndex:0], GetHighlightedItem());
// There are no more items now so going down should stay at the last item.
[controller() keyDown:event];
EXPECT_EQ([[controller() items] objectAtIndex:0], GetHighlightedItem());
}
TEST_F(AvatarMenuBubbleControllerTest, UpArrow) {
EXPECT_NSEQ(nil, GetHighlightedItem());
NSEvent* event =
cocoa_test_event_utils::KeyEventWithCharacter(NSUpArrowFunctionKey);
// Going up with no item selected should start the selection at the last
// item.
[controller() keyDown:event];
EXPECT_EQ([[controller() items] objectAtIndex:0], GetHighlightedItem());
[controller() keyDown:event];
EXPECT_EQ([[controller() items] objectAtIndex:1], GetHighlightedItem());
// There are no more items now so going up should stay at the first item.
[controller() keyDown:event];
EXPECT_EQ([[controller() items] objectAtIndex:1], GetHighlightedItem());
}
...@@ -43,6 +43,9 @@ NSEvent* LeftMouseDownAtPointInWindow(NSPoint point, NSWindow* window); ...@@ -43,6 +43,9 @@ NSEvent* LeftMouseDownAtPointInWindow(NSPoint point, NSWindow* window);
std::pair<NSEvent*, NSEvent*> MouseClickInView(NSView* view, std::pair<NSEvent*, NSEvent*> MouseClickInView(NSView* view,
NSUInteger clickCount); NSUInteger clickCount);
// Returns a key event with the given character.
NSEvent* KeyEventWithCharacter(unichar c);
} // namespace cocoa_test_event_utils } // namespace cocoa_test_event_utils
#endif // UI_BASE_TEST_COCOA_TEST_EVENT_UTILS_H_ #endif // UI_BASE_TEST_COCOA_TEST_EVENT_UTILS_H_
...@@ -83,4 +83,18 @@ std::pair<NSEvent*,NSEvent*> MouseClickInView(NSView* view, ...@@ -83,4 +83,18 @@ std::pair<NSEvent*,NSEvent*> MouseClickInView(NSView* view,
return std::make_pair(down, up); return std::make_pair(down, up);
} }
NSEvent* KeyEventWithCharacter(unichar c) {
NSString* chars = [NSString stringWithCharacters:&c length:1];
return [NSEvent keyEventWithType:NSKeyDown
location:NSZeroPoint
modifierFlags:0
timestamp:0.0
windowNumber:0
context:nil
characters:chars
charactersIgnoringModifiers:chars
isARepeat:NO
keyCode:0];
}
} // namespace cocoa_test_event_utils } // namespace cocoa_test_event_utils
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment