| // 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/label_button.h" |
| |
| #include <stddef.h> |
| |
| #include <algorithm> |
| #include <utility> |
| |
| #include "base/lazy_instance.h" |
| #include "base/logging.h" |
| #include "build/build_config.h" |
| #include "ui/accessibility/ax_enums.mojom.h" |
| #include "ui/accessibility/ax_node_data.h" |
| #include "ui/gfx/animation/throb_animation.h" |
| #include "ui/gfx/canvas.h" |
| #include "ui/gfx/color_utils.h" |
| #include "ui/gfx/font_list.h" |
| #include "ui/gfx/geometry/vector2d.h" |
| #include "ui/native_theme/native_theme.h" |
| #include "ui/native_theme/themed_vector_icon.h" |
| #include "ui/views/animation/ink_drop.h" |
| #include "ui/views/background.h" |
| #include "ui/views/controls/button/label_button_border.h" |
| #include "ui/views/metadata/metadata_impl_macros.h" |
| #include "ui/views/painter.h" |
| #include "ui/views/style/platform_style.h" |
| #include "ui/views/view_class_properties.h" |
| #include "ui/views/window/dialog_delegate.h" |
| |
| namespace views { |
| |
| LabelButton::LabelButton(ButtonListener* listener, |
| const base::string16& text, |
| int button_context) |
| : Button(listener), |
| cached_normal_font_list_( |
| style::GetFont(button_context, style::STYLE_PRIMARY)), |
| cached_default_button_font_list_( |
| style::GetFont(button_context, style::STYLE_DIALOG_BUTTON_DEFAULT)) { |
| ink_drop_container_ = AddChildView(std::make_unique<InkDropContainerView>()); |
| ink_drop_container_->SetVisible(false); |
| |
| image_ = AddChildView(std::make_unique<ImageView>()); |
| image_->set_can_process_events_within_subtree(false); |
| |
| label_ = |
| AddChildView(std::make_unique<LabelButtonLabel>(text, button_context)); |
| label_->SetAutoColorReadabilityEnabled(false); |
| label_->SetHorizontalAlignment(gfx::ALIGN_TO_HEAD); |
| |
| SetAnimationDuration(base::TimeDelta::FromMilliseconds(170)); |
| SetTextInternal(text); |
| } |
| |
| LabelButton::~LabelButton() = default; |
| |
| gfx::ImageSkia LabelButton::GetImage(ButtonState for_state) const { |
| if (for_state != STATE_NORMAL && |
| button_state_image_models_[for_state].IsEmpty()) { |
| for_state = STATE_NORMAL; |
| } |
| |
| const auto& image_model = button_state_image_models_[for_state]; |
| if (image_model.IsImage()) |
| return image_model.GetImage().AsImageSkia(); |
| |
| if (image_model.IsVectorIcon()) { |
| return ui::ThemedVectorIcon(image_model.GetVectorIcon()) |
| .GetImageSkia(GetNativeTheme()); |
| } |
| |
| return gfx::ImageSkia(); |
| } |
| |
| void LabelButton::SetImage(ButtonState for_state, const gfx::ImageSkia& image) { |
| SetImageModel(for_state, ui::ImageModel::FromImageSkia(image)); |
| } |
| |
| void LabelButton::SetImageModel(ButtonState for_state, |
| const ui::ImageModel& image_model) { |
| button_state_image_models_[for_state] = image_model; |
| UpdateImage(); |
| } |
| |
| const base::string16& LabelButton::GetText() const { |
| return label_->GetText(); |
| } |
| |
| void LabelButton::SetText(const base::string16& text) { |
| SetTextInternal(text); |
| } |
| |
| void LabelButton::ShrinkDownThenClearText() { |
| if (GetText().empty()) |
| return; |
| // First, we recalculate preferred size for the new mode (without the label). |
| shrinking_down_label_ = true; |
| PreferredSizeChanged(); |
| // Second, we clear the label right away if the button is already small. |
| ClearTextIfShrunkDown(); |
| } |
| |
| void LabelButton::SetTextColor(ButtonState for_state, SkColor color) { |
| button_state_colors_[for_state] = color; |
| if (for_state == STATE_DISABLED) |
| label_->SetDisabledColor(color); |
| else if (for_state == GetState()) |
| label_->SetEnabledColor(color); |
| explicitly_set_colors_[for_state] = true; |
| } |
| |
| void LabelButton::SetEnabledTextColors(base::Optional<SkColor> color) { |
| ButtonState states[] = {STATE_NORMAL, STATE_HOVERED, STATE_PRESSED}; |
| if (color.has_value()) { |
| for (auto state : states) |
| SetTextColor(state, color.value()); |
| return; |
| } |
| for (auto state : states) |
| explicitly_set_colors_[state] = false; |
| ResetColorsFromNativeTheme(); |
| } |
| |
| void LabelButton::SetTextShadows(const gfx::ShadowValues& shadows) { |
| label_->SetShadows(shadows); |
| } |
| |
| void LabelButton::SetTextSubpixelRenderingEnabled(bool enabled) { |
| label_->SetSubpixelRenderingEnabled(enabled); |
| } |
| |
| void LabelButton::SetElideBehavior(gfx::ElideBehavior elide_behavior) { |
| label_->SetElideBehavior(elide_behavior); |
| } |
| |
| void LabelButton::SetHorizontalAlignment(gfx::HorizontalAlignment alignment) { |
| DCHECK_NE(gfx::ALIGN_TO_HEAD, alignment); |
| if (GetHorizontalAlignment() == alignment) |
| return; |
| horizontal_alignment_ = alignment; |
| OnPropertyChanged(&min_size_, kPropertyEffectsLayout); |
| } |
| |
| gfx::HorizontalAlignment LabelButton::GetHorizontalAlignment() const { |
| return horizontal_alignment_; |
| } |
| |
| gfx::Size LabelButton::GetMinSize() const { |
| return min_size_; |
| } |
| |
| void LabelButton::SetMinSize(const gfx::Size& min_size) { |
| if (GetMinSize() == min_size) |
| return; |
| min_size_ = min_size; |
| OnPropertyChanged(&min_size_, kPropertyEffectsPreferredSizeChanged); |
| } |
| |
| gfx::Size LabelButton::GetMaxSize() const { |
| return max_size_; |
| } |
| |
| void LabelButton::SetMaxSize(const gfx::Size& max_size) { |
| if (GetMaxSize() == max_size) |
| return; |
| max_size_ = max_size; |
| OnPropertyChanged(&max_size_, kPropertyEffectsPreferredSizeChanged); |
| } |
| |
| bool LabelButton::GetIsDefault() const { |
| return is_default_; |
| } |
| |
| void LabelButton::SetIsDefault(bool is_default) { |
| // TODO(estade): move this to MdTextButton once |style_| is removed. |
| if (GetIsDefault() == is_default) |
| return; |
| is_default_ = is_default; |
| ui::Accelerator accel(ui::VKEY_RETURN, ui::EF_NONE); |
| if (is_default) |
| AddAccelerator(accel); |
| else |
| RemoveAccelerator(accel); |
| OnPropertyChanged(&is_default_, UpdateStyleToIndicateDefaultStatus()); |
| } |
| |
| int LabelButton::GetImageLabelSpacing() const { |
| return image_label_spacing_; |
| } |
| |
| void LabelButton::SetImageLabelSpacing(int spacing) { |
| if (GetImageLabelSpacing() == spacing) |
| return; |
| image_label_spacing_ = spacing; |
| OnPropertyChanged(&image_label_spacing_, |
| kPropertyEffectsPreferredSizeChanged); |
| } |
| |
| bool LabelButton::GetImageCentered() const { |
| return image_centered_; |
| } |
| |
| void LabelButton::SetImageCentered(bool image_centered) { |
| if (GetImageCentered() == image_centered) |
| return; |
| image_centered_ = image_centered; |
| OnPropertyChanged(&image_centered_, kPropertyEffectsLayout); |
| } |
| |
| std::unique_ptr<LabelButtonBorder> LabelButton::CreateDefaultBorder() const { |
| auto border = std::make_unique<LabelButtonBorder>(); |
| border->set_insets(views::LabelButtonAssetBorder::GetDefaultInsets()); |
| return border; |
| } |
| |
| void LabelButton::SetBorder(std::unique_ptr<Border> border) { |
| border_is_themed_border_ = false; |
| View::SetBorder(std::move(border)); |
| } |
| |
| void LabelButton::OnBoundsChanged(const gfx::Rect& previous_bounds) { |
| ClearTextIfShrunkDown(); |
| Button::OnBoundsChanged(previous_bounds); |
| } |
| |
| gfx::Size LabelButton::CalculatePreferredSize() const { |
| gfx::Size size = GetUnclampedSizeWithoutLabel(); |
| |
| // Account for the label only when the button is not shrinking down to hide |
| // the label entirely. |
| if (!shrinking_down_label_) { |
| const gfx::Size preferred_label_size = label_->GetPreferredSize(); |
| size.Enlarge(preferred_label_size.width(), 0); |
| size.SetToMax( |
| gfx::Size(0, preferred_label_size.height() + GetInsets().height())); |
| } |
| |
| size.SetToMax(GetMinSize()); |
| |
| // Clamp size to max size (if valid). |
| const gfx::Size max_size = GetMaxSize(); |
| if (max_size.width() > 0) |
| size.set_width(std::min(max_size.width(), size.width())); |
| if (max_size.height() > 0) |
| size.set_height(std::min(max_size.height(), size.height())); |
| |
| return size; |
| } |
| |
| gfx::Size LabelButton::GetMinimumSize() const { |
| if (label_->GetElideBehavior() == gfx::ElideBehavior::NO_ELIDE) |
| return GetPreferredSize(); |
| |
| gfx::Size size = image_->GetPreferredSize(); |
| const gfx::Insets insets(GetInsets()); |
| size.Enlarge(insets.width(), insets.height()); |
| |
| size.SetToMax(GetMinSize()); |
| const gfx::Size max_size = GetMaxSize(); |
| if (max_size.width() > 0) |
| size.set_width(std::min(max_size.width(), size.width())); |
| if (max_size.height() > 0) |
| size.set_height(std::min(max_size.height(), size.height())); |
| |
| return size; |
| } |
| |
| int LabelButton::GetHeightForWidth(int width) const { |
| const gfx::Size size_without_label = GetUnclampedSizeWithoutLabel(); |
| // Get label height for the remaining width. |
| const int label_height_with_insets = |
| label_->GetHeightForWidth(width - size_without_label.width()) + |
| GetInsets().height(); |
| |
| // Height is the larger of size without label and label height with insets. |
| int height = std::max(size_without_label.height(), label_height_with_insets); |
| |
| height = std::max(height, GetMinSize().height()); |
| |
| // Clamp height to the maximum height (if valid). |
| const gfx::Size max_size = GetMaxSize(); |
| if (max_size.height() > 0) |
| return std::min(max_size.height(), height); |
| |
| return height; |
| } |
| |
| void LabelButton::Layout() { |
| gfx::Rect image_area = GetLocalBounds(); |
| |
| ink_drop_container_->SetBoundsRect(image_area); |
| |
| gfx::Insets insets = GetInsets(); |
| // If the button have a limited space to fit in, the image and the label |
| // may overlap with the border, which often times contains a lot of empty |
| // padding. |
| image_area.Inset(insets.left(), 0, insets.right(), 0); |
| // The space that the label can use. Labels truncate horizontally, so there |
| // is no need to allow the label to take up the complete horizontal space. |
| gfx::Rect label_area = image_area; |
| |
| gfx::Size image_size = image_->GetPreferredSize(); |
| image_size.SetToMin(image_area.size()); |
| |
| const auto horizontal_alignment = GetHorizontalAlignment(); |
| if (!image_size.IsEmpty()) { |
| int image_space = image_size.width() + GetImageLabelSpacing(); |
| if (horizontal_alignment == gfx::ALIGN_RIGHT) |
| label_area.Inset(0, 0, image_space, 0); |
| else |
| label_area.Inset(image_space, 0, 0, 0); |
| } |
| |
| gfx::Size label_size( |
| std::min(label_area.width(), label_->GetPreferredSize().width()), |
| label_area.height()); |
| |
| gfx::Point image_origin = image_area.origin(); |
| if (label_->GetMultiLine() && !image_centered_) { |
| // This code assumes the text is vertically centered. |
| DCHECK_EQ(gfx::ALIGN_MIDDLE, label_->GetVerticalAlignment()); |
| int label_height = label_->GetHeightForWidth(label_size.width()); |
| int first_line_y = |
| label_area.y() + (label_area.height() - label_height) / 2; |
| int image_origin_y = |
| first_line_y + |
| (label_->font_list().GetHeight() - image_size.height()) / 2; |
| image_origin.Offset(0, std::max(0, image_origin_y)); |
| } else { |
| image_origin.Offset(0, (image_area.height() - image_size.height()) / 2); |
| } |
| if (horizontal_alignment == gfx::ALIGN_CENTER) { |
| const int spacing = (image_size.width() > 0 && label_size.width() > 0) |
| ? GetImageLabelSpacing() |
| : 0; |
| const int total_width = image_size.width() + label_size.width() + spacing; |
| image_origin.Offset((image_area.width() - total_width) / 2, 0); |
| } else if (horizontal_alignment == gfx::ALIGN_RIGHT) { |
| image_origin.Offset(image_area.width() - image_size.width(), 0); |
| } |
| image_->SetBoundsRect(gfx::Rect(image_origin, image_size)); |
| |
| gfx::Rect label_bounds = label_area; |
| if (label_area.width() == label_size.width()) { |
| // Label takes up the whole area. |
| } else if (horizontal_alignment == gfx::ALIGN_CENTER) { |
| label_bounds.ClampToCenteredSize(label_size); |
| } else { |
| label_bounds.set_size(label_size); |
| if (horizontal_alignment == gfx::ALIGN_RIGHT) |
| label_bounds.Offset(label_area.width() - label_size.width(), 0); |
| } |
| |
| label_->SetBoundsRect(label_bounds); |
| Button::Layout(); |
| } |
| |
| void LabelButton::EnableCanvasFlippingForRTLUI(bool flip) { |
| Button::EnableCanvasFlippingForRTLUI(flip); |
| image_->EnableCanvasFlippingForRTLUI(flip); |
| } |
| |
| void LabelButton::GetAccessibleNodeData(ui::AXNodeData* node_data) { |
| if (GetIsDefault()) |
| node_data->AddState(ax::mojom::State::kDefault); |
| Button::GetAccessibleNodeData(node_data); |
| } |
| |
| ui::NativeTheme::Part LabelButton::GetThemePart() const { |
| return ui::NativeTheme::kPushButton; |
| } |
| |
| gfx::Rect LabelButton::GetThemePaintRect() const { |
| return GetLocalBounds(); |
| } |
| |
| ui::NativeTheme::State LabelButton::GetThemeState( |
| ui::NativeTheme::ExtraParams* params) const { |
| GetExtraParams(params); |
| switch (GetState()) { |
| case STATE_NORMAL: |
| return ui::NativeTheme::kNormal; |
| case STATE_HOVERED: |
| return ui::NativeTheme::kHovered; |
| case STATE_PRESSED: |
| return ui::NativeTheme::kPressed; |
| case STATE_DISABLED: |
| return ui::NativeTheme::kDisabled; |
| case STATE_COUNT: |
| NOTREACHED(); |
| } |
| return ui::NativeTheme::kNormal; |
| } |
| |
| const gfx::Animation* LabelButton::GetThemeAnimation() const { |
| return &hover_animation(); |
| } |
| |
| ui::NativeTheme::State LabelButton::GetBackgroundThemeState( |
| ui::NativeTheme::ExtraParams* params) const { |
| GetExtraParams(params); |
| return ui::NativeTheme::kNormal; |
| } |
| |
| ui::NativeTheme::State LabelButton::GetForegroundThemeState( |
| ui::NativeTheme::ExtraParams* params) const { |
| GetExtraParams(params); |
| return ui::NativeTheme::kHovered; |
| } |
| |
| void LabelButton::UpdateImage() { |
| image_->SetImage(GetImage(GetVisualState())); |
| } |
| |
| void LabelButton::AddLayerBeneathView(ui::Layer* new_layer) { |
| image()->SetPaintToLayer(); |
| image()->layer()->SetFillsBoundsOpaquely(false); |
| ink_drop_container()->SetVisible(true); |
| ink_drop_container()->AddLayerBeneathView(new_layer); |
| } |
| |
| void LabelButton::RemoveLayerBeneathView(ui::Layer* old_layer) { |
| ink_drop_container()->RemoveLayerBeneathView(old_layer); |
| ink_drop_container()->SetVisible(false); |
| image()->DestroyLayer(); |
| } |
| |
| void LabelButton::GetExtraParams(ui::NativeTheme::ExtraParams* params) const { |
| params->button.checked = false; |
| params->button.indeterminate = false; |
| params->button.is_default = GetIsDefault(); |
| params->button.is_focused = HasFocus() && IsAccessibilityFocusable(); |
| params->button.has_border = false; |
| params->button.classic_state = 0; |
| params->button.background_color = label_->GetBackgroundColor(); |
| } |
| |
| PropertyEffects LabelButton::UpdateStyleToIndicateDefaultStatus() { |
| // Check that a subclass hasn't replaced the Label font. These buttons may |
| // never be given default status. |
| DCHECK_EQ(cached_normal_font_list_.GetFontSize(), |
| label()->font_list().GetFontSize()); |
| // TODO(tapted): This should use style::GetFont(), but this part can just be |
| // deleted when default buttons no longer go bold. Colors will need updating |
| // still. |
| label_->SetFontList(GetIsDefault() ? cached_default_button_font_list_ |
| : cached_normal_font_list_); |
| ResetLabelEnabledColor(); |
| return kPropertyEffectsPreferredSizeChanged; |
| } |
| |
| void LabelButton::ChildPreferredSizeChanged(View* child) { |
| PreferredSizeChanged(); |
| } |
| |
| void LabelButton::AddedToWidget() { |
| if (PlatformStyle::kInactiveWidgetControlsAppearDisabled) { |
| paint_as_active_subscription_ = |
| GetWidget()->RegisterPaintAsActiveChangedCallback(base::BindRepeating( |
| &LabelButton::VisualStateChanged, base::Unretained(this))); |
| } |
| } |
| |
| void LabelButton::RemovedFromWidget() { |
| paint_as_active_subscription_.reset(); |
| } |
| |
| void LabelButton::OnFocus() { |
| Button::OnFocus(); |
| // Typically the border renders differently when focused. |
| SchedulePaint(); |
| } |
| |
| void LabelButton::OnBlur() { |
| Button::OnBlur(); |
| // Typically the border renders differently when focused. |
| SchedulePaint(); |
| } |
| |
| void LabelButton::OnThemeChanged() { |
| Button::OnThemeChanged(); |
| ResetColorsFromNativeTheme(); |
| UpdateImage(); |
| if (border_is_themed_border_) |
| View::SetBorder(PlatformStyle::CreateThemedLabelButtonBorder(this)); |
| ResetLabelEnabledColor(); |
| // The entire button has to be repainted here, since the native theme can |
| // define the tint for the entire background/border/focus ring. |
| SchedulePaint(); |
| } |
| |
| void LabelButton::StateChanged(ButtonState old_state) { |
| Button::StateChanged(old_state); |
| ResetLabelEnabledColor(); |
| VisualStateChanged(); |
| } |
| |
| void LabelButton::SetTextInternal(const base::string16& text) { |
| SetAccessibleName(text); |
| label_->SetText(text); |
| |
| // Setting text cancels ShrinkDownThenClearText(). |
| const auto effects = shrinking_down_label_ |
| ? kPropertyEffectsPreferredSizeChanged |
| : kPropertyEffectsNone; |
| shrinking_down_label_ = false; |
| |
| // TODO(pkasting): Remove this and forward callback subscriptions to the |
| // underlying label property when Label is converted to properties. |
| OnPropertyChanged(label_, effects); |
| } |
| |
| void LabelButton::ClearTextIfShrunkDown() { |
| const gfx::Size preferred_size = GetPreferredSize(); |
| if (shrinking_down_label_ && width() <= preferred_size.width() && |
| height() <= preferred_size.height()) { |
| // Once the button shrinks down to its preferred size (that disregards the |
| // current text), we finish the operation by clearing the text. |
| SetText(base::string16()); |
| } |
| } |
| |
| gfx::Size LabelButton::GetUnclampedSizeWithoutLabel() const { |
| const gfx::Size image_size = image_->GetPreferredSize(); |
| gfx::Size size = image_size; |
| const gfx::Insets insets(GetInsets()); |
| size.Enlarge(insets.width(), insets.height()); |
| |
| // Accommodate for spacing between image and text if both are present. |
| if (image_size.width() > 0 && !GetText().empty() && !shrinking_down_label_) |
| size.Enlarge(GetImageLabelSpacing(), 0); |
| |
| // Make the size at least as large as the minimum size needed by the border. |
| if (border()) |
| size.SetToMax(border()->GetMinimumSize()); |
| |
| return size; |
| } |
| |
| Button::ButtonState LabelButton::GetVisualState() const { |
| const bool force_disabled = |
| PlatformStyle::kInactiveWidgetControlsAppearDisabled && GetWidget() && |
| !GetWidget()->ShouldPaintAsActive(); |
| return force_disabled ? STATE_DISABLED : GetState(); |
| } |
| |
| void LabelButton::VisualStateChanged() { |
| UpdateImage(); |
| label_->SetEnabled(GetVisualState() != STATE_DISABLED); |
| } |
| |
| void LabelButton::ResetColorsFromNativeTheme() { |
| const ui::NativeTheme* theme = GetNativeTheme(); |
| // Since this is a LabelButton, use the label colors. |
| SkColor colors[STATE_COUNT] = { |
| theme->GetSystemColor(ui::NativeTheme::kColorId_LabelEnabledColor), |
| theme->GetSystemColor(ui::NativeTheme::kColorId_LabelEnabledColor), |
| theme->GetSystemColor(ui::NativeTheme::kColorId_LabelEnabledColor), |
| theme->GetSystemColor(ui::NativeTheme::kColorId_LabelDisabledColor), |
| }; |
| |
| label_->SetBackground(nullptr); |
| label_->SetAutoColorReadabilityEnabled(false); |
| |
| for (size_t state = STATE_NORMAL; state < STATE_COUNT; ++state) { |
| if (!explicitly_set_colors_[state]) { |
| SetTextColor(static_cast<ButtonState>(state), colors[state]); |
| explicitly_set_colors_[state] = false; |
| } |
| } |
| } |
| |
| void LabelButton::ResetLabelEnabledColor() { |
| const SkColor color = button_state_colors_[GetState()]; |
| if (GetState() != STATE_DISABLED && label_->GetEnabledColor() != color) |
| label_->SetEnabledColor(color); |
| } |
| |
| BEGIN_METADATA(LabelButton) |
| METADATA_PARENT_CLASS(Button) |
| ADD_PROPERTY_METADATA(LabelButton, base::string16, Text) |
| ADD_PROPERTY_METADATA(LabelButton, |
| gfx::HorizontalAlignment, |
| HorizontalAlignment) |
| ADD_PROPERTY_METADATA(LabelButton, gfx::Size, MinSize) |
| ADD_PROPERTY_METADATA(LabelButton, gfx::Size, MaxSize) |
| ADD_PROPERTY_METADATA(LabelButton, bool, IsDefault) |
| ADD_PROPERTY_METADATA(LabelButton, int, ImageLabelSpacing) |
| ADD_PROPERTY_METADATA(LabelButton, bool, ImageCentered) |
| END_METADATA() |
| |
| } // namespace views |