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