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