1 /* 2 * Copyright (C) 2004, 2006, 2008 Apple Inc. All rights reserved. 3 * 4 * Redistribution and use in source and binary forms, with or without 5 * modification, are permitted provided that the following conditions 6 * are met: 7 * 1. Redistributions of source code must retain the above copyright 8 * notice, this list of conditions and the following disclaimer. 9 * 2. Redistributions in binary form must reproduce the above copyright 10 * notice, this list of conditions and the following disclaimer in the 11 * documentation and/or other materials provided with the distribution. 12 * 13 * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY 14 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 15 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 16 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR 17 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 18 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 19 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 20 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY 21 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 22 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 */ 25 26 #include "config.h" 27 #include "platform/scroll/Scrollbar.h" 28 29 #include <algorithm> 30 #include "platform/graphics/GraphicsContext.h" 31 #include "platform/PlatformGestureEvent.h" 32 #include "platform/PlatformMouseEvent.h" 33 #include "platform/scroll/ScrollAnimator.h" 34 #include "platform/scroll/ScrollView.h" 35 #include "platform/scroll/ScrollableArea.h" 36 #include "platform/scroll/ScrollbarTheme.h" 37 38 #if ((OS(POSIX) && !OS(MACOSX)) || OS(WIN)) 39 // The position of the scrollbar thumb affects the appearance of the steppers, so 40 // when the thumb moves, we have to invalidate them for painting. 41 #define THUMB_POSITION_AFFECTS_BUTTONS 42 #endif 43 44 namespace blink { 45 46 PassRefPtr<Scrollbar> Scrollbar::create(ScrollableArea* scrollableArea, ScrollbarOrientation orientation, ScrollbarControlSize size) 47 { 48 return adoptRef(new Scrollbar(scrollableArea, orientation, size)); 49 } 50 51 Scrollbar::Scrollbar(ScrollableArea* scrollableArea, ScrollbarOrientation orientation, ScrollbarControlSize controlSize, ScrollbarTheme* theme) 52 : m_scrollableArea(scrollableArea) 53 , m_orientation(orientation) 54 , m_controlSize(controlSize) 55 , m_theme(theme) 56 , m_visibleSize(0) 57 , m_totalSize(0) 58 , m_currentPos(0) 59 , m_dragOrigin(0) 60 , m_hoveredPart(NoPart) 61 , m_pressedPart(NoPart) 62 , m_pressedPos(0) 63 , m_scrollPos(0) 64 , m_draggingDocument(false) 65 , m_documentDragPos(0) 66 , m_enabled(true) 67 , m_scrollTimer(this, &Scrollbar::autoscrollTimerFired) 68 , m_overlapsResizer(false) 69 , m_suppressInvalidation(false) 70 , m_isAlphaLocked(false) 71 { 72 if (!m_theme) 73 m_theme = ScrollbarTheme::theme(); 74 75 m_theme->registerScrollbar(this); 76 77 // FIXME: This is ugly and would not be necessary if we fix cross-platform code to actually query for 78 // scrollbar thickness and use it when sizing scrollbars (rather than leaving one dimension of the scrollbar 79 // alone when sizing). 80 int thickness = m_theme->scrollbarThickness(controlSize); 81 Widget::setFrameRect(IntRect(0, 0, thickness, thickness)); 82 83 m_currentPos = scrollableAreaCurrentPos(); 84 } 85 86 Scrollbar::~Scrollbar() 87 { 88 stopTimerIfNeeded(); 89 90 m_theme->unregisterScrollbar(this); 91 } 92 93 void Scrollbar::removeFromParent() 94 { 95 if (parent()) 96 toScrollView(parent())->removeChild(this); 97 } 98 99 ScrollView* Scrollbar::parentScrollView() const 100 { 101 return parent() && parent()->isScrollView() ? toScrollView(parent()) : 0; 102 } 103 104 ScrollbarOverlayStyle Scrollbar::scrollbarOverlayStyle() const 105 { 106 return m_scrollableArea ? m_scrollableArea->scrollbarOverlayStyle() : ScrollbarOverlayStyleDefault; 107 } 108 109 void Scrollbar::getTickmarks(Vector<IntRect>& tickmarks) const 110 { 111 if (m_scrollableArea) 112 m_scrollableArea->getTickmarks(tickmarks); 113 } 114 115 bool Scrollbar::isScrollableAreaActive() const 116 { 117 return m_scrollableArea && m_scrollableArea->isActive(); 118 } 119 120 bool Scrollbar::isScrollViewScrollbar() const 121 { 122 return parent() && parent()->isFrameView() && toScrollView(parent())->isScrollViewScrollbar(this); 123 } 124 125 bool Scrollbar::isLeftSideVerticalScrollbar() const 126 { 127 if (m_orientation == VerticalScrollbar && m_scrollableArea) 128 return m_scrollableArea->shouldPlaceVerticalScrollbarOnLeft(); 129 return false; 130 } 131 132 void Scrollbar::offsetDidChange() 133 { 134 ASSERT(m_scrollableArea); 135 136 float position = scrollableAreaCurrentPos(); 137 if (position == m_currentPos) 138 return; 139 140 int oldThumbPosition = theme()->thumbPosition(this); 141 m_currentPos = position; 142 updateThumbPosition(); 143 if (m_pressedPart == ThumbPart) 144 setPressedPos(m_pressedPos + theme()->thumbPosition(this) - oldThumbPosition); 145 } 146 147 void Scrollbar::setProportion(int visibleSize, int totalSize) 148 { 149 if (visibleSize == m_visibleSize && totalSize == m_totalSize) 150 return; 151 152 m_visibleSize = visibleSize; 153 m_totalSize = totalSize; 154 155 updateThumbProportion(); 156 } 157 158 void Scrollbar::updateThumb() 159 { 160 #ifdef THUMB_POSITION_AFFECTS_BUTTONS 161 invalidate(); 162 #else 163 theme()->invalidateParts(this, ForwardTrackPart | BackTrackPart | ThumbPart); 164 #endif 165 } 166 167 void Scrollbar::updateThumbPosition() 168 { 169 updateThumb(); 170 } 171 172 void Scrollbar::updateThumbProportion() 173 { 174 updateThumb(); 175 } 176 177 void Scrollbar::paint(GraphicsContext* context, const IntRect& damageRect) 178 { 179 if (!frameRect().intersects(damageRect)) 180 return; 181 182 if (!theme()->paint(this, context, damageRect)) 183 Widget::paint(context, damageRect); 184 } 185 186 void Scrollbar::autoscrollTimerFired(Timer<Scrollbar>*) 187 { 188 autoscrollPressedPart(theme()->autoscrollTimerDelay()); 189 } 190 191 static bool thumbUnderMouse(Scrollbar* scrollbar) 192 { 193 int thumbPos = scrollbar->theme()->trackPosition(scrollbar) + scrollbar->theme()->thumbPosition(scrollbar); 194 int thumbLength = scrollbar->theme()->thumbLength(scrollbar); 195 return scrollbar->pressedPos() >= thumbPos && scrollbar->pressedPos() < thumbPos + thumbLength; 196 } 197 198 void Scrollbar::autoscrollPressedPart(double delay) 199 { 200 // Don't do anything for the thumb or if nothing was pressed. 201 if (m_pressedPart == ThumbPart || m_pressedPart == NoPart) 202 return; 203 204 // Handle the track. 205 if ((m_pressedPart == BackTrackPart || m_pressedPart == ForwardTrackPart) && thumbUnderMouse(this)) { 206 theme()->invalidatePart(this, m_pressedPart); 207 setHoveredPart(ThumbPart); 208 return; 209 } 210 211 // Handle the arrows and track. 212 if (m_scrollableArea && m_scrollableArea->scroll(pressedPartScrollDirection(), pressedPartScrollGranularity())) 213 startTimerIfNeeded(delay); 214 } 215 216 void Scrollbar::startTimerIfNeeded(double delay) 217 { 218 // Don't do anything for the thumb. 219 if (m_pressedPart == ThumbPart) 220 return; 221 222 // Handle the track. We halt track scrolling once the thumb is level 223 // with us. 224 if ((m_pressedPart == BackTrackPart || m_pressedPart == ForwardTrackPart) && thumbUnderMouse(this)) { 225 theme()->invalidatePart(this, m_pressedPart); 226 setHoveredPart(ThumbPart); 227 return; 228 } 229 230 // We can't scroll if we've hit the beginning or end. 231 ScrollDirection dir = pressedPartScrollDirection(); 232 if (dir == ScrollUp || dir == ScrollLeft) { 233 if (m_currentPos == 0) 234 return; 235 } else { 236 if (m_currentPos == maximum()) 237 return; 238 } 239 240 m_scrollTimer.startOneShot(delay, FROM_HERE); 241 } 242 243 void Scrollbar::stopTimerIfNeeded() 244 { 245 if (m_scrollTimer.isActive()) 246 m_scrollTimer.stop(); 247 } 248 249 ScrollDirection Scrollbar::pressedPartScrollDirection() 250 { 251 if (m_orientation == HorizontalScrollbar) { 252 if (m_pressedPart == BackButtonStartPart || m_pressedPart == BackButtonEndPart || m_pressedPart == BackTrackPart) 253 return ScrollLeft; 254 return ScrollRight; 255 } else { 256 if (m_pressedPart == BackButtonStartPart || m_pressedPart == BackButtonEndPart || m_pressedPart == BackTrackPart) 257 return ScrollUp; 258 return ScrollDown; 259 } 260 } 261 262 ScrollGranularity Scrollbar::pressedPartScrollGranularity() 263 { 264 if (m_pressedPart == BackButtonStartPart || m_pressedPart == BackButtonEndPart || m_pressedPart == ForwardButtonStartPart || m_pressedPart == ForwardButtonEndPart) 265 return ScrollByLine; 266 return ScrollByPage; 267 } 268 269 void Scrollbar::moveThumb(int pos, bool draggingDocument) 270 { 271 if (!m_scrollableArea) 272 return; 273 274 int delta = pos - m_pressedPos; 275 276 if (draggingDocument) { 277 if (m_draggingDocument) 278 delta = pos - m_documentDragPos; 279 m_draggingDocument = true; 280 FloatPoint currentPosition = m_scrollableArea->scrollAnimator()->currentPosition(); 281 float destinationPosition = (m_orientation == HorizontalScrollbar ? currentPosition.x() : currentPosition.y()) + delta; 282 destinationPosition = m_scrollableArea->clampScrollPosition(m_orientation, destinationPosition); 283 m_scrollableArea->scrollToOffsetWithoutAnimation(m_orientation, destinationPosition); 284 m_documentDragPos = pos; 285 return; 286 } 287 288 if (m_draggingDocument) { 289 delta += m_pressedPos - m_documentDragPos; 290 m_draggingDocument = false; 291 } 292 293 // Drag the thumb. 294 int thumbPos = theme()->thumbPosition(this); 295 int thumbLen = theme()->thumbLength(this); 296 int trackLen = theme()->trackLength(this); 297 if (delta > 0) 298 delta = std::min(trackLen - thumbLen - thumbPos, delta); 299 else if (delta < 0) 300 delta = std::max(-thumbPos, delta); 301 302 float minPos = m_scrollableArea->minimumScrollPosition(m_orientation); 303 float maxPos = m_scrollableArea->maximumScrollPosition(m_orientation); 304 if (delta) { 305 float newPosition = static_cast<float>(thumbPos + delta) * (maxPos - minPos) / (trackLen - thumbLen) + minPos; 306 m_scrollableArea->scrollToOffsetWithoutAnimation(m_orientation, newPosition); 307 } 308 } 309 310 void Scrollbar::setHoveredPart(ScrollbarPart part) 311 { 312 if (part == m_hoveredPart) 313 return; 314 315 if ((m_hoveredPart == NoPart || part == NoPart) && theme()->invalidateOnMouseEnterExit()) 316 invalidate(); // Just invalidate the whole scrollbar, since the buttons at either end change anyway. 317 else if (m_pressedPart == NoPart) { // When there's a pressed part, we don't draw a hovered state, so there's no reason to invalidate. 318 theme()->invalidatePart(this, part); 319 theme()->invalidatePart(this, m_hoveredPart); 320 } 321 m_hoveredPart = part; 322 } 323 324 void Scrollbar::setPressedPart(ScrollbarPart part) 325 { 326 if (m_pressedPart != NoPart) 327 theme()->invalidatePart(this, m_pressedPart); 328 m_pressedPart = part; 329 if (m_pressedPart != NoPart) 330 theme()->invalidatePart(this, m_pressedPart); 331 else if (m_hoveredPart != NoPart) // When we no longer have a pressed part, we can start drawing a hovered state on the hovered part. 332 theme()->invalidatePart(this, m_hoveredPart); 333 } 334 335 bool Scrollbar::gestureEvent(const PlatformGestureEvent& evt) 336 { 337 switch (evt.type()) { 338 case PlatformEvent::GestureTapDown: 339 setPressedPart(theme()->hitTest(this, evt.position())); 340 m_pressedPos = orientation() == HorizontalScrollbar ? convertFromContainingWindow(evt.position()).x() : convertFromContainingWindow(evt.position()).y(); 341 return true; 342 case PlatformEvent::GestureTapDownCancel: 343 case PlatformEvent::GestureScrollBegin: 344 if (m_pressedPart != ThumbPart) 345 return false; 346 m_scrollPos = m_pressedPos; 347 return true; 348 case PlatformEvent::GestureScrollUpdate: 349 case PlatformEvent::GestureScrollUpdateWithoutPropagation: 350 if (m_pressedPart != ThumbPart) 351 return false; 352 m_scrollPos += orientation() == HorizontalScrollbar ? evt.deltaX() : evt.deltaY(); 353 moveThumb(m_scrollPos, false); 354 return true; 355 case PlatformEvent::GestureScrollEnd: 356 case PlatformEvent::GestureLongPress: 357 case PlatformEvent::GestureFlingStart: 358 m_scrollPos = 0; 359 m_pressedPos = 0; 360 setPressedPart(NoPart); 361 return false; 362 case PlatformEvent::GestureTap: { 363 if (m_pressedPart != ThumbPart && m_pressedPart != NoPart && m_scrollableArea 364 && m_scrollableArea->scroll(pressedPartScrollDirection(), pressedPartScrollGranularity())) { 365 return true; 366 } 367 m_scrollPos = 0; 368 m_pressedPos = 0; 369 setPressedPart(NoPart); 370 return false; 371 } 372 default: 373 // By default, we assume that gestures don't deselect the scrollbar. 374 return true; 375 } 376 } 377 378 void Scrollbar::mouseMoved(const PlatformMouseEvent& evt) 379 { 380 if (m_pressedPart == ThumbPart) { 381 if (theme()->shouldSnapBackToDragOrigin(this, evt)) { 382 if (m_scrollableArea) 383 m_scrollableArea->scrollToOffsetWithoutAnimation(m_orientation, m_dragOrigin + m_scrollableArea->minimumScrollPosition(m_orientation)); 384 } else { 385 moveThumb(m_orientation == HorizontalScrollbar ? 386 convertFromContainingWindow(evt.position()).x() : 387 convertFromContainingWindow(evt.position()).y(), theme()->shouldDragDocumentInsteadOfThumb(this, evt)); 388 } 389 return; 390 } 391 392 if (m_pressedPart != NoPart) 393 m_pressedPos = orientation() == HorizontalScrollbar ? convertFromContainingWindow(evt.position()).x() : convertFromContainingWindow(evt.position()).y(); 394 395 ScrollbarPart part = theme()->hitTest(this, evt.position()); 396 if (part != m_hoveredPart) { 397 if (m_pressedPart != NoPart) { 398 if (part == m_pressedPart) { 399 // The mouse is moving back over the pressed part. We 400 // need to start up the timer action again. 401 startTimerIfNeeded(theme()->autoscrollTimerDelay()); 402 theme()->invalidatePart(this, m_pressedPart); 403 } else if (m_hoveredPart == m_pressedPart) { 404 // The mouse is leaving the pressed part. Kill our timer 405 // if needed. 406 stopTimerIfNeeded(); 407 theme()->invalidatePart(this, m_pressedPart); 408 } 409 } 410 411 setHoveredPart(part); 412 } 413 414 return; 415 } 416 417 void Scrollbar::mouseEntered() 418 { 419 if (m_scrollableArea) 420 m_scrollableArea->mouseEnteredScrollbar(this); 421 } 422 423 void Scrollbar::mouseExited() 424 { 425 if (m_scrollableArea) 426 m_scrollableArea->mouseExitedScrollbar(this); 427 setHoveredPart(NoPart); 428 } 429 430 void Scrollbar::mouseUp(const PlatformMouseEvent& mouseEvent) 431 { 432 setPressedPart(NoPart); 433 m_pressedPos = 0; 434 m_draggingDocument = false; 435 stopTimerIfNeeded(); 436 437 if (m_scrollableArea) { 438 // m_hoveredPart won't be updated until the next mouseMoved or mouseDown, so we have to hit test 439 // to really know if the mouse has exited the scrollbar on a mouseUp. 440 ScrollbarPart part = theme()->hitTest(this, mouseEvent.position()); 441 if (part == NoPart) 442 m_scrollableArea->mouseExitedScrollbar(this); 443 } 444 } 445 446 void Scrollbar::mouseDown(const PlatformMouseEvent& evt) 447 { 448 // Early exit for right click 449 if (evt.button() == RightButton) 450 return; 451 452 setPressedPart(theme()->hitTest(this, evt.position())); 453 int pressedPos = orientation() == HorizontalScrollbar ? convertFromContainingWindow(evt.position()).x() : convertFromContainingWindow(evt.position()).y(); 454 455 if ((m_pressedPart == BackTrackPart || m_pressedPart == ForwardTrackPart) && theme()->shouldCenterOnThumb(this, evt)) { 456 setHoveredPart(ThumbPart); 457 setPressedPart(ThumbPart); 458 m_dragOrigin = m_currentPos; 459 int thumbLen = theme()->thumbLength(this); 460 int desiredPos = pressedPos; 461 // Set the pressed position to the middle of the thumb so that when we do the move, the delta 462 // will be from the current pixel position of the thumb to the new desired position for the thumb. 463 m_pressedPos = theme()->trackPosition(this) + theme()->thumbPosition(this) + thumbLen / 2; 464 moveThumb(desiredPos); 465 return; 466 } else if (m_pressedPart == ThumbPart) 467 m_dragOrigin = m_currentPos; 468 469 m_pressedPos = pressedPos; 470 471 autoscrollPressedPart(theme()->initialAutoscrollTimerDelay()); 472 } 473 474 void Scrollbar::setFrameRect(const IntRect& rect) 475 { 476 // Get our window resizer rect and see if we overlap. Adjust to avoid the overlap 477 // if necessary. 478 IntRect adjustedRect(rect); 479 bool overlapsResizer = false; 480 ScrollView* view = parentScrollView(); 481 if (view && !rect.isEmpty() && !view->windowResizerRect().isEmpty()) { 482 IntRect resizerRect = view->convertFromContainingWindow(view->windowResizerRect()); 483 if (rect.intersects(resizerRect)) { 484 if (orientation() == HorizontalScrollbar) { 485 int overlap = rect.maxX() - resizerRect.x(); 486 if (overlap > 0 && resizerRect.maxX() >= rect.maxX()) { 487 adjustedRect.setWidth(rect.width() - overlap); 488 overlapsResizer = true; 489 } 490 } else { 491 int overlap = rect.maxY() - resizerRect.y(); 492 if (overlap > 0 && resizerRect.maxY() >= rect.maxY()) { 493 adjustedRect.setHeight(rect.height() - overlap); 494 overlapsResizer = true; 495 } 496 } 497 } 498 } 499 if (overlapsResizer != m_overlapsResizer) { 500 m_overlapsResizer = overlapsResizer; 501 if (view) 502 view->adjustScrollbarsAvoidingResizerCount(m_overlapsResizer ? 1 : -1); 503 } 504 505 Widget::setFrameRect(adjustedRect); 506 } 507 508 void Scrollbar::setParent(Widget* parentView) 509 { 510 if (!parentView && m_overlapsResizer && parentScrollView()) 511 parentScrollView()->adjustScrollbarsAvoidingResizerCount(-1); 512 Widget::setParent(parentView); 513 } 514 515 void Scrollbar::setEnabled(bool e) 516 { 517 if (m_enabled == e) 518 return; 519 m_enabled = e; 520 theme()->updateEnabledState(this); 521 invalidate(); 522 } 523 524 bool Scrollbar::isOverlayScrollbar() const 525 { 526 return m_theme->usesOverlayScrollbars(); 527 } 528 529 bool Scrollbar::shouldParticipateInHitTesting() 530 { 531 // Non-overlay scrollbars should always participate in hit testing. 532 if (!isOverlayScrollbar()) 533 return true; 534 return m_scrollableArea->scrollAnimator()->shouldScrollbarParticipateInHitTesting(this); 535 } 536 537 bool Scrollbar::isWindowActive() const 538 { 539 return m_scrollableArea && m_scrollableArea->isActive(); 540 } 541 542 void Scrollbar::invalidateRect(const IntRect& rect) 543 { 544 if (suppressInvalidation()) 545 return; 546 547 if (m_scrollableArea) 548 m_scrollableArea->invalidateScrollbar(this, rect); 549 } 550 551 IntRect Scrollbar::convertToContainingView(const IntRect& localRect) const 552 { 553 if (m_scrollableArea) 554 return m_scrollableArea->convertFromScrollbarToContainingView(this, localRect); 555 556 return Widget::convertToContainingView(localRect); 557 } 558 559 IntRect Scrollbar::convertFromContainingView(const IntRect& parentRect) const 560 { 561 if (m_scrollableArea) 562 return m_scrollableArea->convertFromContainingViewToScrollbar(this, parentRect); 563 564 return Widget::convertFromContainingView(parentRect); 565 } 566 567 IntPoint Scrollbar::convertToContainingView(const IntPoint& localPoint) const 568 { 569 if (m_scrollableArea) 570 return m_scrollableArea->convertFromScrollbarToContainingView(this, localPoint); 571 572 return Widget::convertToContainingView(localPoint); 573 } 574 575 IntPoint Scrollbar::convertFromContainingView(const IntPoint& parentPoint) const 576 { 577 if (m_scrollableArea) 578 return m_scrollableArea->convertFromContainingViewToScrollbar(this, parentPoint); 579 580 return Widget::convertFromContainingView(parentPoint); 581 } 582 583 float Scrollbar::scrollableAreaCurrentPos() const 584 { 585 if (!m_scrollableArea) 586 return 0; 587 588 if (m_orientation == HorizontalScrollbar) 589 return m_scrollableArea->scrollPosition().x() - m_scrollableArea->minimumScrollPosition().x(); 590 591 return m_scrollableArea->scrollPosition().y() - m_scrollableArea->minimumScrollPosition().y(); 592 } 593 594 } // namespace blink 595