summaryrefslogtreecommitdiffstats
path: root/ui/app_list
diff options
context:
space:
mode:
authortapted@chromium.org <tapted@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2013-03-08 13:41:41 +0000
committertapted@chromium.org <tapted@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2013-03-08 13:41:41 +0000
commit9b69c61ce91158ad35916fb817083c70bf8047d5 (patch)
treec52622ed053dfe0f5ab19604af28115c61f6c6ba /ui/app_list
parent0f50dc6f200f8a6eb73e6a4972c39aa9cbc6fc1f (diff)
downloadchromium_src-9b69c61ce91158ad35916fb817083c70bf8047d5.zip
chromium_src-9b69c61ce91158ad35916fb817083c70bf8047d5.tar.gz
chromium_src-9b69c61ce91158ad35916fb817083c70bf8047d5.tar.bz2
OSX app list pagination and gesture scrolling.
This change adds pagination to the OSX app list by creating an additional layer of NSCollectionView. Each page has an NSCollectionView with up to 16 items, and the pages are stored in an enclosing NSCollectionView with a single row. This allows items to be laid out in clusters on each page to mimic the behavior on ChromeOS and Windows when scrolling between pages. Gesture scrolling is enabled, and it snaps to the nearest page when the gesture finishes. BUG=138633 TEST=Added tests: AppsGridControllerTest.{Pagination, TwoPageModel} Review URL: https://chromiumcodereview.appspot.com/12319090 git-svn-id: svn://svn.chromium.org/chrome/trunk/src@186943 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'ui/app_list')
-rw-r--r--ui/app_list/app_list.gyp2
-rw-r--r--ui/app_list/cocoa/app_list_window_controller.mm3
-rw-r--r--ui/app_list/cocoa/apps_grid_controller.h21
-rw-r--r--ui/app_list/cocoa/apps_grid_controller.mm239
-rw-r--r--ui/app_list/cocoa/apps_grid_controller_unittest.mm118
-rw-r--r--ui/app_list/cocoa/scroll_view_with_no_scrollbars.h30
-rw-r--r--ui/app_list/cocoa/scroll_view_with_no_scrollbars.mm80
7 files changed, 421 insertions, 72 deletions
diff --git a/ui/app_list/app_list.gyp b/ui/app_list/app_list.gyp
index 227a002..144b878 100644
--- a/ui/app_list/app_list.gyp
+++ b/ui/app_list/app_list.gyp
@@ -41,6 +41,8 @@
'cocoa/apps_grid_controller.mm',
'cocoa/apps_grid_view_item.h',
'cocoa/apps_grid_view_item.mm',
+ 'cocoa/scroll_view_with_no_scrollbars.h',
+ 'cocoa/scroll_view_with_no_scrollbars.mm',
'pagination_model.cc',
'pagination_model.h',
'pagination_model_observer.h',
diff --git a/ui/app_list/cocoa/app_list_window_controller.mm b/ui/app_list/cocoa/app_list_window_controller.mm
index 599025f..44f7f59 100644
--- a/ui/app_list/cocoa/app_list_window_controller.mm
+++ b/ui/app_list/cocoa/app_list_window_controller.mm
@@ -37,7 +37,8 @@
if ((self = [super initWithWindow:controlledWindow])) {
appsGridController_.reset([gridController retain]);
[[self window] setDelegate:self];
- [[self window] makeFirstResponder:[appsGridController_ collectionView]];
+ [[self window] makeFirstResponder:[appsGridController_
+ collectionViewAtPageIndex:0]];
}
return self;
}
diff --git a/ui/app_list/cocoa/apps_grid_controller.h b/ui/app_list/cocoa/apps_grid_controller.h
index 0ba26ad..3a5f052 100644
--- a/ui/app_list/cocoa/apps_grid_controller.h
+++ b/ui/app_list/cocoa/apps_grid_controller.h
@@ -9,6 +9,7 @@
#include "base/memory/scoped_nsobject.h"
#include "base/memory/scoped_ptr.h"
+#include "ui/app_list/cocoa/scroll_view_with_no_scrollbars.h"
namespace app_list {
class AppListModel;
@@ -16,20 +17,28 @@ class AppListViewDelegate;
class AppsGridDelegateBridge;
}
+@class AppsGridViewItem;
+
// Controls a grid of views, representing AppListModel::Apps sub models.
-@interface AppsGridController : NSViewController {
+@interface AppsGridController : NSViewController<GestureScrollDelegate> {
@private
scoped_ptr<app_list::AppListModel> model_;
scoped_ptr<app_list::AppListViewDelegate> delegate_;
scoped_ptr<app_list::AppsGridDelegateBridge> bridge_;
+ scoped_nsobject<NSMutableArray> pages_;
scoped_nsobject<NSMutableArray> items_;
+
+ // Whether we are currently animating a scroll to the nearest page.
+ BOOL animatingScroll_;
}
- (id)initWithViewDelegate:
(scoped_ptr<app_list::AppListViewDelegate>)appListViewDelegate;
-- (NSCollectionView*)collectionView;
+- (NSCollectionView*)collectionViewAtPageIndex:(size_t)pageIndex;
+
+- (NSButton*)viewAtItemIndex:(size_t)itemIndex;
- (app_list::AppListModel*)model;
@@ -37,8 +46,16 @@ class AppsGridDelegateBridge;
- (void)setModel:(scoped_ptr<app_list::AppListModel>)model;
+// Calls delegate_->ActivateAppListItem for the currently selected item by
+// simulating a click.
- (void)activateSelection;
+// Return the number of pages of icons in the grid.
+- (size_t)pageCount;
+
+// Scroll to a page in the grid view with an animation.
+- (void)scrollToPage:(size_t)pageIndex;
+
@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 41aea7f..55628ba 100644
--- a/ui/app_list/cocoa/apps_grid_controller.mm
+++ b/ui/app_list/cocoa/apps_grid_controller.mm
@@ -16,34 +16,65 @@ namespace {
// OSX app list has hardcoded rows and columns for now.
const int kFixedRows = 4;
const int kFixedColumns = 4;
+const int kItemsPerPage = kFixedRows * kFixedColumns;
// Padding space in pixels for fixed layout.
const CGFloat kLeftRightPadding = 20;
-const CGFloat kTopPadding = 1;
+const CGFloat kTopPadding = 16;
// Preferred tile size when showing in fixed layout.
const CGFloat kPreferredTileWidth = 88;
const CGFloat kPreferredTileHeight = 98;
+const CGFloat kViewWidth =
+ kFixedColumns * kPreferredTileWidth + 2 * kLeftRightPadding;
+const CGFloat kViewHeight = kFixedRows * kPreferredTileHeight;
+
} // namespace
@interface AppsGridController ()
+// Cancel a currently running scroll animation.
+- (void)cancelScrollAnimation;
+
+// Index of the page with the most content currently visible.
+- (size_t)nearestPageIndex;
+
+// Make an item prototype containing an app button for an NSCollectionView page.
+- (NSCollectionViewItem*)makeItemPrototype;
+
+// Make an empty NSCollectionView positioned horizontally for |pageIndex|.
+- (NSCollectionView*)makePageForIndex:(size_t)pageIndex;
+
+// Bootstrap the views this class controls.
- (void)loadAndSetView;
+// Action for buttons in the grid.
+- (void)onItemClicked:(id)sender;
+
+- (AppsGridViewItem*)itemAtPageIndex:(size_t)pageIndex
+ indexInPage:(size_t)indexInPage;
+
- (AppsGridViewItem*)itemAtIndex:(size_t)itemIndex;
// Update the model in full, and rebuild subviews.
- (void)modelUpdated;
+// Return the button selected in first page with a selection.
+- (NSButton*)selectedButton;
+
+// The scroll view holding the grid pages.
+- (NSScrollView*)gridScrollView;
+
+- (NSView*)pagesContainerView;
+
+// Create any new pages after updating |items_|.
+- (void)updatePages:(size_t)startItemIndex;
+
// Bridged method for ui::ListModelObserver.
- (void)listItemsAdded:(size_t)start
count:(size_t)count;
-- (void)onItemClicked:(id)sender;
-
-- (NSButton*)selectedButton;
-
@end
namespace app_list {
@@ -78,6 +109,7 @@ class AppsGridDelegateBridge : public ui::ListModelObserver {
scoped_ptr<app_list::AppListModel> model(new app_list::AppListModel);
delegate_.reset(appListViewDelegate.release());
bridge_.reset(new app_list::AppsGridDelegateBridge(self));
+ pages_.reset([[NSMutableArray alloc] init]);
items_.reset([[NSMutableArray alloc] init]);
if (delegate_)
delegate_->SetModel(model.get());
@@ -92,9 +124,13 @@ class AppsGridDelegateBridge : public ui::ListModelObserver {
[super dealloc];
}
-- (NSCollectionView*)collectionView {
- return base::mac::ObjCCastStrict<NSCollectionView>(
- [base::mac::ObjCCastStrict<NSScrollView>([self view]) documentView]);
+- (NSCollectionView*)collectionViewAtPageIndex:(size_t)pageIndex {
+ return [pages_ objectAtIndex:pageIndex];
+}
+
+- (NSButton*)viewAtItemIndex:(size_t)itemIndex {
+ return base::mac::ObjCCastStrict<NSButton>(
+ [[self itemAtIndex:itemIndex] view]);
}
- (app_list::AppListModel*)model {
@@ -127,11 +163,53 @@ class AppsGridDelegateBridge : public ui::ListModelObserver {
[[self selectedButton] performClick:self];
}
-- (void)loadAndSetView {
- const CGFloat kViewWidth = kFixedColumns * kPreferredTileWidth +
- 2 * kLeftRightPadding;
- const CGFloat kViewHeight = kFixedRows * kPreferredTileHeight + kTopPadding;
+- (size_t)pageCount {
+ return [pages_ count];
+}
+
+- (void)scrollToPage:(size_t)pageIndex {
+ NSClipView* clipView = [[self gridScrollView] contentView];
+ NSPoint newOrigin = [clipView bounds].origin;
+ // Scrolling outside of this range is edge elasticity, which animates
+ // automatically.
+ if ((pageIndex == 0 && (newOrigin.x <= 0)) ||
+ (pageIndex + 1 == [self pageCount] &&
+ newOrigin.x >= pageIndex * kViewWidth)) {
+ return;
+ }
+
+ newOrigin.x = pageIndex * kViewWidth;
+ [NSAnimationContext beginGrouping];
+ [[clipView animator] setBoundsOrigin:newOrigin];
+ [NSAnimationContext endGrouping];
+ animatingScroll_ = YES;
+}
+
+- (void)cancelScrollAnimation {
+ NSClipView* clipView = [[self gridScrollView] contentView];
+ [NSAnimationContext beginGrouping];
+ [[NSAnimationContext currentContext] setDuration:0];
+ [[clipView animator] setBoundsOrigin:[clipView bounds].origin];
+ [NSAnimationContext endGrouping];
+ animatingScroll_ = NO;
+}
+
+- (size_t)nearestPageIndex {
+ return lround(
+ NSMinX([[[self gridScrollView] contentView] bounds]) / kViewWidth);
+}
+
+- (void)userScrolling:(BOOL)isScrolling {
+ if (isScrolling) {
+ if (animatingScroll_)
+ [self cancelScrollAnimation];
+ } else {
+ [self scrollToPage:[self nearestPageIndex]];
+ }
+}
+
+- (NSCollectionViewItem*)makeItemPrototype {
scoped_nsobject<NSButton> prototypeButton(
[[NSButton alloc] initWithFrame:NSZeroRect]);
[prototypeButton setImagePosition:NSImageAbove];
@@ -140,26 +218,40 @@ class AppsGridDelegateBridge : public ui::ListModelObserver {
[prototypeButton setAction:@selector(onItemClicked:)];
[prototypeButton setBordered:NO];
- scoped_nsobject<AppsGridViewItem> prototype([[AppsGridViewItem alloc] init]);
- [prototype setView:prototypeButton];
+ scoped_nsobject<AppsGridViewItem> itemPrototype(
+ [[AppsGridViewItem alloc] init]);
+ [itemPrototype setView:prototypeButton];
+ return itemPrototype.autorelease();
+}
+- (NSCollectionView*)makePageForIndex:(size_t)pageIndex {
+ NSRect pageFrame = NSMakeRect(
+ kLeftRightPadding + kViewWidth * pageIndex, 0,
+ kViewWidth, kViewHeight);
NSSize itemSize = NSMakeSize(kPreferredTileWidth, kPreferredTileHeight);
- scoped_nsobject<NSCollectionView> tmpCollectionView(
- [[NSCollectionView alloc] init]);
- [tmpCollectionView setMaxNumberOfRows:kFixedRows];
- [tmpCollectionView setMinItemSize:itemSize];
- [tmpCollectionView setMaxItemSize:itemSize];
- [tmpCollectionView setItemPrototype:prototype];
- [tmpCollectionView setSelectable:YES];
-
- NSRect scrollFrame = NSMakeRect(0, 0, kViewWidth, kViewHeight);
- scoped_nsobject<NSScrollView> scrollView(
- [[NSScrollView alloc] initWithFrame:scrollFrame]);
+ scoped_nsobject<NSCollectionView> itemCollectionView(
+ [[NSCollectionView alloc] initWithFrame:pageFrame]);
+ [itemCollectionView setMaxNumberOfRows:kFixedRows];
+ [itemCollectionView setMinItemSize:itemSize];
+ [itemCollectionView setMaxItemSize:itemSize];
+ [itemCollectionView setSelectable:YES];
+ [itemCollectionView setFocusRingType:NSFocusRingTypeNone];
+ [itemCollectionView setItemPrototype:[self makeItemPrototype]];
+ return itemCollectionView.autorelease();
+}
+
+- (void)loadAndSetView {
+ scoped_nsobject<NSView> pagesContainer(
+ [[NSView alloc] initWithFrame:NSZeroRect]);
+
+ NSRect scrollFrame = NSMakeRect(0, 0, kViewWidth, kViewHeight + kTopPadding);
+ scoped_nsobject<ScrollViewWithNoScrollbars> scrollView(
+ [[ScrollViewWithNoScrollbars alloc] initWithFrame:scrollFrame]);
[scrollView setBorderType:NSNoBorder];
[scrollView setLineScroll:kViewWidth];
[scrollView setPageScroll:kViewWidth];
- [scrollView setScrollsDynamically:NO];
- [scrollView setDocumentView:tmpCollectionView];
+ [scrollView setDelegate:self];
+ [scrollView setDocumentView:pagesContainer];
[self setView:scrollView];
}
@@ -172,34 +264,103 @@ class AppsGridDelegateBridge : public ui::ListModelObserver {
}
}
-- (AppsGridViewItem*)itemAtIndex:(size_t)itemIndex {
+- (AppsGridViewItem*)itemAtPageIndex:(size_t)pageIndex
+ indexInPage:(size_t)indexInPage {
return base::mac::ObjCCastStrict<AppsGridViewItem>(
- [[self collectionView] itemAtIndex:itemIndex]);
+ [[self collectionViewAtPageIndex:pageIndex] itemAtIndex:indexInPage]);
+}
+
+- (AppsGridViewItem*)itemAtIndex:(size_t)itemIndex {
+ const size_t pageIndex = itemIndex / kItemsPerPage;
+ return [self itemAtPageIndex:pageIndex
+ indexInPage:itemIndex - pageIndex * kItemsPerPage];
}
- (void)modelUpdated {
[items_ removeAllObjects];
- [[self collectionView] setContent:items_];
- if (model_ && model_->apps()->item_count())
+ if (model_ && model_->apps()->item_count()) {
[self listItemsAdded:0
count:model_->apps()->item_count()];
+ } else {
+ [self updatePages:0];
+ }
}
- (NSButton*)selectedButton {
- NSIndexSet* selection = [[self collectionView] selectionIndexes];
- if ([selection count])
- return [[self itemAtIndex:[selection firstIndex]] button];
+ NSIndexSet* selection = nil;
+ size_t pageIndex = 0;
+ for (; pageIndex < [self pageCount]; ++pageIndex) {
+ selection = [[self collectionViewAtPageIndex:pageIndex] selectionIndexes];
+ if ([selection count] > 0)
+ break;
+ }
+
+ if (pageIndex == [self pageCount])
+ return nil;
+
+ return [[self itemAtPageIndex:pageIndex
+ indexInPage:[selection firstIndex]] button];
+}
+
+- (NSScrollView*)gridScrollView {
+ return base::mac::ObjCCastStrict<NSScrollView>([self view]);
+}
+
+- (NSView*)pagesContainerView {
+ return [[self gridScrollView] documentView];
+}
+
+- (void)updatePages:(size_t)startItemIndex {
+ // Note there is always at least one page.
+ size_t targetPages = 1;
+ if ([items_ count] != 0)
+ targetPages = ([items_ count] - 1) / kItemsPerPage + 1;
+
+ const size_t currentPages = [self pageCount];
+ // First see if the number of pages have changed.
+ if (targetPages != currentPages) {
+ if (targetPages < currentPages) {
+ // Pages need to be removed.
+ [pages_ removeObjectsInRange:NSMakeRange(targetPages,
+ currentPages - targetPages)];
+ } else {
+ // Pages need to be added.
+ for (size_t i = currentPages; i < targetPages; ++i)
+ [pages_ addObject:[self makePageForIndex:i]];
+ }
+
+ [[self pagesContainerView] setSubviews:pages_];
+ NSSize pagesSize = NSMakeSize(kViewWidth * targetPages, kViewHeight);
+ [[self pagesContainerView] setFrameSize:pagesSize];
+ }
- return nil;
+ const size_t startPage = startItemIndex / kItemsPerPage;
+ // All pages on or after |startPage| may need items added or removed.
+ for (size_t pageIndex = startPage; pageIndex < targetPages; ++pageIndex) {
+ size_t startIndex = pageIndex * kItemsPerPage;
+ size_t length = kItemsPerPage;
+ // Check if it's the last page, and it's not full.
+ if (startIndex + length > [items_ count])
+ length = [items_ count] - startIndex;
+
+ [[self collectionViewAtPageIndex:pageIndex]
+ setContent:[items_ subarrayWithRange:NSMakeRange(startIndex, length)]];
+ }
}
- (void)listItemsAdded:(size_t)start
count:(size_t)count {
- for (size_t i = start; i < start + count; ++i)
- [items_ insertObject:[NSNumber numberWithInt:i] atIndex:i];
-
- [[self collectionView] setContent:items_];
+ // NSCollectionView animates objects based on how the content array changes
+ // between calls to setContent. The pointer to the AppListItemModel gives a
+ // unique identifier to use, even though the pointer is never actually used
+ // for the object it points to.
+ for (size_t i = start; i < start + count; ++i) {
+ app_list::AppListItemModel* itemModel = model_->apps()->GetItemAt(i);
+ [items_ insertObject:[NSValue valueWithPointer:itemModel]
+ atIndex:i];
+ }
+ [self updatePages:start];
for (size_t i = start; i < start + count; ++i)
[[self itemAtIndex:i] setModel:model_->apps()->GetItemAt(i)];
}
diff --git a/ui/app_list/cocoa/apps_grid_controller_unittest.mm b/ui/app_list/cocoa/apps_grid_controller_unittest.mm
index 3c09fc5..76d1345 100644
--- a/ui/app_list/cocoa/apps_grid_controller_unittest.mm
+++ b/ui/app_list/cocoa/apps_grid_controller_unittest.mm
@@ -15,6 +15,8 @@
namespace {
+const size_t kItemsPerPage = 16;
+
class AppsGridControllerTest : public ui::CocoaTest {
public:
AppsGridControllerTest() {
@@ -34,7 +36,7 @@ class AppsGridControllerTest : public ui::CocoaTest {
[[test_window() contentView] addSubview:[apps_grid_controller_ view]];
[test_window() makePretendKeyWindowAndSetFirstResponder:
- [apps_grid_controller_ collectionView]];
+ [apps_grid_controller_ collectionViewAtPageIndex:0]];
}
virtual void TearDown() OVERRIDE {
@@ -56,6 +58,14 @@ class AppsGridControllerTest : public ui::CocoaTest {
[test_window() keyDown:cocoa_test_event_utils::KeyEventWithCharacter(c)];
}
+ // Do a bulk replacement of the items in the grid.
+ void ReplaceTestModel(int item_count) {
+ scoped_ptr<app_list::test::AppListTestModel> new_model(
+ new app_list::test::AppListTestModel);
+ new_model->PopulateApps(item_count);
+ [apps_grid_controller_ setModel:new_model.PassAs<app_list::AppListModel>()];
+ }
+
void DelayForCollectionView() {
message_loop_.PostDelayedTask(FROM_HERE, MessageLoop::QuitClosure(),
base::TimeDelta::FromMilliseconds(100));
@@ -67,22 +77,21 @@ class AppsGridControllerTest : public ui::CocoaTest {
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];
- }
+ NSButton* GetItemViewAt(size_t index) {
+ return [apps_grid_controller_ viewAtItemIndex:index];
+ }
- return nil;
+ NSCollectionView* GetPageAt(size_t index) {
+ return [apps_grid_controller_ collectionViewAtPageIndex:index];
}
+ // TODO(tapted): Update this to work for selections on other than the first
+ // page.
NSView* GetSelectedView() {
- NSIndexSet* selection =
- [[apps_grid_controller_ collectionView] selectionIndexes];
+ NSIndexSet* selection = [GetPageAt(0) selectionIndexes];
if ([selection count]) {
- NSCollectionViewItem* item = [[apps_grid_controller_ collectionView]
- itemAtIndex:[selection firstIndex]];
+ NSCollectionViewItem* item =
+ [GetPageAt(0) itemAtIndex:[selection firstIndex]];
return [item view];
}
@@ -114,8 +123,12 @@ TEST_VIEW(AppsGridControllerTest, [apps_grid_controller_ view]);
// Test showing with an empty model.
TEST_F(AppsGridControllerTest, EmptyModelAndShow) {
EXPECT_TRUE([[apps_grid_controller_ view] superview]);
- EXPECT_TRUE([[apps_grid_controller_ collectionView] superview]);
- EXPECT_EQ(0u, [[[apps_grid_controller_ collectionView] content] count]);
+
+ // First page should always exist, even if empty.
+ EXPECT_EQ(1u, [apps_grid_controller_ pageCount]);
+ EXPECT_EQ(0u, [[GetPageAt(0) content] count]);
+ EXPECT_TRUE([GetPageAt(0) superview]); // The pages container.
+ EXPECT_TRUE([[GetPageAt(0) superview] superview]);
}
// Test with a single item.
@@ -127,14 +140,15 @@ TEST_F(AppsGridControllerTest, DISABLED_SingleEntryModel) {
// 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]);
+ EXPECT_EQ(1u, [apps_grid_controller_ pageCount]);
+ EXPECT_EQ(0u, [[GetPageAt(0) content] count]);
model()->PopulateApps(1);
SinkEvents();
- EXPECT_FALSE([[apps_grid_controller_ collectionView] animations]);
+ EXPECT_FALSE([GetPageAt(0) animations]);
- EXPECT_EQ(1u, [[[apps_grid_controller_ collectionView] content] count]);
- NSArray* subviews = [[apps_grid_controller_ collectionView] subviews];
+ EXPECT_EQ(1u, [[GetPageAt(0) content] count]);
+ NSArray* subviews = [GetPageAt(0) subviews];
EXPECT_EQ(1u, [subviews count]);
// Note that using GetItemViewAt(0) here also works, and returns non-nil even
@@ -148,21 +162,65 @@ TEST_F(AppsGridControllerTest, DISABLED_SingleEntryModel) {
EXPECT_EQ(std::string("Item 0"), delegate()->last_activated()->title());
}
+// Test activating an item on the second page (the 17th item).
+TEST_F(AppsGridControllerTest, DISABLED_TwoPageModel) {
+ DelayForCollectionView();
+ ReplaceTestModel(kItemsPerPage * 2);
+ [apps_grid_controller_ scrollToPage:1];
+
+ // The NSScrollView animator ignores the duration configured on the
+ // NSAnimationContext (set by CocoaTest::Init), so we need to delay here.
+ DelayForCollectionView();
+ NSArray* subviews = [GetPageAt(1) subviews];
+ NSView* subview = [subviews objectAtIndex:0];
+ // Launch the item.
+ SimulateClick(subview);
+ SinkEvents();
+ EXPECT_EQ(1, delegate()->activate_count());
+ EXPECT_EQ(std::string("Item 16"), delegate()->last_activated()->title());
+}
+
// Test setModel.
TEST_F(AppsGridControllerTest, ReplaceModel) {
const size_t kOrigItems = 1;
const size_t kNewItems = 2;
model()->PopulateApps(kOrigItems);
- EXPECT_EQ(kOrigItems,
- [[[apps_grid_controller_ collectionView] content] count]);
-
- scoped_ptr<app_list::test::AppListTestModel> new_model(
- new app_list::test::AppListTestModel);
- new_model->PopulateApps(kNewItems);
- [apps_grid_controller_ setModel:new_model.PassAs<app_list::AppListModel>()];
- EXPECT_EQ(kNewItems,
- [[[apps_grid_controller_ collectionView] content] count]);
+ EXPECT_EQ(kOrigItems, [[GetPageAt(0) content] count]);
+
+ ReplaceTestModel(kNewItems);
+ EXPECT_EQ(kNewItems, [[GetPageAt(0) content] count]);
+}
+
+// Test pagination.
+TEST_F(AppsGridControllerTest, Pagination) {
+ model()->PopulateApps(1);
+ EXPECT_EQ(1u, [apps_grid_controller_ pageCount]);
+ EXPECT_EQ(1u, [[GetPageAt(0) content] count]);
+
+ ReplaceTestModel(kItemsPerPage);
+ EXPECT_EQ(1u, [apps_grid_controller_ pageCount]);
+ EXPECT_EQ(kItemsPerPage, [[GetPageAt(0) content] count]);
+
+ // Test adding an item onto the next page.
+ model()->PopulateApps(1); // Now 17 items.
+ EXPECT_EQ(2u, [apps_grid_controller_ pageCount]);
+ EXPECT_EQ(kItemsPerPage, [[GetPageAt(0) content] count]);
+ EXPECT_EQ(1u, [[GetPageAt(1) content] count]);
+
+ // Test N pages with the last page having one empty spot.
+ const size_t kPagesToTest = 3;
+ ReplaceTestModel(kPagesToTest * kItemsPerPage - 1);
+ EXPECT_EQ(kPagesToTest, [apps_grid_controller_ pageCount]);
+ for (size_t page_index = 0; page_index < kPagesToTest - 1; ++page_index) {
+ EXPECT_EQ(kItemsPerPage, [[GetPageAt(page_index) content] count]);
+ }
+ EXPECT_EQ(kItemsPerPage - 1, [[GetPageAt(kPagesToTest - 1) content] count]);
+
+ // Test removing pages.
+ ReplaceTestModel(1);
+ EXPECT_EQ(1u, [apps_grid_controller_ pageCount]);
+ EXPECT_EQ(1u, [[GetPageAt(0) content] count]);
}
// Tests basic left-right keyboard navigation on the first page, later tests
@@ -170,7 +228,7 @@ TEST_F(AppsGridControllerTest, ReplaceModel) {
TEST_F(AppsGridControllerTest, DISABLED_FirstPageKeyboardNavigation) {
model()->PopulateApps(3);
SinkEvents();
- EXPECT_EQ(3u, [[[apps_grid_controller_ collectionView] content] count]);
+ EXPECT_EQ(3u, [[GetPageAt(0) content] count]);
SimulateKeyPress(NSRightArrowFunctionKey);
SinkEvents();
@@ -197,11 +255,11 @@ TEST_F(AppsGridControllerTest, DISABLED_FirstPageKeyboardNavigation) {
// Test runtime updates: adding items, changing titles and icons.
TEST_F(AppsGridControllerTest, ModelUpdates) {
model()->PopulateApps(2);
- EXPECT_EQ(2u, [[[apps_grid_controller_ collectionView] content] count]);
+ EXPECT_EQ(2u, [[GetPageAt(0) content] count]);
// Add an item (PopulateApps will create a duplicate "Item 0").
model()->PopulateApps(1);
- EXPECT_EQ(3u, [[[apps_grid_controller_ collectionView] content] count]);
+ EXPECT_EQ(3u, [[GetPageAt(0) content] count]);
NSButton* button = base::mac::ObjCCastStrict<NSButton>(GetItemViewAt(2));
EXPECT_NSEQ(@"Item 0", [button title]);
diff --git a/ui/app_list/cocoa/scroll_view_with_no_scrollbars.h b/ui/app_list/cocoa/scroll_view_with_no_scrollbars.h
new file mode 100644
index 0000000..0697e240
--- /dev/null
+++ b/ui/app_list/cocoa/scroll_view_with_no_scrollbars.h
@@ -0,0 +1,30 @@
+// Copyright 2013 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.
+
+#ifndef UI_APP_LIST_COCOA_SCROLL_VIEW_WITH_NO_SCROLLBARS_H_
+#define UI_APP_LIST_COCOA_SCROLL_VIEW_WITH_NO_SCROLLBARS_H_
+
+#include <Cocoa/Cocoa.h>
+
+// Delegate to notify when a user interaction to scroll completes.
+@protocol GestureScrollDelegate
+
+// Called when a scroll gesture is observed, or when it completes.
+- (void)userScrolling:(BOOL)isScrolling;
+
+@end
+
+// NSScrollView has a quirk when created programatically that causes gesture
+// scrolling to fail if it does not have a scroll bar. This provides a scroll
+// view using custom scrollers that are not visible.
+@interface ScrollViewWithNoScrollbars : NSScrollView {
+ @private
+ id<GestureScrollDelegate> delegate_;
+}
+
+@property(assign, nonatomic) id<GestureScrollDelegate> delegate;
+
+@end
+
+#endif // UI_APP_LIST_COCOA_SCROLL_VIEW_WITH_NO_SCROLLBARS_H_
diff --git a/ui/app_list/cocoa/scroll_view_with_no_scrollbars.mm b/ui/app_list/cocoa/scroll_view_with_no_scrollbars.mm
new file mode 100644
index 0000000..707f929
--- /dev/null
+++ b/ui/app_list/cocoa/scroll_view_with_no_scrollbars.mm
@@ -0,0 +1,80 @@
+// Copyright 2013 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.
+
+#include "ui/app_list/cocoa/scroll_view_with_no_scrollbars.h"
+
+#include "base/mac/mac_util.h"
+#include "base/memory/scoped_nsobject.h"
+
+#if !defined(MAC_OS_X_VERSION_10_7) || \
+ MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_7
+
+enum {
+ NSEventPhaseNone = 0,
+ NSEventPhaseBegan = 0x1 << 0,
+ NSEventPhaseStationary = 0x1 << 1,
+ NSEventPhaseChanged = 0x1 << 2,
+ NSEventPhaseEnded = 0x1 << 3,
+ NSEventPhaseCancelled = 0x1 << 4,
+};
+typedef NSUInteger NSEventPhase;
+
+@interface NSEvent (LionAPI)
+
+- (NSEventPhase)momentumPhase;
+- (NSEventPhase)phase;
+
+@end
+
+#endif // 10.7
+
+@interface InvisibleScroller : NSScroller;
+@end
+
+@implementation InvisibleScroller
+
+// Makes it non-interactive (and invisible) on Lion with both 10.6 and 10.7
+// SDKs. TODO(tapted): Find a way to make it non-interactive on Snow Leopard.
+// TODO(tapted): Find a way to make it take up no space on Lion with a 10.6 SDK.
+- (NSRect)rectForPart:(NSScrollerPart)aPart {
+ return NSZeroRect;
+}
+
+@end
+
+@implementation ScrollViewWithNoScrollbars
+
+@synthesize delegate = delegate_;
+
+- (id)initWithFrame:(NSRect)frame {
+ if ((self = [super initWithFrame:frame])) {
+ [self setHasHorizontalScroller:YES];
+ NSRect horizontalScrollerRect = [self bounds];
+ horizontalScrollerRect.size.height = 0;
+ scoped_nsobject<InvisibleScroller> horizontalScroller(
+ [[InvisibleScroller alloc] initWithFrame:horizontalScrollerRect]);
+ [self setHorizontalScroller:horizontalScroller];
+ }
+ return self;
+}
+
+- (void)endGestureWithEvent:(NSEvent*)event {
+ [super endGestureWithEvent:event];
+ if (!base::mac::IsOSLionOrLater())
+ [delegate_ userScrolling:NO];
+}
+
+- (void)scrollWheel:(NSEvent*)event {
+ [super scrollWheel:event];
+ if (![event respondsToSelector:@selector(momentumPhase)])
+ return;
+
+ BOOL scrollComplete = [event momentumPhase] == NSEventPhaseEnded ||
+ ([event momentumPhase] == NSEventPhaseNone &&
+ [event phase] == NSEventPhaseEnded);
+
+ [delegate_ userScrolling:!scrollComplete];
+}
+
+@end