• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 * Copyright (C) 2008 Apple Inc. All Rights Reserved.
3 * Copyright (C) 2009 Google Inc. All Rights Reserved.
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions
7 * are met:
8 * 1. Redistributions of source code must retain the above copyright
9 *    notice, this list of conditions and the following disclaimer.
10 * 2. Redistributions in binary form must reproduce the above copyright
11 *    notice, this list of conditions and the following disclaimer in the
12 *    documentation and/or other materials provided with the distribution.
13 *
14 * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
15 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
16 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
17 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE INC. OR
18 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
19 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
20 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
21 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
22 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
23 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
24 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
25 */
26
27#include "config.h"
28#include "ScrollbarThemeChromiumMac.h"
29
30#include "FrameView.h"
31#include "ImageBuffer.h"
32#include "PlatformBridge.h"
33#include "PlatformMouseEvent.h"
34#include "ScrollView.h"
35#include <Carbon/Carbon.h>
36#include <wtf/StdLibExtras.h>
37#include <wtf/UnusedParam.h>
38
39
40// FIXME: There are repainting problems due to Aqua scroll bar buttons' visual overflow.
41
42using namespace std;
43using namespace WebCore;
44
45// This file (and its associated .h file) is a clone of ScrollbarThemeMac.mm.
46// Because we want to draw tickmarks in the scrollbar, we must maintain a fork.
47// Please maintain this file by performing parallel changes to it.
48//
49// The only changes from ScrollbarThemeMac should be:
50// - The classname change from ScrollbarThemeMac to ScrollbarThemeChromiumMac.
51// - In paint() the code to paint the track, tickmarks, and thumb separately.
52// - In paint() the thumb is drawn via ChromeBridge/WebThemeEngine.
53//
54// For all other differences, if it was introduced in this file, then the
55// maintainer forgot to include it in the list; otherwise it is an update that
56// should have been applied to this file but was not.
57
58static HashSet<Scrollbar*>* gScrollbars;
59
60@interface ScrollbarPrefsObserver : NSObject
61{
62
63}
64
65+ (void)registerAsObserver;
66+ (void)appearancePrefsChanged:(NSNotification*)theNotification;
67+ (void)behaviorPrefsChanged:(NSNotification*)theNotification;
68
69@end
70
71@implementation ScrollbarPrefsObserver
72
73+ (void)appearancePrefsChanged:(NSNotification*)unusedNotification
74{
75    UNUSED_PARAM(unusedNotification);
76
77    static_cast<ScrollbarThemeChromiumMac*>(ScrollbarTheme::nativeTheme())->preferencesChanged();
78    if (!gScrollbars)
79        return;
80    HashSet<Scrollbar*>::iterator end = gScrollbars->end();
81    for (HashSet<Scrollbar*>::iterator it = gScrollbars->begin(); it != end; ++it) {
82        (*it)->styleChanged();
83        (*it)->invalidate();
84    }
85}
86
87+ (void)behaviorPrefsChanged:(NSNotification*)unusedNotification
88{
89    UNUSED_PARAM(unusedNotification);
90
91    static_cast<ScrollbarThemeChromiumMac*>(ScrollbarTheme::nativeTheme())->preferencesChanged();
92}
93
94+ (void)registerAsObserver
95{
96    [[NSDistributedNotificationCenter defaultCenter] addObserver:self selector:@selector(appearancePrefsChanged:) name:@"AppleAquaScrollBarVariantChanged" object:nil suspensionBehavior:NSNotificationSuspensionBehaviorDeliverImmediately];
97    [[NSDistributedNotificationCenter defaultCenter] addObserver:self selector:@selector(behaviorPrefsChanged:) name:@"AppleNoRedisplayAppearancePreferenceChanged" object:nil suspensionBehavior:NSNotificationSuspensionBehaviorCoalesce];
98}
99
100@end
101
102namespace WebCore {
103
104ScrollbarTheme* ScrollbarTheme::nativeTheme()
105{
106    DEFINE_STATIC_LOCAL(ScrollbarThemeChromiumMac, theme, ());
107    return &theme;
108}
109
110// FIXME: Get these numbers from CoreUI.
111static int cScrollbarThickness[] = { 15, 11 };
112static int cRealButtonLength[] = { 28, 21 };
113static int cButtonInset[] = { 14, 11 };
114static int cButtonHitInset[] = { 3, 2 };
115// cRealButtonLength - cButtonInset
116static int cButtonLength[] = { 14, 10 };
117static int cThumbMinLength[] = { 26, 20 };
118
119static int cOuterButtonLength[] = { 16, 14 }; // The outer button in a double button pair is a bit bigger.
120static int cOuterButtonOverlap = 2;
121
122static float gInitialButtonDelay = 0.5f;
123static float gAutoscrollButtonDelay = 0.05f;
124static bool gJumpOnTrackClick = false;
125static ScrollbarButtonsPlacement gButtonPlacement = ScrollbarButtonsDoubleEnd;
126
127static void updateArrowPlacement()
128{
129    NSString *buttonPlacement = [[NSUserDefaults standardUserDefaults] objectForKey:@"AppleScrollBarVariant"];
130    if ([buttonPlacement isEqualToString:@"Single"])
131        gButtonPlacement = ScrollbarButtonsSingle;
132    else if ([buttonPlacement isEqualToString:@"DoubleMin"])
133        gButtonPlacement = ScrollbarButtonsDoubleStart;
134    else if ([buttonPlacement isEqualToString:@"DoubleBoth"])
135        gButtonPlacement = ScrollbarButtonsDoubleBoth;
136    else
137        gButtonPlacement = ScrollbarButtonsDoubleEnd; // The default is ScrollbarButtonsDoubleEnd.
138}
139
140void ScrollbarThemeChromiumMac::registerScrollbar(Scrollbar* scrollbar)
141{
142    if (!gScrollbars)
143        gScrollbars = new HashSet<Scrollbar*>;
144    gScrollbars->add(scrollbar);
145}
146
147void ScrollbarThemeChromiumMac::unregisterScrollbar(Scrollbar* scrollbar)
148{
149    gScrollbars->remove(scrollbar);
150    if (gScrollbars->isEmpty()) {
151        delete gScrollbars;
152        gScrollbars = 0;
153    }
154}
155
156ScrollbarThemeChromiumMac::ScrollbarThemeChromiumMac()
157{
158    static bool initialized;
159    if (!initialized) {
160        initialized = true;
161        [ScrollbarPrefsObserver registerAsObserver];
162        preferencesChanged();
163    }
164}
165
166ScrollbarThemeChromiumMac::~ScrollbarThemeChromiumMac()
167{
168}
169
170void ScrollbarThemeChromiumMac::preferencesChanged()
171{
172    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
173    [defaults synchronize];
174    updateArrowPlacement();
175    gInitialButtonDelay = [defaults floatForKey:@"NSScrollerButtonDelay"];
176    gAutoscrollButtonDelay = [defaults floatForKey:@"NSScrollerButtonPeriod"];
177    gJumpOnTrackClick = [defaults boolForKey:@"AppleScrollerPagingBehavior"];
178}
179
180int ScrollbarThemeChromiumMac::scrollbarThickness(ScrollbarControlSize controlSize)
181{
182    return cScrollbarThickness[controlSize];
183}
184
185double ScrollbarThemeChromiumMac::initialAutoscrollTimerDelay()
186{
187    return gInitialButtonDelay;
188}
189
190double ScrollbarThemeChromiumMac::autoscrollTimerDelay()
191{
192    return gAutoscrollButtonDelay;
193}
194
195ScrollbarButtonsPlacement ScrollbarThemeChromiumMac::buttonsPlacement() const
196{
197    return gButtonPlacement;
198}
199
200bool ScrollbarThemeChromiumMac::hasButtons(Scrollbar* scrollbar)
201{
202    return scrollbar->enabled() && (scrollbar->orientation() == HorizontalScrollbar ?
203             scrollbar->width() :
204             scrollbar->height()) >= 2 * (cRealButtonLength[scrollbar->controlSize()] - cButtonHitInset[scrollbar->controlSize()]);
205}
206
207bool ScrollbarThemeChromiumMac::hasThumb(Scrollbar* scrollbar)
208{
209    return scrollbar->enabled() && (scrollbar->orientation() == HorizontalScrollbar ?
210             scrollbar->width() :
211             scrollbar->height()) >= 2 * cButtonInset[scrollbar->controlSize()] + cThumbMinLength[scrollbar->controlSize()] + 1;
212}
213
214static IntRect buttonRepaintRect(const IntRect& buttonRect, ScrollbarOrientation orientation, ScrollbarControlSize controlSize, bool start)
215{
216    IntRect paintRect(buttonRect);
217    if (orientation == HorizontalScrollbar) {
218        paintRect.setWidth(cRealButtonLength[controlSize]);
219        if (!start)
220            paintRect.setX(buttonRect.x() - (cRealButtonLength[controlSize] - buttonRect.width()));
221    } else {
222        paintRect.setHeight(cRealButtonLength[controlSize]);
223        if (!start)
224            paintRect.setY(buttonRect.y() - (cRealButtonLength[controlSize] - buttonRect.height()));
225    }
226
227    return paintRect;
228}
229
230IntRect ScrollbarThemeChromiumMac::backButtonRect(Scrollbar* scrollbar, ScrollbarPart part, bool painting)
231{
232    IntRect result;
233
234    if (part == BackButtonStartPart && (buttonsPlacement() == ScrollbarButtonsNone || buttonsPlacement() == ScrollbarButtonsDoubleEnd))
235        return result;
236
237    if (part == BackButtonEndPart && (buttonsPlacement() == ScrollbarButtonsNone || buttonsPlacement() == ScrollbarButtonsDoubleStart || buttonsPlacement() == ScrollbarButtonsSingle))
238        return result;
239
240    int thickness = scrollbarThickness(scrollbar->controlSize());
241    bool outerButton = part == BackButtonStartPart && (buttonsPlacement() == ScrollbarButtonsDoubleStart || buttonsPlacement() == ScrollbarButtonsDoubleBoth);
242    if (outerButton) {
243        if (scrollbar->orientation() == HorizontalScrollbar)
244            result = IntRect(scrollbar->x(), scrollbar->y(), cOuterButtonLength[scrollbar->controlSize()] + painting ? cOuterButtonOverlap : 0, thickness);
245        else
246            result = IntRect(scrollbar->x(), scrollbar->y(), thickness, cOuterButtonLength[scrollbar->controlSize()] + painting ? cOuterButtonOverlap : 0);
247        return result;
248    }
249
250    // Our repaint rect is slightly larger, since we are a button that is adjacent to the track.
251    if (scrollbar->orientation() == HorizontalScrollbar) {
252        int start = part == BackButtonStartPart ? scrollbar->x() : scrollbar->x() + scrollbar->width() - cOuterButtonLength[scrollbar->controlSize()] - cButtonLength[scrollbar->controlSize()];
253        result = IntRect(start, scrollbar->y(), cButtonLength[scrollbar->controlSize()], thickness);
254    } else {
255        int start = part == BackButtonStartPart ? scrollbar->y() : scrollbar->y() + scrollbar->height() - cOuterButtonLength[scrollbar->controlSize()] - cButtonLength[scrollbar->controlSize()];
256        result = IntRect(scrollbar->x(), start, thickness, cButtonLength[scrollbar->controlSize()]);
257    }
258
259    if (painting)
260        return buttonRepaintRect(result, scrollbar->orientation(), scrollbar->controlSize(), part == BackButtonStartPart);
261    return result;
262}
263
264IntRect ScrollbarThemeChromiumMac::forwardButtonRect(Scrollbar* scrollbar, ScrollbarPart part, bool painting)
265{
266    IntRect result;
267
268    if (part == ForwardButtonEndPart && (buttonsPlacement() == ScrollbarButtonsNone || buttonsPlacement() == ScrollbarButtonsDoubleStart))
269        return result;
270
271    if (part == ForwardButtonStartPart && (buttonsPlacement() == ScrollbarButtonsNone || buttonsPlacement() == ScrollbarButtonsDoubleEnd || buttonsPlacement() == ScrollbarButtonsSingle))
272        return result;
273
274    int thickness = scrollbarThickness(scrollbar->controlSize());
275    int outerButtonLength = cOuterButtonLength[scrollbar->controlSize()];
276    int buttonLength = cButtonLength[scrollbar->controlSize()];
277
278    bool outerButton = part == ForwardButtonEndPart && (buttonsPlacement() == ScrollbarButtonsDoubleEnd || buttonsPlacement() == ScrollbarButtonsDoubleBoth);
279    if (outerButton) {
280        if (scrollbar->orientation() == HorizontalScrollbar) {
281            result = IntRect(scrollbar->x() + scrollbar->width() - outerButtonLength, scrollbar->y(), outerButtonLength, thickness);
282            if (painting)
283                result.inflateX(cOuterButtonOverlap);
284        } else {
285            result = IntRect(scrollbar->x(), scrollbar->y() + scrollbar->height() - outerButtonLength, thickness, outerButtonLength);
286            if (painting)
287                result.inflateY(cOuterButtonOverlap);
288        }
289        return result;
290    }
291
292    if (scrollbar->orientation() == HorizontalScrollbar) {
293        int start = part == ForwardButtonEndPart ? scrollbar->x() + scrollbar->width() - buttonLength : scrollbar->x() + outerButtonLength;
294        result = IntRect(start, scrollbar->y(), buttonLength, thickness);
295    } else {
296        int start = part == ForwardButtonEndPart ? scrollbar->y() + scrollbar->height() - buttonLength : scrollbar->y() + outerButtonLength;
297        result = IntRect(scrollbar->x(), start, thickness, buttonLength);
298    }
299    if (painting)
300        return buttonRepaintRect(result, scrollbar->orientation(), scrollbar->controlSize(), part == ForwardButtonStartPart);
301    return result;
302}
303
304IntRect ScrollbarThemeChromiumMac::trackRect(Scrollbar* scrollbar, bool painting)
305{
306    if (painting || !hasButtons(scrollbar))
307        return scrollbar->frameRect();
308
309    IntRect result;
310    int thickness = scrollbarThickness(scrollbar->controlSize());
311    int startWidth = 0;
312    int endWidth = 0;
313    int outerButtonLength = cOuterButtonLength[scrollbar->controlSize()];
314    int buttonLength = cButtonLength[scrollbar->controlSize()];
315    int doubleButtonLength = outerButtonLength + buttonLength;
316    switch (buttonsPlacement()) {
317        case ScrollbarButtonsSingle:
318            startWidth = buttonLength;
319            endWidth = buttonLength;
320            break;
321        case ScrollbarButtonsDoubleStart:
322            startWidth = doubleButtonLength;
323            break;
324        case ScrollbarButtonsDoubleEnd:
325            endWidth = doubleButtonLength;
326            break;
327        case ScrollbarButtonsDoubleBoth:
328            startWidth = doubleButtonLength;
329            endWidth = doubleButtonLength;
330            break;
331        default:
332            break;
333    }
334
335    int totalWidth = startWidth + endWidth;
336    if (scrollbar->orientation() == HorizontalScrollbar)
337        return IntRect(scrollbar->x() + startWidth, scrollbar->y(), scrollbar->width() - totalWidth, thickness);
338    return IntRect(scrollbar->x(), scrollbar->y() + startWidth, thickness, scrollbar->height() - totalWidth);
339}
340
341int ScrollbarThemeChromiumMac::minimumThumbLength(Scrollbar* scrollbar)
342{
343    return cThumbMinLength[scrollbar->controlSize()];
344}
345
346bool ScrollbarThemeChromiumMac::shouldCenterOnThumb(Scrollbar*, const PlatformMouseEvent& evt)
347{
348    if (evt.button() != LeftButton)
349        return false;
350    if (gJumpOnTrackClick)
351        return !evt.altKey();
352    return evt.altKey();
353}
354
355static int scrollbarPartToHIPressedState(ScrollbarPart part)
356{
357    switch (part) {
358        case BackButtonStartPart:
359            return kThemeTopOutsideArrowPressed;
360        case BackButtonEndPart:
361            return kThemeTopOutsideArrowPressed; // This does not make much sense.  For some reason the outside constant is required.
362        case ForwardButtonStartPart:
363            return kThemeTopInsideArrowPressed;
364        case ForwardButtonEndPart:
365            return kThemeBottomOutsideArrowPressed;
366        case ThumbPart:
367            return kThemeThumbPressed;
368        default:
369            return 0;
370    }
371}
372
373static PlatformBridge::ThemePaintState scrollbarStateToThemeState(Scrollbar* scrollbar) {
374    if (!scrollbar->enabled())
375        return PlatformBridge::StateDisabled;
376    if (!scrollbar->scrollableArea()->isActive())
377        return PlatformBridge::StateInactive;
378    if (scrollbar->pressedPart() == ThumbPart)
379        return PlatformBridge::StatePressed;
380
381    return PlatformBridge::StateActive;
382}
383
384bool ScrollbarThemeChromiumMac::paint(Scrollbar* scrollbar, GraphicsContext* context, const IntRect& damageRect)
385{
386    HIThemeTrackDrawInfo trackInfo;
387    trackInfo.version = 0;
388    trackInfo.kind = scrollbar->controlSize() == RegularScrollbar ? kThemeMediumScrollBar : kThemeSmallScrollBar;
389    trackInfo.bounds = scrollbar->frameRect();
390    trackInfo.min = 0;
391    trackInfo.max = scrollbar->maximum();
392    trackInfo.value = scrollbar->currentPos();
393    trackInfo.trackInfo.scrollbar.viewsize = scrollbar->visibleSize();
394    trackInfo.attributes = 0;
395    if (scrollbar->orientation() == HorizontalScrollbar)
396        trackInfo.attributes |= kThemeTrackHorizontal;
397
398    if (!scrollbar->enabled())
399        trackInfo.enableState = kThemeTrackDisabled;
400    else
401        trackInfo.enableState = scrollbar->scrollableArea()->isActive() ? kThemeTrackActive : kThemeTrackInactive;
402
403    if (!hasButtons(scrollbar))
404        trackInfo.enableState = kThemeTrackNothingToScroll;
405    trackInfo.trackInfo.scrollbar.pressState = scrollbarPartToHIPressedState(scrollbar->pressedPart());
406
407    CGAffineTransform currentCTM = CGContextGetCTM(context->platformContext());
408
409    // The Aqua scrollbar is buggy when rotated and scaled.  We will just draw into a bitmap if we detect a scale or rotation.
410    bool canDrawDirectly = currentCTM.a == 1.0f && currentCTM.b == 0.0f && currentCTM.c == 0.0f && (currentCTM.d == 1.0f || currentCTM.d == -1.0f);
411    GraphicsContext* drawingContext = context;
412    OwnPtr<ImageBuffer> imageBuffer;
413    if (!canDrawDirectly) {
414        trackInfo.bounds = IntRect(IntPoint(), scrollbar->frameRect().size());
415
416        IntRect bufferRect(scrollbar->frameRect());
417        bufferRect.intersect(damageRect);
418        bufferRect.move(-scrollbar->frameRect().x(), -scrollbar->frameRect().y());
419
420        imageBuffer = ImageBuffer::create(bufferRect.size());
421        if (!imageBuffer)
422            return true;
423
424        drawingContext = imageBuffer->context();
425    }
426
427    // Draw thumbless.
428    HIThemeDrawTrack(&trackInfo, 0, drawingContext->platformContext(), kHIThemeOrientationNormal);
429
430    Vector<IntRect> tickmarks;
431    scrollbar->scrollableArea()->getTickmarks(tickmarks);
432    if (scrollbar->orientation() == VerticalScrollbar && tickmarks.size()) {
433        drawingContext->save();
434        drawingContext->setShouldAntialias(false);
435        drawingContext->setStrokeColor(Color(0xCC, 0xAA, 0x00, 0xFF), ColorSpaceDeviceRGB);
436        drawingContext->setFillColor(Color(0xFF, 0xDD, 0x00, 0xFF), ColorSpaceDeviceRGB);
437
438        IntRect thumbArea = trackRect(scrollbar, false);
439        if (!canDrawDirectly) {
440            thumbArea.setX(0);
441            thumbArea.setY(0);
442        }
443        // The ends are rounded and the thumb doesn't go there.
444        thumbArea.inflateY(-thumbArea.width());
445
446        for (Vector<IntRect>::const_iterator i = tickmarks.begin(); i != tickmarks.end(); ++i) {
447            // Calculate how far down (in %) the tick-mark should appear.
448            const float percent = static_cast<float>(i->y()) / scrollbar->totalSize();
449            if (percent < 0.0 || percent > 1.0)
450              continue;
451
452            // Calculate how far down (in pixels) the tick-mark should appear.
453            const int yPos = static_cast<int>((thumbArea.y() + (thumbArea.height() * percent))) & ~1;
454
455            // Paint.
456            const int indent = 2;
457            FloatRect tickRect(thumbArea.x() + indent, yPos, thumbArea.width() - 2 * indent - 1, 2);
458            drawingContext->fillRect(tickRect);
459            drawingContext->strokeRect(tickRect, 1);
460        }
461
462        drawingContext->restore();
463    }
464
465    if (hasThumb(scrollbar)) {
466        PlatformBridge::ThemePaintScrollbarInfo scrollbarInfo;
467        scrollbarInfo.orientation = scrollbar->orientation() == HorizontalScrollbar ? PlatformBridge::ScrollbarOrientationHorizontal : PlatformBridge::ScrollbarOrientationVertical;
468        scrollbarInfo.parent = scrollbar->parent() && scrollbar->parent()->isFrameView() && static_cast<FrameView*>(scrollbar->parent())->isScrollViewScrollbar(scrollbar) ? PlatformBridge::ScrollbarParentScrollView : PlatformBridge::ScrollbarParentRenderLayer;
469        scrollbarInfo.maxValue = scrollbar->maximum();
470        scrollbarInfo.currentValue = scrollbar->currentPos();
471        scrollbarInfo.visibleSize = scrollbar->visibleSize();
472        scrollbarInfo.totalSize = scrollbar->totalSize();
473
474        PlatformBridge::paintScrollbarThumb(
475            drawingContext,
476            scrollbarStateToThemeState(scrollbar),
477            scrollbar->controlSize() == RegularScrollbar ? PlatformBridge::SizeRegular : PlatformBridge::SizeSmall,
478            scrollbar->frameRect(),
479            scrollbarInfo);
480    }
481
482    if (!canDrawDirectly)
483        context->drawImageBuffer(imageBuffer.get(), ColorSpaceDeviceRGB, scrollbar->frameRect().location());
484
485    return true;
486}
487
488}
489
490