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