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