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