Scrollbar.cpp revision cad810f21b803229eb11403f9209855525a25d57
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 "AccessibilityScrollbar.h" 30#include "AXObjectCache.h" 31#include "EventHandler.h" 32#include "Frame.h" 33#include "FrameView.h" 34#include "GraphicsContext.h" 35#include "PlatformMouseEvent.h" 36#include "ScrollbarClient.h" 37#include "ScrollbarTheme.h" 38 39#include <algorithm> 40 41using namespace std; 42 43#if (PLATFORM(CHROMIUM) && (OS(LINUX) || OS(FREEBSD))) || 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(ScrollbarClient* client, ScrollbarOrientation orientation, ScrollbarControlSize size) 53{ 54 return adoptRef(new Scrollbar(client, orientation, size)); 55} 56#endif 57 58int Scrollbar::maxOverlapBetweenPages() 59{ 60 static int maxOverlapBetweenPages = ScrollbarTheme::nativeTheme()->maxOverlapBetweenPages(); 61 return maxOverlapBetweenPages; 62} 63 64Scrollbar::Scrollbar(ScrollbarClient* client, ScrollbarOrientation orientation, ScrollbarControlSize controlSize, 65 ScrollbarTheme* theme) 66 : m_client(client) 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_enabled(true) 81 , m_scrollTimer(this, &Scrollbar::autoscrollTimerFired) 82 , m_overlapsResizer(false) 83 , m_suppressInvalidation(false) 84{ 85 if (!m_theme) 86 m_theme = ScrollbarTheme::nativeTheme(); 87 88 m_theme->registerScrollbar(this); 89 90 // FIXME: This is ugly and would not be necessary if we fix cross-platform code to actually query for 91 // scrollbar thickness and use it when sizing scrollbars (rather than leaving one dimension of the scrollbar 92 // alone when sizing). 93 int thickness = m_theme->scrollbarThickness(controlSize); 94 Widget::setFrameRect(IntRect(0, 0, thickness, thickness)); 95} 96 97Scrollbar::~Scrollbar() 98{ 99 if (AXObjectCache::accessibilityEnabled() && axObjectCache()) 100 axObjectCache()->remove(this); 101 102 stopTimerIfNeeded(); 103 104 m_theme->unregisterScrollbar(this); 105} 106 107bool Scrollbar::setValue(int v, ScrollSource source) 108{ 109 v = max(min(v, m_totalSize - m_visibleSize), 0); 110 if (value() == v) 111 return false; // Our value stayed the same. 112 setCurrentPos(v, source); 113 return true; 114} 115 116void Scrollbar::setProportion(int visibleSize, int totalSize) 117{ 118 if (visibleSize == m_visibleSize && totalSize == m_totalSize) 119 return; 120 121 m_visibleSize = visibleSize; 122 m_totalSize = totalSize; 123 124 updateThumbProportion(); 125} 126 127void Scrollbar::setSteps(int lineStep, int pageStep, int pixelsPerStep) 128{ 129 m_lineStep = lineStep; 130 m_pageStep = pageStep; 131 m_pixelStep = 1.0f / pixelsPerStep; 132} 133 134bool Scrollbar::scroll(ScrollDirection direction, ScrollGranularity granularity, float multiplier) 135{ 136#if HAVE(ACCESSIBILITY) 137 if (AXObjectCache::accessibilityEnabled() && axObjectCache()) 138 axObjectCache()->postNotification(axObjectCache()->getOrCreate(this), 0, AXObjectCache::AXValueChanged, true); 139#endif 140 141 // Ignore perpendicular scrolls. 142 if ((m_orientation == HorizontalScrollbar) ? (direction == ScrollUp || direction == ScrollDown) : (direction == ScrollLeft || direction == ScrollRight)) 143 return false; 144 float step = 0; 145 switch (granularity) { 146 case ScrollByLine: step = m_lineStep; break; 147 case ScrollByPage: step = m_pageStep; break; 148 case ScrollByDocument: step = m_totalSize; break; 149 case ScrollByPixel: step = m_pixelStep; break; 150 } 151 if (direction == ScrollUp || direction == ScrollLeft) 152 multiplier = -multiplier; 153 if (client()) 154 return client()->scroll(m_orientation, granularity, step, multiplier); 155 156 return setCurrentPos(max(min(m_currentPos + (step * multiplier), static_cast<float>(m_totalSize - m_visibleSize)), 0.0f), NotFromScrollAnimator); 157} 158 159void Scrollbar::updateThumb() 160{ 161#ifdef THUMB_POSITION_AFFECTS_BUTTONS 162 invalidate(); 163#else 164 theme()->invalidateParts(this, ForwardTrackPart | BackTrackPart | ThumbPart); 165#endif 166} 167 168void Scrollbar::updateThumbPosition() 169{ 170 updateThumb(); 171} 172 173void Scrollbar::updateThumbProportion() 174{ 175 updateThumb(); 176} 177 178void Scrollbar::paint(GraphicsContext* context, const IntRect& damageRect) 179{ 180 if (context->updatingControlTints() && theme()->supportsControlTints()) { 181 invalidate(); 182 return; 183 } 184 185 if (context->paintingDisabled() || !frameRect().intersects(damageRect)) 186 return; 187 188 if (!theme()->paint(this, context, damageRect)) 189 Widget::paint(context, damageRect); 190} 191 192void Scrollbar::autoscrollTimerFired(Timer<Scrollbar>*) 193{ 194 autoscrollPressedPart(theme()->autoscrollTimerDelay()); 195} 196 197static bool thumbUnderMouse(Scrollbar* scrollbar) 198{ 199 int thumbPos = scrollbar->theme()->trackPosition(scrollbar) + scrollbar->theme()->thumbPosition(scrollbar); 200 int thumbLength = scrollbar->theme()->thumbLength(scrollbar); 201 return scrollbar->pressedPos() >= thumbPos && scrollbar->pressedPos() < thumbPos + thumbLength; 202} 203 204void Scrollbar::autoscrollPressedPart(double delay) 205{ 206 // Don't do anything for the thumb or if nothing was pressed. 207 if (m_pressedPart == ThumbPart || m_pressedPart == NoPart) 208 return; 209 210 // Handle the track. 211 if ((m_pressedPart == BackTrackPart || m_pressedPart == ForwardTrackPart) && thumbUnderMouse(this)) { 212 theme()->invalidatePart(this, m_pressedPart); 213 setHoveredPart(ThumbPart); 214 return; 215 } 216 217 // Handle the arrows and track. 218 if (scroll(pressedPartScrollDirection(), pressedPartScrollGranularity())) 219 startTimerIfNeeded(delay); 220} 221 222void Scrollbar::startTimerIfNeeded(double delay) 223{ 224 // Don't do anything for the thumb. 225 if (m_pressedPart == ThumbPart) 226 return; 227 228 // Handle the track. We halt track scrolling once the thumb is level 229 // with us. 230 if ((m_pressedPart == BackTrackPart || m_pressedPart == ForwardTrackPart) && thumbUnderMouse(this)) { 231 theme()->invalidatePart(this, m_pressedPart); 232 setHoveredPart(ThumbPart); 233 return; 234 } 235 236 // We can't scroll if we've hit the beginning or end. 237 ScrollDirection dir = pressedPartScrollDirection(); 238 if (dir == ScrollUp || dir == ScrollLeft) { 239 if (m_currentPos == 0) 240 return; 241 } else { 242 if (m_currentPos == maximum()) 243 return; 244 } 245 246 m_scrollTimer.startOneShot(delay); 247} 248 249void Scrollbar::stopTimerIfNeeded() 250{ 251 if (m_scrollTimer.isActive()) 252 m_scrollTimer.stop(); 253} 254 255ScrollDirection Scrollbar::pressedPartScrollDirection() 256{ 257 if (m_orientation == HorizontalScrollbar) { 258 if (m_pressedPart == BackButtonStartPart || m_pressedPart == BackButtonEndPart || m_pressedPart == BackTrackPart) 259 return ScrollLeft; 260 return ScrollRight; 261 } else { 262 if (m_pressedPart == BackButtonStartPart || m_pressedPart == BackButtonEndPart || m_pressedPart == BackTrackPart) 263 return ScrollUp; 264 return ScrollDown; 265 } 266} 267 268ScrollGranularity Scrollbar::pressedPartScrollGranularity() 269{ 270 if (m_pressedPart == BackButtonStartPart || m_pressedPart == BackButtonEndPart || m_pressedPart == ForwardButtonStartPart || m_pressedPart == ForwardButtonEndPart) 271 return ScrollByLine; 272 return ScrollByPage; 273} 274 275void Scrollbar::moveThumb(int pos) 276{ 277 // Drag the thumb. 278 int thumbPos = theme()->thumbPosition(this); 279 int thumbLen = theme()->thumbLength(this); 280 int trackLen = theme()->trackLength(this); 281 int maxPos = trackLen - thumbLen; 282 int delta = pos - m_pressedPos; 283 if (delta > 0) 284 delta = min(maxPos - thumbPos, delta); 285 else if (delta < 0) 286 delta = max(-thumbPos, delta); 287 if (delta) 288 setCurrentPos(static_cast<float>(thumbPos + delta) * maximum() / (trackLen - thumbLen), NotFromScrollAnimator); 289} 290 291bool Scrollbar::setCurrentPos(float pos, ScrollSource source) 292{ 293 if ((source != FromScrollAnimator) && client()) 294 client()->setScrollPositionAndStopAnimation(m_orientation, pos); 295 296 if (pos == m_currentPos) 297 return false; 298 299 int oldValue = value(); 300 int oldThumbPos = theme()->thumbPosition(this); 301 m_currentPos = pos; 302 updateThumbPosition(); 303 if (m_pressedPart == ThumbPart) 304 setPressedPos(m_pressedPos + theme()->thumbPosition(this) - oldThumbPos); 305 306 if (value() != oldValue && client()) 307 client()->valueChanged(this); 308 return true; 309} 310 311void Scrollbar::setHoveredPart(ScrollbarPart part) 312{ 313 if (part == m_hoveredPart) 314 return; 315 316 if ((m_hoveredPart == NoPart || part == NoPart) && theme()->invalidateOnMouseEnterExit()) 317 invalidate(); // Just invalidate the whole scrollbar, since the buttons at either end change anyway. 318 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. 319 theme()->invalidatePart(this, part); 320 theme()->invalidatePart(this, m_hoveredPart); 321 } 322 m_hoveredPart = part; 323} 324 325void Scrollbar::setPressedPart(ScrollbarPart part) 326{ 327 if (m_pressedPart != NoPart) 328 theme()->invalidatePart(this, m_pressedPart); 329 m_pressedPart = part; 330 if (m_pressedPart != NoPart) 331 theme()->invalidatePart(this, m_pressedPart); 332 else if (m_hoveredPart != NoPart) // When we no longer have a pressed part, we can start drawing a hovered state on the hovered part. 333 theme()->invalidatePart(this, m_hoveredPart); 334} 335 336bool Scrollbar::mouseMoved(const PlatformMouseEvent& evt) 337{ 338 if (m_pressedPart == ThumbPart) { 339 if (theme()->shouldSnapBackToDragOrigin(this, evt)) 340 setCurrentPos(m_dragOrigin, NotFromScrollAnimator); 341 else { 342 moveThumb(m_orientation == HorizontalScrollbar ? 343 convertFromContainingWindow(evt.pos()).x() : 344 convertFromContainingWindow(evt.pos()).y()); 345 } 346 return true; 347 } 348 349 if (m_pressedPart != NoPart) 350 m_pressedPos = (orientation() == HorizontalScrollbar ? convertFromContainingWindow(evt.pos()).x() : convertFromContainingWindow(evt.pos()).y()); 351 352 ScrollbarPart part = theme()->hitTest(this, evt); 353 if (part != m_hoveredPart) { 354 if (m_pressedPart != NoPart) { 355 if (part == m_pressedPart) { 356 // The mouse is moving back over the pressed part. We 357 // need to start up the timer action again. 358 startTimerIfNeeded(theme()->autoscrollTimerDelay()); 359 theme()->invalidatePart(this, m_pressedPart); 360 } else if (m_hoveredPart == m_pressedPart) { 361 // The mouse is leaving the pressed part. Kill our timer 362 // if needed. 363 stopTimerIfNeeded(); 364 theme()->invalidatePart(this, m_pressedPart); 365 } 366 } 367 368 setHoveredPart(part); 369 } 370 371 return true; 372} 373 374bool Scrollbar::mouseExited() 375{ 376 setHoveredPart(NoPart); 377 return true; 378} 379 380bool Scrollbar::mouseUp() 381{ 382 setPressedPart(NoPart); 383 m_pressedPos = 0; 384 stopTimerIfNeeded(); 385 386 if (parent() && parent()->isFrameView()) 387 static_cast<FrameView*>(parent())->frame()->eventHandler()->setMousePressed(false); 388 389 return true; 390} 391 392bool Scrollbar::mouseDown(const PlatformMouseEvent& evt) 393{ 394 // Early exit for right click 395 if (evt.button() == RightButton) 396 return true; // FIXME: Handled as context menu by Qt right now. Should just avoid even calling this method on a right click though. 397 398 setPressedPart(theme()->hitTest(this, evt)); 399 int pressedPos = (orientation() == HorizontalScrollbar ? convertFromContainingWindow(evt.pos()).x() : convertFromContainingWindow(evt.pos()).y()); 400 401 if ((m_pressedPart == BackTrackPart || m_pressedPart == ForwardTrackPart) && theme()->shouldCenterOnThumb(this, evt)) { 402 setHoveredPart(ThumbPart); 403 setPressedPart(ThumbPart); 404 m_dragOrigin = m_currentPos; 405 int thumbLen = theme()->thumbLength(this); 406 int desiredPos = pressedPos; 407 // Set the pressed position to the middle of the thumb so that when we do the move, the delta 408 // will be from the current pixel position of the thumb to the new desired position for the thumb. 409 m_pressedPos = theme()->trackPosition(this) + theme()->thumbPosition(this) + thumbLen / 2; 410 moveThumb(desiredPos); 411 return true; 412 } else if (m_pressedPart == ThumbPart) 413 m_dragOrigin = m_currentPos; 414 415 m_pressedPos = pressedPos; 416 417 autoscrollPressedPart(theme()->initialAutoscrollTimerDelay()); 418 return true; 419} 420 421void Scrollbar::setFrameRect(const IntRect& rect) 422{ 423 // Get our window resizer rect and see if we overlap. Adjust to avoid the overlap 424 // if necessary. 425 IntRect adjustedRect(rect); 426 bool overlapsResizer = false; 427 ScrollView* view = parent(); 428 if (view && !rect.isEmpty() && !view->windowResizerRect().isEmpty()) { 429 IntRect resizerRect = view->convertFromContainingWindow(view->windowResizerRect()); 430 if (rect.intersects(resizerRect)) { 431 if (orientation() == HorizontalScrollbar) { 432 int overlap = rect.right() - resizerRect.x(); 433 if (overlap > 0 && resizerRect.right() >= rect.right()) { 434 adjustedRect.setWidth(rect.width() - overlap); 435 overlapsResizer = true; 436 } 437 } else { 438 int overlap = rect.bottom() - resizerRect.y(); 439 if (overlap > 0 && resizerRect.bottom() >= rect.bottom()) { 440 adjustedRect.setHeight(rect.height() - overlap); 441 overlapsResizer = true; 442 } 443 } 444 } 445 } 446 if (overlapsResizer != m_overlapsResizer) { 447 m_overlapsResizer = overlapsResizer; 448 if (view) 449 view->adjustScrollbarsAvoidingResizerCount(m_overlapsResizer ? 1 : -1); 450 } 451 452 Widget::setFrameRect(adjustedRect); 453} 454 455void Scrollbar::setParent(ScrollView* parentView) 456{ 457 if (!parentView && m_overlapsResizer && parent()) 458 parent()->adjustScrollbarsAvoidingResizerCount(-1); 459 Widget::setParent(parentView); 460} 461 462void Scrollbar::setEnabled(bool e) 463{ 464 if (m_enabled == e) 465 return; 466 m_enabled = e; 467 invalidate(); 468} 469 470bool Scrollbar::isWindowActive() const 471{ 472 return m_client && m_client->isActive(); 473} 474 475AXObjectCache* Scrollbar::axObjectCache() const 476{ 477 if (!parent() || !parent()->isFrameView()) 478 return 0; 479 480 Document* document = static_cast<FrameView*>(parent())->frame()->document(); 481 return document->axObjectCache(); 482} 483 484void Scrollbar::invalidateRect(const IntRect& rect) 485{ 486 if (suppressInvalidation()) 487 return; 488 if (m_client) 489 m_client->invalidateScrollbarRect(this, rect); 490} 491 492IntRect Scrollbar::convertToContainingView(const IntRect& localRect) const 493{ 494 if (m_client) 495 return m_client->convertFromScrollbarToContainingView(this, localRect); 496 497 return Widget::convertToContainingView(localRect); 498} 499 500IntRect Scrollbar::convertFromContainingView(const IntRect& parentRect) const 501{ 502 if (m_client) 503 return m_client->convertFromContainingViewToScrollbar(this, parentRect); 504 505 return Widget::convertFromContainingView(parentRect); 506} 507 508IntPoint Scrollbar::convertToContainingView(const IntPoint& localPoint) const 509{ 510 if (m_client) 511 return m_client->convertFromScrollbarToContainingView(this, localPoint); 512 513 return Widget::convertToContainingView(localPoint); 514} 515 516IntPoint Scrollbar::convertFromContainingView(const IntPoint& parentPoint) const 517{ 518 if (m_client) 519 return m_client->convertFromContainingViewToScrollbar(this, parentPoint); 520 521 return Widget::convertFromContainingView(parentPoint); 522} 523 524} 525