// 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/scroll_view.h" #include "base/logging.h" #include "ui/events/event.h" #include "ui/gfx/canvas.h" #include "ui/native_theme/native_theme.h" #include "ui/views/border.h" #include "ui/views/controls/scrollbar/native_scroll_bar.h" #include "ui/views/widget/root_view.h" namespace views { const char ScrollView::kViewClassName[] = "ScrollView"; namespace { // Subclass of ScrollView that resets the border when the theme changes. class ScrollViewWithBorder : public views::ScrollView { public: ScrollViewWithBorder() {} // View overrides; void OnNativeThemeChanged(const ui::NativeTheme* theme) override { SetBorder(Border::CreateSolidBorder( 1, theme->GetSystemColor(ui::NativeTheme::kColorId_UnfocusedBorderColor))); } private: DISALLOW_COPY_AND_ASSIGN(ScrollViewWithBorder); }; class ScrollCornerView : public views::View { public: ScrollCornerView() {} void OnPaint(gfx::Canvas* canvas) override { ui::NativeTheme::ExtraParams ignored; GetNativeTheme()->Paint(canvas->sk_canvas(), ui::NativeTheme::kScrollbarCorner, ui::NativeTheme::kNormal, GetLocalBounds(), ignored); } private: DISALLOW_COPY_AND_ASSIGN(ScrollCornerView); }; // Returns the position for the view so that it isn't scrolled off the visible // region. int CheckScrollBounds(int viewport_size, int content_size, int current_pos) { int max = std::max(content_size - viewport_size, 0); if (current_pos < 0) return 0; if (current_pos > max) return max; return current_pos; } // Make sure the content is not scrolled out of bounds void CheckScrollBounds(View* viewport, View* view) { if (!view) return; int x = CheckScrollBounds(viewport->width(), view->width(), -view->x()); int y = CheckScrollBounds(viewport->height(), view->height(), -view->y()); // This is no op if bounds are the same view->SetBounds(-x, -y, view->width(), view->height()); } // Used by ScrollToPosition() to make sure the new position fits within the // allowed scroll range. int AdjustPosition(int current_position, int new_position, int content_size, int viewport_size) { if (-current_position == new_position) return new_position; if (new_position < 0) return 0; const int max_position = std::max(0, content_size - viewport_size); return (new_position > max_position) ? max_position : new_position; } } // namespace // Viewport contains the contents View of the ScrollView. class ScrollView::Viewport : public View { public: Viewport() {} ~Viewport() override {} const char* GetClassName() const override { return "ScrollView::Viewport"; } void ScrollRectToVisible(const gfx::Rect& rect) override { if (!has_children() || !parent()) return; View* contents = child_at(0); gfx::Rect scroll_rect(rect); scroll_rect.Offset(-contents->x(), -contents->y()); static_cast(parent())->ScrollContentsRegionToBeVisible( scroll_rect); } void ChildPreferredSizeChanged(View* child) override { if (parent()) parent()->Layout(); } private: DISALLOW_COPY_AND_ASSIGN(Viewport); }; ScrollView::ScrollView() : contents_(NULL), contents_viewport_(new Viewport()), header_(NULL), header_viewport_(new Viewport()), horiz_sb_(new NativeScrollBar(true)), vert_sb_(new NativeScrollBar(false)), corner_view_(new ScrollCornerView()), min_height_(-1), max_height_(-1), hide_horizontal_scrollbar_(false) { set_notify_enter_exit_on_child(true); AddChildView(contents_viewport_); AddChildView(header_viewport_); // Don't add the scrollbars as children until we discover we need them // (ShowOrHideScrollBar). horiz_sb_->SetVisible(false); horiz_sb_->set_controller(this); vert_sb_->SetVisible(false); vert_sb_->set_controller(this); corner_view_->SetVisible(false); } ScrollView::~ScrollView() { // The scrollbars may not have been added, delete them to ensure they get // deleted. delete horiz_sb_; delete vert_sb_; delete corner_view_; } // static ScrollView* ScrollView::CreateScrollViewWithBorder() { return new ScrollViewWithBorder(); } void ScrollView::SetContents(View* a_view) { SetHeaderOrContents(contents_viewport_, a_view, &contents_); } void ScrollView::SetHeader(View* header) { SetHeaderOrContents(header_viewport_, header, &header_); } gfx::Rect ScrollView::GetVisibleRect() const { if (!contents_) return gfx::Rect(); return gfx::Rect(-contents_->x(), -contents_->y(), contents_viewport_->width(), contents_viewport_->height()); } void ScrollView::ClipHeightTo(int min_height, int max_height) { min_height_ = min_height; max_height_ = max_height; } int ScrollView::GetScrollBarWidth() const { return vert_sb_ ? vert_sb_->GetLayoutSize() : 0; } int ScrollView::GetScrollBarHeight() const { return horiz_sb_ ? horiz_sb_->GetLayoutSize() : 0; } void ScrollView::SetHorizontalScrollBar(ScrollBar* horiz_sb) { DCHECK(horiz_sb); horiz_sb->SetVisible(horiz_sb_->visible()); delete horiz_sb_; horiz_sb->set_controller(this); horiz_sb_ = horiz_sb; } void ScrollView::SetVerticalScrollBar(ScrollBar* vert_sb) { DCHECK(vert_sb); vert_sb->SetVisible(vert_sb_->visible()); delete vert_sb_; vert_sb->set_controller(this); vert_sb_ = vert_sb; } gfx::Size ScrollView::GetPreferredSize() const { if (!is_bounded()) return View::GetPreferredSize(); gfx::Size size = contents()->GetPreferredSize(); size.SetToMax(gfx::Size(size.width(), min_height_)); size.SetToMin(gfx::Size(size.width(), max_height_)); gfx::Insets insets = GetInsets(); size.Enlarge(insets.width(), insets.height()); return size; } int ScrollView::GetHeightForWidth(int width) const { if (!is_bounded()) return View::GetHeightForWidth(width); gfx::Insets insets = GetInsets(); width = std::max(0, width - insets.width()); int height = contents()->GetHeightForWidth(width) + insets.height(); return std::min(std::max(height, min_height_), max_height_); } void ScrollView::Layout() { if (is_bounded()) { int content_width = width(); int content_height = contents()->GetHeightForWidth(content_width); if (content_height > height()) { content_width = std::max(content_width - GetScrollBarWidth(), 0); content_height = contents()->GetHeightForWidth(content_width); } if (contents()->bounds().size() != gfx::Size(content_width, content_height)) contents()->SetBounds(0, 0, content_width, content_height); } // Most views will want to auto-fit the available space. Most of them want to // use all available width (without overflowing) and only overflow in // height. Examples are HistoryView, MostVisitedView, DownloadTabView, etc. // Other views want to fit in both ways. An example is PrintView. To make both // happy, assume a vertical scrollbar but no horizontal scrollbar. To override // this default behavior, the inner view has to calculate the available space, // used ComputeScrollBarsVisibility() to use the same calculation that is done // here and sets its bound to fit within. gfx::Rect viewport_bounds = GetContentsBounds(); const int contents_x = viewport_bounds.x(); const int contents_y = viewport_bounds.y(); if (viewport_bounds.IsEmpty()) { // There's nothing to layout. return; } const int header_height = std::min(viewport_bounds.height(), header_ ? header_->GetPreferredSize().height() : 0); viewport_bounds.set_height( std::max(0, viewport_bounds.height() - header_height)); viewport_bounds.set_y(viewport_bounds.y() + header_height); // viewport_size is the total client space available. gfx::Size viewport_size = viewport_bounds.size(); // Assumes a vertical scrollbar since most of the current views are designed // for this. int horiz_sb_height = GetScrollBarHeight(); int vert_sb_width = GetScrollBarWidth(); viewport_bounds.set_width(viewport_bounds.width() - vert_sb_width); // Update the bounds right now so the inner views can fit in it. contents_viewport_->SetBoundsRect(viewport_bounds); // Give |contents_| a chance to update its bounds if it depends on the // viewport. if (contents_) contents_->Layout(); bool should_layout_contents = false; bool horiz_sb_required = false; bool vert_sb_required = false; if (contents_) { gfx::Size content_size = contents_->size(); ComputeScrollBarsVisibility(viewport_size, content_size, &horiz_sb_required, &vert_sb_required); } bool corner_view_required = horiz_sb_required && vert_sb_required; // Take action. SetControlVisibility(horiz_sb_, horiz_sb_required); SetControlVisibility(vert_sb_, vert_sb_required); SetControlVisibility(corner_view_, corner_view_required); // Non-default. if (horiz_sb_required) { viewport_bounds.set_height( std::max(0, viewport_bounds.height() - horiz_sb_height)); should_layout_contents = true; } // Default. if (!vert_sb_required) { viewport_bounds.set_width(viewport_bounds.width() + vert_sb_width); should_layout_contents = true; } if (horiz_sb_required) { int height_offset = horiz_sb_->GetContentOverlapSize(); horiz_sb_->SetBounds(0, viewport_bounds.bottom() - height_offset, viewport_bounds.right(), horiz_sb_height + height_offset); } if (vert_sb_required) { int width_offset = vert_sb_->GetContentOverlapSize(); vert_sb_->SetBounds(viewport_bounds.right() - width_offset, 0, vert_sb_width + width_offset, viewport_bounds.bottom()); } if (corner_view_required) { // Show the resize corner. corner_view_->SetBounds(viewport_bounds.right(), viewport_bounds.bottom(), vert_sb_width, horiz_sb_height); } // Update to the real client size with the visible scrollbars. contents_viewport_->SetBoundsRect(viewport_bounds); if (should_layout_contents && contents_) contents_->Layout(); header_viewport_->SetBounds(contents_x, contents_y, viewport_bounds.width(), header_height); if (header_) header_->Layout(); CheckScrollBounds(header_viewport_, header_); CheckScrollBounds(contents_viewport_, contents_); SchedulePaint(); UpdateScrollBarPositions(); } bool ScrollView::OnKeyPressed(const ui::KeyEvent& event) { bool processed = false; // Give vertical scrollbar priority if (vert_sb_->visible()) processed = vert_sb_->OnKeyPressed(event); if (!processed && horiz_sb_->visible()) processed = horiz_sb_->OnKeyPressed(event); return processed; } bool ScrollView::OnMouseWheel(const ui::MouseWheelEvent& e) { bool processed = false; if (vert_sb_->visible()) processed = vert_sb_->OnMouseWheel(e); if (horiz_sb_->visible()) processed = horiz_sb_->OnMouseWheel(e) || processed; return processed; } void ScrollView::OnMouseEntered(const ui::MouseEvent& event) { if (horiz_sb_) horiz_sb_->OnMouseEnteredScrollView(event); if (vert_sb_) vert_sb_->OnMouseEnteredScrollView(event); } void ScrollView::OnMouseExited(const ui::MouseEvent& event) { if (horiz_sb_) horiz_sb_->OnMouseExitedScrollView(event); if (vert_sb_) vert_sb_->OnMouseExitedScrollView(event); } void ScrollView::OnGestureEvent(ui::GestureEvent* event) { // If the event happened on one of the scrollbars, then those events are // sent directly to the scrollbars. Otherwise, only scroll events are sent to // the scrollbars. bool scroll_event = event->type() == ui::ET_GESTURE_SCROLL_UPDATE || event->type() == ui::ET_GESTURE_SCROLL_BEGIN || event->type() == ui::ET_GESTURE_SCROLL_END || event->type() == ui::ET_SCROLL_FLING_START; if (vert_sb_->visible()) { if (vert_sb_->bounds().Contains(event->location()) || scroll_event) vert_sb_->OnGestureEvent(event); } if (!event->handled() && horiz_sb_->visible()) { if (horiz_sb_->bounds().Contains(event->location()) || scroll_event) horiz_sb_->OnGestureEvent(event); } } const char* ScrollView::GetClassName() const { return kViewClassName; } void ScrollView::ScrollToPosition(ScrollBar* source, int position) { if (!contents_) return; if (source == horiz_sb_ && horiz_sb_->visible()) { position = AdjustPosition(contents_->x(), position, contents_->width(), contents_viewport_->width()); if (-contents_->x() == position) return; contents_->SetX(-position); if (header_) { header_->SetX(-position); header_->SchedulePaintInRect(header_->GetVisibleBounds()); } } else if (source == vert_sb_ && vert_sb_->visible()) { position = AdjustPosition(contents_->y(), position, contents_->height(), contents_viewport_->height()); if (-contents_->y() == position) return; contents_->SetY(-position); } contents_->SchedulePaintInRect(contents_->GetVisibleBounds()); } int ScrollView::GetScrollIncrement(ScrollBar* source, bool is_page, bool is_positive) { bool is_horizontal = source->IsHorizontal(); int amount = 0; if (contents_) { if (is_page) { amount = contents_->GetPageScrollIncrement( this, is_horizontal, is_positive); } else { amount = contents_->GetLineScrollIncrement( this, is_horizontal, is_positive); } if (amount > 0) return amount; } // No view, or the view didn't return a valid amount. if (is_page) { return is_horizontal ? contents_viewport_->width() : contents_viewport_->height(); } return is_horizontal ? contents_viewport_->width() / 5 : contents_viewport_->height() / 5; } void ScrollView::SetHeaderOrContents(View* parent, View* new_view, View** member) { if (*member == new_view) return; delete *member; *member = new_view; if (*member) parent->AddChildView(*member); Layout(); } void ScrollView::ScrollContentsRegionToBeVisible(const gfx::Rect& rect) { if (!contents_ || (!horiz_sb_->visible() && !vert_sb_->visible())) return; // Figure out the maximums for this scroll view. const int contents_max_x = std::max(contents_viewport_->width(), contents_->width()); const int contents_max_y = std::max(contents_viewport_->height(), contents_->height()); // Make sure x and y are within the bounds of [0,contents_max_*]. int x = std::max(0, std::min(contents_max_x, rect.x())); int y = std::max(0, std::min(contents_max_y, rect.y())); // Figure out how far and down the rectangle will go taking width // and height into account. This will be "clipped" by the viewport. const int max_x = std::min(contents_max_x, x + std::min(rect.width(), contents_viewport_->width())); const int max_y = std::min(contents_max_y, y + std::min(rect.height(), contents_viewport_->height())); // See if the rect is already visible. Note the width is (max_x - x) // and the height is (max_y - y) to take into account the clipping of // either viewport or the content size. const gfx::Rect vis_rect = GetVisibleRect(); if (vis_rect.Contains(gfx::Rect(x, y, max_x - x, max_y - y))) return; // Shift contents_'s X and Y so that the region is visible. If we // need to shift up or left from where we currently are then we need // to get it so that the content appears in the upper/left // corner. This is done by setting the offset to -X or -Y. For down // or right shifts we need to make sure it appears in the // lower/right corner. This is calculated by taking max_x or max_y // and scaling it back by the size of the viewport. const int new_x = (vis_rect.x() > x) ? x : std::max(0, max_x - contents_viewport_->width()); const int new_y = (vis_rect.y() > y) ? y : std::max(0, max_y - contents_viewport_->height()); contents_->SetX(-new_x); if (header_) header_->SetX(-new_x); contents_->SetY(-new_y); UpdateScrollBarPositions(); } void ScrollView::ComputeScrollBarsVisibility(const gfx::Size& vp_size, const gfx::Size& content_size, bool* horiz_is_shown, bool* vert_is_shown) const { // Try to fit both ways first, then try vertical bar only, then horizontal // bar only, then defaults to both shown. if (content_size.width() <= vp_size.width() && content_size.height() <= vp_size.height()) { *horiz_is_shown = false; *vert_is_shown = false; } else if (content_size.width() <= vp_size.width() - GetScrollBarWidth()) { *horiz_is_shown = false; *vert_is_shown = true; } else if (content_size.height() <= vp_size.height() - GetScrollBarHeight()) { *horiz_is_shown = true; *vert_is_shown = false; } else { *horiz_is_shown = true; *vert_is_shown = true; } if (hide_horizontal_scrollbar_) *horiz_is_shown = false; } // Make sure that a single scrollbar is created and visible as needed void ScrollView::SetControlVisibility(View* control, bool should_show) { if (!control) return; if (should_show) { if (!control->visible()) { AddChildView(control); control->SetVisible(true); } } else { RemoveChildView(control); control->SetVisible(false); } } void ScrollView::UpdateScrollBarPositions() { if (!contents_) return; if (horiz_sb_->visible()) { int vw = contents_viewport_->width(); int cw = contents_->width(); int origin = contents_->x(); horiz_sb_->Update(vw, cw, -origin); } if (vert_sb_->visible()) { int vh = contents_viewport_->height(); int ch = contents_->height(); int origin = contents_->y(); vert_sb_->Update(vh, ch, -origin); } } // VariableRowHeightScrollHelper ---------------------------------------------- VariableRowHeightScrollHelper::VariableRowHeightScrollHelper( Controller* controller) : controller_(controller) { } VariableRowHeightScrollHelper::~VariableRowHeightScrollHelper() { } int VariableRowHeightScrollHelper::GetPageScrollIncrement( ScrollView* scroll_view, bool is_horizontal, bool is_positive) { if (is_horizontal) return 0; // y coordinate is most likely negative. int y = abs(scroll_view->contents()->y()); int vis_height = scroll_view->contents()->parent()->height(); if (is_positive) { // Align the bottom most row to the top of the view. int bottom = std::min(scroll_view->contents()->height() - 1, y + vis_height); RowInfo bottom_row_info = GetRowInfo(bottom); // If 0, ScrollView will provide a default value. return std::max(0, bottom_row_info.origin - y); } else { // Align the row on the previous page to to the top of the view. int last_page_y = y - vis_height; RowInfo last_page_info = GetRowInfo(std::max(0, last_page_y)); if (last_page_y != last_page_info.origin) return std::max(0, y - last_page_info.origin - last_page_info.height); return std::max(0, y - last_page_info.origin); } } int VariableRowHeightScrollHelper::GetLineScrollIncrement( ScrollView* scroll_view, bool is_horizontal, bool is_positive) { if (is_horizontal) return 0; // y coordinate is most likely negative. int y = abs(scroll_view->contents()->y()); RowInfo row = GetRowInfo(y); if (is_positive) { return row.height - (y - row.origin); } else if (y == row.origin) { row = GetRowInfo(std::max(0, row.origin - 1)); return y - row.origin; } else { return y - row.origin; } } VariableRowHeightScrollHelper::RowInfo VariableRowHeightScrollHelper::GetRowInfo(int y) { return controller_->GetRowInfo(y); } // FixedRowHeightScrollHelper ----------------------------------------------- FixedRowHeightScrollHelper::FixedRowHeightScrollHelper(int top_margin, int row_height) : VariableRowHeightScrollHelper(NULL), top_margin_(top_margin), row_height_(row_height) { DCHECK_GT(row_height, 0); } VariableRowHeightScrollHelper::RowInfo FixedRowHeightScrollHelper::GetRowInfo(int y) { if (y < top_margin_) return RowInfo(0, top_margin_); return RowInfo((y - top_margin_) / row_height_ * row_height_ + top_margin_, row_height_); } } // namespace views