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 "Scrollbar.h" 28 29#include "GraphicsContext.h" 30#include "PlatformMouseEvent.h" 31#include "ScrollAnimator.h" 32#include "ScrollView.h" 33#include "ScrollableArea.h" 34#include "ScrollbarTheme.h" 35#include <algorithm> 36 37#if ENABLE(GESTURE_EVENTS) 38#include "PlatformGestureEvent.h" 39#endif 40 41using namespace std; 42 43#if PLATFORM(GTK) 44// The position of the scrollbar thumb affects the appearance of the steppers, so 45// when the thumb moves, we have to invalidate them for painting. 46#define THUMB_POSITION_AFFECTS_BUTTONS 47#endif 48 49namespace WebCore { 50 51#if !PLATFORM(EFL) 52PassRefPtr<Scrollbar> Scrollbar::createNativeScrollbar(ScrollableArea* scrollableArea, ScrollbarOrientation orientation, ScrollbarControlSize size) 53{ 54 return adoptRef(new Scrollbar(scrollableArea, orientation, size)); 55} 56#endif 57 58int Scrollbar::maxOverlapBetweenPages() 59{ 60 static int maxOverlapBetweenPages = ScrollbarTheme::theme()->maxOverlapBetweenPages(); 61 return maxOverlapBetweenPages; 62} 63 64Scrollbar::Scrollbar(ScrollableArea* scrollableArea, ScrollbarOrientation orientation, ScrollbarControlSize controlSize, 65 ScrollbarTheme* theme) 66 : m_scrollableArea(scrollableArea) 67 , m_orientation(orientation) 68 , m_controlSize(controlSize) 69 , m_theme(theme) 70 , m_visibleSize(0) 71 , m_totalSize(0) 72 , m_currentPos(0) 73 , m_dragOrigin(0) 74 , m_lineStep(0) 75 , m_pageStep(0) 76 , m_pixelStep(1) 77 , m_hoveredPart(NoPart) 78 , m_pressedPart(NoPart) 79 , m_pressedPos(0) 80 , m_scrollPos(0) 81 , m_draggingDocument(false) 82 , m_documentDragPos(0) 83 , m_enabled(true) 84 , m_scrollTimer(this, &Scrollbar::autoscrollTimerFired) 85 , m_overlapsResizer(false) 86 , m_suppressInvalidation(false) 87 , m_isAlphaLocked(false) 88{ 89 if (!m_theme) 90 m_theme = ScrollbarTheme::theme(); 91 92 m_theme->registerScrollbar(this); 93 94 // FIXME: This is ugly and would not be necessary if we fix cross-platform code to actually query for 95 // scrollbar thickness and use it when sizing scrollbars (rather than leaving one dimension of the scrollbar 96 // alone when sizing). 97 int thickness = m_theme->scrollbarThickness(controlSize); 98 Widget::setFrameRect(IntRect(0, 0, thickness, thickness)); 99 100 if (m_scrollableArea) 101 m_currentPos = static_cast<float>(m_scrollableArea->scrollPosition(this)); 102} 103 104Scrollbar::~Scrollbar() 105{ 106 stopTimerIfNeeded(); 107 108 m_theme->unregisterScrollbar(this); 109} 110 111ScrollbarOverlayStyle Scrollbar::scrollbarOverlayStyle() const 112{ 113 return m_scrollableArea ? m_scrollableArea->scrollbarOverlayStyle() : ScrollbarOverlayStyleDefault; 114} 115 116void Scrollbar::getTickmarks(Vector<IntRect>& tickmarks) const 117{ 118 if (m_scrollableArea) 119 m_scrollableArea->getTickmarks(tickmarks); 120} 121 122bool Scrollbar::isScrollableAreaActive() const 123{ 124 return m_scrollableArea && m_scrollableArea->isActive(); 125} 126 127bool Scrollbar::isScrollViewScrollbar() const 128{ 129 return parent() && parent()->isScrollViewScrollbar(this); 130} 131 132void Scrollbar::offsetDidChange() 133{ 134 ASSERT(m_scrollableArea); 135 136 float position = static_cast<float>(m_scrollableArea->scrollPosition(this)); 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 147void 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 158void Scrollbar::setSteps(int lineStep, int pageStep, int pixelsPerStep) 159{ 160 m_lineStep = lineStep; 161 m_pageStep = pageStep; 162 m_pixelStep = 1.0f / pixelsPerStep; 163} 164 165void Scrollbar::updateThumb() 166{ 167#ifdef THUMB_POSITION_AFFECTS_BUTTONS 168 invalidate(); 169#else 170 theme()->invalidateParts(this, ForwardTrackPart | BackTrackPart | ThumbPart); 171#endif 172} 173 174void Scrollbar::updateThumbPosition() 175{ 176 updateThumb(); 177} 178 179void Scrollbar::updateThumbProportion() 180{ 181 updateThumb(); 182} 183 184void 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 198void Scrollbar::autoscrollTimerFired(Timer<Scrollbar>*) 199{ 200 autoscrollPressedPart(theme()->autoscrollTimerDelay()); 201} 202 203static 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 210void 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 228void 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 255void Scrollbar::stopTimerIfNeeded() 256{ 257 if (m_scrollTimer.isActive()) 258 m_scrollTimer.stop(); 259} 260 261ScrollDirection 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 274ScrollGranularity 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 281void 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 int destinationPosition = (m_orientation == HorizontalScrollbar ? currentPosition.x() : currentPosition.y()) + delta; 294 if (delta > 0) 295 destinationPosition = min(destinationPosition + delta, maximum()); 296 else if (delta < 0) 297 destinationPosition = max(destinationPosition + delta, 0); 298 m_scrollableArea->scrollToOffsetWithoutAnimation(m_orientation, destinationPosition); 299 m_documentDragPos = pos; 300 return; 301 } 302 303 if (m_draggingDocument) { 304 delta += m_pressedPos - m_documentDragPos; 305 m_draggingDocument = false; 306 } 307 308 // Drag the thumb. 309 int thumbPos = theme()->thumbPosition(this); 310 int thumbLen = theme()->thumbLength(this); 311 int trackLen = theme()->trackLength(this); 312 int maxPos = trackLen - thumbLen; 313 if (delta > 0) 314 delta = min(maxPos - thumbPos, delta); 315 else if (delta < 0) 316 delta = max(-thumbPos, delta); 317 318 if (delta) { 319 float newPosition = static_cast<float>(thumbPos + delta) * maximum() / (trackLen - thumbLen); 320 m_scrollableArea->scrollToOffsetWithoutAnimation(m_orientation, newPosition); 321 } 322} 323 324void Scrollbar::setHoveredPart(ScrollbarPart part) 325{ 326 if (part == m_hoveredPart) 327 return; 328 329 if ((m_hoveredPart == NoPart || part == NoPart) && theme()->invalidateOnMouseEnterExit()) 330 invalidate(); // Just invalidate the whole scrollbar, since the buttons at either end change anyway. 331 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. 332 theme()->invalidatePart(this, part); 333 theme()->invalidatePart(this, m_hoveredPart); 334 } 335 m_hoveredPart = part; 336} 337 338void Scrollbar::setPressedPart(ScrollbarPart part) 339{ 340 if (m_pressedPart != NoPart) 341 theme()->invalidatePart(this, m_pressedPart); 342 m_pressedPart = part; 343 if (m_pressedPart != NoPart) 344 theme()->invalidatePart(this, m_pressedPart); 345 else if (m_hoveredPart != NoPart) // When we no longer have a pressed part, we can start drawing a hovered state on the hovered part. 346 theme()->invalidatePart(this, m_hoveredPart); 347} 348 349#if ENABLE(GESTURE_EVENTS) 350bool Scrollbar::gestureEvent(const PlatformGestureEvent& evt) 351{ 352 bool handled = false; 353 switch (evt.type()) { 354 case PlatformEvent::GestureTapDown: 355 setPressedPart(theme()->hitTest(this, evt.position())); 356 m_pressedPos = (orientation() == HorizontalScrollbar ? convertFromContainingWindow(evt.position()).x() : convertFromContainingWindow(evt.position()).y()); 357 return true; 358 case PlatformEvent::GestureTapDownCancel: 359 case PlatformEvent::GestureScrollBegin: 360 if (m_pressedPart == ThumbPart) { 361 m_scrollPos = m_pressedPos; 362 return true; 363 } 364 break; 365 case PlatformEvent::GestureScrollUpdate: 366 case PlatformEvent::GestureScrollUpdateWithoutPropagation: 367 if (m_pressedPart == ThumbPart) { 368 m_scrollPos += HorizontalScrollbar ? evt.deltaX() : evt.deltaY(); 369 moveThumb(m_scrollPos, false); 370 return true; 371 } 372 break; 373 case PlatformEvent::GestureScrollEnd: 374 m_scrollPos = 0; 375 break; 376 case PlatformEvent::GestureTap: 377 if (m_pressedPart != ThumbPart && m_pressedPart != NoPart) 378 handled = m_scrollableArea && m_scrollableArea->scroll(pressedPartScrollDirection(), pressedPartScrollGranularity()); 379 break; 380 default: 381 break; 382 } 383 setPressedPart(NoPart); 384 m_pressedPos = 0; 385 return handled; 386} 387#endif 388 389bool Scrollbar::mouseMoved(const PlatformMouseEvent& evt) 390{ 391 if (m_pressedPart == ThumbPart) { 392 if (theme()->shouldSnapBackToDragOrigin(this, evt)) { 393 if (m_scrollableArea) 394 m_scrollableArea->scrollToOffsetWithoutAnimation(m_orientation, m_dragOrigin); 395 } else { 396 moveThumb(m_orientation == HorizontalScrollbar ? 397 convertFromContainingWindow(evt.position()).x() : 398 convertFromContainingWindow(evt.position()).y(), theme()->shouldDragDocumentInsteadOfThumb(this, evt)); 399 } 400 return true; 401 } 402 403 if (m_pressedPart != NoPart) 404 m_pressedPos = (orientation() == HorizontalScrollbar ? convertFromContainingWindow(evt.position()).x() : convertFromContainingWindow(evt.position()).y()); 405 406 ScrollbarPart part = theme()->hitTest(this, evt.position()); 407 if (part != m_hoveredPart) { 408 if (m_pressedPart != NoPart) { 409 if (part == m_pressedPart) { 410 // The mouse is moving back over the pressed part. We 411 // need to start up the timer action again. 412 startTimerIfNeeded(theme()->autoscrollTimerDelay()); 413 theme()->invalidatePart(this, m_pressedPart); 414 } else if (m_hoveredPart == m_pressedPart) { 415 // The mouse is leaving the pressed part. Kill our timer 416 // if needed. 417 stopTimerIfNeeded(); 418 theme()->invalidatePart(this, m_pressedPart); 419 } 420 } 421 422 setHoveredPart(part); 423 } 424 425 return true; 426} 427 428void Scrollbar::mouseEntered() 429{ 430 if (m_scrollableArea) 431 m_scrollableArea->mouseEnteredScrollbar(this); 432} 433 434bool Scrollbar::mouseExited() 435{ 436 if (m_scrollableArea) 437 m_scrollableArea->mouseExitedScrollbar(this); 438 setHoveredPart(NoPart); 439 return true; 440} 441 442bool 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 return true; 458} 459 460bool Scrollbar::mouseDown(const PlatformMouseEvent& evt) 461{ 462 // Early exit for right click 463 if (evt.button() == RightButton) 464 return true; // FIXME: Handled as context menu by Qt right now. Should just avoid even calling this method on a right click though. 465 466 setPressedPart(theme()->hitTest(this, evt.position())); 467 int pressedPos = (orientation() == HorizontalScrollbar ? convertFromContainingWindow(evt.position()).x() : convertFromContainingWindow(evt.position()).y()); 468 469 if ((m_pressedPart == BackTrackPart || m_pressedPart == ForwardTrackPart) && theme()->shouldCenterOnThumb(this, evt)) { 470 setHoveredPart(ThumbPart); 471 setPressedPart(ThumbPart); 472 m_dragOrigin = m_currentPos; 473 int thumbLen = theme()->thumbLength(this); 474 int desiredPos = pressedPos; 475 // Set the pressed position to the middle of the thumb so that when we do the move, the delta 476 // will be from the current pixel position of the thumb to the new desired position for the thumb. 477 m_pressedPos = theme()->trackPosition(this) + theme()->thumbPosition(this) + thumbLen / 2; 478 moveThumb(desiredPos); 479 return true; 480 } else if (m_pressedPart == ThumbPart) 481 m_dragOrigin = m_currentPos; 482 483 m_pressedPos = pressedPos; 484 485 autoscrollPressedPart(theme()->initialAutoscrollTimerDelay()); 486 return true; 487} 488 489void Scrollbar::setFrameRect(const IntRect& rect) 490{ 491 // Get our window resizer rect and see if we overlap. Adjust to avoid the overlap 492 // if necessary. 493 IntRect adjustedRect(rect); 494 bool overlapsResizer = false; 495 ScrollView* view = parent(); 496 if (view && !rect.isEmpty() && !view->windowResizerRect().isEmpty()) { 497 IntRect resizerRect = view->convertFromContainingWindow(view->windowResizerRect()); 498 if (rect.intersects(resizerRect)) { 499 if (orientation() == HorizontalScrollbar) { 500 int overlap = rect.maxX() - resizerRect.x(); 501 if (overlap > 0 && resizerRect.maxX() >= rect.maxX()) { 502 adjustedRect.setWidth(rect.width() - overlap); 503 overlapsResizer = true; 504 } 505 } else { 506 int overlap = rect.maxY() - resizerRect.y(); 507 if (overlap > 0 && resizerRect.maxY() >= rect.maxY()) { 508 adjustedRect.setHeight(rect.height() - overlap); 509 overlapsResizer = true; 510 } 511 } 512 } 513 } 514 if (overlapsResizer != m_overlapsResizer) { 515 m_overlapsResizer = overlapsResizer; 516 if (view) 517 view->adjustScrollbarsAvoidingResizerCount(m_overlapsResizer ? 1 : -1); 518 } 519 520 Widget::setFrameRect(adjustedRect); 521} 522 523void Scrollbar::setParent(ScrollView* parentView) 524{ 525 if (!parentView && m_overlapsResizer && parent()) 526 parent()->adjustScrollbarsAvoidingResizerCount(-1); 527 Widget::setParent(parentView); 528} 529 530void Scrollbar::setEnabled(bool e) 531{ 532 if (m_enabled == e) 533 return; 534 m_enabled = e; 535 theme()->updateEnabledState(this); 536 invalidate(); 537} 538 539bool Scrollbar::isOverlayScrollbar() const 540{ 541 return m_theme->usesOverlayScrollbars(); 542} 543 544bool Scrollbar::shouldParticipateInHitTesting() 545{ 546 // Non-overlay scrollbars should always participate in hit testing. 547 if (!isOverlayScrollbar()) 548 return true; 549 return m_scrollableArea->scrollAnimator()->shouldScrollbarParticipateInHitTesting(this); 550} 551 552bool Scrollbar::isWindowActive() const 553{ 554 return m_scrollableArea && m_scrollableArea->isActive(); 555} 556 557void Scrollbar::invalidateRect(const IntRect& rect) 558{ 559 if (suppressInvalidation()) 560 return; 561 562 if (m_scrollableArea) 563 m_scrollableArea->invalidateScrollbar(this, rect); 564} 565 566IntRect Scrollbar::convertToContainingView(const IntRect& localRect) const 567{ 568 if (m_scrollableArea) 569 return m_scrollableArea->convertFromScrollbarToContainingView(this, localRect); 570 571 return Widget::convertToContainingView(localRect); 572} 573 574IntRect Scrollbar::convertFromContainingView(const IntRect& parentRect) const 575{ 576 if (m_scrollableArea) 577 return m_scrollableArea->convertFromContainingViewToScrollbar(this, parentRect); 578 579 return Widget::convertFromContainingView(parentRect); 580} 581 582IntPoint Scrollbar::convertToContainingView(const IntPoint& localPoint) const 583{ 584 if (m_scrollableArea) 585 return m_scrollableArea->convertFromScrollbarToContainingView(this, localPoint); 586 587 return Widget::convertToContainingView(localPoint); 588} 589 590IntPoint Scrollbar::convertFromContainingView(const IntPoint& parentPoint) const 591{ 592 if (m_scrollableArea) 593 return m_scrollableArea->convertFromContainingViewToScrollbar(this, parentPoint); 594 595 return Widget::convertFromContainingView(parentPoint); 596} 597 598} // namespace WebCore 599