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