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