1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #include "ui/views/controls/button/text_button.h"
9 #include "base/logging.h"
10 #include "grit/ui_resources.h"
11 #include "ui/base/resource/resource_bundle.h"
12 #include "ui/gfx/animation/throb_animation.h"
13 #include "ui/gfx/canvas.h"
14 #include "ui/gfx/image/image.h"
15 #include "ui/views/controls/button/button.h"
16 #include "ui/views/painter.h"
17 #include "ui/views/widget/widget.h"
20 #include "skia/ext/skia_utils_win.h"
21 #include "ui/gfx/platform_font_win.h"
22 #include "ui/native_theme/native_theme_win.h"
29 // Default space between the icon and text.
30 const int kDefaultIconTextSpacing
= 5;
32 // Preferred padding between text and edge.
33 const int kPreferredPaddingHorizontal
= 6;
34 const int kPreferredPaddingVertical
= 5;
36 // Preferred padding between text and edge for NativeTheme border.
37 const int kPreferredNativeThemePaddingHorizontal
= 12;
38 const int kPreferredNativeThemePaddingVertical
= 5;
40 // By default the focus rect is drawn at the border of the view. For a button,
41 // we inset the focus rect by 3 pixels so that it doesn't draw on top of the
42 // button's border. This roughly matches how the Windows native focus rect for
43 // buttons looks. A subclass that draws a button with different padding may need
44 // to provide a different focus painter and do something different.
45 const int kFocusRectInset
= 3;
47 // How long the hover fade animation should last.
48 const int kHoverAnimationDurationMs
= 170;
51 // These sizes are from http://msdn.microsoft.com/en-us/library/aa511279.aspx
52 const int kMinWidthDLUs
= 50;
53 const int kMinHeightDLUs
= 14;
56 // The default hot and pushed button image IDs; normal has none by default.
57 const int kHotImages
[] = IMAGE_GRID(IDR_TEXTBUTTON_HOVER
);
58 const int kPushedImages
[] = IMAGE_GRID(IDR_TEXTBUTTON_PRESSED
);
63 const char TextButtonBase::kViewClassName
[] = "TextButtonBase";
65 const char TextButton::kViewClassName
[] = "TextButton";
68 // TextButtonBorder -----------------------------------------------------------
70 TextButtonBorder::TextButtonBorder() {
73 TextButtonBorder::~TextButtonBorder() {
76 void TextButtonBorder::Paint(const View
& view
, gfx::Canvas
* canvas
) {
79 gfx::Insets
TextButtonBorder::GetInsets() const {
83 gfx::Size
TextButtonBorder::GetMinimumSize() const {
87 void TextButtonBorder::SetInsets(const gfx::Insets
& insets
) {
92 // TextButtonDefaultBorder ----------------------------------------------------
94 TextButtonDefaultBorder::TextButtonDefaultBorder()
95 : vertical_padding_(kPreferredPaddingVertical
) {
96 set_hot_painter(Painter::CreateImageGridPainter(kHotImages
));
97 set_pushed_painter(Painter::CreateImageGridPainter(kPushedImages
));
98 SetInsets(gfx::Insets(vertical_padding_
, kPreferredPaddingHorizontal
,
99 vertical_padding_
, kPreferredPaddingHorizontal
));
102 TextButtonDefaultBorder::~TextButtonDefaultBorder() {
105 void TextButtonDefaultBorder::Paint(const View
& view
, gfx::Canvas
* canvas
) {
106 const TextButton
* button
= static_cast<const TextButton
*>(&view
);
107 int state
= button
->state();
108 bool animating
= button
->GetAnimation()->is_animating();
110 Painter
* painter
= normal_painter_
.get();
111 // Use the hot painter when we're hovered. Also use the hot painter when we're
112 // STATE_NORMAL and |animating| so that we show throb animations started from
113 // CustomButton::StartThrobbing which should start throbbing the button
114 // regardless of whether it is hovered.
115 if (button
->show_multiple_icon_states() &&
116 ((state
== TextButton::STATE_HOVERED
) ||
117 (state
== TextButton::STATE_PRESSED
) ||
118 ((state
== TextButton::STATE_NORMAL
) && animating
))) {
119 painter
= (state
== TextButton::STATE_PRESSED
) ?
120 pushed_painter_
.get() : hot_painter_
.get();
124 // TODO(pkasting): Really this should crossfade between states so it could
125 // handle the case of having a non-NULL |normal_painter_|.
126 canvas
->SaveLayerAlpha(static_cast<uint8
>(
127 button
->GetAnimation()->CurrentValueBetween(0, 255)));
128 painter
->Paint(canvas
, view
.size());
131 painter
->Paint(canvas
, view
.size());
136 gfx::Size
TextButtonDefaultBorder::GetMinimumSize() const {
139 size
.SetToMax(normal_painter_
->GetMinimumSize());
141 size
.SetToMax(hot_painter_
->GetMinimumSize());
143 size
.SetToMax(pushed_painter_
->GetMinimumSize());
148 // TextButtonNativeThemeBorder ------------------------------------------------
150 TextButtonNativeThemeBorder::TextButtonNativeThemeBorder(
151 NativeThemeDelegate
* delegate
)
152 : delegate_(delegate
) {
153 SetInsets(gfx::Insets(kPreferredNativeThemePaddingVertical
,
154 kPreferredNativeThemePaddingHorizontal
,
155 kPreferredNativeThemePaddingVertical
,
156 kPreferredNativeThemePaddingHorizontal
));
159 TextButtonNativeThemeBorder::~TextButtonNativeThemeBorder() {
162 void TextButtonNativeThemeBorder::Paint(const View
& view
, gfx::Canvas
* canvas
) {
163 const ui::NativeTheme
* theme
= view
.GetNativeTheme();
164 const TextButtonBase
* tb
= static_cast<const TextButton
*>(&view
);
165 ui::NativeTheme::Part part
= delegate_
->GetThemePart();
166 gfx::Rect
rect(delegate_
->GetThemePaintRect());
168 if (tb
->show_multiple_icon_states() &&
169 delegate_
->GetThemeAnimation() != NULL
&&
170 delegate_
->GetThemeAnimation()->is_animating()) {
171 // Paint background state.
172 ui::NativeTheme::ExtraParams prev_extra
;
173 ui::NativeTheme::State prev_state
=
174 delegate_
->GetBackgroundThemeState(&prev_extra
);
175 theme
->Paint(canvas
->sk_canvas(), part
, prev_state
, rect
, prev_extra
);
177 // Composite foreground state above it.
178 ui::NativeTheme::ExtraParams extra
;
179 ui::NativeTheme::State state
= delegate_
->GetForegroundThemeState(&extra
);
180 int alpha
= delegate_
->GetThemeAnimation()->CurrentValueBetween(0, 255);
181 canvas
->SaveLayerAlpha(static_cast<uint8
>(alpha
));
182 theme
->Paint(canvas
->sk_canvas(), part
, state
, rect
, extra
);
185 ui::NativeTheme::ExtraParams extra
;
186 ui::NativeTheme::State state
= delegate_
->GetThemeState(&extra
);
187 theme
->Paint(canvas
->sk_canvas(), part
, state
, rect
, extra
);
192 // TextButtonBase -------------------------------------------------------------
194 TextButtonBase::TextButtonBase(ButtonListener
* listener
,
195 const base::string16
& text
)
196 : CustomButton(listener
),
197 alignment_(ALIGN_LEFT
),
201 show_multiple_icon_states_(true),
204 use_enabled_color_from_theme_(true),
205 use_disabled_color_from_theme_(true),
206 use_highlight_color_from_theme_(true),
207 use_hover_color_from_theme_(true),
208 focus_painter_(Painter::CreateDashedFocusPainter()) {
210 SetAnimationDuration(kHoverAnimationDurationMs
);
213 TextButtonBase::~TextButtonBase() {
216 void TextButtonBase::SetIsDefault(bool is_default
) {
217 if (is_default
== is_default_
)
219 is_default_
= is_default
;
221 AddAccelerator(ui::Accelerator(ui::VKEY_RETURN
, ui::EF_NONE
));
223 RemoveAccelerator(ui::Accelerator(ui::VKEY_RETURN
, ui::EF_NONE
));
227 void TextButtonBase::SetText(const base::string16
& text
) {
231 SetAccessibleName(text
);
235 void TextButtonBase::SetFontList(const gfx::FontList
& font_list
) {
236 font_list_
= font_list
;
240 void TextButtonBase::SetEnabledColor(SkColor color
) {
241 color_enabled_
= color
;
242 use_enabled_color_from_theme_
= false;
246 void TextButtonBase::SetDisabledColor(SkColor color
) {
247 color_disabled_
= color
;
248 use_disabled_color_from_theme_
= false;
252 void TextButtonBase::SetHighlightColor(SkColor color
) {
253 color_highlight_
= color
;
254 use_highlight_color_from_theme_
= false;
257 void TextButtonBase::SetHoverColor(SkColor color
) {
258 color_hover_
= color
;
259 use_hover_color_from_theme_
= false;
262 void TextButtonBase::ClearMaxTextSize() {
263 max_text_size_
= text_size_
;
266 void TextButtonBase::SetShowMultipleIconStates(bool show_multiple_icon_states
) {
267 show_multiple_icon_states_
= show_multiple_icon_states
;
270 void TextButtonBase::SetMultiLine(bool multi_line
) {
271 if (multi_line
!= multi_line_
) {
272 multi_line_
= multi_line
;
273 max_text_size_
.SetSize(0, 0);
279 gfx::Size
TextButtonBase::GetPreferredSize() const {
280 gfx::Insets insets
= GetInsets();
282 // Use the max size to set the button boundaries.
283 // In multiline mode max size can be undefined while
284 // width() is 0, so max it out with current text size.
285 gfx::Size
prefsize(std::max(max_text_size_
.width(),
286 text_size_
.width()) + insets
.width(),
287 std::max(max_text_size_
.height(),
288 text_size_
.height()) + insets
.height());
291 prefsize
.set_width(std::min(max_width_
, prefsize
.width()));
293 prefsize
.set_width(std::max(prefsize
.width(), min_width_
));
294 prefsize
.set_height(std::max(prefsize
.height(), min_height_
));
299 int TextButtonBase::GetHeightForWidth(int w
) const {
301 return View::GetHeightForWidth(w
);
304 w
= std::min(max_width_
, w
);
307 CalculateTextSize(&text_size
, w
);
308 int height
= text_size
.height() + GetInsets().height();
310 return std::max(height
, min_height_
);
313 void TextButtonBase::OnPaint(gfx::Canvas
* canvas
) {
314 PaintButton(canvas
, PB_NORMAL
);
317 void TextButtonBase::OnBoundsChanged(const gfx::Rect
& previous_bounds
) {
322 const gfx::Animation
* TextButtonBase::GetAnimation() const {
323 return hover_animation_
.get();
326 void TextButtonBase::UpdateColor() {
327 color_
= enabled() ? color_enabled_
: color_disabled_
;
330 void TextButtonBase::UpdateTextSize() {
331 int text_width
= width();
332 // If width is defined, use GetTextBounds.width() for maximum text width,
333 // as it will take size of checkbox/radiobutton into account.
334 if (text_width
!= 0) {
335 gfx::Rect text_bounds
= GetTextBounds();
336 text_width
= text_bounds
.width();
338 CalculateTextSize(&text_size_
, text_width
);
339 // Before layout width() is 0, and multiline text will be treated as one line.
340 // Do not store max_text_size in this case. UpdateTextSize will be called
341 // again once width() changes.
342 if (!multi_line_
|| text_width
!= 0) {
343 max_text_size_
.SetSize(std::max(max_text_size_
.width(), text_size_
.width()),
344 std::max(max_text_size_
.height(),
345 text_size_
.height()));
346 PreferredSizeChanged();
350 void TextButtonBase::CalculateTextSize(gfx::Size
* text_size
,
351 int max_width
) const {
352 int h
= font_list_
.GetHeight();
353 int w
= multi_line_
? max_width
: 0;
354 int flags
= ComputeCanvasStringFlags();
356 flags
|= gfx::Canvas::NO_ELLIPSIS
;
358 gfx::Canvas::SizeStringInt(text_
, font_list_
, &w
, &h
, 0, flags
);
359 text_size
->SetSize(w
, h
);
362 void TextButtonBase::OnPaintText(gfx::Canvas
* canvas
, PaintButtonMode mode
) {
363 gfx::Rect
text_bounds(GetTextBounds());
364 if (text_bounds
.width() > 0) {
365 // Because the text button can (at times) draw multiple elements on the
366 // canvas, we can not mirror the button by simply flipping the canvas as
367 // doing this will mirror the text itself. Flipping the canvas will also
368 // make the icons look wrong because icons are almost always represented as
369 // direction-insensitive images and such images should never be flipped
372 // Due to the above, we must perform the flipping manually for RTL UIs.
373 text_bounds
.set_x(GetMirroredXForRect(text_bounds
));
375 SkColor text_color
= (show_multiple_icon_states_
&&
376 (state() == STATE_HOVERED
|| state() == STATE_PRESSED
)) ?
377 color_hover_
: color_
;
379 int draw_string_flags
= gfx::Canvas::DefaultCanvasTextAlignment() |
380 ComputeCanvasStringFlags();
382 if (mode
== PB_FOR_DRAG
) {
383 // Disable sub-pixel rendering as background is transparent.
384 draw_string_flags
|= gfx::Canvas::NO_SUBPIXEL_RENDERING
;
385 canvas
->DrawStringRectWithHalo(text_
, font_list_
,
386 SK_ColorBLACK
, SK_ColorWHITE
,
387 text_bounds
, draw_string_flags
);
389 canvas
->DrawStringRectWithFlags(text_
, font_list_
, text_color
,
390 text_bounds
, draw_string_flags
);
395 int TextButtonBase::ComputeCanvasStringFlags() const {
399 int flags
= gfx::Canvas::MULTI_LINE
;
400 switch (alignment_
) {
402 flags
|= gfx::Canvas::TEXT_ALIGN_LEFT
;
405 flags
|= gfx::Canvas::TEXT_ALIGN_RIGHT
;
408 flags
|= gfx::Canvas::TEXT_ALIGN_CENTER
;
414 void TextButtonBase::OnFocus() {
420 void TextButtonBase::OnBlur() {
426 void TextButtonBase::GetExtraParams(
427 ui::NativeTheme::ExtraParams
* params
) const {
428 params
->button
.checked
= false;
429 params
->button
.indeterminate
= false;
430 params
->button
.is_default
= false;
431 params
->button
.is_focused
= false;
432 params
->button
.has_border
= false;
433 params
->button
.classic_state
= 0;
434 params
->button
.background_color
=
435 GetNativeTheme()->GetSystemColor(
436 ui::NativeTheme::kColorId_ButtonBackgroundColor
);
439 gfx::Rect
TextButtonBase::GetContentBounds(int extra_width
) const {
440 gfx::Insets insets
= GetInsets();
441 int available_width
= width() - insets
.width();
442 int content_width
= text_size_
.width() + extra_width
;
446 content_x
= insets
.left();
449 content_x
= width() - insets
.right() - content_width
;
450 if (content_x
< insets
.left())
451 content_x
= insets
.left();
454 content_x
= insets
.left() + std::max(0,
455 (available_width
- content_width
) / 2);
458 content_width
= std::min(content_width
,
459 width() - insets
.right() - content_x
);
460 int available_height
= height() - insets
.height();
461 int content_y
= (available_height
- text_size_
.height()) / 2 + insets
.top();
463 gfx::Rect
bounds(content_x
, content_y
, content_width
, text_size_
.height());
467 gfx::Rect
TextButtonBase::GetTextBounds() const {
468 return GetContentBounds(0);
471 void TextButtonBase::SetFocusPainter(scoped_ptr
<Painter
> focus_painter
) {
472 focus_painter_
= focus_painter
.Pass();
475 void TextButtonBase::PaintButton(gfx::Canvas
* canvas
, PaintButtonMode mode
) {
476 if (mode
== PB_NORMAL
) {
477 OnPaintBackground(canvas
);
478 OnPaintBorder(canvas
);
479 Painter::PaintFocusPainter(this, canvas
, focus_painter_
.get());
482 OnPaintText(canvas
, mode
);
485 gfx::Size
TextButtonBase::GetMinimumSize() const {
486 return max_text_size_
;
489 void TextButtonBase::OnEnabledChanged() {
490 // We should always call UpdateColor() since the state of the button might be
491 // changed by other functions like CustomButton::SetState().
493 CustomButton::OnEnabledChanged();
496 const char* TextButtonBase::GetClassName() const {
497 return kViewClassName
;
500 void TextButtonBase::OnNativeThemeChanged(const ui::NativeTheme
* theme
) {
501 if (use_enabled_color_from_theme_
) {
502 color_enabled_
= theme
->GetSystemColor(
503 ui::NativeTheme::kColorId_ButtonEnabledColor
);
505 if (use_disabled_color_from_theme_
) {
506 color_disabled_
= theme
->GetSystemColor(
507 ui::NativeTheme::kColorId_ButtonDisabledColor
);
509 if (use_highlight_color_from_theme_
) {
510 color_highlight_
= theme
->GetSystemColor(
511 ui::NativeTheme::kColorId_ButtonHighlightColor
);
513 if (use_hover_color_from_theme_
) {
514 color_hover_
= theme
->GetSystemColor(
515 ui::NativeTheme::kColorId_ButtonHoverColor
);
520 gfx::Rect
TextButtonBase::GetThemePaintRect() const {
521 return GetLocalBounds();
524 ui::NativeTheme::State
TextButtonBase::GetThemeState(
525 ui::NativeTheme::ExtraParams
* params
) const {
526 GetExtraParams(params
);
529 return ui::NativeTheme::kDisabled
;
531 return ui::NativeTheme::kNormal
;
533 return ui::NativeTheme::kHovered
;
535 return ui::NativeTheme::kPressed
;
537 NOTREACHED() << "Unknown state: " << state();
538 return ui::NativeTheme::kNormal
;
542 const gfx::Animation
* TextButtonBase::GetThemeAnimation() const {
544 if (GetNativeTheme() == ui::NativeThemeWin::instance()) {
545 return ui::NativeThemeWin::instance()->IsThemingActive() ?
546 hover_animation_
.get() : NULL
;
549 return hover_animation_
.get();
552 ui::NativeTheme::State
TextButtonBase::GetBackgroundThemeState(
553 ui::NativeTheme::ExtraParams
* params
) const {
554 GetExtraParams(params
);
555 return ui::NativeTheme::kNormal
;
558 ui::NativeTheme::State
TextButtonBase::GetForegroundThemeState(
559 ui::NativeTheme::ExtraParams
* params
) const {
560 GetExtraParams(params
);
561 return ui::NativeTheme::kHovered
;
565 // TextButton -----------------------------------------------------------------
567 TextButton::TextButton(ButtonListener
* listener
, const base::string16
& text
)
568 : TextButtonBase(listener
, text
),
569 icon_placement_(ICON_ON_LEFT
),
570 has_hover_icon_(false),
571 has_pushed_icon_(false),
572 icon_text_spacing_(kDefaultIconTextSpacing
),
573 ignore_minimum_size_(true),
574 full_justification_(false) {
575 SetBorder(scoped_ptr
<Border
>(new TextButtonDefaultBorder
));
576 SetFocusPainter(Painter::CreateDashedFocusPainterWithInsets(
577 gfx::Insets(kFocusRectInset
, kFocusRectInset
,
578 kFocusRectInset
, kFocusRectInset
)));
581 TextButton::~TextButton() {
584 void TextButton::SetIcon(const gfx::ImageSkia
& icon
) {
589 void TextButton::SetHoverIcon(const gfx::ImageSkia
& icon
) {
591 has_hover_icon_
= true;
595 void TextButton::SetPushedIcon(const gfx::ImageSkia
& icon
) {
597 has_pushed_icon_
= true;
601 gfx::Size
TextButton::GetPreferredSize() const {
602 gfx::Size
prefsize(TextButtonBase::GetPreferredSize());
603 prefsize
.Enlarge(icon_
.width(), 0);
604 prefsize
.set_height(std::max(prefsize
.height(), icon_
.height()));
606 // Use the max size to set the button boundaries.
607 if (icon_
.width() > 0 && !text_
.empty())
608 prefsize
.Enlarge(icon_text_spacing_
, 0);
611 prefsize
.set_width(std::min(max_width_
, prefsize
.width()));
614 // Clamp the size returned to at least the minimum size.
615 if (!ignore_minimum_size_
) {
616 gfx::PlatformFontWin
* platform_font
= static_cast<gfx::PlatformFontWin
*>(
617 font_list_
.GetPrimaryFont().platform_font());
618 prefsize
.set_width(std::max(
620 platform_font
->horizontal_dlus_to_pixels(kMinWidthDLUs
)));
621 prefsize
.set_height(std::max(
623 platform_font
->vertical_dlus_to_pixels(kMinHeightDLUs
)));
627 prefsize
.set_width(std::max(prefsize
.width(), min_width_
));
628 prefsize
.set_height(std::max(prefsize
.height(), min_height_
));
633 void TextButton::PaintButton(gfx::Canvas
* canvas
, PaintButtonMode mode
) {
634 if (full_justification_
&& icon_placement_
== ICON_ON_LEFT
)
635 set_alignment(ALIGN_RIGHT
);
637 TextButtonBase::PaintButton(canvas
, mode
);
638 OnPaintIcon(canvas
, mode
);
641 void TextButton::OnPaintIcon(gfx::Canvas
* canvas
, PaintButtonMode mode
) {
642 const gfx::ImageSkia
& icon
= GetImageToPaint();
644 if (icon
.width() > 0) {
645 gfx::Rect text_bounds
= GetTextBounds();
647 int spacing
= text_
.empty() ? 0 : icon_text_spacing_
;
648 gfx::Insets insets
= GetInsets();
649 switch (icon_placement_
) {
651 icon_x
= full_justification_
? insets
.left()
652 : text_bounds
.x() - icon
.width() - spacing
;
655 icon_x
= full_justification_
? width() - insets
.right() - icon
.width()
656 : text_bounds
.right() + spacing
;
659 DCHECK(text_
.empty());
660 icon_x
= (width() - insets
.width() - icon
.width()) / 2 + insets
.left();
667 int available_height
= height() - insets
.height();
668 int icon_y
= (available_height
- icon
.height()) / 2 + insets
.top();
670 // Mirroring the icon position if necessary.
671 gfx::Rect
icon_bounds(icon_x
, icon_y
, icon
.width(), icon
.height());
672 icon_bounds
.set_x(GetMirroredXForRect(icon_bounds
));
673 canvas
->DrawImageInt(icon
, icon_bounds
.x(), icon_bounds
.y());
677 void TextButton::set_ignore_minimum_size(bool ignore_minimum_size
) {
678 ignore_minimum_size_
= ignore_minimum_size
;
681 void TextButton::set_full_justification(bool full_justification
) {
682 full_justification_
= full_justification
;
685 const char* TextButton::GetClassName() const {
686 return kViewClassName
;
689 ui::NativeTheme::Part
TextButton::GetThemePart() const {
690 return ui::NativeTheme::kPushButton
;
693 void TextButton::GetExtraParams(ui::NativeTheme::ExtraParams
* params
) const {
694 TextButtonBase::GetExtraParams(params
);
695 params
->button
.is_default
= is_default_
;
698 gfx::Rect
TextButton::GetTextBounds() const {
701 const gfx::ImageSkia
& icon
= GetImageToPaint();
702 if (icon
.width() > 0)
703 extra_width
= icon
.width() + (text_
.empty() ? 0 : icon_text_spacing_
);
705 gfx::Rect
bounds(GetContentBounds(extra_width
));
707 if (extra_width
> 0) {
708 // Make sure the icon is always fully visible.
709 if (icon_placement_
== ICON_ON_LEFT
) {
710 bounds
.Inset(extra_width
, 0, 0, 0);
711 } else if (icon_placement_
== ICON_ON_RIGHT
) {
712 bounds
.Inset(0, 0, extra_width
, 0);
719 const gfx::ImageSkia
& TextButton::GetImageToPaint() const {
720 if (show_multiple_icon_states_
) {
721 if (has_hover_icon_
&& (state() == STATE_HOVERED
))
723 if (has_pushed_icon_
&& (state() == STATE_PRESSED
))