Scrollbar.cpp revision 635860845790a19bf50bbc51ba8fb66a96dde068
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 "EventHandler.h" 30#include "Frame.h" 31#include "FrameView.h" 32#include "GraphicsContext.h" 33#include "PlatformMouseEvent.h" 34#include "ScrollbarClient.h" 35#include "ScrollbarTheme.h" 36 37#include <algorithm> 38 39using std::max; 40using std::min; 41 42namespace WebCore { 43 44#if !PLATFORM(GTK) 45PassRefPtr<Scrollbar> Scrollbar::createNativeScrollbar(ScrollbarClient* client, ScrollbarOrientation orientation, ScrollbarControlSize size) 46{ 47 return adoptRef(new Scrollbar(client, orientation, size)); 48} 49#endif 50 51Scrollbar::Scrollbar(ScrollbarClient* client, ScrollbarOrientation orientation, ScrollbarControlSize controlSize, 52 ScrollbarTheme* theme) 53 : m_client(client) 54 , m_orientation(orientation) 55 , m_controlSize(controlSize) 56 , m_theme(theme) 57 , m_visibleSize(0) 58 , m_totalSize(0) 59 , m_currentPos(0) 60 , m_lineStep(0) 61 , m_pageStep(0) 62 , m_pixelStep(1) 63 , m_hoveredPart(NoPart) 64 , m_pressedPart(NoPart) 65 , m_pressedPos(0) 66 , m_enabled(true) 67 , m_scrollTimer(this, &Scrollbar::autoscrollTimerFired) 68 , m_overlapsResizer(false) 69 , m_suppressInvalidation(false) 70{ 71 if (!m_theme) 72 m_theme = ScrollbarTheme::nativeTheme(); 73 74 m_theme->registerScrollbar(this); 75 76 // FIXME: This is ugly and would not be necessary if we fix cross-platform code to actually query for 77 // scrollbar thickness and use it when sizing scrollbars (rather than leaving one dimension of the scrollbar 78 // alone when sizing). 79 int thickness = m_theme->scrollbarThickness(controlSize); 80 Widget::setFrameRect(IntRect(0, 0, thickness, thickness)); 81} 82 83Scrollbar::~Scrollbar() 84{ 85 stopTimerIfNeeded(); 86 87 m_theme->unregisterScrollbar(this); 88} 89 90bool Scrollbar::setValue(int v) 91{ 92 v = max(min(v, m_totalSize - m_visibleSize), 0); 93 if (value() == v) 94 return false; // Our value stayed the same. 95 m_currentPos = v; 96 97 updateThumbPosition(); 98 99 if (client()) 100 client()->valueChanged(this); 101 102 return true; 103} 104 105void Scrollbar::setProportion(int visibleSize, int totalSize) 106{ 107 if (visibleSize == m_visibleSize && totalSize == m_totalSize) 108 return; 109 110 m_visibleSize = visibleSize; 111 m_totalSize = totalSize; 112 113 updateThumbProportion(); 114} 115 116void Scrollbar::setSteps(int lineStep, int pageStep, int pixelsPerStep) 117{ 118 m_lineStep = lineStep; 119 m_pageStep = pageStep; 120 m_pixelStep = 1.0f / pixelsPerStep; 121} 122 123bool Scrollbar::scroll(ScrollDirection direction, ScrollGranularity granularity, float multiplier) 124{ 125 float step = 0; 126 if ((direction == ScrollUp && m_orientation == VerticalScrollbar) || (direction == ScrollLeft && m_orientation == HorizontalScrollbar)) 127 step = -1; 128 else if ((direction == ScrollDown && m_orientation == VerticalScrollbar) || (direction == ScrollRight && m_orientation == HorizontalScrollbar)) 129 step = 1; 130 131 if (granularity == ScrollByLine) 132 step *= m_lineStep; 133 else if (granularity == ScrollByPage) 134 step *= m_pageStep; 135 else if (granularity == ScrollByDocument) 136 step *= m_totalSize; 137 else if (granularity == ScrollByPixel) 138 step *= m_pixelStep; 139 140 float newPos = m_currentPos + step * multiplier; 141 float maxPos = m_totalSize - m_visibleSize; 142 newPos = max(min(newPos, maxPos), 0.0f); 143 144 if (newPos == m_currentPos) 145 return false; 146 147 int oldValue = value(); 148 m_currentPos = newPos; 149 updateThumbPosition(); 150 151 if (value() != oldValue && client()) 152 client()->valueChanged(this); 153 154 // return true even if the integer value did not change so that scroll event gets eaten 155 return true; 156} 157 158void Scrollbar::updateThumbPosition() 159{ 160 theme()->invalidateParts(this, ForwardTrackPart | BackTrackPart | ThumbPart); 161} 162 163void Scrollbar::updateThumbProportion() 164{ 165 theme()->invalidateParts(this, ForwardTrackPart | BackTrackPart | ThumbPart); 166} 167 168void Scrollbar::paint(GraphicsContext* context, const IntRect& damageRect) 169{ 170 if (context->updatingControlTints() && theme()->supportsControlTints()) { 171 invalidate(); 172 return; 173 } 174 175 if (context->paintingDisabled() || !frameRect().intersects(damageRect)) 176 return; 177 178 if (!theme()->paint(this, context, damageRect)) 179 Widget::paint(context, damageRect); 180} 181 182void Scrollbar::autoscrollTimerFired(Timer<Scrollbar>*) 183{ 184 autoscrollPressedPart(theme()->autoscrollTimerDelay()); 185} 186 187static bool thumbUnderMouse(Scrollbar* scrollbar) 188{ 189 int thumbPos = scrollbar->theme()->trackPosition(scrollbar) + scrollbar->theme()->thumbPosition(scrollbar); 190 int thumbLength = scrollbar->theme()->thumbLength(scrollbar); 191 return scrollbar->pressedPos() >= thumbPos && scrollbar->pressedPos() < thumbPos + thumbLength; 192} 193 194void Scrollbar::autoscrollPressedPart(double delay) 195{ 196 // Don't do anything for the thumb or if nothing was pressed. 197 if (m_pressedPart == ThumbPart || m_pressedPart == NoPart) 198 return; 199 200 // Handle the track. 201 if ((m_pressedPart == BackTrackPart || m_pressedPart == ForwardTrackPart) && thumbUnderMouse(this)) { 202 theme()->invalidatePart(this, m_pressedPart); 203 setHoveredPart(ThumbPart); 204 return; 205 } 206 207 // Handle the arrows and track. 208 if (scroll(pressedPartScrollDirection(), pressedPartScrollGranularity())) 209 startTimerIfNeeded(delay); 210} 211 212void Scrollbar::startTimerIfNeeded(double delay) 213{ 214 // Don't do anything for the thumb. 215 if (m_pressedPart == ThumbPart) 216 return; 217 218 // Handle the track. We halt track scrolling once the thumb is level 219 // with us. 220 if ((m_pressedPart == BackTrackPart || m_pressedPart == ForwardTrackPart) && thumbUnderMouse(this)) { 221 theme()->invalidatePart(this, m_pressedPart); 222 setHoveredPart(ThumbPart); 223 return; 224 } 225 226 // We can't scroll if we've hit the beginning or end. 227 ScrollDirection dir = pressedPartScrollDirection(); 228 if (dir == ScrollUp || dir == ScrollLeft) { 229 if (m_currentPos == 0) 230 return; 231 } else { 232 if (m_currentPos == maximum()) 233 return; 234 } 235 236 m_scrollTimer.startOneShot(delay); 237} 238 239void Scrollbar::stopTimerIfNeeded() 240{ 241 if (m_scrollTimer.isActive()) 242 m_scrollTimer.stop(); 243} 244 245ScrollDirection Scrollbar::pressedPartScrollDirection() 246{ 247 if (m_orientation == HorizontalScrollbar) { 248 if (m_pressedPart == BackButtonStartPart || m_pressedPart == BackButtonEndPart || m_pressedPart == BackTrackPart) 249 return ScrollLeft; 250 return ScrollRight; 251 } else { 252 if (m_pressedPart == BackButtonStartPart || m_pressedPart == BackButtonEndPart || m_pressedPart == BackTrackPart) 253 return ScrollUp; 254 return ScrollDown; 255 } 256} 257 258ScrollGranularity Scrollbar::pressedPartScrollGranularity() 259{ 260 if (m_pressedPart == BackButtonStartPart || m_pressedPart == BackButtonEndPart || m_pressedPart == ForwardButtonStartPart || m_pressedPart == ForwardButtonEndPart) 261 return ScrollByLine; 262 return ScrollByPage; 263} 264 265void Scrollbar::moveThumb(int pos) 266{ 267 // Drag the thumb. 268 int thumbPos = theme()->thumbPosition(this); 269 int thumbLen = theme()->thumbLength(this); 270 int trackLen = theme()->trackLength(this); 271 int maxPos = trackLen - thumbLen; 272 int delta = pos - pressedPos(); 273 if (delta > 0) 274 delta = min(maxPos - thumbPos, delta); 275 else if (delta < 0) 276 delta = max(-thumbPos, delta); 277 if (delta) { 278 setValue(static_cast<int>(static_cast<float>(thumbPos + delta) * maximum() / (trackLen - thumbLen))); 279 setPressedPos(pressedPos() + theme()->thumbPosition(this) - thumbPos); 280 } 281} 282 283void Scrollbar::setHoveredPart(ScrollbarPart part) 284{ 285 if (part == m_hoveredPart) 286 return; 287 288 if ((m_hoveredPart == NoPart || part == NoPart) && theme()->invalidateOnMouseEnterExit()) 289 invalidate(); // Just invalidate the whole scrollbar, since the buttons at either end change anyway. 290 else if (m_pressedPart == NoPart) { 291 theme()->invalidatePart(this, part); 292 theme()->invalidatePart(this, m_hoveredPart); 293 } 294 m_hoveredPart = part; 295} 296 297void Scrollbar::setPressedPart(ScrollbarPart part) 298{ 299 if (m_pressedPart != NoPart) 300 theme()->invalidatePart(this, m_pressedPart); 301 m_pressedPart = part; 302 if (m_pressedPart != NoPart) 303 theme()->invalidatePart(this, m_pressedPart); 304} 305 306bool Scrollbar::mouseMoved(const PlatformMouseEvent& evt) 307{ 308 if (m_pressedPart == ThumbPart) { 309 moveThumb(m_orientation == HorizontalScrollbar ? 310 convertFromContainingWindow(evt.pos()).x() : 311 convertFromContainingWindow(evt.pos()).y()); 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 340bool Scrollbar::mouseExited() 341{ 342 setHoveredPart(NoPart); 343 return true; 344} 345 346bool 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 358bool 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 ((pressedPart() == BackTrackPart || pressedPart() == ForwardTrackPart) && theme()->shouldCenterOnThumb(this, evt)) { 368 setHoveredPart(ThumbPart); 369 setPressedPart(ThumbPart); 370 int thumbLen = theme()->thumbLength(this); 371 int desiredPos = pressedPos; 372 // Set the pressed position to the middle of the thumb so that when we do the move, the delta 373 // will be from the current pixel position of the thumb to the new desired position for the thumb. 374 m_pressedPos = theme()->trackPosition(this) + theme()->thumbPosition(this) + thumbLen / 2; 375 moveThumb(desiredPos); 376 return true; 377 } 378 379 m_pressedPos = pressedPos; 380 381 autoscrollPressedPart(theme()->initialAutoscrollTimerDelay()); 382 return true; 383} 384 385void Scrollbar::setFrameRect(const IntRect& rect) 386{ 387 // Get our window resizer rect and see if we overlap. Adjust to avoid the overlap 388 // if necessary. 389 IntRect adjustedRect(rect); 390 if (parent()) { 391 bool overlapsResizer = false; 392 ScrollView* view = parent(); 393 IntRect resizerRect = view->convertFromContainingWindow(view->windowResizerRect()); 394 if (rect.intersects(resizerRect)) { 395 if (orientation() == HorizontalScrollbar) { 396 int overlap = rect.right() - resizerRect.x(); 397 if (overlap > 0 && resizerRect.right() >= rect.right()) { 398 adjustedRect.setWidth(rect.width() - overlap); 399 overlapsResizer = true; 400 } 401 } else { 402 int overlap = rect.bottom() - resizerRect.y(); 403 if (overlap > 0 && resizerRect.bottom() >= rect.bottom()) { 404 adjustedRect.setHeight(rect.height() - overlap); 405 overlapsResizer = true; 406 } 407 } 408 } 409 410 if (overlapsResizer != m_overlapsResizer) { 411 m_overlapsResizer = overlapsResizer; 412 view->adjustScrollbarsAvoidingResizerCount(m_overlapsResizer ? 1 : -1); 413 } 414 } 415 416 Widget::setFrameRect(adjustedRect); 417} 418 419void Scrollbar::setParent(ScrollView* parentView) 420{ 421 if (!parentView && m_overlapsResizer && parent()) 422 parent()->adjustScrollbarsAvoidingResizerCount(-1); 423 Widget::setParent(parentView); 424} 425 426void Scrollbar::setEnabled(bool e) 427{ 428 if (m_enabled == e) 429 return; 430 m_enabled = e; 431 invalidate(); 432} 433 434bool Scrollbar::isWindowActive() const 435{ 436 return m_client && m_client->isActive(); 437} 438 439void Scrollbar::invalidateRect(const IntRect& rect) 440{ 441 if (suppressInvalidation()) 442 return; 443 if (m_client) 444 m_client->invalidateScrollbarRect(this, rect); 445} 446 447PlatformMouseEvent Scrollbar::transformEvent(const PlatformMouseEvent& event) 448{ 449 return event; 450} 451 452} 453