summaryrefslogtreecommitdiffstats
path: root/ui/app_list
diff options
context:
space:
mode:
authortapted@chromium.org <tapted@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2013-02-22 00:25:45 +0000
committertapted@chromium.org <tapted@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2013-02-22 00:25:45 +0000
commit78696c90f20f7029aeeb9900276a63ef3507d5fd (patch)
tree8dfb2e8471500a15dd8e83f30202aeb91d5a105a /ui/app_list
parentcf1f70530111e85c49fd861ad9d52dbe29ae4cc6 (diff)
downloadchromium_src-78696c90f20f7029aeeb9900276a63ef3507d5fd.zip
chromium_src-78696c90f20f7029aeeb9900276a63ef3507d5fd.tar.gz
chromium_src-78696c90f20f7029aeeb9900276a63ef3507d5fd.tar.bz2
Add an app list view delegate for the Cocoa app list, and add basic keyboard usage.
This change adds a concrete type for AppListControllerDelegate to allow the ui code to communicate back to the browser code. In order for the app list to be dismissed, a key handler is added to the first responder such that pressing escape will dismiss the app list via the delegate. The key handler also handles basic keyboard navigation and selection highlighting. BUG=138633 TEST=Covered by app_list_unitests, and browser_tests: AppListControllerBrowserTest.ShowAndDismiss Review URL: https://chromiumcodereview.appspot.com/12294007 git-svn-id: svn://svn.chromium.org/chrome/trunk/src@183928 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'ui/app_list')
-rw-r--r--ui/app_list/cocoa/app_list_window_controller.h4
-rw-r--r--ui/app_list/cocoa/app_list_window_controller.mm38
-rw-r--r--ui/app_list/cocoa/app_list_window_controller_unittest.mm20
-rw-r--r--ui/app_list/cocoa/apps_grid_controller.h2
-rw-r--r--ui/app_list/cocoa/apps_grid_controller.mm45
-rw-r--r--ui/app_list/cocoa/apps_grid_controller_unittest.mm111
-rw-r--r--ui/app_list/test/app_list_test_view_delegate.cc8
-rw-r--r--ui/app_list/test/app_list_test_view_delegate.h4
8 files changed, 198 insertions, 34 deletions
diff --git a/ui/app_list/cocoa/app_list_window_controller.h b/ui/app_list/cocoa/app_list_window_controller.h
index 654ebc9..84ea7e6 100644
--- a/ui/app_list/cocoa/app_list_window_controller.h
+++ b/ui/app_list/cocoa/app_list_window_controller.h
@@ -12,12 +12,14 @@
@class AppsGridController;
// Controller for the app list NSWindow.
-@interface AppListWindowController : NSWindowController {
+@interface AppListWindowController : NSWindowController<NSWindowDelegate> {
scoped_nsobject<AppsGridController> appsGridController_;
}
- (id)initWithGridController:(AppsGridController*)gridController;
+- (AppsGridController*)appsGridController;
+
@end
#endif // UI_APP_LIST_COCOA_APP_LIST_WINDOW_CONTROLLER_H_
diff --git a/ui/app_list/cocoa/app_list_window_controller.mm b/ui/app_list/cocoa/app_list_window_controller.mm
index aa49232..599025f 100644
--- a/ui/app_list/cocoa/app_list_window_controller.mm
+++ b/ui/app_list/cocoa/app_list_window_controller.mm
@@ -4,16 +4,30 @@
#import "ui/app_list/cocoa/app_list_window_controller.h"
+#include "ui/app_list/app_list_view_delegate.h"
#import "ui/app_list/cocoa/apps_grid_controller.h"
+@interface AppListWindow : NSWindow;
+@end
+
+@implementation AppListWindow
+
+// If we initialize a window with NSBorderlessWindowMask, it will not accept key
+// events (among other things) unless canBecomeKeyWindow is overridden.
+- (BOOL)canBecomeKeyWindow {
+ return YES;
+}
+
+@end
+
@implementation AppListWindowController;
- (id)initWithGridController:(AppsGridController*)gridController {
scoped_nsobject<NSWindow> controlledWindow(
- [[NSWindow alloc] initWithContentRect:[[gridController view] bounds]
- styleMask:NSBorderlessWindowMask
- backing:NSBackingStoreBuffered
- defer:NO]);
+ [[AppListWindow alloc] initWithContentRect:[[gridController view] bounds]
+ styleMask:NSBorderlessWindowMask
+ backing:NSBackingStoreBuffered
+ defer:NO]);
[controlledWindow setContentView:[gridController view]];
[controlledWindow setReleasedWhenClosed:NO];
[controlledWindow setBackgroundColor:[NSColor clearColor]];
@@ -22,8 +36,24 @@
if ((self = [super initWithWindow:controlledWindow])) {
appsGridController_.reset([gridController retain]);
+ [[self window] setDelegate:self];
+ [[self window] makeFirstResponder:[appsGridController_ collectionView]];
}
return self;
}
+- (AppsGridController*)appsGridController {
+ return appsGridController_;
+}
+
+- (void)doCommandBySelector:(SEL)command {
+ if (command == @selector(cancel:)) {
+ if ([appsGridController_ delegate])
+ [appsGridController_ delegate]->Dismiss();
+ } else if (command == @selector(insertNewline:) ||
+ command == @selector(insertLineBreak:)) {
+ [appsGridController_ activateSelection];
+ }
+}
+
@end
diff --git a/ui/app_list/cocoa/app_list_window_controller_unittest.mm b/ui/app_list/cocoa/app_list_window_controller_unittest.mm
index f882a8d..bb78cde 100644
--- a/ui/app_list/cocoa/app_list_window_controller_unittest.mm
+++ b/ui/app_list/cocoa/app_list_window_controller_unittest.mm
@@ -4,9 +4,10 @@
#import "base/memory/scoped_nsobject.h"
#import "testing/gtest_mac.h"
-#import "ui/app_list/app_list_view_delegate.h"
+#include "ui/app_list/app_list_view_delegate.h"
#import "ui/app_list/cocoa/apps_grid_controller.h"
#import "ui/app_list/cocoa/app_list_window_controller.h"
+#include "ui/app_list/test/app_list_test_view_delegate.h"
#import "ui/base/test/ui_cocoa_test_helper.h"
namespace {
@@ -20,13 +21,19 @@ class AppListWindowControllerTest : public ui::CocoaTest {
scoped_nsobject<AppListWindowController> controller_;
+ app_list::test::AppListTestViewDelegate* delegate() {
+ return static_cast<app_list::test::AppListTestViewDelegate*>(
+ [[controller_ appsGridController] delegate]);
+ }
+
private:
DISALLOW_COPY_AND_ASSIGN(AppListWindowControllerTest);
};
AppListWindowControllerTest::AppListWindowControllerTest() {
Init();
- scoped_ptr<app_list::AppListViewDelegate> delegate;
+ scoped_ptr<app_list::AppListViewDelegate> delegate(
+ new app_list::test::AppListTestViewDelegate);
scoped_nsobject<AppsGridController> content(
[[AppsGridController alloc] initWithViewDelegate:delegate.Pass()]);
controller_.reset(
@@ -52,3 +59,12 @@ TEST_F(AppListWindowControllerTest, ShowHideCloseRelease) {
EXPECT_FALSE([[controller_ window] isVisible]);
[[controller_ window] makeKeyAndOrderFront:nil];
}
+
+// Test that the key bound to cancel (usually Escape) asks the controller to
+// dismiss the window.
+TEST_F(AppListWindowControllerTest, DismissWithEscape) {
+ [[controller_ window] makeKeyAndOrderFront:nil];
+ EXPECT_EQ(0, delegate()->dismiss_count());
+ [[controller_ window] cancelOperation:controller_];
+ EXPECT_EQ(1, delegate()->dismiss_count());
+}
diff --git a/ui/app_list/cocoa/apps_grid_controller.h b/ui/app_list/cocoa/apps_grid_controller.h
index 5ec9ca7..0ba26ad 100644
--- a/ui/app_list/cocoa/apps_grid_controller.h
+++ b/ui/app_list/cocoa/apps_grid_controller.h
@@ -37,6 +37,8 @@ class AppsGridDelegateBridge;
- (void)setModel:(scoped_ptr<app_list::AppListModel>)model;
+- (void)activateSelection;
+
@end
#endif // UI_APP_LIST_COCOA_APPS_GRID_CONTROLLER_H_
diff --git a/ui/app_list/cocoa/apps_grid_controller.mm b/ui/app_list/cocoa/apps_grid_controller.mm
index e53f784..1a4d123 100644
--- a/ui/app_list/cocoa/apps_grid_controller.mm
+++ b/ui/app_list/cocoa/apps_grid_controller.mm
@@ -6,6 +6,8 @@
#include "base/mac/foundation_util.h"
#include "base/strings/sys_string_conversions.h"
+#include "skia/ext/skia_utils_mac.h"
+#include "ui/app_list/app_list_constants.h"
#include "ui/app_list/app_list_item_model.h"
#include "ui/app_list/app_list_model.h"
#include "ui/app_list/app_list_model_observer.h"
@@ -42,6 +44,27 @@ const CGFloat kPreferredTileHeight = 98;
- (void)onItemClicked:(id)sender;
+- (NSButton*)selectedButton;
+
+@end
+
+@interface AppsGridViewItem : NSCollectionViewItem;
+@end
+
+@implementation AppsGridViewItem
+
+- (void)setSelected:(BOOL)flag {
+ if (flag) {
+ [[base::mac::ObjCCastStrict<NSButton>([self view]) cell]
+ setBackgroundColor:[NSColor lightGrayColor]];
+ } else {
+ [[base::mac::ObjCCastStrict<NSButton>([self view]) cell]
+ setBackgroundColor:gfx::SkColorToCalibratedNSColor(
+ app_list::kContentsBackgroundColor)];
+ }
+ [super setSelected:flag];
+}
+
@end
namespace app_list {
@@ -116,6 +139,10 @@ class AppsGridDelegateBridge : public ui::ListModelObserver {
[self modelUpdated];
}
+- (void)activateSelection {
+ [[self selectedButton] performClick:self];
+}
+
- (void)loadAndSetView {
const CGFloat kViewWidth = kFixedColumns * kPreferredTileWidth +
2 * kLeftRightPadding;
@@ -127,10 +154,9 @@ class AppsGridDelegateBridge : public ui::ListModelObserver {
[prototypeButton setButtonType:NSMomentaryPushInButton];
[prototypeButton setTarget:self];
[prototypeButton setAction:@selector(onItemClicked:)];
- [prototypeButton setShowsBorderOnlyWhileMouseInside:YES];
+ [prototypeButton setBordered:NO];
- scoped_nsobject<NSCollectionViewItem> prototype(
- [[NSCollectionViewItem alloc] init]);
+ scoped_nsobject<AppsGridViewItem> prototype([[AppsGridViewItem alloc] init]);
[prototype setView:prototypeButton];
NSSize itemSize = NSMakeSize(kPreferredTileWidth, kPreferredTileHeight);
@@ -140,6 +166,7 @@ class AppsGridDelegateBridge : public ui::ListModelObserver {
[tmpCollectionView setMinItemSize:itemSize];
[tmpCollectionView setMaxItemSize:itemSize];
[tmpCollectionView setItemPrototype:prototype];
+ [tmpCollectionView setSelectable:YES];
NSRect scrollFrame = NSMakeRect(0, 0, kViewWidth, kViewHeight);
scoped_nsobject<NSScrollView> scrollView(
@@ -168,10 +195,20 @@ class AppsGridDelegateBridge : public ui::ListModelObserver {
count:model_->apps()->item_count()];
}
+- (NSButton*)selectedButton {
+ NSIndexSet* selection = [[self collectionView] selectionIndexes];
+ if ([selection count]) {
+ NSCollectionViewItem* item =
+ [[self collectionView] itemAtIndex:[selection firstIndex]];
+ return base::mac::ObjCCastStrict<NSButton>([item view]);
+ }
+ return nil;
+}
+
- (void)listItemsAdded:(size_t)start
count:(size_t)count {
for (size_t i = start; i < start + count; ++i)
- [items_ insertObject:[NSNull null] atIndex:i];
+ [items_ insertObject:[NSNumber numberWithInt:i] atIndex:i];
[[self collectionView] setContent:items_];
diff --git a/ui/app_list/cocoa/apps_grid_controller_unittest.mm b/ui/app_list/cocoa/apps_grid_controller_unittest.mm
index 4445184..d70b251 100644
--- a/ui/app_list/cocoa/apps_grid_controller_unittest.mm
+++ b/ui/app_list/cocoa/apps_grid_controller_unittest.mm
@@ -16,7 +16,9 @@ namespace {
class AppsGridControllerTest : public ui::CocoaTest {
public:
- AppsGridControllerTest() {}
+ AppsGridControllerTest() {
+ Init();
+ }
virtual void SetUp() OVERRIDE {
ui::CocoaTest::SetUp();
@@ -30,6 +32,8 @@ class AppsGridControllerTest : public ui::CocoaTest {
[apps_grid_controller_ setModel:model.Pass()];
[[test_window() contentView] addSubview:[apps_grid_controller_ view]];
+ [test_window() makePretendKeyWindowAndSetFirstResponder:
+ [apps_grid_controller_ collectionView]];
}
protected:
@@ -41,6 +45,44 @@ class AppsGridControllerTest : public ui::CocoaTest {
[NSApp postEvent:events.second atStart:NO];
}
+ // Send a key press to the first responder.
+ void SimulateKeyPress(unichar c) {
+ [test_window() keyDown:cocoa_test_event_utils::KeyEventWithCharacter(c)];
+ }
+
+ void DelayForCollectionView() {
+ message_loop_.PostDelayedTask(FROM_HERE, MessageLoop::QuitClosure(),
+ base::TimeDelta::FromMilliseconds(100));
+ message_loop_.Run();
+ }
+
+ void SinkEvents() {
+ message_loop_.PostTask(FROM_HERE, MessageLoop::QuitClosure());
+ message_loop_.Run();
+ }
+
+ NSView* GetItemViewAt(size_t index) {
+ if (index < [[[apps_grid_controller_ collectionView] content] count]) {
+ NSCollectionViewItem* item = [[apps_grid_controller_ collectionView]
+ itemAtIndex:index];
+ return [item view];
+ }
+
+ return nil;
+ }
+
+ NSView* GetSelectedView() {
+ NSIndexSet* selection =
+ [[apps_grid_controller_ collectionView] selectionIndexes];
+ if ([selection count]) {
+ NSCollectionViewItem* item = [[apps_grid_controller_ collectionView]
+ itemAtIndex:[selection firstIndex]];
+ return [item view];
+ }
+
+ return nil;
+ }
+
app_list::test::AppListTestViewDelegate* delegate() {
return static_cast<app_list::test::AppListTestViewDelegate*>(
[apps_grid_controller_ delegate]);
@@ -54,6 +96,8 @@ class AppsGridControllerTest : public ui::CocoaTest {
scoped_nsobject<AppsGridController> apps_grid_controller_;
private:
+ MessageLoopForUI message_loop_;
+
DISALLOW_COPY_AND_ASSIGN(AppsGridControllerTest);
};
@@ -69,35 +113,31 @@ TEST_F(AppsGridControllerTest, EmptyModelAndShow) {
}
// Test with a single item.
-// This test is disabled in builders until the delay to wait for the animations
-// can be removed, or some other solution is found.
+// This test is disabled in builders until the delay to wait for the collection
+// view to load subviews can be removed, or some other solution is found.
TEST_F(AppsGridControllerTest, DISABLED_SingleEntryModel) {
- const size_t kTotalItems = 1;
- MessageLoopForUI message_loop;
-
+ // We need to "wake up" the NSCollectionView, otherwise it does not
+ // immediately update its subviews later in this function.
+ // When this test is run by itself, it's enough just to send a keypress (and
+ // this delay is not needed).
+ DelayForCollectionView();
EXPECT_EQ(0u, [[[apps_grid_controller_ collectionView] content] count]);
- [NSAnimationContext beginGrouping];
- [[NSAnimationContext currentContext] setDuration:0.0];
- model()->PopulateApps(kTotalItems);
- [NSAnimationContext endGrouping];
-
- EXPECT_EQ(kTotalItems,
- [[[apps_grid_controller_ collectionView] content] count]);
-
- message_loop.PostDelayedTask(FROM_HERE,
- MessageLoop::QuitClosure(),
- base::TimeDelta::FromMilliseconds(100));
- message_loop.Run();
+ model()->PopulateApps(1);
+ SinkEvents();
+ EXPECT_FALSE([[apps_grid_controller_ collectionView] animations]);
+ EXPECT_EQ(1u, [[[apps_grid_controller_ collectionView] content] count]);
NSArray* subviews = [[apps_grid_controller_ collectionView] subviews];
- EXPECT_EQ(kTotalItems, [subviews count]);
+ EXPECT_EQ(1u, [subviews count]);
+
+ // Note that using GetItemViewAt(0) here also works, and returns non-nil even
+ // without the delay, but a "click" on it does not register without the delay.
NSView* subview = [subviews objectAtIndex:0];
// Launch the item.
SimulateClick(subview);
- message_loop.PostTask(FROM_HERE, MessageLoop::QuitClosure());
- message_loop.Run();
+ SinkEvents();
EXPECT_EQ(1, delegate()->activate_count());
EXPECT_EQ(std::string("Item 0"), delegate()->last_activated()->title());
}
@@ -118,3 +158,32 @@ TEST_F(AppsGridControllerTest, ReplaceModel) {
EXPECT_EQ(kNewItems,
[[[apps_grid_controller_ collectionView] content] count]);
}
+
+// Tests basic left-right keyboard navigation on the first page, later tests
+// will test keyboard navigation across pages and other corner cases.
+TEST_F(AppsGridControllerTest, DISABLED_FirstPageKeyboardNavigation) {
+ model()->PopulateApps(3);
+ SinkEvents();
+ EXPECT_EQ(3u, [[[apps_grid_controller_ collectionView] content] count]);
+
+ SimulateKeyPress(NSRightArrowFunctionKey);
+ SinkEvents();
+ EXPECT_EQ(GetSelectedView(), GetItemViewAt(0));
+
+ SimulateKeyPress(NSRightArrowFunctionKey);
+ SinkEvents();
+ EXPECT_EQ(GetSelectedView(), GetItemViewAt(1));
+
+ SimulateKeyPress(NSLeftArrowFunctionKey);
+ SinkEvents();
+ EXPECT_EQ(GetSelectedView(), GetItemViewAt(0));
+
+ // Go to the last item, and launch it.
+ SimulateKeyPress(NSRightArrowFunctionKey);
+ SimulateKeyPress(NSRightArrowFunctionKey);
+ [apps_grid_controller_ activateSelection];
+ SinkEvents();
+ EXPECT_EQ(GetSelectedView(), GetItemViewAt(2));
+ EXPECT_EQ(1, delegate()->activate_count());
+ EXPECT_EQ(std::string("Item 2"), delegate()->last_activated()->title());
+}
diff --git a/ui/app_list/test/app_list_test_view_delegate.cc b/ui/app_list/test/app_list_test_view_delegate.cc
index 9d8ae38..a63e91b 100644
--- a/ui/app_list/test/app_list_test_view_delegate.cc
+++ b/ui/app_list/test/app_list_test_view_delegate.cc
@@ -10,7 +10,9 @@ namespace app_list {
namespace test {
AppListTestViewDelegate::AppListTestViewDelegate()
- : activate_count_(0), last_activated_(NULL) {
+ : activate_count_(0),
+ dismiss_count_(0),
+ last_activated_(NULL) {
}
AppListTestViewDelegate::~AppListTestViewDelegate() {}
@@ -25,6 +27,10 @@ void AppListTestViewDelegate::ActivateAppListItem(AppListItemModel* item,
++activate_count_;
}
+void AppListTestViewDelegate::Dismiss() {
+ ++dismiss_count_;
+}
+
gfx::ImageSkia AppListTestViewDelegate::GetWindowIcon() {
return gfx::ImageSkia();
}
diff --git a/ui/app_list/test/app_list_test_view_delegate.h b/ui/app_list/test/app_list_test_view_delegate.h
index 72c174a..85215d0 100644
--- a/ui/app_list/test/app_list_test_view_delegate.h
+++ b/ui/app_list/test/app_list_test_view_delegate.h
@@ -18,6 +18,7 @@ class AppListTestViewDelegate : public AppListViewDelegate {
virtual ~AppListTestViewDelegate();
int activate_count() { return activate_count_; }
+ int dismiss_count() { return dismiss_count_; }
AppListItemModel* last_activated() { return last_activated_; }
// AppListViewDelegate overrides:
@@ -32,13 +33,14 @@ class AppListTestViewDelegate : public AppListViewDelegate {
virtual void InvokeSearchResultAction(const SearchResult& result,
int action_index,
int event_flags) OVERRIDE {}
- virtual void Dismiss() OVERRIDE {}
+ virtual void Dismiss() OVERRIDE;
virtual void ViewClosing() OVERRIDE {}
virtual void ViewActivationChanged(bool active) OVERRIDE {}
virtual gfx::ImageSkia GetWindowIcon() OVERRIDE;
private:
int activate_count_;
+ int dismiss_count_;
AppListItemModel* last_activated_;
};