Home | History | Annotate | Download | only in mac
      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     CGAffineTransform currentCTM = CGContextGetCTM(context->platformContext());
    382 
    383     // The Aqua scrollbar is buggy when rotated and scaled.  We will just draw into a bitmap if we detect a scale or rotation.
    384     bool canDrawDirectly = currentCTM.a == 1.0f && currentCTM.b == 0.0f && currentCTM.c == 0.0f && (currentCTM.d == 1.0f || currentCTM.d == -1.0f);
    385     if (canDrawDirectly)
    386         HIThemeDrawTrack(&trackInfo, 0, context->platformContext(), kHIThemeOrientationNormal);
    387     else {
    388         trackInfo.bounds = IntRect(IntPoint(), scrollbar->frameRect().size());
    389 
    390         IntRect bufferRect(scrollbar->frameRect());
    391         bufferRect.intersect(damageRect);
    392         bufferRect.move(-scrollbar->frameRect().x(), -scrollbar->frameRect().y());
    393 
    394         OwnPtr<ImageBuffer> imageBuffer = ImageBuffer::create(bufferRect.size());
    395         if (!imageBuffer)
    396             return true;
    397 
    398         HIThemeDrawTrack(&trackInfo, 0, imageBuffer->context()->platformContext(), kHIThemeOrientationNormal);
    399         context->drawImage(imageBuffer->image(), DeviceColorSpace, scrollbar->frameRect().location());
    400     }
    401 
    402     return true;
    403 }
    404 
    405 }
    406 
    407