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