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/scroll_view.h" 6 7 #include "base/logging.h" 8 #include "ui/events/event.h" 9 #include "ui/gfx/canvas.h" 10 #include "ui/native_theme/native_theme.h" 11 #include "ui/views/border.h" 12 #include "ui/views/controls/scrollbar/native_scroll_bar.h" 13 #include "ui/views/widget/root_view.h" 14 15 namespace views { 16 17 const char ScrollView::kViewClassName[] = "ScrollView"; 18 19 namespace { 20 21 // Subclass of ScrollView that resets the border when the theme changes. 22 class ScrollViewWithBorder : public views::ScrollView { 23 public: 24 ScrollViewWithBorder() {} 25 26 // View overrides; 27 virtual void OnNativeThemeChanged(const ui::NativeTheme* theme) OVERRIDE { 28 SetBorder(Border::CreateSolidBorder( 29 1, 30 theme->GetSystemColor(ui::NativeTheme::kColorId_UnfocusedBorderColor))); 31 } 32 33 private: 34 DISALLOW_COPY_AND_ASSIGN(ScrollViewWithBorder); 35 }; 36 37 class ScrollCornerView : public views::View { 38 public: 39 ScrollCornerView() {} 40 41 virtual void OnPaint(gfx::Canvas* canvas) OVERRIDE { 42 ui::NativeTheme::ExtraParams ignored; 43 GetNativeTheme()->Paint(canvas->sk_canvas(), 44 ui::NativeTheme::kScrollbarCorner, 45 ui::NativeTheme::kNormal, 46 GetLocalBounds(), 47 ignored); 48 } 49 50 private: 51 DISALLOW_COPY_AND_ASSIGN(ScrollCornerView); 52 }; 53 54 // Returns the position for the view so that it isn't scrolled off the visible 55 // region. 56 int CheckScrollBounds(int viewport_size, int content_size, int current_pos) { 57 int max = std::max(content_size - viewport_size, 0); 58 if (current_pos < 0) 59 return 0; 60 if (current_pos > max) 61 return max; 62 return current_pos; 63 } 64 65 // Make sure the content is not scrolled out of bounds 66 void CheckScrollBounds(View* viewport, View* view) { 67 if (!view) 68 return; 69 70 int x = CheckScrollBounds(viewport->width(), view->width(), -view->x()); 71 int y = CheckScrollBounds(viewport->height(), view->height(), -view->y()); 72 73 // This is no op if bounds are the same 74 view->SetBounds(-x, -y, view->width(), view->height()); 75 } 76 77 // Used by ScrollToPosition() to make sure the new position fits within the 78 // allowed scroll range. 79 int AdjustPosition(int current_position, 80 int new_position, 81 int content_size, 82 int viewport_size) { 83 if (-current_position == new_position) 84 return new_position; 85 if (new_position < 0) 86 return 0; 87 const int max_position = std::max(0, content_size - viewport_size); 88 return (new_position > max_position) ? max_position : new_position; 89 } 90 91 } // namespace 92 93 // Viewport contains the contents View of the ScrollView. 94 class ScrollView::Viewport : public View { 95 public: 96 Viewport() {} 97 virtual ~Viewport() {} 98 99 virtual const char* GetClassName() const OVERRIDE { 100 return "ScrollView::Viewport"; 101 } 102 103 virtual void ScrollRectToVisible(const gfx::Rect& rect) OVERRIDE { 104 if (!has_children() || !parent()) 105 return; 106 107 View* contents = child_at(0); 108 gfx::Rect scroll_rect(rect); 109 scroll_rect.Offset(-contents->x(), -contents->y()); 110 static_cast<ScrollView*>(parent())->ScrollContentsRegionToBeVisible( 111 scroll_rect); 112 } 113 114 virtual void ChildPreferredSizeChanged(View* child) OVERRIDE { 115 if (parent()) 116 parent()->Layout(); 117 } 118 119 private: 120 DISALLOW_COPY_AND_ASSIGN(Viewport); 121 }; 122 123 ScrollView::ScrollView() 124 : contents_(NULL), 125 contents_viewport_(new Viewport()), 126 header_(NULL), 127 header_viewport_(new Viewport()), 128 horiz_sb_(new NativeScrollBar(true)), 129 vert_sb_(new NativeScrollBar(false)), 130 corner_view_(new ScrollCornerView()), 131 min_height_(-1), 132 max_height_(-1), 133 hide_horizontal_scrollbar_(false) { 134 set_notify_enter_exit_on_child(true); 135 136 AddChildView(contents_viewport_); 137 AddChildView(header_viewport_); 138 139 // Don't add the scrollbars as children until we discover we need them 140 // (ShowOrHideScrollBar). 141 horiz_sb_->SetVisible(false); 142 horiz_sb_->set_controller(this); 143 vert_sb_->SetVisible(false); 144 vert_sb_->set_controller(this); 145 corner_view_->SetVisible(false); 146 } 147 148 ScrollView::~ScrollView() { 149 // The scrollbars may not have been added, delete them to ensure they get 150 // deleted. 151 delete horiz_sb_; 152 delete vert_sb_; 153 delete corner_view_; 154 } 155 156 // static 157 ScrollView* ScrollView::CreateScrollViewWithBorder() { 158 return new ScrollViewWithBorder(); 159 } 160 161 void ScrollView::SetContents(View* a_view) { 162 SetHeaderOrContents(contents_viewport_, a_view, &contents_); 163 } 164 165 void ScrollView::SetHeader(View* header) { 166 SetHeaderOrContents(header_viewport_, header, &header_); 167 } 168 169 gfx::Rect ScrollView::GetVisibleRect() const { 170 if (!contents_) 171 return gfx::Rect(); 172 return gfx::Rect(-contents_->x(), -contents_->y(), 173 contents_viewport_->width(), contents_viewport_->height()); 174 } 175 176 void ScrollView::ClipHeightTo(int min_height, int max_height) { 177 min_height_ = min_height; 178 max_height_ = max_height; 179 } 180 181 int ScrollView::GetScrollBarWidth() const { 182 return vert_sb_ ? vert_sb_->GetLayoutSize() : 0; 183 } 184 185 int ScrollView::GetScrollBarHeight() const { 186 return horiz_sb_ ? horiz_sb_->GetLayoutSize() : 0; 187 } 188 189 void ScrollView::SetHorizontalScrollBar(ScrollBar* horiz_sb) { 190 DCHECK(horiz_sb); 191 horiz_sb->SetVisible(horiz_sb_->visible()); 192 delete horiz_sb_; 193 horiz_sb->set_controller(this); 194 horiz_sb_ = horiz_sb; 195 } 196 197 void ScrollView::SetVerticalScrollBar(ScrollBar* vert_sb) { 198 DCHECK(vert_sb); 199 vert_sb->SetVisible(vert_sb_->visible()); 200 delete vert_sb_; 201 vert_sb->set_controller(this); 202 vert_sb_ = vert_sb; 203 } 204 205 gfx::Size ScrollView::GetPreferredSize() const { 206 if (!is_bounded()) 207 return View::GetPreferredSize(); 208 209 gfx::Size size = contents()->GetPreferredSize(); 210 size.SetToMax(gfx::Size(size.width(), min_height_)); 211 size.SetToMin(gfx::Size(size.width(), max_height_)); 212 gfx::Insets insets = GetInsets(); 213 size.Enlarge(insets.width(), insets.height()); 214 return size; 215 } 216 217 int ScrollView::GetHeightForWidth(int width) const { 218 if (!is_bounded()) 219 return View::GetHeightForWidth(width); 220 221 gfx::Insets insets = GetInsets(); 222 width = std::max(0, width - insets.width()); 223 int height = contents()->GetHeightForWidth(width) + insets.height(); 224 return std::min(std::max(height, min_height_), max_height_); 225 } 226 227 void ScrollView::Layout() { 228 if (is_bounded()) { 229 int content_width = width(); 230 int content_height = contents()->GetHeightForWidth(content_width); 231 if (content_height > height()) { 232 content_width = std::max(content_width - GetScrollBarWidth(), 0); 233 content_height = contents()->GetHeightForWidth(content_width); 234 } 235 if (contents()->bounds().size() != gfx::Size(content_width, content_height)) 236 contents()->SetBounds(0, 0, content_width, content_height); 237 } 238 239 // Most views will want to auto-fit the available space. Most of them want to 240 // use all available width (without overflowing) and only overflow in 241 // height. Examples are HistoryView, MostVisitedView, DownloadTabView, etc. 242 // Other views want to fit in both ways. An example is PrintView. To make both 243 // happy, assume a vertical scrollbar but no horizontal scrollbar. To override 244 // this default behavior, the inner view has to calculate the available space, 245 // used ComputeScrollBarsVisibility() to use the same calculation that is done 246 // here and sets its bound to fit within. 247 gfx::Rect viewport_bounds = GetContentsBounds(); 248 const int contents_x = viewport_bounds.x(); 249 const int contents_y = viewport_bounds.y(); 250 if (viewport_bounds.IsEmpty()) { 251 // There's nothing to layout. 252 return; 253 } 254 255 const int header_height = 256 std::min(viewport_bounds.height(), 257 header_ ? header_->GetPreferredSize().height() : 0); 258 viewport_bounds.set_height( 259 std::max(0, viewport_bounds.height() - header_height)); 260 viewport_bounds.set_y(viewport_bounds.y() + header_height); 261 // viewport_size is the total client space available. 262 gfx::Size viewport_size = viewport_bounds.size(); 263 // Assumes a vertical scrollbar since most of the current views are designed 264 // for this. 265 int horiz_sb_height = GetScrollBarHeight(); 266 int vert_sb_width = GetScrollBarWidth(); 267 viewport_bounds.set_width(viewport_bounds.width() - vert_sb_width); 268 // Update the bounds right now so the inner views can fit in it. 269 contents_viewport_->SetBoundsRect(viewport_bounds); 270 271 // Give |contents_| a chance to update its bounds if it depends on the 272 // viewport. 273 if (contents_) 274 contents_->Layout(); 275 276 bool should_layout_contents = false; 277 bool horiz_sb_required = false; 278 bool vert_sb_required = false; 279 if (contents_) { 280 gfx::Size content_size = contents_->size(); 281 ComputeScrollBarsVisibility(viewport_size, 282 content_size, 283 &horiz_sb_required, 284 &vert_sb_required); 285 } 286 bool corner_view_required = horiz_sb_required && vert_sb_required; 287 // Take action. 288 SetControlVisibility(horiz_sb_, horiz_sb_required); 289 SetControlVisibility(vert_sb_, vert_sb_required); 290 SetControlVisibility(corner_view_, corner_view_required); 291 292 // Non-default. 293 if (horiz_sb_required) { 294 viewport_bounds.set_height( 295 std::max(0, viewport_bounds.height() - horiz_sb_height)); 296 should_layout_contents = true; 297 } 298 // Default. 299 if (!vert_sb_required) { 300 viewport_bounds.set_width(viewport_bounds.width() + vert_sb_width); 301 should_layout_contents = true; 302 } 303 304 if (horiz_sb_required) { 305 int height_offset = horiz_sb_->GetContentOverlapSize(); 306 horiz_sb_->SetBounds(0, 307 viewport_bounds.bottom() - height_offset, 308 viewport_bounds.right(), 309 horiz_sb_height + height_offset); 310 } 311 if (vert_sb_required) { 312 int width_offset = vert_sb_->GetContentOverlapSize(); 313 vert_sb_->SetBounds(viewport_bounds.right() - width_offset, 314 0, 315 vert_sb_width + width_offset, 316 viewport_bounds.bottom()); 317 } 318 if (corner_view_required) { 319 // Show the resize corner. 320 corner_view_->SetBounds(viewport_bounds.right(), 321 viewport_bounds.bottom(), 322 vert_sb_width, 323 horiz_sb_height); 324 } 325 326 // Update to the real client size with the visible scrollbars. 327 contents_viewport_->SetBoundsRect(viewport_bounds); 328 if (should_layout_contents && contents_) 329 contents_->Layout(); 330 331 header_viewport_->SetBounds(contents_x, contents_y, 332 viewport_bounds.width(), header_height); 333 if (header_) 334 header_->Layout(); 335 336 CheckScrollBounds(header_viewport_, header_); 337 CheckScrollBounds(contents_viewport_, contents_); 338 SchedulePaint(); 339 UpdateScrollBarPositions(); 340 } 341 342 bool ScrollView::OnKeyPressed(const ui::KeyEvent& event) { 343 bool processed = false; 344 345 // Give vertical scrollbar priority 346 if (vert_sb_->visible()) 347 processed = vert_sb_->OnKeyPressed(event); 348 349 if (!processed && horiz_sb_->visible()) 350 processed = horiz_sb_->OnKeyPressed(event); 351 352 return processed; 353 } 354 355 bool ScrollView::OnMouseWheel(const ui::MouseWheelEvent& e) { 356 bool processed = false; 357 358 if (vert_sb_->visible()) 359 processed = vert_sb_->OnMouseWheel(e); 360 361 if (horiz_sb_->visible()) 362 processed = horiz_sb_->OnMouseWheel(e) || processed; 363 364 return processed; 365 } 366 367 void ScrollView::OnMouseEntered(const ui::MouseEvent& event) { 368 if (horiz_sb_) 369 horiz_sb_->OnMouseEnteredScrollView(event); 370 if (vert_sb_) 371 vert_sb_->OnMouseEnteredScrollView(event); 372 } 373 374 void ScrollView::OnMouseExited(const ui::MouseEvent& event) { 375 if (horiz_sb_) 376 horiz_sb_->OnMouseExitedScrollView(event); 377 if (vert_sb_) 378 vert_sb_->OnMouseExitedScrollView(event); 379 } 380 381 void ScrollView::OnGestureEvent(ui::GestureEvent* event) { 382 // If the event happened on one of the scrollbars, then those events are 383 // sent directly to the scrollbars. Otherwise, only scroll events are sent to 384 // the scrollbars. 385 bool scroll_event = event->type() == ui::ET_GESTURE_SCROLL_UPDATE || 386 event->type() == ui::ET_GESTURE_SCROLL_BEGIN || 387 event->type() == ui::ET_GESTURE_SCROLL_END || 388 event->type() == ui::ET_SCROLL_FLING_START; 389 390 if (vert_sb_->visible()) { 391 if (vert_sb_->bounds().Contains(event->location()) || scroll_event) 392 vert_sb_->OnGestureEvent(event); 393 } 394 if (!event->handled() && horiz_sb_->visible()) { 395 if (horiz_sb_->bounds().Contains(event->location()) || scroll_event) 396 horiz_sb_->OnGestureEvent(event); 397 } 398 } 399 400 const char* ScrollView::GetClassName() const { 401 return kViewClassName; 402 } 403 404 void ScrollView::ScrollToPosition(ScrollBar* source, int position) { 405 if (!contents_) 406 return; 407 408 if (source == horiz_sb_ && horiz_sb_->visible()) { 409 position = AdjustPosition(contents_->x(), position, contents_->width(), 410 contents_viewport_->width()); 411 if (-contents_->x() == position) 412 return; 413 contents_->SetX(-position); 414 if (header_) { 415 header_->SetX(-position); 416 header_->SchedulePaintInRect(header_->GetVisibleBounds()); 417 } 418 } else if (source == vert_sb_ && vert_sb_->visible()) { 419 position = AdjustPosition(contents_->y(), position, contents_->height(), 420 contents_viewport_->height()); 421 if (-contents_->y() == position) 422 return; 423 contents_->SetY(-position); 424 } 425 contents_->SchedulePaintInRect(contents_->GetVisibleBounds()); 426 } 427 428 int ScrollView::GetScrollIncrement(ScrollBar* source, bool is_page, 429 bool is_positive) { 430 bool is_horizontal = source->IsHorizontal(); 431 int amount = 0; 432 if (contents_) { 433 if (is_page) { 434 amount = contents_->GetPageScrollIncrement( 435 this, is_horizontal, is_positive); 436 } else { 437 amount = contents_->GetLineScrollIncrement( 438 this, is_horizontal, is_positive); 439 } 440 if (amount > 0) 441 return amount; 442 } 443 // No view, or the view didn't return a valid amount. 444 if (is_page) { 445 return is_horizontal ? contents_viewport_->width() : 446 contents_viewport_->height(); 447 } 448 return is_horizontal ? contents_viewport_->width() / 5 : 449 contents_viewport_->height() / 5; 450 } 451 452 void ScrollView::SetHeaderOrContents(View* parent, 453 View* new_view, 454 View** member) { 455 if (*member == new_view) 456 return; 457 458 delete *member; 459 *member = new_view; 460 if (*member) 461 parent->AddChildView(*member); 462 Layout(); 463 } 464 465 void ScrollView::ScrollContentsRegionToBeVisible(const gfx::Rect& rect) { 466 if (!contents_ || (!horiz_sb_->visible() && !vert_sb_->visible())) 467 return; 468 469 // Figure out the maximums for this scroll view. 470 const int contents_max_x = 471 std::max(contents_viewport_->width(), contents_->width()); 472 const int contents_max_y = 473 std::max(contents_viewport_->height(), contents_->height()); 474 475 // Make sure x and y are within the bounds of [0,contents_max_*]. 476 int x = std::max(0, std::min(contents_max_x, rect.x())); 477 int y = std::max(0, std::min(contents_max_y, rect.y())); 478 479 // Figure out how far and down the rectangle will go taking width 480 // and height into account. This will be "clipped" by the viewport. 481 const int max_x = std::min(contents_max_x, 482 x + std::min(rect.width(), contents_viewport_->width())); 483 const int max_y = std::min(contents_max_y, 484 y + std::min(rect.height(), contents_viewport_->height())); 485 486 // See if the rect is already visible. Note the width is (max_x - x) 487 // and the height is (max_y - y) to take into account the clipping of 488 // either viewport or the content size. 489 const gfx::Rect vis_rect = GetVisibleRect(); 490 if (vis_rect.Contains(gfx::Rect(x, y, max_x - x, max_y - y))) 491 return; 492 493 // Shift contents_'s X and Y so that the region is visible. If we 494 // need to shift up or left from where we currently are then we need 495 // to get it so that the content appears in the upper/left 496 // corner. This is done by setting the offset to -X or -Y. For down 497 // or right shifts we need to make sure it appears in the 498 // lower/right corner. This is calculated by taking max_x or max_y 499 // and scaling it back by the size of the viewport. 500 const int new_x = 501 (vis_rect.x() > x) ? x : std::max(0, max_x - contents_viewport_->width()); 502 const int new_y = 503 (vis_rect.y() > y) ? y : std::max(0, max_y - 504 contents_viewport_->height()); 505 506 contents_->SetX(-new_x); 507 if (header_) 508 header_->SetX(-new_x); 509 contents_->SetY(-new_y); 510 UpdateScrollBarPositions(); 511 } 512 513 void ScrollView::ComputeScrollBarsVisibility(const gfx::Size& vp_size, 514 const gfx::Size& content_size, 515 bool* horiz_is_shown, 516 bool* vert_is_shown) const { 517 // Try to fit both ways first, then try vertical bar only, then horizontal 518 // bar only, then defaults to both shown. 519 if (content_size.width() <= vp_size.width() && 520 content_size.height() <= vp_size.height()) { 521 *horiz_is_shown = false; 522 *vert_is_shown = false; 523 } else if (content_size.width() <= vp_size.width() - GetScrollBarWidth()) { 524 *horiz_is_shown = false; 525 *vert_is_shown = true; 526 } else if (content_size.height() <= vp_size.height() - GetScrollBarHeight()) { 527 *horiz_is_shown = true; 528 *vert_is_shown = false; 529 } else { 530 *horiz_is_shown = true; 531 *vert_is_shown = true; 532 } 533 534 if (hide_horizontal_scrollbar_) 535 *horiz_is_shown = false; 536 } 537 538 // Make sure that a single scrollbar is created and visible as needed 539 void ScrollView::SetControlVisibility(View* control, bool should_show) { 540 if (!control) 541 return; 542 if (should_show) { 543 if (!control->visible()) { 544 AddChildView(control); 545 control->SetVisible(true); 546 } 547 } else { 548 RemoveChildView(control); 549 control->SetVisible(false); 550 } 551 } 552 553 void ScrollView::UpdateScrollBarPositions() { 554 if (!contents_) 555 return; 556 557 if (horiz_sb_->visible()) { 558 int vw = contents_viewport_->width(); 559 int cw = contents_->width(); 560 int origin = contents_->x(); 561 horiz_sb_->Update(vw, cw, -origin); 562 } 563 if (vert_sb_->visible()) { 564 int vh = contents_viewport_->height(); 565 int ch = contents_->height(); 566 int origin = contents_->y(); 567 vert_sb_->Update(vh, ch, -origin); 568 } 569 } 570 571 // VariableRowHeightScrollHelper ---------------------------------------------- 572 573 VariableRowHeightScrollHelper::VariableRowHeightScrollHelper( 574 Controller* controller) : controller_(controller) { 575 } 576 577 VariableRowHeightScrollHelper::~VariableRowHeightScrollHelper() { 578 } 579 580 int VariableRowHeightScrollHelper::GetPageScrollIncrement( 581 ScrollView* scroll_view, bool is_horizontal, bool is_positive) { 582 if (is_horizontal) 583 return 0; 584 // y coordinate is most likely negative. 585 int y = abs(scroll_view->contents()->y()); 586 int vis_height = scroll_view->contents()->parent()->height(); 587 if (is_positive) { 588 // Align the bottom most row to the top of the view. 589 int bottom = std::min(scroll_view->contents()->height() - 1, 590 y + vis_height); 591 RowInfo bottom_row_info = GetRowInfo(bottom); 592 // If 0, ScrollView will provide a default value. 593 return std::max(0, bottom_row_info.origin - y); 594 } else { 595 // Align the row on the previous page to to the top of the view. 596 int last_page_y = y - vis_height; 597 RowInfo last_page_info = GetRowInfo(std::max(0, last_page_y)); 598 if (last_page_y != last_page_info.origin) 599 return std::max(0, y - last_page_info.origin - last_page_info.height); 600 return std::max(0, y - last_page_info.origin); 601 } 602 } 603 604 int VariableRowHeightScrollHelper::GetLineScrollIncrement( 605 ScrollView* scroll_view, bool is_horizontal, bool is_positive) { 606 if (is_horizontal) 607 return 0; 608 // y coordinate is most likely negative. 609 int y = abs(scroll_view->contents()->y()); 610 RowInfo row = GetRowInfo(y); 611 if (is_positive) { 612 return row.height - (y - row.origin); 613 } else if (y == row.origin) { 614 row = GetRowInfo(std::max(0, row.origin - 1)); 615 return y - row.origin; 616 } else { 617 return y - row.origin; 618 } 619 } 620 621 VariableRowHeightScrollHelper::RowInfo 622 VariableRowHeightScrollHelper::GetRowInfo(int y) { 623 return controller_->GetRowInfo(y); 624 } 625 626 // FixedRowHeightScrollHelper ----------------------------------------------- 627 628 FixedRowHeightScrollHelper::FixedRowHeightScrollHelper(int top_margin, 629 int row_height) 630 : VariableRowHeightScrollHelper(NULL), 631 top_margin_(top_margin), 632 row_height_(row_height) { 633 DCHECK_GT(row_height, 0); 634 } 635 636 VariableRowHeightScrollHelper::RowInfo 637 FixedRowHeightScrollHelper::GetRowInfo(int y) { 638 if (y < top_margin_) 639 return RowInfo(0, top_margin_); 640 return RowInfo((y - top_margin_) / row_height_ * row_height_ + top_margin_, 641 row_height_); 642 } 643 644 } // namespace views 645