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/scrollbar/base_scroll_bar.h"
6
7#include "base/bind.h"
8#include "base/bind_helpers.h"
9#include "base/callback.h"
10#include "base/compiler_specific.h"
11#include "base/message_loop/message_loop.h"
12#include "base/strings/string16.h"
13#include "base/strings/utf_string_conversions.h"
14#include "build/build_config.h"
15#include "ui/base/l10n/l10n_util.h"
16#include "ui/events/event.h"
17#include "ui/events/keycodes/keyboard_codes.h"
18#include "ui/gfx/canvas.h"
19#include "ui/gfx/geometry/safe_integer_conversions.h"
20#include "ui/strings/grit/ui_strings.h"
21#include "ui/views/controls/menu/menu_item_view.h"
22#include "ui/views/controls/menu/menu_runner.h"
23#include "ui/views/controls/scroll_view.h"
24#include "ui/views/controls/scrollbar/base_scroll_bar_thumb.h"
25#include "ui/views/widget/widget.h"
26
27#if defined(OS_LINUX)
28#include "ui/gfx/screen.h"
29#endif
30
31#undef min
32#undef max
33
34namespace views {
35
36///////////////////////////////////////////////////////////////////////////////
37// BaseScrollBar, public:
38
39BaseScrollBar::BaseScrollBar(bool horizontal, BaseScrollBarThumb* thumb)
40    : ScrollBar(horizontal),
41      thumb_(thumb),
42      contents_size_(0),
43      contents_scroll_offset_(0),
44      viewport_size_(0),
45      thumb_track_state_(CustomButton::STATE_NORMAL),
46      last_scroll_amount_(SCROLL_NONE),
47      repeater_(base::Bind(&BaseScrollBar::TrackClicked,
48                           base::Unretained(this))),
49      context_menu_mouse_position_(0) {
50  AddChildView(thumb_);
51
52  set_context_menu_controller(this);
53  thumb_->set_context_menu_controller(this);
54}
55
56void BaseScrollBar::ScrollByAmount(ScrollAmount amount) {
57  int offset = contents_scroll_offset_;
58  switch (amount) {
59    case SCROLL_START:
60      offset = GetMinPosition();
61      break;
62    case SCROLL_END:
63      offset = GetMaxPosition();
64      break;
65    case SCROLL_PREV_LINE:
66      offset -= GetScrollIncrement(false, false);
67      offset = std::max(GetMinPosition(), offset);
68      break;
69    case SCROLL_NEXT_LINE:
70      offset += GetScrollIncrement(false, true);
71      offset = std::min(GetMaxPosition(), offset);
72      break;
73    case SCROLL_PREV_PAGE:
74      offset -= GetScrollIncrement(true, false);
75      offset = std::max(GetMinPosition(), offset);
76      break;
77    case SCROLL_NEXT_PAGE:
78      offset += GetScrollIncrement(true, true);
79      offset = std::min(GetMaxPosition(), offset);
80      break;
81    default:
82      break;
83  }
84  contents_scroll_offset_ = offset;
85  ScrollContentsToOffset();
86}
87
88BaseScrollBar::~BaseScrollBar() {
89}
90
91void BaseScrollBar::ScrollToThumbPosition(int thumb_position,
92                                          bool scroll_to_middle) {
93  contents_scroll_offset_ =
94      CalculateContentsOffset(thumb_position, scroll_to_middle);
95  if (contents_scroll_offset_ < GetMinPosition()) {
96    contents_scroll_offset_ = GetMinPosition();
97  } else if (contents_scroll_offset_ > GetMaxPosition()) {
98    contents_scroll_offset_ = GetMaxPosition();
99  }
100  ScrollContentsToOffset();
101  SchedulePaint();
102}
103
104bool BaseScrollBar::ScrollByContentsOffset(int contents_offset) {
105  int old_offset = contents_scroll_offset_;
106  contents_scroll_offset_ -= contents_offset;
107  if (contents_scroll_offset_ < GetMinPosition()) {
108    contents_scroll_offset_ = GetMinPosition();
109  } else if (contents_scroll_offset_ > GetMaxPosition()) {
110    contents_scroll_offset_ = GetMaxPosition();
111  }
112  if (old_offset == contents_scroll_offset_)
113    return false;
114
115  ScrollContentsToOffset();
116  return true;
117}
118
119void BaseScrollBar::OnThumbStateChanged(CustomButton::ButtonState old_state,
120                                        CustomButton::ButtonState new_state) {
121  if (old_state == CustomButton::STATE_PRESSED &&
122      new_state == CustomButton::STATE_NORMAL &&
123      GetThumbTrackState() == CustomButton::STATE_HOVERED) {
124    SetThumbTrackState(CustomButton::STATE_NORMAL);
125  }
126}
127
128///////////////////////////////////////////////////////////////////////////////
129// BaseScrollBar, View implementation:
130
131bool BaseScrollBar::OnMousePressed(const ui::MouseEvent& event) {
132  if (event.IsOnlyLeftMouseButton())
133    ProcessPressEvent(event);
134  return true;
135}
136
137void BaseScrollBar::OnMouseReleased(const ui::MouseEvent& event) {
138  SetState(HitTestPoint(event.location()) ?
139           CustomButton::STATE_HOVERED : CustomButton::STATE_NORMAL);
140}
141
142void BaseScrollBar::OnMouseCaptureLost() {
143  SetState(CustomButton::STATE_NORMAL);
144}
145
146void BaseScrollBar::OnMouseEntered(const ui::MouseEvent& event) {
147  SetThumbTrackState(CustomButton::STATE_HOVERED);
148}
149
150void BaseScrollBar::OnMouseExited(const ui::MouseEvent& event) {
151  if (GetThumbTrackState() == CustomButton::STATE_HOVERED)
152    SetState(CustomButton::STATE_NORMAL);
153}
154
155bool BaseScrollBar::OnKeyPressed(const ui::KeyEvent& event) {
156  ScrollAmount amount = SCROLL_NONE;
157  switch (event.key_code()) {
158    case ui::VKEY_UP:
159      if (!IsHorizontal())
160        amount = SCROLL_PREV_LINE;
161      break;
162    case ui::VKEY_DOWN:
163      if (!IsHorizontal())
164        amount = SCROLL_NEXT_LINE;
165      break;
166    case ui::VKEY_LEFT:
167      if (IsHorizontal())
168        amount = SCROLL_PREV_LINE;
169      break;
170    case ui::VKEY_RIGHT:
171      if (IsHorizontal())
172        amount = SCROLL_NEXT_LINE;
173      break;
174    case ui::VKEY_PRIOR:
175      amount = SCROLL_PREV_PAGE;
176      break;
177    case ui::VKEY_NEXT:
178      amount = SCROLL_NEXT_PAGE;
179      break;
180    case ui::VKEY_HOME:
181      amount = SCROLL_START;
182      break;
183    case ui::VKEY_END:
184      amount = SCROLL_END;
185      break;
186    default:
187      break;
188  }
189  if (amount != SCROLL_NONE) {
190    ScrollByAmount(amount);
191    return true;
192  }
193  return false;
194}
195
196bool BaseScrollBar::OnMouseWheel(const ui::MouseWheelEvent& event) {
197  OnScroll(event.x_offset(), event.y_offset());
198  return true;
199}
200
201void BaseScrollBar::OnGestureEvent(ui::GestureEvent* event) {
202  // If a fling is in progress, then stop the fling for any incoming gesture
203  // event (except for the GESTURE_END event that is generated at the end of the
204  // fling).
205  if (scroll_animator_.get() && scroll_animator_->is_scrolling() &&
206      (event->type() != ui::ET_GESTURE_END ||
207       event->details().touch_points() > 1)) {
208    scroll_animator_->Stop();
209  }
210
211  if (event->type() == ui::ET_GESTURE_TAP_DOWN) {
212    ProcessPressEvent(*event);
213    event->SetHandled();
214    return;
215  }
216
217  if (event->type() == ui::ET_GESTURE_LONG_PRESS) {
218    // For a long-press, the repeater started in tap-down should continue. So
219    // return early.
220    return;
221  }
222
223  SetState(CustomButton::STATE_NORMAL);
224
225  if (event->type() == ui::ET_GESTURE_TAP) {
226    // TAP_DOWN would have already scrolled some amount. So scrolling again on
227    // TAP is not necessary.
228    event->SetHandled();
229    return;
230  }
231
232  if (event->type() == ui::ET_GESTURE_SCROLL_BEGIN ||
233      event->type() == ui::ET_GESTURE_SCROLL_END) {
234    event->SetHandled();
235    return;
236  }
237
238  if (event->type() == ui::ET_GESTURE_SCROLL_UPDATE) {
239    float scroll_amount_f;
240    int scroll_amount;
241    if (IsHorizontal()) {
242      scroll_amount_f = event->details().scroll_x() - roundoff_error_.x();
243      scroll_amount = gfx::ToRoundedInt(scroll_amount_f);
244      roundoff_error_.set_x(scroll_amount - scroll_amount_f);
245    } else {
246      scroll_amount_f = event->details().scroll_y() - roundoff_error_.y();
247      scroll_amount = gfx::ToRoundedInt(scroll_amount_f);
248      roundoff_error_.set_y(scroll_amount - scroll_amount_f);
249    }
250    if (ScrollByContentsOffset(scroll_amount))
251      event->SetHandled();
252    return;
253  }
254
255  if (event->type() == ui::ET_SCROLL_FLING_START) {
256    if (!scroll_animator_.get())
257      scroll_animator_.reset(new ScrollAnimator(this));
258    scroll_animator_->Start(
259        IsHorizontal() ?  event->details().velocity_x() : 0.f,
260        IsHorizontal() ? 0.f : event->details().velocity_y());
261    event->SetHandled();
262  }
263}
264
265///////////////////////////////////////////////////////////////////////////////
266// BaseScrollBar, ScrollDelegate implementation:
267
268bool BaseScrollBar::OnScroll(float dx, float dy) {
269  return IsHorizontal() ? ScrollByContentsOffset(dx) :
270                          ScrollByContentsOffset(dy);
271}
272
273///////////////////////////////////////////////////////////////////////////////
274// BaseScrollBar, ContextMenuController implementation:
275
276enum ScrollBarContextMenuCommands {
277  ScrollBarContextMenuCommand_ScrollHere = 1,
278  ScrollBarContextMenuCommand_ScrollStart,
279  ScrollBarContextMenuCommand_ScrollEnd,
280  ScrollBarContextMenuCommand_ScrollPageUp,
281  ScrollBarContextMenuCommand_ScrollPageDown,
282  ScrollBarContextMenuCommand_ScrollPrev,
283  ScrollBarContextMenuCommand_ScrollNext
284};
285
286void BaseScrollBar::ShowContextMenuForView(View* source,
287                                           const gfx::Point& p,
288                                           ui::MenuSourceType source_type) {
289  Widget* widget = GetWidget();
290  gfx::Rect widget_bounds = widget->GetWindowBoundsInScreen();
291  gfx::Point temp_pt(p.x() - widget_bounds.x(), p.y() - widget_bounds.y());
292  View::ConvertPointFromWidget(this, &temp_pt);
293  context_menu_mouse_position_ = IsHorizontal() ? temp_pt.x() : temp_pt.y();
294
295  views::MenuItemView* menu = new views::MenuItemView(this);
296  // MenuRunner takes ownership of |menu|.
297  menu_runner_.reset(new MenuRunner(
298      menu, MenuRunner::HAS_MNEMONICS | views::MenuRunner::CONTEXT_MENU));
299  menu->AppendDelegateMenuItem(ScrollBarContextMenuCommand_ScrollHere);
300  menu->AppendSeparator();
301  menu->AppendDelegateMenuItem(ScrollBarContextMenuCommand_ScrollStart);
302  menu->AppendDelegateMenuItem(ScrollBarContextMenuCommand_ScrollEnd);
303  menu->AppendSeparator();
304  menu->AppendDelegateMenuItem(ScrollBarContextMenuCommand_ScrollPageUp);
305  menu->AppendDelegateMenuItem(ScrollBarContextMenuCommand_ScrollPageDown);
306  menu->AppendSeparator();
307  menu->AppendDelegateMenuItem(ScrollBarContextMenuCommand_ScrollPrev);
308  menu->AppendDelegateMenuItem(ScrollBarContextMenuCommand_ScrollNext);
309  if (menu_runner_->RunMenuAt(GetWidget(),
310                              NULL,
311                              gfx::Rect(p, gfx::Size()),
312                              MENU_ANCHOR_TOPLEFT,
313                              source_type) == MenuRunner::MENU_DELETED) {
314    return;
315  }
316}
317
318///////////////////////////////////////////////////////////////////////////////
319// BaseScrollBar, Menu::Delegate implementation:
320
321base::string16 BaseScrollBar::GetLabel(int id) const {
322  int ids_value = 0;
323  switch (id) {
324    case ScrollBarContextMenuCommand_ScrollHere:
325      ids_value = IDS_APP_SCROLLBAR_CXMENU_SCROLLHERE;
326      break;
327    case ScrollBarContextMenuCommand_ScrollStart:
328      ids_value = IsHorizontal() ? IDS_APP_SCROLLBAR_CXMENU_SCROLLLEFTEDGE
329                                 : IDS_APP_SCROLLBAR_CXMENU_SCROLLHOME;
330      break;
331    case ScrollBarContextMenuCommand_ScrollEnd:
332      ids_value = IsHorizontal() ? IDS_APP_SCROLLBAR_CXMENU_SCROLLRIGHTEDGE
333                                 : IDS_APP_SCROLLBAR_CXMENU_SCROLLEND;
334      break;
335    case ScrollBarContextMenuCommand_ScrollPageUp:
336      ids_value = IDS_APP_SCROLLBAR_CXMENU_SCROLLPAGEUP;
337      break;
338    case ScrollBarContextMenuCommand_ScrollPageDown:
339      ids_value = IDS_APP_SCROLLBAR_CXMENU_SCROLLPAGEDOWN;
340      break;
341    case ScrollBarContextMenuCommand_ScrollPrev:
342      ids_value = IsHorizontal() ? IDS_APP_SCROLLBAR_CXMENU_SCROLLLEFT
343                                 : IDS_APP_SCROLLBAR_CXMENU_SCROLLUP;
344      break;
345    case ScrollBarContextMenuCommand_ScrollNext:
346      ids_value = IsHorizontal() ? IDS_APP_SCROLLBAR_CXMENU_SCROLLRIGHT
347                                 : IDS_APP_SCROLLBAR_CXMENU_SCROLLDOWN;
348      break;
349    default:
350      NOTREACHED() << "Invalid BaseScrollBar Context Menu command!";
351  }
352
353  return ids_value ? l10n_util::GetStringUTF16(ids_value) : base::string16();
354}
355
356bool BaseScrollBar::IsCommandEnabled(int id) const {
357  switch (id) {
358    case ScrollBarContextMenuCommand_ScrollPageUp:
359    case ScrollBarContextMenuCommand_ScrollPageDown:
360      return !IsHorizontal();
361  }
362  return true;
363}
364
365void BaseScrollBar::ExecuteCommand(int id) {
366  switch (id) {
367    case ScrollBarContextMenuCommand_ScrollHere:
368      ScrollToThumbPosition(context_menu_mouse_position_, true);
369      break;
370    case ScrollBarContextMenuCommand_ScrollStart:
371      ScrollByAmount(SCROLL_START);
372      break;
373    case ScrollBarContextMenuCommand_ScrollEnd:
374      ScrollByAmount(SCROLL_END);
375      break;
376    case ScrollBarContextMenuCommand_ScrollPageUp:
377      ScrollByAmount(SCROLL_PREV_PAGE);
378      break;
379    case ScrollBarContextMenuCommand_ScrollPageDown:
380      ScrollByAmount(SCROLL_NEXT_PAGE);
381      break;
382    case ScrollBarContextMenuCommand_ScrollPrev:
383      ScrollByAmount(SCROLL_PREV_LINE);
384      break;
385    case ScrollBarContextMenuCommand_ScrollNext:
386      ScrollByAmount(SCROLL_NEXT_LINE);
387      break;
388  }
389}
390
391///////////////////////////////////////////////////////////////////////////////
392// BaseScrollBar, ScrollBar implementation:
393
394void BaseScrollBar::Update(int viewport_size,
395                           int content_size,
396                           int contents_scroll_offset) {
397  ScrollBar::Update(viewport_size, content_size, contents_scroll_offset);
398
399  // Make sure contents_size is always > 0 to avoid divide by zero errors in
400  // calculations throughout this code.
401  contents_size_ = std::max(1, content_size);
402
403  viewport_size_ = std::max(1, viewport_size);
404
405  if (content_size < 0)
406    content_size = 0;
407  if (contents_scroll_offset < 0)
408    contents_scroll_offset = 0;
409  if (contents_scroll_offset > content_size)
410    contents_scroll_offset = content_size;
411  contents_scroll_offset_ = contents_scroll_offset;
412
413  // Thumb Height and Thumb Pos.
414  // The height of the thumb is the ratio of the Viewport height to the
415  // content size multiplied by the height of the thumb track.
416  double ratio = static_cast<double>(viewport_size) / contents_size_;
417  int thumb_size = static_cast<int>(ratio * GetTrackSize());
418  thumb_->SetSize(thumb_size);
419
420  int thumb_position = CalculateThumbPosition(contents_scroll_offset);
421  thumb_->SetPosition(thumb_position);
422}
423
424int BaseScrollBar::GetPosition() const {
425  return thumb_->GetPosition();
426}
427
428///////////////////////////////////////////////////////////////////////////////
429// BaseScrollBar, protected:
430
431BaseScrollBarThumb* BaseScrollBar::GetThumb() const {
432  return thumb_;
433}
434
435CustomButton::ButtonState BaseScrollBar::GetThumbTrackState() const {
436  return thumb_track_state_;
437}
438
439void BaseScrollBar::ScrollToPosition(int position) {
440  controller()->ScrollToPosition(this, position);
441}
442
443int BaseScrollBar::GetScrollIncrement(bool is_page, bool is_positive) {
444  return controller()->GetScrollIncrement(this, is_page, is_positive);
445}
446
447///////////////////////////////////////////////////////////////////////////////
448// BaseScrollBar, private:
449
450int BaseScrollBar::GetThumbSizeForTest() {
451  return thumb_->GetSize();
452}
453
454void BaseScrollBar::ProcessPressEvent(const ui::LocatedEvent& event) {
455  SetThumbTrackState(CustomButton::STATE_PRESSED);
456  gfx::Rect thumb_bounds = thumb_->bounds();
457  if (IsHorizontal()) {
458    if (GetMirroredXInView(event.x()) < thumb_bounds.x()) {
459      last_scroll_amount_ = SCROLL_PREV_PAGE;
460    } else if (GetMirroredXInView(event.x()) > thumb_bounds.right()) {
461      last_scroll_amount_ = SCROLL_NEXT_PAGE;
462    }
463  } else {
464    if (event.y() < thumb_bounds.y()) {
465      last_scroll_amount_ = SCROLL_PREV_PAGE;
466    } else if (event.y() > thumb_bounds.bottom()) {
467      last_scroll_amount_ = SCROLL_NEXT_PAGE;
468    }
469  }
470  TrackClicked();
471  repeater_.Start();
472}
473
474void BaseScrollBar::SetState(CustomButton::ButtonState state) {
475  SetThumbTrackState(state);
476  repeater_.Stop();
477}
478
479void BaseScrollBar::TrackClicked() {
480  if (last_scroll_amount_ != SCROLL_NONE)
481    ScrollByAmount(last_scroll_amount_);
482}
483
484void BaseScrollBar::ScrollContentsToOffset() {
485  ScrollToPosition(contents_scroll_offset_);
486  thumb_->SetPosition(CalculateThumbPosition(contents_scroll_offset_));
487}
488
489int BaseScrollBar::GetTrackSize() const {
490  gfx::Rect track_bounds = GetTrackBounds();
491  return IsHorizontal() ? track_bounds.width() : track_bounds.height();
492}
493
494int BaseScrollBar::CalculateThumbPosition(int contents_scroll_offset) const {
495  // In some combination of viewport_size and contents_size_, the result of
496  // simple division can be rounded and there could be 1 pixel gap even when the
497  // contents scroll down to the bottom. See crbug.com/244671
498  if (contents_scroll_offset + viewport_size_ == contents_size_) {
499    int track_size = GetTrackSize();
500    return track_size - (viewport_size_ * GetTrackSize() / contents_size_);
501  }
502  return (contents_scroll_offset * GetTrackSize()) / contents_size_;
503}
504
505int BaseScrollBar::CalculateContentsOffset(int thumb_position,
506                                           bool scroll_to_middle) const {
507  if (scroll_to_middle)
508    thumb_position = thumb_position - (thumb_->GetSize() / 2);
509  return (thumb_position * contents_size_) / GetTrackSize();
510}
511
512void BaseScrollBar::SetThumbTrackState(CustomButton::ButtonState state) {
513  thumb_track_state_ = state;
514  SchedulePaint();
515}
516
517}  // namespace views
518