Home | History | Annotate | Download | only in platform
      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