910e62b5创建于 1月15日历史提交
// Copyright 2020 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/bubble/bubble_dialog_model_host.h"

#include <utility>

#include "base/memory/raw_ptr.h"
#include "ui/accessibility/ax_enums.mojom.h"
#include "ui/base/class_property.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_header_macros.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/base/models/combobox_model.h"
#include "ui/base/models/dialog_model.h"
#include "ui/base/models/dialog_model_field.h"
#include "ui/base/models/simple_combobox_model.h"
#include "ui/base/mojom/dialog_button.mojom.h"
#include "ui/base/mojom/ui_base_types.mojom-shared.h"
#include "ui/base/ui_base_types.h"
#include "ui/gfx/color_utils.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/strings/grit/ui_strings.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/border.h"
#include "ui/views/bubble/bubble_dialog_delegate_view.h"
#include "ui/views/bubble/bubble_dialog_utils.h"
#include "ui/views/controls/button/checkbox.h"
#include "ui/views/controls/button/label_button_border.h"
#include "ui/views/controls/button/md_text_button.h"
#include "ui/views/controls/combobox/combobox.h"
#include "ui/views/controls/editable_combobox/editable_password_combobox.h"
#include "ui/views/controls/label.h"
#include "ui/views/controls/scroll_view.h"
#include "ui/views/controls/separator.h"
#include "ui/views/controls/styled_label.h"
#include "ui/views/controls/textfield/textfield.h"
#include "ui/views/controls/theme_tracking_image_view.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/layout/box_layout_view.h"
#include "ui/views/layout/fill_layout.h"
#include "ui/views/layout/layout_provider.h"
#include "ui/views/style/typography.h"
#include "ui/views/view_class_properties.h"

namespace views {
namespace {

// Extra margin to be added to contents view when it's inside a scroll view.
constexpr int kScrollViewVerticalMargin = 2;

BubbleDialogModelHost::FieldType GetFieldTypeForField(
    ui::DialogModelField* field) {
  DCHECK(field);
  switch (field->type()) {
    case ui::DialogModelField::kParagraph:
      return BubbleDialogModelHost::FieldType::kText;
    case ui::DialogModelField::kCheckbox:
      return BubbleDialogModelHost::FieldType::kControl;
    case ui::DialogModelField::kTextfield:
      return BubbleDialogModelHost::FieldType::kControl;
    case ui::DialogModelField::kPasswordField:
      return BubbleDialogModelHost::FieldType::kControl;
    case ui::DialogModelField::kCombobox:
      return BubbleDialogModelHost::FieldType::kControl;
    case ui::DialogModelField::kMenuItem:
      return BubbleDialogModelHost::FieldType::kMenuItem;
    case ui::DialogModelField::kTitleItem:
      // No need to handle titles.
      NOTREACHED();
    case ui::DialogModelField::kSection:
      // TODO(pbos): Handle nested/multiple sections.
      NOTREACHED();
    case ui::DialogModelField::kSeparator:
      return BubbleDialogModelHost::FieldType::kMenuItem;
    case ui::DialogModelField::kCustom:
      return static_cast<BubbleDialogModelHost::CustomView*>(
                 field->AsCustomField()->field())
          ->field_type();
  }
}

int GetFieldTopMargin(LayoutProvider* layout_provider,
                      const BubbleDialogModelHost::FieldType& field_type,
                      const BubbleDialogModelHost::FieldType& last_field_type) {
  // Menu item preceded by non-menu item should have margin
  if (field_type == BubbleDialogModelHost::FieldType::kMenuItem &&
      last_field_type == BubbleDialogModelHost::FieldType::kMenuItem) {
    return 0;
  }
  const bool is_control_list =
      field_type == BubbleDialogModelHost::FieldType::kControl &&
      last_field_type == BubbleDialogModelHost::FieldType::kControl;
  return layout_provider->GetDistanceMetric(
      is_control_list ? DISTANCE_CONTROL_LIST_VERTICAL
                      : DISTANCE_UNRELATED_CONTROL_VERTICAL);
}

int GetDialogTopMargins(LayoutProvider* layout_provider,
                        ui::DialogModelField* first_field) {
  CHECK(first_field);

  const BubbleDialogModelHost::FieldType field_type =
      GetFieldTypeForField(first_field);
  switch (field_type) {
    case BubbleDialogModelHost::FieldType::kMenuItem:
      return 0;
    case BubbleDialogModelHost::FieldType::kControl:
      return layout_provider->GetDistanceMetric(
          DISTANCE_DIALOG_CONTENT_MARGIN_TOP_CONTROL);
    case BubbleDialogModelHost::FieldType::kText:
      return layout_provider->GetDistanceMetric(
          DISTANCE_DIALOG_CONTENT_MARGIN_TOP_TEXT);
  }
}

int GetDialogBottomMargins(LayoutProvider* layout_provider,
                           ui::DialogModelField* last_field,
                           bool has_buttons) {
  CHECK(last_field);
  const BubbleDialogModelHost::FieldType field_type =
      GetFieldTypeForField(last_field);
  switch (field_type) {
    case BubbleDialogModelHost::FieldType::kMenuItem:
      return has_buttons ? layout_provider->GetDistanceMetric(
                               DISTANCE_DIALOG_CONTENT_MARGIN_BOTTOM_CONTROL)
                         : 0;
    case BubbleDialogModelHost::FieldType::kControl:
      return layout_provider->GetDistanceMetric(
          DISTANCE_DIALOG_CONTENT_MARGIN_BOTTOM_CONTROL);
    case BubbleDialogModelHost::FieldType::kText:
      return layout_provider->GetDistanceMetric(
          DISTANCE_DIALOG_CONTENT_MARGIN_BOTTOM_TEXT);
  }
}

// A subclass of Checkbox that allows using an external Label/StyledLabel view
// instead of LabelButton's internal label. This is required for the
// Label/StyledLabel to be clickable, while supporting Links which requires a
// StyledLabel.
class CheckboxControl : public Checkbox {
  METADATA_HEADER(CheckboxControl, Checkbox)

 public:
  CheckboxControl(std::unique_ptr<View> label, int label_line_height)
      : label_line_height_(label_line_height) {
    auto* layout = SetLayoutManager(std::make_unique<BoxLayout>());
    layout->set_between_child_spacing(LayoutProvider::Get()->GetDistanceMetric(
        DISTANCE_RELATED_LABEL_HORIZONTAL));
    layout->set_cross_axis_alignment(BoxLayout::CrossAxisAlignment::kStart);

    // TODO(accessibility): There is no `SetAccessibilityProperties` which takes
    // a labelling view to set the accessible name.
    GetViewAccessibility().SetName(*label.get());

    AddChildView(std::move(label));
  }

  gfx::Size CalculatePreferredSize(
      const SizeBounds& available_size) const override {
    // Skip LabelButton to use LayoutManager.
    return View::CalculatePreferredSize(available_size);
  }

  void OnThemeChanged() override {
    Checkbox::OnThemeChanged();
    // This offsets the image to align with the first line of text. See
    // LabelButton::Layout().
    image_container_view()->SetBorder(CreateEmptyBorder(gfx::Insets::TLBR(
        (label_line_height_ -
         image_container_view()->GetPreferredSize({}).height()) /
            2,
        0, 0, 0)));
  }

  const int label_line_height_;
};

BEGIN_METADATA(CheckboxControl)
END_METADATA

struct DialogModelHostField {
  raw_ptr<ui::DialogModelField> dialog_model_field = nullptr;

  // View representing the entire field.
  raw_ptr<View> field_view = nullptr;

  // Child view to |field_view|, if any, that's used for focus. For instance,
  // a textfield row would be a container that contains both a
  // views::Textfield and a descriptive label. In this case |focusable_view|
  // would refer to the views::Textfield which is also what would gain focus.
  raw_ptr<View> focusable_view = nullptr;
};

View* GetTargetView(const DialogModelHostField& field_view_info) {
  return field_view_info.focusable_view ? field_view_info.focusable_view.get()
                                        : field_view_info.field_view.get();
}

// TODO(pbos): Consider externalizing this functionality into a different
// format that could feasibly be adopted by LayoutManagers. This is used for
// BoxLayouts (but could be others) to agree on columns' preferred width as a
// replacement for using GridLayout.
class LayoutConsensusGroup {
 public:
  LayoutConsensusGroup() = default;
  ~LayoutConsensusGroup() { DCHECK(children_.empty()); }

  void AddView(View* view) {
    children_.insert(view);
    // Because this may change the max preferred/min size, invalidate all child
    // layouts.
    for (View* child : children_) {
      child->InvalidateLayout();
    }
  }

  void RemoveView(View* view) { children_.erase(view); }

  // Get the union of all preferred sizes within the group.
  gfx::Size GetMaxPreferredSize(const SizeBounds& available_size) const {
    gfx::Size size;
    for (View* child : children_) {
      DCHECK_EQ(1u, child->children().size());
      size.SetToMax(
          child->children().front()->GetPreferredSize(available_size));
    }
    return size;
  }

  // Get the union of all minimum sizes within the group.
  gfx::Size GetMaxMinimumSize() const {
    gfx::Size size;
    for (View* child : children_) {
      DCHECK_EQ(1u, child->children().size());
      size.SetToMax(child->children().front()->GetMinimumSize());
    }
    return size;
  }

 private:
  base::flat_set<raw_ptr<View, CtnExperimental>> children_;
};

class LayoutConsensusView : public View {
  METADATA_HEADER(LayoutConsensusView, View)

 public:
  LayoutConsensusView(LayoutConsensusGroup* group, std::unique_ptr<View> view)
      : group_(group) {
    group->AddView(this);
    SetLayoutManager(std::make_unique<FillLayout>());
    AddChildView(std::move(view));
  }

  ~LayoutConsensusView() override { group_->RemoveView(this); }

  gfx::Size CalculatePreferredSize(
      const SizeBounds& available_size) const override {
    const gfx::Size group_preferred_size =
        group_->GetMaxPreferredSize(available_size);
    DCHECK_EQ(1u, children().size());
    const gfx::Size child_preferred_size =
        children()[0]->GetPreferredSize(available_size);
    // TODO(pbos): This uses the max width, but could be configurable to use
    // either direction.
    return gfx::Size(group_preferred_size.width(),
                     child_preferred_size.height());
  }

  gfx::Size GetMinimumSize() const override {
    const gfx::Size group_minimum_size = group_->GetMaxMinimumSize();
    DCHECK_EQ(1u, children().size());
    const gfx::Size child_minimum_size = children()[0]->GetMinimumSize();
    // TODO(pbos): This uses the max width, but could be configurable to use
    // either direction.
    return gfx::Size(group_minimum_size.width(), child_minimum_size.height());
  }

 private:
  const raw_ptr<LayoutConsensusGroup> group_;
};

BEGIN_METADATA(LayoutConsensusView)
END_METADATA

}  // namespace

BubbleDialogModelHost::CustomView::CustomView(std::unique_ptr<View> view,
                                              FieldType field_type,
                                              View* focusable_view)
    : view_(std::move(view)),
      field_type_(field_type),
      focusable_view_(focusable_view) {}

BubbleDialogModelHost::CustomView::~CustomView() = default;

std::unique_ptr<View> BubbleDialogModelHost::CustomView::TransferView() {
  DCHECK(view_);
  return std::move(view_);
}

class BubbleDialogModelHostContentsView final : public DialogModelSectionHost {
  METADATA_HEADER(BubbleDialogModelHostContentsView, DialogModelSectionHost)

 public:
  // TODO(pbos): Break this dependency on BubbleDialogModelHost once most of
  // OnFieldAdded etc. has moved into this class.
  BubbleDialogModelHostContentsView(
      ui::DialogModelSection* contents,
      ui::ElementIdentifier initially_focused_field_id)
      : contents_(contents),
        initially_focused_field_id_(initially_focused_field_id),
        on_field_added_subscription_(
            contents->AddOnFieldAddedCallback(base::BindRepeating(
                &BubbleDialogModelHostContentsView::OnFieldAdded,
                base::Unretained(this)))),
        on_field_changed_subscription_(
            contents->AddOnFieldChangedCallback(base::BindRepeating(
                &BubbleDialogModelHostContentsView::OnFieldChanged,
                base::Unretained(this)))) {
    // Note that between-child spacing is manually handled using kMarginsKey.
    SetOrientation(views::BoxLayout::Orientation::kVertical);

    // Add all fields from the model.
    for (const auto& field : contents_->fields()) {
      OnFieldAdded(field.get());
    }
  }

  [[nodiscard]] base::CallbackListSubscription AddOnContentsChangedCallback(
      base::RepeatingClosure on_contents_changed) {
    return on_contents_changed_.Add(std::move(on_contents_changed));
  }

  // TODO(pbos): Move (most) of the host's OnFieldAdded stuff into here. If
  // anything remains try to have BubbleDialogModelHost observe it directly.
  void OnFieldAdded(ui::DialogModelField* field) {
    switch (field->type()) {
      case ui::DialogModelField::kParagraph:
        AddOrUpdateParagraph(field->AsParagraph());
        break;
      case ui::DialogModelField::kCheckbox:
        AddOrUpdateCheckbox(field->AsCheckbox());
        break;
      case ui::DialogModelField::kCombobox:
        AddOrUpdateCombobox(field->AsCombobox());
        break;
      case ui::DialogModelField::kMenuItem:
        AddOrUpdateMenuItem(field->AsMenuItem());
        break;
      case ui::DialogModelField::kTitleItem:
        // No need to handle titles.
        NOTREACHED();
      case ui::DialogModelField::kSection:
        // TODO(pbos): Handle nested/multiple sections.
        NOTREACHED();
      case ui::DialogModelField::kSeparator:
        AddOrUpdateSeparator(field);
        break;
      case ui::DialogModelField::kTextfield:
        AddOrUpdateTextfield(field->AsTextfield());
        break;
      case ui::DialogModelField::kPasswordField:
        AddOrUpdatePasswordField(field->AsPasswordField());
        break;
      case ui::DialogModelField::kCustom:
        AddOrUpdateCustomField(field->AsCustomField());
        break;
    }
    OnFieldChanged(field);
  }

  void OnFieldChanged(ui::DialogModelField* field) {
    const DialogModelHostField host_field = FindDialogModelHostField(field);

    if (host_field.field_view) {
      host_field.field_view->SetVisible(field->is_visible());
    }
    on_contents_changed_.Notify();
  }

  // TODO(pbos): Remove the need for this method by making sure the host always
  // outlives us. Currently we do outlive them. Widget, WidgetDelegate and
  // RootView lifetimes are complicated.
  void Detach() {
    fields_.clear();
    RemoveAllChildViews();
    contents_ = nullptr;
  }

  void AddOrUpdateParagraph(ui::DialogModelParagraph* model_field) {
    // TODO(pbos): Handle updating existing field.

    std::unique_ptr<View> view =
        model_field->header().empty()
            ? CreateViewForLabel(model_field->label())
            : CreateViewForParagraphWithHeader(model_field->label(),
                                               model_field->header());
    DialogModelHostField info{model_field, view.get(), nullptr};
    view->SetProperty(kElementIdentifierKey, model_field->id());
    AddDialogModelHostField(std::move(view), info);
  }

  void AddOrUpdateCheckbox(ui::DialogModelCheckbox* model_field) {
    // TODO(pbos): Handle updating existing field.

    std::unique_ptr<CheckboxControl> checkbox;
    if (DialogModelLabelRequiresStyledLabel(model_field->label())) {
      auto label = CreateStyledLabelForDialogModelLabel(model_field->label());
      const int line_height = label->GetLineHeight();
      checkbox =
          std::make_unique<CheckboxControl>(std::move(label), line_height);
    } else {
      auto label = CreateLabelForDialogModelLabel(model_field->label());
      const int line_height = label->GetLineHeight();
      checkbox =
          std::make_unique<CheckboxControl>(std::move(label), line_height);
    }
    checkbox->SetChecked(model_field->is_checked());

    checkbox->SetCallback(base::BindRepeating(
        [](ui::DialogModelCheckbox* model_field,
           base::PassKey<DialogModelFieldHost> pass_key, Checkbox* checkbox,
           const ui::Event& event) {
          model_field->OnChecked(pass_key, checkbox->GetChecked());
        },
        model_field, GetPassKey(), checkbox.get()));

    DialogModelHostField info{model_field, checkbox.get(), nullptr};
    AddDialogModelHostField(std::move(checkbox), info);
  }
  void AddOrUpdateCombobox(ui::DialogModelCombobox* model_field) {
    // TODO(pbos): Handle updating existing field.

    auto combobox = std::make_unique<Combobox>(model_field->combobox_model());
    combobox->GetViewAccessibility().SetName(
        model_field->accessible_name().empty()
            ? model_field->label()
            : model_field->accessible_name());
    combobox->SetCallback(base::BindRepeating(
        [](ui::DialogModelCombobox* model_field,
           base::PassKey<DialogModelFieldHost> pass_key,
           Combobox* combobox) { model_field->OnPerformAction(pass_key); },
        model_field, GetPassKey(), combobox.get()));

    combobox->SetSelectedIndex(model_field->selected_index());
    property_changed_subscriptions_.push_back(
        combobox->AddSelectedIndexChangedCallback(base::BindRepeating(
            [](ui::DialogModelCombobox* model_field,
               base::PassKey<DialogModelFieldHost> pass_key,
               Combobox* combobox) {
              model_field->OnSelectedIndexChanged(
                  pass_key, combobox->GetSelectedIndex().value());
            },
            model_field, GetPassKey(), combobox.get())));
    AddViewForLabelAndField(model_field, model_field->label(),
                            std::move(combobox));
  }

  void AddOrUpdateMenuItem(ui::DialogModelMenuItem* model_field) {
    // TODO(pbos): Handle updating existing field.

    // TODO(crbug.com/40224983): Implement this for enabled items. Sorry!
    DCHECK(!model_field->is_enabled());

    auto item = std::make_unique<LabelButton>(
        base::BindRepeating(
            [](ui::DialogModelMenuItem* model_field,
               base::PassKey<DialogModelFieldHost> pass_key,
               const ui::Event& event) {
              model_field->OnActivated(pass_key, event.flags());
            },
            model_field, GetPassKey()),
        model_field->label());
    item->SetImageModel(Button::STATE_NORMAL, model_field->icon());
    const auto* const layout_provider = LayoutProvider::Get();
    item->SetBorder(views::CreateEmptyBorder(gfx::Insets::VH(
        layout_provider->GetDistanceMetric(DISTANCE_CONTROL_LIST_VERTICAL) / 2,
        layout_provider->GetDistanceMetric(
            DISTANCE_BUTTON_HORIZONTAL_PADDING))));

    item->SetEnabled(model_field->is_enabled());
    item->SetProperty(kElementIdentifierKey, model_field->id());

    DialogModelHostField info{model_field, item.get(), nullptr};
    AddDialogModelHostField(std::move(item), info);
  }

  void AddOrUpdatePasswordField(ui::DialogModelPasswordField* model_field) {
    // TODO(pbos): Support updates to the existing model.

    auto view = std::make_unique<BoxLayoutView>();
    view->SetOrientation(BoxLayout::Orientation::kVertical);

    std::unique_ptr<Label> error_label;
    if (!model_field->incorrect_password_text().empty()) {
      // TODO(droger): Implement the supporting text directly in Textfield.
      error_label = std::make_unique<Label>(
          model_field->incorrect_password_text(),
          style::CONTEXT_TEXTFIELD_SUPPORTING_TEXT, style::STYLE_INVALID);
      error_label->SetMultiLine(true);
      error_label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
      error_label->SetVisible(false);
    }

    // Use a combobox with empty model, rather than a Textfield, so that it
    // supports the "reveal" functionality (eye icon).
    // TODO(droger): add better support for passwords in the regular Textfield.
    auto password_combobox = std::make_unique<views::EditablePasswordCombobox>(
        std::make_unique<ui::SimpleComboboxModel>(
            std::vector<ui::SimpleComboboxModel::Item>()),
        views::style::CONTEXT_TEXTFIELD, style::STYLE_PRIMARY_MONOSPACED,
        false);
    password_combobox->SetPasswordIconTooltips(
        l10n_util::GetStringUTF16(IDS_SETTINGS_PASSWORD_SHOW),
        l10n_util::GetStringUTF16(IDS_SETTINGS_PASSWORD_HIDE));
    password_combobox->GetViewAccessibility().SetName(
        model_field->accessible_name().empty()
            ? model_field->label()
            : model_field->accessible_name());

    password_combobox->SetCallback(base::BindRepeating(
        [](ui::DialogModelPasswordField* model_field,
           base::PassKey<DialogModelFieldHost> pass_key,
           EditablePasswordCombobox* combobox, Label* error_label) {
          model_field->OnTextChanged(pass_key, combobox->GetText());
          combobox->SetInvalid(false);
          if (error_label) {
            error_label->SetVisible(false);
          }
        },
        model_field, GetPassKey(), password_combobox.get(), error_label.get()));

    property_changed_subscriptions_.push_back(
        model_field->AddOnInvalidateCallback(
            GetPassKey(),
            base::BindRepeating(
                [](EditablePasswordCombobox* combobox, Label* error_label) {
                  combobox->SetText(std::u16string());
                  combobox->SetInvalid(true);
                  if (error_label) {
                    error_label->SetVisible(true);
                    error_label->GetViewAccessibility().AnnounceAlert(
                        error_label->GetText());
                  }
                },
                password_combobox.get(), error_label.get())));

    view->AddChildView(std::move(password_combobox));
    if (error_label) {
      view->AddChildView(std::move(error_label));
    }

    AddViewForLabelAndField(model_field, model_field->label(), std::move(view));
  }

  void AddOrUpdateTextfield(ui::DialogModelTextfield* model_field) {
    // TODO(pbos): Support updates to the existing model.

    auto textfield = std::make_unique<Textfield>();
    textfield->GetViewAccessibility().SetName(
        model_field->accessible_name().empty()
            ? model_field->label()
            : model_field->accessible_name());
    textfield->SetText(model_field->text());

    // If this textfield is initially focused the text should be initially
    // selected as well.
    // TODO(pbos): Fix this for non-unique IDs. This should not select all text
    // for all textfields with that ID.
    if (initially_focused_field_id_ &&
        model_field->id() == initially_focused_field_id_) {
      textfield->SelectAll(true);
    }

    property_changed_subscriptions_.push_back(
        textfield->AddTextChangedCallback(base::BindRepeating(
            [](ui::DialogModelTextfield* model_field,
               base::PassKey<DialogModelFieldHost> pass_key,
               Textfield* textfield) {
              model_field->OnTextChanged(pass_key,
                                         std::u16string(textfield->GetText()));
            },
            model_field, GetPassKey(), textfield.get())));

    AddViewForLabelAndField(model_field, model_field->label(),
                            std::move(textfield));
  }

  void AddOrUpdateCustomField(ui::DialogModelCustomField* model_field) {
    auto* custom_view =
        static_cast<BubbleDialogModelHost::CustomView*>(model_field->field());
    std::unique_ptr<View> view = custom_view->TransferView();
    View* focusable_view = custom_view->TransferFocusableView();
    if (!focusable_view) {
      CHECK(view);
      view->SetProperty(kElementIdentifierKey, model_field->id());
    }
    DialogModelHostField info{model_field, view.get(), focusable_view};
    AddDialogModelHostField(std::move(view), info);
  }

  void AddOrUpdateSeparator(ui::DialogModelField* model_field) {
    DCHECK_EQ(ui::DialogModelField::Type::kSeparator, model_field->type());
    // TODO(pbos): Support updates to the existing model.

    auto separator = std::make_unique<Separator>();
    DialogModelHostField info{model_field, separator.get(), nullptr};
    AddDialogModelHostField(std::move(separator), info);
  }

  void AddViewForLabelAndField(ui::DialogModelField* model_field,
                               const std::u16string& label_text,
                               std::unique_ptr<View> field) {
    auto box_layout = std::make_unique<BoxLayoutView>();

    box_layout->SetBetweenChildSpacing(LayoutProvider::Get()->GetDistanceMetric(
        DISTANCE_RELATED_CONTROL_HORIZONTAL));

    DialogModelHostField info{model_field, box_layout.get(), field.get()};

    auto label = std::make_unique<Label>(label_text, style::CONTEXT_LABEL,
                                         style::STYLE_PRIMARY);
    label->SetHorizontalAlignment(gfx::ALIGN_LEFT);

    box_layout->AddChildView(std::make_unique<LayoutConsensusView>(
        &textfield_first_column_group_, std::move(label)));
    box_layout->SetFlexForView(
        box_layout->AddChildView(std::make_unique<LayoutConsensusView>(
            &textfield_second_column_group_, std::move(field))),
        1);

    AddDialogModelHostField(std::move(box_layout), info);
  }

  static bool DialogModelLabelRequiresStyledLabel(
      const ui::DialogModelLabel& dialog_label) {
    return !dialog_label.replacements().empty();
  }

  static std::unique_ptr<View> CreateViewForLabel(
      const ui::DialogModelLabel& dialog_label) {
    if (DialogModelLabelRequiresStyledLabel(dialog_label)) {
      return CreateStyledLabelForDialogModelLabel(dialog_label);
    }
    return CreateLabelForDialogModelLabel(dialog_label);
  }

  static std::unique_ptr<StyledLabel> CreateStyledLabelForDialogModelLabel(
      const ui::DialogModelLabel& dialog_label) {
    DCHECK(DialogModelLabelRequiresStyledLabel(dialog_label));
    const std::vector<ui::DialogModelLabel::TextReplacement>& replacements =
        dialog_label.replacements();

    // Retrieve the replacements strings to create the text.
    std::vector<std::u16string> string_replacements;
    for (auto replacement : replacements) {
      string_replacements.push_back(replacement.text());
    }
    std::vector<size_t> offsets;
    const std::u16string text = l10n_util::GetStringFUTF16(
        dialog_label.message_id(), string_replacements, &offsets);

    auto styled_label = std::make_unique<StyledLabel>();
    styled_label->SetText(text);
    styled_label->SetDefaultTextStyle(dialog_label.is_secondary()
                                          ? style::STYLE_SECONDARY
                                          : style::STYLE_PRIMARY);

    // Style the replacements as needed.
    DCHECK_EQ(string_replacements.size(), offsets.size());
    for (size_t i = 0; i < replacements.size(); ++i) {
      auto replacement = replacements[i];
      // No styling needed if replacement is neither a link nor emphasized text.
      if (!replacement.callback().has_value() && !replacement.is_emphasized()) {
        continue;
      }

      StyledLabel::RangeStyleInfo style_info;
      if (replacement.callback().has_value()) {
        style_info = StyledLabel::RangeStyleInfo::CreateForLink(
            replacement.callback().value());
        style_info.accessible_name = replacement.accessible_name().value();
      } else if (replacement.is_emphasized()) {
        style_info.text_style = views::style::STYLE_EMPHASIZED;
      }

      auto offset = offsets[i];
      styled_label->AddStyleRange(
          gfx::Range(offset, offset + replacement.text().length()), style_info);
    }

    return styled_label;
  }

  static std::unique_ptr<Label> CreateLabelForDialogModelLabel(
      const ui::DialogModelLabel& dialog_label) {
    DCHECK(!DialogModelLabelRequiresStyledLabel(dialog_label));

    auto text_label = std::make_unique<Label>(
        dialog_label.GetString(), style::CONTEXT_DIALOG_BODY_TEXT,
        dialog_label.is_secondary() ? style::STYLE_SECONDARY
                                    : style::STYLE_PRIMARY);
    text_label->SetMultiLine(true);
    text_label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
    return text_label;
  }

  std::unique_ptr<View> CreateViewForParagraphWithHeader(
      const ui::DialogModelLabel& dialog_label,
      const std::u16string& header) {
    auto view = std::make_unique<BoxLayoutView>();
    view->SetOrientation(BoxLayout::Orientation::kVertical);

    auto* header_label = view->AddChildView(std::make_unique<Label>(
        header, style::CONTEXT_DIALOG_BODY_TEXT, style::STYLE_PRIMARY));
    header_label->SetHorizontalAlignment(gfx::ALIGN_LEFT);

    view->AddChildView(CreateViewForLabel(dialog_label));
    return view;
  }

  void AddDialogModelHostField(std::unique_ptr<View> view,
                               const DialogModelHostField& field_view_info) {
    DCHECK_EQ(view.get(), field_view_info.field_view);

    AddChildView(std::move(view));

    DCHECK(field_view_info.dialog_model_field);
    DCHECK(field_view_info.field_view);
#if DCHECK_IS_ON()
    // Make sure none of the info is already in use.
    for (const auto& info : fields_) {
      DCHECK_NE(info.field_view, field_view_info.field_view);
      DCHECK_NE(info.dialog_model_field, field_view_info.dialog_model_field);
      if (info.focusable_view) {
        DCHECK_NE(info.focusable_view, field_view_info.focusable_view);
      }
    }
#endif  // DCHECK_IS_ON()
    fields_.push_back(field_view_info);
    View* const target = GetTargetView(field_view_info);
    target->SetProperty(kElementIdentifierKey,
                        field_view_info.dialog_model_field->id());
    for (const auto& accelerator :
         field_view_info.dialog_model_field->accelerators()) {
      target->AddAccelerator(accelerator);
    }
  }

  DialogModelHostField FindDialogModelHostField(ui::DialogModelField* field) {
    for (const auto& info : fields_) {
      if (info.dialog_model_field == field) {
        return info;
      }
    }
    // TODO(pbos): `field` could correspond to a button.
    return {};
  }

  DialogModelHostField FindDialogModelHostField(View* view) {
    for (const auto& info : fields_) {
      if (info.field_view == view) {
        return info;
      }
    }
    NOTREACHED();
  }

 private:
  // TODO(pbos): These should be const as we should never outlive our parent or
  // DialogModelSection. Currently this isn't true because WidgetDelegate gets
  // destroyed by Widget in OnNativeWidgetDestroyed before the content gets
  // destroyed inside DestroyRootView in ~Widget.
  raw_ptr<ui::DialogModelSection> contents_;

  const ui::ElementIdentifier initially_focused_field_id_;

  const base::CallbackListSubscription on_field_added_subscription_;
  const base::CallbackListSubscription on_field_changed_subscription_;

  std::vector<DialogModelHostField> fields_;
  // TODO(pbos): Try to work away this list if the parent can observe enough
  // things as a ViewObserver of us.
  base::RepeatingClosureList on_contents_changed_;

  std::vector<base::CallbackListSubscription> property_changed_subscriptions_;

  LayoutConsensusGroup textfield_first_column_group_;
  LayoutConsensusGroup textfield_second_column_group_;
};

BEGIN_METADATA(BubbleDialogModelHostContentsView)
END_METADATA

std::unique_ptr<DialogModelSectionHost> DialogModelSectionHost::Create(
    ui::DialogModelSection* section,
    ui::ElementIdentifier initially_focused_field_id) {
  return std::make_unique<BubbleDialogModelHostContentsView>(
      section, initially_focused_field_id);
}

BEGIN_METADATA(DialogModelSectionHost)
END_METADATA

BubbleDialogModelHost::ThemeChangedObserver::ThemeChangedObserver(
    BubbleDialogModelHost* parent,
    BubbleDialogModelHostContentsView* contents_view)
    : parent_(parent) {
  observation_.Observe(contents_view);
}
BubbleDialogModelHost::ThemeChangedObserver::~ThemeChangedObserver() = default;

void BubbleDialogModelHost::ThemeChangedObserver::OnViewThemeChanged(
    View* view) {
  parent_->UpdateWindowIcon(view->GetColorProvider());
}

BubbleDialogModelHost::BubbleDialogModelHost(
    std::unique_ptr<ui::DialogModel> model,
    View* anchor_view,
    BubbleBorder::Arrow arrow,
    bool autosize)
    : BubbleDialogModelHost(base::PassKey<BubbleDialogModelHost>(),
                            std::move(model),
                            anchor_view,
                            arrow,
                            ui::mojom::ModalType::kNone,
                            autosize) {}

BubbleDialogModelHost::BubbleDialogModelHost(
    base::PassKey<BubbleDialogModelHost>,
    std::unique_ptr<ui::DialogModel> model,
    View* anchor_view,
    BubbleBorder::Arrow arrow,
    ui::mojom::ModalType modal_type,
    bool autosize)
    : BubbleDialogDelegate(anchor_view,
                           arrow,
                           views::BubbleBorder::DIALOG_SHADOW,
                           autosize),
      model_(std::move(model)),
      // Make sure the modal type is set before calling InitContentsView which
      // uses IsModalDialog().
      contents_view_(
          (SetModalType(modal_type), InitContentsView(model_->contents()))),
      on_contents_changed_subscription_(
          contents_view_->AddOnContentsChangedCallback(
              base::BindRepeating(&BubbleDialogModelHost::OnContentsViewChanged,
                                  base::Unretained(this)))),
      theme_observer_(this, contents_view_) {
  SetOwnedByWidget(OwnedByWidgetPassKey());
  model_->set_host(DialogModelHost::GetPassKey(), this);

  // Dialog callbacks can safely refer to |model_|, they can't be called after
  // Widget::Close() calls WidgetWillClose() synchronously so there shouldn't
  // be any dangling references after model removal.
  SetAcceptCallbackWithClose(base::BindRepeating(
      &ui::DialogModel::OnDialogAcceptAction, base::Unretained(model_.get()),
      DialogModelHost::GetPassKey()));
  SetCancelCallbackWithClose(base::BindRepeating(
      &ui::DialogModel::OnDialogCancelAction, base::Unretained(model_.get()),
      DialogModelHost::GetPassKey()));
  SetCloseCallback(base::BindOnce(&ui::DialogModel::OnDialogCloseAction,
                                  base::Unretained(model_.get()),
                                  DialogModelHost::GetPassKey()));

  // WindowClosingCallback happens on native widget destruction which is after
  // |model_| reset. Hence routing this callback through |this| so that we
  // only forward the call to DialogModel::OnWindowClosing if we haven't
  // already been closed.
  RegisterWindowClosingCallback(base::BindOnce(
      &BubbleDialogModelHost::OnWindowClosing, base::Unretained(this)));

  int button_mask = static_cast<int>(ui::mojom::DialogButton::kNone);
  auto* ok_button = model_->ok_button(DialogModelHost::GetPassKey());
  if (ok_button) {
    button_mask |= static_cast<int>(ui::mojom::DialogButton::kOk);
    ConfigureBubbleButtonForParams(*this, /*button_view=*/nullptr,
                                   ui::mojom::DialogButton::kOk, *ok_button);
  }

  auto* cancel_button = model_->cancel_button(DialogModelHost::GetPassKey());
  if (cancel_button) {
    button_mask |= static_cast<int>(ui::mojom::DialogButton::kCancel);
    ConfigureBubbleButtonForParams(*this, /*button_view=*/nullptr,
                                   ui::mojom::DialogButton::kCancel,
                                   *cancel_button);
  }

  // TODO(pbos): Consider refactoring ::SetExtraView() so it can be called
  // after the Widget is created and still be picked up. Moving this to
  // OnWidgetInitialized() will not work until then.
  if (ui::DialogModel::Button* extra_button =
          model_->extra_button(DialogModelHost::GetPassKey())) {
    DCHECK(!model_->extra_link(DialogModelHost::GetPassKey()));
    auto builder =
        views::Builder<MdTextButton>()
            .SetCallback(base::BindRepeating(
                &ui::DialogModel::Button::OnPressed,
                base::Unretained(extra_button), DialogModelHost::GetPassKey()))
            .SetText(extra_button->label());
    if (extra_button->style()) {
      builder.SetStyle(extra_button->style().value());
    }
    builder.SetEnabled(extra_button->is_enabled());
    SetExtraView(std::move(builder).Build());
  } else if (ui::DialogModelLabel::TextReplacement* extra_link =
                 model_->extra_link(DialogModelHost::GetPassKey())) {
    DCHECK(extra_link->callback().has_value());
    auto link = std::make_unique<views::Link>(extra_link->text());
    link->SetCallback(extra_link->callback().value());
    SetExtraView(std::move(link));
  }

  SetButtons(button_mask);

  if (model_->override_default_button(DialogModelHost::GetPassKey())) {
    SetDefaultButton(static_cast<int>(
        model_->override_default_button(DialogModelHost::GetPassKey())
            .value()));
  }

  SetTitle(model_->title(DialogModelHost::GetPassKey()));

  if (!model_->accessible_title(DialogModelHost::GetPassKey()).empty()) {
    SetAccessibleTitle(model_->accessible_title(DialogModelHost::GetPassKey()));
  }

  SetSubtitle(model_->subtitle(DialogModelHost::GetPassKey()));
  // This is added due to crbug.com/1518993 which adds a subtitle that needs
  // line wrapping. If any future dialogs need the subtitle to not line wrap,
  // this behavior will need to be configurable via the builder.
  SetSubtitleAllowCharacterBreak(true);

  if (!model_->main_image(DialogModelHost::GetPassKey()).IsEmpty()) {
    SetMainImage(model_->main_image(DialogModelHost::GetPassKey()));
  }

  if (model_->override_show_close_button(DialogModelHost::GetPassKey())) {
    SetShowCloseButton(
        *model_->override_show_close_button(DialogModelHost::GetPassKey()));
  } else {
    SetShowCloseButton(!IsModalDialog());
  }

  if (!model_->icon(DialogModelHost::GetPassKey()).IsEmpty()) {
    SetIcon(model_->icon(DialogModelHost::GetPassKey()));
    SetShowIcon(true);
  }

  if (model_->is_alert_dialog(DialogModelHost::GetPassKey())) {
#if BUILDFLAG(IS_WIN)
    // This is taken from LocationBarBubbleDelegateView. See
    // GetAccessibleRoleForReason(). crbug.com/1125118: Windows ATs only
    // announce these bubbles if the alert role is used, despite it not being
    // the most appropriate choice.
    // TODO(accessibility): review the role mappings for alerts and dialogs,
    // making sure they are translated to the best candidate in each flatform
    // without resorting to hacks like this.
    SetAccessibleWindowRole(ax::mojom::Role::kAlert);
#else
    SetAccessibleWindowRole(ax::mojom::Role::kAlertDialog);
#endif
  }

  set_internal_name(model_->internal_name(DialogModelHost::GetPassKey()));

  set_close_on_deactivate(
      model_->close_on_deactivate(DialogModelHost::GetPassKey()));

  // TODO(pbos): Reconsider this for dialogs which have no actions (look like
  // menus). This is probably too wide for the TabGroupEditorBubbleView which is
  // currently being converted.
  set_fixed_width(LayoutProvider::Get()->GetDistanceMetric(
      anchor_view ? DISTANCE_BUBBLE_PREFERRED_WIDTH
                  : DISTANCE_MODAL_DIALOG_PREFERRED_WIDTH));

  if (model_->footnote_label()) {
    SetFootnoteView(BubbleDialogModelHostContentsView::CreateViewForLabel(
        *model_->footnote_label()));
  }

  // Make sure we're up to date with initial contents state.
  UpdateSpacingAndMargins();
}

BubbleDialogModelHost::~BubbleDialogModelHost() {
  // Detach ContentsView as it's referring to state that's about to be
  // destroyed.
  contents_view_->Detach();
}

std::unique_ptr<BubbleDialogModelHost> BubbleDialogModelHost::CreateModal(
    std::unique_ptr<ui::DialogModel> model,
    ui::mojom::ModalType modal_type,
    bool autosize) {
  DCHECK_NE(modal_type, ui::mojom::ModalType::kNone);
  return std::make_unique<BubbleDialogModelHost>(
      base::PassKey<BubbleDialogModelHost>(), std::move(model), nullptr,
      BubbleBorder::Arrow::NONE, modal_type, autosize);
}

View* BubbleDialogModelHost::GetInitiallyFocusedView() {
  // TODO(pbos): Migrate this override to use
  // WidgetDelegate::SetInitiallyFocusedView() in constructor once it exists.
  // TODO(pbos): Try to prevent uses of GetInitiallyFocusedView() after Close()
  // and turn this in to a DCHECK for |model_| existence. This should fix
  // https://crbug.com/1130181 for now.
  if (!model_) {
    return BubbleDialogDelegate::GetInitiallyFocusedView();
  }

  // TODO(pbos): Reconsider the uniqueness requirement, maybe this should select
  // the first one? If so add corresponding GetFirst query to DialogModel.
  ui::ElementIdentifier unique_id =
      model_->initially_focused_field(DialogModelHost::GetPassKey());

  if (!unique_id) {
    return BubbleDialogDelegate::GetInitiallyFocusedView();
  }

  if (ui::DialogModel::Button* const ok_button =
          model_->ok_button(DialogModelHost::GetPassKey());
      ok_button && unique_id == ok_button->id()) {
    return GetOkButton();
  }

  if (ui::DialogModel::Button* const cancel_button =
          model_->cancel_button(DialogModelHost::GetPassKey());
      cancel_button && unique_id == cancel_button->id()) {
    return GetCancelButton();
  }

  if (ui::DialogModel::Button* const extra_button =
          model_->extra_button(DialogModelHost::GetPassKey());
      extra_button && unique_id == extra_button->id()) {
    return GetExtraView();
  }

  return GetTargetView(contents_view_->FindDialogModelHostField(
      model_->GetFieldByUniqueId(unique_id)));
}

void BubbleDialogModelHost::OnWidgetInitialized() {
  // Dialog buttons are added on dialog initialization.
  UpdateDialogButtons();

  if (const ui::ImageModel& banner =
          model_->banner(DialogModelHost::GetPassKey());
      !banner.IsEmpty()) {
    const ui::ImageModel& dark_mode_banner =
        model_->dark_mode_banner(DialogModelHost::GetPassKey());
    auto banner_view = std::make_unique<ThemeTrackingImageView>(
        banner.Rasterize(contents_view_->GetColorProvider()),
        (dark_mode_banner.IsEmpty() ? banner : dark_mode_banner)
            .Rasterize(contents_view_->GetColorProvider()),
        base::BindRepeating(&views::BubbleDialogDelegate::background_color,
                            base::Unretained(this)));
    // The banner is supposed to be purely decorative.
    banner_view->GetViewAccessibility().SetIsIgnored(true);
    GetBubbleFrameView()->SetHeaderView(std::move(banner_view));
  }
}

void BubbleDialogModelHost::Close() {
  DCHECK(model_);
  DCHECK(GetWidget());
  GetWidget()->Close();

  // Synchronously destroy |model_|. Widget::Close() being asynchronous should
  // not be observable by the model or client code.

  // Notify the model of window closing before destroying it (as if
  // Widget::Close)
  model_->OnDialogDestroying(DialogModelHost::GetPassKey());

  // Detach ContentsView as it's referring to state that's about to be
  // destroyed.
  contents_view_->Detach();
  model_.reset();
}

BubbleDialogModelHostContentsView* BubbleDialogModelHost::InitContentsView(
    ui::DialogModelSection* contents) {
  auto contents_view_unique =
      std::make_unique<BubbleDialogModelHostContentsView>(
          contents,
          model_->initially_focused_field(DialogModelHost::GetPassKey()));

  BubbleDialogModelHostContentsView* const contents_view =
      contents_view_unique.get();

  if (IsModalDialog()) {
    // Margins are added directly in the dialog. When the dialog is modal, these
    // contents are wrapped by a scroll view and margins are added outside of it
    // (instead of outside this contents). This causes some items (e.g
    // emphasized buttons) to be cut by the scroll view margins (see
    // crbug.com/1360772). Since we do want the margins outside the scroll view
    // (so they are always present when scrolling), we add
    // `kScrollViewVerticalMargin` inside the contents view and later remove it
    // from the dialog margins.
    // TODO(crbug.com/40855129): Remove this workaround when contents view
    // directly supports a scroll view.
    contents_view_unique->SetInsideBorderInsets(
        gfx::Insets::VH(kScrollViewVerticalMargin, 0));

    // TODO(crbug.com/40855129): Non modal dialogs size is not dependent on its
    // content. Thus, the content has to be manually set by the view inside a
    // scroll view. Modal dialogs handle their own size via constrained windows,
    // so we can add a scroll view to the DialogModel directly.
    const int kMaxDialogHeight = LayoutProvider::Get()->GetDistanceMetric(
        DISTANCE_MODAL_DIALOG_SCROLLABLE_AREA_MAX_HEIGHT);
    auto scroll_view = std::make_unique<views::ScrollView>();
    scroll_view->ClipHeightTo(0, kMaxDialogHeight);
    scroll_view->SetHorizontalScrollBarMode(
        views::ScrollView::ScrollBarMode::kDisabled);
    scroll_view->SetContents(std::move(contents_view_unique));
    SetContentsView(std::move(scroll_view));
  } else {
    SetContentsView(std::move(contents_view_unique));
  }
  return contents_view;
}

void BubbleDialogModelHost::OnContentsViewChanged() {
  UpdateSpacingAndMargins();
}

void BubbleDialogModelHost::OnDialogButtonChanged() {
  UpdateDialogButtons();
}

void BubbleDialogModelHost::UpdateWindowIcon(
    const ui::ColorProvider* color_provider) {
  if (!ShouldShowWindowIcon()) {
    return;
  }
  const ui::ImageModel dark_mode_icon =
      model_->dark_mode_icon(DialogModelHost::GetPassKey());
  if (!dark_mode_icon.IsEmpty() &&
      color_utils::IsDark(
          background_color().ResolveToSkColor(color_provider))) {
    SetIcon(dark_mode_icon);
    return;
  }
  SetIcon(model_->icon(DialogModelHost::GetPassKey()));
}

void BubbleDialogModelHost::UpdateSpacingAndMargins() {
  LayoutProvider* const layout_provider = LayoutProvider::Get();
  gfx::Insets dialog_side_insets =
      layout_provider->GetInsetsMetric(InsetsMetric::INSETS_DIALOG);
  if (GetWindowTitle().empty()) {
    // If there is no title, increase the margin at the top to match the title
    // margin, so that the text is not too close to the top edge.
    dialog_side_insets.set_top(
        layout_provider->GetInsetsMetric(InsetsMetric::INSETS_DIALOG_TITLE)
            .top());
  } else {
    dialog_side_insets.set_top(0);
  }
  dialog_side_insets.set_bottom(0);

  ui::DialogModelField* first_field = nullptr;
  ui::DialogModelField* last_field = nullptr;

  auto* scroll_view =
      views::ScrollView::GetScrollViewForContents(contents_view_);
  const views::View::Views& children = scroll_view
                                           ? scroll_view->contents()->children()
                                           : contents_view_->children();

  if (children.empty()) {
    // TODO(pbos): Copied from the BubbleDialogDelegate constructor. Maybe there
    // should be a way to reset them if we become empty?
    set_margins(layout_provider->GetDialogInsetsForContentType(
        DialogContentType::kText, DialogContentType::kText));
    return;
  }

  for (View* const view : children) {
    ui::DialogModelField* const field =
        contents_view_->FindDialogModelHostField(view).dialog_model_field;

    const FieldType field_type = GetFieldTypeForField(field);

    gfx::Insets side_insets =
        field_type == FieldType::kMenuItem ? gfx::Insets() : dialog_side_insets;

    if (!first_field) {
      first_field = field;
      view->SetProperty(kMarginsKey, side_insets);
    } else {
      DCHECK(last_field);

      const FieldType last_field_type = GetFieldTypeForField(last_field);
      side_insets.set_top(
          GetFieldTopMargin(layout_provider, field_type, last_field_type));
      view->SetProperty(kMarginsKey, side_insets);
    }
    last_field = field;
  }

  contents_view_->InvalidateLayout();
  // Set margins based on the first and last item. Note that we remove margins
  // that were already added to contents view at construction.
  // TODO(crbug.com/40855129): Remove the extra margin workaround when contents
  // view directly supports a scroll view.
  const int extra_margin = scroll_view ? kScrollViewVerticalMargin : 0;
  const int top_margin =
      GetDialogTopMargins(layout_provider, first_field) - extra_margin;
  const int bottom_margin =
      GetDialogBottomMargins(
          layout_provider, last_field,
          buttons() != static_cast<int>(ui::mojom::DialogButton::kNone)) -
      extra_margin;
  set_margins(gfx::Insets::TLBR(top_margin >= 0 ? top_margin : 0, 0,
                                bottom_margin >= 0 ? bottom_margin : 0, 0));
}

void BubbleDialogModelHost::OnWindowClosing() {
  // If the model has been removed we have already notified it of closing on the
  // ::Close() stack.
  if (!model_) {
    return;
  }
  model_->OnDialogDestroying(DialogModelHost::GetPassKey());
  // TODO(pbos): Do we need to reset `model_` and destroy contents? See Close().
}

void BubbleDialogModelHost::UpdateDialogButtons() {
  if (ui::DialogModel::Button* const ok_button =
          model_->ok_button(DialogModelHost::GetPassKey())) {
    ConfigureBubbleButtonForParams(*this, GetOkButton(),
                                   ui::mojom::DialogButton::kOk, *ok_button);
  }
  if (ui::DialogModel::Button* const cancel_button =
          model_->cancel_button(DialogModelHost::GetPassKey())) {
    ConfigureBubbleButtonForParams(*this, GetCancelButton(),
                                   ui::mojom::DialogButton::kCancel,
                                   *cancel_button);
  }
  if (ui::DialogModel::Button* const extra_button =
          model_->extra_button(DialogModelHost::GetPassKey())) {
    auto* const extra_button_view = static_cast<MdTextButton*>(GetExtraView());
    extra_button_view->SetText(extra_button->label());
    extra_button_view->SetVisible(extra_button->is_visible());
    extra_button_view->SetEnabled(extra_button->is_enabled());
    extra_button_view->SetProperty(kElementIdentifierKey, extra_button->id());
  }
}

bool BubbleDialogModelHost::IsModalDialog() const {
  return GetModalType() != ui::mojom::ModalType::kNone;
}

}  // namespace views