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.
4
5 #include "ui/views/controls/button/label_button.h"
6
7 #include <stddef.h>
8
9 #include <algorithm>
10 #include <utility>
11
12 #include "base/lazy_instance.h"
13 #include "base/logging.h"
14 #include "build/build_config.h"
15 #include "ui/accessibility/ax_enums.mojom.h"
16 #include "ui/accessibility/ax_node_data.h"
17 #include "ui/gfx/animation/throb_animation.h"
18 #include "ui/gfx/canvas.h"
19 #include "ui/gfx/color_utils.h"
20 #include "ui/gfx/font_list.h"
21 #include "ui/gfx/geometry/vector2d.h"
22 #include "ui/native_theme/native_theme.h"
23 #include "ui/views/animation/ink_drop.h"
24 #include "ui/views/background.h"
25 #include "ui/views/controls/button/label_button_border.h"
26 #include "ui/views/metadata/metadata_impl_macros.h"
27 #include "ui/views/painter.h"
28 #include "ui/views/style/platform_style.h"
29 #include "ui/views/view_class_properties.h"
30 #include "ui/views/window/dialog_delegate.h"
31
32 namespace views {
33
LabelButton(ButtonListener * listener,const base::string16 & text,int button_context)34 LabelButton::LabelButton(ButtonListener* listener,
35 const base::string16& text,
36 int button_context)
37 : Button(listener),
38 cached_normal_font_list_(
39 style::GetFont(button_context, style::STYLE_PRIMARY)),
40 cached_default_button_font_list_(
41 style::GetFont(button_context, style::STYLE_DIALOG_BUTTON_DEFAULT)) {
42 ink_drop_container_ = AddChildView(std::make_unique<InkDropContainerView>());
43 ink_drop_container_->SetVisible(false);
44
45 image_ = AddChildView(std::make_unique<ImageView>());
46 image_->set_can_process_events_within_subtree(false);
47
48 label_ =
49 AddChildView(std::make_unique<LabelButtonLabel>(text, button_context));
50 label_->SetAutoColorReadabilityEnabled(false);
51 label_->SetHorizontalAlignment(gfx::ALIGN_TO_HEAD);
52
53 SetAnimationDuration(base::TimeDelta::FromMilliseconds(170));
54 SetTextInternal(text);
55 }
56
57 LabelButton::~LabelButton() = default;
58
GetImage(ButtonState for_state) const59 gfx::ImageSkia LabelButton::GetImage(ButtonState for_state) const {
60 if (for_state != STATE_NORMAL && button_state_images_[for_state].isNull())
61 return button_state_images_[STATE_NORMAL];
62 return button_state_images_[for_state];
63 }
64
SetImage(ButtonState for_state,const gfx::ImageSkia & image)65 void LabelButton::SetImage(ButtonState for_state, const gfx::ImageSkia& image) {
66 button_state_images_[for_state] = image;
67 UpdateImage();
68 }
69
GetText() const70 const base::string16& LabelButton::GetText() const {
71 return label_->GetText();
72 }
73
SetText(const base::string16 & text)74 void LabelButton::SetText(const base::string16& text) {
75 SetTextInternal(text);
76 }
77
ShrinkDownThenClearText()78 void LabelButton::ShrinkDownThenClearText() {
79 if (GetText().empty())
80 return;
81 // First, we recalculate preferred size for the new mode (without the label).
82 shrinking_down_label_ = true;
83 PreferredSizeChanged();
84 // Second, we clear the label right away if the button is already small.
85 ClearTextIfShrunkDown();
86 }
87
SetTextColor(ButtonState for_state,SkColor color)88 void LabelButton::SetTextColor(ButtonState for_state, SkColor color) {
89 button_state_colors_[for_state] = color;
90 if (for_state == STATE_DISABLED)
91 label_->SetDisabledColor(color);
92 else if (for_state == state())
93 label_->SetEnabledColor(color);
94 explicitly_set_colors_[for_state] = true;
95 }
96
SetEnabledTextColors(base::Optional<SkColor> color)97 void LabelButton::SetEnabledTextColors(base::Optional<SkColor> color) {
98 ButtonState states[] = {STATE_NORMAL, STATE_HOVERED, STATE_PRESSED};
99 if (color.has_value()) {
100 for (auto state : states)
101 SetTextColor(state, color.value());
102 return;
103 }
104 for (auto state : states)
105 explicitly_set_colors_[state] = false;
106 ResetColorsFromNativeTheme();
107 }
108
SetTextShadows(const gfx::ShadowValues & shadows)109 void LabelButton::SetTextShadows(const gfx::ShadowValues& shadows) {
110 label_->SetShadows(shadows);
111 }
112
SetTextSubpixelRenderingEnabled(bool enabled)113 void LabelButton::SetTextSubpixelRenderingEnabled(bool enabled) {
114 label_->SetSubpixelRenderingEnabled(enabled);
115 }
116
SetElideBehavior(gfx::ElideBehavior elide_behavior)117 void LabelButton::SetElideBehavior(gfx::ElideBehavior elide_behavior) {
118 label_->SetElideBehavior(elide_behavior);
119 }
120
SetHorizontalAlignment(gfx::HorizontalAlignment alignment)121 void LabelButton::SetHorizontalAlignment(gfx::HorizontalAlignment alignment) {
122 DCHECK_NE(gfx::ALIGN_TO_HEAD, alignment);
123 if (GetHorizontalAlignment() == alignment)
124 return;
125 horizontal_alignment_ = alignment;
126 OnPropertyChanged(&min_size_, kPropertyEffectsLayout);
127 }
128
GetHorizontalAlignment() const129 gfx::HorizontalAlignment LabelButton::GetHorizontalAlignment() const {
130 return horizontal_alignment_;
131 }
132
GetMinSize() const133 gfx::Size LabelButton::GetMinSize() const {
134 return min_size_;
135 }
136
SetMinSize(const gfx::Size & min_size)137 void LabelButton::SetMinSize(const gfx::Size& min_size) {
138 if (GetMinSize() == min_size)
139 return;
140 min_size_ = min_size;
141 ResetCachedPreferredSize();
142 OnPropertyChanged(&min_size_, kPropertyEffectsNone);
143 }
144
GetMaxSize() const145 gfx::Size LabelButton::GetMaxSize() const {
146 return max_size_;
147 }
148
SetMaxSize(const gfx::Size & max_size)149 void LabelButton::SetMaxSize(const gfx::Size& max_size) {
150 if (GetMaxSize() == max_size)
151 return;
152 max_size_ = max_size;
153 ResetCachedPreferredSize();
154 OnPropertyChanged(&max_size_, kPropertyEffectsNone);
155 }
156
GetIsDefault() const157 bool LabelButton::GetIsDefault() const {
158 return is_default_;
159 }
160
SetIsDefault(bool is_default)161 void LabelButton::SetIsDefault(bool is_default) {
162 // TODO(estade): move this to MdTextButton once |style_| is removed.
163 if (GetIsDefault() == is_default)
164 return;
165 is_default_ = is_default;
166 ui::Accelerator accel(ui::VKEY_RETURN, ui::EF_NONE);
167 if (is_default)
168 AddAccelerator(accel);
169 else
170 RemoveAccelerator(accel);
171 OnPropertyChanged(&is_default_, UpdateStyleToIndicateDefaultStatus());
172 }
173
GetImageLabelSpacing() const174 int LabelButton::GetImageLabelSpacing() const {
175 return image_label_spacing_;
176 }
177
SetImageLabelSpacing(int spacing)178 void LabelButton::SetImageLabelSpacing(int spacing) {
179 if (GetImageLabelSpacing() == spacing)
180 return;
181 image_label_spacing_ = spacing;
182 ResetCachedPreferredSize();
183 OnPropertyChanged(&image_label_spacing_, kPropertyEffectsLayout);
184 }
185
GetImageCentered() const186 bool LabelButton::GetImageCentered() const {
187 return image_centered_;
188 }
189
SetImageCentered(bool image_centered)190 void LabelButton::SetImageCentered(bool image_centered) {
191 if (GetImageCentered() == image_centered)
192 return;
193 image_centered_ = image_centered;
194 OnPropertyChanged(&image_centered_, kPropertyEffectsLayout);
195 }
196
CreateDefaultBorder() const197 std::unique_ptr<LabelButtonBorder> LabelButton::CreateDefaultBorder() const {
198 auto border = std::make_unique<LabelButtonBorder>();
199 border->set_insets(views::LabelButtonAssetBorder::GetDefaultInsets());
200 return border;
201 }
202
SetBorder(std::unique_ptr<Border> border)203 void LabelButton::SetBorder(std::unique_ptr<Border> border) {
204 border_is_themed_border_ = false;
205 View::SetBorder(std::move(border));
206 ResetCachedPreferredSize();
207 }
208
OnBoundsChanged(const gfx::Rect & previous_bounds)209 void LabelButton::OnBoundsChanged(const gfx::Rect& previous_bounds) {
210 ClearTextIfShrunkDown();
211 Button::OnBoundsChanged(previous_bounds);
212 }
213
CalculatePreferredSize() const214 gfx::Size LabelButton::CalculatePreferredSize() const {
215 // Cache the computed size, as recomputing it is an expensive operation.
216 if (!cached_preferred_size_) {
217 gfx::Size size = GetUnclampedSizeWithoutLabel();
218
219 // Disregard label in the preferred size if the button is shrinking down to
220 // show no label soon.
221 if (!shrinking_down_label_) {
222 const gfx::Size preferred_label_size = label_->GetPreferredSize();
223 size.Enlarge(preferred_label_size.width(), 0);
224
225 // Increase the height of the label (with insets) if larger.
226 size.set_height(std::max(
227 preferred_label_size.height() + GetInsets().height(), size.height()));
228 }
229
230 size.SetToMax(GetMinSize());
231
232 // Clamp size to max size (if valid).
233 const gfx::Size max_size = GetMaxSize();
234 if (max_size.width() > 0)
235 size.set_width(std::min(max_size.width(), size.width()));
236 if (max_size.height() > 0)
237 size.set_height(std::min(max_size.height(), size.height()));
238
239 cached_preferred_size_ = size;
240 }
241
242 return cached_preferred_size_.value();
243 }
244
GetMinimumSize() const245 gfx::Size LabelButton::GetMinimumSize() const {
246 if (label_->GetElideBehavior() == gfx::ElideBehavior::NO_ELIDE)
247 return GetPreferredSize();
248
249 gfx::Size size = image_->GetPreferredSize();
250 const gfx::Insets insets(GetInsets());
251 size.Enlarge(insets.width(), insets.height());
252
253 size.SetToMax(GetMinSize());
254 const gfx::Size max_size = GetMaxSize();
255 if (max_size.width() > 0)
256 size.set_width(std::min(max_size.width(), size.width()));
257 if (max_size.height() > 0)
258 size.set_height(std::min(max_size.height(), size.height()));
259
260 return size;
261 }
262
GetHeightForWidth(int width) const263 int LabelButton::GetHeightForWidth(int width) const {
264 const gfx::Size size_without_label = GetUnclampedSizeWithoutLabel();
265 // Get label height for the remaining width.
266 const int label_height_with_insets =
267 label_->GetHeightForWidth(width - size_without_label.width()) +
268 GetInsets().height();
269
270 // Height is the larger of size without label and label height with insets.
271 int height = std::max(size_without_label.height(), label_height_with_insets);
272
273 height = std::max(height, GetMinSize().height());
274
275 // Clamp height to the maximum height (if valid).
276 const gfx::Size max_size = GetMaxSize();
277 if (max_size.height() > 0)
278 return std::min(max_size.height(), height);
279
280 return height;
281 }
282
Layout()283 void LabelButton::Layout() {
284 gfx::Rect child_area = GetLocalBounds();
285
286 ink_drop_container_->SetBoundsRect(child_area);
287 // The space that the label can use. Its actual bounds may be smaller if the
288 // label is short.
289 gfx::Rect label_area = child_area;
290
291 gfx::Insets insets = GetInsets();
292 child_area.Inset(insets);
293 // Labels can paint over the vertical component of the border insets.
294 label_area.Inset(insets.left(), 0, insets.right(), 0);
295
296 gfx::Size image_size = image_->GetPreferredSize();
297 image_size.SetToMin(child_area.size());
298
299 const auto horizontal_alignment = GetHorizontalAlignment();
300 if (!image_size.IsEmpty()) {
301 int image_space = image_size.width() + GetImageLabelSpacing();
302 if (horizontal_alignment == gfx::ALIGN_RIGHT)
303 label_area.Inset(0, 0, image_space, 0);
304 else
305 label_area.Inset(image_space, 0, 0, 0);
306 }
307
308 gfx::Size label_size(
309 std::min(label_area.width(), label_->GetPreferredSize().width()),
310 label_area.height());
311
312 gfx::Point image_origin = child_area.origin();
313 if (label_->GetMultiLine() && !image_centered_) {
314 image_origin.Offset(
315 0, std::max(
316 0, (label_->font_list().GetHeight() - image_size.height()) / 2));
317 } else {
318 image_origin.Offset(0, (child_area.height() - image_size.height()) / 2);
319 }
320 if (horizontal_alignment == gfx::ALIGN_CENTER) {
321 const int spacing = (image_size.width() > 0 && label_size.width() > 0)
322 ? GetImageLabelSpacing()
323 : 0;
324 const int total_width = image_size.width() + label_size.width() + spacing;
325 image_origin.Offset((child_area.width() - total_width) / 2, 0);
326 } else if (horizontal_alignment == gfx::ALIGN_RIGHT) {
327 image_origin.Offset(child_area.width() - image_size.width(), 0);
328 }
329 image_->SetBoundsRect(gfx::Rect(image_origin, image_size));
330
331 gfx::Rect label_bounds = label_area;
332 if (label_area.width() == label_size.width()) {
333 // Label takes up the whole area.
334 } else if (horizontal_alignment == gfx::ALIGN_CENTER) {
335 label_bounds.ClampToCenteredSize(label_size);
336 } else {
337 label_bounds.set_size(label_size);
338 if (horizontal_alignment == gfx::ALIGN_RIGHT)
339 label_bounds.Offset(label_area.width() - label_size.width(), 0);
340 }
341
342 label_->SetBoundsRect(label_bounds);
343 Button::Layout();
344 }
345
EnableCanvasFlippingForRTLUI(bool flip)346 void LabelButton::EnableCanvasFlippingForRTLUI(bool flip) {
347 Button::EnableCanvasFlippingForRTLUI(flip);
348 image_->EnableCanvasFlippingForRTLUI(flip);
349 }
350
GetAccessibleNodeData(ui::AXNodeData * node_data)351 void LabelButton::GetAccessibleNodeData(ui::AXNodeData* node_data) {
352 if (GetIsDefault())
353 node_data->AddState(ax::mojom::State::kDefault);
354 Button::GetAccessibleNodeData(node_data);
355 }
356
GetThemePart() const357 ui::NativeTheme::Part LabelButton::GetThemePart() const {
358 return ui::NativeTheme::kPushButton;
359 }
360
GetThemePaintRect() const361 gfx::Rect LabelButton::GetThemePaintRect() const {
362 return GetLocalBounds();
363 }
364
GetThemeState(ui::NativeTheme::ExtraParams * params) const365 ui::NativeTheme::State LabelButton::GetThemeState(
366 ui::NativeTheme::ExtraParams* params) const {
367 GetExtraParams(params);
368 switch (state()) {
369 case STATE_NORMAL:
370 return ui::NativeTheme::kNormal;
371 case STATE_HOVERED:
372 return ui::NativeTheme::kHovered;
373 case STATE_PRESSED:
374 return ui::NativeTheme::kPressed;
375 case STATE_DISABLED:
376 return ui::NativeTheme::kDisabled;
377 case STATE_COUNT:
378 NOTREACHED() << "Unknown state: " << state();
379 }
380 return ui::NativeTheme::kNormal;
381 }
382
GetThemeAnimation() const383 const gfx::Animation* LabelButton::GetThemeAnimation() const {
384 return &hover_animation();
385 }
386
GetBackgroundThemeState(ui::NativeTheme::ExtraParams * params) const387 ui::NativeTheme::State LabelButton::GetBackgroundThemeState(
388 ui::NativeTheme::ExtraParams* params) const {
389 GetExtraParams(params);
390 return ui::NativeTheme::kNormal;
391 }
392
GetForegroundThemeState(ui::NativeTheme::ExtraParams * params) const393 ui::NativeTheme::State LabelButton::GetForegroundThemeState(
394 ui::NativeTheme::ExtraParams* params) const {
395 GetExtraParams(params);
396 return ui::NativeTheme::kHovered;
397 }
398
UpdateImage()399 void LabelButton::UpdateImage() {
400 image_->SetImage(GetImage(GetVisualState()));
401 ResetCachedPreferredSize();
402 }
403
UpdateThemedBorder()404 void LabelButton::UpdateThemedBorder() {
405 // Don't override borders set by others.
406 if (!border_is_themed_border_)
407 return;
408
409 SetBorder(PlatformStyle::CreateThemedLabelButtonBorder(this));
410 border_is_themed_border_ = true;
411 }
412
AddLayerBeneathView(ui::Layer * new_layer)413 void LabelButton::AddLayerBeneathView(ui::Layer* new_layer) {
414 image()->SetPaintToLayer();
415 image()->layer()->SetFillsBoundsOpaquely(false);
416 ink_drop_container()->SetVisible(true);
417 ink_drop_container()->AddLayerBeneathView(new_layer);
418 }
419
RemoveLayerBeneathView(ui::Layer * old_layer)420 void LabelButton::RemoveLayerBeneathView(ui::Layer* old_layer) {
421 ink_drop_container()->RemoveLayerBeneathView(old_layer);
422 ink_drop_container()->SetVisible(false);
423 image()->DestroyLayer();
424 }
425
GetExtraParams(ui::NativeTheme::ExtraParams * params) const426 void LabelButton::GetExtraParams(ui::NativeTheme::ExtraParams* params) const {
427 params->button.checked = false;
428 params->button.indeterminate = false;
429 params->button.is_default = GetIsDefault();
430 params->button.is_focused = HasFocus() && IsAccessibilityFocusable();
431 params->button.has_border = false;
432 params->button.classic_state = 0;
433 params->button.background_color = label_->GetBackgroundColor();
434 }
435
UpdateStyleToIndicateDefaultStatus()436 PropertyEffects LabelButton::UpdateStyleToIndicateDefaultStatus() {
437 // Check that a subclass hasn't replaced the Label font. These buttons may
438 // never be given default status.
439 DCHECK_EQ(cached_normal_font_list_.GetFontSize(),
440 label()->font_list().GetFontSize());
441 // TODO(tapted): This should use style::GetFont(), but this part can just be
442 // deleted when default buttons no longer go bold. Colors will need updating
443 // still.
444 label_->SetFontList(GetIsDefault() ? cached_default_button_font_list_
445 : cached_normal_font_list_);
446 ResetLabelEnabledColor();
447 return kPropertyEffectsLayout;
448 }
449
ChildPreferredSizeChanged(View * child)450 void LabelButton::ChildPreferredSizeChanged(View* child) {
451 PreferredSizeChanged();
452 }
453
PreferredSizeChanged()454 void LabelButton::PreferredSizeChanged() {
455 ResetCachedPreferredSize();
456 Button::PreferredSizeChanged();
457 }
458
OnFocus()459 void LabelButton::OnFocus() {
460 Button::OnFocus();
461 // Typically the border renders differently when focused.
462 SchedulePaint();
463 }
464
OnBlur()465 void LabelButton::OnBlur() {
466 Button::OnBlur();
467 // Typically the border renders differently when focused.
468 SchedulePaint();
469 }
470
OnThemeChanged()471 void LabelButton::OnThemeChanged() {
472 Button::OnThemeChanged();
473 ResetColorsFromNativeTheme();
474 UpdateThemedBorder();
475 ResetLabelEnabledColor();
476 // Invalidate the layout to pickup the new insets from the border.
477 InvalidateLayout();
478 // The entire button has to be repainted here, since the native theme can
479 // define the tint for the entire background/border/focus ring.
480 SchedulePaint();
481 }
482
StateChanged(ButtonState old_state)483 void LabelButton::StateChanged(ButtonState old_state) {
484 const gfx::Size previous_image_size(image_->GetPreferredSize());
485 UpdateImage();
486 ResetLabelEnabledColor();
487 label_->SetEnabled(state() != STATE_DISABLED);
488 if (image_->GetPreferredSize() != previous_image_size)
489 InvalidateLayout();
490 Button::StateChanged(old_state);
491 }
492
SetTextInternal(const base::string16 & text)493 void LabelButton::SetTextInternal(const base::string16& text) {
494 SetAccessibleName(text);
495 label_->SetText(text);
496
497 // Setting text cancels ShrinkDownThenClearText().
498 if (shrinking_down_label_) {
499 shrinking_down_label_ = false;
500 PreferredSizeChanged();
501 }
502
503 // TODO(pkasting): Remove this and forward callback subscriptions to the
504 // underlying label property when Label is converted to properties.
505 OnPropertyChanged(label_, kPropertyEffectsNone);
506 }
507
ClearTextIfShrunkDown()508 void LabelButton::ClearTextIfShrunkDown() {
509 if (!cached_preferred_size_)
510 CalculatePreferredSize();
511 if (shrinking_down_label_ && width() <= cached_preferred_size_->width() &&
512 height() <= cached_preferred_size_->height()) {
513 // Once the button shrinks down to its preferred size (that disregards the
514 // current text), we finish the operation by clearing the text.
515 shrinking_down_label_ = false;
516 SetText(base::string16());
517 }
518 }
519
ResetCachedPreferredSize()520 void LabelButton::ResetCachedPreferredSize() {
521 cached_preferred_size_ = base::nullopt;
522 }
523
GetUnclampedSizeWithoutLabel() const524 gfx::Size LabelButton::GetUnclampedSizeWithoutLabel() const {
525 const gfx::Size image_size = image_->GetPreferredSize();
526 gfx::Size size = image_size;
527 const gfx::Insets insets(GetInsets());
528 size.Enlarge(insets.width(), insets.height());
529
530 // Accommodate for spacing between image and text if both are present.
531 if (image_size.width() > 0 && !GetText().empty() && !shrinking_down_label_)
532 size.Enlarge(GetImageLabelSpacing(), 0);
533
534 // Make the size at least as large as the minimum size needed by the border.
535 if (border())
536 size.SetToMax(border()->GetMinimumSize());
537
538 return size;
539 }
540
ResetColorsFromNativeTheme()541 void LabelButton::ResetColorsFromNativeTheme() {
542 const ui::NativeTheme* theme = GetNativeTheme();
543 // Since this is a LabelButton, use the label colors.
544 SkColor colors[STATE_COUNT] = {
545 theme->GetSystemColor(ui::NativeTheme::kColorId_LabelEnabledColor),
546 theme->GetSystemColor(ui::NativeTheme::kColorId_LabelEnabledColor),
547 theme->GetSystemColor(ui::NativeTheme::kColorId_LabelEnabledColor),
548 theme->GetSystemColor(ui::NativeTheme::kColorId_LabelDisabledColor),
549 };
550
551 label_->SetBackground(nullptr);
552 label_->SetAutoColorReadabilityEnabled(false);
553
554 for (size_t state = STATE_NORMAL; state < STATE_COUNT; ++state) {
555 if (!explicitly_set_colors_[state]) {
556 SetTextColor(static_cast<ButtonState>(state), colors[state]);
557 explicitly_set_colors_[state] = false;
558 }
559 }
560 }
561
ResetLabelEnabledColor()562 void LabelButton::ResetLabelEnabledColor() {
563 const SkColor color = button_state_colors_[state()];
564 if (state() != STATE_DISABLED && label_->GetEnabledColor() != color)
565 label_->SetEnabledColor(color);
566 }
567
568 BEGIN_METADATA(LabelButton)
569 METADATA_PARENT_CLASS(Button)
570 ADD_PROPERTY_METADATA(LabelButton, base::string16, Text)
571 ADD_PROPERTY_METADATA(LabelButton,
572 gfx::HorizontalAlignment,
573 HorizontalAlignment)
574 ADD_PROPERTY_METADATA(LabelButton, gfx::Size, MinSize)
575 ADD_PROPERTY_METADATA(LabelButton, gfx::Size, MaxSize)
576 ADD_PROPERTY_METADATA(LabelButton, bool, IsDefault)
577 ADD_PROPERTY_METADATA(LabelButton, int, ImageLabelSpacing)
578 ADD_PROPERTY_METADATA(LabelButton, bool, ImageCentered)
579 END_METADATA()
580
581 } // namespace views
582