/* * This file is part of the theme implementation for form controls in WebCore. * * Copyright (C) 2005, 2006 Apple Computer, Inc. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 59 Temple Place - Suite 330, * Boston, MA 02111-1307, USA. */ #import "config.h" #import "RenderThemeMac.h" #import "CSSStyleSelector.h" #import "CSSValueKeywords.h" #import "Document.h" #import "Element.h" #import "FoundationExtras.h" #import "FrameView.h" #import "GraphicsContext.h" #import "HTMLInputElement.h" #import "Image.h" #import "LocalCurrentGraphicsContext.h" #import "RenderSlider.h" #import "RenderView.h" #import "WebCoreSystemInterface.h" #import #import #import using std::min; // The methods in this file are specific to the Mac OS X platform. // FIXME: The platform-independent code in this class should be factored out and merged with RenderThemeSafari. @interface WebCoreRenderThemeNotificationObserver : NSObject { WebCore::RenderTheme *_theme; } - (id)initWithTheme:(WebCore::RenderTheme *)theme; - (void)systemColorsDidChange:(NSNotification *)notification; @end @implementation WebCoreRenderThemeNotificationObserver - (id)initWithTheme:(WebCore::RenderTheme *)theme { [super init]; _theme = theme; return self; } - (void)systemColorsDidChange:(NSNotification *)notification { ASSERT([[notification name] isEqualToString:NSSystemColorsDidChangeNotification]); _theme->platformColorsDidChange(); } @end namespace WebCore { enum { topMargin, rightMargin, bottomMargin, leftMargin }; enum { topPadding, rightPadding, bottomPadding, leftPadding }; RenderTheme* theme() { static RenderThemeMac* macTheme = new RenderThemeMac; return macTheme; } RenderThemeMac::RenderThemeMac() : m_resizeCornerImage(0) , m_isSliderThumbHorizontalPressed(false) , m_isSliderThumbVerticalPressed(false) , m_notificationObserver(AdoptNS, [[WebCoreRenderThemeNotificationObserver alloc] initWithTheme:this]) { [[NSNotificationCenter defaultCenter] addObserver:m_notificationObserver.get() selector:@selector(systemColorsDidChange:) name:NSSystemColorsDidChangeNotification object:nil]; } RenderThemeMac::~RenderThemeMac() { [[NSNotificationCenter defaultCenter] removeObserver:m_notificationObserver.get()]; delete m_resizeCornerImage; } Color RenderThemeMac::platformActiveSelectionBackgroundColor() const { NSColor* color = [[NSColor selectedTextBackgroundColor] colorUsingColorSpaceName:NSDeviceRGBColorSpace]; return Color(static_cast(255.0 * [color redComponent]), static_cast(255.0 * [color greenComponent]), static_cast(255.0 * [color blueComponent])); } Color RenderThemeMac::platformInactiveSelectionBackgroundColor() const { NSColor* color = [[NSColor secondarySelectedControlColor] colorUsingColorSpaceName:NSDeviceRGBColorSpace]; return Color(static_cast(255.0 * [color redComponent]), static_cast(255.0 * [color greenComponent]), static_cast(255.0 * [color blueComponent])); } Color RenderThemeMac::activeListBoxSelectionBackgroundColor() const { NSColor* color = [[NSColor alternateSelectedControlColor] colorUsingColorSpaceName:NSDeviceRGBColorSpace]; return Color(static_cast(255.0 * [color redComponent]), static_cast(255.0 * [color greenComponent]), static_cast(255.0 * [color blueComponent])); } void RenderThemeMac::systemFont(int propId, FontDescription& fontDescription) const { static FontDescription systemFont; static FontDescription smallSystemFont; static FontDescription menuFont; static FontDescription labelFont; static FontDescription miniControlFont; static FontDescription smallControlFont; static FontDescription controlFont; FontDescription* cachedDesc; NSFont* font = nil; switch (propId) { case CSS_VAL_SMALL_CAPTION: cachedDesc = &smallSystemFont; if (!smallSystemFont.isAbsoluteSize()) font = [NSFont systemFontOfSize:[NSFont smallSystemFontSize]]; break; case CSS_VAL_MENU: cachedDesc = &menuFont; if (!menuFont.isAbsoluteSize()) font = [NSFont menuFontOfSize:[NSFont systemFontSize]]; break; case CSS_VAL_STATUS_BAR: cachedDesc = &labelFont; if (!labelFont.isAbsoluteSize()) font = [NSFont labelFontOfSize:[NSFont labelFontSize]]; break; case CSS_VAL__WEBKIT_MINI_CONTROL: cachedDesc = &miniControlFont; if (!miniControlFont.isAbsoluteSize()) font = [NSFont systemFontOfSize:[NSFont systemFontSizeForControlSize:NSMiniControlSize]]; break; case CSS_VAL__WEBKIT_SMALL_CONTROL: cachedDesc = &smallControlFont; if (!smallControlFont.isAbsoluteSize()) font = [NSFont systemFontOfSize:[NSFont systemFontSizeForControlSize:NSSmallControlSize]]; break; case CSS_VAL__WEBKIT_CONTROL: cachedDesc = &controlFont; if (!controlFont.isAbsoluteSize()) font = [NSFont systemFontOfSize:[NSFont systemFontSizeForControlSize:NSRegularControlSize]]; break; default: cachedDesc = &systemFont; if (!systemFont.isAbsoluteSize()) font = [NSFont systemFontOfSize:[NSFont systemFontSize]]; } if (font) { cachedDesc->setIsAbsoluteSize(true); cachedDesc->setGenericFamily(FontDescription::NoFamily); cachedDesc->firstFamily().setFamily([font familyName]); cachedDesc->setSpecifiedSize([font pointSize]); NSFontTraitMask traits = [[NSFontManager sharedFontManager] traitsOfFont:font]; cachedDesc->setBold(traits & NSBoldFontMask); cachedDesc->setItalic(traits & NSItalicFontMask); } fontDescription = *cachedDesc; } bool RenderThemeMac::isControlStyled(const RenderStyle* style, const BorderData& border, const BackgroundLayer& background, const Color& backgroundColor) const { if (style->appearance() == TextFieldAppearance || style->appearance() == TextAreaAppearance || style->appearance() == ListboxAppearance) return style->border() != border; return RenderTheme::isControlStyled(style, border, background, backgroundColor); } void RenderThemeMac::paintResizeControl(GraphicsContext* c, const IntRect& r) { Image* resizeCornerImage = this->resizeCornerImage(); IntPoint imagePoint(r.right() - resizeCornerImage->width(), r.bottom() - resizeCornerImage->height()); c->drawImage(resizeCornerImage, imagePoint); } void RenderThemeMac::adjustRepaintRect(const RenderObject* o, IntRect& r) { switch (o->style()->appearance()) { case CheckboxAppearance: { // Since we query the prototype cell, we need to update its state to match. setCheckboxCellState(o, r); // We inflate the rect as needed to account for padding included in the cell to accommodate the checkbox // shadow" and the check. We don't consider this part of the bounds of the control in WebKit. r = inflateRect(r, checkboxSizes()[[checkbox() controlSize]], checkboxMargins()); break; } case RadioAppearance: { // Since we query the prototype cell, we need to update its state to match. setRadioCellState(o, r); // We inflate the rect as needed to account for padding included in the cell to accommodate the checkbox // shadow" and the check. We don't consider this part of the bounds of the control in WebKit. r = inflateRect(r, radioSizes()[[radio() controlSize]], radioMargins()); break; } case PushButtonAppearance: case ButtonAppearance: { // Since we query the prototype cell, we need to update its state to match. setButtonCellState(o, r); // We inflate the rect as needed to account for padding included in the cell to accommodate the checkbox // shadow" and the check. We don't consider this part of the bounds of the control in WebKit. if ([button() bezelStyle] == NSRoundedBezelStyle) r = inflateRect(r, buttonSizes()[[button() controlSize]], buttonMargins()); break; } case MenulistAppearance: { setPopupButtonCellState(o, r); r = inflateRect(r, popupButtonSizes()[[popupButton() controlSize]], popupButtonMargins()); break; } default: break; } } IntRect RenderThemeMac::inflateRect(const IntRect& r, const IntSize& size, const int* margins) const { // Only do the inflation if the available width/height are too small. Otherwise try to // fit the glow/check space into the available box's width/height. int widthDelta = r.width() - (size.width() + margins[leftMargin] + margins[rightMargin]); int heightDelta = r.height() - (size.height() + margins[topMargin] + margins[bottomMargin]); IntRect result(r); if (widthDelta < 0) { result.setX(result.x() - margins[leftMargin]); result.setWidth(result.width() - widthDelta); } if (heightDelta < 0) { result.setY(result.y() - margins[topMargin]); result.setHeight(result.height() - heightDelta); } return result; } void RenderThemeMac::updateCheckedState(NSCell* cell, const RenderObject* o) { bool oldIndeterminate = [cell state] == NSMixedState; bool indeterminate = isIndeterminate(o); bool checked = isChecked(o); if (oldIndeterminate != indeterminate) { [cell setState:indeterminate ? NSMixedState : (checked ? NSOnState : NSOffState)]; return; } bool oldChecked = [cell state] == NSOnState; if (checked != oldChecked) [cell setState:checked ? NSOnState : NSOffState]; } void RenderThemeMac::updateEnabledState(NSCell* cell, const RenderObject* o) { bool oldEnabled = [cell isEnabled]; bool enabled = isEnabled(o); if (enabled != oldEnabled) [cell setEnabled:enabled]; } void RenderThemeMac::updateFocusedState(NSCell* cell, const RenderObject* o) { bool oldFocused = [cell showsFirstResponder]; bool focused = isFocused(o) && o->style()->outlineStyleIsAuto(); if (focused != oldFocused) [cell setShowsFirstResponder:focused]; } void RenderThemeMac::updatePressedState(NSCell* cell, const RenderObject* o) { bool oldPressed = [cell isHighlighted]; bool pressed = (o->element() && o->element()->active()); if (pressed != oldPressed) [cell setHighlighted:pressed]; } short RenderThemeMac::baselinePosition(const RenderObject* o) const { if (o->style()->appearance() == CheckboxAppearance || o->style()->appearance() == RadioAppearance) return o->marginTop() + o->height() - 2; // The baseline is 2px up from the bottom of the checkbox/radio in AppKit. return RenderTheme::baselinePosition(o); } bool RenderThemeMac::controlSupportsTints(const RenderObject* o) const { // An alternate way to implement this would be to get the appropriate cell object // and call the private _needRedrawOnWindowChangedKeyState method. An advantage of // that would be that we would match AppKit behavior more closely, but a disadvantage // would be that we would rely on an AppKit SPI method. if (!isEnabled(o)) return false; // Checkboxes only have tint when checked. if (o->style()->appearance() == CheckboxAppearance) return isChecked(o); // For now assume other controls have tint if enabled. return true; } NSControlSize RenderThemeMac::controlSizeForFont(RenderStyle* style) const { int fontSize = style->fontSize(); if (fontSize >= 16) return NSRegularControlSize; if (fontSize >= 11) return NSSmallControlSize; return NSMiniControlSize; } void RenderThemeMac::setControlSize(NSCell* cell, const IntSize* sizes, const IntSize& minSize) { NSControlSize size; if (minSize.width() >= sizes[NSRegularControlSize].width() && minSize.height() >= sizes[NSRegularControlSize].height()) size = NSRegularControlSize; else if (minSize.width() >= sizes[NSSmallControlSize].width() && minSize.height() >= sizes[NSSmallControlSize].height()) size = NSSmallControlSize; else size = NSMiniControlSize; if (size != [cell controlSize]) // Only update if we have to, since AppKit does work even if the size is the same. [cell setControlSize:size]; } IntSize RenderThemeMac::sizeForFont(RenderStyle* style, const IntSize* sizes) const { return sizes[controlSizeForFont(style)]; } IntSize RenderThemeMac::sizeForSystemFont(RenderStyle* style, const IntSize* sizes) const { return sizes[controlSizeForSystemFont(style)]; } void RenderThemeMac::setSizeFromFont(RenderStyle* style, const IntSize* sizes) const { // FIXME: Check is flawed, since it doesn't take min-width/max-width into account. IntSize size = sizeForFont(style, sizes); if (style->width().isIntrinsicOrAuto() && size.width() > 0) style->setWidth(Length(size.width(), Fixed)); if (style->height().isAuto() && size.height() > 0) style->setHeight(Length(size.height(), Fixed)); } void RenderThemeMac::setFontFromControlSize(CSSStyleSelector* selector, RenderStyle* style, NSControlSize controlSize) const { FontDescription fontDescription; fontDescription.setIsAbsoluteSize(true); fontDescription.setGenericFamily(FontDescription::SerifFamily); NSFont* font = [NSFont systemFontOfSize:[NSFont systemFontSizeForControlSize:controlSize]]; fontDescription.firstFamily().setFamily([font familyName]); fontDescription.setComputedSize([font pointSize]); fontDescription.setSpecifiedSize([font pointSize]); // Reset line height style->setLineHeight(RenderStyle::initialLineHeight()); if (style->setFontDescription(fontDescription)) style->font().update(); } NSControlSize RenderThemeMac::controlSizeForSystemFont(RenderStyle* style) const { int fontSize = style->fontSize(); if (fontSize >= [NSFont systemFontSizeForControlSize:NSRegularControlSize]) return NSRegularControlSize; if (fontSize >= [NSFont systemFontSizeForControlSize:NSSmallControlSize]) return NSSmallControlSize; return NSMiniControlSize; } bool RenderThemeMac::paintCheckbox(RenderObject* o, const RenderObject::PaintInfo&, const IntRect& r) { // Determine the width and height needed for the control and prepare the cell for painting. setCheckboxCellState(o, r); // We inflate the rect as needed to account for padding included in the cell to accommodate the checkbox // shadow" and the check. We don't consider this part of the bounds of the control in WebKit. NSButtonCell* checkbox = this->checkbox(); IntRect inflatedRect = inflateRect(r, checkboxSizes()[[checkbox controlSize]], checkboxMargins()); [checkbox drawWithFrame:NSRect(inflatedRect) inView:o->view()->frameView()->getDocumentView()]; [checkbox setControlView:nil]; return false; } const IntSize* RenderThemeMac::checkboxSizes() const { static const IntSize sizes[3] = { IntSize(14, 14), IntSize(12, 12), IntSize(10, 10) }; return sizes; } const int* RenderThemeMac::checkboxMargins() const { static const int margins[3][4] = { { 3, 4, 4, 2 }, { 4, 3, 3, 3 }, { 4, 3, 3, 3 }, }; return margins[[checkbox() controlSize]]; } void RenderThemeMac::setCheckboxCellState(const RenderObject* o, const IntRect& r) { NSButtonCell* checkbox = this->checkbox(); // Set the control size based off the rectangle we're painting into. setControlSize(checkbox, checkboxSizes(), r.size()); // Update the various states we respond to. updateCheckedState(checkbox, o); updateEnabledState(checkbox, o); updatePressedState(checkbox, o); updateFocusedState(checkbox, o); } void RenderThemeMac::setCheckboxSize(RenderStyle* style) const { // If the width and height are both specified, then we have nothing to do. if (!style->width().isIntrinsicOrAuto() && !style->height().isAuto()) return; // Use the font size to determine the intrinsic width of the control. setSizeFromFont(style, checkboxSizes()); } bool RenderThemeMac::paintRadio(RenderObject* o, const RenderObject::PaintInfo&, const IntRect& r) { // Determine the width and height needed for the control and prepare the cell for painting. setRadioCellState(o, r); // We inflate the rect as needed to account for padding included in the cell to accommodate the checkbox // shadow" and the check. We don't consider this part of the bounds of the control in WebKit. NSButtonCell* radio = this->radio(); IntRect inflatedRect = inflateRect(r, radioSizes()[[radio controlSize]], radioMargins()); [radio drawWithFrame:NSRect(inflatedRect) inView:o->view()->frameView()->getDocumentView()]; [radio setControlView:nil]; return false; } const IntSize* RenderThemeMac::radioSizes() const { static const IntSize sizes[3] = { IntSize(14, 15), IntSize(12, 13), IntSize(10, 10) }; return sizes; } const int* RenderThemeMac::radioMargins() const { static const int margins[3][4] = { { 2, 2, 4, 2 }, { 3, 2, 3, 2 }, { 1, 0, 2, 0 }, }; return margins[[radio() controlSize]]; } void RenderThemeMac::setRadioCellState(const RenderObject* o, const IntRect& r) { NSButtonCell* radio = this->radio(); // Set the control size based off the rectangle we're painting into. setControlSize(radio, radioSizes(), r.size()); // Update the various states we respond to. updateCheckedState(radio, o); updateEnabledState(radio, o); updatePressedState(radio, o); updateFocusedState(radio, o); } void RenderThemeMac::setRadioSize(RenderStyle* style) const { // If the width and height are both specified, then we have nothing to do. if (!style->width().isIntrinsicOrAuto() && !style->height().isAuto()) return; // Use the font size to determine the intrinsic width of the control. setSizeFromFont(style, radioSizes()); } void RenderThemeMac::setButtonPaddingFromControlSize(RenderStyle* style, NSControlSize size) const { // Just use 8px. AppKit wants to use 11px for mini buttons, but that padding is just too large // for real-world Web sites (creating a huge necessary minimum width for buttons whose space is // by definition constrained, since we select mini only for small cramped environments. // This also guarantees the HTML4