blob: 6a2b912d6ec8dcc0de716bc41a66148d6cf96de9 [file] [log] [blame]
// Copyright (c) 2010 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/scroll_view.h"
#include "base/logging.h"
#include "views/controls/scrollbar/native_scroll_bar.h"
#include "views/widget/root_view.h"
namespace views {
const char* const ScrollView::kViewClassName = "views/ScrollView";
// Viewport contains the contents View of the ScrollView.
class Viewport : public View {
public:
Viewport() {}
virtual ~Viewport() {}
virtual void ScrollRectToVisible(const gfx::Rect& rect) {
if (!has_children() || !parent())
return;
View* contents = GetChildViewAt(0);
gfx::Rect scroll_rect(rect);
scroll_rect.Offset(-contents->x(), -contents->y());
static_cast<ScrollView*>(parent())->ScrollContentsRegionToBeVisible(
scroll_rect);
}
private:
DISALLOW_COPY_AND_ASSIGN(Viewport);
};
ScrollView::ScrollView() {
Init(new NativeScrollBar(true), new NativeScrollBar(false), NULL);
}
ScrollView::ScrollView(ScrollBar* horizontal_scrollbar,
ScrollBar* vertical_scrollbar,
View* resize_corner) {
Init(horizontal_scrollbar, vertical_scrollbar, resize_corner);
}
ScrollView::~ScrollView() {
// If scrollbars are currently not used, delete them
if (!horiz_sb_->parent())
delete horiz_sb_;
if (!vert_sb_->parent())
delete vert_sb_;
if (resize_corner_ && !resize_corner_->parent())
delete resize_corner_;
}
void ScrollView::SetContents(View* a_view) {
if (contents_ && contents_ != a_view) {
viewport_->RemoveChildView(contents_);
delete contents_;
contents_ = NULL;
}
if (a_view) {
contents_ = a_view;
viewport_->AddChildView(contents_);
}
Layout();
}
View* ScrollView::GetContents() const {
return contents_;
}
void ScrollView::Init(ScrollBar* horizontal_scrollbar,
ScrollBar* vertical_scrollbar,
View* resize_corner) {
DCHECK(horizontal_scrollbar && vertical_scrollbar);
contents_ = NULL;
horiz_sb_ = horizontal_scrollbar;
vert_sb_ = vertical_scrollbar;
resize_corner_ = resize_corner;
viewport_ = new Viewport();
AddChildView(viewport_);
// Don't add the scrollbars as children until we discover we need them
// (ShowOrHideScrollBar).
horiz_sb_->SetVisible(false);
horiz_sb_->SetController(this);
vert_sb_->SetVisible(false);
vert_sb_->SetController(this);
if (resize_corner_)
resize_corner_->SetVisible(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->IsVisible()) {
AddChildView(control);
control->SetVisible(true);
}
} else {
RemoveChildView(control);
control->SetVisible(false);
}
}
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;
}
}
void ScrollView::Layout() {
// Most views will want to auto-fit the available space. Most of them want to
// use the 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 = GetLocalBounds();
// Realign it to 0 so it can be used as-is for SetBounds().
viewport_bounds.set_origin(gfx::Point(0, 0));
// viewport_size is the total client space available.
gfx::Size viewport_size = viewport_bounds.size();
if (viewport_bounds.IsEmpty()) {
// There's nothing to layout.
return;
}
// Assumes a vertical scrollbar since most 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.
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 resize_corner_required = resize_corner_ && horiz_sb_required &&
vert_sb_required;
// Take action.
SetControlVisibility(horiz_sb_, horiz_sb_required);
SetControlVisibility(vert_sb_, vert_sb_required);
SetControlVisibility(resize_corner_, resize_corner_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) {
horiz_sb_->SetBounds(0,
viewport_bounds.bottom(),
viewport_bounds.right(),
horiz_sb_height);
}
if (vert_sb_required) {
vert_sb_->SetBounds(viewport_bounds.right(),
0,
vert_sb_width,
viewport_bounds.bottom());
}
if (resize_corner_required) {
// Show the resize corner.
resize_corner_->SetBounds(viewport_bounds.right(),
viewport_bounds.bottom(),
vert_sb_width,
horiz_sb_height);
}
// Update to the real client size with the visible scrollbars.
viewport_->SetBoundsRect(viewport_bounds);
if (should_layout_contents && contents_)
contents_->Layout();
CheckScrollBounds();
SchedulePaint();
UpdateScrollBarPositions();
}
int ScrollView::CheckScrollBounds(int viewport_size,
int content_size,
int current_pos) {
int max = std::max(content_size - viewport_size, 0);
if (current_pos < 0)
current_pos = 0;
else if (current_pos > max)
current_pos = max;
return current_pos;
}
void ScrollView::CheckScrollBounds() {
if (contents_) {
int x, y;
x = CheckScrollBounds(viewport_->width(),
contents_->width(),
-contents_->x());
y = CheckScrollBounds(viewport_->height(),
contents_->height(),
-contents_->y());
// This is no op if bounds are the same
contents_->SetBounds(-x, -y, contents_->width(), contents_->height());
}
}
gfx::Rect ScrollView::GetVisibleRect() const {
if (!contents_)
return gfx::Rect();
const int x =
(horiz_sb_ && horiz_sb_->IsVisible()) ? horiz_sb_->GetPosition() : 0;
const int y =
(vert_sb_ && vert_sb_->IsVisible()) ? vert_sb_->GetPosition() : 0;
return gfx::Rect(x, y, viewport_->width(), viewport_->height());
}
void ScrollView::ScrollContentsRegionToBeVisible(const gfx::Rect& rect) {
if (!contents_ || ((!horiz_sb_ || !horiz_sb_->IsVisible()) &&
(!vert_sb_ || !vert_sb_->IsVisible()))) {
return;
}
// Figure out the maximums for this scroll view.
const int contents_max_x =
std::max(viewport_->width(), contents_->width());
const int contents_max_y =
std::max(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(), viewport_->width()));
const int max_y = std::min(contents_max_y,
y + std::min(rect.height(), 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 - viewport_->width());
const int new_y =
(vis_rect.y() > y) ? y : std::max(0, max_y - viewport_->height());
contents_->SetX(-new_x);
contents_->SetY(-new_y);
UpdateScrollBarPositions();
}
void ScrollView::UpdateScrollBarPositions() {
if (!contents_) {
return;
}
if (horiz_sb_->IsVisible()) {
int vw = viewport_->width();
int cw = contents_->width();
int origin = contents_->x();
horiz_sb_->Update(vw, cw, -origin);
}
if (vert_sb_->IsVisible()) {
int vh = viewport_->height();
int ch = contents_->height();
int origin = contents_->y();
vert_sb_->Update(vh, ch, -origin);
}
}
// TODO(ACW): We should really use ScrollWindowEx as needed
void ScrollView::ScrollToPosition(ScrollBar* source, int position) {
if (!contents_)
return;
if (source == horiz_sb_ && horiz_sb_->IsVisible()) {
int vw = viewport_->width();
int cw = contents_->width();
int origin = contents_->x();
if (-origin != position) {
int max_pos = std::max(0, cw - vw);
if (position < 0)
position = 0;
else if (position > max_pos)
position = max_pos;
contents_->SetX(-position);
contents_->SchedulePaintInRect(contents_->GetVisibleBounds());
}
} else if (source == vert_sb_ && vert_sb_->IsVisible()) {
int vh = viewport_->height();
int ch = contents_->height();
int origin = contents_->y();
if (-origin != position) {
int max_pos = std::max(0, ch - vh);
if (position < 0)
position = 0;
else if (position > max_pos)
position = max_pos;
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;
View* view = GetContents();
if (view) {
if (is_page)
amount = view->GetPageScrollIncrement(this, is_horizontal, is_positive);
else
amount = view->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 ? viewport_->width() : viewport_->height();
return is_horizontal ? viewport_->width() / 5 : viewport_->height() / 5;
}
bool ScrollView::OnKeyPressed(const KeyEvent& event) {
bool processed = false;
// Give vertical scrollbar priority
if (vert_sb_->IsVisible()) {
processed = vert_sb_->OnKeyPressed(event);
}
if (!processed && horiz_sb_->IsVisible()) {
processed = horiz_sb_->OnKeyPressed(event);
}
return processed;
}
bool ScrollView::OnMouseWheel(const MouseWheelEvent& e) {
bool processed = false;
// Give vertical scrollbar priority
if (vert_sb_->IsVisible()) {
processed = vert_sb_->OnMouseWheel(e);
}
if (!processed && horiz_sb_->IsVisible()) {
processed = horiz_sb_->OnMouseWheel(e);
}
return processed;
}
std::string ScrollView::GetClassName() const {
return kViewClassName;
}
int ScrollView::GetScrollBarWidth() const {
return vert_sb_->GetLayoutSize();
}
int ScrollView::GetScrollBarHeight() const {
return horiz_sb_->GetLayoutSize();
}
// 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->GetContents()->y());
int vis_height = scroll_view->GetContents()->parent()->height();
if (is_positive) {
// Align the bottom most row to the top of the view.
int bottom = std::min(scroll_view->GetContents()->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->GetContents()->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