// Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "ui/views/controls/table/table_view.h" #include #include "base/auto_reset.h" #include "base/i18n/rtl.h" #include "ui/base/events/event.h" #include "ui/gfx/canvas.h" #include "ui/gfx/image/image_skia.h" #include "ui/gfx/rect_conversions.h" #include "ui/gfx/skia_util.h" #include "ui/native_theme/native_theme.h" #include "ui/views/controls/scroll_view.h" #include "ui/views/controls/table/table_grouper.h" #include "ui/views/controls/table/table_header.h" #include "ui/views/controls/table/table_utils.h" #include "ui/views/controls/table/table_view_observer.h" #include "ui/views/controls/table/table_view_row_background_painter.h" // Padding around the text (on each side). static const int kTextVerticalPadding = 3; static const int kTextHorizontalPadding = 6; // Size of images. static const int kImageSize = 16; static const int kGroupingIndicatorSize = 6; namespace views { namespace { // Returns result, unless ascending is false in which case -result is returned. int SwapCompareResult(int result, bool ascending) { return ascending ? result : -result; } // Populates |model_index_to_range_start| based on the |grouper|. void GetModelIndexToRangeStart(TableGrouper* grouper, int row_count, std::map* model_index_to_range_start) { for (int model_index = 0; model_index < row_count;) { GroupRange range; grouper->GetGroupRange(model_index, &range); DCHECK_GT(range.length, 0); for (int range_counter = 0; range_counter < range.length; range_counter++) (*model_index_to_range_start)[range_counter + model_index] = model_index; model_index += range.length; } } // Returns the color id for the background of selected text. |has_focus| // indicates if the table has focus. ui::NativeTheme::ColorId text_background_color_id(bool has_focus) { return has_focus ? ui::NativeTheme::kColorId_TableSelectionBackgroundFocused : ui::NativeTheme::kColorId_TableSelectionBackgroundUnfocused; } // Returns the color id for text. |has_focus| indicates if the table has focus. ui::NativeTheme::ColorId selected_text_color_id(bool has_focus) { return has_focus ? ui::NativeTheme::kColorId_TableSelectedText : ui::NativeTheme::kColorId_TableSelectedTextUnfocused; } } // namespace // Used as the comparator to sort the contents of the table. struct TableView::SortHelper { explicit SortHelper(TableView* table) : table(table) {} bool operator()(int model_index1, int model_index2) { return table->CompareRows(model_index1, model_index2) < 0; } TableView* table; }; // Used as the comparator to sort the contents of the table when a TableGrouper // is present. When groups are present we sort the groups based on the first row // in the group and within the groups we keep the same order as the model. struct TableView::GroupSortHelper { explicit GroupSortHelper(TableView* table) : table(table) {} bool operator()(int model_index1, int model_index2) { const int range1 = model_index_to_range_start[model_index1]; const int range2 = model_index_to_range_start[model_index2]; 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_index1 < model_index2; } return table->CompareRows(range1, range2) < 0; } TableView* table; std::map model_index_to_range_start; }; TableView::VisibleColumn::VisibleColumn() : x(0), width(0) {} TableView::VisibleColumn::~VisibleColumn() {} TableView::PaintRegion::PaintRegion() : min_row(0), max_row(0), min_column(0), max_column(0) { } TableView::PaintRegion::~PaintRegion() {} TableView::TableView(ui::TableModel* model, const std::vector& columns, TableTypes table_type, bool single_selection) : model_(NULL), columns_(columns), header_(NULL), table_type_(table_type), single_selection_(single_selection), table_view_observer_(NULL), row_height_(font_.GetHeight() + kTextVerticalPadding * 2), last_parent_width_(0), layout_width_(0), grouper_(NULL), in_set_visible_column_width_(false) { for (size_t i = 0; i < columns.size(); ++i) { VisibleColumn visible_column; visible_column.column = columns[i]; visible_columns_.push_back(visible_column); } set_focusable(true); SetModel(model); } TableView::~TableView() { if (model_) model_->SetObserver(NULL); } // TODO: this doesn't support arbitrarily changing the model, rename this to // ClearModel() or something. void TableView::SetModel(ui::TableModel* model) { if (model == model_) return; if (model_) model_->SetObserver(NULL); model_ = model; selection_model_.Clear(); if (model_) model_->SetObserver(this); } View* TableView::CreateParentIfNecessary() { ScrollView* scroll_view = ScrollView::CreateScrollViewWithBorder(); scroll_view->SetContents(this); CreateHeaderIfNecessary(); if (header_) scroll_view->SetHeader(header_); return scroll_view; } void TableView::SetRowBackgroundPainter( scoped_ptr painter) { row_background_painter_ = painter.Pass(); } void TableView::SetGrouper(TableGrouper* grouper) { grouper_ = grouper; SortItemsAndUpdateMapping(); } int TableView::RowCount() const { return model_ ? model_->RowCount() : 0; } int TableView::SelectedRowCount() { return static_cast(selection_model_.size()); } void TableView::Select(int model_row) { if (!model_) return; SelectByViewIndex(model_row == -1 ? -1 : ModelToView(model_row)); } int TableView::FirstSelectedRow() { return SelectedRowCount() == 0 ? -1 : selection_model_.selected_indices()[0]; } void TableView::SetColumnVisibility(int id, bool is_visible) { if (is_visible == IsColumnVisible(id)) return; if (is_visible) { VisibleColumn visible_column; visible_column.column = FindColumnByID(id); visible_columns_.push_back(visible_column); } else { for (size_t i = 0; i < visible_columns_.size(); ++i) { if (visible_columns_[i].column.id == id) { visible_columns_.erase(visible_columns_.begin() + i); break; } } } UpdateVisibleColumnSizes(); PreferredSizeChanged(); SchedulePaint(); if (header_) header_->SchedulePaint(); } void TableView::ToggleSortOrder(int visible_column_index) { DCHECK(visible_column_index >= 0 && visible_column_index < static_cast(visible_columns_.size())); if (!visible_columns_[visible_column_index].column.sortable) return; const int column_id = visible_columns_[visible_column_index].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); // Only persist two sort descriptors. if (sort.size() > 2) sort.resize(2); } SetSortDescriptors(sort); } bool TableView::IsColumnVisible(int id) const { for (size_t i = 0; i < visible_columns_.size(); ++i) { if (visible_columns_[i].column.id == id) return true; } return false; } void TableView::AddColumn(const ui::TableColumn& col) { DCHECK(!HasColumn(col.id)); columns_.push_back(col); } bool TableView::HasColumn(int id) const { for (size_t i = 0; i < columns_.size(); ++i) { if (columns_[i].id == id) return true; } return false; } void TableView::SetVisibleColumnWidth(int index, int width) { DCHECK(index >= 0 && index < static_cast(visible_columns_.size())); if (visible_columns_[index].width == width) return; base::AutoReset reseter(&in_set_visible_column_width_, true); visible_columns_[index].width = width; for (size_t i = index + 1; i < visible_columns_.size(); ++i) { visible_columns_[i].x = visible_columns_[i - 1].x + visible_columns_[i - 1].width; } PreferredSizeChanged(); SchedulePaint(); } int TableView::ModelToView(int model_index) const { if (!is_sorted()) return model_index; DCHECK_GE(model_index, 0) << " negative model_index " << model_index; DCHECK_LT(model_index, RowCount()) << " out of bounds model_index " << model_index; return model_to_view_[model_index]; } int TableView::ViewToModel(int view_index) const { if (!is_sorted()) return view_index; DCHECK_GE(view_index, 0) << " negative view_index " << view_index; DCHECK_LT(view_index, RowCount()) << " out of bounds view_index " << view_index; return view_to_model_[view_index]; } void TableView::Layout() { // parent()->parent() is the scrollview. When its width changes we force // recalculating column sizes. View* scroll_view = parent() ? parent()->parent() : NULL; if (scroll_view) { const int scroll_view_width = scroll_view->GetContentsBounds().width(); if (scroll_view_width != last_parent_width_) { last_parent_width_ = scroll_view_width; if (!in_set_visible_column_width_) { // Layout to the parent (the Viewport), which differs from // |scroll_view_width| when scrollbars are present. layout_width_ = parent()->width(); UpdateVisibleColumnSizes(); } } } // We have to override Layout like this since we're contained in a ScrollView. gfx::Size pref = GetPreferredSize(); int width = pref.width(); int height = pref.height(); if (parent()) { width = std::max(parent()->width(), width); height = std::max(parent()->height(), height); } SetBounds(x(), y(), width, height); } gfx::Size TableView::GetPreferredSize() { int width = 50; if (header_ && !visible_columns_.empty()) width = visible_columns_.back().x + visible_columns_.back().width; return gfx::Size(width, RowCount() * row_height_); } bool TableView::OnKeyPressed(const ui::KeyEvent& event) { if (!HasFocus()) return false; switch (event.key_code()) { case ui::VKEY_A: // control-a selects all. if (event.IsControlDown() && !single_selection_ && RowCount()) { ui::ListSelectionModel selection_model; selection_model.SetSelectedIndex(selection_model_.active()); for (int i = 0; i < RowCount(); ++i) selection_model.AddIndexToSelection(i); SetSelectionModel(selection_model); return true; } break; case ui::VKEY_HOME: if (RowCount()) SelectByViewIndex(0); return true; case ui::VKEY_END: if (RowCount()) SelectByViewIndex(RowCount() - 1); return true; case ui::VKEY_UP: AdvanceSelection(ADVANCE_DECREMENT); return true; case ui::VKEY_DOWN: AdvanceSelection(ADVANCE_INCREMENT); return true; default: break; } if (table_view_observer_) table_view_observer_->OnKeyDown(event.key_code()); return false; } bool TableView::OnMousePressed(const ui::MouseEvent& event) { RequestFocus(); if (!event.IsOnlyLeftMouseButton()) return true; const int row = event.y() / row_height_; if (row < 0 || row >= RowCount()) return true; if (event.GetClickCount() == 2) { SelectByViewIndex(row); if (table_view_observer_) table_view_observer_->OnDoubleClick(); } else if (event.GetClickCount() == 1) { ui::ListSelectionModel new_model; ConfigureSelectionModelForEvent(event, &new_model); SetSelectionModel(new_model); } return true; } bool TableView::GetTooltipText(const gfx::Point& p, string16* tooltip) const { return GetTooltipImpl(p, tooltip, NULL); } bool TableView::GetTooltipTextOrigin(const gfx::Point& p, gfx::Point* loc) const { return GetTooltipImpl(p, NULL, loc); } void TableView::OnModelChanged() { selection_model_.Clear(); NumRowsChanged(); } void TableView::OnItemsChanged(int start, int length) { SortItemsAndUpdateMapping(); } void TableView::OnItemsAdded(int start, int length) { for (int i = 0; i < length; ++i) selection_model_.IncrementFrom(start); NumRowsChanged(); } void TableView::OnItemsRemoved(int start, int length) { // Determine the currently selected index in terms of the view. We inline the // implementation here since ViewToModel() has DCHECKs that fail since the // model has changed but |model_to_view_| has not been updated yet. const int previously_selected_model_index = FirstSelectedRow(); int previously_selected_view_index = previously_selected_model_index; if (previously_selected_model_index != -1 && is_sorted()) previously_selected_view_index = model_to_view_[previously_selected_model_index]; for (int i = 0; i < length; ++i) selection_model_.DecrementFrom(start); NumRowsChanged(); // If the selection was empty and is no longer empty select the same visual // index. if (selection_model_.empty() && previously_selected_view_index != -1 && RowCount()) { selection_model_.SetSelectedIndex( ViewToModel(std::min(RowCount() - 1, previously_selected_view_index))); } if (table_view_observer_) table_view_observer_->OnSelectionChanged(); } gfx::Point TableView::GetKeyboardContextMenuLocation() { int first_selected = FirstSelectedRow(); gfx::Rect vis_bounds(GetVisibleBounds()); int y = vis_bounds.height() / 2; if (first_selected != -1) { gfx::Rect cell_bounds(GetRowBounds(first_selected)); if (cell_bounds.bottom() >= vis_bounds.y() && cell_bounds.bottom() < vis_bounds.bottom()) { y = cell_bounds.bottom(); } } gfx::Point screen_loc(0, y); if (base::i18n::IsRTL()) screen_loc.set_x(width()); ConvertPointToScreen(this, &screen_loc); return screen_loc; } void TableView::OnPaint(gfx::Canvas* canvas) { // Don't invoke View::OnPaint so that we can render our own focus border. canvas->DrawColor(GetNativeTheme()->GetSystemColor( ui::NativeTheme::kColorId_TableBackground)); if (!RowCount() || visible_columns_.empty()) return; const PaintRegion region(GetPaintRegion(GetPaintBounds(canvas))); if (region.min_column == -1) return; // No need to paint anything. const SkColor selected_bg_color = GetNativeTheme()->GetSystemColor( text_background_color_id(HasFocus())); const SkColor fg_color = GetNativeTheme()->GetSystemColor( ui::NativeTheme::kColorId_TableText); const SkColor selected_fg_color = GetNativeTheme()->GetSystemColor( selected_text_color_id(HasFocus())); for (int i = region.min_row; i < region.max_row; ++i) { const int model_index = ViewToModel(i); const bool is_selected = selection_model_.IsSelected(model_index); if (is_selected) { canvas->FillRect(GetRowBounds(i), selected_bg_color); } else if (row_background_painter_) { row_background_painter_->PaintRowBackground(model_index, GetRowBounds(i), canvas); } if (selection_model_.active() == i && HasFocus()) canvas->DrawFocusRect(GetRowBounds(i)); for (int j = region.min_column; j < region.max_column; ++j) { const gfx::Rect cell_bounds(GetCellBounds(i, j)); int text_x = kTextHorizontalPadding + cell_bounds.x(); // Provide space for the grouping indicator, but draw it separately. if (j == 0 && grouper_) text_x += kGroupingIndicatorSize + kTextHorizontalPadding; // Always paint the icon in the first visible column. if (j == 0 && table_type_ == ICON_AND_TEXT) { gfx::ImageSkia image = model_->GetIcon(model_index); if (!image.isNull()) { int image_x = GetMirroredXWithWidthInView(text_x, kImageSize); canvas->DrawImageInt( image, 0, 0, image.width(), image.height(), image_x, cell_bounds.y() + (cell_bounds.height() - kImageSize) / 2, kImageSize, kImageSize, true); } text_x += kImageSize + kTextHorizontalPadding; } if (text_x < cell_bounds.right() - kTextHorizontalPadding) { canvas->DrawStringInt( model_->GetText(model_index, visible_columns_[j].column.id), font_, is_selected ? selected_fg_color : fg_color, GetMirroredXWithWidthInView(text_x, cell_bounds.right() - text_x - kTextHorizontalPadding), cell_bounds.y() + kTextVerticalPadding, cell_bounds.right() - text_x, cell_bounds.height() - kTextVerticalPadding * 2, TableColumnAlignmentToCanvasAlignment( visible_columns_[j].column.alignment)); } } } if (!grouper_ || region.min_column > 0) return; const SkColor grouping_color = GetNativeTheme()->GetSystemColor( ui::NativeTheme::kColorId_TableGroupingIndicatorColor); SkPaint grouping_paint; grouping_paint.setColor(grouping_color); grouping_paint.setStyle(SkPaint::kFill_Style); grouping_paint.setAntiAlias(true); const int group_indicator_x = GetMirroredXInView(GetCellBounds(0, 0).x() + kTextHorizontalPadding + kGroupingIndicatorSize / 2); for (int i = region.min_row; i < region.max_row; ) { const int model_index = ViewToModel(i); GroupRange range; grouper_->GetGroupRange(model_index, &range); DCHECK_GT(range.length, 0); // The order of rows in a group is consistent regardless of sort, so it's ok // to do this calculation. const int start = i - (model_index - range.start); const int last = start + range.length - 1; const gfx::Rect start_cell_bounds(GetCellBounds(start, 0)); if (start != last) { const gfx::Rect last_cell_bounds(GetCellBounds(last, 0)); canvas->FillRect(gfx::Rect( group_indicator_x - kGroupingIndicatorSize / 2, start_cell_bounds.CenterPoint().y(), kGroupingIndicatorSize, last_cell_bounds.y() - start_cell_bounds.y()), grouping_color); canvas->DrawCircle(gfx::Point(group_indicator_x, last_cell_bounds.CenterPoint().y()), kGroupingIndicatorSize / 2, grouping_paint); } canvas->DrawCircle(gfx::Point(group_indicator_x, start_cell_bounds.CenterPoint().y()), kGroupingIndicatorSize / 2, grouping_paint); i = last + 1; } } void TableView::OnFocus() { SchedulePaintForSelection(); } void TableView::OnBlur() { SchedulePaintForSelection(); } void TableView::NumRowsChanged() { SortItemsAndUpdateMapping(); PreferredSizeChanged(); SchedulePaint(); } void TableView::SetSortDescriptors(const SortDescriptors& sort_descriptors) { sort_descriptors_ = sort_descriptors; SortItemsAndUpdateMapping(); if (header_) header_->SchedulePaint(); } void TableView::SortItemsAndUpdateMapping() { if (!is_sorted()) { view_to_model_.clear(); model_to_view_.clear(); } else { const int row_count = RowCount(); view_to_model_.resize(row_count); model_to_view_.resize(row_count); for (int i = 0; i < row_count; ++i) view_to_model_[i] = i; if (grouper_) { GroupSortHelper sort_helper(this); GetModelIndexToRangeStart(grouper_, RowCount(), &sort_helper.model_index_to_range_start); std::sort(view_to_model_.begin(), view_to_model_.end(), sort_helper); } else { std::sort(view_to_model_.begin(), view_to_model_.end(), SortHelper(this)); } for (int i = 0; i < row_count; ++i) model_to_view_[view_to_model_[i]] = i; model_->ClearCollator(); } SchedulePaint(); } int TableView::CompareRows(int model_row1, int model_row2) { const int sort_result = model_->CompareValues( model_row1, model_row2, sort_descriptors_[0].column_id); if (sort_result == 0 && sort_descriptors_.size() > 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); } gfx::Rect TableView::GetRowBounds(int row) const { return gfx::Rect(0, row * row_height_, width(), row_height_); } gfx::Rect TableView::GetCellBounds(int row, int visible_column_index) const { if (!header_) return GetRowBounds(row); const VisibleColumn& vis_col(visible_columns_[visible_column_index]); return gfx::Rect(vis_col.x, row * row_height_, vis_col.width, row_height_); } void TableView::AdjustCellBoundsForText(int visible_column_index, gfx::Rect* bounds) const { int text_x = kTextHorizontalPadding + bounds->x(); if (visible_column_index == 0) { if (grouper_) text_x += kGroupingIndicatorSize + kTextHorizontalPadding; if (table_type_ == ICON_AND_TEXT) text_x += kImageSize + kTextHorizontalPadding; } bounds->set_x(text_x); bounds->set_width( std::max(0, bounds->right() - kTextHorizontalPadding - text_x)); } void TableView::CreateHeaderIfNecessary() { // Only create a header if there is more than one column or the title of the // only column is not empty. if (header_ || (columns_.size() == 1 && columns_[0].title.empty())) return; header_ = new TableHeader(this); } void TableView::UpdateVisibleColumnSizes() { if (!header_) return; std::vector columns; for (size_t i = 0; i < visible_columns_.size(); ++i) columns.push_back(visible_columns_[i].column); int first_column_padding = 0; if (table_type_ == ICON_AND_TEXT && header_) first_column_padding += kImageSize + kTextHorizontalPadding; if (grouper_) first_column_padding += kGroupingIndicatorSize + kTextHorizontalPadding; std::vector sizes = views::CalculateTableColumnSizes( layout_width_, first_column_padding, header_->font(), font_, std::max(kTextHorizontalPadding, TableHeader::kHorizontalPadding) * 2, TableHeader::kSortIndicatorWidth, columns, model_); DCHECK_EQ(visible_columns_.size(), sizes.size()); int x = 0; for (size_t i = 0; i < visible_columns_.size(); ++i) { visible_columns_[i].x = x; visible_columns_[i].width = sizes[i]; x += sizes[i]; } } TableView::PaintRegion TableView::GetPaintRegion( const gfx::Rect& bounds) const { DCHECK(!visible_columns_.empty()); DCHECK(RowCount()); PaintRegion region; region.min_row = std::min(RowCount() - 1, std::max(0, bounds.y() / row_height_)); region.max_row = bounds.bottom() / row_height_; if (bounds.bottom() % row_height_ != 0) region.max_row++; region.max_row = std::min(region.max_row, RowCount()); if (!header_) { region.max_column = 1; return region; } const int paint_x = GetMirroredXForRect(bounds); const int paint_max_x = paint_x + bounds.width(); region.min_column = -1; region.max_column = visible_columns_.size(); for (size_t i = 0; i < visible_columns_.size(); ++i) { int max_x = visible_columns_[i].x + visible_columns_[i].width; if (region.min_column == -1 && max_x >= paint_x) region.min_column = static_cast(i); if (region.min_column != -1 && visible_columns_[i].x >= paint_max_x) { region.max_column = i; break; } } return region; } gfx::Rect TableView::GetPaintBounds(gfx::Canvas* canvas) const { SkRect sk_clip_rect; if (canvas->sk_canvas()->getClipBounds(&sk_clip_rect)) return gfx::ToEnclosingRect(gfx::SkRectToRectF(sk_clip_rect)); return GetVisibleBounds(); } void TableView::SchedulePaintForSelection() { if (selection_model_.size() == 1) { const int first_model_row = FirstSelectedRow(); SchedulePaintInRect(GetRowBounds(ModelToView(first_model_row))); if (first_model_row != selection_model_.active()) SchedulePaintInRect(GetRowBounds(ModelToView(selection_model_.active()))); } else if (selection_model_.size() > 1) { SchedulePaint(); } } ui::TableColumn TableView::FindColumnByID(int id) const { for (size_t i = 0; i < columns_.size(); ++i) { if (columns_[i].id == id) return columns_[i]; } NOTREACHED(); return ui::TableColumn(); } void TableView::SelectByViewIndex(int view_index) { ui::ListSelectionModel new_selection; if (view_index != -1) { SelectRowsInRangeFrom(view_index, true, &new_selection); new_selection.set_anchor(ViewToModel(view_index)); new_selection.set_active(ViewToModel(view_index)); } SetSelectionModel(new_selection); } void TableView::SetSelectionModel(const ui::ListSelectionModel& new_selection) { if (new_selection.Equals(selection_model_)) return; SchedulePaintForSelection(); selection_model_.Copy(new_selection); SchedulePaintForSelection(); // Scroll the group for the active item to visible. if (selection_model_.active() != -1) { gfx::Rect vis_rect(GetVisibleBounds()); const GroupRange range(GetGroupRange(selection_model_.active())); const int start_y = GetRowBounds(ModelToView(range.start)).y(); const int end_y = GetRowBounds(ModelToView(range.start + range.length - 1)).bottom(); vis_rect.set_y(start_y); vis_rect.set_height(end_y - start_y); ScrollRectToVisible(vis_rect); } if (table_view_observer_) table_view_observer_->OnSelectionChanged(); } void TableView::AdvanceSelection(AdvanceDirection direction) { if (selection_model_.active() == -1) { SelectByViewIndex(0); return; } int view_index = ModelToView(selection_model_.active()); if (direction == ADVANCE_DECREMENT) view_index = std::max(0, view_index - 1); else view_index = std::min(RowCount() - 1, view_index + 1); SelectByViewIndex(view_index); } void TableView::ConfigureSelectionModelForEvent( const ui::MouseEvent& event, ui::ListSelectionModel* model) const { const int view_index = event.y() / row_height_; DCHECK(view_index >= 0 && view_index < RowCount()); if (selection_model_.anchor() == -1 || single_selection_ || (!event.IsControlDown() && !event.IsShiftDown())) { SelectRowsInRangeFrom(view_index, true, model); model->set_anchor(ViewToModel(view_index)); model->set_active(ViewToModel(view_index)); return; } if ((event.IsControlDown() && event.IsShiftDown()) || event.IsShiftDown()) { // control-shift: copy existing model and make sure rows between anchor and // |view_index| are selected. // shift: reset selection so that only rows between anchor and |view_index| // are selected. if (event.IsControlDown() && event.IsShiftDown()) model->Copy(selection_model_); else model->set_anchor(selection_model_.anchor()); for (int i = std::min(view_index, ModelToView(model->anchor())), end = std::max(view_index, ModelToView(model->anchor())); i <= end; ++i) { SelectRowsInRangeFrom(i, true, model); } model->set_active(ViewToModel(view_index)); } else { DCHECK(event.IsControlDown()); // Toggle the selection state of |view_index| and set the anchor/active to // it and don't change the state of any other rows. model->Copy(selection_model_); model->set_anchor(ViewToModel(view_index)); model->set_active(ViewToModel(view_index)); SelectRowsInRangeFrom(view_index, !model->IsSelected(ViewToModel(view_index)), model); } } void TableView::SelectRowsInRangeFrom(int view_index, bool select, ui::ListSelectionModel* model) const { const GroupRange range(GetGroupRange(ViewToModel(view_index))); for (int i = 0; i < range.length; ++i) { if (select) model->AddIndexToSelection(range.start + i); else model->RemoveIndexFromSelection(range.start + i); } } GroupRange TableView::GetGroupRange(int model_index) const { GroupRange range; if (grouper_) { grouper_->GetGroupRange(model_index, &range); } else { range.start = model_index; range.length = 1; } return range; } bool TableView::GetTooltipImpl(const gfx::Point& location, string16* tooltip, gfx::Point* tooltip_origin) const { const int row = location.y() / row_height_; if (row < 0 || row >= RowCount() || visible_columns_.empty()) return false; const int x = GetMirroredXInView(location.x()); const int column = GetClosestVisibleColumnIndex(this, x); if (x < visible_columns_[column].x || x > (visible_columns_[column].x + visible_columns_[column].width)) return false; const string16 text(model_->GetText(ViewToModel(row), visible_columns_[column].column.id)); if (text.empty()) return false; gfx::Rect cell_bounds(GetCellBounds(row, column)); AdjustCellBoundsForText(column, &cell_bounds); const int right = std::min(GetVisibleBounds().right(), cell_bounds.right()); if (right > cell_bounds.x() && font_.GetStringWidth(text) <= (right - cell_bounds.x())) return false; if (tooltip) *tooltip = text; if (tooltip_origin) { tooltip_origin->SetPoint(cell_bounds.x(), cell_bounds.y() + kTextVerticalPadding); } return true; } } // namespace views