summaryrefslogtreecommitdiffstats
path: root/views/controls/table
diff options
context:
space:
mode:
Diffstat (limited to 'views/controls/table')
-rw-r--r--views/controls/table/group_table_view.cc193
-rw-r--r--views/controls/table/group_table_view.h82
-rw-r--r--views/controls/table/table_view.cc1570
-rw-r--r--views/controls/table/table_view.h676
-rw-r--r--views/controls/table/table_view_unittest.cc381
5 files changed, 2902 insertions, 0 deletions
diff --git a/views/controls/table/group_table_view.cc b/views/controls/table/group_table_view.cc
new file mode 100644
index 0000000..5e6a155
--- /dev/null
+++ b/views/controls/table/group_table_view.cc
@@ -0,0 +1,193 @@
+// 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 "views/controls/table/group_table_view.h"
+
+#include "app/gfx/chrome_canvas.h"
+#include "base/message_loop.h"
+#include "base/task.h"
+
+namespace views {
+
+static const COLORREF kSeparatorLineColor = RGB(208, 208, 208);
+static const int kSeparatorLineThickness = 1;
+
+const char GroupTableView::kViewClassName[] = "views/GroupTableView";
+
+GroupTableView::GroupTableView(GroupTableModel* model,
+ const std::vector<TableColumn>& columns,
+ TableTypes table_type,
+ bool single_selection,
+ bool resizable_columns,
+ bool autosize_columns)
+ : TableView(model, columns, table_type, false, resizable_columns,
+ autosize_columns),
+ model_(model),
+ sync_selection_factory_(this) {
+}
+
+GroupTableView::~GroupTableView() {
+}
+
+void GroupTableView::SyncSelection() {
+ int index = 0;
+ int row_count = model_->RowCount();
+ GroupRange group_range;
+ while (index < row_count) {
+ model_->GetGroupRangeForItem(index, &group_range);
+ if (group_range.length == 1) {
+ // No synching required for single items.
+ index++;
+ } else {
+ // We need to select the group if at least one item is selected.
+ bool should_select = false;
+ for (int i = group_range.start;
+ i < group_range.start + group_range.length; ++i) {
+ if (IsItemSelected(i)) {
+ should_select = true;
+ break;
+ }
+ }
+ if (should_select) {
+ for (int i = group_range.start;
+ i < group_range.start + group_range.length; ++i) {
+ SetSelectedState(i, true);
+ }
+ }
+ index += group_range.length;
+ }
+ }
+}
+
+void GroupTableView::OnKeyDown(unsigned short virtual_keycode) {
+ // In a list view, multiple items can be selected but only one item has the
+ // focus. This creates a problem when the arrow keys are used for navigating
+ // between items in the list view. An example will make this more clear:
+ //
+ // Suppose we have 5 items in the list view, and three of these items are
+ // part of one group:
+ //
+ // Index0: ItemA (No Group)
+ // Index1: ItemB (GroupX)
+ // Index2: ItemC (GroupX)
+ // Index3: ItemD (GroupX)
+ // Index4: ItemE (No Group)
+ //
+ // When GroupX is selected (say, by clicking on ItemD with the mouse),
+ // GroupTableView::SyncSelection() will make sure ItemB, ItemC and ItemD are
+ // selected. Also, the item with the focus will be ItemD (simply because
+ // this is the item the user happened to click on). If then the UP arrow is
+ // pressed once, the focus will be switched to ItemC and not to ItemA and the
+ // end result is that we are stuck in GroupX even though the intention was to
+ // switch to ItemA.
+ //
+ // For that exact reason, we need to set the focus appropriately when we
+ // detect that one of the arrow keys is pressed. Thus, when it comes time
+ // for the list view control to actually switch the focus, the right item
+ // will be selected.
+ if ((virtual_keycode != VK_UP) && (virtual_keycode != VK_DOWN)) {
+ TableView::OnKeyDown(virtual_keycode);
+ return;
+ }
+
+ // We start by finding the index of the item with the focus. If no item
+ // currently has the focus, then this routine doesn't do anything.
+ int focused_index;
+ int row_count = model_->RowCount();
+ for (focused_index = 0; focused_index < row_count; focused_index++) {
+ if (ItemHasTheFocus(focused_index)) {
+ break;
+ }
+ }
+
+ if (focused_index == row_count) {
+ return;
+ }
+ DCHECK_LT(focused_index, row_count);
+
+ // Nothing to do if the item which has the focus is not part of a group.
+ GroupRange group_range;
+ model_->GetGroupRangeForItem(focused_index, &group_range);
+ if (group_range.length == 1) {
+ return;
+ }
+
+ // If the user pressed the UP key, then the focus should be set to the
+ // topmost element in the group. If the user pressed the DOWN key, the focus
+ // should be set to the bottommost element.
+ if (virtual_keycode == VK_UP) {
+ SetFocusOnItem(group_range.start);
+ } else {
+ DCHECK_EQ(virtual_keycode, VK_DOWN);
+ SetFocusOnItem(group_range.start + group_range.length - 1);
+ }
+}
+
+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() {
+ // 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
+ // items. For that reason, we post a task to be performed later, after all
+ // selection messages have been processed. In the meantime we just ignore all
+ // selection notifications.
+ if (sync_selection_factory_.empty()) {
+ MessageLoop::current()->PostTask(FROM_HERE,
+ sync_selection_factory_.NewRunnableMethod(
+ &GroupTableView::SyncSelection));
+ }
+ TableView::OnSelectedStateChanged();
+}
+
+// Draws the line separator betweens the groups.
+void GroupTableView::PostPaint(int model_row, int column, bool selected,
+ const CRect& bounds, HDC hdc) {
+ GroupRange 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);
+ HPEN hPenOld = (HPEN) SelectObject(hdc, hPen);
+ int x = static_cast<int>(bounds.right - kSeparatorLineThickness);
+ MoveToEx(hdc, x, bounds.top, NULL);
+ LineTo(hdc, x, bounds.bottom);
+
+ // We paint a separator line after the last item of a group.
+ 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);
+ }
+ SelectObject(hdc, hPenOld);
+ DeleteObject(hPen);
+}
+
+std::string GroupTableView::GetClassName() const {
+ return kViewClassName;
+}
+
+} // namespace views
diff --git a/views/controls/table/group_table_view.h b/views/controls/table/group_table_view.h
new file mode 100644
index 0000000..d128759
--- /dev/null
+++ b/views/controls/table/group_table_view.h
@@ -0,0 +1,82 @@
+// 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.
+
+#ifndef VIEWS_CONTROLS_TABLE_GROUP_TABLE_VIEW_H_
+#define VIEWS_CONTROLS_TABLE_GROUP_TABLE_VIEW_H_
+
+#include "base/task.h"
+#include "views/controls/table/table_view.h"
+
+// The GroupTableView adds grouping to the TableView class.
+// It allows to have groups of rows that act as a single row from the selection
+// perspective. Groups are visually separated by a horizontal line.
+
+namespace views {
+
+struct GroupRange {
+ int start;
+ int length;
+};
+
+// The model driving the GroupTableView.
+class GroupTableModel : public TableModel {
+ public:
+ // Populates the passed range with the first row/last row (included)
+ // that this item belongs to.
+ virtual void GetGroupRangeForItem(int item, GroupRange* range) = 0;
+};
+
+class GroupTableView : public TableView {
+ public:
+ // The view class name.
+ static const char kViewClassName[];
+
+ GroupTableView(GroupTableModel* model,
+ const std::vector<TableColumn>& columns,
+ TableTypes table_type, bool single_selection,
+ bool resizable_columns, bool autosize_columns);
+ virtual ~GroupTableView();
+
+ virtual std::string GetClassName() const;
+
+ protected:
+ // Notification from the ListView that the selected state of an item has
+ // changed.
+ void OnSelectedStateChanged();
+
+ // Extra-painting required to draw the separator line between groups.
+ virtual bool ImplementPostPaint() { return true; }
+ 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
+ // keys), we must take action when an arrow key is pressed. The reason we
+ // need to process this message has to do with the manner in which the focus
+ // 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_;
+
+ // A factory to make the selection consistent among groups.
+ ScopedRunnableMethodFactory<GroupTableView> sync_selection_factory_;
+
+ // Maps from model row to start of group.
+ std::map<int,int> model_index_to_range_start_map_;
+
+ DISALLOW_COPY_AND_ASSIGN(GroupTableView);
+};
+
+} // namespace views
+
+#endif // VIEWS_CONTROLS_TABLE_GROUP_TABLE_VIEW_H_
diff --git a/views/controls/table/table_view.cc b/views/controls/table/table_view.cc
new file mode 100644
index 0000000..f8c7303
--- /dev/null
+++ b/views/controls/table/table_view.cc
@@ -0,0 +1,1570 @@
+// 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 "views/controls/table/table_view.h"
+
+#include <algorithm>
+#include <windowsx.h>
+
+#include "app/gfx/chrome_canvas.h"
+#include "app/gfx/favicon_size.h"
+#include "app/gfx/icon_util.h"
+#include "app/l10n_util_win.h"
+#include "app/resource_bundle.h"
+#include "base/string_util.h"
+#include "base/win_util.h"
+#include "skia/ext/skia_utils_win.h"
+#include "skia/include/SkBitmap.h"
+#include "skia/include/SkColorFilter.h"
+#include "views/controls/hwnd_view.h"
+
+namespace views {
+
+// Added to column width to prevent truncation.
+const int kListViewTextPadding = 15;
+// Additional column width necessary if column has icons.
+const int kListViewIconWidthAndPadding = 18;
+
+// TableModel -----------------------------------------------------------------
+
+// static
+const int TableView::kImageSize = 18;
+
+// 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);
+ Collator* collator = GetCollator();
+
+ 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;
+}
+
+Collator* TableModel::GetCollator() {
+ if (!collator) {
+ UErrorCode create_status = U_ZERO_ERROR;
+ collator = Collator::createInstance(create_status);
+ if (!U_SUCCESS(create_status)) {
+ collator = NULL;
+ NOTREACHED();
+ }
+ }
+ return collator;
+}
+
+// TableView ------------------------------------------------------------------
+
+TableView::TableView(TableModel* model,
+ const std::vector<TableColumn>& columns,
+ TableTypes table_type,
+ bool single_selection,
+ bool resizable_columns,
+ bool autosize_columns)
+ : model_(model),
+ table_view_observer_(NULL),
+ visible_columns_(),
+ all_columns_(),
+ column_count_(static_cast<int>(columns.size())),
+ table_type_(table_type),
+ single_selection_(single_selection),
+ ignore_listview_change_(false),
+ custom_colors_enabled_(false),
+ sized_columns_(false),
+ autosize_columns_(autosize_columns),
+ resizable_columns_(resizable_columns),
+ list_view_(NULL),
+ header_original_handler_(NULL),
+ original_handler_(NULL),
+ table_view_wrapper_(this),
+ custom_cell_font_(NULL),
+ content_offset_(0) {
+ for (std::vector<TableColumn>::const_iterator i = columns.begin();
+ i != columns.end(); ++i) {
+ AddColumn(*i);
+ visible_columns_.push_back(i->id);
+ }
+}
+
+TableView::~TableView() {
+ if (list_view_) {
+ if (model_)
+ model_->SetObserver(NULL);
+ }
+ if (custom_cell_font_)
+ DeleteObject(custom_cell_font_);
+}
+
+void TableView::SetModel(TableModel* model) {
+ if (model == model_)
+ return;
+
+ if (list_view_ && model_)
+ model_->SetObserver(NULL);
+ model_ = model;
+ if (list_view_ && model_)
+ model_->SetObserver(this);
+ if (list_view_)
+ 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 gfx::Rect& previous,
+ const gfx::Rect& current) {
+ if (!list_view_)
+ return;
+ SendMessage(list_view_, WM_SETREDRAW, static_cast<WPARAM>(FALSE), 0);
+ Layout();
+ if ((!sized_columns_ || autosize_columns_) && width() > 0) {
+ sized_columns_ = true;
+ ResetColumnSizes();
+ }
+ UpdateContentOffset();
+ SendMessage(list_view_, WM_SETREDRAW, static_cast<WPARAM>(TRUE), 0);
+}
+
+int TableView::RowCount() {
+ if (!list_view_)
+ return 0;
+ return ListView_GetItemCount(list_view_);
+}
+
+int TableView::SelectedRowCount() {
+ if (!list_view_)
+ return 0;
+ return ListView_GetSelectedCount(list_view_);
+}
+
+void TableView::Select(int model_row) {
+ if (!list_view_)
+ return;
+
+ DCHECK(model_row >= 0 && model_row < RowCount());
+ SendMessage(list_view_, WM_SETREDRAW, static_cast<WPARAM>(FALSE), 0);
+ ignore_listview_change_ = true;
+
+ // Unselect everything.
+ ListView_SetItemState(list_view_, -1, 0, LVIS_SELECTED);
+
+ // Select the specified item.
+ 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_, 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 model_row, bool state) {
+ if (!list_view_)
+ return;
+
+ DCHECK(model_row >= 0 && model_row < RowCount());
+
+ ignore_listview_change_ = true;
+
+ // Select the specified item.
+ ListView_SetItemState(list_view_, model_to_view(model_row),
+ state ? LVIS_SELECTED : 0, LVIS_SELECTED);
+
+ ignore_listview_change_ = false;
+}
+
+void TableView::SetFocusOnItem(int model_row) {
+ if (!list_view_)
+ return;
+
+ DCHECK(model_row >= 0 && model_row < RowCount());
+
+ ignore_listview_change_ = true;
+
+ // Set the focus to the given item.
+ ListView_SetItemState(list_view_, model_to_view(model_row), LVIS_FOCUSED,
+ LVIS_FOCUSED);
+
+ ignore_listview_change_ = false;
+}
+
+int TableView::FirstSelectedRow() {
+ if (!list_view_)
+ return -1;
+
+ 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 model_row) {
+ if (!list_view_)
+ return false;
+
+ 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 model_row) {
+ if (!list_view_)
+ return false;
+
+ 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, LastSelectedViewIndex());
+}
+
+TableView::iterator TableView::SelectionEnd() {
+ return TableView::iterator(this, -1);
+}
+
+void TableView::OnItemsChanged(int start, int length) {
+ if (!list_view_)
+ return;
+
+ if (length == -1) {
+ DCHECK(start >= 0);
+ length = model_->RowCount() - start;
+ }
+ int row_count = RowCount();
+ DCHECK(start >= 0 && length > 0 && start + length <= row_count);
+ SendMessage(list_view_, WM_SETREDRAW, static_cast<WPARAM>(FALSE), 0);
+ if (table_type_ == ICON_AND_TEXT) {
+ // The redraw event does not include the icon in the clip rect, preventing
+ // our icon from being repainted. So far the only way I could find around
+ // this is to change the image for the item. Even if the image does not
+ // exist, it causes the clip rect to include the icon's bounds so we can
+ // paint it in the post paint event.
+ LVITEM lv_item;
+ memset(&lv_item, 0, sizeof(LVITEM));
+ lv_item.mask = LVIF_IMAGE;
+ for (int i = start; i < start + length; ++i) {
+ // Retrieve the current icon index.
+ 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.
+ lv_item.iImage = (lv_item.iImage + 1) % 2;
+ DCHECK((lv_item.iImage == 0) || (lv_item.iImage == 1));
+ r = ListView_SetItem(list_view_, &lv_item);
+ DCHECK(r);
+ }
+ }
+ UpdateListViewCache(start, length, false);
+ SendMessage(list_view_, WM_SETREDRAW, static_cast<WPARAM>(TRUE), 0);
+}
+
+void TableView::OnModelChanged() {
+ if (!list_view_)
+ return;
+
+ int current_row_count = ListView_GetItemCount(list_view_);
+ if (current_row_count > 0)
+ OnItemsRemoved(0, current_row_count);
+ if (model_ && model_->RowCount())
+ OnItemsAdded(0, model_->RowCount());
+}
+
+void TableView::OnItemsAdded(int start, int length) {
+ if (!list_view_)
+ return;
+
+ DCHECK(start >= 0 && length > 0 && start <= RowCount());
+ SendMessage(list_view_, WM_SETREDRAW, static_cast<WPARAM>(FALSE), 0);
+ UpdateListViewCache(start, length, true);
+ SendMessage(list_view_, WM_SETREDRAW, static_cast<WPARAM>(TRUE), 0);
+}
+
+void TableView::OnItemsRemoved(int start, int length) {
+ if (!list_view_)
+ return;
+
+ 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);
+ 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 {
+ // 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)
+ ListView_DeleteItem(list_view_, start);
+ }
+ }
+
+ SendMessage(list_view_, WM_SETREDRAW, static_cast<WPARAM>(TRUE), 0);
+
+ // If the row count goes to zero and we had a selection LVN_ITEMCHANGED isn't
+ // invoked, so we handle it here.
+ //
+ // When the model is set to NULL all the rows are removed. We don't notify
+ // the delegate in this case as setting the model to NULL is usually done as
+ // the last step before being deleted and callers shouldn't have to deal with
+ // getting a selection change when the model is being reset.
+ if (model_ && table_view_observer_ && had_selection && RowCount() == 0)
+ table_view_observer_->OnSelectionChanged();
+}
+
+void TableView::AddColumn(const TableColumn& col) {
+ DCHECK(all_columns_.count(col.id) == 0);
+ all_columns_[col.id] = col;
+}
+
+void TableView::SetColumns(const std::vector<TableColumn>& columns) {
+ // Remove the currently visible columns.
+ while (!visible_columns_.empty())
+ SetColumnVisibility(visible_columns_.front(), false);
+
+ all_columns_.clear();
+ for (std::vector<TableColumn>::const_iterator i = columns.begin();
+ 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() {
+ column_count_ = static_cast<int>(visible_columns_.size());
+ ResetColumnSizes();
+}
+
+void TableView::SetColumnVisibility(int id, bool is_visible) {
+ bool changed = false;
+ for (std::vector<int>::iterator i = visible_columns_.begin();
+ i != visible_columns_.end(); ++i) {
+ if (*i == id) {
+ if (is_visible) {
+ // It's already visible, bail out early.
+ return;
+ } else {
+ int index = static_cast<int>(i - visible_columns_.begin());
+ // This could be called before the native list view has been created
+ // (in CreateNativeControl, called when the view is added to a
+ // Widget). In that case since the column is not in
+ // visible_columns_ it will not be added later on when it is created.
+ if (list_view_)
+ SendMessage(list_view_, LVM_DELETECOLUMN, index, 0);
+ visible_columns_.erase(i);
+ changed = true;
+ break;
+ }
+ }
+ }
+ if (is_visible) {
+ visible_columns_.push_back(id);
+ TableColumn& column = all_columns_[id];
+ InsertColumn(column, column_count_);
+ if (column.min_visible_width == 0) {
+ // ListView_GetStringWidth must be padded or else truncation will occur.
+ column.min_visible_width = ListView_GetStringWidth(list_view_,
+ column.title.c_str()) +
+ kListViewTextPadding;
+ }
+ changed = true;
+ }
+ if (changed)
+ OnColumnsChanged();
+}
+
+void TableView::SetVisibleColumns(const std::vector<int>& columns) {
+ size_t old_count = visible_columns_.size();
+ size_t new_count = columns.size();
+ // remove the old columns
+ if (list_view_) {
+ for (std::vector<int>::reverse_iterator i = visible_columns_.rbegin();
+ i != visible_columns_.rend(); ++i) {
+ int index = static_cast<int>(i - visible_columns_.rend());
+ SendMessage(list_view_, LVM_DELETECOLUMN, index, 0);
+ }
+ }
+ visible_columns_ = columns;
+ // Insert the new columns.
+ if (list_view_) {
+ for (std::vector<int>::iterator i = visible_columns_.begin();
+ i != visible_columns_.end(); ++i) {
+ int index = static_cast<int>(i - visible_columns_.end());
+ InsertColumn(all_columns_[*i], index);
+ }
+ }
+ OnColumnsChanged();
+}
+
+bool TableView::IsColumnVisible(int id) const {
+ for (std::vector<int>::const_iterator i = visible_columns_.begin();
+ i != visible_columns_.end(); ++i)
+ if (*i == id) {
+ return true;
+ }
+ return false;
+}
+
+const TableColumn& TableView::GetColumnAtPosition(int pos) {
+ return all_columns_[visible_columns_[pos]];
+}
+
+bool TableView::HasColumn(int id) {
+ return all_columns_.count(id) > 0;
+}
+
+gfx::Point TableView::GetKeyboardContextMenuLocation() {
+ int first_selected = FirstSelectedRow();
+ int y = height() / 2;
+ if (first_selected != -1) {
+ RECT cell_bounds;
+ RECT client_rect;
+ if (ListView_GetItemRect(GetNativeControlHWND(), first_selected,
+ &cell_bounds, LVIR_BOUNDS) &&
+ GetClientRect(GetNativeControlHWND(), &client_rect) &&
+ cell_bounds.bottom >= 0 && cell_bounds.bottom < client_rect.bottom) {
+ y = cell_bounds.bottom;
+ }
+ }
+ gfx::Point screen_loc(0, y);
+ if (UILayoutIsRightToLeft())
+ screen_loc.set_x(width());
+ ConvertPointToScreen(this, &screen_loc);
+ return screen_loc;
+}
+
+void TableView::SetCustomColorsEnabled(bool custom_colors_enabled) {
+ custom_colors_enabled_ = custom_colors_enabled;
+}
+
+bool TableView::GetCellColors(int model_row,
+ int column,
+ ItemColor* foreground,
+ ItemColor* background,
+ LOGFONT* logfont) {
+ return false;
+}
+
+static int GetViewIndexFromMouseEvent(HWND window, LPARAM l_param) {
+ int x = GET_X_LPARAM(l_param);
+ int y = GET_Y_LPARAM(l_param);
+ LVHITTESTINFO hit_info = {0};
+ hit_info.pt.x = x;
+ hit_info.pt.y = y;
+ return ListView_HitTest(window, &hit_info);
+}
+
+// static
+LRESULT CALLBACK TableView::TableWndProc(HWND window,
+ UINT message,
+ WPARAM w_param,
+ LPARAM l_param) {
+ TableView* table_view = reinterpret_cast<TableViewWrapper*>(
+ GetWindowLongPtr(window, GWLP_USERDATA))->table_view;
+
+ // Is the mouse down on the table?
+ static bool in_mouse_down = false;
+ // Should we select on mouse up?
+ static bool select_on_mouse_up = false;
+
+ // If the mouse is down, this is the location of the mouse down message.
+ static int mouse_down_x, mouse_down_y;
+
+ switch (message) {
+ case WM_CONTEXTMENU: {
+ // This addresses two problems seen with context menus in right to left
+ // locales:
+ // 1. The mouse coordinates in the l_param were occasionally wrong in
+ // weird ways. This is most often seen when right clicking on the
+ // list-view twice in a row.
+ // 2. Right clicking on the icon would show the scrollbar menu.
+ //
+ // As a work around this uses the position of the cursor and ignores
+ // the position supplied in the l_param.
+ if (table_view->UILayoutIsRightToLeft() &&
+ (GET_X_LPARAM(l_param) != -1 || GET_Y_LPARAM(l_param) != -1)) {
+ CPoint screen_point;
+ GetCursorPos(&screen_point);
+ CPoint table_point = screen_point;
+ CRect client_rect;
+ if (ScreenToClient(window, &table_point) &&
+ GetClientRect(window, &client_rect) &&
+ client_rect.PtInRect(table_point)) {
+ // The point is over the client area of the table, handle it ourself.
+ // But first select the row if it isn't already selected.
+ LVHITTESTINFO hit_info = {0};
+ hit_info.pt.x = table_point.x;
+ hit_info.pt.y = table_point.y;
+ int view_index = ListView_HitTest(window, &hit_info);
+ if (view_index != -1) {
+ int model_index = table_view->view_to_model(view_index);
+ if (!table_view->IsItemSelected(model_index))
+ table_view->Select(model_index);
+ }
+ table_view->OnContextMenu(screen_point);
+ return 0; // So that default processing doesn't occur.
+ }
+ }
+ // else case: default handling is fine, so break and let the default
+ // handler service the request (which will likely calls us back with
+ // OnContextMenu).
+ break;
+ }
+
+ case WM_CANCELMODE: {
+ if (in_mouse_down) {
+ in_mouse_down = false;
+ return 0;
+ }
+ break;
+ }
+
+ case WM_ERASEBKGND:
+ // We make WM_ERASEBKGND do nothing (returning 1 indicates we handled
+ // the request). We do this so that the table view doesn't flicker during
+ // resizing.
+ return 1;
+
+ case WM_PAINT: {
+ LRESULT result = CallWindowProc(table_view->original_handler_, window,
+ message, w_param, l_param);
+ table_view->PostPaint();
+ return result;
+ }
+
+ case WM_KEYDOWN: {
+ if (!table_view->single_selection_ && w_param == 'A' &&
+ GetKeyState(VK_CONTROL) < 0 && table_view->RowCount() > 0) {
+ // Select everything.
+ ListView_SetItemState(window, -1, LVIS_SELECTED, LVIS_SELECTED);
+ // And make the first row focused.
+ ListView_SetItemState(window, 0, LVIS_FOCUSED, LVIS_FOCUSED);
+ return 0;
+ } else if (w_param == VK_DELETE && table_view->table_view_observer_) {
+ table_view->table_view_observer_->OnTableViewDelete(table_view);
+ return 0;
+ }
+ // else case: fall through to default processing.
+ break;
+ }
+
+ case WM_LBUTTONDBLCLK: {
+ if (w_param == MK_LBUTTON)
+ table_view->OnDoubleClick();
+ return 0;
+ }
+
+ case WM_LBUTTONUP: {
+ if (in_mouse_down) {
+ in_mouse_down = false;
+ ReleaseCapture();
+ SetFocus(window);
+ if (select_on_mouse_up) {
+ int view_index = GetViewIndexFromMouseEvent(window, l_param);
+ if (view_index != -1)
+ table_view->Select(table_view->view_to_model(view_index));
+ }
+ return 0;
+ }
+ break;
+ }
+
+ case WM_LBUTTONDOWN: {
+ // ListView treats clicking on an area outside the text of a column as
+ // drag to select. This is confusing when the selection is shown across
+ // the whole row. For this reason we override the default handling for
+ // mouse down/move/up and treat the whole row as draggable. That is, no
+ // matter where you click in the row we'll attempt to start dragging.
+ //
+ // Only do custom mouse handling if no other mouse buttons are down.
+ if ((w_param | (MK_LBUTTON | MK_CONTROL | MK_SHIFT)) ==
+ (MK_LBUTTON | MK_CONTROL | MK_SHIFT)) {
+ if (in_mouse_down)
+ return 0;
+
+ int view_index = GetViewIndexFromMouseEvent(window, l_param);
+ if (view_index != -1) {
+ table_view->ignore_listview_change_ = true;
+ in_mouse_down = true;
+ select_on_mouse_up = false;
+ mouse_down_x = GET_X_LPARAM(l_param);
+ mouse_down_y = GET_Y_LPARAM(l_param);
+ int model_index = table_view->view_to_model(view_index);
+ bool select = true;
+ if (w_param & MK_CONTROL) {
+ select = false;
+ if (!table_view->IsItemSelected(model_index)) {
+ if (table_view->single_selection_) {
+ // Single selection mode and the row isn't selected, select
+ // only it.
+ table_view->Select(model_index);
+ } else {
+ // Not single selection, add this row to the selection.
+ table_view->SetSelectedState(model_index, true);
+ }
+ } else {
+ // Remove this row from the selection.
+ table_view->SetSelectedState(model_index, false);
+ }
+ ListView_SetSelectionMark(window, view_index);
+ } else if (!table_view->single_selection_ && w_param & MK_SHIFT) {
+ int mark_view_index = ListView_GetSelectionMark(window);
+ if (mark_view_index != -1) {
+ // Unselect everything.
+ ListView_SetItemState(window, -1, 0, LVIS_SELECTED);
+ select = false;
+
+ // Select from mark to mouse down location.
+ for (int i = std::min(view_index, mark_view_index),
+ max_i = std::max(view_index, mark_view_index); i <= max_i;
+ ++i) {
+ table_view->SetSelectedState(table_view->view_to_model(i),
+ true);
+ }
+ }
+ }
+ // Make the row the user clicked on the focused row.
+ ListView_SetItemState(window, view_index, LVIS_FOCUSED,
+ LVIS_FOCUSED);
+ if (select) {
+ if (!table_view->IsItemSelected(model_index)) {
+ // Clear all.
+ ListView_SetItemState(window, -1, 0, LVIS_SELECTED);
+ // And select the row the user clicked on.
+ table_view->SetSelectedState(model_index, true);
+ } else {
+ // The item is already selected, don't clear the state right away
+ // in case the user drags. Instead wait for mouse up, then only
+ // select the row the user clicked on.
+ select_on_mouse_up = true;
+ }
+ ListView_SetSelectionMark(window, view_index);
+ }
+ table_view->ignore_listview_change_ = false;
+ table_view->OnSelectedStateChanged();
+ SetCapture(window);
+ return 0;
+ }
+ // else case, continue on to default handler
+ }
+ break;
+ }
+
+ case WM_MOUSEMOVE: {
+ if (in_mouse_down) {
+ int x = GET_X_LPARAM(l_param);
+ int y = GET_Y_LPARAM(l_param);
+ if (View::ExceededDragThreshold(x - mouse_down_x, y - mouse_down_y)) {
+ // We're about to start drag and drop, which results in no mouse up.
+ // Release capture and reset state.
+ ReleaseCapture();
+ in_mouse_down = false;
+
+ NMLISTVIEW details;
+ memset(&details, 0, sizeof(details));
+ details.hdr.code = LVN_BEGINDRAG;
+ SendMessage(::GetParent(window), WM_NOTIFY, 0,
+ reinterpret_cast<LPARAM>(&details));
+ }
+ return 0;
+ }
+ break;
+ }
+
+ default:
+ break;
+ }
+ DCHECK(table_view->original_handler_);
+ return CallWindowProc(table_view->original_handler_, window, message, w_param,
+ l_param);
+}
+
+LRESULT CALLBACK TableView::TableHeaderWndProc(HWND window, UINT message,
+ WPARAM w_param, LPARAM l_param) {
+ TableView* table_view = reinterpret_cast<TableViewWrapper*>(
+ GetWindowLongPtr(window, GWLP_USERDATA))->table_view;
+
+ switch (message) {
+ case WM_SETCURSOR:
+ if (!table_view->resizable_columns_)
+ // Prevents the cursor from changing to the resize cursor.
+ return TRUE;
+ break;
+ case WM_LBUTTONDBLCLK:
+ if (!table_view->resizable_columns_)
+ // Prevents the double-click on the column separator from auto-resizing
+ // the column.
+ return TRUE;
+ break;
+ default:
+ break;
+ }
+ DCHECK(table_view->header_original_handler_);
+ return CallWindowProc(table_view->header_original_handler_,
+ window, message, w_param, l_param);
+}
+
+HWND TableView::CreateNativeControl(HWND parent_container) {
+ int style = WS_CHILD | LVS_REPORT | LVS_SHOWSELALWAYS;
+ if (single_selection_)
+ style |= LVS_SINGLESEL;
+ // If there's only one column and the title string is empty, don't show a
+ // header.
+ if (all_columns_.size() == 1) {
+ std::map<int, TableColumn>::const_iterator first =
+ all_columns_.begin();
+ if (first->second.title.empty())
+ style |= LVS_NOCOLUMNHEADER;
+ }
+ list_view_ = ::CreateWindowEx(WS_EX_CLIENTEDGE | GetAdditionalRTLStyle(),
+ WC_LISTVIEW,
+ L"",
+ style,
+ 0, 0, width(), height(),
+ parent_container, NULL, NULL, NULL);
+
+ // Make the selection extend across the row.
+ // Reduce overdraw/flicker artifacts by double buffering.
+ DWORD list_view_style = LVS_EX_FULLROWSELECT;
+ if (win_util::GetWinVersion() > win_util::WINVERSION_2000) {
+ list_view_style |= LVS_EX_DOUBLEBUFFER;
+ }
+ if (table_type_ == CHECK_BOX_AND_TEXT)
+ list_view_style |= LVS_EX_CHECKBOXES;
+ ListView_SetExtendedListViewStyleEx(list_view_, 0, list_view_style);
+ l10n_util::AdjustUIFontForWindow(list_view_);
+
+ // Add the columns.
+ for (std::vector<int>::iterator i = visible_columns_.begin();
+ i != visible_columns_.end(); ++i) {
+ InsertColumn(all_columns_[*i],
+ static_cast<int>(i - visible_columns_.begin()));
+ }
+
+ if (model_)
+ model_->SetObserver(this);
+
+ // Add the groups.
+ if (model_ && model_->HasGroups() &&
+ win_util::GetWinVersion() > win_util::WINVERSION_2000) {
+ ListView_EnableGroupView(list_view_, true);
+
+ TableModel::Groups groups = model_->GetGroups();
+ LVGROUP group = { 0 };
+ group.cbSize = sizeof(LVGROUP);
+ group.mask = LVGF_HEADER | LVGF_ALIGN | LVGF_GROUPID;
+ group.uAlign = LVGA_HEADER_LEFT;
+ for (size_t i = 0; i < groups.size(); ++i) {
+ group.pszHeader = const_cast<wchar_t*>(groups[i].title.c_str());
+ group.iGroupId = groups[i].id;
+ ListView_InsertGroup(list_view_, static_cast<int>(i), &group);
+ }
+ }
+
+ // Set the # of rows.
+ if (model_)
+ UpdateListViewCache(0, model_->RowCount(), true);
+
+ if (table_type_ == ICON_AND_TEXT) {
+ 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).
+ 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);
+ }
+
+ if (!resizable_columns_) {
+ // To disable the resizing of columns we'll filter the events happening on
+ // the header. We also need to intercept the HDM_LAYOUT to size the header
+ // for the Chrome headers.
+ HWND header = ListView_GetHeader(list_view_);
+ DCHECK(header);
+ SetWindowLongPtr(header, GWLP_USERDATA,
+ reinterpret_cast<LONG_PTR>(&table_view_wrapper_));
+ header_original_handler_ = win_util::SetWindowProc(header,
+ &TableView::TableHeaderWndProc);
+ }
+
+ SetWindowLongPtr(list_view_, GWLP_USERDATA,
+ reinterpret_cast<LONG_PTR>(&table_view_wrapper_));
+ original_handler_ =
+ win_util::SetWindowProc(list_view_, &TableView::TableWndProc);
+
+ // Bug 964884: detach the IME attached to this window.
+ // We should attach IMEs only when we need to input CJK strings.
+ ::ImmAssociateContextEx(list_view_, NULL, 0);
+
+ UpdateContentOffset();
+
+ 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;
+
+ LVCOLUMN column = { 0 };
+ column.mask = LVCF_TEXT|LVCF_FMT;
+ column.pszText = const_cast<LPWSTR>(tc.title.c_str());
+ switch (tc.alignment) {
+ case TableColumn::LEFT:
+ column.fmt = LVCFMT_LEFT;
+ break;
+ case TableColumn::RIGHT:
+ column.fmt = LVCFMT_RIGHT;
+ break;
+ case TableColumn::CENTER:
+ column.fmt = LVCFMT_CENTER;
+ break;
+ default:
+ NOTREACHED();
+ }
+ if (tc.width != -1) {
+ column.mask |= LVCF_WIDTH;
+ column.cx = tc.width;
+ }
+ column.mask |= LVCF_SUBITEM;
+ // Sub-items are 1s indexed.
+ 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, LPNMHDR hdr) {
+ if (!model_)
+ return 0;
+
+ switch (hdr->code) {
+ case NM_CUSTOMDRAW: {
+ // 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.
+ NMLISTVIEW* state_change = reinterpret_cast<NMLISTVIEW*>(hdr);
+ if ((state_change->uChanged & LVIF_STATE) != 0) {
+ if ((state_change->uOldState & LVIS_SELECTED) !=
+ (state_change->uNewState & LVIS_SELECTED)) {
+ // Selected state of the item changed.
+ OnSelectedStateChanged();
+ }
+ if ((state_change->uOldState & LVIS_STATEIMAGEMASK) !=
+ (state_change->uNewState & LVIS_STATEIMAGEMASK)) {
+ // Checked state of the item changed.
+ bool is_checked =
+ ((state_change->uNewState & LVIS_STATEIMAGEMASK) ==
+ INDEXTOSTATEIMAGEMASK(2));
+ 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;
+
+ // If we see a key down message, we need to invoke the OnKeyDown handler
+ // in order to give our class (or any subclass) and opportunity to perform
+ // a key down triggered action, if such action is necessary.
+ case LVN_KEYDOWN: {
+ NMLVKEYDOWN* key_down_message = reinterpret_cast<NMLVKEYDOWN*>(hdr);
+ OnKeyDown(key_down_message->wVKey);
+ break;
+ }
+
+ case LVN_COLUMNCLICK: {
+ const TableColumn& column = GetColumnAtPosition(
+ reinterpret_cast<NMLISTVIEW*>(hdr)->iSubItem);
+ if (column.sortable)
+ ToggleSortOrder(column.id);
+ break;
+ }
+
+ case LVN_MARQUEEBEGIN: // We don't want the marque selection.
+ return 1;
+
+ default:
+ break;
+ }
+ return 0;
+}
+
+void TableView::OnDestroy() {
+ if (table_type_ == ICON_AND_TEXT) {
+ HIMAGELIST image_list =
+ 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);
+}
+
+int TableView::GetColumnWidth(int column_id) {
+ if (!list_view_)
+ return -1;
+
+ std::vector<int>::const_iterator i =
+ std::find(visible_columns_.begin(), visible_columns_.end(), column_id);
+ if (i == visible_columns_.end())
+ return -1;
+
+ return ListView_GetColumnWidth(
+ list_view_, static_cast<int>(i - visible_columns_.begin()));
+}
+
+LRESULT TableView::OnCustomDraw(NMLVCUSTOMDRAW* draw_info) {
+ switch (draw_info->nmcd.dwDrawStage) {
+ case CDDS_PREPAINT: {
+ return CDRF_NOTIFYITEMDRAW;
+ }
+ case CDDS_ITEMPREPAINT: {
+ // The list-view is about to paint an item, tell it we want to
+ // notified when it paints every subitem.
+ LRESULT r = CDRF_NOTIFYSUBITEMDRAW;
+ if (table_type_ == ICON_AND_TEXT)
+ r |= CDRF_NOTIFYPOSTPAINT;
+ return r;
+ }
+ case (CDDS_ITEMPREPAINT | CDDS_SUBITEM): {
+ // The list-view is painting a subitem. See if the colors should be
+ // changed from the default.
+ if (custom_colors_enabled_) {
+ // At this time, draw_info->clrText and draw_info->clrTextBk are not
+ // set. So we pass in an ItemColor to GetCellColors. If
+ // ItemColor.color_is_set is true, then we use the provided color.
+ ItemColor foreground = {0};
+ ItemColor background = {0};
+
+ LOGFONT logfont;
+ GetObject(GetWindowFont(list_view_), sizeof(logfont), &logfont);
+
+ if (GetCellColors(view_to_model(
+ static_cast<int>(draw_info->nmcd.dwItemSpec)),
+ draw_info->iSubItem,
+ &foreground,
+ &background,
+ &logfont)) {
+ // TODO(tc): Creating/deleting a font for every cell seems like a
+ // waste if the font hasn't changed. Maybe we should use a struct
+ // with a bool like we do with colors?
+ if (custom_cell_font_)
+ DeleteObject(custom_cell_font_);
+ l10n_util::AdjustUIFont(&logfont);
+ custom_cell_font_ = CreateFontIndirect(&logfont);
+ SelectObject(draw_info->nmcd.hdc, custom_cell_font_);
+ draw_info->clrText = foreground.color_is_set
+ ? skia::SkColorToCOLORREF(foreground.color)
+ : CLR_DEFAULT;
+ draw_info->clrTextBk = background.color_is_set
+ ? skia::SkColorToCOLORREF(background.color)
+ : CLR_DEFAULT;
+ return CDRF_NEWFONT;
+ }
+ }
+ return CDRF_DODEFAULT;
+ }
+ case CDDS_ITEMPOSTPAINT: {
+ DCHECK((table_type_ == ICON_AND_TEXT) || (ImplementPostPaint()));
+ int view_index = static_cast<int>(draw_info->nmcd.dwItemSpec);
+ // We get notifications for empty items, just ignore them.
+ 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(model_index);
+ if (!image.isNull()) {
+ // Get the rect that holds the icon.
+ CRect icon_rect, client_rect;
+ if (ListView_GetItemRect(list_view_, view_index, &icon_rect,
+ LVIR_ICON) &&
+ GetClientRect(list_view_, &client_rect)) {
+ CRect intersection;
+ // Client rect includes the header but we need to make sure we don't
+ // paint into it.
+ client_rect.top += content_offset_;
+ // Make sure the region need to paint is visible.
+ if (intersection.IntersectRect(&icon_rect, &client_rect)) {
+ ChromeCanvas canvas(icon_rect.Width(), icon_rect.Height(), false);
+
+ // It seems the state in nmcd.uItemState is not correct.
+ // We'll retrieve it explicitly.
+ int selected = ListView_GetItemState(
+ list_view_, view_index, LVIS_SELECTED | LVIS_DROPHILITED);
+ bool drop_highlight = ((selected & LVIS_DROPHILITED) != 0);
+ int bg_color_index;
+ if (!IsEnabled())
+ bg_color_index = COLOR_3DFACE;
+ else if (drop_highlight)
+ bg_color_index = COLOR_HIGHLIGHT;
+ else if (selected)
+ bg_color_index = HasFocus() ? COLOR_HIGHLIGHT : COLOR_3DFACE;
+ else
+ bg_color_index = COLOR_WINDOW;
+ // NOTE: This may be invoked without the ListView filling in the
+ // background (or rather windows paints background, then invokes
+ // this twice). As such, we always fill in the background.
+ canvas.drawColor(
+ skia::COLORREFToSkColor(GetSysColor(bg_color_index)),
+ SkPorterDuff::kSrc_Mode);
+ // + 1 for padding (we declared the image as 18x18 in the list-
+ // view when they are 16x16 so we get an extra pixel of padding).
+ canvas.DrawBitmapInt(image, 0, 0,
+ image.width(), image.height(),
+ 1, 1, kFavIconSize, kFavIconSize, true);
+
+ // Only paint the visible region of the icon.
+ RECT to_draw = { intersection.left - icon_rect.left,
+ intersection.top - icon_rect.top,
+ 0, 0 };
+ to_draw.right = to_draw.left +
+ (intersection.right - intersection.left);
+ to_draw.bottom = to_draw.top +
+ (intersection.bottom - intersection.top);
+ canvas.getTopPlatformDevice().drawToHDC(draw_info->nmcd.hdc,
+ intersection.left,
+ intersection.top,
+ &to_draw);
+ r = CDRF_SKIPDEFAULT;
+ }
+ }
+ }
+ }
+ if (ImplementPostPaint()) {
+ CRect cell_rect;
+ 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;
+ }
+ }
+ return r;
+ }
+ default:
+ return CDRF_DODEFAULT;
+ }
+}
+
+void TableView::UpdateListViewCache(int start, int length, bool add) {
+ ignore_listview_change_ = true;
+ UpdateListViewCache0(start, length, add);
+ ignore_listview_change_ = false;
+}
+
+void TableView::ResetColumnSizes() {
+ if (!list_view_)
+ return;
+
+ // See comment in TableColumn for what this does.
+ int width = this->width();
+ CRect native_bounds;
+ if (GetClientRect(GetNativeControlHWND(), &native_bounds) &&
+ native_bounds.Width() > 0) {
+ // Prefer the bounds of the window over our bounds, which may be different.
+ width = native_bounds.Width();
+ }
+
+ float percent = 0;
+ int fixed_width = 0;
+ int autosize_width = 0;
+
+ for (std::vector<int>::const_iterator i = visible_columns_.begin();
+ i != visible_columns_.end(); ++i) {
+ TableColumn& col = all_columns_[*i];
+ int col_index = static_cast<int>(i - visible_columns_.begin());
+ if (col.width == -1) {
+ if (col.percent > 0) {
+ percent += col.percent;
+ } else {
+ autosize_width += col.min_visible_width;
+ }
+ } else {
+ fixed_width += ListView_GetColumnWidth(list_view_, col_index);
+ }
+ }
+
+ // Now do a pass to set the actual sizes of auto-sized and
+ // percent-sized columns.
+ int available_width = width - fixed_width - autosize_width;
+ for (std::vector<int>::const_iterator i = visible_columns_.begin();
+ i != visible_columns_.end(); ++i) {
+ TableColumn& col = all_columns_[*i];
+ if (col.width == -1) {
+ int col_index = static_cast<int>(i - visible_columns_.begin());
+ if (col.percent > 0) {
+ if (available_width > 0) {
+ int col_width =
+ static_cast<int>(available_width * (col.percent / percent));
+ available_width -= col_width;
+ percent -= col.percent;
+ ListView_SetColumnWidth(list_view_, col_index, col_width);
+ }
+ } else {
+ int col_width = col.min_visible_width;
+ // If no "percent" columns, the last column acts as one, if auto-sized.
+ if (percent == 0.f && available_width > 0 &&
+ col_index == column_count_ - 1) {
+ col_width += available_width;
+ }
+ ListView_SetColumnWidth(list_view_, col_index, col_width);
+ }
+ }
+ }
+}
+
+gfx::Size TableView::GetPreferredSize() {
+ return 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 (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);
+ }
+ }
+
+ memset(&item, 0, sizeof(LVITEM));
+
+ // NOTE: I don't quite get why the iSubItem in the following is not offset
+ // by 1. According to the docs it should be offset by one, but that doesn't
+ // work.
+ if (table_type_ == CHECK_BOX_AND_TEXT) {
+ start_column = 1;
+ item.iSubItem = 0;
+ item.mask = LVIF_TEXT | LVIF_STATE;
+ item.stateMask = LVIS_STATEIMAGEMASK;
+ for (int i = start; i < max_row; ++i) {
+ std::wstring text = model_->GetText(i, visible_columns_[0]);
+ 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);
+ }
+ }
+
+ item.stateMask = 0;
+ item.mask = LVIF_TEXT;
+ if (table_type_ == ICON_AND_TEXT) {
+ item.mask |= LVIF_IMAGE;
+ }
+ for (int j = start_column; j < column_count_; ++j) {
+ 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 = 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());
+ item.iImage = 0;
+ ListView_SetItem(list_view_, &item);
+
+ // Compute width in px, using current font.
+ int string_width = ListView_GetStringWidth(list_view_, item.pszText);
+ // The width of an icon belongs to the first column.
+ if (j == 0 && table_type_ == ICON_AND_TEXT)
+ string_width += kListViewIconWidthAndPadding;
+ max_text_width = std::max(string_width, max_text_width);
+ }
+
+ // ListView_GetStringWidth must be padded or else truncation will occur
+ // (MSDN). 15px matches the Win32/LVSCW_AUTOSIZE_USEHEADER behavior.
+ max_text_width += kListViewTextPadding;
+
+ // Protect against partial update.
+ if (max_text_width > col.min_visible_width ||
+ (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() {
+ if (!ignore_listview_change_ && table_view_observer_) {
+ table_view_observer_->OnDoubleClick();
+ }
+}
+
+void TableView::OnSelectedStateChanged() {
+ if (!ignore_listview_change_ && table_view_observer_) {
+ table_view_observer_->OnSelectionChanged();
+ }
+}
+
+void TableView::OnKeyDown(unsigned short virtual_keycode) {
+ if (!ignore_listview_change_ && table_view_observer_) {
+ table_view_observer_->OnKeyDown(virtual_keycode);
+ }
+}
+
+void TableView::OnCheckedStateChanged(int model_row, bool is_checked) {
+ if (!ignore_listview_change_)
+ model_->SetChecked(model_row, is_checked);
+}
+
+int TableView::PreviousSelectedViewIndex(int view_index) {
+ DCHECK(view_index >= 0);
+ if (!list_view_ || view_index <= 0)
+ return -1;
+
+ int row_count = RowCount();
+ if (row_count == 0)
+ return -1; // Empty table, nothing can be selected.
+
+ // For some reason
+ // ListView_GetNextItem(list_view_,item, LVNI_SELECTED | LVNI_ABOVE)
+ // fails on Vista (always returns -1), so we iterate through the indices.
+ view_index = std::min(view_index, row_count);
+ while (--view_index >= 0 && !IsItemSelected(view_to_model(view_index)));
+ return view_index;
+}
+
+int TableView::LastSelectedViewIndex() {
+ return PreviousSelectedViewIndex(RowCount());
+}
+
+void TableView::UpdateContentOffset() {
+ content_offset_ = 0;
+
+ if (!list_view_)
+ return;
+
+ HWND header = ListView_GetHeader(list_view_);
+ if (!header)
+ return;
+
+ POINT origin = {0, 0};
+ MapWindowPoints(header, list_view_, &origin, 1);
+
+ CRect header_bounds;
+ GetWindowRect(header, &header_bounds);
+
+ content_offset_ = origin.y + header_bounds.Height();
+}
+
+//
+// TableSelectionIterator
+//
+TableSelectionIterator::TableSelectionIterator(TableView* view,
+ int view_index)
+ : table_view_(view),
+ view_index_(view_index) {
+ UpdateModelIndexFromViewIndex();
+}
+
+TableSelectionIterator& TableSelectionIterator::operator=(
+ const TableSelectionIterator& other) {
+ view_index_ = other.view_index_;
+ model_index_ = other.model_index_;
+ return *this;
+}
+
+bool TableSelectionIterator::operator==(const TableSelectionIterator& other) {
+ return (other.view_index_ == view_index_);
+}
+
+bool TableSelectionIterator::operator!=(const TableSelectionIterator& other) {
+ return (other.view_index_ != view_index_);
+}
+
+TableSelectionIterator& TableSelectionIterator::operator++() {
+ view_index_ = table_view_->PreviousSelectedViewIndex(view_index_);
+ UpdateModelIndexFromViewIndex();
+ return *this;
+}
+
+int TableSelectionIterator::operator*() {
+ return model_index_;
+}
+
+void TableSelectionIterator::UpdateModelIndexFromViewIndex() {
+ if (view_index_ == -1)
+ model_index_ = -1;
+ else
+ model_index_ = table_view_->view_to_model(view_index_);
+}
+
+} // namespace views
diff --git a/views/controls/table/table_view.h b/views/controls/table/table_view.h
new file mode 100644
index 0000000..8061d27
--- /dev/null
+++ b/views/controls/table/table_view.h
@@ -0,0 +1,676 @@
+// 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.
+
+#ifndef VIEWS_CONTROLS_TABLE_TABLE_VIEW_H_
+#define VIEWS_CONTROLS_TABLE_TABLE_VIEW_H_
+
+#include "build/build_config.h"
+
+#if defined(OS_WIN)
+#include <windows.h>
+#endif // defined(OS_WIN)
+
+#include <map>
+#include <unicode/coll.h>
+#include <unicode/uchar.h>
+#include <vector>
+
+#include "app/l10n_util.h"
+#include "base/logging.h"
+#include "skia/include/SkColor.h"
+#if defined(OS_WIN)
+// TODO(port): remove the ifdef when native_control.h is ported.
+#include "views/controls/native_control.h"
+#endif // defined(OS_WIN)
+
+class SkBitmap;
+
+// A TableView is a view that displays multiple rows with any number of columns.
+// TableView is driven by a TableModel. The model returns the contents
+// 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 views {
+
+class ListView;
+class ListViewParent;
+class TableView;
+struct TableColumn;
+
+// The cells in the first column of a table can contain:
+// - only text
+// - a small icon (16x16) and some text
+// - a check box and some text
+enum TableTypes {
+ TEXT_ONLY = 0,
+ ICON_AND_TEXT,
+ CHECK_BOX_AND_TEXT
+};
+
+// Any time the TableModel changes, it must notify its observer.
+class TableModelObserver {
+ public:
+ // Invoked when the model has been completely changed.
+ virtual void OnModelChanged() = 0;
+
+ // Invoked when a range of items has changed.
+ virtual void OnItemsChanged(int start, int length) = 0;
+
+ // Invoked when new items are added.
+ virtual void OnItemsAdded(int start, int length) = 0;
+
+ // Invoked when a range of items has been removed.
+ virtual void OnItemsRemoved(int start, int length) = 0;
+};
+
+// The model driving the TableView.
+class TableModel {
+ public:
+ // See HasGroups, get GetGroupID for details as to how this is used.
+ struct Group {
+ // The title text for the group.
+ std::wstring title;
+
+ // Unique id for the group.
+ int id;
+ };
+ typedef std::vector<Group> Groups;
+
+ // Number of rows in the model.
+ virtual int RowCount() = 0;
+
+ // Returns the value at a particular location in text.
+ virtual std::wstring GetText(int row, int column_id) = 0;
+
+ // Returns the small icon (16x16) that should be displayed in the first
+ // column before the text. This is only used when the TableView was created
+ // with the ICON_AND_TEXT table type. Returns an isNull() bitmap if there is
+ // no bitmap.
+ virtual SkBitmap GetIcon(int row);
+
+ // Sets whether a particular row is checked. This is only invoked
+ // if the TableView was created with show_check_in_first_column true.
+ virtual void SetChecked(int row, bool is_checked) {
+ NOTREACHED();
+ }
+
+ // Returns whether a particular row is checked. This is only invoked
+ // if the TableView was created with show_check_in_first_column true.
+ virtual bool IsChecked(int row) {
+ return false;
+ }
+
+ // Returns true if the TableView has groups. Groups provide a way to visually
+ // delineate the rows in a table view. When groups are enabled table view
+ // shows a visual separator for each group, followed by all the rows in
+ // the group.
+ //
+ // On win2k a visual separator is not rendered for the group headers.
+ virtual bool HasGroups() { return false; }
+
+ // Returns the groups.
+ // This is only used if HasGroups returns true.
+ virtual Groups GetGroups() {
+ // If you override HasGroups to return true, you must override this as
+ // well.
+ NOTREACHED();
+ return std::vector<Group>();
+ }
+
+ // Returns the group id of the specified row.
+ // This is only used if HasGroups returns true.
+ virtual int GetGroupID(int row) {
+ // If you override HasGroups to return true, you must override this as
+ // well.
+ NOTREACHED();
+ return 0;
+ }
+
+ // 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);
+
+ protected:
+ // Returns the collator used by CompareValues.
+ Collator* GetCollator();
+};
+
+// TableColumn specifies the title, alignment and size of a particular column.
+struct TableColumn {
+ enum Alignment {
+ LEFT, RIGHT, CENTER
+ };
+
+ TableColumn()
+ : id(0),
+ title(),
+ alignment(LEFT),
+ width(-1),
+ percent(),
+ min_visible_width(0),
+ sortable(false) {
+ }
+ TableColumn(int id, const std::wstring title, Alignment alignment, int width)
+ : id(id),
+ title(title),
+ alignment(alignment),
+ width(width),
+ percent(0),
+ min_visible_width(0),
+ sortable(false) {
+ }
+ TableColumn(int id, const std::wstring title, Alignment alignment, int width,
+ float percent)
+ : id(id),
+ title(title),
+ alignment(alignment),
+ width(width),
+ percent(percent),
+ 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
+ // bahalf of the caller.
+ TableColumn(int id, Alignment alignment, int width)
+ : id(id),
+ alignment(alignment),
+ width(width),
+ percent(0),
+ min_visible_width(0),
+ sortable(false) {
+ title = l10n_util::GetString(id);
+ }
+ TableColumn(int id, Alignment alignment, int width, float percent)
+ : id(id),
+ alignment(alignment),
+ width(width),
+ percent(percent),
+ min_visible_width(0),
+ sortable(false) {
+ title = l10n_util::GetString(id);
+ }
+
+ // A unique identifier for the column.
+ int id;
+
+ // The title for the column.
+ std::wstring title;
+
+ // Alignment for the content.
+ Alignment alignment;
+
+ // The size of a column may be specified in two ways:
+ // 1. A fixed width. Set the width field to a positive number and the
+ // column will be given that width, in pixels.
+ // 2. As a percentage of the available width. If width is -1, and percent is
+ // > 0, the column is given a width of
+ // available_width * percent / total_percent.
+ // 3. If the width == -1 and percent == 0, the column is autosized based on
+ // the width of the column header text.
+ //
+ // Sizing is done in four passes. Fixed width columns are given
+ // their width, percentages are applied, autosized columns are autosized,
+ // and finally percentages are applied again taking into account the widths
+ // of autosized columns.
+ int width;
+ float percent;
+
+ // The minimum width required for all items in this column
+ // (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 view_index);
+ TableSelectionIterator& operator=(const TableSelectionIterator& other);
+ bool operator==(const TableSelectionIterator& other);
+ bool operator!=(const TableSelectionIterator& other);
+ TableSelectionIterator& operator++();
+ int operator*();
+
+ private:
+ void UpdateModelIndexFromViewIndex();
+
+ TableView* table_view_;
+ 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.
+class TableViewObserver {
+ public:
+ virtual ~TableViewObserver() {}
+
+ // Invoked when the selection changes.
+ virtual void OnSelectionChanged() = 0;
+
+ // Optional method invoked when the user double clicks on the table.
+ virtual void OnDoubleClick() {}
+
+ // Optional method invoked when the user hits a key with the table in focus.
+ virtual void OnKeyDown(unsigned short virtual_keycode) {}
+
+ // Invoked when the user presses the delete key.
+ virtual void OnTableViewDelete(TableView* table_view) {}
+};
+
+#if defined(OS_WIN)
+// TODO(port): Port TableView.
+class TableView : public NativeControl,
+ public TableModelObserver {
+ public:
+ typedef TableSelectionIterator iterator;
+
+ // A helper struct for GetCellColors. Set |color_is_set| to true if color is
+ // set. See OnCustomDraw for more details on why we need this.
+ struct ItemColor {
+ bool color_is_set;
+ 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).
+ // When autosize_columns is true, columns always fill the available width. If
+ // false, columns are not resized when the table is resized. An extra empty
+ // column at the right fills the remaining space.
+ // When resizable_columns is true, users can resize columns by dragging the
+ // separator on the column header. NOTE: Right now this is always true. The
+ // code to set it false is still in place to be a base for future, better
+ // resizing behavior (see http://b/issue?id=874646 ), but no one uses or
+ // tests the case where this flag is false.
+ // Note that setting both resizable_columns and autosize_columns to false is
+ // probably not a good idea, as there is no way for the user to increase a
+ // column's size in that case.
+ TableView(TableModel* model, const std::vector<TableColumn>& columns,
+ TableTypes table_type, bool single_selection,
+ bool resizable_columns, bool autosize_columns);
+ virtual ~TableView();
+
+ // Assigns a new model to the table view, detaching the old one if present.
+ // If |model| is NULL, the table view cannot be used after this call. This
+ // should be called in the containing view's destructor to avoid destruction
+ // issues when the model needs to be deleted before the table.
+ void SetModel(TableModel* model);
+ TableModel* model() const { return model_; }
+
+ // Resorts the contents.
+ void SetSortDescriptors(const SortDescriptors& sort_descriptors);
+
+ // Current sort.
+ const SortDescriptors& sort_descriptors() const { return sort_descriptors_; }
+
+ void DidChangeBounds(const gfx::Rect& previous,
+ const gfx::Rect& current);
+
+ // Returns the number of rows in the TableView.
+ int RowCount();
+
+ // Returns the number of selected rows.
+ int SelectedRowCount();
+
+ // Selects the specified item, making sure it's visible.
+ 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 model_row, bool state);
+
+ // Sets the focus to the item at the given index.
+ void SetFocusOnItem(int model_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 model_row);
+
+ // Returns true if the item at the specified index has the focus.
+ bool ItemHasTheFocus(int model_row);
+
+ // Returns an iterator over the selection. The iterator proceeds from the
+ // 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();
+
+ // TableModelObserver methods.
+ virtual void OnModelChanged();
+ virtual void OnItemsChanged(int start, int length);
+ virtual void OnItemsAdded(int start, int length);
+ virtual void OnItemsRemoved(int start, int length);
+
+ void SetObserver(TableViewObserver* observer) {
+ table_view_observer_ = observer;
+ }
+ TableViewObserver* observer() const { return table_view_observer_; }
+
+ // Replaces the set of known columns without changing the current visible
+ // columns.
+ void SetColumns(const std::vector<TableColumn>& columns);
+ void AddColumn(const TableColumn& col);
+ bool HasColumn(int id);
+
+ // Sets which columns (by id) are displayed. All transient size and position
+ // information is lost.
+ void SetVisibleColumns(const std::vector<int>& columns);
+ void SetColumnVisibility(int id, bool is_visible);
+ bool IsColumnVisible(int id) const;
+
+ // Resets the size of the columns based on the sizes passed to the
+ // constructor. Your normally needn't invoked this, it's done for you the
+ // first time the TableView is given a valid size.
+ void ResetColumnSizes();
+
+ // Sometimes we may want to size the TableView to a specific width and
+ // height.
+ virtual gfx::Size GetPreferredSize();
+ void set_preferred_size(const gfx::Size& size) { preferred_size_ = size; }
+
+ // 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:
+ // Overriden to return the position of the first selected row.
+ virtual gfx::Point GetKeyboardContextMenuLocation();
+
+ // 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
+ // GetCellColors is never invoked.
+ void SetCustomColorsEnabled(bool custom_colors_enabled);
+
+ // Notification from the ListView that the selected state of an item has
+ // changed.
+ virtual void OnSelectedStateChanged();
+
+ // Notification from the ListView that the used double clicked the table.
+ virtual void OnDoubleClick();
+
+ // Subclasses can implement this method if they need to be notified of a key
+ // press event. Other wise, it appeals to table_view_observer_
+ virtual void OnKeyDown(unsigned short virtual_keycode);
+
+ // 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 model_row,
+ int column,
+ ItemColor* foreground,
+ ItemColor* background,
+ LOGFONT* logfont);
+
+ // Subclasses that want to perform some custom painting (on top of the regular
+ // list view painting) should return true here and implement the PostPaint
+ // method.
+ virtual bool ImplementPostPaint() { return false; }
+ // Subclasses can implement in this method extra-painting for cells.
+ virtual void PostPaint(int model_row, int column, bool selected,
+ const CRect& bounds, HDC device_context) { }
+ virtual void PostPaint() {}
+
+ virtual HWND CreateNativeControl(HWND parent_container);
+
+ virtual LRESULT OnNotify(int w_param, LPNMHDR l_param);
+
+ // 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() {}
+
+ // Returns the width of the specified column by id, or -1 if the column isn't
+ // visible.
+ int GetColumnWidth(int column_id);
+
+ // Returns the offset from the top of the client area to the start of the
+ // content.
+ int content_offset() const { return content_offset_; }
+
+ // Size (width and height) of images.
+ static const int kImageSize;
+
+ 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
+ // subclass (in the OO sense of TableView).
+ struct TableViewWrapper {
+ explicit TableViewWrapper(TableView* view) : table_view(view) { }
+ TableView* table_view;
+ };
+
+ friend class ListViewParent;
+ friend class TableSelectionIterator;
+
+ 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);
+
+ // Update headers and internal state after columns have changed
+ void OnColumnsChanged();
+
+ // Updates the ListView with values from the model. See UpdateListViewCache0
+ // for a complete description.
+ // This turns off redrawing, and invokes UpdateListViewCache0 to do the
+ // actual updating.
+ void UpdateListViewCache(int start, int length, bool add);
+
+ // Updates ListView with values from the model.
+ // If add is true, this adds length items starting at index start.
+ // If add is not true, the items are not added, the but the values in the
+ // range start - [start + length] are updated from the model.
+ void UpdateListViewCache0(int start, int length, bool add);
+
+ // Notification from the ListView that the checked state of the item has
+ // changed.
+ void OnCheckedStateChanged(int model_row, bool is_checked);
+
+ // 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 view index in the table view, or -1 if the table
+ // is empty, or nothing is selected.
+ //
+ // 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);
+
+ // Window procedure of the list view class. We subclass the list view to
+ // ignore WM_ERASEBKGND, which gives smoother painting during resizing.
+ static LRESULT CALLBACK TableWndProc(HWND window,
+ UINT message,
+ WPARAM w_param,
+ LPARAM l_param);
+
+ // Window procedure of the header class. We subclass the header of the table
+ // to disable resizing of columns.
+ static LRESULT CALLBACK TableHeaderWndProc(HWND window, UINT message,
+ WPARAM w_param, LPARAM l_param);
+
+ // Updates content_offset_ from the position of the header.
+ void UpdateContentOffset();
+
+ TableModel* model_;
+ TableTypes table_type_;
+ TableViewObserver* table_view_observer_;
+
+ // An ordered list of id's into all_columns_ representing current visible
+ // columns.
+ std::vector<int> visible_columns_;
+
+ // Mapping of an int id to a TableColumn representing all possible columns.
+ std::map<int, TableColumn> all_columns_;
+
+ // Cached value of columns_.size()
+ int column_count_;
+
+ // Selection mode.
+ bool single_selection_;
+
+ // If true, any events that would normally be propagated to the observer
+ // are ignored. For example, if this is true and the selection changes in
+ // the listview, the observer is not notified.
+ bool ignore_listview_change_;
+
+ // Reflects the value passed to SetCustomColorsEnabled.
+ bool custom_colors_enabled_;
+
+ // Whether or not the columns have been sized in the ListView. This is
+ // set to true the first time Layout() is invoked and we have a valid size.
+ bool sized_columns_;
+
+ // Whether or not columns should automatically be resized to fill the
+ // the available width when the list view is resized.
+ bool autosize_columns_;
+
+ // Whether or not the user can resize columns.
+ bool resizable_columns_;
+
+ // NOTE: While this has the name View in it, it's not a view. Rather it's
+ // a wrapper around the List-View window.
+ HWND list_view_;
+
+ // The list view's header original proc handler. It is required when
+ // subclassing.
+ WNDPROC header_original_handler_;
+
+ // Window procedure of the listview before we subclassed it.
+ WNDPROC original_handler_;
+
+ // A wrapper around 'this' used when "subclassing" the list view and header.
+ TableViewWrapper table_view_wrapper_;
+
+ // A custom font we use when overriding the font type for a specific cell.
+ HFONT custom_cell_font_;
+
+ // The preferred size of the table view.
+ gfx::Size preferred_size_;
+
+ 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);
+};
+#endif // defined(OS_WIN)
+
+} // namespace views
+
+#endif // VIEWS_CONTROLS_TABLE_TABLE_VIEW_H_
diff --git a/views/controls/table/table_view_unittest.cc b/views/controls/table/table_view_unittest.cc
new file mode 100644
index 0000000..e0f08e2
--- /dev/null
+++ b/views/controls/table/table_view_unittest.cc
@@ -0,0 +1,381 @@
+// 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/message_loop.h"
+#include "base/string_util.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "views/controls/table/table_view.h"
+#include "views/window/window_delegate.h"
+#include "views/window/window_win.h"
+
+using views::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 views::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(views::TableModelObserver* observer);
+ virtual int CompareValues(int row1, int row2, int column_id);
+
+ private:
+ views::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(views::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, views::WindowDelegate {
+ public:
+ virtual void SetUp();
+ virtual void TearDown();
+
+ virtual views::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_;
+ views::Window* window_;
+};
+
+void TableViewTest::SetUp() {
+ OleInitialize(NULL);
+ model_.reset(CreateModel());
+ std::vector<views::TableColumn> columns;
+ columns.resize(2);
+ columns[0].id = 0;
+ columns[1].id = 1;
+ table_ = new TableView(model_.get(), columns, views::ICON_AND_TEXT,
+ false, false, false);
+ window_ =
+ views::Window::CreateChromeWindow(NULL,
+ gfx::Rect(100, 100, 512, 512),
+ this);
+}
+
+void TableViewTest::TearDown() {
+ window_->Close();
+ // Temporary workaround to avoid leak of RootView::pending_paint_task_.
+ message_loop_.RunAllPending();
+ OleUninitialize();
+}
+
+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();
+}
+
+// NullModelTableViewTest ------------------------------------------------------
+
+class NullModelTableViewTest : public TableViewTest {
+ protected:
+ // Creates the model.
+ TestTableModel* CreateModel() {
+ return NULL;
+ }
+};
+
+// 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);
+}
+
+TEST_F(NullModelTableViewTest, NullModel) {
+ // There's nothing explicit to test. If there is a bug in TableView relating
+ // to a NULL model we'll crash.
+}