910e62b5创建于 1月15日历史提交
// Copyright 2012 The Chromium Authors
// 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/checkbox.h"

#include <stddef.h>

#include <memory>
#include <utility>
#include <variant>

#include "base/functional/bind.h"
#include "third_party/skia/include/core/SkPath.h"
#include "ui/accessibility/ax_node_data.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/base/ui_base_features.h"
#include "ui/color/color_id.h"
#include "ui/color/color_provider.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/color_palette.h"
#include "ui/gfx/color_utils.h"
#include "ui/gfx/geometry/size_f.h"
#include "ui/gfx/geometry/skia_conversions.h"
#include "ui/gfx/image/image_skia_operations.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/native_theme/native_theme.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/animation/ink_drop.h"
#include "ui/views/animation/ink_drop_host.h"
#include "ui/views/animation/ink_drop_impl.h"
#include "ui/views/animation/ink_drop_painted_layer_delegates.h"
#include "ui/views/animation/ink_drop_ripple.h"
#include "ui/views/controls/button/button_controller.h"
#include "ui/views/controls/button/label_button_border.h"
#include "ui/views/controls/focus_ring.h"
#include "ui/views/controls/highlight_path_generator.h"
#include "ui/views/layout/layout_provider.h"
#include "ui/views/painter.h"
#include "ui/views/resources/grit/views_resources.h"
#include "ui/views/style/platform_style.h"
#include "ui/views/style/typography.h"
#include "ui/views/vector_icons.h"

namespace views {

namespace {
constexpr gfx::Size kCheckboxInkDropSize = gfx::Size(24, 24);
constexpr float kCheckboxIconDipSize = 16;
constexpr int kCheckboxIconCornerRadius = 2;
}  // namespace

class Checkbox::FocusRingHighlightPathGenerator
    : public views::HighlightPathGenerator {
 public:
  SkPath GetHighlightPath(const views::View* view) override {
    SkPath path;
    auto* checkbox = static_cast<const views::Checkbox*>(view);
    if (checkbox->image_container_view()->bounds().IsEmpty()) {
      return path;
    }
    return checkbox->GetFocusRingPath();
  }
};

Checkbox::Checkbox(const std::u16string& label,
                   PressedCallback callback,
                   int button_context)
    : LabelButton(std::move(callback), label, button_context) {
  SetImageCentered(false);
  SetHorizontalAlignment(gfx::ALIGN_LEFT);

  SetRequestFocusOnPress(false);
  InkDrop::Get(this)->SetMode(views::InkDropHost::InkDropMode::ON);
  SetHasInkDropActionOnClick(true);
  InkDrop::UseInkDropWithoutAutoHighlight(InkDrop::Get(this),
                                          /*highlight_on_hover=*/false);
  InkDrop::Get(this)->SetCreateRippleCallback(base::BindRepeating(
      [](Checkbox* host) {
        // The "small" size is 21dp, the large size is 1.33 * 21dp = 28dp.
        return InkDrop::Get(host)->CreateSquareRipple(
            host->image_container_view()
                ->GetMirroredContentsBounds()
                .CenterPoint(),
            gfx::Size(21, 21));
      },
      this));
  InkDrop::Get(this)->SetBaseColorCallback(base::BindRepeating(
      [](Checkbox* host) {
        // Usually ink-drop ripples match the text color. Checkboxes use the
        // color of the unchecked, enabled icon.
        return host->GetIconImageColor(IconState::ENABLED);
      },
      this));

  // Limit the checkbox height to match the legacy appearance.
  const gfx::Size preferred_size(LabelButton::CalculatePreferredSize({}));
  SetMinSize(gfx::Size(0, preferred_size.height() + 4));

  // Checkboxes always have a focus ring, even when the platform otherwise
  // doesn't generally use them for buttons.
  SetInstallFocusRingOnFocus(true);
  FocusRing::Get(this)->SetPathGenerator(
      std::make_unique<FocusRingHighlightPathGenerator>());

  // Avoid the default ink-drop mask to allow the ripple effect to extend beyond
  // the checkbox view (otherwise it gets clipped which looks weird).
  views::InstallEmptyHighlightPathGenerator(this);

  InkDrop::Install(image_container_view(),
                   std::make_unique<InkDropHost>(image_container_view()));
  SetInkDropView(image_container_view());
  InkDrop::Get(image_container_view())->SetMode(InkDropHost::InkDropMode::ON);

  // Allow ImageView to capture mouse events in order for InkDrop effects to
  // trigger.
  image_container_view()->SetCanProcessEventsWithinSubtree(true);

  // Avoid the default ink-drop mask to allow the InkDrop effect to extend
  // beyond the image view (otherwise it gets clipped which looks weird).
  views::InstallEmptyHighlightPathGenerator(image_container_view());

  InkDrop::Get(image_container_view())
      ->SetCreateHighlightCallback(base::BindRepeating(
          [](View* host) {
            int radius =
                InkDropHost::GetLargeSize(kCheckboxInkDropSize).width() / 2;
            return std::make_unique<views::InkDropHighlight>(
                gfx::PointF(host->GetContentsBounds().CenterPoint()),
                std::make_unique<CircleLayerDelegate>(
                    views::InkDrop::Get(host)->GetBaseColor(), radius));
          },
          image_container_view()));

  InkDrop::Get(image_container_view())
      ->SetCreateRippleCallback(base::BindRepeating(
          [](View* host) {
            return InkDrop::Get(host)->CreateSquareRipple(
                host->GetContentsBounds().CenterPoint(), kCheckboxInkDropSize);
          },
          image_container_view()));

  // Usually ink-drop ripples match the text color. Checkboxes use the
  // color of the unchecked, enabled icon.
  InkDrop::Get(image_container_view())
      ->SetBaseColor(ui::kColorCheckboxForegroundUnchecked);

  GetViewAccessibility().SetRole(ax::mojom::Role::kCheckBox);
  SetAndUpdateAccessibleDefaultActionVerb();
  UpdateAccessibleCheckedState();
}

Checkbox::~Checkbox() = default;

void Checkbox::SetChecked(bool checked) {
  if (GetChecked() == checked) {
    return;
  }
  checked_ = checked;
  UpdateImage();
  OnPropertyChanged(&checked_, PropertyEffects::kNone);
  NotifyViewControllerCallback();
  SetAndUpdateAccessibleDefaultActionVerb();
  UpdateAccessibleCheckedState();
}

bool Checkbox::GetChecked() const {
  return checked_;
}

base::CallbackListSubscription Checkbox::AddCheckedChangedCallback(
    PropertyChangedCallback callback) {
  return AddPropertyChangedCallback(&checked_, callback);
}

void Checkbox::SetMultiLine(bool multi_line) {
  if (GetMultiLine() == multi_line) {
    return;
  }
  label()->SetMultiLine(multi_line);
  // TODO(pkasting): Remove this and forward callback subscriptions to the
  // underlying label property when Label is converted to properties.
  OnPropertyChanged(this, PropertyEffects::kNone);
}

bool Checkbox::GetMultiLine() const {
  return label()->GetMultiLine();
}

void Checkbox::SetCheckedIconImageColor(SkColor color) {
  checked_icon_image_color_ = color;
}

gfx::ImageSkia Checkbox::GetImage(ButtonState for_state) const {
  const int icon_state = GetIconState(for_state);

  const SkColor container_color = GetIconImageColor(icon_state);
  if (GetChecked()) {
    const gfx::ImageSkia check_icon = gfx::CreateVectorIcon(
        GetVectorIcon(), kCheckboxIconDipSize, GetIconCheckColor(icon_state));

    return gfx::ImageSkiaOperations::CreateImageWithRoundRectBackground(
        gfx::SizeF(kCheckboxIconDipSize, kCheckboxIconDipSize),
        kCheckboxIconCornerRadius, container_color, check_icon);
  }
  return gfx::CreateVectorIcon(GetVectorIcon(), kCheckboxIconDipSize,
                               container_color);
}

std::unique_ptr<LabelButtonBorder> Checkbox::CreateDefaultBorder() const {
  std::unique_ptr<LabelButtonBorder> border =
      LabelButton::CreateDefaultBorder();
  border->set_insets(
      LayoutProvider::Get()->GetInsetsMetric(INSETS_CHECKBOX_RADIO_BUTTON));
  return border;
}

std::unique_ptr<ActionViewInterface> Checkbox::GetActionViewInterface() {
  return std::make_unique<CheckboxActionViewInterface>(this);
}

void Checkbox::UpdateAccessibleCheckedState() {
  GetViewAccessibility().SetCheckedState(GetChecked()
                                             ? ax::mojom::CheckedState::kTrue
                                             : ax::mojom::CheckedState::kFalse);
}

void Checkbox::OnThemeChanged() {
  LabelButton::OnThemeChanged();
}

SkPath Checkbox::GetFocusRingPath() const {
  gfx::Rect bounds = image_container_view()->GetMirroredContentsBounds();
  return SkPath::Rect(RectToSkRect(bounds));
}

SkColor Checkbox::GetIconImageColor(int icon_state) const {
  if (icon_state & IconState::CHECKED) {
    return GetColorProvider()->GetColor(
        (icon_state & IconState::ENABLED)
            ? ui::kColorCheckboxContainer
            : ui::kColorCheckboxContainerDisabled);
  }
  return GetColorProvider()->GetColor((icon_state & IconState::ENABLED)
                                          ? ui::kColorCheckboxOutline
                                          : ui::kColorCheckboxOutlineDisabled);
}

SkColor Checkbox::GetIconCheckColor(int icon_state) const {
  DCHECK(GetChecked());

  // Use the overridden checked icon image color instead if set.
  if (checked_icon_image_color_.has_value()) {
    return checked_icon_image_color_.value();
  }

  return GetColorProvider()->GetColor((icon_state & IconState::ENABLED)
                                          ? ui::kColorCheckboxCheck
                                          : ui::kColorCheckboxCheckDisabled);
}

const gfx::VectorIcon& Checkbox::GetVectorIcon() const {
  return GetChecked() ? kCheckboxCheckCr2023Icon : kCheckboxNormalCr2023Icon;
}

int Checkbox::GetIconState(ButtonState for_state) const {
  int icon_state = 0;
  if (GetChecked()) {
    icon_state |= IconState::CHECKED;
  }
  if (for_state != STATE_DISABLED) {
    icon_state |= IconState::ENABLED;
  }
  return icon_state;
}

void Checkbox::NotifyClick(const ui::Event& event) {
  SetChecked(!GetChecked());
  LabelButton::NotifyClick(event);
}

ui::NativeTheme::Part Checkbox::GetThemePart() const {
  return ui::NativeTheme::kCheckbox;
}

void Checkbox::GetExtraParams(ui::NativeTheme::ExtraParams* params) const {
  LabelButton::GetExtraParams(params);
  std::get<ui::NativeTheme::ButtonExtraParams>(*params).checked = GetChecked();
}

void Checkbox::SetAndUpdateAccessibleDefaultActionVerb() {
  SetDefaultActionVerb(checked_ ? ax::mojom::DefaultActionVerb::kUncheck
                                : ax::mojom::DefaultActionVerb::kCheck);
  UpdateAccessibleDefaultActionVerb();
}

CheckboxActionViewInterface::CheckboxActionViewInterface(Checkbox* action_view)
    : LabelButtonActionViewInterface(action_view), action_view_(action_view) {}

void CheckboxActionViewInterface::ActionItemChangedImpl(
    actions::ActionItem* action_item) {
  LabelButtonActionViewInterface::ActionItemChangedImpl(action_item);
  action_view_->SetChecked(action_item->GetChecked());
}

void CheckboxActionViewInterface::OnViewChangedImpl(
    actions::ActionItem* action_item) {
  LabelButtonActionViewInterface::OnViewChangedImpl(action_item);
  // The checked property is tied together for all checkboxes that are linked to
  // the same ActionItem.
  action_item->SetChecked(action_view_->GetChecked());
}

BEGIN_METADATA(Checkbox)
ADD_PROPERTY_METADATA(bool, Checked)
ADD_PROPERTY_METADATA(bool, MultiLine)
END_METADATA

}  // namespace views