bb1ce579420e7632dca2396589b696bc38eb1718
[WebKit-https.git] / Source / WebCore / platform / Scrollbar.cpp
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 "GraphicsContext.h"
30 #include "PlatformMouseEvent.h"
31 #include "ScrollAnimator.h"
32 #include "ScrollableArea.h"
33 #include "ScrollbarTheme.h"
34 #include <algorithm>
35
36 #if ENABLE(GESTURE_EVENTS)
37 #include "PlatformGestureEvent.h"
38 #endif
39
40 // FIXME: The following #includes are a layering violation and should be removed.
41 #include "AXObjectCache.h"
42 #include "AccessibilityScrollbar.h"
43 #include "Document.h"
44 #include "EventHandler.h"
45 #include "Frame.h"
46 #include "FrameView.h"
47
48 using namespace std;
49
50 #if (PLATFORM(CHROMIUM) && (OS(UNIX) && !OS(DARWIN))) || PLATFORM(GTK)
51 // The position of the scrollbar thumb affects the appearance of the steppers, so
52 // when the thumb moves, we have to invalidate them for painting.
53 #define THUMB_POSITION_AFFECTS_BUTTONS
54 #endif
55
56 namespace WebCore {
57
58 #if !PLATFORM(EFL)
59 PassRefPtr<Scrollbar> Scrollbar::createNativeScrollbar(ScrollableArea* scrollableArea, ScrollbarOrientation orientation, ScrollbarControlSize size)
60 {
61     return adoptRef(new Scrollbar(scrollableArea, orientation, size));
62 }
63 #endif
64
65 int Scrollbar::maxOverlapBetweenPages()
66 {
67     static int maxOverlapBetweenPages = ScrollbarTheme::theme()->maxOverlapBetweenPages();
68     return maxOverlapBetweenPages;
69 }
70
71 Scrollbar::Scrollbar(ScrollableArea* scrollableArea, ScrollbarOrientation orientation, ScrollbarControlSize controlSize,
72                      ScrollbarTheme* theme)
73     : m_scrollableArea(scrollableArea)
74     , m_orientation(orientation)
75     , m_controlSize(controlSize)
76     , m_theme(theme)
77     , m_visibleSize(0)
78     , m_totalSize(0)
79     , m_currentPos(0)
80     , m_dragOrigin(0)
81     , m_lineStep(0)
82     , m_pageStep(0)
83     , m_pixelStep(1)
84     , m_hoveredPart(NoPart)
85     , m_pressedPart(NoPart)
86     , m_pressedPos(0)
87     , m_scrollPos(0)
88     , m_draggingDocument(false)
89     , m_documentDragPos(0)
90     , m_enabled(true)
91     , m_scrollTimer(this, &Scrollbar::autoscrollTimerFired)
92     , m_overlapsResizer(false)
93     , m_suppressInvalidation(false)
94     , m_isAlphaLocked(false)
95 {
96     if (!m_theme)
97         m_theme = ScrollbarTheme::theme();
98
99     m_theme->registerScrollbar(this);
100
101     // FIXME: This is ugly and would not be necessary if we fix cross-platform code to actually query for
102     // scrollbar thickness and use it when sizing scrollbars (rather than leaving one dimension of the scrollbar
103     // alone when sizing).
104     int thickness = m_theme->scrollbarThickness(controlSize);
105     Widget::setFrameRect(IntRect(0, 0, thickness, thickness));
106
107     if (m_scrollableArea)
108         m_currentPos = static_cast<float>(m_scrollableArea->scrollPosition(this));
109 }
110
111 Scrollbar::~Scrollbar()
112 {
113     if (AXObjectCache* cache = existingAXObjectCache())
114         cache->remove(this);
115     
116     stopTimerIfNeeded();
117     
118     m_theme->unregisterScrollbar(this);
119 }
120
121 ScrollbarOverlayStyle Scrollbar::scrollbarOverlayStyle() const
122 {
123     return m_scrollableArea ? m_scrollableArea->scrollbarOverlayStyle() : ScrollbarOverlayStyleDefault;
124 }
125
126 void Scrollbar::getTickmarks(Vector<IntRect>& tickmarks) const
127 {
128     if (m_scrollableArea)
129         m_scrollableArea->getTickmarks(tickmarks);
130 }
131
132 bool Scrollbar::isScrollableAreaActive() const
133 {
134     return m_scrollableArea && m_scrollableArea->isActive();
135 }
136
137 bool Scrollbar::isScrollViewScrollbar() const
138 {
139     return parent() && parent()->isFrameView() && toFrameView(parent())->isScrollViewScrollbar(this);
140 }
141
142 void Scrollbar::offsetDidChange()
143 {
144     ASSERT(m_scrollableArea);
145
146     float position = static_cast<float>(m_scrollableArea->scrollPosition(this));
147     if (position == m_currentPos)
148         return;
149
150     int oldThumbPosition = theme()->thumbPosition(this);
151     m_currentPos = position;
152     updateThumbPosition();
153     if (m_pressedPart == ThumbPart)
154         setPressedPos(m_pressedPos + theme()->thumbPosition(this) - oldThumbPosition);    
155 }
156
157 void Scrollbar::setProportion(int visibleSize, int totalSize)
158 {
159     if (visibleSize == m_visibleSize && totalSize == m_totalSize)
160         return;
161
162     m_visibleSize = visibleSize;
163     m_totalSize = totalSize;
164
165     updateThumbProportion();
166 }
167
168 void Scrollbar::setSteps(int lineStep, int pageStep, int pixelsPerStep)
169 {
170     m_lineStep = lineStep;
171     m_pageStep = pageStep;
172     m_pixelStep = 1.0f / pixelsPerStep;
173 }
174
175 void Scrollbar::updateThumb()
176 {
177 #ifdef THUMB_POSITION_AFFECTS_BUTTONS
178     invalidate();
179 #else
180     theme()->invalidateParts(this, ForwardTrackPart | BackTrackPart | ThumbPart);
181 #endif
182 }
183
184 void Scrollbar::updateThumbPosition()
185 {
186     updateThumb();
187 }
188
189 void Scrollbar::updateThumbProportion()
190 {
191     updateThumb();
192 }
193
194 void Scrollbar::paint(GraphicsContext* context, const IntRect& damageRect)
195 {
196     if (context->updatingControlTints() && theme()->supportsControlTints()) {
197         invalidate();
198         return;
199     }
200
201     if (context->paintingDisabled() || !frameRect().intersects(damageRect))
202         return;
203
204     if (!theme()->paint(this, context, damageRect))
205         Widget::paint(context, damageRect);
206 }
207
208 void Scrollbar::autoscrollTimerFired(Timer<Scrollbar>*)
209 {
210     autoscrollPressedPart(theme()->autoscrollTimerDelay());
211 }
212
213 static bool thumbUnderMouse(Scrollbar* scrollbar)
214 {
215     int thumbPos = scrollbar->theme()->trackPosition(scrollbar) + scrollbar->theme()->thumbPosition(scrollbar);
216     int thumbLength = scrollbar->theme()->thumbLength(scrollbar);
217     return scrollbar->pressedPos() >= thumbPos && scrollbar->pressedPos() < thumbPos + thumbLength;
218 }
219
220 void Scrollbar::autoscrollPressedPart(double delay)
221 {
222     // Don't do anything for the thumb or if nothing was pressed.
223     if (m_pressedPart == ThumbPart || m_pressedPart == NoPart)
224         return;
225
226     // Handle the track.
227     if ((m_pressedPart == BackTrackPart || m_pressedPart == ForwardTrackPart) && thumbUnderMouse(this)) {
228         theme()->invalidatePart(this, m_pressedPart);
229         setHoveredPart(ThumbPart);
230         return;
231     }
232
233     // Handle the arrows and track.
234     if (m_scrollableArea && m_scrollableArea->scroll(pressedPartScrollDirection(), pressedPartScrollGranularity()))
235         startTimerIfNeeded(delay);
236 }
237
238 void Scrollbar::startTimerIfNeeded(double delay)
239 {
240     // Don't do anything for the thumb.
241     if (m_pressedPart == ThumbPart)
242         return;
243
244     // Handle the track.  We halt track scrolling once the thumb is level
245     // with us.
246     if ((m_pressedPart == BackTrackPart || m_pressedPart == ForwardTrackPart) && thumbUnderMouse(this)) {
247         theme()->invalidatePart(this, m_pressedPart);
248         setHoveredPart(ThumbPart);
249         return;
250     }
251
252     // We can't scroll if we've hit the beginning or end.
253     ScrollDirection dir = pressedPartScrollDirection();
254     if (dir == ScrollUp || dir == ScrollLeft) {
255         if (m_currentPos == 0)
256             return;
257     } else {
258         if (m_currentPos == maximum())
259             return;
260     }
261
262     m_scrollTimer.startOneShot(delay);
263 }
264
265 void Scrollbar::stopTimerIfNeeded()
266 {
267     if (m_scrollTimer.isActive())
268         m_scrollTimer.stop();
269 }
270
271 ScrollDirection Scrollbar::pressedPartScrollDirection()
272 {
273     if (m_orientation == HorizontalScrollbar) {
274         if (m_pressedPart == BackButtonStartPart || m_pressedPart == BackButtonEndPart || m_pressedPart == BackTrackPart)
275             return ScrollLeft;
276         return ScrollRight;
277     } else {
278         if (m_pressedPart == BackButtonStartPart || m_pressedPart == BackButtonEndPart || m_pressedPart == BackTrackPart)
279             return ScrollUp;
280         return ScrollDown;
281     }
282 }
283
284 ScrollGranularity Scrollbar::pressedPartScrollGranularity()
285 {
286     if (m_pressedPart == BackButtonStartPart || m_pressedPart == BackButtonEndPart ||  m_pressedPart == ForwardButtonStartPart || m_pressedPart == ForwardButtonEndPart)
287         return ScrollByLine;
288     return ScrollByPage;
289 }
290
291 void Scrollbar::moveThumb(int pos, bool draggingDocument)
292 {
293     if (!m_scrollableArea)
294         return;
295
296     int delta = pos - m_pressedPos;
297
298     if (draggingDocument) {
299         if (m_draggingDocument)
300             delta = pos - m_documentDragPos;
301         m_draggingDocument = true;
302         FloatPoint currentPosition = m_scrollableArea->scrollAnimator()->currentPosition();
303         int destinationPosition = (m_orientation == HorizontalScrollbar ? currentPosition.x() : currentPosition.y()) + delta;
304         if (delta > 0)
305             destinationPosition = min(destinationPosition + delta, maximum());
306         else if (delta < 0)
307             destinationPosition = max(destinationPosition + delta, 0);
308         m_scrollableArea->scrollToOffsetWithoutAnimation(m_orientation, destinationPosition);
309         m_documentDragPos = pos;
310         return;
311     }
312
313     if (m_draggingDocument) {
314         delta += m_pressedPos - m_documentDragPos;
315         m_draggingDocument = false;
316     }
317
318     // Drag the thumb.
319     int thumbPos = theme()->thumbPosition(this);
320     int thumbLen = theme()->thumbLength(this);
321     int trackLen = theme()->trackLength(this);
322     int maxPos = trackLen - thumbLen;
323     if (delta > 0)
324         delta = min(maxPos - thumbPos, delta);
325     else if (delta < 0)
326         delta = max(-thumbPos, delta);
327     
328     if (delta) {
329         float newPosition = static_cast<float>(thumbPos + delta) * maximum() / (trackLen - thumbLen);
330         m_scrollableArea->scrollToOffsetWithoutAnimation(m_orientation, newPosition);
331     }
332 }
333
334 void Scrollbar::setHoveredPart(ScrollbarPart part)
335 {
336     if (part == m_hoveredPart)
337         return;
338
339     if ((m_hoveredPart == NoPart || part == NoPart) && theme()->invalidateOnMouseEnterExit())
340         invalidate();  // Just invalidate the whole scrollbar, since the buttons at either end change anyway.
341     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.
342         theme()->invalidatePart(this, part);
343         theme()->invalidatePart(this, m_hoveredPart);
344     }
345     m_hoveredPart = part;
346 }
347
348 void Scrollbar::setPressedPart(ScrollbarPart part)
349 {
350     if (m_pressedPart != NoPart)
351         theme()->invalidatePart(this, m_pressedPart);
352     m_pressedPart = part;
353     if (m_pressedPart != NoPart)
354         theme()->invalidatePart(this, m_pressedPart);
355     else if (m_hoveredPart != NoPart)  // When we no longer have a pressed part, we can start drawing a hovered state on the hovered part.
356         theme()->invalidatePart(this, m_hoveredPart);
357 }
358
359 #if ENABLE(GESTURE_EVENTS)
360 bool Scrollbar::gestureEvent(const PlatformGestureEvent& evt)
361 {
362     bool handled = false;
363     switch (evt.type()) {
364     case PlatformEvent::GestureTapDown:
365         setPressedPart(theme()->hitTest(this, evt.position()));
366         m_pressedPos = (orientation() == HorizontalScrollbar ? convertFromContainingWindow(evt.position()).x() : convertFromContainingWindow(evt.position()).y());
367         return true;
368     case PlatformEvent::GestureTapDownCancel:
369     case PlatformEvent::GestureScrollBegin:
370         if (m_pressedPart == ThumbPart) {
371             m_scrollPos = m_pressedPos;
372             return true;
373         }
374         break;
375     case PlatformEvent::GestureScrollUpdate:
376     case PlatformEvent::GestureScrollUpdateWithoutPropagation:
377         if (m_pressedPart == ThumbPart) {
378             m_scrollPos += HorizontalScrollbar ? evt.deltaX() : evt.deltaY();
379             moveThumb(m_scrollPos, false);
380             return true;
381         }
382         break;
383     case PlatformEvent::GestureScrollEnd:
384         m_scrollPos = 0;
385         break;
386     case PlatformEvent::GestureTap:
387         if (m_pressedPart != ThumbPart && m_pressedPart != NoPart)
388             handled = m_scrollableArea && m_scrollableArea->scroll(pressedPartScrollDirection(), pressedPartScrollGranularity());
389         break;
390     default:
391         break;
392     }
393     setPressedPart(NoPart);
394     m_pressedPos = 0;
395     return handled;
396 }
397 #endif
398
399 bool Scrollbar::mouseMoved(const PlatformMouseEvent& evt)
400 {
401     if (m_pressedPart == ThumbPart) {
402         if (theme()->shouldSnapBackToDragOrigin(this, evt)) {
403             if (m_scrollableArea)
404                 m_scrollableArea->scrollToOffsetWithoutAnimation(m_orientation, m_dragOrigin);
405         } else {
406             moveThumb(m_orientation == HorizontalScrollbar ? 
407                       convertFromContainingWindow(evt.position()).x() :
408                       convertFromContainingWindow(evt.position()).y(), theme()->shouldDragDocumentInsteadOfThumb(this, evt));
409         }
410         return true;
411     }
412
413     if (m_pressedPart != NoPart)
414         m_pressedPos = (orientation() == HorizontalScrollbar ? convertFromContainingWindow(evt.position()).x() : convertFromContainingWindow(evt.position()).y());
415
416     ScrollbarPart part = theme()->hitTest(this, evt.position());
417     if (part != m_hoveredPart) {
418         if (m_pressedPart != NoPart) {
419             if (part == m_pressedPart) {
420                 // The mouse is moving back over the pressed part.  We
421                 // need to start up the timer action again.
422                 startTimerIfNeeded(theme()->autoscrollTimerDelay());
423                 theme()->invalidatePart(this, m_pressedPart);
424             } else if (m_hoveredPart == m_pressedPart) {
425                 // The mouse is leaving the pressed part.  Kill our timer
426                 // if needed.
427                 stopTimerIfNeeded();
428                 theme()->invalidatePart(this, m_pressedPart);
429             }
430         } 
431         
432         setHoveredPart(part);
433     } 
434
435     return true;
436 }
437
438 void Scrollbar::mouseEntered()
439 {
440     if (m_scrollableArea)
441         m_scrollableArea->mouseEnteredScrollbar(this);
442 }
443
444 bool Scrollbar::mouseExited()
445 {
446     if (m_scrollableArea)
447         m_scrollableArea->mouseExitedScrollbar(this);
448     setHoveredPart(NoPart);
449     return true;
450 }
451
452 bool Scrollbar::mouseUp(const PlatformMouseEvent& mouseEvent)
453 {
454     setPressedPart(NoPart);
455     m_pressedPos = 0;
456     m_draggingDocument = false;
457     stopTimerIfNeeded();
458
459     if (m_scrollableArea) {
460         // m_hoveredPart won't be updated until the next mouseMoved or mouseDown, so we have to hit test
461         // to really know if the mouse has exited the scrollbar on a mouseUp.
462         ScrollbarPart part = theme()->hitTest(this, mouseEvent.position());
463         if (part == NoPart)
464             m_scrollableArea->mouseExitedScrollbar(this);
465     }
466
467     if (parent() && parent()->isFrameView())
468         toFrameView(parent())->frame()->eventHandler()->setMousePressed(false);
469
470     return true;
471 }
472
473 bool Scrollbar::mouseDown(const PlatformMouseEvent& evt)
474 {
475     // Early exit for right click
476     if (evt.button() == RightButton)
477         return true; // FIXME: Handled as context menu by Qt right now.  Should just avoid even calling this method on a right click though.
478
479     setPressedPart(theme()->hitTest(this, evt.position()));
480     int pressedPos = (orientation() == HorizontalScrollbar ? convertFromContainingWindow(evt.position()).x() : convertFromContainingWindow(evt.position()).y());
481     
482     if ((m_pressedPart == BackTrackPart || m_pressedPart == ForwardTrackPart) && theme()->shouldCenterOnThumb(this, evt)) {
483         setHoveredPart(ThumbPart);
484         setPressedPart(ThumbPart);
485         m_dragOrigin = m_currentPos;
486         int thumbLen = theme()->thumbLength(this);
487         int desiredPos = pressedPos;
488         // Set the pressed position to the middle of the thumb so that when we do the move, the delta
489         // will be from the current pixel position of the thumb to the new desired position for the thumb.
490         m_pressedPos = theme()->trackPosition(this) + theme()->thumbPosition(this) + thumbLen / 2;
491         moveThumb(desiredPos);
492         return true;
493     } else if (m_pressedPart == ThumbPart)
494         m_dragOrigin = m_currentPos;
495     
496     m_pressedPos = pressedPos;
497
498     autoscrollPressedPart(theme()->initialAutoscrollTimerDelay());
499     return true;
500 }
501
502 void Scrollbar::setFrameRect(const IntRect& rect)
503 {
504     // Get our window resizer rect and see if we overlap. Adjust to avoid the overlap
505     // if necessary.
506     IntRect adjustedRect(rect);
507     bool overlapsResizer = false;
508     ScrollView* view = parent();
509     if (view && !rect.isEmpty() && !view->windowResizerRect().isEmpty()) {
510         IntRect resizerRect = view->convertFromContainingWindow(view->windowResizerRect());
511         if (rect.intersects(resizerRect)) {
512             if (orientation() == HorizontalScrollbar) {
513                 int overlap = rect.maxX() - resizerRect.x();
514                 if (overlap > 0 && resizerRect.maxX() >= rect.maxX()) {
515                     adjustedRect.setWidth(rect.width() - overlap);
516                     overlapsResizer = true;
517                 }
518             } else {
519                 int overlap = rect.maxY() - resizerRect.y();
520                 if (overlap > 0 && resizerRect.maxY() >= rect.maxY()) {
521                     adjustedRect.setHeight(rect.height() - overlap);
522                     overlapsResizer = true;
523                 }
524             }
525         }
526     }
527     if (overlapsResizer != m_overlapsResizer) {
528         m_overlapsResizer = overlapsResizer;
529         if (view)
530             view->adjustScrollbarsAvoidingResizerCount(m_overlapsResizer ? 1 : -1);
531     }
532
533     Widget::setFrameRect(adjustedRect);
534 }
535
536 void Scrollbar::setParent(ScrollView* parentView)
537 {
538     if (!parentView && m_overlapsResizer && parent())
539         parent()->adjustScrollbarsAvoidingResizerCount(-1);
540     Widget::setParent(parentView);
541 }
542
543 void Scrollbar::setEnabled(bool e)
544
545     if (m_enabled == e)
546         return;
547     m_enabled = e;
548     theme()->updateEnabledState(this);
549     invalidate();
550 }
551
552 bool Scrollbar::isOverlayScrollbar() const
553 {
554     return m_theme->usesOverlayScrollbars();
555 }
556
557 bool Scrollbar::shouldParticipateInHitTesting()
558 {
559     // Non-overlay scrollbars should always participate in hit testing.
560     if (!isOverlayScrollbar())
561         return true;
562     return m_scrollableArea->scrollAnimator()->shouldScrollbarParticipateInHitTesting(this);
563 }
564
565 bool Scrollbar::isWindowActive() const
566 {
567     return m_scrollableArea && m_scrollableArea->isActive();
568 }
569
570 AXObjectCache* Scrollbar::existingAXObjectCache() const
571 {
572     if (!parent())
573         return 0;
574     
575     return parent()->axObjectCache();
576 }
577
578 void Scrollbar::invalidateRect(const IntRect& rect)
579 {
580     if (suppressInvalidation())
581         return;
582
583     if (m_scrollableArea)
584         m_scrollableArea->invalidateScrollbar(this, rect);
585 }
586
587 IntRect Scrollbar::convertToContainingView(const IntRect& localRect) const
588 {
589     if (m_scrollableArea)
590         return m_scrollableArea->convertFromScrollbarToContainingView(this, localRect);
591
592     return Widget::convertToContainingView(localRect);
593 }
594
595 IntRect Scrollbar::convertFromContainingView(const IntRect& parentRect) const
596 {
597     if (m_scrollableArea)
598         return m_scrollableArea->convertFromContainingViewToScrollbar(this, parentRect);
599
600     return Widget::convertFromContainingView(parentRect);
601 }
602
603 IntPoint Scrollbar::convertToContainingView(const IntPoint& localPoint) const
604 {
605     if (m_scrollableArea)
606         return m_scrollableArea->convertFromScrollbarToContainingView(this, localPoint);
607
608     return Widget::convertToContainingView(localPoint);
609 }
610
611 IntPoint Scrollbar::convertFromContainingView(const IntPoint& parentPoint) const
612 {
613     if (m_scrollableArea)
614         return m_scrollableArea->convertFromContainingViewToScrollbar(this, parentPoint);
615
616     return Widget::convertFromContainingView(parentPoint);
617 }
618
619 } // namespace WebCore