Fix for <rdar://problem/8890255>
[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     NSString *buttonPlacement = [[NSUserDefaults standardUserDefaults] objectForKey:@"AppleScrollBarVariant"];
139     if ([buttonPlacement isEqualToString:@"Single"])
140         gButtonPlacement = ScrollbarButtonsSingle;
141     else if ([buttonPlacement isEqualToString:@"DoubleMin"])
142         gButtonPlacement = ScrollbarButtonsDoubleStart;
143     else if ([buttonPlacement isEqualToString:@"DoubleBoth"])
144         gButtonPlacement = ScrollbarButtonsDoubleBoth;
145     else
146         gButtonPlacement = ScrollbarButtonsDoubleEnd; // The default is ScrollbarButtonsDoubleEnd.
147 }
148
149 void ScrollbarThemeMac::registerScrollbar(Scrollbar* scrollbar)
150 {
151 #if defined(USE_WK_SCROLLBAR_PAINTER)
152     WKScrollbarPainterRef scrollbarPainter = wkMakeScrollbarPainter(scrollbar->controlSize(),
153         scrollbar->orientation() == HorizontalScrollbar);
154     scrollbarMap()->add(scrollbar, scrollbarPainter);
155 #else
156     scrollbarMap()->add(scrollbar);
157 #endif
158 }
159
160 void ScrollbarThemeMac::unregisterScrollbar(Scrollbar* scrollbar)
161 {
162     scrollbarMap()->remove(scrollbar);
163 }
164
165 ScrollbarThemeMac::ScrollbarThemeMac()
166 {
167     static bool initialized;
168     if (!initialized) {
169         initialized = true;
170         [ScrollbarPrefsObserver registerAsObserver];
171         preferencesChanged();
172     }
173 }
174
175 ScrollbarThemeMac::~ScrollbarThemeMac()
176 {
177 }
178
179 void ScrollbarThemeMac::preferencesChanged()
180 {
181     NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
182     [defaults synchronize];
183     updateArrowPlacement();
184     gInitialButtonDelay = [defaults floatForKey:@"NSScrollerButtonDelay"];
185     gAutoscrollButtonDelay = [defaults floatForKey:@"NSScrollerButtonPeriod"];
186     gJumpOnTrackClick = [defaults boolForKey:@"AppleScrollerPagingBehavior"];
187 }
188
189 int ScrollbarThemeMac::scrollbarThickness(ScrollbarControlSize controlSize)
190 {
191     return cScrollbarThickness[controlSize];
192 }
193
194 bool ScrollbarThemeMac::usesOverlayScrollbars() const
195 {
196     // FIXME: This should be enabled when <rdar://problem/8492788> is resolved.
197     return false;
198 }
199
200 double ScrollbarThemeMac::initialAutoscrollTimerDelay()
201 {
202     return gInitialButtonDelay;
203 }
204
205 double ScrollbarThemeMac::autoscrollTimerDelay()
206 {
207     return gAutoscrollButtonDelay;
208 }
209     
210 ScrollbarButtonsPlacement ScrollbarThemeMac::buttonsPlacement() const
211 {
212     return gButtonPlacement;
213 }
214
215 bool ScrollbarThemeMac::hasButtons(Scrollbar* scrollbar)
216 {
217     return scrollbar->enabled() && (scrollbar->orientation() == HorizontalScrollbar ? 
218              scrollbar->width() : 
219              scrollbar->height()) >= 2 * (cRealButtonLength[scrollbar->controlSize()] - cButtonHitInset[scrollbar->controlSize()]);
220 }
221
222 bool ScrollbarThemeMac::hasThumb(Scrollbar* scrollbar)
223 {
224     return scrollbar->enabled() && (scrollbar->orientation() == HorizontalScrollbar ? 
225              scrollbar->width() : 
226              scrollbar->height()) >= 2 * cButtonInset[scrollbar->controlSize()] + cThumbMinLength[scrollbar->controlSize()] + 1;
227 }
228
229 static IntRect buttonRepaintRect(const IntRect& buttonRect, ScrollbarOrientation orientation, ScrollbarControlSize controlSize, bool start)
230 {
231     IntRect paintRect(buttonRect);
232     if (orientation == HorizontalScrollbar) {
233         paintRect.setWidth(cRealButtonLength[controlSize]);
234         if (!start)
235             paintRect.setX(buttonRect.x() - (cRealButtonLength[controlSize] - buttonRect.width()));
236     } else {
237         paintRect.setHeight(cRealButtonLength[controlSize]);
238         if (!start)
239             paintRect.setY(buttonRect.y() - (cRealButtonLength[controlSize] - buttonRect.height()));
240     }
241
242     return paintRect;
243 }
244
245 IntRect ScrollbarThemeMac::backButtonRect(Scrollbar* scrollbar, ScrollbarPart part, bool painting)
246 {
247     IntRect result;
248     
249     if (part == BackButtonStartPart && (buttonsPlacement() == ScrollbarButtonsNone || buttonsPlacement() == ScrollbarButtonsDoubleEnd))
250         return result;
251     
252     if (part == BackButtonEndPart && (buttonsPlacement() == ScrollbarButtonsNone || buttonsPlacement() == ScrollbarButtonsDoubleStart || buttonsPlacement() == ScrollbarButtonsSingle))
253         return result;
254         
255     int thickness = scrollbarThickness(scrollbar->controlSize());
256     bool outerButton = part == BackButtonStartPart && (buttonsPlacement() == ScrollbarButtonsDoubleStart || buttonsPlacement() == ScrollbarButtonsDoubleBoth);
257     if (outerButton) {
258         if (scrollbar->orientation() == HorizontalScrollbar)
259             result = IntRect(scrollbar->x(), scrollbar->y(), cOuterButtonLength[scrollbar->controlSize()] + painting ? cOuterButtonOverlap : 0, thickness);
260         else
261             result = IntRect(scrollbar->x(), scrollbar->y(), thickness, cOuterButtonLength[scrollbar->controlSize()] + painting ? cOuterButtonOverlap : 0);
262         return result;
263     }
264     
265     // Our repaint rect is slightly larger, since we are a button that is adjacent to the track.
266     if (scrollbar->orientation() == HorizontalScrollbar) {
267         int start = part == BackButtonStartPart ? scrollbar->x() : scrollbar->x() + scrollbar->width() - cOuterButtonLength[scrollbar->controlSize()] - cButtonLength[scrollbar->controlSize()];
268         result = IntRect(start, scrollbar->y(), cButtonLength[scrollbar->controlSize()], thickness);
269     } else {
270         int start = part == BackButtonStartPart ? scrollbar->y() : scrollbar->y() + scrollbar->height() - cOuterButtonLength[scrollbar->controlSize()] - cButtonLength[scrollbar->controlSize()];
271         result = IntRect(scrollbar->x(), start, thickness, cButtonLength[scrollbar->controlSize()]);
272     }
273     
274     if (painting)
275         return buttonRepaintRect(result, scrollbar->orientation(), scrollbar->controlSize(), part == BackButtonStartPart);
276     return result;
277 }
278
279 IntRect ScrollbarThemeMac::forwardButtonRect(Scrollbar* scrollbar, ScrollbarPart part, bool painting)
280 {
281     IntRect result;
282     
283     if (part == ForwardButtonEndPart && (buttonsPlacement() == ScrollbarButtonsNone || buttonsPlacement() == ScrollbarButtonsDoubleStart))
284         return result;
285     
286     if (part == ForwardButtonStartPart && (buttonsPlacement() == ScrollbarButtonsNone || buttonsPlacement() == ScrollbarButtonsDoubleEnd || buttonsPlacement() == ScrollbarButtonsSingle))
287         return result;
288         
289     int thickness = scrollbarThickness(scrollbar->controlSize());
290     int outerButtonLength = cOuterButtonLength[scrollbar->controlSize()];
291     int buttonLength = cButtonLength[scrollbar->controlSize()];
292     
293     bool outerButton = part == ForwardButtonEndPart && (buttonsPlacement() == ScrollbarButtonsDoubleEnd || buttonsPlacement() == ScrollbarButtonsDoubleBoth);
294     if (outerButton) {
295         if (scrollbar->orientation() == HorizontalScrollbar) {
296             result = IntRect(scrollbar->x() + scrollbar->width() - outerButtonLength, scrollbar->y(), outerButtonLength, thickness);
297             if (painting)
298                 result.inflateX(cOuterButtonOverlap);
299         } else {
300             result = IntRect(scrollbar->x(), scrollbar->y() + scrollbar->height() - outerButtonLength, thickness, outerButtonLength);
301             if (painting)
302                 result.inflateY(cOuterButtonOverlap);
303         }
304         return result;
305     }
306     
307     if (scrollbar->orientation() == HorizontalScrollbar) {
308         int start = part == ForwardButtonEndPart ? scrollbar->x() + scrollbar->width() - buttonLength : scrollbar->x() + outerButtonLength;
309         result = IntRect(start, scrollbar->y(), buttonLength, thickness);
310     } else {
311         int start = part == ForwardButtonEndPart ? scrollbar->y() + scrollbar->height() - buttonLength : scrollbar->y() + outerButtonLength;
312         result = IntRect(scrollbar->x(), start, thickness, buttonLength);
313     }
314     if (painting)
315         return buttonRepaintRect(result, scrollbar->orientation(), scrollbar->controlSize(), part == ForwardButtonStartPart);
316     return result;
317 }
318
319 IntRect ScrollbarThemeMac::trackRect(Scrollbar* scrollbar, bool painting)
320 {
321     if (painting || !hasButtons(scrollbar))
322         return scrollbar->frameRect();
323     
324     IntRect result;
325     int thickness = scrollbarThickness(scrollbar->controlSize());
326     int startWidth = 0;
327     int endWidth = 0;
328     int outerButtonLength = cOuterButtonLength[scrollbar->controlSize()];
329     int buttonLength = cButtonLength[scrollbar->controlSize()];
330     int doubleButtonLength = outerButtonLength + buttonLength;
331     switch (buttonsPlacement()) {
332         case ScrollbarButtonsSingle:
333             startWidth = buttonLength;
334             endWidth = buttonLength;
335             break;
336         case ScrollbarButtonsDoubleStart:
337             startWidth = doubleButtonLength;
338             break;
339         case ScrollbarButtonsDoubleEnd:
340             endWidth = doubleButtonLength;
341             break;
342         case ScrollbarButtonsDoubleBoth:
343             startWidth = doubleButtonLength;
344             endWidth = doubleButtonLength;
345             break;
346         default:
347             break;
348     }
349     
350     int totalWidth = startWidth + endWidth;
351     if (scrollbar->orientation() == HorizontalScrollbar)
352         return IntRect(scrollbar->x() + startWidth, scrollbar->y(), scrollbar->width() - totalWidth, thickness);
353     return IntRect(scrollbar->x(), scrollbar->y() + startWidth, thickness, scrollbar->height() - totalWidth);
354 }
355
356 int ScrollbarThemeMac::minimumThumbLength(Scrollbar* scrollbar)
357 {
358     return cThumbMinLength[scrollbar->controlSize()];
359 }
360
361 bool ScrollbarThemeMac::shouldCenterOnThumb(Scrollbar*, const PlatformMouseEvent& evt)
362 {
363     if (evt.button() != LeftButton)
364         return false;
365     if (gJumpOnTrackClick)
366         return !evt.altKey();
367     return evt.altKey();
368 }
369
370 static int scrollbarPartToHIPressedState(ScrollbarPart part)
371 {
372     switch (part) {
373         case BackButtonStartPart:
374             return kThemeTopOutsideArrowPressed;
375         case BackButtonEndPart:
376             return kThemeTopOutsideArrowPressed; // This does not make much sense.  For some reason the outside constant is required.
377         case ForwardButtonStartPart:
378             return kThemeTopInsideArrowPressed;
379         case ForwardButtonEndPart:
380             return kThemeBottomOutsideArrowPressed;
381         case ThumbPart:
382             return kThemeThumbPressed;
383         default:
384             return 0;
385     }
386 }
387
388 bool ScrollbarThemeMac::paint(Scrollbar* scrollbar, GraphicsContext* context, const IntRect& damageRect)
389 {
390 #if defined(USE_WK_SCROLLBAR_PAINTER)
391     context->save();
392     context->clip(damageRect);
393     context->translate(scrollbar->frameRect().x(), scrollbar->frameRect().y());
394     LocalCurrentGraphicsContext localContext(context);
395     wkScrollbarPainterPaint(scrollbarMap()->get(scrollbar).get(),
396                             scrollbar->enabled(),
397                             scrollbar->currentPos() / scrollbar->maximum(),
398                             static_cast<CGFloat>(scrollbar->visibleSize()) / scrollbar->totalSize(),
399                             scrollbar->frameRect());
400     context->restore();
401     return true;
402 #endif
403
404     HIThemeTrackDrawInfo trackInfo;
405     trackInfo.version = 0;
406     trackInfo.kind = scrollbar->controlSize() == RegularScrollbar ? kThemeMediumScrollBar : kThemeSmallScrollBar;
407     trackInfo.bounds = scrollbar->frameRect();
408     trackInfo.min = 0;
409     trackInfo.max = scrollbar->maximum();
410     trackInfo.value = scrollbar->currentPos();
411     trackInfo.trackInfo.scrollbar.viewsize = scrollbar->visibleSize();
412     trackInfo.attributes = 0;
413     if (scrollbar->orientation() == HorizontalScrollbar)
414         trackInfo.attributes |= kThemeTrackHorizontal;
415
416     if (!scrollbar->enabled())
417         trackInfo.enableState = kThemeTrackDisabled;
418     else
419         trackInfo.enableState = scrollbar->client()->isActive() ? kThemeTrackActive : kThemeTrackInactive;
420
421     if (hasThumb(scrollbar))
422         trackInfo.attributes |= kThemeTrackShowThumb;
423     else if (!hasButtons(scrollbar))
424         trackInfo.enableState = kThemeTrackNothingToScroll;
425     trackInfo.trackInfo.scrollbar.pressState = scrollbarPartToHIPressedState(scrollbar->pressedPart());
426     
427     // The Aqua scrollbar is buggy when rotated and scaled.  We will just draw into a bitmap if we detect a scale or rotation.
428     const AffineTransform& currentCTM = context->getCTM();
429     bool canDrawDirectly = currentCTM.isIdentityOrTranslationOrFlipped();
430     if (canDrawDirectly)
431         HIThemeDrawTrack(&trackInfo, 0, context->platformContext(), kHIThemeOrientationNormal);
432     else {
433         trackInfo.bounds = IntRect(IntPoint(), scrollbar->frameRect().size());
434         
435         IntRect bufferRect(scrollbar->frameRect());
436         bufferRect.intersect(damageRect);
437         bufferRect.move(-scrollbar->frameRect().x(), -scrollbar->frameRect().y());
438         
439         OwnPtr<ImageBuffer> imageBuffer = ImageBuffer::create(bufferRect.size());
440         if (!imageBuffer)
441             return true;
442         
443         HIThemeDrawTrack(&trackInfo, 0, imageBuffer->context()->platformContext(), kHIThemeOrientationNormal);
444         context->drawImageBuffer(imageBuffer.get(), ColorSpaceDeviceRGB, scrollbar->frameRect().location());
445     }
446
447     return true;
448 }
449
450 }
451