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