Home | History | Annotate | Download | only in controls
      1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 #include "ui/views/controls/scroll_view.h"
      6 
      7 #include "base/logging.h"
      8 #include "ui/base/events/event.h"
      9 #include "ui/native_theme/native_theme.h"
     10 #include "ui/views/border.h"
     11 #include "ui/views/controls/scrollbar/native_scroll_bar.h"
     12 #include "ui/views/widget/root_view.h"
     13 
     14 namespace views {
     15 
     16 const char ScrollView::kViewClassName[] = "ScrollView";
     17 
     18 namespace {
     19 
     20 // Subclass of ScrollView that resets the border when the theme changes.
     21 class ScrollViewWithBorder : public views::ScrollView {
     22  public:
     23   ScrollViewWithBorder() {
     24     SetThemeSpecificState();
     25   }
     26 
     27   // View overrides;
     28   virtual void OnNativeThemeChanged(const ui::NativeTheme* theme) OVERRIDE {
     29     SetThemeSpecificState();
     30   }
     31 
     32  private:
     33   void SetThemeSpecificState() {
     34     set_border(Border::CreateSolidBorder(
     35         1, GetNativeTheme()->GetSystemColor(
     36             ui::NativeTheme::kColorId_UnfocusedBorderColor)));
     37   }
     38 
     39   DISALLOW_COPY_AND_ASSIGN(ScrollViewWithBorder);
     40 };
     41 
     42 // Returns the position for the view so that it isn't scrolled off the visible
     43 // region.
     44 int CheckScrollBounds(int viewport_size, int content_size, int current_pos) {
     45   int max = std::max(content_size - viewport_size, 0);
     46   if (current_pos < 0)
     47     return 0;
     48   if (current_pos > max)
     49     return max;
     50   return current_pos;
     51 }
     52 
     53 // Make sure the content is not scrolled out of bounds
     54 void CheckScrollBounds(View* viewport, View* view) {
     55   if (!view)
     56     return;
     57 
     58   int x = CheckScrollBounds(viewport->width(), view->width(), -view->x());
     59   int y = CheckScrollBounds(viewport->height(), view->height(), -view->y());
     60 
     61   // This is no op if bounds are the same
     62   view->SetBounds(-x, -y, view->width(), view->height());
     63 }
     64 
     65 // Used by ScrollToPosition() to make sure the new position fits within the
     66 // allowed scroll range.
     67 int AdjustPosition(int current_position,
     68                    int new_position,
     69                    int content_size,
     70                    int viewport_size) {
     71   if (-current_position == new_position)
     72     return new_position;
     73   if (new_position < 0)
     74     return 0;
     75   const int max_position = std::max(0, content_size - viewport_size);
     76   return (new_position > max_position) ? max_position : new_position;
     77 }
     78 
     79 }  // namespace
     80 
     81 // Viewport contains the contents View of the ScrollView.
     82 class ScrollView::Viewport : public View {
     83  public:
     84   Viewport() {}
     85   virtual ~Viewport() {}
     86 
     87   virtual const char* GetClassName() const OVERRIDE {
     88     return "ScrollView::Viewport";
     89   }
     90 
     91   virtual void ScrollRectToVisible(const gfx::Rect& rect) OVERRIDE {
     92     if (!has_children() || !parent())
     93       return;
     94 
     95     View* contents = child_at(0);
     96     gfx::Rect scroll_rect(rect);
     97     scroll_rect.Offset(-contents->x(), -contents->y());
     98     static_cast<ScrollView*>(parent())->ScrollContentsRegionToBeVisible(
     99         scroll_rect);
    100   }
    101 
    102   virtual void ChildPreferredSizeChanged(View* child) OVERRIDE {
    103     if (parent())
    104       parent()->Layout();
    105   }
    106 
    107  private:
    108   DISALLOW_COPY_AND_ASSIGN(Viewport);
    109 };
    110 
    111 ScrollView::ScrollView()
    112     : contents_(NULL),
    113       contents_viewport_(new Viewport()),
    114       header_(NULL),
    115       header_viewport_(new Viewport()),
    116       horiz_sb_(new NativeScrollBar(true)),
    117       vert_sb_(new NativeScrollBar(false)),
    118       resize_corner_(NULL),
    119       hide_horizontal_scrollbar_(false) {
    120   set_notify_enter_exit_on_child(true);
    121 
    122   AddChildView(contents_viewport_);
    123   AddChildView(header_viewport_);
    124 
    125   // Don't add the scrollbars as children until we discover we need them
    126   // (ShowOrHideScrollBar).
    127   horiz_sb_->SetVisible(false);
    128   horiz_sb_->set_controller(this);
    129   vert_sb_->SetVisible(false);
    130   vert_sb_->set_controller(this);
    131   if (resize_corner_)
    132     resize_corner_->SetVisible(false);
    133 }
    134 
    135 ScrollView::~ScrollView() {
    136   // The scrollbars may not have been added, delete them to ensure they get
    137   // deleted.
    138   delete horiz_sb_;
    139   delete vert_sb_;
    140 
    141   if (resize_corner_ && !resize_corner_->parent())
    142     delete resize_corner_;
    143 }
    144 
    145 // static
    146 ScrollView* ScrollView::CreateScrollViewWithBorder() {
    147   return new ScrollViewWithBorder();
    148 }
    149 
    150 void ScrollView::SetContents(View* a_view) {
    151   SetHeaderOrContents(contents_viewport_, a_view, &contents_);
    152 }
    153 
    154 void ScrollView::SetHeader(View* header) {
    155   SetHeaderOrContents(header_viewport_, header, &header_);
    156 }
    157 
    158 gfx::Rect ScrollView::GetVisibleRect() const {
    159   if (!contents_)
    160     return gfx::Rect();
    161   return gfx::Rect(-contents_->x(), -contents_->y(),
    162                    contents_viewport_->width(), contents_viewport_->height());
    163 }
    164 
    165 int ScrollView::GetScrollBarWidth() const {
    166   return vert_sb_ ? vert_sb_->GetLayoutSize() : 0;
    167 }
    168 
    169 int ScrollView::GetScrollBarHeight() const {
    170   return horiz_sb_ ? horiz_sb_->GetLayoutSize() : 0;
    171 }
    172 
    173 void ScrollView::SetHorizontalScrollBar(ScrollBar* horiz_sb) {
    174   DCHECK(horiz_sb);
    175   horiz_sb->SetVisible(horiz_sb_->visible());
    176   delete horiz_sb_;
    177   horiz_sb->set_controller(this);
    178   horiz_sb_ = horiz_sb;
    179 }
    180 
    181 void ScrollView::SetVerticalScrollBar(ScrollBar* vert_sb) {
    182   DCHECK(vert_sb);
    183   vert_sb->SetVisible(vert_sb_->visible());
    184   delete vert_sb_;
    185   vert_sb->set_controller(this);
    186   vert_sb_ = vert_sb;
    187 }
    188 
    189 void ScrollView::Layout() {
    190   // Most views will want to auto-fit the available space. Most of them want to
    191   // use all available width (without overflowing) and only overflow in
    192   // height. Examples are HistoryView, MostVisitedView, DownloadTabView, etc.
    193   // Other views want to fit in both ways. An example is PrintView. To make both
    194   // happy, assume a vertical scrollbar but no horizontal scrollbar. To override
    195   // this default behavior, the inner view has to calculate the available space,
    196   // used ComputeScrollBarsVisibility() to use the same calculation that is done
    197   // here and sets its bound to fit within.
    198   gfx::Rect viewport_bounds = GetContentsBounds();
    199   const int contents_x = viewport_bounds.x();
    200   const int contents_y = viewport_bounds.y();
    201   if (viewport_bounds.IsEmpty()) {
    202     // There's nothing to layout.
    203     return;
    204   }
    205 
    206   const int header_height =
    207       std::min(viewport_bounds.height(),
    208                header_ ? header_->GetPreferredSize().height() : 0);
    209   viewport_bounds.set_height(
    210       std::max(0, viewport_bounds.height() - header_height));
    211   viewport_bounds.set_y(viewport_bounds.y() + header_height);
    212   // viewport_size is the total client space available.
    213   gfx::Size viewport_size = viewport_bounds.size();
    214   // Assumes a vertical scrollbar since most of the current views are designed
    215   // for this.
    216   int horiz_sb_height = GetScrollBarHeight();
    217   int vert_sb_width = GetScrollBarWidth();
    218   viewport_bounds.set_width(viewport_bounds.width() - vert_sb_width);
    219   // Update the bounds right now so the inner views can fit in it.
    220   contents_viewport_->SetBoundsRect(viewport_bounds);
    221 
    222   // Give |contents_| a chance to update its bounds if it depends on the
    223   // viewport.
    224   if (contents_)
    225     contents_->Layout();
    226 
    227   bool should_layout_contents = false;
    228   bool horiz_sb_required = false;
    229   bool vert_sb_required = false;
    230   if (contents_) {
    231     gfx::Size content_size = contents_->size();
    232     ComputeScrollBarsVisibility(viewport_size,
    233                                 content_size,
    234                                 &horiz_sb_required,
    235                                 &vert_sb_required);
    236   }
    237   bool resize_corner_required = resize_corner_ && horiz_sb_required &&
    238                                 vert_sb_required;
    239   // Take action.
    240   SetControlVisibility(horiz_sb_, horiz_sb_required);
    241   SetControlVisibility(vert_sb_, vert_sb_required);
    242   SetControlVisibility(resize_corner_, resize_corner_required);
    243 
    244   // Non-default.
    245   if (horiz_sb_required) {
    246     viewport_bounds.set_height(
    247         std::max(0, viewport_bounds.height() - horiz_sb_height));
    248     should_layout_contents = true;
    249   }
    250   // Default.
    251   if (!vert_sb_required) {
    252     viewport_bounds.set_width(viewport_bounds.width() + vert_sb_width);
    253     should_layout_contents = true;
    254   }
    255 
    256   if (horiz_sb_required) {
    257     int height_offset = horiz_sb_->GetContentOverlapSize();
    258     horiz_sb_->SetBounds(0,
    259                          viewport_bounds.bottom() - height_offset,
    260                          viewport_bounds.right(),
    261                          horiz_sb_height + height_offset);
    262   }
    263   if (vert_sb_required) {
    264     int width_offset = vert_sb_->GetContentOverlapSize();
    265     vert_sb_->SetBounds(viewport_bounds.right() - width_offset,
    266                         0,
    267                         vert_sb_width + width_offset,
    268                         viewport_bounds.bottom());
    269   }
    270   if (resize_corner_required) {
    271     // Show the resize corner.
    272     resize_corner_->SetBounds(viewport_bounds.right(),
    273                               viewport_bounds.bottom(),
    274                               vert_sb_width,
    275                               horiz_sb_height);
    276   }
    277 
    278   // Update to the real client size with the visible scrollbars.
    279   contents_viewport_->SetBoundsRect(viewport_bounds);
    280   if (should_layout_contents && contents_)
    281     contents_->Layout();
    282 
    283   header_viewport_->SetBounds(contents_x, contents_y,
    284                               viewport_bounds.width(), header_height);
    285   if (header_)
    286     header_->Layout();
    287 
    288   CheckScrollBounds(header_viewport_, header_);
    289   CheckScrollBounds(contents_viewport_, contents_);
    290   SchedulePaint();
    291   UpdateScrollBarPositions();
    292 }
    293 
    294 bool ScrollView::OnKeyPressed(const ui::KeyEvent& event) {
    295   bool processed = false;
    296 
    297   // Give vertical scrollbar priority
    298   if (vert_sb_->visible())
    299     processed = vert_sb_->OnKeyPressed(event);
    300 
    301   if (!processed && horiz_sb_->visible())
    302     processed = horiz_sb_->OnKeyPressed(event);
    303 
    304   return processed;
    305 }
    306 
    307 bool ScrollView::OnMouseWheel(const ui::MouseWheelEvent& e) {
    308   bool processed = false;
    309   // Give vertical scrollbar priority
    310   if (vert_sb_->visible())
    311     processed = vert_sb_->OnMouseWheel(e);
    312 
    313   if (!processed && horiz_sb_->visible())
    314     processed = horiz_sb_->OnMouseWheel(e);
    315 
    316   return processed;
    317 }
    318 
    319 void ScrollView::OnMouseEntered(const ui::MouseEvent& event) {
    320   if (horiz_sb_)
    321     horiz_sb_->OnMouseEnteredScrollView(event);
    322   if (vert_sb_)
    323     vert_sb_->OnMouseEnteredScrollView(event);
    324 }
    325 
    326 void ScrollView::OnMouseExited(const ui::MouseEvent& event) {
    327   if (horiz_sb_)
    328     horiz_sb_->OnMouseExitedScrollView(event);
    329   if (vert_sb_)
    330     vert_sb_->OnMouseExitedScrollView(event);
    331 }
    332 
    333 void ScrollView::OnGestureEvent(ui::GestureEvent* event) {
    334   // If the event happened on one of the scrollbars, then those events are
    335   // sent directly to the scrollbars. Otherwise, only scroll events are sent to
    336   // the scrollbars.
    337   bool scroll_event = event->type() == ui::ET_GESTURE_SCROLL_UPDATE ||
    338                       event->type() == ui::ET_GESTURE_SCROLL_BEGIN ||
    339                       event->type() == ui::ET_GESTURE_SCROLL_END ||
    340                       event->type() == ui::ET_SCROLL_FLING_START;
    341 
    342   if (vert_sb_->visible()) {
    343     if (vert_sb_->bounds().Contains(event->location()) || scroll_event)
    344       vert_sb_->OnGestureEvent(event);
    345   }
    346   if (!event->handled() && horiz_sb_->visible()) {
    347     if (horiz_sb_->bounds().Contains(event->location()) || scroll_event)
    348       horiz_sb_->OnGestureEvent(event);
    349   }
    350 }
    351 
    352 const char* ScrollView::GetClassName() const {
    353   return kViewClassName;
    354 }
    355 
    356 void ScrollView::ScrollToPosition(ScrollBar* source, int position) {
    357   if (!contents_)
    358     return;
    359 
    360   if (source == horiz_sb_ && horiz_sb_->visible()) {
    361     position = AdjustPosition(contents_->x(), position, contents_->width(),
    362                               contents_viewport_->width());
    363     if (-contents_->x() == position)
    364       return;
    365     contents_->SetX(-position);
    366     if (header_) {
    367       header_->SetX(-position);
    368       header_->SchedulePaintInRect(header_->GetVisibleBounds());
    369     }
    370   } else if (source == vert_sb_ && vert_sb_->visible()) {
    371     position = AdjustPosition(contents_->y(), position, contents_->height(),
    372                               contents_viewport_->height());
    373     if (-contents_->y() == position)
    374       return;
    375     contents_->SetY(-position);
    376   }
    377   contents_->SchedulePaintInRect(contents_->GetVisibleBounds());
    378 }
    379 
    380 int ScrollView::GetScrollIncrement(ScrollBar* source, bool is_page,
    381                                    bool is_positive) {
    382   bool is_horizontal = source->IsHorizontal();
    383   int amount = 0;
    384   if (contents_) {
    385     if (is_page) {
    386       amount = contents_->GetPageScrollIncrement(
    387           this, is_horizontal, is_positive);
    388     } else {
    389       amount = contents_->GetLineScrollIncrement(
    390           this, is_horizontal, is_positive);
    391     }
    392     if (amount > 0)
    393       return amount;
    394   }
    395   // No view, or the view didn't return a valid amount.
    396   if (is_page) {
    397     return is_horizontal ? contents_viewport_->width() :
    398                            contents_viewport_->height();
    399   }
    400   return is_horizontal ? contents_viewport_->width() / 5 :
    401                          contents_viewport_->height() / 5;
    402 }
    403 
    404 void ScrollView::SetHeaderOrContents(View* parent,
    405                                      View* new_view,
    406                                      View** member) {
    407   if (*member == new_view)
    408     return;
    409 
    410   delete *member;
    411   *member = new_view;
    412   if (*member)
    413     parent->AddChildView(*member);
    414   Layout();
    415 }
    416 
    417 void ScrollView::ScrollContentsRegionToBeVisible(const gfx::Rect& rect) {
    418   if (!contents_ || (!horiz_sb_->visible() && !vert_sb_->visible()))
    419     return;
    420 
    421   // Figure out the maximums for this scroll view.
    422   const int contents_max_x =
    423       std::max(contents_viewport_->width(), contents_->width());
    424   const int contents_max_y =
    425       std::max(contents_viewport_->height(), contents_->height());
    426 
    427   // Make sure x and y are within the bounds of [0,contents_max_*].
    428   int x = std::max(0, std::min(contents_max_x, rect.x()));
    429   int y = std::max(0, std::min(contents_max_y, rect.y()));
    430 
    431   // Figure out how far and down the rectangle will go taking width
    432   // and height into account.  This will be "clipped" by the viewport.
    433   const int max_x = std::min(contents_max_x,
    434       x + std::min(rect.width(), contents_viewport_->width()));
    435   const int max_y = std::min(contents_max_y,
    436       y + std::min(rect.height(), contents_viewport_->height()));
    437 
    438   // See if the rect is already visible. Note the width is (max_x - x)
    439   // and the height is (max_y - y) to take into account the clipping of
    440   // either viewport or the content size.
    441   const gfx::Rect vis_rect = GetVisibleRect();
    442   if (vis_rect.Contains(gfx::Rect(x, y, max_x - x, max_y - y)))
    443     return;
    444 
    445   // Shift contents_'s X and Y so that the region is visible. If we
    446   // need to shift up or left from where we currently are then we need
    447   // to get it so that the content appears in the upper/left
    448   // corner. This is done by setting the offset to -X or -Y.  For down
    449   // or right shifts we need to make sure it appears in the
    450   // lower/right corner. This is calculated by taking max_x or max_y
    451   // and scaling it back by the size of the viewport.
    452   const int new_x =
    453       (vis_rect.x() > x) ? x : std::max(0, max_x - contents_viewport_->width());
    454   const int new_y =
    455       (vis_rect.y() > y) ? y : std::max(0, max_y -
    456                                         contents_viewport_->height());
    457 
    458   contents_->SetX(-new_x);
    459   if (header_)
    460     header_->SetX(-new_x);
    461   contents_->SetY(-new_y);
    462   UpdateScrollBarPositions();
    463 }
    464 
    465 void ScrollView::ComputeScrollBarsVisibility(const gfx::Size& vp_size,
    466                                              const gfx::Size& content_size,
    467                                              bool* horiz_is_shown,
    468                                              bool* vert_is_shown) const {
    469   // Try to fit both ways first, then try vertical bar only, then horizontal
    470   // bar only, then defaults to both shown.
    471   if (content_size.width() <= vp_size.width() &&
    472       content_size.height() <= vp_size.height()) {
    473     *horiz_is_shown = false;
    474     *vert_is_shown = false;
    475   } else if (content_size.width() <= vp_size.width() - GetScrollBarWidth()) {
    476     *horiz_is_shown = false;
    477     *vert_is_shown = true;
    478   } else if (content_size.height() <= vp_size.height() - GetScrollBarHeight()) {
    479     *horiz_is_shown = true;
    480     *vert_is_shown = false;
    481   } else {
    482     *horiz_is_shown = true;
    483     *vert_is_shown = true;
    484   }
    485 
    486   if (hide_horizontal_scrollbar_)
    487     *horiz_is_shown = false;
    488 }
    489 
    490 // Make sure that a single scrollbar is created and visible as needed
    491 void ScrollView::SetControlVisibility(View* control, bool should_show) {
    492   if (!control)
    493     return;
    494   if (should_show) {
    495     if (!control->visible()) {
    496       AddChildView(control);
    497       control->SetVisible(true);
    498     }
    499   } else {
    500     RemoveChildView(control);
    501     control->SetVisible(false);
    502   }
    503 }
    504 
    505 void ScrollView::UpdateScrollBarPositions() {
    506   if (!contents_)
    507     return;
    508 
    509   if (horiz_sb_->visible()) {
    510     int vw = contents_viewport_->width();
    511     int cw = contents_->width();
    512     int origin = contents_->x();
    513     horiz_sb_->Update(vw, cw, -origin);
    514   }
    515   if (vert_sb_->visible()) {
    516     int vh = contents_viewport_->height();
    517     int ch = contents_->height();
    518     int origin = contents_->y();
    519     vert_sb_->Update(vh, ch, -origin);
    520   }
    521 }
    522 
    523 // VariableRowHeightScrollHelper ----------------------------------------------
    524 
    525 VariableRowHeightScrollHelper::VariableRowHeightScrollHelper(
    526     Controller* controller) : controller_(controller) {
    527 }
    528 
    529 VariableRowHeightScrollHelper::~VariableRowHeightScrollHelper() {
    530 }
    531 
    532 int VariableRowHeightScrollHelper::GetPageScrollIncrement(
    533     ScrollView* scroll_view, bool is_horizontal, bool is_positive) {
    534   if (is_horizontal)
    535     return 0;
    536   // y coordinate is most likely negative.
    537   int y = abs(scroll_view->contents()->y());
    538   int vis_height = scroll_view->contents()->parent()->height();
    539   if (is_positive) {
    540     // Align the bottom most row to the top of the view.
    541     int bottom = std::min(scroll_view->contents()->height() - 1,
    542                           y + vis_height);
    543     RowInfo bottom_row_info = GetRowInfo(bottom);
    544     // If 0, ScrollView will provide a default value.
    545     return std::max(0, bottom_row_info.origin - y);
    546   } else {
    547     // Align the row on the previous page to to the top of the view.
    548     int last_page_y = y - vis_height;
    549     RowInfo last_page_info = GetRowInfo(std::max(0, last_page_y));
    550     if (last_page_y != last_page_info.origin)
    551       return std::max(0, y - last_page_info.origin - last_page_info.height);
    552     return std::max(0, y - last_page_info.origin);
    553   }
    554 }
    555 
    556 int VariableRowHeightScrollHelper::GetLineScrollIncrement(
    557     ScrollView* scroll_view, bool is_horizontal, bool is_positive) {
    558   if (is_horizontal)
    559     return 0;
    560   // y coordinate is most likely negative.
    561   int y = abs(scroll_view->contents()->y());
    562   RowInfo row = GetRowInfo(y);
    563   if (is_positive) {
    564     return row.height - (y - row.origin);
    565   } else if (y == row.origin) {
    566     row = GetRowInfo(std::max(0, row.origin - 1));
    567     return y - row.origin;
    568   } else {
    569     return y - row.origin;
    570   }
    571 }
    572 
    573 VariableRowHeightScrollHelper::RowInfo
    574     VariableRowHeightScrollHelper::GetRowInfo(int y) {
    575   return controller_->GetRowInfo(y);
    576 }
    577 
    578 // FixedRowHeightScrollHelper -----------------------------------------------
    579 
    580 FixedRowHeightScrollHelper::FixedRowHeightScrollHelper(int top_margin,
    581                                                        int row_height)
    582     : VariableRowHeightScrollHelper(NULL),
    583       top_margin_(top_margin),
    584       row_height_(row_height) {
    585   DCHECK_GT(row_height, 0);
    586 }
    587 
    588 VariableRowHeightScrollHelper::RowInfo
    589     FixedRowHeightScrollHelper::GetRowInfo(int y) {
    590   if (y < top_margin_)
    591     return RowInfo(0, top_margin_);
    592   return RowInfo((y - top_margin_) / row_height_ * row_height_ + top_margin_,
    593                  row_height_);
    594 }
    595 
    596 }  // namespace views
    597