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