diff options
-rw-r--r-- | chrome/SConscript.unit_tests | 1 | ||||
-rw-r--r-- | chrome/browser/views/keyword_editor_view.cc | 26 | ||||
-rw-r--r-- | chrome/test/unit/unittests.vcproj | 8 | ||||
-rw-r--r-- | chrome/views/group_table_view.cc | 36 | ||||
-rw-r--r-- | chrome/views/group_table_view.h | 24 | ||||
-rw-r--r-- | chrome/views/table_view.cc | 496 | ||||
-rw-r--r-- | chrome/views/table_view.h | 181 | ||||
-rw-r--r-- | chrome/views/table_view_unittest.cc | 363 | ||||
-rw-r--r-- | chrome/views/views.vcproj | 4 |
9 files changed, 978 insertions, 161 deletions
diff --git a/chrome/SConscript.unit_tests b/chrome/SConscript.unit_tests index afd6eaa..98d80e6 100644 --- a/chrome/SConscript.unit_tests +++ b/chrome/SConscript.unit_tests @@ -233,6 +233,7 @@ if env_test['PLATFORM'] == 'win32': 'renderer/spellcheck_unittest.cc', 'views/focus_manager_unittest.cc', 'views/grid_layout_unittest.cc', + 'views/table_view_unittest.cc', 'views/view_unittest.cc', 'test/test_notification_tracker.cc', 'test/testing_profile.cc', diff --git a/chrome/browser/views/keyword_editor_view.cc b/chrome/browser/views/keyword_editor_view.cc index a4e446d..9ef5527 100644 --- a/chrome/browser/views/keyword_editor_view.cc +++ b/chrome/browser/views/keyword_editor_view.cc @@ -471,12 +471,20 @@ void KeywordEditorView::Init() { columns.push_back( TableColumn(IDS_SEARCH_ENGINES_EDITOR_DESCRIPTION_COLUMN, TableColumn::LEFT, -1, .75)); + columns.back().sortable = true; columns.push_back( TableColumn(IDS_SEARCH_ENGINES_EDITOR_KEYWORD_COLUMN, TableColumn::LEFT, -1, .25)); + columns.back().sortable = true; table_view_ = new ChromeViews::TableView(table_model_.get(), columns, ChromeViews::ICON_AND_TEXT, false, true, true); table_view_->SetObserver(this); + // Make the table initially sorted by name. + ChromeViews::TableView::SortDescriptors sort; + sort.push_back( + ChromeViews::TableView::SortDescriptor( + IDS_SEARCH_ENGINES_EDITOR_DESCRIPTION_COLUMN, true)); + table_view_->SetSortDescriptors(sort); add_button_ = new ChromeViews::NativeButton( l10n_util::GetString(IDS_SEARCH_ENGINES_EDITOR_NEW_BUTTON)); @@ -586,20 +594,20 @@ void KeywordEditorView::ButtonPressed(ChromeViews::NativeButton* sender) { // Remove the observer while we modify the model, that way we don't need to // worry about the model calling us back when we mutate it. url_model_->RemoveObserver(this); - int last_row = -1; + int last_view_row = -1; for (ChromeViews::TableView::iterator i = table_view_->SelectionBegin(); i != table_view_->SelectionEnd(); ++i) { - last_row = *i; - const TemplateURL* template_url = &table_model_->GetTemplateURL(last_row); + last_view_row = table_view_->model_to_view(*i); + const TemplateURL* template_url = &table_model_->GetTemplateURL(*i); // Make sure to remove from the table model first, otherwise the // TemplateURL would be freed. - table_model_->Remove(last_row); + table_model_->Remove(*i); url_model_->Remove(template_url); } - if (last_row >= table_model_->RowCount()) - last_row = table_model_->RowCount() - 1; - if (last_row >= 0) - table_view_->Select(last_row); + if (last_view_row >= table_model_->RowCount()) + last_view_row = table_model_->RowCount() - 1; + if (last_view_row >= 0) + table_view_->Select(table_view_->view_to_model(last_view_row)); url_model_->AddObserver(this); // We may have removed the default provider. Enable the Suggest checkbox @@ -676,5 +684,5 @@ void KeywordEditorView::MakeDefaultSearchProvider(int index) { table_model_->MoveToMainGroup(index); // And select it. - table_view_->Select(new_index); + table_view_->Select(table_model_->IndexOfTemplateURL(keyword)); } diff --git a/chrome/test/unit/unittests.vcproj b/chrome/test/unit/unittests.vcproj index 68950ad..64e7d57 100644 --- a/chrome/test/unit/unittests.vcproj +++ b/chrome/test/unit/unittests.vcproj @@ -407,6 +407,14 @@ </File> </Filter> <Filter + Name="TestTableView" + > + <File + RelativePath="..\..\views\table_view_unittest.cc" + > + </File> + </Filter> + <Filter Name="TestAnimation" > <File diff --git a/chrome/views/group_table_view.cc b/chrome/views/group_table_view.cc index 5088997..4642770 100644 --- a/chrome/views/group_table_view.cc +++ b/chrome/views/group_table_view.cc @@ -124,7 +124,31 @@ void GroupTableView::OnKeyDown(unsigned short virtual_keycode) { } } -void GroupTableView::OnSelectedStateChanged(int item, bool is_selected) { +void GroupTableView::PrepareForSort() { + GroupRange range; + int row_count = RowCount(); + model_index_to_range_start_map_.clear(); + for (int model_row = 0; model_row < row_count;) { + model_->GetGroupRangeForItem(model_row, &range); + for (int range_counter = 0; range_counter < range.length; range_counter++) + model_index_to_range_start_map_[range_counter + model_row] = model_row; + model_row += range.length; + } +} + +int GroupTableView::CompareRows(int model_row1, int model_row2) { + int range1 = model_index_to_range_start_map_[model_row1]; + int range2 = model_index_to_range_start_map_[model_row2]; + if (range1 == range2) { + // The two rows are in the same group, sort so that items in the same group + // always appear in the same order. + return model_row1 - model_row2; + } + // Sort by the first entry of each of the groups. + return TableView::CompareRows(range1, range2); +} + +void GroupTableView::OnSelectedStateChanged(int model_row, bool is_selected) { // The goal is to make sure all items for a same group are in a consistent // state in term of selection. When a user clicks an item, several selection // messages are sent, possibly including unselecting all currently selected @@ -136,14 +160,14 @@ void GroupTableView::OnSelectedStateChanged(int item, bool is_selected) { sync_selection_factory_.NewRunnableMethod( &GroupTableView::SyncSelection)); } - TableView::OnSelectedStateChanged(item, is_selected); + TableView::OnSelectedStateChanged(model_row, is_selected); } // Draws the line separator betweens the groups. -void GroupTableView::PostPaint(int row, int column, bool selected, +void GroupTableView::PostPaint(int model_row, int column, bool selected, const CRect& bounds, HDC hdc) { GroupRange group_range; - model_->GetGroupRangeForItem(row, &group_range); + model_->GetGroupRangeForItem(model_row, &group_range); // We always paint a vertical line at the end of the last cell. HPEN hPen = CreatePen(PS_SOLID, kSeparatorLineThickness, kSeparatorLineColor); @@ -153,7 +177,7 @@ void GroupTableView::PostPaint(int row, int column, bool selected, LineTo(hdc, x, bounds.bottom); // We paint a separator line after the last item of a group. - if (row == (group_range.start + group_range.length - 1)) { + if (model_row == (group_range.start + group_range.length - 1)) { int y = static_cast<int>(bounds.bottom - kSeparatorLineThickness); MoveToEx(hdc, 0, y, NULL); LineTo(hdc, bounds.Width(), y); @@ -165,5 +189,5 @@ void GroupTableView::PostPaint(int row, int column, bool selected, std::string GroupTableView::GetClassName() const { return kViewClassName; } -} // Namespace +} // Namespace diff --git a/chrome/views/group_table_view.h b/chrome/views/group_table_view.h index 6772542..d534132 100644 --- a/chrome/views/group_table_view.h +++ b/chrome/views/group_table_view.h @@ -2,8 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -#ifndef CHROME_VIEWS_GROUP_TABLE_VIEW_H__ -#define CHROME_VIEWS_GROUP_TABLE_VIEW_H__ +#ifndef CHROME_VIEWS_GROUP_TABLE_VIEW_H_ +#define CHROME_VIEWS_GROUP_TABLE_VIEW_H_ #include "base/task.h" #include "chrome/views/table_view.h" @@ -43,11 +43,11 @@ class GroupTableView : public TableView { protected: // Notification from the ListView that the selected state of an item has // changed. - void OnSelectedStateChanged(int item, bool is_selected); + void OnSelectedStateChanged(int model_row, bool is_selected); // Extra-painting required to draw the separator line between groups. virtual bool ImplementPostPaint() { return true; } - virtual void PostPaint(int row, int column, bool selected, + virtual void PostPaint(int model_row, int column, bool selected, const CRect& bounds, HDC device_context); // In order to make keyboard navigation possible (using the Up and Down @@ -56,19 +56,27 @@ class GroupTableView : public TableView { // needs to be set on a group item when a group is selected. virtual void OnKeyDown(unsigned short virtual_keycode); + // Overriden to make sure rows in the same group stay grouped together. + virtual int CompareRows(int model_row1, int model_row2); + + // Updates model_index_to_range_start_map_ from the model. + virtual void PrepareForSort(); + private: // Make the selection of group consistent. void SyncSelection(); - GroupTableModel *model_; + GroupTableModel* model_; // A factory to make the selection consistent among groups. ScopedRunnableMethodFactory<GroupTableView> sync_selection_factory_; - DISALLOW_EVIL_CONSTRUCTORS(GroupTableView); + // Maps from model row to start of group. + std::map<int,int> model_index_to_range_start_map_; + + DISALLOW_COPY_AND_ASSIGN(GroupTableView); }; } // namespace ChromeViews -#endif // CHROME_VIEWS_GROUP_TABLE_VIEW_H__ - +#endif // CHROME_VIEWS_GROUP_TABLE_VIEW_H_ diff --git a/chrome/views/table_view.cc b/chrome/views/table_view.cc index ab4d717..e50cf04 100644 --- a/chrome/views/table_view.cc +++ b/chrome/views/table_view.cc @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +#include <algorithm> #include <windowsx.h> #include "chrome/views/table_view.h" @@ -11,12 +12,15 @@ #include "base/gfx/skia_utils.h" #include "chrome/common/gfx/chrome_canvas.h" #include "chrome/common/gfx/favicon_size.h" +#include "chrome/common/gfx/icon_util.h" #include "chrome/common/resource_bundle.h" #include "chrome/common/win_util.h" #include "chrome/views/hwnd_view.h" #include "chrome/views/view_container.h" #include "SkBitmap.h" #include "SkColorFilter.h" +#include "unicode/coll.h" +#include "unicode/uchar.h" namespace ChromeViews { @@ -25,10 +29,49 @@ const int kListViewTextPadding = 15; // Additional column width necessary if column has icons. const int kListViewIconWidthAndPadding = 18; +const int kImageSize = 18; + +// TableModel ----------------------------------------------------------------- + +// Used for sorting. +static Collator* collator = NULL; + SkBitmap TableModel::GetIcon(int row) { return SkBitmap(); } +int TableModel::CompareValues(int row1, int row2, int column_id) { + DCHECK(row1 >= 0 && row1 < RowCount() && + row2 >= 0 && row2 < RowCount()); + std::wstring value1 = GetText(row1, column_id); + std::wstring value2 = GetText(row2, column_id); + + if (!collator) { + UErrorCode create_status = U_ZERO_ERROR; + collator = Collator::createInstance(create_status); + if (!U_SUCCESS(create_status)) { + collator = NULL; + NOTREACHED(); + } + } + + if (collator) { + UErrorCode compare_status = U_ZERO_ERROR; + UCollationResult compare_result = collator->compare( + static_cast<const UChar*>(value1.c_str()), + static_cast<int>(value1.length()), + static_cast<const UChar*>(value2.c_str()), + static_cast<int>(value2.length()), + compare_status); + DCHECK(U_SUCCESS(compare_status)); + return compare_result; + } + NOTREACHED(); + return 0; +} + +// TableView ------------------------------------------------------------------ + TableView::TableView(TableModel* model, const std::vector<TableColumn>& columns, TableTypes table_type, @@ -40,7 +83,6 @@ TableView::TableView(TableModel* model, visible_columns_(), all_columns_(), column_count_(static_cast<int>(columns.size())), - cache_data_(true), table_type_(table_type), single_selection_(single_selection), ignore_listview_change_(false), @@ -77,6 +119,31 @@ void TableView::SetModel(TableModel* model) { OnModelChanged(); } +void TableView::SetSortDescriptors(const SortDescriptors& sort_descriptors) { + if (!sort_descriptors_.empty()) { + ResetColumnSortImage(sort_descriptors_[0].column_id, + NO_SORT); + } + sort_descriptors_ = sort_descriptors; + if (!sort_descriptors_.empty()) { + ResetColumnSortImage( + sort_descriptors_[0].column_id, + sort_descriptors_[0].ascending ? ASCENDING_SORT : DESCENDING_SORT); + } + if (!list_view_) + return; + + // For some reason we have to turn off/on redraw, otherwise the display + // isn't updated when done. + SendMessage(list_view_, WM_SETREDRAW, static_cast<WPARAM>(FALSE), 0); + + UpdateItemsLParams(0, 0); + + SortItemsAndUpdateMapping(); + + SendMessage(list_view_, WM_SETREDRAW, static_cast<WPARAM>(TRUE), 0); +} + void TableView::DidChangeBounds(const CRect& previous, const CRect& current) { if (!list_view_) @@ -103,11 +170,11 @@ int TableView::SelectedRowCount() { return ListView_GetSelectedCount(list_view_); } -void TableView::Select(int item) { +void TableView::Select(int model_row) { if (!list_view_) return; - DCHECK(item >= 0 && item < RowCount()); + DCHECK(model_row >= 0 && model_row < RowCount()); SendMessage(list_view_, WM_SETREDRAW, static_cast<WPARAM>(FALSE), 0); ignore_listview_change_ = true; @@ -115,41 +182,44 @@ void TableView::Select(int item) { ListView_SetItemState(list_view_, -1, 0, LVIS_SELECTED); // Select the specified item. - ListView_SetItemState(list_view_, item, LVIS_SELECTED | LVIS_FOCUSED, + int view_row = model_to_view(model_row); + ListView_SetItemState(list_view_, view_row, LVIS_SELECTED | LVIS_FOCUSED, LVIS_SELECTED | LVIS_FOCUSED); // Make it visible. - ListView_EnsureVisible(list_view_, item, FALSE); + ListView_EnsureVisible(list_view_, view_row, FALSE); ignore_listview_change_ = false; SendMessage(list_view_, WM_SETREDRAW, static_cast<WPARAM>(TRUE), 0); if (table_view_observer_) table_view_observer_->OnSelectionChanged(); } -void TableView::SetSelectedState(int item, bool state) { +void TableView::SetSelectedState(int model_row, bool state) { if (!list_view_) return; - DCHECK(item >= 0 && item < RowCount()); + DCHECK(model_row >= 0 && model_row < RowCount()); ignore_listview_change_ = true; // Select the specified item. - ListView_SetItemState(list_view_, item, LVIS_SELECTED, LVIS_SELECTED); + ListView_SetItemState(list_view_, model_to_view(model_row), + state ? LVIS_SELECTED : 0, LVIS_SELECTED); ignore_listview_change_ = false; } -void TableView::SetFocusOnItem(int item) { +void TableView::SetFocusOnItem(int model_row) { if (!list_view_) return; - DCHECK(item >= 0 && item < RowCount()); + DCHECK(model_row >= 0 && model_row < RowCount()); ignore_listview_change_ = true; // Set the focus to the given item. - ListView_SetItemState(list_view_, item, LVIS_FOCUSED, LVIS_FOCUSED); + ListView_SetItemState(list_view_, model_to_view(model_row), LVIS_FOCUSED, + LVIS_FOCUSED); ignore_listview_change_ = false; } @@ -158,30 +228,30 @@ int TableView::FirstSelectedRow() { if (!list_view_) return -1; - return ListView_GetNextItem(list_view_, -1, LVNI_ALL | LVIS_SELECTED); + int view_row = ListView_GetNextItem(list_view_, -1, LVNI_ALL | LVIS_SELECTED); + return view_row == -1 ? -1 : view_to_model(view_row); } - -bool TableView::IsItemSelected(int item) { +bool TableView::IsItemSelected(int model_row) { if (!list_view_) return false; - DCHECK(item >= 0 && item < RowCount()); - return (ListView_GetItemState(list_view_, item, LVIS_SELECTED) == - LVIS_SELECTED); + DCHECK(model_row >= 0 && model_row < RowCount()); + return (ListView_GetItemState(list_view_, model_to_view(model_row), + LVIS_SELECTED) == LVIS_SELECTED); } -bool TableView::ItemHasTheFocus(int item) { +bool TableView::ItemHasTheFocus(int model_row) { if (!list_view_) return false; - DCHECK(item >= 0 && item < RowCount()); - return (ListView_GetItemState(list_view_, item, LVIS_FOCUSED) == - LVIS_FOCUSED); + DCHECK(model_row >= 0 && model_row < RowCount()); + return (ListView_GetItemState(list_view_, model_to_view(model_row), + LVIS_FOCUSED) == LVIS_FOCUSED); } TableView::iterator TableView::SelectionBegin() { - return TableView::iterator(this, LastSelectedIndex()); + return TableView::iterator(this, LastSelectedViewIndex()); } TableView::iterator TableView::SelectionEnd() { @@ -194,11 +264,7 @@ void TableView::OnItemsChanged(int start, int length) { if (length == -1) { DCHECK(start >= 0); - if (cache_data_) { - length = model_->RowCount() - start; - } else { - length = RowCount() - start; - } + length = model_->RowCount() - start; } int row_count = RowCount(); DCHECK(start >= 0 && length > 0 && start + length <= row_count); @@ -214,7 +280,7 @@ void TableView::OnItemsChanged(int start, int length) { lv_item.mask = LVIF_IMAGE; for (int i = start; i < start + length; ++i) { // Retrieve the current icon index. - lv_item.iItem = i; + lv_item.iItem = model_to_view(i); BOOL r = ListView_GetItem(list_view_, &lv_item); DCHECK(r); // Set the current icon index to the other image. @@ -224,15 +290,10 @@ void TableView::OnItemsChanged(int start, int length) { DCHECK(r); } } - if (!cache_data_) { - ListView_RedrawItems(list_view_, start, start + length); - } else { - UpdateListViewCache(start, length, false); - } + UpdateListViewCache(start, length, false); SendMessage(list_view_, WM_SETREDRAW, static_cast<WPARAM>(TRUE), 0); } - void TableView::OnModelChanged() { if (!list_view_) return; @@ -250,12 +311,7 @@ void TableView::OnItemsAdded(int start, int length) { DCHECK(start >= 0 && length > 0 && start <= RowCount()); SendMessage(list_view_, WM_SETREDRAW, static_cast<WPARAM>(FALSE), 0); - if (!cache_data_) { - ListView_SetItemCount(list_view_, model_->RowCount()); - ListView_RedrawItems(list_view_, start, start + length); - } else { - UpdateListViewCache(start, length, true); - } + UpdateListViewCache(start, length, true); SendMessage(list_view_, WM_SETREDRAW, static_cast<WPARAM>(TRUE), 0); } @@ -263,23 +319,62 @@ void TableView::OnItemsRemoved(int start, int length) { if (!list_view_) return; - DCHECK(start >= 0 && length > 0 && start + length <= RowCount()); + if (start < 0 || length < 0 || start + length > RowCount()) { + NOTREACHED(); + return; + } + SendMessage(list_view_, WM_SETREDRAW, static_cast<WPARAM>(FALSE), 0); + bool had_selection = (SelectedRowCount() > 0); - if (!cache_data_) { - // TODO(sky): Make sure this triggers a repaint. - ListView_SetItemCount(list_view_, model_->RowCount()); + int old_row_count = RowCount(); + if (start == 0 && length == RowCount()) { + // Everything was removed. + ListView_DeleteAllItems(list_view_); + view_to_model_.reset(NULL); + model_to_view_.reset(NULL); } else { - // Update the cache. - if (start == 0 && length == RowCount()) { - ListView_DeleteAllItems(list_view_); + // Only a portion of the data was removed. + if (is_sorted()) { + int new_row_count = model_->RowCount(); + std::vector<int> view_items_to_remove; + view_items_to_remove.reserve(length); + // Iterate through the elements, updating the view_to_model_ mapping + // as well as collecting the rows that need to be deleted. + for (int i = 0, removed_count = 0; i < old_row_count; ++i) { + int model_index = view_to_model(i); + if (model_index >= start) { + if (model_index < start + length) { + // This item was removed. + view_items_to_remove.push_back(i); + model_index = -1; + } else { + model_index -= length; + } + } + if (model_index >= 0) { + view_to_model_[i - static_cast<int>(view_items_to_remove.size())] = + model_index; + } + } + + // Update the model_to_view mapping from the updated view_to_model + // mapping. + for (int i = 0; i < new_row_count; ++i) + model_to_view_[view_to_model_[i]] = i; + + // And finally delete the items. We do this backwards as the items were + // added ordered smallest to largest. + for (int i = length - 1; i >= 0; --i) + ListView_DeleteItem(list_view_, view_items_to_remove[i]); } else { - for (int i = 0; i < length; ++i) { + for (int i = 0; i < length; ++i) ListView_DeleteItem(list_view_, start); - } } } + SendMessage(list_view_, WM_SETREDRAW, static_cast<WPARAM>(TRUE), 0); + // We don't seem to get notification in this case. if (table_view_observer_ && had_selection && RowCount() == 0) table_view_observer_->OnSelectionChanged(); @@ -296,6 +391,16 @@ void TableView::SetColumns(const std::vector<TableColumn>& columns) { i != columns.end(); ++i) { AddColumn(*i); } + + // Remove any sort descriptors that are no longer valid. + SortDescriptors sort = sort_descriptors(); + for (SortDescriptors::iterator i = sort.begin(); i != sort.end();) { + if (all_columns_.count(i->column_id) == 0) + i = sort.erase(i); + else + ++i; + } + sort_descriptors_ = sort; } void TableView::OnColumnsChanged() { @@ -385,7 +490,7 @@ void TableView::SetCustomColorsEnabled(bool custom_colors_enabled) { custom_colors_enabled_ = custom_colors_enabled; } -bool TableView::GetCellColors(int row, +bool TableView::GetCellColors(int model_row, int column, ItemColor* foreground, ItemColor* background, @@ -444,8 +549,6 @@ HWND TableView::CreateNativeControl(HWND parent_container) { int style = WS_CHILD | LVS_REPORT | LVS_SHOWSELALWAYS; if (single_selection_) style |= LVS_SINGLESEL; - if (!cache_data_) - style |= LVS_OWNERDATA; // If there's only one column and the title string is empty, don't show a // header. if (all_columns_.size() == 1) { @@ -497,28 +600,22 @@ HWND TableView::CreateNativeControl(HWND parent_container) { } // Set the # of rows. - if (cache_data_) { - UpdateListViewCache(0, model_->RowCount(), true); - } else if (table_type_ == CHECK_BOX_AND_TEXT) { - ListView_SetItemCount(list_view_, model_->RowCount()); - ListView_SetCallbackMask(list_view_, LVIS_STATEIMAGEMASK); - } + UpdateListViewCache(0, model_->RowCount(), true); - // Load the default icon. if (table_type_ == ICON_AND_TEXT) { - HIMAGELIST image_list = ImageList_Create(18, 18, ILC_COLOR, 1, 1); - // TODO(jcampan): include a default icon image. - // This icon will not be found. The list view will still layout with the - // right space for the icon so we can paint our own icon. - HBITMAP image = LoadBitmap(NULL, L"IDR_WHATEVER"); - ImageList_Add(image_list, image, NULL); - DeleteObject(image); + HIMAGELIST image_list = + ImageList_Create(kImageSize, kImageSize, ILC_COLOR32, 2, 2); // We create 2 phony images because we are going to switch images at every // refresh in order to force a refresh of the icon area (somehow the clip // rect does not include the icon). - image = LoadBitmap(NULL, L"IDR_WHATEVER_AGAIN"); - ImageList_Add(image_list, image, NULL); - DeleteObject(image); + ChromeCanvas canvas(kImageSize, kImageSize, false); + // Make the background completely transparent. + canvas.drawColor(SK_ColorBLACK, SkPorterDuff::kClear_Mode); + HICON empty_icon = + IconUtil::CreateHICONFromSkBitmap(canvas.ExtractBitmap()); + ImageList_AddIcon(image_list, empty_icon); + ImageList_AddIcon(image_list, empty_icon); + DeleteObject(empty_icon); ListView_SetImageList(list_view_, image_list, LVSIL_SMALL); } @@ -548,6 +645,114 @@ HWND TableView::CreateNativeControl(HWND parent_container) { return list_view_; } +void TableView::ToggleSortOrder(int column_id) { + SortDescriptors sort = sort_descriptors(); + if (!sort.empty() && sort[0].column_id == column_id) { + sort[0].ascending = !sort[0].ascending; + } else { + SortDescriptor descriptor(column_id, true); + sort.insert(sort.begin(), descriptor); + if (sort.size() > 2) { + // Only persist two sort descriptors. + sort.resize(2); + } + } + SetSortDescriptors(sort); +} + +void TableView::UpdateItemsLParams(int start, int length) { + LVITEM item; + memset(&item, 0, sizeof(LVITEM)); + item.mask = LVIF_PARAM; + int row_count = RowCount(); + for (int i = 0; i < row_count; ++i) { + item.iItem = i; + int model_index = view_to_model(i); + if (length > 0 && model_index >= start) + model_index += length; + item.lParam = static_cast<LPARAM>(model_index); + ListView_SetItem(list_view_, &item); + } +} + +void TableView::SortItemsAndUpdateMapping() { + if (!is_sorted()) { + ListView_SortItems(list_view_, &TableView::NaturalSortFunc, this); + view_to_model_.reset(NULL); + model_to_view_.reset(NULL); + return; + } + + PrepareForSort(); + + // Sort the items. + ListView_SortItems(list_view_, &TableView::SortFunc, this); + + // Cleanup the collator. + if (collator) { + delete collator; + collator = NULL; + } + + // Update internal mapping to match how items were actually sorted. + int row_count = RowCount(); + model_to_view_.reset(new int[row_count]); + view_to_model_.reset(new int[row_count]); + LVITEM item; + memset(&item, 0, sizeof(LVITEM)); + item.mask = LVIF_PARAM; + for (int i = 0; i < row_count; ++i) { + item.iItem = i; + ListView_GetItem(list_view_, &item); + int model_index = static_cast<int>(item.lParam); + view_to_model_[i] = model_index; + model_to_view_[model_index] = i; + } +} + +// static +int CALLBACK TableView::SortFunc(LPARAM model_index_1_p, + LPARAM model_index_2_p, + LPARAM table_view_param) { + int model_index_1 = static_cast<int>(model_index_1_p); + int model_index_2 = static_cast<int>(model_index_2_p); + TableView* table_view = reinterpret_cast<TableView*>(table_view_param); + return table_view->CompareRows(model_index_1, model_index_2); +} + +// static +int CALLBACK TableView::NaturalSortFunc(LPARAM model_index_1_p, + LPARAM model_index_2_p, + LPARAM table_view_param) { + return model_index_1_p - model_index_2_p; +} + +void TableView::ResetColumnSortImage(int column_id, SortDirection direction) { + if (!list_view_ || column_id == -1) + return; + + std::vector<int>::const_iterator i = + std::find(visible_columns_.begin(), visible_columns_.end(), column_id); + if (i == visible_columns_.end()) + return; + + HWND header = ListView_GetHeader(list_view_); + if (!header) + return; + + int column_index = static_cast<int>(i - visible_columns_.begin()); + HDITEM header_item; + memset(&header_item, 0, sizeof(header_item)); + header_item.mask = HDI_FORMAT; + Header_GetItem(header, column_index, &header_item); + header_item.fmt &= ~(HDF_SORTUP | HDF_SORTDOWN); + if (direction == ASCENDING_SORT) + header_item.fmt |= HDF_SORTUP; + else if (direction == DESCENDING_SORT) + header_item.fmt |= HDF_SORTDOWN; + Header_SetItem(header, column_index, &header_item); +} + void TableView::InsertColumn(const TableColumn& tc, int index) { if (!list_view_) return; @@ -577,6 +782,11 @@ void TableView::InsertColumn(const TableColumn& tc, int index) { column.iSubItem = index + 1; SendMessage(list_view_, LVM_INSERTCOLUMN, index, reinterpret_cast<LPARAM>(&column)); + if (is_sorted() && sort_descriptors_[0].column_id == tc.id) { + ResetColumnSortImage( + tc.id, + sort_descriptors_[0].ascending ? ASCENDING_SORT : DESCENDING_SORT); + } } LRESULT TableView::OnNotify(int w_param, NMHDR* hdr) { @@ -585,6 +795,7 @@ LRESULT TableView::OnNotify(int w_param, NMHDR* hdr) { // Draw notification. dwDragState indicates the current stage of drawing. return OnCustomDraw(reinterpret_cast<NMLVCUSTOMDRAW*>(hdr)); } + case LVN_ITEMCHANGED: { // Notification that the state of an item has changed. The state // includes such things as whether the item is selected or checked. @@ -594,7 +805,8 @@ LRESULT TableView::OnNotify(int w_param, NMHDR* hdr) { (state_change->uNewState & LVIS_SELECTED)) { // Selected state of the item changed. bool is_selected = ((state_change->uNewState & LVIS_SELECTED) != 0); - OnSelectedStateChanged(state_change->iItem, is_selected); + OnSelectedStateChanged(view_to_model(state_change->iItem), + is_selected); } if ((state_change->uOldState & LVIS_STATEIMAGEMASK) != (state_change->uNewState & LVIS_STATEIMAGEMASK)) { @@ -602,17 +814,20 @@ LRESULT TableView::OnNotify(int w_param, NMHDR* hdr) { bool is_checked = ((state_change->uNewState & LVIS_STATEIMAGEMASK) == INDEXTOSTATEIMAGEMASK(2)); - OnCheckedStateChanged(state_change->iItem, is_checked); + OnCheckedStateChanged(view_to_model(state_change->iItem), + is_checked); } } break; } + case HDN_BEGINTRACKW: case HDN_BEGINTRACKA: // Prevent clicks so columns cannot be resized. if (!resizable_columns_) return TRUE; break; + case NM_DBLCLK: OnDoubleClick(); break; @@ -626,6 +841,14 @@ LRESULT TableView::OnNotify(int w_param, NMHDR* hdr) { break; } + case LVN_COLUMNCLICK: { + const TableColumn& column = GetColumnAtPosition( + reinterpret_cast<NMLISTVIEW*>(hdr)->iSubItem); + if (column.sortable) + ToggleSortOrder(column.id); + break; + } + default: break; } @@ -635,13 +858,44 @@ LRESULT TableView::OnNotify(int w_param, NMHDR* hdr) { void TableView::OnDestroy() { if (table_type_ == ICON_AND_TEXT) { HIMAGELIST image_list = - ListView_GetImageList(GetNativeControlHWND(), LVSIL_SMALL); + ListView_GetImageList(GetNativeControlHWND(), LVSIL_SMALL); DCHECK(image_list); if (image_list) ImageList_Destroy(image_list); } } +// Returns result, unless ascending is false in which case -result is returned. +static int SwapCompareResult(int result, bool ascending) { + return ascending ? result : -result; +} + +int TableView::CompareRows(int model_row1, int model_row2) { + if (model_->HasGroups()) { + // By default ListView sorts the elements regardless of groups. In such + // a situation the groups display only the items they contain. This results + // in the visual order differing from the item indices. I could not find + // a way to iterate over the visual order in this situation. As a workaround + // this forces the items to be sorted by groups as well, which means the + // visual order matches the item indices. + int g1 = model_->GetGroupID(model_row1); + int g2 = model_->GetGroupID(model_row2); + if (g1 != g2) + return g1 - g2; + } + int sort_result = model_->CompareValues( + model_row1, model_row2, sort_descriptors_[0].column_id); + if (sort_result == 0 && sort_descriptors_.size() > 1 && + sort_descriptors_[1].column_id != -1) { + // Try the secondary sort. + return SwapCompareResult( + model_->CompareValues(model_row1, model_row2, + sort_descriptors_[1].column_id), + sort_descriptors_[1].ascending); + } + return SwapCompareResult(sort_result, sort_descriptors_[0].ascending); +} + LRESULT TableView::OnCustomDraw(NMLVCUSTOMDRAW* draw_info) { switch (draw_info->nmcd.dwDrawStage) { case CDDS_PREPAINT: { @@ -668,7 +922,8 @@ LRESULT TableView::OnCustomDraw(NMLVCUSTOMDRAW* draw_info) { LOGFONT logfont; GetObject(GetWindowFont(list_view_), sizeof(logfont), &logfont); - if (GetCellColors(static_cast<int>(draw_info->nmcd.dwItemSpec), + if (GetCellColors(view_to_model( + static_cast<int>(draw_info->nmcd.dwItemSpec)), draw_info->iSubItem, &foreground, &background, @@ -693,19 +948,19 @@ LRESULT TableView::OnCustomDraw(NMLVCUSTOMDRAW* draw_info) { } case CDDS_ITEMPOSTPAINT: { DCHECK((table_type_ == ICON_AND_TEXT) || (ImplementPostPaint())); - int n_item = static_cast<int>(draw_info->nmcd.dwItemSpec); + int view_index = static_cast<int>(draw_info->nmcd.dwItemSpec); // We get notifications for empty items, just ignore them. - if (n_item >= model_->RowCount()) { + if (view_index >= model_->RowCount()) return CDRF_DODEFAULT; - } + int model_index = view_to_model(view_index); LRESULT r = CDRF_DODEFAULT; // First let's take care of painting the right icon. if (table_type_ == ICON_AND_TEXT) { - SkBitmap image = model_->GetIcon(n_item); + SkBitmap image = model_->GetIcon(model_index); if (!image.isNull()) { // Get the rect that holds the icon. CRect icon_rect, client_rect; - if (ListView_GetItemRect(list_view_, n_item, &icon_rect, + if (ListView_GetItemRect(list_view_, view_index, &icon_rect, LVIR_ICON) && GetClientRect(list_view_, &client_rect)) { CRect intersection; @@ -718,7 +973,7 @@ LRESULT TableView::OnCustomDraw(NMLVCUSTOMDRAW* draw_info) { // It seems the state in nmcd.uItemState is not correct. // We'll retrieve it explicitly. - int selected = ListView_GetItemState(list_view_, n_item, + int selected = ListView_GetItemState(list_view_, view_index, LVIS_SELECTED); int bg_color_index; if (!IsEnabled()) @@ -758,8 +1013,9 @@ LRESULT TableView::OnCustomDraw(NMLVCUSTOMDRAW* draw_info) { } if (ImplementPostPaint()) { CRect cell_rect; - if (ListView_GetItemRect(list_view_, n_item, &cell_rect, LVIR_BOUNDS)) { - PostPaint(n_item, 0, false, cell_rect, draw_info->nmcd.hdc); + if (ListView_GetItemRect(list_view_, view_index, &cell_rect, + LVIR_BOUNDS)) { + PostPaint(model_index, 0, false, cell_rect, draw_info->nmcd.hdc); r = CDRF_SKIPDEFAULT; } } @@ -847,21 +1103,29 @@ void TableView::GetPreferredSize(CSize* out) { *out = preferred_size_; } - void TableView::UpdateListViewCache0(int start, int length, bool add) { + if (is_sorted()) { + if (add) + UpdateItemsLParams(start, length); + else + UpdateItemsLParams(0, 0); + } + LVITEM item = {0}; int start_column = 0; int max_row = start + length; const bool has_groups = (win_util::GetWinVersion() > win_util::WINVERSION_2000 && model_->HasGroups()); - if (has_groups) - item.mask = LVIF_GROUPID; if (add) { + if (has_groups) + item.mask = LVIF_GROUPID; + item.mask |= LVIF_PARAM; for (int i = start; i < max_row; ++i) { item.iItem = i; if (has_groups) item.iGroupId = model_->GetGroupID(i); + item.lParam = i; ListView_InsertItem(list_view_, &item); } } @@ -878,14 +1142,12 @@ void TableView::UpdateListViewCache0(int start, int length, bool add) { item.stateMask = LVIS_STATEIMAGEMASK; for (int i = start; i < max_row; ++i) { std::wstring text = model_->GetText(i, visible_columns_[0]); - item.iItem = i; + item.iItem = add ? i : model_to_view(i); item.pszText = const_cast<LPWSTR>(text.c_str()); item.state = INDEXTOSTATEIMAGEMASK(model_->IsChecked(i) ? 2 : 1); ListView_SetItem(list_view_, &item); } } - if (start_column == column_count_) - return; item.stateMask = 0; item.mask = LVIF_TEXT; @@ -896,7 +1158,7 @@ void TableView::UpdateListViewCache0(int start, int length, bool add) { TableColumn& col = all_columns_[visible_columns_[j]]; int max_text_width = ListView_GetStringWidth(list_view_, col.title.c_str()); for (int i = start; i < max_row; ++i) { - item.iItem = i; + item.iItem = add ? i : model_to_view(i); item.iSubItem = j; std::wstring text = model_->GetText(i, visible_columns_[j]); item.pszText = const_cast<LPWSTR>(text.c_str()); @@ -917,10 +1179,18 @@ void TableView::UpdateListViewCache0(int start, int length, bool add) { // Protect against partial update. if (max_text_width > col.min_visible_width || - (start == 0 && length == model_->RowCount())) { + (start == 0 && length == model_->RowCount())) { col.min_visible_width = max_text_width; } } + + if (is_sorted()) { + // NOTE: As most of our tables are smallish I'm not going to optimize this. + // If our tables become large and frequently update, then it'll make sense + // to optimize this. + + SortItemsAndUpdateMapping(); + } } void TableView::OnDoubleClick() { @@ -941,15 +1211,14 @@ void TableView::OnKeyDown(unsigned short virtual_keycode) { } } -void TableView::OnCheckedStateChanged(int item, bool is_checked) { - if (!ignore_listview_change_) { - model_->SetChecked(item, is_checked); - } +void TableView::OnCheckedStateChanged(int model_row, bool is_checked) { + if (!ignore_listview_change_) + model_->SetChecked(model_row, is_checked); } -int TableView::PreviousSelectedIndex(int item) { - DCHECK(item >= 0); - if (!list_view_ || item <= 0) +int TableView::PreviousSelectedViewIndex(int view_index) { + DCHECK(view_index >= 0); + if (!list_view_ || view_index <= 0) return -1; int row_count = RowCount(); @@ -959,13 +1228,13 @@ int TableView::PreviousSelectedIndex(int item) { // For some reason // ListView_GetNextItem(list_view_,item, LVNI_SELECTED | LVNI_ABOVE) // fails on Vista (always returns -1), so we iterate through the indices. - item = std::min(item, row_count); - while (--item >= 0 && !IsItemSelected(item)); - return item; + view_index = std::min(view_index, row_count); + while (--view_index >= 0 && !IsItemSelected(view_to_model(view_index))); + return view_index; } -int TableView::LastSelectedIndex() { - return PreviousSelectedIndex(RowCount()); +int TableView::LastSelectedViewIndex() { + return PreviousSelectedViewIndex(RowCount()); } void TableView::UpdateContentOffset() { @@ -991,31 +1260,42 @@ void TableView::UpdateContentOffset() { // TableSelectionIterator // TableSelectionIterator::TableSelectionIterator(TableView* view, - int index) - : table_view_(view), index_(index) { + int view_index) + : table_view_(view), + view_index_(view_index) { + UpdateModelIndexFromViewIndex(); } TableSelectionIterator& TableSelectionIterator::operator=( const TableSelectionIterator& other) { - index_ = other.index_; + view_index_ = other.view_index_; + model_index_ = other.model_index_; return *this; } bool TableSelectionIterator::operator==(const TableSelectionIterator& other) { - return (other.index_ == index_); + return (other.view_index_ == view_index_); } bool TableSelectionIterator::operator!=(const TableSelectionIterator& other) { - return (other.index_ != index_); + return (other.view_index_ != view_index_); } TableSelectionIterator& TableSelectionIterator::operator++() { - index_ = table_view_->PreviousSelectedIndex(index_); + view_index_ = table_view_->PreviousSelectedViewIndex(view_index_); + UpdateModelIndexFromViewIndex(); return *this; } int TableSelectionIterator::operator*() { - return index_; + return model_index_; +} + +void TableSelectionIterator::UpdateModelIndexFromViewIndex() { + if (view_index_ == -1) + model_index_ = -1; + else + model_index_ = table_view_->view_to_model(view_index_); } } // namespace diff --git a/chrome/views/table_view.h b/chrome/views/table_view.h index 30f28f8..a2cfc40 100644 --- a/chrome/views/table_view.h +++ b/chrome/views/table_view.h @@ -22,8 +22,22 @@ class SkBitmap; // to display. TableModel also has an Observer which is used to notify // TableView of changes to the model so that the display may be updated // appropriately. +// // TableView itself has an observer that is notified when the selection // changes. +// +// Tables may be sorted either by directly invoking SetSortDescriptors or by +// marking the column as sortable and the user doing a gesture to sort the +// contents. TableView itself maintains the sort so that the underlying model +// isn't effected. +// +// When a table is sorted the model coordinates do not necessarily match the +// view coordinates. All table methods are in terms of the model. If you need to +// convert to view coordinates use model_to_view. +// +// Sorting is done by a locale sensitive string sort. You can customize the +// sort by way of overriding CompareValues. +// // TableView is a wrapper around the window type ListView in report mode. namespace ChromeViews { @@ -125,6 +139,14 @@ class TableModel { // Sets the observer for the model. The TableView should NOT take ownership // of the observer. virtual void SetObserver(TableModelObserver* observer) = 0; + + // Compares the values in the column with id |column_id| for the two rows. + // Returns a value < 0, == 0 or > 0 as to whether the first value is + // <, == or > the second value. + // + // This implementation does a case insensitive locale specific string + // comparison. + virtual int CompareValues(int row1, int row2, int column_id); }; // TableColumn specifies the title, alignment and size of a particular column. @@ -139,7 +161,8 @@ struct TableColumn { alignment(LEFT), width(-1), percent(), - min_visible_width(0) { + min_visible_width(0), + sortable(false) { } TableColumn(int id, const std::wstring title, Alignment alignment, int width) : id(id), @@ -147,7 +170,8 @@ struct TableColumn { alignment(alignment), width(width), percent(0), - min_visible_width(0) { + min_visible_width(0), + sortable(false) { } TableColumn(int id, const std::wstring title, Alignment alignment, int width, float percent) @@ -156,7 +180,8 @@ struct TableColumn { alignment(alignment), width(width), percent(percent), - min_visible_width(0) { + min_visible_width(0), + sortable(false) { } // It's common (but not required) to use the title's IDS_* tag as the column // id. In this case, the provided conveniences look up the title string on @@ -166,7 +191,8 @@ struct TableColumn { alignment(alignment), width(width), percent(0), - min_visible_width(0) { + min_visible_width(0), + sortable(false) { title = l10n_util::GetString(id); } TableColumn(int id, Alignment alignment, int width, float percent) @@ -174,7 +200,8 @@ struct TableColumn { alignment(alignment), width(width), percent(percent), - min_visible_width(0) { + min_visible_width(0), + sortable(false) { title = l10n_util::GetString(id); } @@ -207,12 +234,15 @@ struct TableColumn { // (including the header) // to be visible. int min_visible_width; + + // Is this column sortable? Default is false + bool sortable; }; // Returned from SelectionBegin/SelectionEnd class TableSelectionIterator { public: - TableSelectionIterator(TableView* view, int index); + TableSelectionIterator(TableView* view, int view_index); TableSelectionIterator& operator=(const TableSelectionIterator& other); bool operator==(const TableSelectionIterator& other); bool operator!=(const TableSelectionIterator& other); @@ -220,8 +250,14 @@ class TableSelectionIterator { int operator*(); private: + void UpdateModelIndexFromViewIndex(); + TableView* table_view_; - int index_; + int view_index_; + + // The index in terms of the model. This is returned from the * operator. This + // is cached to avoid dependencies on the view_to_model mapping. + int model_index_; }; // TableViewObserver is notified about the TableView selection. @@ -251,6 +287,22 @@ class TableView : public NativeControl, SkColor color; }; + // Describes a sorted column. + struct SortDescriptor { + SortDescriptor() : column_id(-1), ascending(true) {} + SortDescriptor(int column_id, bool ascending) + : column_id(column_id), + ascending(ascending) { } + + // ID of the sorted column. + int column_id; + + // Is the sort ascending? + bool ascending; + }; + + typedef std::vector<SortDescriptor> SortDescriptors; + // Creates a new table using the model and columns specified. // The table type applies to the content of the first column (text, icon and // text, checkbox and text). @@ -276,6 +328,12 @@ class TableView : public NativeControl, // issues when the model needs to be deleted before the table. void SetModel(TableModel* model); + // Resorts the contents. + void SetSortDescriptors(const SortDescriptors& sort_descriptors); + + // Current sort. + const SortDescriptors& sort_descriptors() const { return sort_descriptors_; } + void DidChangeBounds(const CRect& previous, const CRect& current); // Returns the number of rows in the TableView. @@ -285,28 +343,30 @@ class TableView : public NativeControl, int SelectedRowCount(); // Selects the specified item, making sure it's visible. - void Select(int item); + void Select(int model_row); // Sets the selected state of an item (without sending any selection // notifications). Note that this routine does NOT set the focus to the // item at the given index. - void SetSelectedState(int item, bool state); + void SetSelectedState(int model_row, bool state); // Sets the focus to the item at the given index. - void SetFocusOnItem(int item); + void SetFocusOnItem(int model_row); - // Returns the first selected row. + // Returns the first selected row in terms of the model. int FirstSelectedRow(); // Returns true if the item at the specified index is selected. - bool IsItemSelected(int item); + bool IsItemSelected(int model_row); // Returns true if the item at the specified index has the focus. - bool ItemHasTheFocus(int item); + bool ItemHasTheFocus(int model_row); // Returns an iterator over the selection. The iterator proceeds from the - // last index to the first. Do NOT use the iterator after you've mutated - // the model. + // last index to the first. + // + // NOTE: the iterator iterates over the visual order (but returns coordinates + // in terms of the model). iterator SelectionBegin(); iterator SelectionEnd(); @@ -342,6 +402,19 @@ class TableView : public NativeControl, void SetPreferredSize(const CSize& preferred_size); virtual void GetPreferredSize(CSize* out); + // Is the table sorted? + bool is_sorted() const { return !sort_descriptors_.empty(); } + + // Maps from the index in terms of the model to that of the view. + int model_to_view(int model_index) const { + return model_to_view_.get() ? model_to_view_[model_index] : model_index; + } + + // Maps from the index in terms of the view to that of the model. + int view_to_model(int view_index) const { + return view_to_model_.get() ? view_to_model_[view_index] : view_index; + } + protected: // Subclasses that want to customize the colors of a particular row/column, // must invoke this passing in true. The default value is false, such that @@ -362,7 +435,7 @@ class TableView : public NativeControl, // Invoked to customize the colors or font at a particular cell. If you // change the colors or font, return true. This is only invoked if // SetCustomColorsEnabled(true) has been invoked. - virtual bool GetCellColors(int row, + virtual bool GetCellColors(int model_row, int column, ItemColor* foreground, ItemColor* background, @@ -373,7 +446,7 @@ class TableView : public NativeControl, // method. virtual bool ImplementPostPaint() { return false; } // Subclasses can implement in this method extra-painting for cells. - virtual void PostPaint(int row, int column, bool selected, + virtual void PostPaint(int model_row, int column, bool selected, const CRect& bounds, HDC device_context) { } virtual HWND CreateNativeControl(HWND parent_container); @@ -383,7 +456,23 @@ class TableView : public NativeControl, // Overriden to destroy the image list. virtual void OnDestroy(); + // Used to sort the two rows. Returns a value < 0, == 0 or > 0 indicating + // whether the row2 comes before row1, row2 is the same as row1 or row1 comes + // after row2. This invokes CompareValues on the model with the sorted column. + virtual int CompareRows(int model_row1, int model_row2); + + // Called before sorting. This does nothing and is intended for subclasses + // that need to cache state used during sorting. + virtual void PrepareForSort() {} + private: + // Direction of a sort. + enum SortDirection { + ASCENDING_SORT, + DESCENDING_SORT, + NO_SORT + }; + // We need this wrapper to pass the table view to the windows proc handler // when subclassing the list view and list view header, as the reinterpret // cast from GetWindowLongPtr would break the pointer if it is pointing to a @@ -398,6 +487,34 @@ class TableView : public NativeControl, LRESULT OnCustomDraw(NMLVCUSTOMDRAW* draw_info); + // Invoked when the user clicks on a column to toggle the sort order. If + // column_id is the primary sorted column the direction of the sort is + // toggled, otherwise column_id is made the primary sorted column. + void ToggleSortOrder(int column_id); + + // Updates the lparam of each of the list view items to be the model index. + // If length is > 0, all items with an index >= start get offset by length. + // This is used during sorting to determine how the items were sorted. + void UpdateItemsLParams(int start, int length); + + // Does the actual sort and updates the mappings (view_to_model and + // model_to_view) appropriately. + void SortItemsAndUpdateMapping(); + + // Method invoked by ListView to compare the two values. Invokes CompareRows. + static int CALLBACK SortFunc(LPARAM model_index_1_p, + LPARAM model_index_2_p, + LPARAM table_view_param); + + // Method invoked by ListView when sorting back to natural state. Returns + // model_index_1_p - model_index_2_p. + static int CALLBACK NaturalSortFunc(LPARAM model_index_1_p, + LPARAM model_index_2_p, + LPARAM table_view_param); + + // Resets the sort image displayed for the specified column. + void ResetColumnSortImage(int column_id, SortDirection direction); + // Adds a new column. void InsertColumn(const TableColumn& tc, int index); @@ -418,15 +535,19 @@ class TableView : public NativeControl, // Notification from the ListView that the checked state of the item has // changed. - void OnCheckedStateChanged(int item, bool is_checked); + void OnCheckedStateChanged(int model_row, bool is_checked); - // Returns the index of the selected item before |item|, or -1 if |item| is - // the first selected item. - int PreviousSelectedIndex(int item); + // Returns the index of the selected item before |view_index|, or -1 if + // |view_index| is the first selected item. + // + // WARNING: this returns coordinates in terms of the view, NOT the model. + int PreviousSelectedViewIndex(int view_index); - // Returns the last selected index in the table view, or -1 if the table + // Returns the last selected view index in the table view, or -1 if the table // is empty, or nothing is selected. - int LastSelectedIndex(); + // + // WARNING: this returns coordinates in terms of the view, NOT the model. + int LastSelectedViewIndex(); // The TableColumn visible at position pos. const TableColumn& GetColumnAtPosition(int pos); @@ -460,10 +581,6 @@ class TableView : public NativeControl, // Cached value of columns_.size() int column_count_; - // Whether or not the data should be cached in the TableView. - // This is currently always true. - bool cache_data_; - // Selection mode. bool single_selection_; @@ -509,8 +626,16 @@ class TableView : public NativeControl, // The offset from the top of the client area to the start of the content. int content_offset_; + // Current sort. + SortDescriptors sort_descriptors_; + + // Mappings used when sorted. + scoped_array<int> view_to_model_; + scoped_array<int> model_to_view_; + DISALLOW_COPY_AND_ASSIGN(TableView); }; -} + +} // namespace #endif // CHROME_VIEWS_TABLE_VIEW_H_ diff --git a/chrome/views/table_view_unittest.cc b/chrome/views/table_view_unittest.cc new file mode 100644 index 0000000..2467f04 --- /dev/null +++ b/chrome/views/table_view_unittest.cc @@ -0,0 +1,363 @@ +// Copyright (c) 2006-2008 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 <vector> + +#include "base/string_util.h" +#include "chrome/views/table_view.h" +#include "chrome/views/window.h" +#include "chrome/views/window_delegate.h" +#include "testing/gtest/include/gtest/gtest.h" + +using ChromeViews::TableView; + +// TestTableModel -------------------------------------------------------------- + +// 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 +class TestTableModel : public ChromeViews::TableModel { + public: + TestTableModel(); + + // 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); + + // TableModel + virtual int RowCount(); + virtual std::wstring GetText(int row, int column_id); + virtual void SetObserver(ChromeViews::TableModelObserver* observer); + virtual int CompareValues(int row1, int row2, int column_id); + + private: + ChromeViews::TableModelObserver* observer_; + + // The data. + std::vector<std::vector<int>> rows_; + + DISALLOW_COPY_AND_ASSIGN(TestTableModel); +}; + +TestTableModel::TestTableModel() : observer_(NULL) { + AddRow(0, 0, 1); + AddRow(1, 1, 1); + AddRow(2, 2, 2); +} + +void TestTableModel::AddRow(int row, int c1_value, int c2_value) { + DCHECK(row >= 0 && row <= static_cast<int>(rows_.size())); + std::vector<int> 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 TestTableModel::RemoveRow(int row) { + DCHECK(row >= 0 && row <= static_cast<int>(rows_.size())); + rows_.erase(rows_.begin() + row); + if (observer_) + observer_->OnItemsRemoved(row, 1); +} + +void TestTableModel::ChangeRow(int row, int c1_value, int c2_value) { + DCHECK(row >= 0 && row < static_cast<int>(rows_.size())); + rows_[row][0] = c1_value; + rows_[row][1] = c2_value; + if (observer_) + observer_->OnItemsChanged(row, 1); +} + +int TestTableModel::RowCount() { + return static_cast<int>(rows_.size()); +} + +std::wstring TestTableModel::GetText(int row, int column_id) { + return IntToWString(rows_[row][column_id]); +} + +void TestTableModel::SetObserver(ChromeViews::TableModelObserver* observer) { + observer_ = observer; +} + +int TestTableModel::CompareValues(int row1, int row2, int column_id) { + return rows_[row1][column_id] - rows_[row2][column_id]; +} + +// TableViewTest --------------------------------------------------------------- + +class TableViewTest : public testing::Test, ChromeViews::WindowDelegate { + public: + virtual void SetUp(); + virtual void TearDown(); + + virtual ChromeViews::View* GetContentsView() { + return table_; + } + + protected: + // Creates the model. + TestTableModel* CreateModel(); + + // Verifies the view order matches that of the supplied arguments. The + // arguments are in terms of the model. For example, values of '1, 0' indicate + // the model index at row 0 is 1 and the model index at row 1 is 0. + void VeriyViewOrder(int first, ...); + + // Verifies the selection matches the supplied arguments. The supplied + // arguments are in terms of this model. This uses the iterator returned by + // SelectionBegin. + void VerifySelectedRows(int first, ...); + + // Configures the state for the various multi-selection tests. + // This selects model rows 0 and 1, and if |sort| is true the first column + // is sorted in descending order. + void SetUpMultiSelectTestState(bool sort); + + scoped_ptr<TestTableModel> model_; + + // The table. This is owned by the window. + TableView* table_; + + private: + MessageLoopForUI message_loop_; + ChromeViews::Window* window_; +}; + +void TableViewTest::SetUp() { + model_.reset(CreateModel()); + std::vector<ChromeViews::TableColumn> columns; + columns.resize(2); + columns[0].id = 0; + columns[1].id = 1; + table_ = new TableView(model_.get(), columns, ChromeViews::ICON_AND_TEXT, + false, false, false); + window_ = + ChromeViews::Window::CreateChromeWindow(NULL, + gfx::Rect(100, 100, 512, 512), + this); +} + +void TableViewTest::TearDown() { + window_->CloseNow(); + // Temporary workaround to avoid leak of RootView::pending_paint_task_. + message_loop_.RunAllPending(); +} + +void TableViewTest::VeriyViewOrder(int first, ...) { + va_list marker; + va_start(marker, first); + int value = first; + int index = 0; + for (int value = first, index = 0; value != -1; index++) { + ASSERT_EQ(value, table_->view_to_model(index)); + value = va_arg(marker, int); + } + va_end(marker); +} + +void TableViewTest::VerifySelectedRows(int first, ...) { + va_list marker; + va_start(marker, first); + int value = first; + int index = 0; + TableView::iterator selection_iterator = table_->SelectionBegin(); + for (int value = first, index = 0; value != -1; index++) { + ASSERT_TRUE(selection_iterator != table_->SelectionEnd()); + ASSERT_EQ(value, *selection_iterator); + value = va_arg(marker, int); + ++selection_iterator; + } + ASSERT_TRUE(selection_iterator == table_->SelectionEnd()); + va_end(marker); +} + +void TableViewTest::SetUpMultiSelectTestState(bool sort) { + // Select two rows. + table_->SetSelectedState(0, true); + table_->SetSelectedState(1, true); + + VerifySelectedRows(1, 0, -1); + if (!sort || HasFatalFailure()) + return; + + // Sort by first column descending. + TableView::SortDescriptors sd; + sd.push_back(TableView::SortDescriptor(0, false)); + table_->SetSortDescriptors(sd); + VeriyViewOrder(2, 1, 0, -1); + if (HasFatalFailure()) + return; + + // Make sure the two rows are sorted. + // NOTE: the order changed because iteration happens over view indices. + VerifySelectedRows(0, 1, -1); +} + +TestTableModel* TableViewTest::CreateModel() { + return new TestTableModel(); +} + +// Tests ----------------------------------------------------------------------- + +// Tests various sorting permutations. +TEST_F(TableViewTest, Sort) { + // Sort by first column descending. + TableView::SortDescriptors sort; + sort.push_back(TableView::SortDescriptor(0, false)); + table_->SetSortDescriptors(sort); + VeriyViewOrder(2, 1, 0, -1); + if (HasFatalFailure()) + return; + + // Sort by second column ascending, first column descending. + sort.clear(); + sort.push_back(TableView::SortDescriptor(1, true)); + sort.push_back(TableView::SortDescriptor(0, false)); + sort[1].ascending = false; + table_->SetSortDescriptors(sort); + VeriyViewOrder(1, 0, 2, -1); + if (HasFatalFailure()) + return; + + // Clear the sort. + table_->SetSortDescriptors(TableView::SortDescriptors()); + VeriyViewOrder(0, 1, 2, -1); + if (HasFatalFailure()) + return; +} + +// Tests changing the model while sorted. +TEST_F(TableViewTest, SortThenChange) { + // Sort by first column descending. + TableView::SortDescriptors sort; + sort.push_back(TableView::SortDescriptor(0, false)); + table_->SetSortDescriptors(sort); + VeriyViewOrder(2, 1, 0, -1); + if (HasFatalFailure()) + return; + + model_->ChangeRow(0, 3, 1); + VeriyViewOrder(0, 2, 1, -1); +} + +// Tests adding to the model while sorted. +TEST_F(TableViewTest, AddToSorted) { + // Sort by first column descending. + TableView::SortDescriptors sort; + sort.push_back(TableView::SortDescriptor(0, false)); + table_->SetSortDescriptors(sort); + VeriyViewOrder(2, 1, 0, -1); + if (HasFatalFailure()) + return; + + // Add row so that it occurs first. + model_->AddRow(0, 5, -1); + VeriyViewOrder(0, 3, 2, 1, -1); + if (HasFatalFailure()) + return; + + // Add row so that it occurs last. + model_->AddRow(0, -1, -1); + VeriyViewOrder(1, 4, 3, 2, 0, -1); +} + +// Tests selection on sort. +TEST_F(TableViewTest, PersistSelectionOnSort) { + // Select row 0. + table_->Select(0); + + // Sort by first column descending. + TableView::SortDescriptors sort; + sort.push_back(TableView::SortDescriptor(0, false)); + table_->SetSortDescriptors(sort); + VeriyViewOrder(2, 1, 0, -1); + if (HasFatalFailure()) + return; + + // Make sure 0 is still selected. + EXPECT_EQ(0, table_->FirstSelectedRow()); +} + +// Tests selection iterator with sort. +TEST_F(TableViewTest, PersistMultiSelectionOnSort) { + SetUpMultiSelectTestState(true); +} + +// Tests selection persists after a change when sorted with iterator. +TEST_F(TableViewTest, PersistMultiSelectionOnChangeWithSort) { + SetUpMultiSelectTestState(true); + if (HasFatalFailure()) + return; + + model_->ChangeRow(0, 3, 1); + + VerifySelectedRows(1, 0, -1); +} + +// Tests selection persists after a remove when sorted with iterator. +TEST_F(TableViewTest, PersistMultiSelectionOnRemoveWithSort) { + SetUpMultiSelectTestState(true); + if (HasFatalFailure()) + return; + + model_->RemoveRow(0); + + VerifySelectedRows(0, -1); +} + +// Tests selection persists after a add when sorted with iterator. +TEST_F(TableViewTest, PersistMultiSelectionOnAddWithSort) { + SetUpMultiSelectTestState(true); + if (HasFatalFailure()) + return; + + model_->AddRow(3, 4, 4); + + VerifySelectedRows(0, 1, -1); +} + +// Tests selection persists after a change with iterator. +TEST_F(TableViewTest, PersistMultiSelectionOnChange) { + SetUpMultiSelectTestState(false); + if (HasFatalFailure()) + return; + + model_->ChangeRow(0, 3, 1); + + VerifySelectedRows(1, 0, -1); +} + +// Tests selection persists after a remove with iterator. +TEST_F(TableViewTest, PersistMultiSelectionOnRemove) { + SetUpMultiSelectTestState(false); + if (HasFatalFailure()) + return; + + model_->RemoveRow(0); + + VerifySelectedRows(0, -1); +} + +// Tests selection persists after a add with iterator. +TEST_F(TableViewTest, PersistMultiSelectionOnAdd) { + SetUpMultiSelectTestState(false); + if (HasFatalFailure()) + return; + + model_->AddRow(3, 4, 4); + + VerifySelectedRows(1, 0, -1); +} diff --git a/chrome/views/views.vcproj b/chrome/views/views.vcproj index ff56556..9f73219 100644 --- a/chrome/views/views.vcproj +++ b/chrome/views/views.vcproj @@ -18,7 +18,7 @@ <Configuration Name="Debug|Win32" ConfigurationType="4" - InheritedPropertySheets=".\views.vsprops;$(SolutionDir)..\build\debug.vsprops;..\tools\build\win\precompiled_wtl.vsprops" + InheritedPropertySheets=".\views.vsprops;$(SolutionDir)..\build\debug.vsprops;..\tools\build\win\precompiled_wtl.vsprops;..\..\third_party\icu38\build\using_icu.vsprops" > <Tool Name="VCPreBuildEventTool" @@ -69,7 +69,7 @@ <Configuration Name="Release|Win32" ConfigurationType="4" - InheritedPropertySheets=".\views.vsprops;$(SolutionDir)..\build\release.vsprops" + InheritedPropertySheets=".\views.vsprops;$(SolutionDir)..\build\release.vsprops;..\..\third_party\icu38\build\using_icu.vsprops" > <Tool Name="VCPreBuildEventTool" |