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 "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"
25
26 #if defined(OS_LINUX)
27 #include "ui/gfx/screen.h"
28 #endif
29
30 #undef min
31 #undef max
32
33 namespace views {
34
35 ///////////////////////////////////////////////////////////////////////////////
36 // BaseScrollBar, public:
37
BaseScrollBar(bool horizontal,BaseScrollBarThumb * thumb)38 BaseScrollBar::BaseScrollBar(bool horizontal, BaseScrollBarThumb* thumb)
39 : ScrollBar(horizontal),
40 thumb_(thumb),
41 contents_size_(0),
42 contents_scroll_offset_(0),
43 viewport_size_(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) {
49 AddChildView(thumb_);
50
51 set_context_menu_controller(this);
52 thumb_->set_context_menu_controller(this);
53 }
54
ScrollByAmount(ScrollAmount amount)55 void BaseScrollBar::ScrollByAmount(ScrollAmount amount) {
56 int offset = contents_scroll_offset_;
57 switch (amount) {
58 case SCROLL_START:
59 offset = GetMinPosition();
60 break;
61 case SCROLL_END:
62 offset = GetMaxPosition();
63 break;
64 case SCROLL_PREV_LINE:
65 offset -= GetScrollIncrement(false, false);
66 offset = std::max(GetMinPosition(), offset);
67 break;
68 case SCROLL_NEXT_LINE:
69 offset += GetScrollIncrement(false, true);
70 offset = std::min(GetMaxPosition(), offset);
71 break;
72 case SCROLL_PREV_PAGE:
73 offset -= GetScrollIncrement(true, false);
74 offset = std::max(GetMinPosition(), offset);
75 break;
76 case SCROLL_NEXT_PAGE:
77 offset += GetScrollIncrement(true, true);
78 offset = std::min(GetMaxPosition(), offset);
79 break;
80 default:
81 break;
82 }
83 contents_scroll_offset_ = offset;
84 ScrollContentsToOffset();
85 }
86
~BaseScrollBar()87 BaseScrollBar::~BaseScrollBar() {
88 }
89
ScrollToThumbPosition(int thumb_position,bool scroll_to_middle)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();
98 }
99 ScrollContentsToOffset();
100 SchedulePaint();
101 }
102
ScrollByContentsOffset(int contents_offset)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();
110 }
111 if (old_offset == contents_scroll_offset_)
112 return false;
113
114 ScrollContentsToOffset();
115 return true;
116 }
117
OnThumbStateChanged(CustomButton::ButtonState old_state,CustomButton::ButtonState new_state)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);
124 }
125 }
126
127 ///////////////////////////////////////////////////////////////////////////////
128 // BaseScrollBar, View implementation:
129
OnMousePressed(const ui::MouseEvent & event)130 bool BaseScrollBar::OnMousePressed(const ui::MouseEvent& event) {
131 if (event.IsOnlyLeftMouseButton())
132 ProcessPressEvent(event);
133 return true;
134 }
135
OnMouseReleased(const ui::MouseEvent & event)136 void BaseScrollBar::OnMouseReleased(const ui::MouseEvent& event) {
137 SetState(HitTestPoint(event.location()) ?
138 CustomButton::STATE_HOVERED : CustomButton::STATE_NORMAL);
139 }
140
OnMouseCaptureLost()141 void BaseScrollBar::OnMouseCaptureLost() {
142 SetState(CustomButton::STATE_NORMAL);
143 }
144
OnMouseEntered(const ui::MouseEvent & event)145 void BaseScrollBar::OnMouseEntered(const ui::MouseEvent& event) {
146 SetThumbTrackState(CustomButton::STATE_HOVERED);
147 }
148
OnMouseExited(const ui::MouseEvent & event)149 void BaseScrollBar::OnMouseExited(const ui::MouseEvent& event) {
150 if (GetThumbTrackState() == CustomButton::STATE_HOVERED)
151 SetState(CustomButton::STATE_NORMAL);
152 }
153
OnKeyPressed(const ui::KeyEvent & event)154 bool BaseScrollBar::OnKeyPressed(const ui::KeyEvent& event) {
155 ScrollAmount amount = SCROLL_NONE;
156 switch (event.key_code()) {
157 case ui::VKEY_UP:
158 if (!IsHorizontal())
159 amount = SCROLL_PREV_LINE;
160 break;
161 case ui::VKEY_DOWN:
162 if (!IsHorizontal())
163 amount = SCROLL_NEXT_LINE;
164 break;
165 case ui::VKEY_LEFT:
166 if (IsHorizontal())
167 amount = SCROLL_PREV_LINE;
168 break;
169 case ui::VKEY_RIGHT:
170 if (IsHorizontal())
171 amount = SCROLL_NEXT_LINE;
172 break;
173 case ui::VKEY_PRIOR:
174 amount = SCROLL_PREV_PAGE;
175 break;
176 case ui::VKEY_NEXT:
177 amount = SCROLL_NEXT_PAGE;
178 break;
179 case ui::VKEY_HOME:
180 amount = SCROLL_START;
181 break;
182 case ui::VKEY_END:
183 amount = SCROLL_END;
184 break;
185 default:
186 break;
187 }
188 if (amount != SCROLL_NONE) {
189 ScrollByAmount(amount);
190 return true;
191 }
192 return false;
193 }
194
OnMouseWheel(const ui::MouseWheelEvent & event)195 bool BaseScrollBar::OnMouseWheel(const ui::MouseWheelEvent& event) {
196 ScrollByContentsOffset(event.y_offset());
197 return true;
198 }
199
OnGestureEvent(ui::GestureEvent * event)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
203 // fling).
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();
208 }
209
210 if (event->type() == ui::ET_GESTURE_TAP_DOWN) {
211 ProcessPressEvent(*event);
212 event->SetHandled();
213 return;
214 }
215
216 if (event->type() == ui::ET_GESTURE_LONG_PRESS) {
217 // For a long-press, the repeater started in tap-down should continue. So
218 // return early.
219 return;
220 }
221
222 SetState(CustomButton::STATE_NORMAL);
223
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.
227 event->SetHandled();
228 return;
229 }
230
231 if (event->type() == ui::ET_GESTURE_SCROLL_BEGIN ||
232 event->type() == ui::ET_GESTURE_SCROLL_END) {
233 event->SetHandled();
234 return;
235 }
236
237 if (event->type() == ui::ET_GESTURE_SCROLL_UPDATE) {
238 if (ScrollByContentsOffset(IsHorizontal() ? event->details().scroll_x() :
239 event->details().scroll_y())) {
240 event->SetHandled();
241 }
242 return;
243 }
244
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());
251 event->SetHandled();
252 }
253 }
254
255 ///////////////////////////////////////////////////////////////////////////////
256 // BaseScrollBar, ScrollDelegate implementation:
257
OnScroll(float dx,float dy)258 bool BaseScrollBar::OnScroll(float dx, float dy) {
259 return IsHorizontal() ? ScrollByContentsOffset(dx) :
260 ScrollByContentsOffset(dy);
261 }
262
263 ///////////////////////////////////////////////////////////////////////////////
264 // BaseScrollBar, ContextMenuController implementation:
265
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
274 };
275
ShowContextMenuForView(View * source,const gfx::Point & p,ui::MenuSourceType source_type)276 void BaseScrollBar::ShowContextMenuForView(View* source,
277 const gfx::Point& p,
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();
284
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)
302 return;
303 }
304
305 ///////////////////////////////////////////////////////////////////////////////
306 // BaseScrollBar, Menu::Delegate implementation:
307
GetLabel(int id) const308 string16 BaseScrollBar::GetLabel(int id) const {
309 int ids_value = 0;
310 switch (id) {
311 case ScrollBarContextMenuCommand_ScrollHere:
312 ids_value = IDS_APP_SCROLLBAR_CXMENU_SCROLLHERE;
313 break;
314 case ScrollBarContextMenuCommand_ScrollStart:
315 ids_value = IsHorizontal() ? IDS_APP_SCROLLBAR_CXMENU_SCROLLLEFTEDGE
316 : IDS_APP_SCROLLBAR_CXMENU_SCROLLHOME;
317 break;
318 case ScrollBarContextMenuCommand_ScrollEnd:
319 ids_value = IsHorizontal() ? IDS_APP_SCROLLBAR_CXMENU_SCROLLRIGHTEDGE
320 : IDS_APP_SCROLLBAR_CXMENU_SCROLLEND;
321 break;
322 case ScrollBarContextMenuCommand_ScrollPageUp:
323 ids_value = IDS_APP_SCROLLBAR_CXMENU_SCROLLPAGEUP;
324 break;
325 case ScrollBarContextMenuCommand_ScrollPageDown:
326 ids_value = IDS_APP_SCROLLBAR_CXMENU_SCROLLPAGEDOWN;
327 break;
328 case ScrollBarContextMenuCommand_ScrollPrev:
329 ids_value = IsHorizontal() ? IDS_APP_SCROLLBAR_CXMENU_SCROLLLEFT
330 : IDS_APP_SCROLLBAR_CXMENU_SCROLLUP;
331 break;
332 case ScrollBarContextMenuCommand_ScrollNext:
333 ids_value = IsHorizontal() ? IDS_APP_SCROLLBAR_CXMENU_SCROLLRIGHT
334 : IDS_APP_SCROLLBAR_CXMENU_SCROLLDOWN;
335 break;
336 default:
337 NOTREACHED() << "Invalid BaseScrollBar Context Menu command!";
338 }
339
340 return ids_value ? l10n_util::GetStringUTF16(ids_value) : string16();
341 }
342
IsCommandEnabled(int id) const343 bool BaseScrollBar::IsCommandEnabled(int id) const {
344 switch (id) {
345 case ScrollBarContextMenuCommand_ScrollPageUp:
346 case ScrollBarContextMenuCommand_ScrollPageDown:
347 return !IsHorizontal();
348 }
349 return true;
350 }
351
ExecuteCommand(int id)352 void BaseScrollBar::ExecuteCommand(int id) {
353 switch (id) {
354 case ScrollBarContextMenuCommand_ScrollHere:
355 ScrollToThumbPosition(context_menu_mouse_position_, true);
356 break;
357 case ScrollBarContextMenuCommand_ScrollStart:
358 ScrollByAmount(SCROLL_START);
359 break;
360 case ScrollBarContextMenuCommand_ScrollEnd:
361 ScrollByAmount(SCROLL_END);
362 break;
363 case ScrollBarContextMenuCommand_ScrollPageUp:
364 ScrollByAmount(SCROLL_PREV_PAGE);
365 break;
366 case ScrollBarContextMenuCommand_ScrollPageDown:
367 ScrollByAmount(SCROLL_NEXT_PAGE);
368 break;
369 case ScrollBarContextMenuCommand_ScrollPrev:
370 ScrollByAmount(SCROLL_PREV_LINE);
371 break;
372 case ScrollBarContextMenuCommand_ScrollNext:
373 ScrollByAmount(SCROLL_NEXT_LINE);
374 break;
375 }
376 }
377
378 ///////////////////////////////////////////////////////////////////////////////
379 // BaseScrollBar, ScrollBar implementation:
380
Update(int viewport_size,int content_size,int contents_scroll_offset)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);
384
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);
388
389 viewport_size_ = std::max(1, viewport_size);
390
391 if (content_size < 0)
392 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;
397
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);
404
405 int thumb_position = CalculateThumbPosition(contents_scroll_offset);
406 thumb_->SetPosition(thumb_position);
407 }
408
GetPosition() const409 int BaseScrollBar::GetPosition() const {
410 return thumb_->GetPosition();
411 }
412
413 ///////////////////////////////////////////////////////////////////////////////
414 // BaseScrollBar, protected:
415
GetThumb() const416 BaseScrollBarThumb* BaseScrollBar::GetThumb() const {
417 return thumb_;
418 }
419
GetThumbTrackState() const420 CustomButton::ButtonState BaseScrollBar::GetThumbTrackState() const {
421 return thumb_track_state_;
422 }
423
ScrollToPosition(int position)424 void BaseScrollBar::ScrollToPosition(int position) {
425 controller()->ScrollToPosition(this, position);
426 }
427
GetScrollIncrement(bool is_page,bool is_positive)428 int BaseScrollBar::GetScrollIncrement(bool is_page, bool is_positive) {
429 return controller()->GetScrollIncrement(this, is_page, is_positive);
430 }
431
432 ///////////////////////////////////////////////////////////////////////////////
433 // BaseScrollBar, private:
434
GetThumbSizeForTest()435 int BaseScrollBar::GetThumbSizeForTest() {
436 return thumb_->GetSize();
437 }
438
ProcessPressEvent(const ui::LocatedEvent & event)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;
447 }
448 } else {
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;
453 }
454 }
455 TrackClicked();
456 repeater_.Start();
457 }
458
SetState(CustomButton::ButtonState state)459 void BaseScrollBar::SetState(CustomButton::ButtonState state) {
460 SetThumbTrackState(state);
461 repeater_.Stop();
462 }
463
TrackClicked()464 void BaseScrollBar::TrackClicked() {
465 if (last_scroll_amount_ != SCROLL_NONE)
466 ScrollByAmount(last_scroll_amount_);
467 }
468
ScrollContentsToOffset()469 void BaseScrollBar::ScrollContentsToOffset() {
470 ScrollToPosition(contents_scroll_offset_);
471 thumb_->SetPosition(CalculateThumbPosition(contents_scroll_offset_));
472 }
473
GetTrackSize() const474 int BaseScrollBar::GetTrackSize() const {
475 gfx::Rect track_bounds = GetTrackBounds();
476 return IsHorizontal() ? track_bounds.width() : track_bounds.height();
477 }
478
CalculateThumbPosition(int contents_scroll_offset) const479 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_);
486 }
487 return (contents_scroll_offset * GetTrackSize()) / contents_size_;
488 }
489
CalculateContentsOffset(int thumb_position,bool scroll_to_middle) const490 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();
495 }
496
SetThumbTrackState(CustomButton::ButtonState state)497 void BaseScrollBar::SetThumbTrackState(CustomButton::ButtonState state) {
498 thumb_track_state_ = state;
499 SchedulePaint();
500 }
501
502 } // namespace views
503