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.
5 #include "ui/views/controls/scrollbar/base_scroll_bar.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 "grit/ui_strings.h"
16 #include "ui/base/l10n/l10n_util.h"
17 #include "ui/events/event.h"
18 #include "ui/events/keycodes/keyboard_codes.h"
19 #include "ui/gfx/canvas.h"
20 #include "ui/views/controls/menu/menu_item_view.h"
21 #include "ui/views/controls/menu/menu_runner.h"
22 #include "ui/views/controls/scroll_view.h"
23 #include "ui/views/controls/scrollbar/base_scroll_bar_thumb.h"
24 #include "ui/views/widget/widget.h"
27 #include "ui/gfx/screen.h"
35 ///////////////////////////////////////////////////////////////////////////////
36 // BaseScrollBar, public:
38 BaseScrollBar::BaseScrollBar(bool horizontal, BaseScrollBarThumb* thumb)
39 : ScrollBar(horizontal),
42 contents_scroll_offset_(0),
44 thumb_track_state_(CustomButton::STATE_NORMAL),
45 last_scroll_amount_(SCROLL_NONE),
46 repeater_(base::Bind(&BaseScrollBar::TrackClicked,
47 base::Unretained(this))),
48 context_menu_mouse_position_(0) {
51 set_context_menu_controller(this);
52 thumb_->set_context_menu_controller(this);
55 void BaseScrollBar::ScrollByAmount(ScrollAmount amount) {
56 int offset = contents_scroll_offset_;
59 offset = GetMinPosition();
62 offset = GetMaxPosition();
64 case SCROLL_PREV_LINE:
65 offset -= GetScrollIncrement(false, false);
66 offset = std::max(GetMinPosition(), offset);
68 case SCROLL_NEXT_LINE:
69 offset += GetScrollIncrement(false, true);
70 offset = std::min(GetMaxPosition(), offset);
72 case SCROLL_PREV_PAGE:
73 offset -= GetScrollIncrement(true, false);
74 offset = std::max(GetMinPosition(), offset);
76 case SCROLL_NEXT_PAGE:
77 offset += GetScrollIncrement(true, true);
78 offset = std::min(GetMaxPosition(), offset);
83 contents_scroll_offset_ = offset;
84 ScrollContentsToOffset();
87 BaseScrollBar::~BaseScrollBar() {
90 void BaseScrollBar::ScrollToThumbPosition(int thumb_position,
91 bool scroll_to_middle) {
92 contents_scroll_offset_ =
93 CalculateContentsOffset(thumb_position, scroll_to_middle);
94 if (contents_scroll_offset_ < GetMinPosition()) {
95 contents_scroll_offset_ = GetMinPosition();
96 } else if (contents_scroll_offset_ > GetMaxPosition()) {
97 contents_scroll_offset_ = GetMaxPosition();
99 ScrollContentsToOffset();
103 bool BaseScrollBar::ScrollByContentsOffset(int contents_offset) {
104 int old_offset = contents_scroll_offset_;
105 contents_scroll_offset_ -= contents_offset;
106 if (contents_scroll_offset_ < GetMinPosition()) {
107 contents_scroll_offset_ = GetMinPosition();
108 } else if (contents_scroll_offset_ > GetMaxPosition()) {
109 contents_scroll_offset_ = GetMaxPosition();
111 if (old_offset == contents_scroll_offset_)
114 ScrollContentsToOffset();
118 void BaseScrollBar::OnThumbStateChanged(CustomButton::ButtonState old_state,
119 CustomButton::ButtonState new_state) {
120 if (old_state == CustomButton::STATE_PRESSED &&
121 new_state == CustomButton::STATE_NORMAL &&
122 GetThumbTrackState() == CustomButton::STATE_HOVERED) {
123 SetThumbTrackState(CustomButton::STATE_NORMAL);
127 ///////////////////////////////////////////////////////////////////////////////
128 // BaseScrollBar, View implementation:
130 bool BaseScrollBar::OnMousePressed(const ui::MouseEvent& event) {
131 if (event.IsOnlyLeftMouseButton())
132 ProcessPressEvent(event);
136 void BaseScrollBar::OnMouseReleased(const ui::MouseEvent& event) {
137 SetState(HitTestPoint(event.location()) ?
138 CustomButton::STATE_HOVERED : CustomButton::STATE_NORMAL);
141 void BaseScrollBar::OnMouseCaptureLost() {
142 SetState(CustomButton::STATE_NORMAL);
145 void BaseScrollBar::OnMouseEntered(const ui::MouseEvent& event) {
146 SetThumbTrackState(CustomButton::STATE_HOVERED);
149 void BaseScrollBar::OnMouseExited(const ui::MouseEvent& event) {
150 if (GetThumbTrackState() == CustomButton::STATE_HOVERED)
151 SetState(CustomButton::STATE_NORMAL);
154 bool BaseScrollBar::OnKeyPressed(const ui::KeyEvent& event) {
155 ScrollAmount amount = SCROLL_NONE;
156 switch (event.key_code()) {
159 amount = SCROLL_PREV_LINE;
163 amount = SCROLL_NEXT_LINE;
167 amount = SCROLL_PREV_LINE;
171 amount = SCROLL_NEXT_LINE;
174 amount = SCROLL_PREV_PAGE;
177 amount = SCROLL_NEXT_PAGE;
180 amount = SCROLL_START;
188 if (amount != SCROLL_NONE) {
189 ScrollByAmount(amount);
195 bool BaseScrollBar::OnMouseWheel(const ui::MouseWheelEvent& event) {
196 ScrollByContentsOffset(event.y_offset());
200 void BaseScrollBar::OnGestureEvent(ui::GestureEvent* event) {
201 // If a fling is in progress, then stop the fling for any incoming gesture
202 // event (except for the GESTURE_END event that is generated at the end of the
204 if (scroll_animator_.get() && scroll_animator_->is_scrolling() &&
205 (event->type() != ui::ET_GESTURE_END ||
206 event->details().touch_points() > 1)) {
207 scroll_animator_->Stop();
210 if (event->type() == ui::ET_GESTURE_TAP_DOWN) {
211 ProcessPressEvent(*event);
216 if (event->type() == ui::ET_GESTURE_LONG_PRESS) {
217 // For a long-press, the repeater started in tap-down should continue. So
222 SetState(CustomButton::STATE_NORMAL);
224 if (event->type() == ui::ET_GESTURE_TAP) {
225 // TAP_DOWN would have already scrolled some amount. So scrolling again on
226 // TAP is not necessary.
231 if (event->type() == ui::ET_GESTURE_SCROLL_BEGIN ||
232 event->type() == ui::ET_GESTURE_SCROLL_END) {
237 if (event->type() == ui::ET_GESTURE_SCROLL_UPDATE) {
238 if (ScrollByContentsOffset(IsHorizontal() ? event->details().scroll_x() :
239 event->details().scroll_y())) {
245 if (event->type() == ui::ET_SCROLL_FLING_START) {
246 if (!scroll_animator_.get())
247 scroll_animator_.reset(new ScrollAnimator(this));
248 scroll_animator_->Start(
249 IsHorizontal() ? event->details().velocity_x() : 0.f,
250 IsHorizontal() ? 0.f : event->details().velocity_y());
255 ///////////////////////////////////////////////////////////////////////////////
256 // BaseScrollBar, ScrollDelegate implementation:
258 bool BaseScrollBar::OnScroll(float dx, float dy) {
259 return IsHorizontal() ? ScrollByContentsOffset(dx) :
260 ScrollByContentsOffset(dy);
263 ///////////////////////////////////////////////////////////////////////////////
264 // BaseScrollBar, ContextMenuController implementation:
266 enum ScrollBarContextMenuCommands {
267 ScrollBarContextMenuCommand_ScrollHere = 1,
268 ScrollBarContextMenuCommand_ScrollStart,
269 ScrollBarContextMenuCommand_ScrollEnd,
270 ScrollBarContextMenuCommand_ScrollPageUp,
271 ScrollBarContextMenuCommand_ScrollPageDown,
272 ScrollBarContextMenuCommand_ScrollPrev,
273 ScrollBarContextMenuCommand_ScrollNext
276 void BaseScrollBar::ShowContextMenuForView(View* source,
278 ui::MenuSourceType source_type) {
279 Widget* widget = GetWidget();
280 gfx::Rect widget_bounds = widget->GetWindowBoundsInScreen();
281 gfx::Point temp_pt(p.x() - widget_bounds.x(), p.y() - widget_bounds.y());
282 View::ConvertPointFromWidget(this, &temp_pt);
283 context_menu_mouse_position_ = IsHorizontal() ? temp_pt.x() : temp_pt.y();
285 views::MenuItemView* menu = new views::MenuItemView(this);
286 // MenuRunner takes ownership of |menu|.
287 menu_runner_.reset(new MenuRunner(menu));
288 menu->AppendDelegateMenuItem(ScrollBarContextMenuCommand_ScrollHere);
289 menu->AppendSeparator();
290 menu->AppendDelegateMenuItem(ScrollBarContextMenuCommand_ScrollStart);
291 menu->AppendDelegateMenuItem(ScrollBarContextMenuCommand_ScrollEnd);
292 menu->AppendSeparator();
293 menu->AppendDelegateMenuItem(ScrollBarContextMenuCommand_ScrollPageUp);
294 menu->AppendDelegateMenuItem(ScrollBarContextMenuCommand_ScrollPageDown);
295 menu->AppendSeparator();
296 menu->AppendDelegateMenuItem(ScrollBarContextMenuCommand_ScrollPrev);
297 menu->AppendDelegateMenuItem(ScrollBarContextMenuCommand_ScrollNext);
298 if (menu_runner_->RunMenuAt(GetWidget(), NULL, gfx::Rect(p, gfx::Size()),
299 views::MenuItemView::TOPLEFT, source_type, MenuRunner::HAS_MNEMONICS |
300 views::MenuRunner::CONTEXT_MENU) ==
301 MenuRunner::MENU_DELETED)
305 ///////////////////////////////////////////////////////////////////////////////
306 // BaseScrollBar, Menu::Delegate implementation:
308 string16 BaseScrollBar::GetLabel(int id) const {
311 case ScrollBarContextMenuCommand_ScrollHere:
312 ids_value = IDS_APP_SCROLLBAR_CXMENU_SCROLLHERE;
314 case ScrollBarContextMenuCommand_ScrollStart:
315 ids_value = IsHorizontal() ? IDS_APP_SCROLLBAR_CXMENU_SCROLLLEFTEDGE
316 : IDS_APP_SCROLLBAR_CXMENU_SCROLLHOME;
318 case ScrollBarContextMenuCommand_ScrollEnd:
319 ids_value = IsHorizontal() ? IDS_APP_SCROLLBAR_CXMENU_SCROLLRIGHTEDGE
320 : IDS_APP_SCROLLBAR_CXMENU_SCROLLEND;
322 case ScrollBarContextMenuCommand_ScrollPageUp:
323 ids_value = IDS_APP_SCROLLBAR_CXMENU_SCROLLPAGEUP;
325 case ScrollBarContextMenuCommand_ScrollPageDown:
326 ids_value = IDS_APP_SCROLLBAR_CXMENU_SCROLLPAGEDOWN;
328 case ScrollBarContextMenuCommand_ScrollPrev:
329 ids_value = IsHorizontal() ? IDS_APP_SCROLLBAR_CXMENU_SCROLLLEFT
330 : IDS_APP_SCROLLBAR_CXMENU_SCROLLUP;
332 case ScrollBarContextMenuCommand_ScrollNext:
333 ids_value = IsHorizontal() ? IDS_APP_SCROLLBAR_CXMENU_SCROLLRIGHT
334 : IDS_APP_SCROLLBAR_CXMENU_SCROLLDOWN;
337 NOTREACHED() << "Invalid BaseScrollBar Context Menu command!";
340 return ids_value ? l10n_util::GetStringUTF16(ids_value) : string16();
343 bool BaseScrollBar::IsCommandEnabled(int id) const {
345 case ScrollBarContextMenuCommand_ScrollPageUp:
346 case ScrollBarContextMenuCommand_ScrollPageDown:
347 return !IsHorizontal();
352 void BaseScrollBar::ExecuteCommand(int id) {
354 case ScrollBarContextMenuCommand_ScrollHere:
355 ScrollToThumbPosition(context_menu_mouse_position_, true);
357 case ScrollBarContextMenuCommand_ScrollStart:
358 ScrollByAmount(SCROLL_START);
360 case ScrollBarContextMenuCommand_ScrollEnd:
361 ScrollByAmount(SCROLL_END);
363 case ScrollBarContextMenuCommand_ScrollPageUp:
364 ScrollByAmount(SCROLL_PREV_PAGE);
366 case ScrollBarContextMenuCommand_ScrollPageDown:
367 ScrollByAmount(SCROLL_NEXT_PAGE);
369 case ScrollBarContextMenuCommand_ScrollPrev:
370 ScrollByAmount(SCROLL_PREV_LINE);
372 case ScrollBarContextMenuCommand_ScrollNext:
373 ScrollByAmount(SCROLL_NEXT_LINE);
378 ///////////////////////////////////////////////////////////////////////////////
379 // BaseScrollBar, ScrollBar implementation:
381 void BaseScrollBar::Update(int viewport_size, int content_size,
382 int contents_scroll_offset) {
383 ScrollBar::Update(viewport_size, content_size, contents_scroll_offset);
385 // Make sure contents_size is always > 0 to avoid divide by zero errors in
386 // calculations throughout this code.
387 contents_size_ = std::max(1, content_size);
389 viewport_size_ = std::max(1, viewport_size);
391 if (content_size < 0)
393 if (contents_scroll_offset < 0)
394 contents_scroll_offset = 0;
395 if (contents_scroll_offset > content_size)
396 contents_scroll_offset = content_size;
398 // Thumb Height and Thumb Pos.
399 // The height of the thumb is the ratio of the Viewport height to the
400 // content size multiplied by the height of the thumb track.
401 double ratio = static_cast<double>(viewport_size) / contents_size_;
402 int thumb_size = static_cast<int>(ratio * GetTrackSize());
403 thumb_->SetSize(thumb_size);
405 int thumb_position = CalculateThumbPosition(contents_scroll_offset);
406 thumb_->SetPosition(thumb_position);
409 int BaseScrollBar::GetPosition() const {
410 return thumb_->GetPosition();
413 ///////////////////////////////////////////////////////////////////////////////
414 // BaseScrollBar, protected:
416 BaseScrollBarThumb* BaseScrollBar::GetThumb() const {
420 CustomButton::ButtonState BaseScrollBar::GetThumbTrackState() const {
421 return thumb_track_state_;
424 void BaseScrollBar::ScrollToPosition(int position) {
425 controller()->ScrollToPosition(this, position);
428 int BaseScrollBar::GetScrollIncrement(bool is_page, bool is_positive) {
429 return controller()->GetScrollIncrement(this, is_page, is_positive);
432 ///////////////////////////////////////////////////////////////////////////////
433 // BaseScrollBar, private:
435 int BaseScrollBar::GetThumbSizeForTest() {
436 return thumb_->GetSize();
439 void BaseScrollBar::ProcessPressEvent(const ui::LocatedEvent& event) {
440 SetThumbTrackState(CustomButton::STATE_PRESSED);
441 gfx::Rect thumb_bounds = thumb_->bounds();
442 if (IsHorizontal()) {
443 if (GetMirroredXInView(event.x()) < thumb_bounds.x()) {
444 last_scroll_amount_ = SCROLL_PREV_PAGE;
445 } else if (GetMirroredXInView(event.x()) > thumb_bounds.right()) {
446 last_scroll_amount_ = SCROLL_NEXT_PAGE;
449 if (event.y() < thumb_bounds.y()) {
450 last_scroll_amount_ = SCROLL_PREV_PAGE;
451 } else if (event.y() > thumb_bounds.bottom()) {
452 last_scroll_amount_ = SCROLL_NEXT_PAGE;
459 void BaseScrollBar::SetState(CustomButton::ButtonState state) {
460 SetThumbTrackState(state);
464 void BaseScrollBar::TrackClicked() {
465 if (last_scroll_amount_ != SCROLL_NONE)
466 ScrollByAmount(last_scroll_amount_);
469 void BaseScrollBar::ScrollContentsToOffset() {
470 ScrollToPosition(contents_scroll_offset_);
471 thumb_->SetPosition(CalculateThumbPosition(contents_scroll_offset_));
474 int BaseScrollBar::GetTrackSize() const {
475 gfx::Rect track_bounds = GetTrackBounds();
476 return IsHorizontal() ? track_bounds.width() : track_bounds.height();
479 int BaseScrollBar::CalculateThumbPosition(int contents_scroll_offset) const {
480 // In some combination of viewport_size and contents_size_, the result of
481 // simple division can be rounded and there could be 1 pixel gap even when the
482 // contents scroll down to the bottom. See crbug.com/244671
483 if (contents_scroll_offset + viewport_size_ == contents_size_) {
484 int track_size = GetTrackSize();
485 return track_size - (viewport_size_ * GetTrackSize() / contents_size_);
487 return (contents_scroll_offset * GetTrackSize()) / contents_size_;
490 int BaseScrollBar::CalculateContentsOffset(int thumb_position,
491 bool scroll_to_middle) const {
492 if (scroll_to_middle)
493 thumb_position = thumb_position - (thumb_->GetSize() / 2);
494 return (thumb_position * contents_size_) / GetTrackSize();
497 void BaseScrollBar::SetThumbTrackState(CustomButton::ButtonState state) {
498 thumb_track_state_ = state;