0aa3e10fad4acec13ba7730f892100ee489d8048
[WebKit-https.git] / Source / WebCore / platform / chromium / ScrollbarThemeChromiumMac.mm
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 "ChromiumBridge.h"
31 #include "FrameView.h"
32 #include "ImageBuffer.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
42 using namespace std;
43 using 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
58 static 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
102 namespace WebCore {
103
104 ScrollbarTheme* ScrollbarTheme::nativeTheme()
105 {
106     DEFINE_STATIC_LOCAL(ScrollbarThemeChromiumMac, theme, ());
107     return &theme;
108 }
109
110 // FIXME: Get these numbers from CoreUI.
111 static int cScrollbarThickness[] = { 15, 11 };
112 static int cRealButtonLength[] = { 28, 21 };
113 static int cButtonInset[] = { 14, 11 };
114 static int cButtonHitInset[] = { 3, 2 };
115 // cRealButtonLength - cButtonInset
116 static int cButtonLength[] = { 14, 10 };
117 static int cThumbMinLength[] = { 26, 20 };
118
119 static int cOuterButtonLength[] = { 16, 14 }; // The outer button in a double button pair is a bit bigger.
120 static int cOuterButtonOverlap = 2;
121
122 static float gInitialButtonDelay = 0.5f;
123 static float gAutoscrollButtonDelay = 0.05f;
124 static bool gJumpOnTrackClick = false;
125 static ScrollbarButtonsPlacement gButtonPlacement = ScrollbarButtonsDoubleEnd;
126
127 static 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
140 void ScrollbarThemeChromiumMac::registerScrollbar(Scrollbar* scrollbar)
141 {
142     if (!gScrollbars)
143         gScrollbars = new HashSet<Scrollbar*>;
144     gScrollbars->add(scrollbar);
145 }
146
147 void ScrollbarThemeChromiumMac::unregisterScrollbar(Scrollbar* scrollbar)
148 {
149     gScrollbars->remove(scrollbar);
150     if (gScrollbars->isEmpty()) {
151         delete gScrollbars;
152         gScrollbars = 0;
153     }
154 }
155
156 ScrollbarThemeChromiumMac::ScrollbarThemeChromiumMac()
157 {
158     static bool initialized;
159     if (!initialized) {
160         initialized = true;
161         [ScrollbarPrefsObserver registerAsObserver];
162         preferencesChanged();
163     }
164 }
165
166 ScrollbarThemeChromiumMac::~ScrollbarThemeChromiumMac()
167 {
168 }
169
170 void 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
180 int ScrollbarThemeChromiumMac::scrollbarThickness(ScrollbarControlSize controlSize)
181 {
182     return cScrollbarThickness[controlSize];
183 }
184
185 double ScrollbarThemeChromiumMac::initialAutoscrollTimerDelay()
186 {
187     return gInitialButtonDelay;
188 }
189
190 double ScrollbarThemeChromiumMac::autoscrollTimerDelay()
191 {
192     return gAutoscrollButtonDelay;
193 }
194
195 ScrollbarButtonsPlacement ScrollbarThemeChromiumMac::buttonsPlacement() const
196 {
197     return gButtonPlacement;
198 }
199
200 bool 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
207 bool 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
214 static 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
230 IntRect 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
264 IntRect 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
304 IntRect 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
341 int ScrollbarThemeChromiumMac::minimumThumbLength(Scrollbar* scrollbar)
342 {
343     return cThumbMinLength[scrollbar->controlSize()];
344 }
345
346 bool 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
355 static 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
373 static ChromiumBridge::ThemePaintState scrollbarStateToThemeState(Scrollbar* scrollbar) {
374     if (!scrollbar->enabled())
375         return ChromiumBridge::StateDisabled;
376     if (!scrollbar->client()->isActive())
377         return ChromiumBridge::StateInactive;
378     if (scrollbar->pressedPart() == ThumbPart)
379         return ChromiumBridge::StatePressed;
380
381     return ChromiumBridge::StateActive;
382 }
383
384 bool 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->client()->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->client()->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.topLeft().y() + (thumbArea.height() * percent))) & ~1;
454
455             // Paint.
456             const int indent = 2;
457             FloatRect tickRect(thumbArea.topLeft().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         ChromiumBridge::ThemePaintScrollbarInfo scrollbarInfo;
467         scrollbarInfo.orientation = scrollbar->orientation() == HorizontalScrollbar ? ChromiumBridge::ScrollbarOrientationHorizontal : ChromiumBridge::ScrollbarOrientationVertical;
468         scrollbarInfo.parent = scrollbar->parent() && scrollbar->parent()->isFrameView() && static_cast<FrameView*>(scrollbar->parent())->isScrollViewScrollbar(scrollbar) ? ChromiumBridge::ScrollbarParentScrollView : ChromiumBridge::ScrollbarParentRenderLayer;
469         scrollbarInfo.maxValue = scrollbar->maximum();
470         scrollbarInfo.currentValue = scrollbar->currentPos();
471         scrollbarInfo.visibleSize = scrollbar->visibleSize();
472         scrollbarInfo.totalSize = scrollbar->totalSize();
473
474         ChromiumBridge::paintScrollbarThumb(
475             drawingContext,
476             scrollbarStateToThemeState(scrollbar),
477             scrollbar->controlSize() == RegularScrollbar ? ChromiumBridge::SizeRegular : ChromiumBridge::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