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