- add sources.
[platform/framework/web/crosswalk.git] / src / ui / views / controls / scrollbar / base_scroll_bar.cc
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
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
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
87 BaseScrollBar::~BaseScrollBar() {
88 }
89
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
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
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
130 bool BaseScrollBar::OnMousePressed(const ui::MouseEvent& event) {
131   if (event.IsOnlyLeftMouseButton())
132     ProcessPressEvent(event);
133   return true;
134 }
135
136 void BaseScrollBar::OnMouseReleased(const ui::MouseEvent& event) {
137   SetState(HitTestPoint(event.location()) ?
138            CustomButton::STATE_HOVERED : CustomButton::STATE_NORMAL);
139 }
140
141 void BaseScrollBar::OnMouseCaptureLost() {
142   SetState(CustomButton::STATE_NORMAL);
143 }
144
145 void BaseScrollBar::OnMouseEntered(const ui::MouseEvent& event) {
146   SetThumbTrackState(CustomButton::STATE_HOVERED);
147 }
148
149 void BaseScrollBar::OnMouseExited(const ui::MouseEvent& event) {
150   if (GetThumbTrackState() == CustomButton::STATE_HOVERED)
151     SetState(CustomButton::STATE_NORMAL);
152 }
153
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
195 bool BaseScrollBar::OnMouseWheel(const ui::MouseWheelEvent& event) {
196   ScrollByContentsOffset(event.y_offset());
197   return true;
198 }
199
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
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
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
308 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
343 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
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
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
409 int BaseScrollBar::GetPosition() const {
410   return thumb_->GetPosition();
411 }
412
413 ///////////////////////////////////////////////////////////////////////////////
414 // BaseScrollBar, protected:
415
416 BaseScrollBarThumb* BaseScrollBar::GetThumb() const {
417   return thumb_;
418 }
419
420 CustomButton::ButtonState BaseScrollBar::GetThumbTrackState() const {
421   return thumb_track_state_;
422 }
423
424 void BaseScrollBar::ScrollToPosition(int position) {
425   controller()->ScrollToPosition(this, position);
426 }
427
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
435 int BaseScrollBar::GetThumbSizeForTest() {
436   return thumb_->GetSize();
437 }
438
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
459 void BaseScrollBar::SetState(CustomButton::ButtonState state) {
460   SetThumbTrackState(state);
461   repeater_.Stop();
462 }
463
464 void BaseScrollBar::TrackClicked() {
465   if (last_scroll_amount_ != SCROLL_NONE)
466     ScrollByAmount(last_scroll_amount_);
467 }
468
469 void BaseScrollBar::ScrollContentsToOffset() {
470   ScrollToPosition(contents_scroll_offset_);
471   thumb_->SetPosition(CalculateThumbPosition(contents_scroll_offset_));
472 }
473
474 int BaseScrollBar::GetTrackSize() const {
475   gfx::Rect track_bounds = GetTrackBounds();
476   return IsHorizontal() ? track_bounds.width() : track_bounds.height();
477 }
478
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_);
486   }
487   return (contents_scroll_offset * GetTrackSize()) / contents_size_;
488 }
489
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();
495 }
496
497 void BaseScrollBar::SetThumbTrackState(CustomButton::ButtonState state) {
498   thumb_track_state_ = state;
499   SchedulePaint();
500 }
501
502 }  // namespace views