// Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "ui/views/controls/button/text_button.h" #include <algorithm> #include "base/logging.h" #include "grit/ui_resources.h" #include "ui/base/resource/resource_bundle.h" #include "ui/gfx/animation/throb_animation.h" #include "ui/gfx/canvas.h" #include "ui/gfx/image/image.h" #include "ui/views/controls/button/button.h" #include "ui/views/painter.h" #include "ui/views/widget/widget.h" #if defined(OS_WIN) #include "skia/ext/skia_utils_win.h" #include "ui/gfx/platform_font_win.h" #include "ui/native_theme/native_theme_win.h" #endif namespace views { namespace { // Default space between the icon and text. const int kDefaultIconTextSpacing = 5; // Preferred padding between text and edge. const int kPreferredPaddingHorizontal = 6; const int kPreferredPaddingVertical = 5; // Preferred padding between text and edge for NativeTheme border. const int kPreferredNativeThemePaddingHorizontal = 12; const int kPreferredNativeThemePaddingVertical = 5; // By default the focus rect is drawn at the border of the view. For a button, // we inset the focus rect by 3 pixels so that it doesn't draw on top of the // button's border. This roughly matches how the Windows native focus rect for // buttons looks. A subclass that draws a button with different padding may need // to provide a different focus painter and do something different. const int kFocusRectInset = 3; // How long the hover fade animation should last. const int kHoverAnimationDurationMs = 170; #if defined(OS_WIN) // These sizes are from http://msdn.microsoft.com/en-us/library/aa511279.aspx const int kMinWidthDLUs = 50; const int kMinHeightDLUs = 14; #endif // The default hot and pushed button image IDs; normal has none by default. const int kHotImages[] = IMAGE_GRID(IDR_TEXTBUTTON_HOVER); const int kPushedImages[] = IMAGE_GRID(IDR_TEXTBUTTON_PRESSED); } // namespace // static const char TextButtonBase::kViewClassName[] = "TextButtonBase"; // static const char TextButton::kViewClassName[] = "TextButton"; // TextButtonBorder ----------------------------------------------------------- TextButtonBorder::TextButtonBorder() { } TextButtonBorder::~TextButtonBorder() { } void TextButtonBorder::Paint(const View& view, gfx::Canvas* canvas) { } gfx::Insets TextButtonBorder::GetInsets() const { return insets_; } gfx::Size TextButtonBorder::GetMinimumSize() const { return gfx::Size(); } void TextButtonBorder::SetInsets(const gfx::Insets& insets) { insets_ = insets; } TextButtonBorder* TextButtonBorder::AsTextButtonBorder() { return this; } const TextButtonBorder* TextButtonBorder::AsTextButtonBorder() const { return this; } // TextButtonDefaultBorder ---------------------------------------------------- TextButtonDefaultBorder::TextButtonDefaultBorder() : vertical_padding_(kPreferredPaddingVertical) { set_hot_painter(Painter::CreateImageGridPainter(kHotImages)); set_pushed_painter(Painter::CreateImageGridPainter(kPushedImages)); SetInsets(gfx::Insets(vertical_padding_, kPreferredPaddingHorizontal, vertical_padding_, kPreferredPaddingHorizontal)); } TextButtonDefaultBorder::~TextButtonDefaultBorder() { } void TextButtonDefaultBorder::Paint(const View& view, gfx::Canvas* canvas) { const TextButton* button = static_cast<const TextButton*>(&view); int state = button->state(); bool animating = button->GetAnimation()->is_animating(); Painter* painter = normal_painter_.get(); // Use the hot painter when we're hovered. Also use the hot painter when we're // STATE_NORMAL and |animating| so that we show throb animations started from // CustomButton::StartThrobbing which should start throbbing the button // regardless of whether it is hovered. if (button->show_multiple_icon_states() && ((state == TextButton::STATE_HOVERED) || (state == TextButton::STATE_PRESSED) || ((state == TextButton::STATE_NORMAL) && animating))) { painter = (state == TextButton::STATE_PRESSED) ? pushed_painter_.get() : hot_painter_.get(); } if (painter) { if (animating) { // TODO(pkasting): Really this should crossfade between states so it could // handle the case of having a non-NULL |normal_painter_|. canvas->SaveLayerAlpha(static_cast<uint8>( button->GetAnimation()->CurrentValueBetween(0, 255))); painter->Paint(canvas, view.size()); canvas->Restore(); } else { painter->Paint(canvas, view.size()); } } } gfx::Size TextButtonDefaultBorder::GetMinimumSize() const { gfx::Size size; if (normal_painter_) size.SetToMax(normal_painter_->GetMinimumSize()); if (hot_painter_) size.SetToMax(hot_painter_->GetMinimumSize()); if (pushed_painter_) size.SetToMax(pushed_painter_->GetMinimumSize()); return size; } // TextButtonNativeThemeBorder ------------------------------------------------ TextButtonNativeThemeBorder::TextButtonNativeThemeBorder( NativeThemeDelegate* delegate) : delegate_(delegate) { SetInsets(gfx::Insets(kPreferredNativeThemePaddingVertical, kPreferredNativeThemePaddingHorizontal, kPreferredNativeThemePaddingVertical, kPreferredNativeThemePaddingHorizontal)); } TextButtonNativeThemeBorder::~TextButtonNativeThemeBorder() { } void TextButtonNativeThemeBorder::Paint(const View& view, gfx::Canvas* canvas) { const ui::NativeTheme* theme = view.GetNativeTheme(); const TextButtonBase* tb = static_cast<const TextButton*>(&view); ui::NativeTheme::Part part = delegate_->GetThemePart(); gfx::Rect rect(delegate_->GetThemePaintRect()); if (tb->show_multiple_icon_states() && delegate_->GetThemeAnimation() != NULL && delegate_->GetThemeAnimation()->is_animating()) { // Paint background state. ui::NativeTheme::ExtraParams prev_extra; ui::NativeTheme::State prev_state = delegate_->GetBackgroundThemeState(&prev_extra); theme->Paint(canvas->sk_canvas(), part, prev_state, rect, prev_extra); // Composite foreground state above it. ui::NativeTheme::ExtraParams extra; ui::NativeTheme::State state = delegate_->GetForegroundThemeState(&extra); int alpha = delegate_->GetThemeAnimation()->CurrentValueBetween(0, 255); canvas->SaveLayerAlpha(static_cast<uint8>(alpha)); theme->Paint(canvas->sk_canvas(), part, state, rect, extra); canvas->Restore(); } else { ui::NativeTheme::ExtraParams extra; ui::NativeTheme::State state = delegate_->GetThemeState(&extra); theme->Paint(canvas->sk_canvas(), part, state, rect, extra); } } // TextButtonBase ------------------------------------------------------------- TextButtonBase::TextButtonBase(ButtonListener* listener, const string16& text) : CustomButton(listener), alignment_(ALIGN_LEFT), font_(ResourceBundle::GetSharedInstance().GetFont( ResourceBundle::BaseFont)), has_text_shadow_(false), active_text_shadow_color_(0), inactive_text_shadow_color_(0), text_shadow_offset_(gfx::Point(1, 1)), min_width_(0), min_height_(0), max_width_(0), show_multiple_icon_states_(true), is_default_(false), multi_line_(false), use_enabled_color_from_theme_(true), use_disabled_color_from_theme_(true), use_highlight_color_from_theme_(true), use_hover_color_from_theme_(true), focus_painter_(Painter::CreateDashedFocusPainter()) { SetText(text); // OnNativeThemeChanged sets the color member variables. TextButtonBase::OnNativeThemeChanged(GetNativeTheme()); SetAnimationDuration(kHoverAnimationDurationMs); } TextButtonBase::~TextButtonBase() { } void TextButtonBase::SetIsDefault(bool is_default) { if (is_default == is_default_) return; is_default_ = is_default; if (is_default_) AddAccelerator(ui::Accelerator(ui::VKEY_RETURN, ui::EF_NONE)); else RemoveAccelerator(ui::Accelerator(ui::VKEY_RETURN, ui::EF_NONE)); SchedulePaint(); } void TextButtonBase::SetText(const string16& text) { if (text == text_) return; text_ = text; SetAccessibleName(text); UpdateTextSize(); } void TextButtonBase::SetFont(const gfx::Font& font) { font_ = font; UpdateTextSize(); } void TextButtonBase::SetEnabledColor(SkColor color) { color_enabled_ = color; use_enabled_color_from_theme_ = false; UpdateColor(); } void TextButtonBase::SetDisabledColor(SkColor color) { color_disabled_ = color; use_disabled_color_from_theme_ = false; UpdateColor(); } void TextButtonBase::SetHighlightColor(SkColor color) { color_highlight_ = color; use_highlight_color_from_theme_ = false; } void TextButtonBase::SetHoverColor(SkColor color) { color_hover_ = color; use_hover_color_from_theme_ = false; } void TextButtonBase::SetTextShadowColors(SkColor active_color, SkColor inactive_color) { active_text_shadow_color_ = active_color; inactive_text_shadow_color_ = inactive_color; has_text_shadow_ = true; } void TextButtonBase::SetTextShadowOffset(int x, int y) { text_shadow_offset_.SetPoint(x, y); } void TextButtonBase::ClearEmbellishing() { has_text_shadow_ = false; } void TextButtonBase::ClearMaxTextSize() { max_text_size_ = text_size_; } void TextButtonBase::SetShowMultipleIconStates(bool show_multiple_icon_states) { show_multiple_icon_states_ = show_multiple_icon_states; } void TextButtonBase::SetMultiLine(bool multi_line) { if (multi_line != multi_line_) { multi_line_ = multi_line; max_text_size_.SetSize(0, 0); UpdateTextSize(); SchedulePaint(); } } gfx::Size TextButtonBase::GetPreferredSize() { gfx::Insets insets = GetInsets(); // Use the max size to set the button boundaries. // In multiline mode max size can be undefined while // width() is 0, so max it out with current text size. gfx::Size prefsize(std::max(max_text_size_.width(), text_size_.width()) + insets.width(), std::max(max_text_size_.height(), text_size_.height()) + insets.height()); if (max_width_ > 0) prefsize.set_width(std::min(max_width_, prefsize.width())); prefsize.set_width(std::max(prefsize.width(), min_width_)); prefsize.set_height(std::max(prefsize.height(), min_height_)); return prefsize; } int TextButtonBase::GetHeightForWidth(int w) { if (!multi_line_) return View::GetHeightForWidth(w); if (max_width_ > 0) w = std::min(max_width_, w); gfx::Size text_size; CalculateTextSize(&text_size, w); int height = text_size.height() + GetInsets().height(); return std::max(height, min_height_); } void TextButtonBase::OnPaint(gfx::Canvas* canvas) { PaintButton(canvas, PB_NORMAL); } void TextButtonBase::OnBoundsChanged(const gfx::Rect& previous_bounds) { if (multi_line_) UpdateTextSize(); } const gfx::Animation* TextButtonBase::GetAnimation() const { return hover_animation_.get(); } void TextButtonBase::UpdateColor() { color_ = enabled() ? color_enabled_ : color_disabled_; } void TextButtonBase::UpdateTextSize() { int text_width = width(); // If width is defined, use GetTextBounds.width() for maximum text width, // as it will take size of checkbox/radiobutton into account. if (text_width != 0) { gfx::Rect text_bounds = GetTextBounds(); text_width = text_bounds.width(); } CalculateTextSize(&text_size_, text_width); // Before layout width() is 0, and multiline text will be treated as one line. // Do not store max_text_size in this case. UpdateTextSize will be called // again once width() changes. if (!multi_line_ || text_width != 0) { max_text_size_.SetSize(std::max(max_text_size_.width(), text_size_.width()), std::max(max_text_size_.height(), text_size_.height())); PreferredSizeChanged(); } } void TextButtonBase::CalculateTextSize(gfx::Size* text_size, int max_width) { int h = font_.GetHeight(); int w = multi_line_ ? max_width : 0; int flags = ComputeCanvasStringFlags(); if (!multi_line_) flags |= gfx::Canvas::NO_ELLIPSIS; gfx::Canvas::SizeStringInt(text_, font_, &w, &h, 0, flags); text_size->SetSize(w, h); } int TextButtonBase::ComputeCanvasStringFlags() const { if (!multi_line_) return 0; int flags = gfx::Canvas::MULTI_LINE; switch (alignment_) { case ALIGN_LEFT: flags |= gfx::Canvas::TEXT_ALIGN_LEFT; break; case ALIGN_RIGHT: flags |= gfx::Canvas::TEXT_ALIGN_RIGHT; break; case ALIGN_CENTER: flags |= gfx::Canvas::TEXT_ALIGN_CENTER; break; } return flags; } void TextButtonBase::OnFocus() { View::OnFocus(); if (focus_painter_) SchedulePaint(); } void TextButtonBase::OnBlur() { View::OnBlur(); if (focus_painter_) SchedulePaint(); } void TextButtonBase::GetExtraParams( ui::NativeTheme::ExtraParams* params) const { params->button.checked = false; params->button.indeterminate = false; params->button.is_default = false; params->button.is_focused = false; params->button.has_border = false; params->button.classic_state = 0; params->button.background_color = GetNativeTheme()->GetSystemColor( ui::NativeTheme::kColorId_ButtonBackgroundColor); } gfx::Rect TextButtonBase::GetContentBounds(int extra_width) const { gfx::Insets insets = GetInsets(); int available_width = width() - insets.width(); int content_width = text_size_.width() + extra_width; int content_x = 0; switch(alignment_) { case ALIGN_LEFT: content_x = insets.left(); break; case ALIGN_RIGHT: content_x = width() - insets.right() - content_width; if (content_x < insets.left()) content_x = insets.left(); break; case ALIGN_CENTER: content_x = insets.left() + std::max(0, (available_width - content_width) / 2); break; } content_width = std::min(content_width, width() - insets.right() - content_x); int available_height = height() - insets.height(); int content_y = (available_height - text_size_.height()) / 2 + insets.top(); gfx::Rect bounds(content_x, content_y, content_width, text_size_.height()); return bounds; } gfx::Rect TextButtonBase::GetTextBounds() const { return GetContentBounds(0); } void TextButtonBase::SetFocusPainter(scoped_ptr<Painter> focus_painter) { focus_painter_ = focus_painter.Pass(); } void TextButtonBase::PaintButton(gfx::Canvas* canvas, PaintButtonMode mode) { if (mode == PB_NORMAL) { OnPaintBackground(canvas); OnPaintBorder(canvas); Painter::PaintFocusPainter(this, canvas, focus_painter_.get()); } gfx::Rect text_bounds(GetTextBounds()); if (text_bounds.width() > 0) { // Because the text button can (at times) draw multiple elements on the // canvas, we can not mirror the button by simply flipping the canvas as // doing this will mirror the text itself. Flipping the canvas will also // make the icons look wrong because icons are almost always represented as // direction-insensitive images and such images should never be flipped // horizontally. // // Due to the above, we must perform the flipping manually for RTL UIs. text_bounds.set_x(GetMirroredXForRect(text_bounds)); SkColor text_color = (show_multiple_icon_states_ && (state() == STATE_HOVERED || state() == STATE_PRESSED)) ? color_hover_ : color_; int draw_string_flags = gfx::Canvas::DefaultCanvasTextAlignment() | ComputeCanvasStringFlags(); if (mode == PB_FOR_DRAG) { // Disable sub-pixel rendering as background is transparent. draw_string_flags |= gfx::Canvas::NO_SUBPIXEL_RENDERING; #if defined(OS_WIN) // TODO(erg): Either port DrawStringWithHalo to linux or find an // alternative here. canvas->DrawStringWithHalo(text_, font_, SK_ColorBLACK, SK_ColorWHITE, text_bounds.x(), text_bounds.y(), text_bounds.width(), text_bounds.height(), draw_string_flags); #else canvas->DrawStringInt(text_, font_, text_color, text_bounds.x(), text_bounds.y(), text_bounds.width(), text_bounds.height(), draw_string_flags); #endif } else { gfx::ShadowValues shadows; if (has_text_shadow_) { SkColor color = GetWidget()->IsActive() ? active_text_shadow_color_ : inactive_text_shadow_color_; shadows.push_back(gfx::ShadowValue(text_shadow_offset_, 0, color)); } canvas->DrawStringWithShadows(text_, font_, text_color, text_bounds, 0, draw_string_flags, shadows); } } } gfx::Size TextButtonBase::GetMinimumSize() { return max_text_size_; } void TextButtonBase::OnEnabledChanged() { // We should always call UpdateColor() since the state of the button might be // changed by other functions like CustomButton::SetState(). UpdateColor(); CustomButton::OnEnabledChanged(); } const char* TextButtonBase::GetClassName() const { return kViewClassName; } void TextButtonBase::OnNativeThemeChanged(const ui::NativeTheme* theme) { if (use_enabled_color_from_theme_) { color_enabled_ = theme->GetSystemColor( ui::NativeTheme::kColorId_ButtonEnabledColor); } if (use_disabled_color_from_theme_) { color_disabled_ = theme->GetSystemColor( ui::NativeTheme::kColorId_ButtonDisabledColor); } if (use_highlight_color_from_theme_) { color_highlight_ = theme->GetSystemColor( ui::NativeTheme::kColorId_ButtonHighlightColor); } if (use_hover_color_from_theme_) { color_hover_ = theme->GetSystemColor( ui::NativeTheme::kColorId_ButtonHoverColor); } UpdateColor(); } gfx::Rect TextButtonBase::GetThemePaintRect() const { return GetLocalBounds(); } ui::NativeTheme::State TextButtonBase::GetThemeState( ui::NativeTheme::ExtraParams* params) const { GetExtraParams(params); switch(state()) { case STATE_DISABLED: return ui::NativeTheme::kDisabled; case STATE_NORMAL: return ui::NativeTheme::kNormal; case STATE_HOVERED: return ui::NativeTheme::kHovered; case STATE_PRESSED: return ui::NativeTheme::kPressed; default: NOTREACHED() << "Unknown state: " << state(); return ui::NativeTheme::kNormal; } } const gfx::Animation* TextButtonBase::GetThemeAnimation() const { #if defined(OS_WIN) if (GetNativeTheme() == ui::NativeThemeWin::instance()) { return ui::NativeThemeWin::instance()->IsThemingActive() ? hover_animation_.get() : NULL; } #endif return hover_animation_.get(); } ui::NativeTheme::State TextButtonBase::GetBackgroundThemeState( ui::NativeTheme::ExtraParams* params) const { GetExtraParams(params); return ui::NativeTheme::kNormal; } ui::NativeTheme::State TextButtonBase::GetForegroundThemeState( ui::NativeTheme::ExtraParams* params) const { GetExtraParams(params); return ui::NativeTheme::kHovered; } // TextButton ----------------------------------------------------------------- TextButton::TextButton(ButtonListener* listener, const string16& text) : TextButtonBase(listener, text), icon_placement_(ICON_ON_LEFT), has_hover_icon_(false), has_pushed_icon_(false), icon_text_spacing_(kDefaultIconTextSpacing), ignore_minimum_size_(true) { set_border(new TextButtonDefaultBorder); SetFocusPainter(Painter::CreateDashedFocusPainterWithInsets( gfx::Insets(kFocusRectInset, kFocusRectInset, kFocusRectInset, kFocusRectInset))); } TextButton::~TextButton() { } void TextButton::SetIcon(const gfx::ImageSkia& icon) { icon_ = icon; SchedulePaint(); } void TextButton::SetHoverIcon(const gfx::ImageSkia& icon) { icon_hover_ = icon; has_hover_icon_ = true; SchedulePaint(); } void TextButton::SetPushedIcon(const gfx::ImageSkia& icon) { icon_pushed_ = icon; has_pushed_icon_ = true; SchedulePaint(); } gfx::Size TextButton::GetPreferredSize() { gfx::Size prefsize(TextButtonBase::GetPreferredSize()); prefsize.Enlarge(icon_.width(), 0); prefsize.set_height(std::max(prefsize.height(), icon_.height())); // Use the max size to set the button boundaries. if (icon_.width() > 0 && !text_.empty()) prefsize.Enlarge(icon_text_spacing_, 0); if (max_width_ > 0) prefsize.set_width(std::min(max_width_, prefsize.width())); #if defined(OS_WIN) // Clamp the size returned to at least the minimum size. if (!ignore_minimum_size_) { gfx::PlatformFontWin* platform_font = static_cast<gfx::PlatformFontWin*>(font_.platform_font()); prefsize.set_width(std::max( prefsize.width(), platform_font->horizontal_dlus_to_pixels(kMinWidthDLUs))); prefsize.set_height(std::max( prefsize.height(), platform_font->vertical_dlus_to_pixels(kMinHeightDLUs))); } #endif prefsize.set_width(std::max(prefsize.width(), min_width_)); prefsize.set_height(std::max(prefsize.height(), min_height_)); return prefsize; } void TextButton::PaintButton(gfx::Canvas* canvas, PaintButtonMode mode) { TextButtonBase::PaintButton(canvas, mode); const gfx::ImageSkia& icon = GetImageToPaint(); if (icon.width() > 0) { gfx::Rect text_bounds = GetTextBounds(); int icon_x; int spacing = text_.empty() ? 0 : icon_text_spacing_; gfx::Insets insets = GetInsets(); if (icon_placement_ == ICON_ON_LEFT) { icon_x = text_bounds.x() - icon.width() - spacing; } else if (icon_placement_ == ICON_ON_RIGHT) { icon_x = text_bounds.right() + spacing; } else { // ICON_CENTERED DCHECK(text_.empty()); icon_x = (width() - insets.width() - icon.width()) / 2 + insets.left(); } int available_height = height() - insets.height(); int icon_y = (available_height - icon.height()) / 2 + insets.top(); // Mirroring the icon position if necessary. gfx::Rect icon_bounds(icon_x, icon_y, icon.width(), icon.height()); icon_bounds.set_x(GetMirroredXForRect(icon_bounds)); canvas->DrawImageInt(icon, icon_bounds.x(), icon_bounds.y()); } } void TextButton::set_ignore_minimum_size(bool ignore_minimum_size) { ignore_minimum_size_ = ignore_minimum_size; } const char* TextButton::GetClassName() const { return kViewClassName; } ui::NativeTheme::Part TextButton::GetThemePart() const { return ui::NativeTheme::kPushButton; } void TextButton::GetExtraParams(ui::NativeTheme::ExtraParams* params) const { TextButtonBase::GetExtraParams(params); params->button.is_default = is_default_; } gfx::Rect TextButton::GetTextBounds() const { int extra_width = 0; const gfx::ImageSkia& icon = GetImageToPaint(); if (icon.width() > 0) extra_width = icon.width() + (text_.empty() ? 0 : icon_text_spacing_); gfx::Rect bounds(GetContentBounds(extra_width)); if (extra_width > 0) { // Make sure the icon is always fully visible. if (icon_placement_ == ICON_ON_LEFT) { bounds.Inset(extra_width, 0, 0, 0); } else if (icon_placement_ == ICON_ON_RIGHT) { bounds.Inset(0, 0, extra_width, 0); } } return bounds; } const gfx::ImageSkia& TextButton::GetImageToPaint() const { if (show_multiple_icon_states_) { if (has_hover_icon_ && (state() == STATE_HOVERED)) return icon_hover_; if (has_pushed_icon_ && (state() == STATE_PRESSED)) return icon_pushed_; } return icon_; } } // namespace views