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