Home | History | Annotate | Download | only in scrollbar
      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
    503