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