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/label_button.h"
7 #include "base/lazy_instance.h"
8 #include "base/logging.h"
9 #include "ui/gfx/animation/throb_animation.h"
10 #include "ui/gfx/canvas.h"
11 #include "ui/gfx/font_list.h"
12 #include "ui/gfx/geometry/vector2d.h"
13 #include "ui/gfx/sys_color_change_listener.h"
14 #include "ui/native_theme/native_theme.h"
15 #include "ui/views/background.h"
16 #include "ui/views/controls/button/label_button_border.h"
17 #include "ui/views/painter.h"
18 #include "ui/views/window/dialog_delegate.h"
20 #if defined(OS_LINUX) && !defined(OS_CHROMEOS)
21 #include "ui/views/linux_ui/linux_ui.h"
26 // The default spacing between the icon and text.
27 const int kSpacing
= 5;
29 #if !(defined(OS_LINUX) && !defined(OS_CHROMEOS))
30 // Default text and shadow colors for STYLE_BUTTON.
31 const SkColor kStyleButtonTextColor
= SK_ColorBLACK
;
32 const SkColor kStyleButtonShadowColor
= SK_ColorWHITE
;
35 const gfx::FontList
& GetDefaultNormalFontList() {
36 static base::LazyInstance
<gfx::FontList
>::Leaky font_list
=
37 LAZY_INSTANCE_INITIALIZER
;
38 return font_list
.Get();
41 const gfx::FontList
& GetDefaultBoldFontList() {
42 static base::LazyInstance
<gfx::FontList
>::Leaky font_list
=
43 LAZY_INSTANCE_INITIALIZER
;
44 if ((font_list
.Get().GetFontStyle() & gfx::Font::BOLD
) == 0) {
45 font_list
.Get() = font_list
.Get().
46 DeriveWithStyle(font_list
.Get().GetFontStyle() | gfx::Font::BOLD
);
47 DCHECK_NE(font_list
.Get().GetFontStyle() & gfx::Font::BOLD
, 0);
49 return font_list
.Get();
57 const int LabelButton::kHoverAnimationDurationMs
= 170;
60 const char LabelButton::kViewClassName
[] = "LabelButton";
62 LabelButton::LabelButton(ButtonListener
* listener
, const base::string16
& text
)
63 : CustomButton(listener
),
64 image_(new ImageView()),
66 cached_normal_font_list_(GetDefaultNormalFontList()),
67 cached_bold_font_list_(GetDefaultBoldFontList()),
68 button_state_images_(),
69 button_state_colors_(),
70 explicitly_set_colors_(),
72 style_(STYLE_TEXTBUTTON
),
73 border_is_themed_border_(true),
74 image_label_spacing_(kSpacing
) {
75 SetAnimationDuration(kHoverAnimationDurationMs
);
79 image_
->set_interactive(false);
82 label_
->SetFontList(cached_normal_font_list_
);
83 label_
->SetAutoColorReadabilityEnabled(false);
84 label_
->SetHorizontalAlignment(gfx::ALIGN_LEFT
);
86 // Initialize the colors, border, and layout.
89 SetAccessibleName(text
);
92 LabelButton::~LabelButton() {}
94 const gfx::ImageSkia
& LabelButton::GetImage(ButtonState for_state
) {
95 if (for_state
!= STATE_NORMAL
&& button_state_images_
[for_state
].isNull())
96 return button_state_images_
[STATE_NORMAL
];
97 return button_state_images_
[for_state
];
100 void LabelButton::SetImage(ButtonState for_state
, const gfx::ImageSkia
& image
) {
101 button_state_images_
[for_state
] = image
;
105 const base::string16
& LabelButton::GetText() const {
106 return label_
->text();
109 void LabelButton::SetText(const base::string16
& text
) {
110 SetAccessibleName(text
);
111 label_
->SetText(text
);
114 void LabelButton::SetTextColor(ButtonState for_state
, SkColor color
) {
115 button_state_colors_
[for_state
] = color
;
116 if (for_state
== STATE_DISABLED
)
117 label_
->SetDisabledColor(color
);
118 else if (for_state
== state())
119 label_
->SetEnabledColor(color
);
120 explicitly_set_colors_
[for_state
] = true;
123 void LabelButton::SetTextShadows(const gfx::ShadowValues
& shadows
) {
124 label_
->SetShadows(shadows
);
127 void LabelButton::SetTextSubpixelRenderingEnabled(bool enabled
) {
128 label_
->SetSubpixelRenderingEnabled(enabled
);
131 bool LabelButton::GetTextMultiLine() const {
132 return label_
->multi_line();
135 void LabelButton::SetTextMultiLine(bool text_multi_line
) {
136 label_
->SetMultiLine(text_multi_line
);
139 const gfx::FontList
& LabelButton::GetFontList() const {
140 return label_
->font_list();
143 void LabelButton::SetFontList(const gfx::FontList
& font_list
) {
144 cached_normal_font_list_
= font_list
;
145 cached_bold_font_list_
= font_list
.DeriveWithStyle(
146 font_list
.GetFontStyle() | gfx::Font::BOLD
);
148 // STYLE_BUTTON uses bold text to indicate default buttons.
150 style_
== STYLE_BUTTON
&& is_default_
?
151 cached_bold_font_list_
: cached_normal_font_list_
);
154 void LabelButton::SetElideBehavior(gfx::ElideBehavior elide_behavior
) {
155 label_
->SetElideBehavior(elide_behavior
);
158 gfx::HorizontalAlignment
LabelButton::GetHorizontalAlignment() const {
159 return label_
->GetHorizontalAlignment();
162 void LabelButton::SetHorizontalAlignment(gfx::HorizontalAlignment alignment
) {
163 label_
->SetHorizontalAlignment(alignment
);
167 void LabelButton::SetMinSize(const gfx::Size
& min_size
) {
168 min_size_
= min_size
;
169 ResetCachedPreferredSize();
172 void LabelButton::SetMaxSize(const gfx::Size
& max_size
) {
173 max_size_
= max_size
;
174 ResetCachedPreferredSize();
177 void LabelButton::SetIsDefault(bool is_default
) {
178 if (is_default
== is_default_
)
180 is_default_
= is_default
;
181 ui::Accelerator
accel(ui::VKEY_RETURN
, ui::EF_NONE
);
182 is_default_
? AddAccelerator(accel
) : RemoveAccelerator(accel
);
184 // STYLE_BUTTON uses bold text to indicate default buttons.
185 if (style_
== STYLE_BUTTON
) {
187 is_default
? cached_bold_font_list_
: cached_normal_font_list_
);
191 void LabelButton::SetStyle(ButtonStyle style
) {
193 // Inset the button focus rect from the actual border; roughly match Windows.
194 if (style
== STYLE_BUTTON
) {
195 SetFocusPainter(nullptr);
197 SetFocusPainter(Painter::CreateDashedFocusPainterWithInsets(
198 gfx::Insets(3, 3, 3, 3)));
200 if (style
== STYLE_BUTTON
) {
201 label_
->SetHorizontalAlignment(gfx::ALIGN_CENTER
);
204 if (style
== STYLE_BUTTON
)
205 SetMinSize(gfx::Size(70, 33));
206 OnNativeThemeChanged(GetNativeTheme());
207 ResetCachedPreferredSize();
210 void LabelButton::SetImageLabelSpacing(int spacing
) {
211 if (spacing
== image_label_spacing_
)
213 image_label_spacing_
= spacing
;
214 ResetCachedPreferredSize();
218 void LabelButton::SetFocusPainter(scoped_ptr
<Painter
> focus_painter
) {
219 focus_painter_
= focus_painter
.Pass();
222 gfx::Size
LabelButton::GetPreferredSize() const {
223 if (cached_preferred_size_valid_
)
224 return cached_preferred_size_
;
226 // Use a temporary label copy for sizing to avoid calculation side-effects.
227 Label
label(GetText(), cached_normal_font_list_
);
228 label
.SetShadows(label_
->shadows());
229 label
.SetMultiLine(GetTextMultiLine());
231 if (style() == STYLE_BUTTON
) {
232 // Some text appears wider when rendered normally than when rendered bold.
233 // Accommodate the widest, as buttons may show bold and shouldn't resize.
234 const int current_width
= label
.GetPreferredSize().width();
235 label
.SetFontList(cached_bold_font_list_
);
236 if (label
.GetPreferredSize().width() < current_width
)
237 label
.SetFontList(cached_normal_font_list_
);
240 // Calculate the required size.
241 const gfx::Size
image_size(image_
->GetPreferredSize());
242 gfx::Size
size(label
.GetPreferredSize());
243 if (image_size
.width() > 0 && size
.width() > 0)
244 size
.Enlarge(image_label_spacing_
, 0);
245 size
.SetToMax(gfx::Size(0, image_size
.height()));
246 const gfx::Insets
insets(GetInsets());
247 size
.Enlarge(image_size
.width() + insets
.width(), insets
.height());
249 // Make the size at least as large as the minimum size needed by the border.
250 size
.SetToMax(border() ? border()->GetMinimumSize() : gfx::Size());
252 // Increase the minimum size monotonically with the preferred size.
253 size
.SetToMax(min_size_
);
256 // Return the largest known size clamped to the maximum size (if valid).
257 if (max_size_
.width() > 0)
258 size
.set_width(std::min(max_size_
.width(), size
.width()));
259 if (max_size_
.height() > 0)
260 size
.set_height(std::min(max_size_
.height(), size
.height()));
262 // Cache this computed size, as recomputing it is an expensive operation.
263 cached_preferred_size_valid_
= true;
264 cached_preferred_size_
= size
;
265 return cached_preferred_size_
;
268 int LabelButton::GetHeightForWidth(int w
) const {
269 w
-= GetInsets().width();
270 const gfx::Size
image_size(image_
->GetPreferredSize());
271 w
-= image_size
.width();
272 if (image_size
.width() > 0 && !GetText().empty())
273 w
-= image_label_spacing_
;
275 int height
= std::max(image_size
.height(), label_
->GetHeightForWidth(w
));
277 height
= std::max(height
, border()->GetMinimumSize().height());
279 height
= std::max(height
, min_size_
.height());
280 if (max_size_
.height() > 0)
281 height
= std::min(height
, max_size_
.height());
285 void LabelButton::Layout() {
286 gfx::HorizontalAlignment adjusted_alignment
= GetHorizontalAlignment();
287 if (base::i18n::IsRTL() && adjusted_alignment
!= gfx::ALIGN_CENTER
)
288 adjusted_alignment
= (adjusted_alignment
== gfx::ALIGN_LEFT
) ?
289 gfx::ALIGN_RIGHT
: gfx::ALIGN_LEFT
;
291 // By default, GetChildAreaBounds() ignores the top and bottom border, but we
292 // want the image to respect it.
293 gfx::Rect
child_area(GetChildAreaBounds());
294 gfx::Rect
label_area(child_area
);
296 gfx::Insets
insets(GetInsets());
297 child_area
.Inset(insets
);
298 // Labels can paint over the vertical component of the border insets.
299 label_area
.Inset(insets
.left(), 0, insets
.right(), 0);
301 gfx::Size
image_size(image_
->GetPreferredSize());
302 image_size
.SetToMin(child_area
.size());
304 // The label takes any remaining width after sizing the image, unless both
305 // views are centered. In that case, using the tighter preferred label width
306 // avoids wasted space within the label that would look like awkward padding.
307 gfx::Size
label_size(label_area
.size());
308 if (!image_size
.IsEmpty() && !label_size
.IsEmpty()) {
309 label_size
.set_width(std::max(child_area
.width() -
310 image_size
.width() - image_label_spacing_
, 0));
311 if (adjusted_alignment
== gfx::ALIGN_CENTER
) {
312 // Ensure multi-line labels paired with images use their available width.
313 label_size
.set_width(
314 std::min(label_size
.width(), label_
->GetPreferredSize().width()));
318 gfx::Point
image_origin(child_area
.origin());
319 image_origin
.Offset(0, (child_area
.height() - image_size
.height()) / 2);
320 if (adjusted_alignment
== gfx::ALIGN_CENTER
) {
321 const int spacing
= (image_size
.width() > 0 && label_size
.width() > 0) ?
322 image_label_spacing_
: 0;
323 const int total_width
= image_size
.width() + label_size
.width() +
325 image_origin
.Offset((child_area
.width() - total_width
) / 2, 0);
326 } else if (adjusted_alignment
== gfx::ALIGN_RIGHT
) {
327 image_origin
.Offset(child_area
.width() - image_size
.width(), 0);
330 gfx::Point
label_origin(label_area
.origin());
331 if (!image_size
.IsEmpty() && adjusted_alignment
!= gfx::ALIGN_RIGHT
) {
332 label_origin
.set_x(image_origin
.x() + image_size
.width() +
333 image_label_spacing_
);
336 image_
->SetBoundsRect(gfx::Rect(image_origin
, image_size
));
337 label_
->SetBoundsRect(gfx::Rect(label_origin
, label_size
));
340 const char* LabelButton::GetClassName() const {
341 return kViewClassName
;
344 scoped_ptr
<LabelButtonBorder
> LabelButton::CreateDefaultBorder() const {
345 return make_scoped_ptr(new LabelButtonBorder(style_
));
348 void LabelButton::SetBorder(scoped_ptr
<Border
> border
) {
349 border_is_themed_border_
= false;
350 View::SetBorder(border
.Pass());
351 ResetCachedPreferredSize();
354 gfx::Rect
LabelButton::GetChildAreaBounds() {
355 return GetLocalBounds();
358 void LabelButton::OnPaint(gfx::Canvas
* canvas
) {
359 View::OnPaint(canvas
);
360 Painter::PaintFocusPainter(this, canvas
, focus_painter_
.get());
363 void LabelButton::OnFocus() {
365 // Typically the border renders differently when focused.
369 void LabelButton::OnBlur() {
371 // Typically the border renders differently when focused.
375 void LabelButton::GetExtraParams(ui::NativeTheme::ExtraParams
* params
) const {
376 params
->button
.checked
= false;
377 params
->button
.indeterminate
= false;
378 params
->button
.is_default
= is_default_
;
379 params
->button
.is_focused
= HasFocus() && IsAccessibilityFocusable();
380 params
->button
.has_border
= false;
381 params
->button
.classic_state
= 0;
382 params
->button
.background_color
= label_
->background_color();
385 void LabelButton::ResetColorsFromNativeTheme() {
386 const ui::NativeTheme
* theme
= GetNativeTheme();
387 SkColor colors
[STATE_COUNT
] = {
388 theme
->GetSystemColor(ui::NativeTheme::kColorId_ButtonEnabledColor
),
389 theme
->GetSystemColor(ui::NativeTheme::kColorId_ButtonHoverColor
),
390 theme
->GetSystemColor(ui::NativeTheme::kColorId_ButtonHoverColor
),
391 theme
->GetSystemColor(ui::NativeTheme::kColorId_ButtonDisabledColor
),
394 // Certain styles do not change text color when hovered or pressed.
395 bool constant_text_color
= false;
396 // Use hardcoded colors for inverted color scheme support and STYLE_BUTTON.
397 if (gfx::IsInvertedColorScheme()) {
398 constant_text_color
= true;
399 colors
[STATE_NORMAL
] = SK_ColorWHITE
;
400 label_
->SetBackgroundColor(SK_ColorBLACK
);
401 label_
->set_background(Background::CreateSolidBackground(SK_ColorBLACK
));
402 label_
->SetAutoColorReadabilityEnabled(true);
403 label_
->SetShadows(gfx::ShadowValues());
404 } else if (style() == STYLE_BUTTON
) {
405 // TODO(erg): This is disabled on desktop linux because of the binary asset
406 // confusion. These details should either be pushed into ui::NativeThemeWin
407 // or should be obsoleted by rendering buttons with paint calls instead of
408 // with static assets. http://crbug.com/350498
409 #if !(defined(OS_LINUX) && !defined(OS_CHROMEOS))
410 constant_text_color
= true;
411 colors
[STATE_NORMAL
] = kStyleButtonTextColor
;
412 label_
->SetBackgroundColor(theme
->GetSystemColor(
413 ui::NativeTheme::kColorId_ButtonBackgroundColor
));
414 label_
->SetAutoColorReadabilityEnabled(false);
415 label_
->SetShadows(gfx::ShadowValues(
416 1, gfx::ShadowValue(gfx::Vector2d(0, 1), 0, kStyleButtonShadowColor
)));
418 label_
->set_background(NULL
);
420 label_
->set_background(NULL
);
423 if (constant_text_color
)
424 colors
[STATE_HOVERED
] = colors
[STATE_PRESSED
] = colors
[STATE_NORMAL
];
426 for (size_t state
= STATE_NORMAL
; state
< STATE_COUNT
; ++state
) {
427 if (!explicitly_set_colors_
[state
]) {
428 SetTextColor(static_cast<ButtonState
>(state
), colors
[state
]);
429 explicitly_set_colors_
[state
] = false;
434 void LabelButton::UpdateImage() {
435 image_
->SetImage(GetImage(state()));
436 ResetCachedPreferredSize();
439 void LabelButton::UpdateThemedBorder() {
440 // Don't override borders set by others.
441 if (!border_is_themed_border_
)
444 scoped_ptr
<LabelButtonBorder
> label_button_border
= CreateDefaultBorder();
446 #if defined(OS_LINUX) && !defined(OS_CHROMEOS)
447 views::LinuxUI
* linux_ui
= views::LinuxUI::instance();
449 SetBorder(linux_ui
->CreateNativeBorder(
450 this, label_button_border
.Pass()));
454 SetBorder(label_button_border
.Pass());
457 border_is_themed_border_
= true;
460 void LabelButton::StateChanged() {
461 const gfx::Size
previous_image_size(image_
->GetPreferredSize());
463 const SkColor color
= button_state_colors_
[state()];
464 if (state() != STATE_DISABLED
&& label_
->enabled_color() != color
)
465 label_
->SetEnabledColor(color
);
466 label_
->SetEnabled(state() != STATE_DISABLED
);
467 if (image_
->GetPreferredSize() != previous_image_size
)
471 void LabelButton::ChildPreferredSizeChanged(View
* child
) {
472 ResetCachedPreferredSize();
473 PreferredSizeChanged();
476 void LabelButton::OnNativeThemeChanged(const ui::NativeTheme
* theme
) {
477 ResetColorsFromNativeTheme();
478 UpdateThemedBorder();
479 // Invalidate the layout to pickup the new insets from the border.
483 ui::NativeTheme::Part
LabelButton::GetThemePart() const {
484 return ui::NativeTheme::kPushButton
;
487 gfx::Rect
LabelButton::GetThemePaintRect() const {
488 return GetLocalBounds();
491 ui::NativeTheme::State
LabelButton::GetThemeState(
492 ui::NativeTheme::ExtraParams
* params
) const {
493 GetExtraParams(params
);
495 case STATE_NORMAL
: return ui::NativeTheme::kNormal
;
496 case STATE_HOVERED
: return ui::NativeTheme::kHovered
;
497 case STATE_PRESSED
: return ui::NativeTheme::kPressed
;
498 case STATE_DISABLED
: return ui::NativeTheme::kDisabled
;
499 case STATE_COUNT
: NOTREACHED() << "Unknown state: " << state();
501 return ui::NativeTheme::kNormal
;
504 const gfx::Animation
* LabelButton::GetThemeAnimation() const {
505 return hover_animation_
.get();
508 ui::NativeTheme::State
LabelButton::GetBackgroundThemeState(
509 ui::NativeTheme::ExtraParams
* params
) const {
510 GetExtraParams(params
);
511 return ui::NativeTheme::kNormal
;
514 ui::NativeTheme::State
LabelButton::GetForegroundThemeState(
515 ui::NativeTheme::ExtraParams
* params
) const {
516 GetExtraParams(params
);
517 return ui::NativeTheme::kHovered
;
520 void LabelButton::ResetCachedPreferredSize() {
521 cached_preferred_size_valid_
= false;
522 cached_preferred_size_
= gfx::Size();