// Copyright 2008, Google Inc. // All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are // met: // // * Redistributions of source code must retain the above copyright // notice, this list of conditions and the following disclaimer. // * Redistributions in binary form must reproduce the above // copyright notice, this list of conditions and the following disclaimer // in the documentation and/or other materials provided with the // distribution. // * Neither the name of Google Inc. nor the names of its // contributors may be used to endorse or promote products derived from // this software without specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include "chrome/views/table_view.h" #include "base/string_util.h" #include "base/win_util.h" #include "base/gfx/skia_utils.h" #include "chrome/common/gfx/chrome_canvas.h" #include "chrome/common/gfx/favicon_size.h" #include "chrome/common/resource_bundle.h" #include "chrome/common/win_util.h" #include "chrome/views/hwnd_view.h" #include "chrome/views/view_container.h" #include "SkBitmap.h" #include "SkColorFilter.h" namespace ChromeViews { // Added to column width to prevent truncation. const int kListViewTextPadding = 15; // Additional column width necessary if column has icons. const int kListViewIconWidthAndPadding = 18; SkBitmap TableModel::GetIcon(int row) { return SkBitmap(); } TableView::TableView(TableModel* model, const std::vector& 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(columns.size())), cache_data_(true), 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), list_view_original_handler_(NULL), header_original_handler_(NULL), table_view_wrapper_(this), custom_cell_font_(NULL), content_offset_(0) { DCHECK(model); for (std::vector::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) { model_ = model; if (model_) OnModelChanged(); } void TableView::DidChangeBounds(const CRect& previous, const CRect& current) { if (!list_view_) return; SendMessage(list_view_, WM_SETREDRAW, static_cast(FALSE), 0); Layout(); if ((!sized_columns_ || autosize_columns_) && GetWidth() > 0) { sized_columns_ = true; ResetColumnSizes(); } UpdateContentOffset(); SendMessage(list_view_, WM_SETREDRAW, static_cast(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 item) { if (!list_view_) return; DCHECK(item >= 0 && item < RowCount()); SendMessage(list_view_, WM_SETREDRAW, static_cast(FALSE), 0); ignore_listview_change_ = true; // Unselect everything. ListView_SetItemState(list_view_, -1, 0, LVIS_SELECTED); // Select the specified item. ListView_SetItemState(list_view_, item, LVIS_SELECTED | LVIS_FOCUSED, LVIS_SELECTED | LVIS_FOCUSED); // Make it visible. ListView_EnsureVisible(list_view_, item, FALSE); ignore_listview_change_ = false; SendMessage(list_view_, WM_SETREDRAW, static_cast(TRUE), 0); if (table_view_observer_) table_view_observer_->OnSelectionChanged(); } void TableView::SetSelectedState(int item, bool state) { if (!list_view_) return; DCHECK(item >= 0 && item < RowCount()); ignore_listview_change_ = true; // Select the specified item. ListView_SetItemState(list_view_, item, LVIS_SELECTED, LVIS_SELECTED); ignore_listview_change_ = false; } void TableView::SetFocusOnItem(int item) { if (!list_view_) return; DCHECK(item >= 0 && item < RowCount()); ignore_listview_change_ = true; // Set the focus to the given item. ListView_SetItemState(list_view_, item, LVIS_FOCUSED, LVIS_FOCUSED); ignore_listview_change_ = false; } int TableView::FirstSelectedRow() { if (!list_view_) return -1; return ListView_GetNextItem(list_view_, -1, LVNI_ALL | LVIS_SELECTED); } bool TableView::IsItemSelected(int item) { if (!list_view_) return false; DCHECK(item >= 0 && item < RowCount()); return (ListView_GetItemState(list_view_, item, LVIS_SELECTED) == LVIS_SELECTED); } bool TableView::ItemHasTheFocus(int item) { if (!list_view_) return false; DCHECK(item >= 0 && item < RowCount()); return (ListView_GetItemState(list_view_, item, LVIS_FOCUSED) == LVIS_FOCUSED); } TableView::iterator TableView::SelectionBegin() { return TableView::iterator(this, LastSelectedIndex()); } 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); if (cache_data_) { length = model_->RowCount() - start; } else { length = RowCount() - start; } } int row_count = RowCount(); DCHECK(start >= 0 && length > 0 && start + length <= row_count); SendMessage(list_view_, WM_SETREDRAW, static_cast(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 = 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); } } if (!cache_data_) { ListView_RedrawItems(list_view_, start, start + length); } else { UpdateListViewCache(start, length, false); } SendMessage(list_view_, WM_SETREDRAW, static_cast(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_->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(FALSE), 0); if (!cache_data_) { ListView_SetItemCount(list_view_, model_->RowCount()); ListView_RedrawItems(list_view_, start, start + length); } else { UpdateListViewCache(start, length, true); } SendMessage(list_view_, WM_SETREDRAW, static_cast(TRUE), 0); } void TableView::OnItemsRemoved(int start, int length) { if (!list_view_) return; DCHECK(start >= 0 && length > 0 && start + length <= RowCount()); SendMessage(list_view_, WM_SETREDRAW, static_cast(FALSE), 0); bool had_selection = (SelectedRowCount() > 0); if (!cache_data_) { // TODO(sky): Make sure this triggers a repaint. ListView_SetItemCount(list_view_, model_->RowCount()); } else { // Update the cache. if (start == 0 && length == RowCount()) { ListView_DeleteAllItems(list_view_); } else { for (int i = 0; i < length; ++i) { ListView_DeleteItem(list_view_, start); } } } SendMessage(list_view_, WM_SETREDRAW, static_cast(TRUE), 0); // We don't seem to get notification in this case. if (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& columns) { all_columns_.empty(); for (std::vector::const_iterator i = columns.begin(); i != columns.end(); ++i) { AddColumn(*i); } } void TableView::OnColumnsChanged() { column_count_ = static_cast(visible_columns_.size()); ResetColumnSizes(); } void TableView::SetColumnVisibility(int id, bool is_visible) { bool changed = false; for (std::vector::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(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 // container). 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& columns) { size_t old_count = visible_columns_.size(); size_t new_count = columns.size(); // remove the old columns if (list_view_) { for (std::vector::reverse_iterator i = visible_columns_.rbegin(); i != visible_columns_.rend(); ++i) { int index = static_cast(i - visible_columns_.rend()); SendMessage(list_view_, LVM_DELETECOLUMN, index, 0); } } visible_columns_ = columns; // Insert the new columns. if (list_view_) { for (std::vector::iterator i = visible_columns_.begin(); i != visible_columns_.end(); ++i) { int index = static_cast(i - visible_columns_.end()); InsertColumn(all_columns_[*i], index); } } OnColumnsChanged(); } bool TableView::IsColumnVisible(int id) const { for (std::vector::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; } void TableView::SetCustomColorsEnabled(bool custom_colors_enabled) { custom_colors_enabled_ = custom_colors_enabled; } bool TableView::GetCellColors(int row, int column, ItemColor* foreground, ItemColor* background, LOGFONT* logfont) { return false; } LRESULT CALLBACK TableView::TableHeaderWndProc(HWND window, UINT message, WPARAM w_param, LPARAM l_param) { TableView* table_view = reinterpret_cast( 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 (!cache_data_) style |= LVS_OWNERDATA; // If there's only one column and the title string is empty, don't show a // header. if (all_columns_.size() == 1) { std::map::const_iterator first = all_columns_.begin(); if (first->second.title.empty()) style |= LVS_NOCOLUMNHEADER; } list_view_ = ::CreateWindowEx(WS_EX_CLIENTEDGE | GetAdditionalExStyle(), WC_LISTVIEW, L"", style, 0, 0, GetWidth(), GetHeight(), parent_container, NULL, NULL, NULL); model_->SetObserver(this); // 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); // Add the columns. for (std::vector::iterator i = visible_columns_.begin(); i != visible_columns_.end(); ++i) { InsertColumn(all_columns_[*i], static_cast(i - visible_columns_.begin())); } // Add the groups. if (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(groups[i].title.c_str()); group.iGroupId = groups[i].id; ListView_InsertGroup(list_view_, static_cast(i), &group); } } // Set the # of rows. if (cache_data_) { UpdateListViewCache(0, model_->RowCount(), true); } else if (table_type_ == CHECK_BOX_AND_TEXT) { ListView_SetItemCount(list_view_, model_->RowCount()); ListView_SetCallbackMask(list_view_, LVIS_STATEIMAGEMASK); } // Load the default icon. if (table_type_ == ICON_AND_TEXT) { HIMAGELIST image_list = ImageList_Create(18, 18, ILC_COLOR, 1, 1); // TODO(jcampan): include a default icon image. // This icon will not be found. The list view will still layout with the // right space for the icon so we can paint our own icon. HBITMAP image = LoadBitmap(NULL, L"IDR_WHATEVER"); ImageList_Add(image_list, image, NULL); DeleteObject(image); // We create 2 phony images because we are going to switch images at every // refresh in order to force a refresh of the icon area (somehow the clip // rect does not include the icon). image = LoadBitmap(NULL, L"IDR_WHATEVER_AGAIN"); ImageList_Add(image_list, image, NULL); DeleteObject(image); 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(&table_view_wrapper_)); header_original_handler_ = win_util::SetWindowProc(header, &TableView::TableHeaderWndProc); } // 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::InsertColumn(const TableColumn& tc, int index) { if (!list_view_) return; LVCOLUMN column = { 0 }; column.mask = LVCF_TEXT|LVCF_FMT; column.pszText = const_cast(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(&column)); } LRESULT TableView::OnNotify(int w_param, NMHDR* hdr) { switch (hdr->code) { case NM_CUSTOMDRAW: { // Draw notification. dwDragState indicates the current stage of drawing. return OnCustomDraw(reinterpret_cast(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(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. bool is_selected = ((state_change->uNewState & LVIS_SELECTED) != 0); OnSelectedStateChanged(state_change->iItem, is_selected); } 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(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(hdr); OnKeyDown(key_down_message->wVKey); break; } 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); } } 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(static_cast(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_); custom_cell_font_ = CreateFontIndirect(&logfont); SelectObject(draw_info->nmcd.hdc, custom_cell_font_); draw_info->clrText = foreground.color_is_set ? gfx::SkColorToCOLORREF(foreground.color) : CLR_DEFAULT; draw_info->clrTextBk = background.color_is_set ? gfx::SkColorToCOLORREF(background.color) : CLR_DEFAULT; return CDRF_NEWFONT; } } return CDRF_DODEFAULT; } case CDDS_ITEMPOSTPAINT: { DCHECK((table_type_ == ICON_AND_TEXT) || (ImplementPostPaint())); int n_item = static_cast(draw_info->nmcd.dwItemSpec); // We get notifications for empty items, just ignore them. if (n_item >= model_->RowCount()) { return CDRF_DODEFAULT; } LRESULT r = CDRF_DODEFAULT; // First let's take care of painting the right icon. if (table_type_ == ICON_AND_TEXT) { SkBitmap image = model_->GetIcon(n_item); if (!image.isNull()) { // Get the rect that holds the icon. CRect icon_rect, client_rect; if (ListView_GetItemRect(list_view_, n_item, &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_, n_item, LVIS_SELECTED); int bg_color_index; if (!IsEnabled()) bg_color_index = COLOR_3DFACE; 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( gfx::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_, n_item, &cell_rect, LVIR_BOUNDS)) { PostPaint(n_item, 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. CRect bounds; GetLocalBounds(&bounds, false); // false so it doesn't include the border. int width = bounds.Width(); if (GetClientRect(GetNativeControlHWND(), &bounds) && bounds.Width() > 0) { // Prefer the bounds of the window over our bounds, which may be different. width = bounds.Width(); } float percent = 0; int fixed_width = 0; int autosize_width = 0; for (std::vector::const_iterator i = visible_columns_.begin(); i != visible_columns_.end(); ++i) { TableColumn& col = all_columns_[*i]; int col_index = static_cast(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::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(i - visible_columns_.begin()); if (col.percent > 0) { if (available_width > 0) { int col_width = static_cast(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); } } } } void TableView::SetPreferredSize(const CSize& preferred_size) { preferred_size_ = preferred_size; } void TableView::GetPreferredSize(CSize* out) { DCHECK(out); *out = preferred_size_; } void TableView::UpdateListViewCache0(int start, int length, bool add) { LVITEM item = {0}; int start_column = 0; int max_row = start + length; const bool has_groups = (win_util::GetWinVersion() > win_util::WINVERSION_2000 && model_->HasGroups()); if (has_groups) item.mask = LVIF_GROUPID; if (add) { for (int i = start; i < max_row; ++i) { item.iItem = i; if (has_groups) item.iGroupId = model_->GetGroupID(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 = i; item.pszText = const_cast(text.c_str()); item.state = INDEXTOSTATEIMAGEMASK(model_->IsChecked(i) ? 2 : 1) ; ListView_SetItem(list_view_, &item); } } if (start_column == column_count_) return; item.stateMask = 0; item.mask = LVIF_TEXT; 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 = i; item.iSubItem = j; std::wstring text = model_->GetText(i, visible_columns_[j]); item.pszText = const_cast(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; } } } void TableView::OnDoubleClick() { if (!ignore_listview_change_ && table_view_observer_) { table_view_observer_->OnDoubleClick(); } } void TableView::OnSelectedStateChanged(int item, bool is_selected) { if (!ignore_listview_change_ && table_view_observer_) { table_view_observer_->OnSelectionChanged(); } } void TableView::OnCheckedStateChanged(int item, bool is_checked) { if (!ignore_listview_change_) { model_->SetChecked(item, is_checked); } } int TableView::NextSelectedIndex(int item) { if (!list_view_) return -1; DCHECK(item >= 0); if (item >= RowCount()) { return LastSelectedIndex(); } // It seems if the list has only 1 element and it is selected that // ListView_GetNextItem always returns 0. if (RowCount() == 1) { return -1; } return ListView_GetNextItem(list_view_, item, LVNI_ALL | LVNI_SELECTED | LVNI_ABOVE); } int TableView::LastSelectedIndex() { if (!list_view_) return -1; int row_count = RowCount(); int last_selected_row = -1; if (row_count > 0) { if (ListView_GetItemState(list_view_, row_count - 1, LVIS_SELECTED) == LVIS_SELECTED) { last_selected_row = row_count - 1; } else { last_selected_row = ListView_GetNextItem(list_view_, row_count - 1, LVNI_ALL | LVNI_SELECTED | LVNI_ABOVE); } } return last_selected_row; } 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 index) : table_view_(view), index_(index) { } TableSelectionIterator& TableSelectionIterator::operator=( const TableSelectionIterator& other) { index_ = other.index_; return *this; } bool TableSelectionIterator::operator==(const TableSelectionIterator& other) { return (other.index_ == index_); } bool TableSelectionIterator::operator!=(const TableSelectionIterator& other) { return (other.index_ != index_); } TableSelectionIterator& TableSelectionIterator::operator++() { index_ = table_view_->NextSelectedIndex(index_); return *this; } int TableSelectionIterator::operator*() { return index_; } } // namespace