ce3be1a6e1585c51cd250dcdfb76245835b9dba4
[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 double ScrollbarThemeMac::initialAutoscrollTimerDelay()
169 {
170     return gInitialButtonDelay;
171 }
172
173 double ScrollbarThemeMac::autoscrollTimerDelay()
174 {
175     return gAutoscrollButtonDelay;
176 }
177     
178 ScrollbarButtonsPlacement ScrollbarThemeMac::buttonsPlacement() const
179 {
180     return gButtonPlacement;
181 }
182
183 bool ScrollbarThemeMac::hasButtons(Scrollbar* scrollbar)
184 {
185     return scrollbar->enabled() && (scrollbar->orientation() == HorizontalScrollbar ? 
186              scrollbar->width() : 
187              scrollbar->height()) >= 2 * (cRealButtonLength[scrollbar->controlSize()] - cButtonHitInset[scrollbar->controlSize()]);
188 }
189
190 bool ScrollbarThemeMac::hasThumb(Scrollbar* scrollbar)
191 {
192     return scrollbar->enabled() && (scrollbar->orientation() == HorizontalScrollbar ? 
193              scrollbar->width() : 
194              scrollbar->height()) >= 2 * cButtonInset[scrollbar->controlSize()] + cThumbMinLength[scrollbar->controlSize()] + 1;
195 }
196
197 static IntRect buttonRepaintRect(const IntRect& buttonRect, ScrollbarOrientation orientation, ScrollbarControlSize controlSize, bool start)
198 {
199     IntRect paintRect(buttonRect);
200     if (orientation == HorizontalScrollbar) {
201         paintRect.setWidth(cRealButtonLength[controlSize]);
202         if (!start)
203             paintRect.setX(buttonRect.x() - (cRealButtonLength[controlSize] - buttonRect.width()));
204     } else {
205         paintRect.setHeight(cRealButtonLength[controlSize]);
206         if (!start)
207             paintRect.setY(buttonRect.y() - (cRealButtonLength[controlSize] - buttonRect.height()));
208     }
209
210     return paintRect;
211 }
212
213 IntRect ScrollbarThemeMac::backButtonRect(Scrollbar* scrollbar, ScrollbarPart part, bool painting)
214 {
215     IntRect result;
216     
217     if (part == BackButtonStartPart && (buttonsPlacement() == ScrollbarButtonsNone || buttonsPlacement() == ScrollbarButtonsDoubleEnd))
218         return result;
219     
220     if (part == BackButtonEndPart && (buttonsPlacement() == ScrollbarButtonsNone || buttonsPlacement() == ScrollbarButtonsDoubleStart || buttonsPlacement() == ScrollbarButtonsSingle))
221         return result;
222         
223     int thickness = scrollbarThickness(scrollbar->controlSize());
224     bool outerButton = part == BackButtonStartPart && (buttonsPlacement() == ScrollbarButtonsDoubleStart || buttonsPlacement() == ScrollbarButtonsDoubleBoth);
225     if (outerButton) {
226         if (scrollbar->orientation() == HorizontalScrollbar)
227             result = IntRect(scrollbar->x(), scrollbar->y(), cOuterButtonLength[scrollbar->controlSize()] + painting ? cOuterButtonOverlap : 0, thickness);
228         else
229             result = IntRect(scrollbar->x(), scrollbar->y(), thickness, cOuterButtonLength[scrollbar->controlSize()] + painting ? cOuterButtonOverlap : 0);
230         return result;
231     }
232     
233     // Our repaint rect is slightly larger, since we are a button that is adjacent to the track.
234     if (scrollbar->orientation() == HorizontalScrollbar) {
235         int start = part == BackButtonStartPart ? scrollbar->x() : scrollbar->x() + scrollbar->width() - cOuterButtonLength[scrollbar->controlSize()] - cButtonLength[scrollbar->controlSize()];
236         result = IntRect(start, scrollbar->y(), cButtonLength[scrollbar->controlSize()], thickness);
237     } else {
238         int start = part == BackButtonStartPart ? scrollbar->y() : scrollbar->y() + scrollbar->height() - cOuterButtonLength[scrollbar->controlSize()] - cButtonLength[scrollbar->controlSize()];
239         result = IntRect(scrollbar->x(), start, thickness, cButtonLength[scrollbar->controlSize()]);
240     }
241     
242     if (painting)
243         return buttonRepaintRect(result, scrollbar->orientation(), scrollbar->controlSize(), part == BackButtonStartPart);
244     return result;
245 }
246
247 IntRect ScrollbarThemeMac::forwardButtonRect(Scrollbar* scrollbar, ScrollbarPart part, bool painting)
248 {
249     IntRect result;
250     
251     if (part == ForwardButtonEndPart && (buttonsPlacement() == ScrollbarButtonsNone || buttonsPlacement() == ScrollbarButtonsDoubleStart))
252         return result;
253     
254     if (part == ForwardButtonStartPart && (buttonsPlacement() == ScrollbarButtonsNone || buttonsPlacement() == ScrollbarButtonsDoubleEnd || buttonsPlacement() == ScrollbarButtonsSingle))
255         return result;
256         
257     int thickness = scrollbarThickness(scrollbar->controlSize());
258     int outerButtonLength = cOuterButtonLength[scrollbar->controlSize()];
259     int buttonLength = cButtonLength[scrollbar->controlSize()];
260     
261     bool outerButton = part == ForwardButtonEndPart && (buttonsPlacement() == ScrollbarButtonsDoubleEnd || buttonsPlacement() == ScrollbarButtonsDoubleBoth);
262     if (outerButton) {
263         if (scrollbar->orientation() == HorizontalScrollbar) {
264             result = IntRect(scrollbar->x() + scrollbar->width() - outerButtonLength, scrollbar->y(), outerButtonLength, thickness);
265             if (painting)
266                 result.inflateX(cOuterButtonOverlap);
267         } else {
268             result = IntRect(scrollbar->x(), scrollbar->y() + scrollbar->height() - outerButtonLength, thickness, outerButtonLength);
269             if (painting)
270                 result.inflateY(cOuterButtonOverlap);
271         }
272         return result;
273     }
274     
275     if (scrollbar->orientation() == HorizontalScrollbar) {
276         int start = part == ForwardButtonEndPart ? scrollbar->x() + scrollbar->width() - buttonLength : scrollbar->x() + outerButtonLength;
277         result = IntRect(start, scrollbar->y(), buttonLength, thickness);
278     } else {
279         int start = part == ForwardButtonEndPart ? scrollbar->y() + scrollbar->height() - buttonLength : scrollbar->y() + outerButtonLength;
280         result = IntRect(scrollbar->x(), start, thickness, buttonLength);
281     }
282     if (painting)
283         return buttonRepaintRect(result, scrollbar->orientation(), scrollbar->controlSize(), part == ForwardButtonStartPart);
284     return result;
285 }
286
287 IntRect ScrollbarThemeMac::trackRect(Scrollbar* scrollbar, bool painting)
288 {
289     if (painting || !hasButtons(scrollbar))
290         return scrollbar->frameRect();
291     
292     IntRect result;
293     int thickness = scrollbarThickness(scrollbar->controlSize());
294     int startWidth = 0;
295     int endWidth = 0;
296     int outerButtonLength = cOuterButtonLength[scrollbar->controlSize()];
297     int buttonLength = cButtonLength[scrollbar->controlSize()];
298     int doubleButtonLength = outerButtonLength + buttonLength;
299     switch (buttonsPlacement()) {
300         case ScrollbarButtonsSingle:
301             startWidth = buttonLength;
302             endWidth = buttonLength;
303             break;
304         case ScrollbarButtonsDoubleStart:
305             startWidth = doubleButtonLength;
306             break;
307         case ScrollbarButtonsDoubleEnd:
308             endWidth = doubleButtonLength;
309             break;
310         case ScrollbarButtonsDoubleBoth:
311             startWidth = doubleButtonLength;
312             endWidth = doubleButtonLength;
313             break;
314         default:
315             break;
316     }
317     
318     int totalWidth = startWidth + endWidth;
319     if (scrollbar->orientation() == HorizontalScrollbar)
320         return IntRect(scrollbar->x() + startWidth, scrollbar->y(), scrollbar->width() - totalWidth, thickness);
321     return IntRect(scrollbar->x(), scrollbar->y() + startWidth, thickness, scrollbar->height() - totalWidth);
322 }
323
324 int ScrollbarThemeMac::minimumThumbLength(Scrollbar* scrollbar)
325 {
326     return cThumbMinLength[scrollbar->controlSize()];
327 }
328
329 bool ScrollbarThemeMac::shouldCenterOnThumb(Scrollbar*, const PlatformMouseEvent& evt)
330 {
331     if (evt.button() != LeftButton)
332         return false;
333     if (gJumpOnTrackClick)
334         return !evt.altKey();
335     return evt.altKey();
336 }
337
338 static int scrollbarPartToHIPressedState(ScrollbarPart part)
339 {
340     switch (part) {
341         case BackButtonStartPart:
342             return kThemeTopOutsideArrowPressed;
343         case BackButtonEndPart:
344             return kThemeTopOutsideArrowPressed; // This does not make much sense.  For some reason the outside constant is required.
345         case ForwardButtonStartPart:
346             return kThemeTopInsideArrowPressed;
347         case ForwardButtonEndPart:
348             return kThemeBottomOutsideArrowPressed;
349         case ThumbPart:
350             return kThemeThumbPressed;
351         default:
352             return 0;
353     }
354 }
355
356 bool ScrollbarThemeMac::paint(Scrollbar* scrollbar, GraphicsContext* context, const IntRect& damageRect)
357 {
358     HIThemeTrackDrawInfo trackInfo;
359     trackInfo.version = 0;
360     trackInfo.kind = scrollbar->controlSize() == RegularScrollbar ? kThemeMediumScrollBar : kThemeSmallScrollBar;
361     trackInfo.bounds = scrollbar->frameRect();
362     trackInfo.min = 0;
363     trackInfo.max = scrollbar->maximum();
364     trackInfo.value = scrollbar->currentPos();
365     trackInfo.trackInfo.scrollbar.viewsize = scrollbar->visibleSize();
366     trackInfo.attributes = 0;
367     if (scrollbar->orientation() == HorizontalScrollbar)
368         trackInfo.attributes |= kThemeTrackHorizontal;
369
370     if (!scrollbar->enabled())
371         trackInfo.enableState = kThemeTrackDisabled;
372     else
373         trackInfo.enableState = scrollbar->client()->isActive() ? kThemeTrackActive : kThemeTrackInactive;
374
375     if (hasThumb(scrollbar))
376         trackInfo.attributes |= kThemeTrackShowThumb;
377     else if (!hasButtons(scrollbar))
378         trackInfo.enableState = kThemeTrackNothingToScroll;
379     trackInfo.trackInfo.scrollbar.pressState = scrollbarPartToHIPressedState(scrollbar->pressedPart());
380     
381     // The Aqua scrollbar is buggy when rotated and scaled.  We will just draw into a bitmap if we detect a scale or rotation.
382     const AffineTransform& currentCTM = context->getCTM();
383     bool canDrawDirectly = currentCTM.isIdentityOrTranslationOrFlipped();
384     if (canDrawDirectly)
385         HIThemeDrawTrack(&trackInfo, 0, context->platformContext(), kHIThemeOrientationNormal);
386     else {
387         trackInfo.bounds = IntRect(IntPoint(), scrollbar->frameRect().size());
388         
389         IntRect bufferRect(scrollbar->frameRect());
390         bufferRect.intersect(damageRect);
391         bufferRect.move(-scrollbar->frameRect().x(), -scrollbar->frameRect().y());
392         
393         OwnPtr<ImageBuffer> imageBuffer = ImageBuffer::create(bufferRect.size());
394         if (!imageBuffer)
395             return true;
396         
397         HIThemeDrawTrack(&trackInfo, 0, imageBuffer->context()->platformContext(), kHIThemeOrientationNormal);
398         context->drawImageBuffer(imageBuffer.get(), ColorSpaceDeviceRGB, scrollbar->frameRect().location());
399     }
400
401     return true;
402 }
403
404 }
405