diff options
author | ben@chromium.org <ben@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2009-05-08 00:34:05 +0000 |
---|---|---|
committer | ben@chromium.org <ben@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2009-05-08 00:34:05 +0000 |
commit | 2362e4fe2905ab75d3230ebc3e307ae53e2b8362 (patch) | |
tree | e6d88357a2021811e0e354f618247217be8bb3da /views/controls/table | |
parent | db23ac3e713dc17509b2b15d3ee634968da45715 (diff) | |
download | chromium_src-2362e4fe2905ab75d3230ebc3e307ae53e2b8362.zip chromium_src-2362e4fe2905ab75d3230ebc3e307ae53e2b8362.tar.gz chromium_src-2362e4fe2905ab75d3230ebc3e307ae53e2b8362.tar.bz2 |
Move src/chrome/views to src/views. RS=darin http://crbug.com/11387
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@15604 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'views/controls/table')
-rw-r--r-- | views/controls/table/group_table_view.cc | 193 | ||||
-rw-r--r-- | views/controls/table/group_table_view.h | 82 | ||||
-rw-r--r-- | views/controls/table/table_view.cc | 1570 | ||||
-rw-r--r-- | views/controls/table/table_view.h | 676 | ||||
-rw-r--r-- | views/controls/table/table_view_unittest.cc | 381 |
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. +} |