032d9f3f58d5484fa8a01d7ce46fb0e9ef446f52
[WebKit-https.git] / Source / WebCore / platform / mac / ScrollbarThemeMac.mm
1 /*
2  * Copyright (C) 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 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 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 "ScrollbarThemeMac.h"
28
29 #include "ImageBuffer.h"
30 #include "LocalCurrentGraphicsContext.h"
31 #include "PlatformMouseEvent.h"
32 #include "ScrollView.h"
33 #include "WebCoreSystemInterface.h"
34 #include <Carbon/Carbon.h>
35 #include <wtf/HashMap.h>
36 #include <wtf/StdLibExtras.h>
37 #include <wtf/UnusedParam.h>
38
39 #if !defined(BUILDING_ON_TIGER) && !defined(BUILDING_ON_LEOPARD) && !defined(BUILDING_ON_SNOW_LEOPARD)
40 #define USE_WK_SCROLLBAR_PAINTER
41 #endif
42
43 // FIXME: There are repainting problems due to Aqua scroll bar buttons' visual overflow.
44
45 using namespace std;
46 using namespace WebCore;
47
48 namespace WebCore {
49
50 #if defined(USE_WK_SCROLLBAR_PAINTER)
51 typedef HashMap<Scrollbar*, RetainPtr<WKScrollbarPainterRef> > ScrollbarPainterMap;
52 #else
53 typedef HashSet<Scrollbar*> ScrollbarPainterMap;
54 #endif
55
56 static ScrollbarPainterMap* scrollbarMap()
57 {
58     static ScrollbarPainterMap* map = new ScrollbarPainterMap;
59     return map;
60 }
61     
62 }
63
64 @interface ScrollbarPrefsObserver : NSObject
65 {
66
67 }
68
69 + (void)registerAsObserver;
70 + (void)appearancePrefsChanged:(NSNotification*)theNotification;
71 + (void)behaviorPrefsChanged:(NSNotification*)theNotification;
72
73 @end
74
75 @implementation ScrollbarPrefsObserver
76
77 + (void)appearancePrefsChanged:(NSNotification*)unusedNotification
78 {
79     UNUSED_PARAM(unusedNotification);
80
81     static_cast<ScrollbarThemeMac*>(ScrollbarTheme::nativeTheme())->preferencesChanged();
82     if (scrollbarMap()->isEmpty())
83         return;
84     ScrollbarPainterMap::iterator end = scrollbarMap()->end();
85     for (ScrollbarPainterMap::iterator it = scrollbarMap()->begin(); it != end; ++it) {
86 #if defined(USE_WK_SCROLLBAR_PAINTER)
87         it->first->styleChanged();
88         it->first->invalidate();
89 #else
90         (*it)->styleChanged();
91         (*it)->invalidate();
92 #endif
93     }
94 }
95
96 + (void)behaviorPrefsChanged:(NSNotification*)unusedNotification
97 {
98     UNUSED_PARAM(unusedNotification);
99
100     static_cast<ScrollbarThemeMac*>(ScrollbarTheme::nativeTheme())->preferencesChanged();
101 }
102
103 + (void)registerAsObserver
104 {
105     [[NSDistributedNotificationCenter defaultCenter] addObserver:self selector:@selector(appearancePrefsChanged:) name:@"AppleAquaScrollBarVariantChanged" object:nil suspensionBehavior:NSNotificationSuspensionBehaviorDeliverImmediately];
106     [[NSDistributedNotificationCenter defaultCenter] addObserver:self selector:@selector(behaviorPrefsChanged:) name:@"AppleNoRedisplayAppearancePreferenceChanged" object:nil suspensionBehavior:NSNotificationSuspensionBehaviorCoalesce];
107 }
108
109 @end
110
111 namespace WebCore {
112
113 ScrollbarTheme* ScrollbarTheme::nativeTheme()
114 {
115     DEFINE_STATIC_LOCAL(ScrollbarThemeMac, theme, ());
116     return &theme;
117 }
118
119 // FIXME: Get these numbers from CoreUI.
120 static int cScrollbarThickness[] = { 15, 11 };
121 static int cRealButtonLength[] = { 28, 21 };
122 static int cButtonInset[] = { 14, 11 };
123 static int cButtonHitInset[] = { 3, 2 };
124 // cRealButtonLength - cButtonInset
125 static int cButtonLength[] = { 14, 10 };
126 static int cThumbMinLength[] = { 26, 20 };
127
128 static int cOuterButtonLength[] = { 16, 14 }; // The outer button in a double button pair is a bit bigger.
129 static int cOuterButtonOverlap = 2;
130
131 static float gInitialButtonDelay = 0.5f;
132 static float gAutoscrollButtonDelay = 0.05f;
133 static bool gJumpOnTrackClick = false;
134 static ScrollbarButtonsPlacement gButtonPlacement = ScrollbarButtonsDoubleEnd;
135
136 static void updateArrowPlacement()
137 {
138 #if defined(USE_WK_SCROLLBAR_PAINTER)
139     gButtonPlacement = ScrollbarButtonsNone;
140 #else
141     NSString *buttonPlacement = [[NSUserDefaults standardUserDefaults] objectForKey:@"AppleScrollBarVariant"];
142     if ([buttonPlacement isEqualToString:@"Single"])
143         gButtonPlacement = ScrollbarButtonsSingle;
144     else if ([buttonPlacement isEqualToString:@"DoubleMin"])
145         gButtonPlacement = ScrollbarButtonsDoubleStart;
146     else if ([buttonPlacement isEqualToString:@"DoubleBoth"])
147         gButtonPlacement = ScrollbarButtonsDoubleBoth;
148     else
149         gButtonPlacement = ScrollbarButtonsDoubleEnd; // The default is ScrollbarButtonsDoubleEnd.
150 #endif
151 }
152
153 void ScrollbarThemeMac::registerScrollbar(Scrollbar* scrollbar)
154 {
155 #if defined(USE_WK_SCROLLBAR_PAINTER)
156     WKScrollbarPainterRef scrollbarPainter = wkMakeScrollbarPainter(scrollbar->controlSize(),
157         scrollbar->orientation() == HorizontalScrollbar);
158     scrollbarMap()->add(scrollbar, scrollbarPainter);
159 #else
160     scrollbarMap()->add(scrollbar);
161 #endif
162 }
163
164 void ScrollbarThemeMac::unregisterScrollbar(Scrollbar* scrollbar)
165 {
166     scrollbarMap()->remove(scrollbar);
167 }
168
169 ScrollbarThemeMac::ScrollbarThemeMac()
170 {
171     static bool initialized;
172     if (!initialized) {
173         initialized = true;
174         [ScrollbarPrefsObserver registerAsObserver];
175         preferencesChanged();
176     }
177 }
178
179 ScrollbarThemeMac::~ScrollbarThemeMac()
180 {
181 }
182
183 void ScrollbarThemeMac::preferencesChanged()
184 {
185     NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
186     [defaults synchronize];
187     updateArrowPlacement();
188     gInitialButtonDelay = [defaults floatForKey:@"NSScrollerButtonDelay"];
189     gAutoscrollButtonDelay = [defaults floatForKey:@"NSScrollerButtonPeriod"];
190     gJumpOnTrackClick = [defaults boolForKey:@"AppleScrollerPagingBehavior"];
191 }
192
193 int ScrollbarThemeMac::scrollbarThickness(ScrollbarControlSize controlSize)
194 {
195     return cScrollbarThickness[controlSize];
196 }
197
198 bool ScrollbarThemeMac::usesOverlayScrollbars() const
199 {
200     // FIXME: This should be enabled when <rdar://problem/8492788> is resolved.
201     return false;
202 }
203
204 double ScrollbarThemeMac::initialAutoscrollTimerDelay()
205 {
206     return gInitialButtonDelay;
207 }
208
209 double ScrollbarThemeMac::autoscrollTimerDelay()
210 {
211     return gAutoscrollButtonDelay;
212 }
213     
214 ScrollbarButtonsPlacement ScrollbarThemeMac::buttonsPlacement() const
215 {
216     return gButtonPlacement;
217 }
218
219 bool ScrollbarThemeMac::hasButtons(Scrollbar* scrollbar)
220 {
221     return scrollbar->enabled() && (scrollbar->orientation() == HorizontalScrollbar ? 
222              scrollbar->width() : 
223              scrollbar->height()) >= 2 * (cRealButtonLength[scrollbar->controlSize()] - cButtonHitInset[scrollbar->controlSize()]);
224 }
225
226 bool ScrollbarThemeMac::hasThumb(Scrollbar* scrollbar)
227 {
228     return scrollbar->enabled() && (scrollbar->orientation() == HorizontalScrollbar ? 
229              scrollbar->width() : 
230              scrollbar->height()) >= 2 * cButtonInset[scrollbar->controlSize()] + cThumbMinLength[scrollbar->controlSize()] + 1;
231 }
232
233 static IntRect buttonRepaintRect(const IntRect& buttonRect, ScrollbarOrientation orientation, ScrollbarControlSize controlSize, bool start)
234 {
235     IntRect paintRect(buttonRect);
236     if (orientation == HorizontalScrollbar) {
237         paintRect.setWidth(cRealButtonLength[controlSize]);
238         if (!start)
239             paintRect.setX(buttonRect.x() - (cRealButtonLength[controlSize] - buttonRect.width()));
240     } else {
241         paintRect.setHeight(cRealButtonLength[controlSize]);
242         if (!start)
243             paintRect.setY(buttonRect.y() - (cRealButtonLength[controlSize] - buttonRect.height()));
244     }
245
246     return paintRect;
247 }
248
249 IntRect ScrollbarThemeMac::backButtonRect(Scrollbar* scrollbar, ScrollbarPart part, bool painting)
250 {
251     IntRect result;
252     
253     if (part == BackButtonStartPart && (buttonsPlacement() == ScrollbarButtonsNone || buttonsPlacement() == ScrollbarButtonsDoubleEnd))
254         return result;
255     
256     if (part == BackButtonEndPart && (buttonsPlacement() == ScrollbarButtonsNone || buttonsPlacement() == ScrollbarButtonsDoubleStart || buttonsPlacement() == ScrollbarButtonsSingle))
257         return result;
258         
259     int thickness = scrollbarThickness(scrollbar->controlSize());
260     bool outerButton = part == BackButtonStartPart && (buttonsPlacement() == ScrollbarButtonsDoubleStart || buttonsPlacement() == ScrollbarButtonsDoubleBoth);
261     if (outerButton) {
262         if (scrollbar->orientation() == HorizontalScrollbar)
263             result = IntRect(scrollbar->x(), scrollbar->y(), cOuterButtonLength[scrollbar->controlSize()] + painting ? cOuterButtonOverlap : 0, thickness);
264         else
265             result = IntRect(scrollbar->x(), scrollbar->y(), thickness, cOuterButtonLength[scrollbar->controlSize()] + painting ? cOuterButtonOverlap : 0);
266         return result;
267     }
268     
269     // Our repaint rect is slightly larger, since we are a button that is adjacent to the track.
270     if (scrollbar->orientation() == HorizontalScrollbar) {
271         int start = part == BackButtonStartPart ? scrollbar->x() : scrollbar->x() + scrollbar->width() - cOuterButtonLength[scrollbar->controlSize()] - cButtonLength[scrollbar->controlSize()];
272         result = IntRect(start, scrollbar->y(), cButtonLength[scrollbar->controlSize()], thickness);
273     } else {
274         int start = part == BackButtonStartPart ? scrollbar->y() : scrollbar->y() + scrollbar->height() - cOuterButtonLength[scrollbar->controlSize()] - cButtonLength[scrollbar->controlSize()];
275         result = IntRect(scrollbar->x(), start, thickness, cButtonLength[scrollbar->controlSize()]);
276     }
277     
278     if (painting)
279         return buttonRepaintRect(result, scrollbar->orientation(), scrollbar->controlSize(), part == BackButtonStartPart);
280     return result;
281 }
282
283 IntRect ScrollbarThemeMac::forwardButtonRect(Scrollbar* scrollbar, ScrollbarPart part, bool painting)
284 {
285     IntRect result;
286     
287     if (part == ForwardButtonEndPart && (buttonsPlacement() == ScrollbarButtonsNone || buttonsPlacement() == ScrollbarButtonsDoubleStart))
288         return result;
289     
290     if (part == ForwardButtonStartPart && (buttonsPlacement() == ScrollbarButtonsNone || buttonsPlacement() == ScrollbarButtonsDoubleEnd || buttonsPlacement() == ScrollbarButtonsSingle))
291         return result;
292         
293     int thickness = scrollbarThickness(scrollbar->controlSize());
294     int outerButtonLength = cOuterButtonLength[scrollbar->controlSize()];
295     int buttonLength = cButtonLength[scrollbar->controlSize()];
296     
297     bool outerButton = part == ForwardButtonEndPart && (buttonsPlacement() == ScrollbarButtonsDoubleEnd || buttonsPlacement() == ScrollbarButtonsDoubleBoth);
298     if (outerButton) {
299         if (scrollbar->orientation() == HorizontalScrollbar) {
300             result = IntRect(scrollbar->x() + scrollbar->width() - outerButtonLength, scrollbar->y(), outerButtonLength, thickness);
301             if (painting)
302                 result.inflateX(cOuterButtonOverlap);
303         } else {
304             result = IntRect(scrollbar->x(), scrollbar->y() + scrollbar->height() - outerButtonLength, thickness, outerButtonLength);
305             if (painting)
306                 result.inflateY(cOuterButtonOverlap);
307         }
308         return result;
309     }
310     
311     if (scrollbar->orientation() == HorizontalScrollbar) {
312         int start = part == ForwardButtonEndPart ? scrollbar->x() + scrollbar->width() - buttonLength : scrollbar->x() + outerButtonLength;
313         result = IntRect(start, scrollbar->y(), buttonLength, thickness);
314     } else {
315         int start = part == ForwardButtonEndPart ? scrollbar->y() + scrollbar->height() - buttonLength : scrollbar->y() + outerButtonLength;
316         result = IntRect(scrollbar->x(), start, thickness, buttonLength);
317     }
318     if (painting)
319         return buttonRepaintRect(result, scrollbar->orientation(), scrollbar->controlSize(), part == ForwardButtonStartPart);
320     return result;
321 }
322
323 IntRect ScrollbarThemeMac::trackRect(Scrollbar* scrollbar, bool painting)
324 {
325     if (painting || !hasButtons(scrollbar))
326         return scrollbar->frameRect();
327     
328     IntRect result;
329     int thickness = scrollbarThickness(scrollbar->controlSize());
330     int startWidth = 0;
331     int endWidth = 0;
332     int outerButtonLength = cOuterButtonLength[scrollbar->controlSize()];
333     int buttonLength = cButtonLength[scrollbar->controlSize()];
334     int doubleButtonLength = outerButtonLength + buttonLength;
335     switch (buttonsPlacement()) {
336         case ScrollbarButtonsSingle:
337             startWidth = buttonLength;
338             endWidth = buttonLength;
339             break;
340         case ScrollbarButtonsDoubleStart:
341             startWidth = doubleButtonLength;
342             break;
343         case ScrollbarButtonsDoubleEnd:
344             endWidth = doubleButtonLength;
345             break;
346         case ScrollbarButtonsDoubleBoth:
347             startWidth = doubleButtonLength;
348             endWidth = doubleButtonLength;
349             break;
350         default:
351             break;
352     }
353     
354     int totalWidth = startWidth + endWidth;
355     if (scrollbar->orientation() == HorizontalScrollbar)
356         return IntRect(scrollbar->x() + startWidth, scrollbar->y(), scrollbar->width() - totalWidth, thickness);
357     return IntRect(scrollbar->x(), scrollbar->y() + startWidth, thickness, scrollbar->height() - totalWidth);
358 }
359
360 int ScrollbarThemeMac::minimumThumbLength(Scrollbar* scrollbar)
361 {
362     return cThumbMinLength[scrollbar->controlSize()];
363 }
364
365 bool ScrollbarThemeMac::shouldCenterOnThumb(Scrollbar*, const PlatformMouseEvent& evt)
366 {
367     if (evt.button() != LeftButton)
368         return false;
369     if (gJumpOnTrackClick)
370         return !evt.altKey();
371     return evt.altKey();
372 }
373
374 static int scrollbarPartToHIPressedState(ScrollbarPart part)
375 {
376     switch (part) {
377         case BackButtonStartPart:
378             return kThemeTopOutsideArrowPressed;
379         case BackButtonEndPart:
380             return kThemeTopOutsideArrowPressed; // This does not make much sense.  For some reason the outside constant is required.
381         case ForwardButtonStartPart:
382             return kThemeTopInsideArrowPressed;
383         case ForwardButtonEndPart:
384             return kThemeBottomOutsideArrowPressed;
385         case ThumbPart:
386             return kThemeThumbPressed;
387         default:
388             return 0;
389     }
390 }
391
392 bool ScrollbarThemeMac::paint(Scrollbar* scrollbar, GraphicsContext* context, const IntRect& damageRect)
393 {
394 #if defined(USE_WK_SCROLLBAR_PAINTER)
395     context->save();
396     context->clip(damageRect);
397     context->translate(scrollbar->frameRect().x(), scrollbar->frameRect().y());
398     LocalCurrentGraphicsContext localContext(context);
399     wkScrollbarPainterPaint(scrollbarMap()->get(scrollbar).get(),
400                             scrollbar->enabled(),
401                             scrollbar->currentPos() / scrollbar->maximum(),
402                             static_cast<CGFloat>(scrollbar->visibleSize()) / scrollbar->totalSize(),
403                             scrollbar->frameRect());
404     context->restore();
405     return true;
406 #endif
407
408     HIThemeTrackDrawInfo trackInfo;
409     trackInfo.version = 0;
410     trackInfo.kind = scrollbar->controlSize() == RegularScrollbar ? kThemeMediumScrollBar : kThemeSmallScrollBar;
411     trackInfo.bounds = scrollbar->frameRect();
412     trackInfo.min = 0;
413     trackInfo.max = scrollbar->maximum();
414     trackInfo.value = scrollbar->currentPos();
415     trackInfo.trackInfo.scrollbar.viewsize = scrollbar->visibleSize();
416     trackInfo.attributes = 0;
417     if (scrollbar->orientation() == HorizontalScrollbar)
418         trackInfo.attributes |= kThemeTrackHorizontal;
419
420     if (!scrollbar->enabled())
421         trackInfo.enableState = kThemeTrackDisabled;
422     else
423         trackInfo.enableState = scrollbar->scrollableArea()->isActive() ? kThemeTrackActive : kThemeTrackInactive;
424
425     if (hasThumb(scrollbar))
426         trackInfo.attributes |= kThemeTrackShowThumb;
427     else if (!hasButtons(scrollbar))
428         trackInfo.enableState = kThemeTrackNothingToScroll;
429     trackInfo.trackInfo.scrollbar.pressState = scrollbarPartToHIPressedState(scrollbar->pressedPart());
430     
431     // The Aqua scrollbar is buggy when rotated and scaled.  We will just draw into a bitmap if we detect a scale or rotation.
432     const AffineTransform& currentCTM = context->getCTM();
433     bool canDrawDirectly = currentCTM.isIdentityOrTranslationOrFlipped();
434     if (canDrawDirectly)
435         HIThemeDrawTrack(&trackInfo, 0, context->platformContext(), kHIThemeOrientationNormal);
436     else {
437         trackInfo.bounds = IntRect(IntPoint(), scrollbar->frameRect().size());
438         
439         IntRect bufferRect(scrollbar->frameRect());
440         bufferRect.intersect(damageRect);
441         bufferRect.move(-scrollbar->frameRect().x(), -scrollbar->frameRect().y());
442         
443         OwnPtr<ImageBuffer> imageBuffer = ImageBuffer::create(bufferRect.size());
444         if (!imageBuffer)
445             return true;
446         
447         HIThemeDrawTrack(&trackInfo, 0, imageBuffer->context()->platformContext(), kHIThemeOrientationNormal);
448         context->drawImageBuffer(imageBuffer.get(), ColorSpaceDeviceRGB, scrollbar->frameRect().location());
449     }
450
451     return true;
452 }
453
454 }
455