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