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