// Copyright (c) 2012 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/views/controls/table/table_view.h" #include "base/strings/string_number_conversions.h" #include "base/strings/utf_string_conversions.h" #include "testing/gtest/include/gtest/gtest.h" #include "ui/views/controls/table/table_grouper.h" #include "ui/views/controls/table/table_header.h" #include "ui/views/controls/table/table_view_observer.h" // Put the tests in the views namespace to make it easier to declare them as // friend classes. namespace views { class TableViewTestHelper { public: explicit TableViewTestHelper(TableView* table) : table_(table) {} std::string GetPaintRegion(const gfx::Rect& bounds) { TableView::PaintRegion region(table_->GetPaintRegion(bounds)); return "rows=" + base::IntToString(region.min_row) + " " + base::IntToString(region.max_row) + " cols=" + base::IntToString(region.min_column) + " " + base::IntToString(region.max_column); } size_t visible_col_count() { return table_->visible_columns().size(); } TableHeader* header() { return table_->header_; } private: TableView* table_; DISALLOW_COPY_AND_ASSIGN(TableViewTestHelper); }; namespace { // TestTableModel2 ------------------------------------------------------------- // Trivial TableModel implementation that is backed by a vector of vectors. // Provides methods for adding/removing/changing the contents that notify the // observer appropriately. // // Initial contents are: // 0, 1 // 1, 1 // 2, 2 // 3, 0 class TestTableModel2 : public ui::TableModel { public: TestTableModel2(); // Adds a new row at index |row| with values |c1_value| and |c2_value|. void AddRow(int row, int c1_value, int c2_value); // Removes the row at index |row|. void RemoveRow(int row); // Changes the values of the row at |row|. void ChangeRow(int row, int c1_value, int c2_value); // ui::TableModel: virtual int RowCount() OVERRIDE; virtual string16 GetText(int row, int column_id) OVERRIDE; virtual void SetObserver(ui::TableModelObserver* observer) OVERRIDE; virtual int CompareValues(int row1, int row2, int column_id) OVERRIDE; private: ui::TableModelObserver* observer_; // The data. std::vector > rows_; DISALLOW_COPY_AND_ASSIGN(TestTableModel2); }; TestTableModel2::TestTableModel2() : observer_(NULL) { AddRow(0, 0, 1); AddRow(1, 1, 1); AddRow(2, 2, 2); AddRow(3, 3, 0); } void TestTableModel2::AddRow(int row, int c1_value, int c2_value) { DCHECK(row >= 0 && row <= static_cast(rows_.size())); std::vector new_row; new_row.push_back(c1_value); new_row.push_back(c2_value); rows_.insert(rows_.begin() + row, new_row); if (observer_) observer_->OnItemsAdded(row, 1); } void TestTableModel2::RemoveRow(int row) { DCHECK(row >= 0 && row <= static_cast(rows_.size())); rows_.erase(rows_.begin() + row); if (observer_) observer_->OnItemsRemoved(row, 1); } void TestTableModel2::ChangeRow(int row, int c1_value, int c2_value) { DCHECK(row >= 0 && row < static_cast(rows_.size())); rows_[row][0] = c1_value; rows_[row][1] = c2_value; if (observer_) observer_->OnItemsChanged(row, 1); } int TestTableModel2::RowCount() { return static_cast(rows_.size()); } string16 TestTableModel2::GetText(int row, int column_id) { return base::IntToString16(rows_[row][column_id]); } void TestTableModel2::SetObserver(ui::TableModelObserver* observer) { observer_ = observer; } int TestTableModel2::CompareValues(int row1, int row2, int column_id) { return rows_[row1][column_id] - rows_[row2][column_id]; } // Returns the view to model mapping as a string. std::string GetViewToModelAsString(TableView* table) { std::string result; for (int i = 0; i < table->RowCount(); ++i) { if (i != 0) result += " "; result += base::IntToString(table->ViewToModel(i)); } return result; } // Returns the model to view mapping as a string. std::string GetModelToViewAsString(TableView* table) { std::string result; for (int i = 0; i < table->RowCount(); ++i) { if (i != 0) result += " "; result += base::IntToString(table->ModelToView(i)); } return result; } class TestTableView : public TableView { public: TestTableView(ui::TableModel* model, const std::vector& columns) : TableView(model, columns, TEXT_ONLY, false) { } // View overrides: virtual bool HasFocus() const OVERRIDE { // Overriden so key processing works. return true; } private: DISALLOW_COPY_AND_ASSIGN(TestTableView); }; } // namespace class TableViewTest : public testing::Test { public: TableViewTest() : table_(NULL) {} virtual void SetUp() OVERRIDE { model_.reset(new TestTableModel2); std::vector columns(2); columns[0].title = ASCIIToUTF16("Title Column 0"); columns[0].sortable = true; columns[1].title = ASCIIToUTF16("Title Column 1"); columns[1].id = 1; columns[1].sortable = true; table_ = new TestTableView(model_.get(), columns); parent_.reset(table_->CreateParentIfNecessary()); parent_->SetBounds(0, 0, 10000, 10000); parent_->Layout(); helper_.reset(new TableViewTestHelper(table_)); } void ClickOnRow(int row, int flags) { const int y = row * table_->row_height(); const ui::MouseEvent pressed(ui::ET_MOUSE_PRESSED, gfx::Point(0, y), gfx::Point(0, y), ui::EF_LEFT_MOUSE_BUTTON | flags); table_->OnMousePressed(pressed); } // Returns the state of the selection model as a string. The format is: // 'active=X anchor=X selection=X X X...'. std::string SelectionStateAsString() const { const ui::ListSelectionModel& model(table_->selection_model()); std::string result = "active=" + base::IntToString(model.active()) + " anchor=" + base::IntToString(model.anchor()) + " selection="; const ui::ListSelectionModel::SelectedIndices& selection( model.selected_indices()); for (size_t i = 0; i < selection.size(); ++i) { if (i != 0) result += " "; result += base::IntToString(selection[i]); } return result; } void PressKey(ui::KeyboardCode code) { ui::KeyEvent event(ui::ET_KEY_PRESSED, code, 0, false); table_->OnKeyPressed(event); } protected: scoped_ptr model_; // Owned by |parent_|. TableView* table_; scoped_ptr helper_; private: scoped_ptr parent_; DISALLOW_COPY_AND_ASSIGN(TableViewTest); }; // Verifies GetPaintRegion. TEST_F(TableViewTest, GetPaintRegion) { // Two columns should be visible. EXPECT_EQ(2u, helper_->visible_col_count()); EXPECT_EQ("rows=0 4 cols=0 2", helper_->GetPaintRegion(table_->bounds())); EXPECT_EQ("rows=0 4 cols=0 1", helper_->GetPaintRegion(gfx::Rect(0, 0, 1, table_->height()))); } // Verifies SetColumnVisibility(). TEST_F(TableViewTest, ColumnVisibility) { // Two columns should be visible. EXPECT_EQ(2u, helper_->visible_col_count()); // Should do nothing (column already visible). table_->SetColumnVisibility(0, true); EXPECT_EQ(2u, helper_->visible_col_count()); // Hide the first column. table_->SetColumnVisibility(0, false); ASSERT_EQ(1u, helper_->visible_col_count()); EXPECT_EQ(1, table_->visible_columns()[0].column.id); EXPECT_EQ("rows=0 4 cols=0 1", helper_->GetPaintRegion(table_->bounds())); // Hide the second column. table_->SetColumnVisibility(1, false); EXPECT_EQ(0u, helper_->visible_col_count()); // Show the second column. table_->SetColumnVisibility(1, true); ASSERT_EQ(1u, helper_->visible_col_count()); EXPECT_EQ(1, table_->visible_columns()[0].column.id); EXPECT_EQ("rows=0 4 cols=0 1", helper_->GetPaintRegion(table_->bounds())); // Show the first column. table_->SetColumnVisibility(0, true); ASSERT_EQ(2u, helper_->visible_col_count()); EXPECT_EQ(1, table_->visible_columns()[0].column.id); EXPECT_EQ(0, table_->visible_columns()[1].column.id); EXPECT_EQ("rows=0 4 cols=0 2", helper_->GetPaintRegion(table_->bounds())); } // Verifies resizing a column works. TEST_F(TableViewTest, Resize) { const int x = table_->visible_columns()[0].width; EXPECT_NE(0, x); // Drag the mouse 1 pixel to the left. const ui::MouseEvent pressed(ui::ET_MOUSE_PRESSED, gfx::Point(x, 0), gfx::Point(x, 0), ui::EF_LEFT_MOUSE_BUTTON); helper_->header()->OnMousePressed(pressed); const ui::MouseEvent dragged(ui::ET_MOUSE_DRAGGED, gfx::Point(x - 1, 0), gfx::Point(x - 1, 0), ui::EF_LEFT_MOUSE_BUTTON); helper_->header()->OnMouseDragged(dragged); // This should shrink the first column and pull the second column in. EXPECT_EQ(x - 1, table_->visible_columns()[0].width); EXPECT_EQ(x - 1, table_->visible_columns()[1].x); } // Assertions for table sorting. TEST_F(TableViewTest, Sort) { // Toggle the sort order of the first column, shouldn't change anything. table_->ToggleSortOrder(0); ASSERT_EQ(1u, table_->sort_descriptors().size()); EXPECT_EQ(0, table_->sort_descriptors()[0].column_id); EXPECT_TRUE(table_->sort_descriptors()[0].ascending); EXPECT_EQ("0 1 2 3", GetViewToModelAsString(table_)); EXPECT_EQ("0 1 2 3", GetModelToViewAsString(table_)); // Invert the sort (first column descending). table_->ToggleSortOrder(0); ASSERT_EQ(1u, table_->sort_descriptors().size()); EXPECT_EQ(0, table_->sort_descriptors()[0].column_id); EXPECT_FALSE(table_->sort_descriptors()[0].ascending); EXPECT_EQ("3 2 1 0", GetViewToModelAsString(table_)); EXPECT_EQ("3 2 1 0", GetModelToViewAsString(table_)); // Change cell 0x3 to -1, meaning we have 0, 1, 2, -1 (in the first column). model_->ChangeRow(3, -1, 0); ASSERT_EQ(1u, table_->sort_descriptors().size()); EXPECT_EQ(0, table_->sort_descriptors()[0].column_id); EXPECT_FALSE(table_->sort_descriptors()[0].ascending); EXPECT_EQ("2 1 0 3", GetViewToModelAsString(table_)); EXPECT_EQ("2 1 0 3", GetModelToViewAsString(table_)); // Invert sort again (first column ascending). table_->ToggleSortOrder(0); ASSERT_EQ(1u, table_->sort_descriptors().size()); EXPECT_EQ(0, table_->sort_descriptors()[0].column_id); EXPECT_TRUE(table_->sort_descriptors()[0].ascending); EXPECT_EQ("3 0 1 2", GetViewToModelAsString(table_)); EXPECT_EQ("1 2 3 0", GetModelToViewAsString(table_)); // Add a row so that model has 0, 3, 1, 2, -1. model_->AddRow(1, 3, 4); ASSERT_EQ(1u, table_->sort_descriptors().size()); EXPECT_EQ(0, table_->sort_descriptors()[0].column_id); EXPECT_TRUE(table_->sort_descriptors()[0].ascending); EXPECT_EQ("4 0 2 3 1", GetViewToModelAsString(table_)); EXPECT_EQ("1 4 2 3 0", GetModelToViewAsString(table_)); // Delete the first row, ending up with 3, 1, 2, -1. model_->RemoveRow(0); ASSERT_EQ(1u, table_->sort_descriptors().size()); EXPECT_EQ(0, table_->sort_descriptors()[0].column_id); EXPECT_TRUE(table_->sort_descriptors()[0].ascending); EXPECT_EQ("3 1 2 0", GetViewToModelAsString(table_)); EXPECT_EQ("3 1 2 0", GetModelToViewAsString(table_)); } namespace { class TableGrouperImpl : public TableGrouper { public: TableGrouperImpl() {} void SetRanges(const std::vector& ranges) { ranges_ = ranges; } // TableGrouper overrides: virtual void GetGroupRange(int model_index, GroupRange* range) OVERRIDE { int offset = 0; size_t range_index = 0; for (; range_index < ranges_.size() && offset < model_index; ++range_index) offset += ranges_[range_index]; if (offset == model_index) { range->start = model_index; range->length = ranges_[range_index]; } else { range->start = offset - ranges_[range_index - 1]; range->length = ranges_[range_index - 1]; } } private: std::vector ranges_; DISALLOW_COPY_AND_ASSIGN(TableGrouperImpl); }; } // namespace // Assertions around grouping. TEST_F(TableViewTest, Grouping) { // Configure the grouper so that there are two groups: // A 0 // 1 // B 2 // 3 TableGrouperImpl grouper; std::vector ranges; ranges.push_back(2); ranges.push_back(2); grouper.SetRanges(ranges); table_->SetGrouper(&grouper); // Toggle the sort order of the first column, shouldn't change anything. table_->ToggleSortOrder(0); ASSERT_EQ(1u, table_->sort_descriptors().size()); EXPECT_EQ(0, table_->sort_descriptors()[0].column_id); EXPECT_TRUE(table_->sort_descriptors()[0].ascending); EXPECT_EQ("0 1 2 3", GetViewToModelAsString(table_)); EXPECT_EQ("0 1 2 3", GetModelToViewAsString(table_)); // Sort descending, resulting: // B 2 // 3 // A 0 // 1 table_->ToggleSortOrder(0); ASSERT_EQ(1u, table_->sort_descriptors().size()); EXPECT_EQ(0, table_->sort_descriptors()[0].column_id); EXPECT_FALSE(table_->sort_descriptors()[0].ascending); EXPECT_EQ("2 3 0 1", GetViewToModelAsString(table_)); EXPECT_EQ("2 3 0 1", GetModelToViewAsString(table_)); // Change the entry in the 4th row to -1. The model now becomes: // A 0 // 1 // B 2 // -1 // Since the first entry in the range didn't change the sort isn't impacted. model_->ChangeRow(3, -1, 0); ASSERT_EQ(1u, table_->sort_descriptors().size()); EXPECT_EQ(0, table_->sort_descriptors()[0].column_id); EXPECT_FALSE(table_->sort_descriptors()[0].ascending); EXPECT_EQ("2 3 0 1", GetViewToModelAsString(table_)); EXPECT_EQ("2 3 0 1", GetModelToViewAsString(table_)); // Change the entry in the 3rd row to -1. The model now becomes: // A 0 // 1 // B -1 // -1 model_->ChangeRow(2, -1, 0); ASSERT_EQ(1u, table_->sort_descriptors().size()); EXPECT_EQ(0, table_->sort_descriptors()[0].column_id); EXPECT_FALSE(table_->sort_descriptors()[0].ascending); EXPECT_EQ("0 1 2 3", GetViewToModelAsString(table_)); EXPECT_EQ("0 1 2 3", GetModelToViewAsString(table_)); // Toggle to ascending sort. table_->ToggleSortOrder(0); ASSERT_EQ(1u, table_->sort_descriptors().size()); EXPECT_EQ(0, table_->sort_descriptors()[0].column_id); EXPECT_TRUE(table_->sort_descriptors()[0].ascending); EXPECT_EQ("2 3 0 1", GetViewToModelAsString(table_)); EXPECT_EQ("2 3 0 1", GetModelToViewAsString(table_)); } namespace { class TableViewObserverImpl : public TableViewObserver { public: TableViewObserverImpl() : selection_changed_count_(0) {} int GetChangedCountAndClear() { const int count = selection_changed_count_; selection_changed_count_ = 0; return count; } // TableViewObserver overrides: virtual void OnSelectionChanged() OVERRIDE { selection_changed_count_++; } private: int selection_changed_count_; DISALLOW_COPY_AND_ASSIGN(TableViewObserverImpl); }; } // namespace // Assertions around changing the selection. TEST_F(TableViewTest, Selection) { TableViewObserverImpl observer; table_->SetObserver(&observer); // Initially no selection. EXPECT_EQ("active=-1 anchor=-1 selection=", SelectionStateAsString()); // Select the last row. table_->Select(3); EXPECT_EQ(1, observer.GetChangedCountAndClear()); EXPECT_EQ("active=3 anchor=3 selection=3", SelectionStateAsString()); // Change sort, shouldn't notify of change (toggle twice so that order // actually changes). table_->ToggleSortOrder(0); table_->ToggleSortOrder(0); EXPECT_EQ(0, observer.GetChangedCountAndClear()); EXPECT_EQ("active=3 anchor=3 selection=3", SelectionStateAsString()); // Remove the selected row, this should notify of a change and update the // selection. model_->RemoveRow(3); EXPECT_EQ(1, observer.GetChangedCountAndClear()); EXPECT_EQ("active=2 anchor=2 selection=2", SelectionStateAsString()); // Insert a row, since the selection in terms of the original model hasn't // changed the observer is not notified. model_->AddRow(0, 1, 2); EXPECT_EQ(0, observer.GetChangedCountAndClear()); EXPECT_EQ("active=3 anchor=3 selection=3", SelectionStateAsString()); table_->SetObserver(NULL); } // Verifies up/down correctly navigates through groups. TEST_F(TableViewTest, KeyUpDown) { // Configure the grouper so that there are three groups: // A 0 // 1 // B 5 // C 2 // 3 model_->AddRow(2, 5, 0); TableGrouperImpl grouper; std::vector ranges; ranges.push_back(2); ranges.push_back(1); ranges.push_back(2); grouper.SetRanges(ranges); table_->SetGrouper(&grouper); TableViewObserverImpl observer; table_->SetObserver(&observer); // Initially no selection. EXPECT_EQ("active=-1 anchor=-1 selection=", SelectionStateAsString()); PressKey(ui::VKEY_DOWN); EXPECT_EQ(1, observer.GetChangedCountAndClear()); EXPECT_EQ("active=0 anchor=0 selection=0 1", SelectionStateAsString()); PressKey(ui::VKEY_DOWN); EXPECT_EQ(1, observer.GetChangedCountAndClear()); EXPECT_EQ("active=1 anchor=1 selection=0 1", SelectionStateAsString()); PressKey(ui::VKEY_DOWN); EXPECT_EQ(1, observer.GetChangedCountAndClear()); EXPECT_EQ("active=2 anchor=2 selection=2", SelectionStateAsString()); PressKey(ui::VKEY_DOWN); EXPECT_EQ(1, observer.GetChangedCountAndClear()); EXPECT_EQ("active=3 anchor=3 selection=3 4", SelectionStateAsString()); PressKey(ui::VKEY_DOWN); EXPECT_EQ(1, observer.GetChangedCountAndClear()); EXPECT_EQ("active=4 anchor=4 selection=3 4", SelectionStateAsString()); PressKey(ui::VKEY_DOWN); EXPECT_EQ(0, observer.GetChangedCountAndClear()); EXPECT_EQ("active=4 anchor=4 selection=3 4", SelectionStateAsString()); PressKey(ui::VKEY_UP); EXPECT_EQ(1, observer.GetChangedCountAndClear()); EXPECT_EQ("active=3 anchor=3 selection=3 4", SelectionStateAsString()); PressKey(ui::VKEY_UP); EXPECT_EQ(1, observer.GetChangedCountAndClear()); EXPECT_EQ("active=2 anchor=2 selection=2", SelectionStateAsString()); PressKey(ui::VKEY_UP); EXPECT_EQ(1, observer.GetChangedCountAndClear()); EXPECT_EQ("active=1 anchor=1 selection=0 1", SelectionStateAsString()); PressKey(ui::VKEY_UP); EXPECT_EQ(1, observer.GetChangedCountAndClear()); EXPECT_EQ("active=0 anchor=0 selection=0 1", SelectionStateAsString()); PressKey(ui::VKEY_UP); EXPECT_EQ(0, observer.GetChangedCountAndClear()); EXPECT_EQ("active=0 anchor=0 selection=0 1", SelectionStateAsString()); // Sort the table descending by column 1, view now looks like: // B 5 model: 2 // C 2 3 // 3 4 // A 0 0 // 1 1 table_->ToggleSortOrder(0); table_->ToggleSortOrder(0); EXPECT_EQ("2 3 4 0 1", GetViewToModelAsString(table_)); table_->Select(-1); EXPECT_EQ("active=-1 anchor=-1 selection=", SelectionStateAsString()); observer.GetChangedCountAndClear(); // Up with nothing selected selects the first row. PressKey(ui::VKEY_UP); EXPECT_EQ(1, observer.GetChangedCountAndClear()); EXPECT_EQ("active=2 anchor=2 selection=2", SelectionStateAsString()); PressKey(ui::VKEY_DOWN); EXPECT_EQ(1, observer.GetChangedCountAndClear()); EXPECT_EQ("active=3 anchor=3 selection=3 4", SelectionStateAsString()); PressKey(ui::VKEY_DOWN); EXPECT_EQ(1, observer.GetChangedCountAndClear()); EXPECT_EQ("active=4 anchor=4 selection=3 4", SelectionStateAsString()); PressKey(ui::VKEY_DOWN); EXPECT_EQ(1, observer.GetChangedCountAndClear()); EXPECT_EQ("active=0 anchor=0 selection=0 1", SelectionStateAsString()); PressKey(ui::VKEY_DOWN); EXPECT_EQ(1, observer.GetChangedCountAndClear()); EXPECT_EQ("active=1 anchor=1 selection=0 1", SelectionStateAsString()); PressKey(ui::VKEY_DOWN); EXPECT_EQ(0, observer.GetChangedCountAndClear()); EXPECT_EQ("active=1 anchor=1 selection=0 1", SelectionStateAsString()); PressKey(ui::VKEY_UP); EXPECT_EQ(1, observer.GetChangedCountAndClear()); EXPECT_EQ("active=0 anchor=0 selection=0 1", SelectionStateAsString()); PressKey(ui::VKEY_UP); EXPECT_EQ(1, observer.GetChangedCountAndClear()); EXPECT_EQ("active=4 anchor=4 selection=3 4", SelectionStateAsString()); PressKey(ui::VKEY_UP); EXPECT_EQ(1, observer.GetChangedCountAndClear()); EXPECT_EQ("active=3 anchor=3 selection=3 4", SelectionStateAsString()); PressKey(ui::VKEY_UP); EXPECT_EQ(1, observer.GetChangedCountAndClear()); EXPECT_EQ("active=2 anchor=2 selection=2", SelectionStateAsString()); PressKey(ui::VKEY_UP); EXPECT_EQ(0, observer.GetChangedCountAndClear()); EXPECT_EQ("active=2 anchor=2 selection=2", SelectionStateAsString()); table_->SetObserver(NULL); } // Verifies home/end do the right thing. TEST_F(TableViewTest, HomeEnd) { // Configure the grouper so that there are three groups: // A 0 // 1 // B 5 // C 2 // 3 model_->AddRow(2, 5, 0); TableGrouperImpl grouper; std::vector ranges; ranges.push_back(2); ranges.push_back(1); ranges.push_back(2); grouper.SetRanges(ranges); table_->SetGrouper(&grouper); TableViewObserverImpl observer; table_->SetObserver(&observer); // Initially no selection. EXPECT_EQ("active=-1 anchor=-1 selection=", SelectionStateAsString()); PressKey(ui::VKEY_HOME); EXPECT_EQ(1, observer.GetChangedCountAndClear()); EXPECT_EQ("active=0 anchor=0 selection=0 1", SelectionStateAsString()); PressKey(ui::VKEY_END); EXPECT_EQ(1, observer.GetChangedCountAndClear()); EXPECT_EQ("active=4 anchor=4 selection=3 4", SelectionStateAsString()); table_->SetObserver(NULL); } // Verifies multiple selection gestures work (control-click, shift-click ...). TEST_F(TableViewTest, Multiselection) { // Configure the grouper so that there are three groups: // A 0 // 1 // B 5 // C 2 // 3 model_->AddRow(2, 5, 0); TableGrouperImpl grouper; std::vector ranges; ranges.push_back(2); ranges.push_back(1); ranges.push_back(2); grouper.SetRanges(ranges); table_->SetGrouper(&grouper); // Initially no selection. EXPECT_EQ("active=-1 anchor=-1 selection=", SelectionStateAsString()); TableViewObserverImpl observer; table_->SetObserver(&observer); // Click on the first row, should select it and the second row. ClickOnRow(0, 0); EXPECT_EQ(1, observer.GetChangedCountAndClear()); EXPECT_EQ("active=0 anchor=0 selection=0 1", SelectionStateAsString()); // Click on the last row, should select it and the row before it. ClickOnRow(4, 0); EXPECT_EQ(1, observer.GetChangedCountAndClear()); EXPECT_EQ("active=4 anchor=4 selection=3 4", SelectionStateAsString()); // Shift click on the third row, should extend selection to it. ClickOnRow(2, ui::EF_SHIFT_DOWN); EXPECT_EQ(1, observer.GetChangedCountAndClear()); EXPECT_EQ("active=2 anchor=4 selection=2 3 4", SelectionStateAsString()); // Control click on third row, should toggle it. ClickOnRow(2, ui::EF_CONTROL_DOWN); EXPECT_EQ(1, observer.GetChangedCountAndClear()); EXPECT_EQ("active=2 anchor=2 selection=3 4", SelectionStateAsString()); // Control-shift click on second row, should extend selection to it. ClickOnRow(1, ui::EF_CONTROL_DOWN | ui::EF_SHIFT_DOWN); EXPECT_EQ(1, observer.GetChangedCountAndClear()); EXPECT_EQ("active=1 anchor=2 selection=0 1 2 3 4", SelectionStateAsString()); // Click on last row again. ClickOnRow(4, 0); EXPECT_EQ(1, observer.GetChangedCountAndClear()); EXPECT_EQ("active=4 anchor=4 selection=3 4", SelectionStateAsString()); table_->SetObserver(NULL); } // Verifies multiple selection gestures work when sorted. TEST_F(TableViewTest, MultiselectionWithSort) { // Configure the grouper so that there are three groups: // A 0 // 1 // B 5 // C 2 // 3 model_->AddRow(2, 5, 0); TableGrouperImpl grouper; std::vector ranges; ranges.push_back(2); ranges.push_back(1); ranges.push_back(2); grouper.SetRanges(ranges); table_->SetGrouper(&grouper); // Sort the table descending by column 1, view now looks like: // B 5 model: 2 // C 2 3 // 3 4 // A 0 0 // 1 1 table_->ToggleSortOrder(0); table_->ToggleSortOrder(0); // Initially no selection. EXPECT_EQ("active=-1 anchor=-1 selection=", SelectionStateAsString()); TableViewObserverImpl observer; table_->SetObserver(&observer); // Click on the third row, should select it and the second row. ClickOnRow(2, 0); EXPECT_EQ(1, observer.GetChangedCountAndClear()); EXPECT_EQ("active=4 anchor=4 selection=3 4", SelectionStateAsString()); // Extend selection to first row. ClickOnRow(0, ui::EF_SHIFT_DOWN); EXPECT_EQ(1, observer.GetChangedCountAndClear()); EXPECT_EQ("active=2 anchor=4 selection=2 3 4", SelectionStateAsString()); table_->SetObserver(NULL); } } // namespace views