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.

#ifndef UI_BASE_MODELS_DIALOG_MODEL_H_
#define UI_BASE_MODELS_DIALOG_MODEL_H_

#include <memory>
#include <optional>
#include <string>
#include <variant>

#include "base/component_export.h"
#include "base/functional/callback.h"
#include "base/functional/callback_forward.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/raw_ptr.h"
#include "base/types/pass_key.h"
#include "ui/base/interaction/element_identifier.h"
#include "ui/base/models/dialog_model_field.h"
#include "ui/base/models/dialog_model_host.h"
#include "ui/base/models/image_model.h"
#include "ui/base/mojom/dialog_button.mojom.h"
#include "ui/base/ui_base_types.h"

namespace ui {

class ComboboxModel;

// Base class for a Delegate associated with (owned by) a model. Provides a link
// from the delegate back to the model it belongs to (through ::dialog_model()),
// from which fields and the DialogModelHost can be accessed.
class COMPONENT_EXPORT(UI_BASE) DialogModelDelegate {
 public:
  DialogModelDelegate() = default;
  DialogModelDelegate(const DialogModelDelegate&) = delete;
  DialogModelDelegate& operator=(const DialogModelDelegate&) = delete;
  virtual ~DialogModelDelegate() = default;

  DialogModel* dialog_model() { return dialog_model_; }

 private:
  friend class DialogModel;
  void set_dialog_model(DialogModel* model) { dialog_model_ = model; }

  raw_ptr<DialogModel> dialog_model_ = nullptr;
};

// DialogModel represents a platform-and-toolkit agnostic data + behavior
// portion of a dialog. This contains the semantics of a dialog, whereas
// DialogModelHost implementations (like views::BubbleDialogModelHost) are
// responsible for interfacing with toolkits to display them. This provides a
// separation of concerns where a DialogModel only needs to be concerned with
// what goes into a dialog, not how it shows.
//
// DialogModel is supported in views, and android. See
// //ui/android/modal_dialog_wrapper.h for limitations of android support.
//
// Example usage (with views as an example DialogModelHost implementation). Note
// that visual presentation (except order of elements) is entirely up to
// DialogModelHost, and separate from client code:
//
// constexpr int kNameTextfield = 1;
// class Delegate : public ui::DialogModelDelegate {
//  public:
//   void OnDialogAccepted() {
//     LOG(ERROR) << "Hello "
//                << dialog_model()->GetTextfield(kNameTextfield)->text();
//   }
// };
// auto model_delegate = std::make_unique<Delegate>();
// auto* model_delegate_ptr = model_delegate.get();
//
// auto dialog_model =
//     ui::DialogModel::Builder(std::move(model_delegate))
//         .SetTitle(u"Hello, world!")
//         .AddOkButton(base::BindOnce(&Delegate::OnDialogAccepted,
//                                     base::Unretained(model_delegate_ptr)))
//         .AddTextfield(
//             u"Name", std::u16string(),
//             ui::DialogModelTextfield::Params().SetUniqueId(kNameTextfield))
//         .Build();
//
// // DialogModelBase::Host specific. In this example, uses views-specific
// // code to set a view as an anchor.
// auto bubble =
//     std::make_unique<views::BubbleDialogModelHost>(std::move(dialog_model));
// bubble->SetAnchorView(anchor_view);
// views::Widget* const widget =
//     views::BubbleDialogDelegate::CreateBubble(std::move(bubble));
// widget->Show();
class COMPONENT_EXPORT(UI_BASE) DialogModel final {
 public:
  // Field class representing a dialog button.
  // TODO(pbos): Consider separating this from DialogModelField completely. For
  // instance it doesn't have a corresponding DialogModelField::Type.
  class COMPONENT_EXPORT(UI_BASE) Button final : public DialogModelField {
   public:
    class COMPONENT_EXPORT(UI_BASE) Params : public DialogModelField::Params {
     public:
      Params();
      Params(const Params&) = delete;
      Params& operator=(const Params&) = delete;
      ~Params();

      Params& SetId(ElementIdentifier id);
      Params& SetLabel(std::u16string label);
      Params& SetStyle(std::optional<ButtonStyle> style);
      Params& SetEnabled(bool is_enabled);

      Params& AddAccelerator(Accelerator accelerator);

      Params& SetVisible(bool is_visible) {
        DialogModelField::Params::SetVisible(is_visible);
        return *this;
      }

     private:
      friend class DialogModel;
      friend class Button;

      ElementIdentifier id_;
      std::u16string label_;
      std::optional<ButtonStyle> style_;
      bool is_enabled_ = true;
      base::flat_set<Accelerator> accelerators_;
    };

    Button(base::RepeatingCallback<void(const Event&)> callback,
           const Params& params);
    Button(const Button&) = delete;
    Button& operator=(const Button&) = delete;
    ~Button() override;

    const std::u16string& label() const { return label_; }
    const std::optional<ButtonStyle> style() const { return style_; }
    bool is_enabled() const { return is_enabled_; }
    void OnPressed(base::PassKey<DialogModelHost>, const Event& event);

   private:
    friend class DialogModel;

    std::u16string label_;
    const std::optional<ButtonStyle> style_;
    bool is_enabled_;
    // The button callback gets called when the button is activated. Whether
    // that happens on key-press, release, etc. is implementation (and platform)
    // dependent.
    base::RepeatingCallback<void(const Event&)> callback_;
  };

  // A variant for button callbacks that allows different behavior to be
  // specified when a button is pressed.
  using ButtonCallbackVariant = std::variant<
      // This is the default -- no callback action is taken when the button is
      // pressed and the dialog is closed.
      decltype(base::DoNothing()),
      // Called exactly once when the button is pressed. The dialog will be
      // closed after the callback is run.
      base::OnceClosure,
      // Returns whether the dialog should be closed. This will be called each
      // time the button is pressed.
      base::RepeatingCallback<bool()>>;

  // Builder for DialogModel. Used for properties that are either only or
  // commonly const after construction.
  class COMPONENT_EXPORT(UI_BASE) Builder final {
   public:
    // Constructs a Builder for a DialogModel with a DialogModelDelegate whose
    // lifetime (and storage) is tied to the lifetime of the DialogModel.
    explicit Builder(std::unique_ptr<DialogModelDelegate> delegate);

    // Constructs a DialogModel without a DialogModelDelegate (that doesn't
    // require storage tied to the DialogModel). For access to the DialogModel
    // during construction (for use in callbacks), use model().
    Builder();

    Builder(const Builder&) = delete;
    Builder& operator=(const Builder&) = delete;

    ~Builder();

    [[nodiscard]] std::unique_ptr<DialogModel> Build();

    // Gets the DialogModel. Used for setting up callbacks that make use of the
    // model later once it's fully constructed. This is useful for dialogs or
    // callbacks that don't use DialogModelDelegate and don't have direct access
    // to the model through DialogModelDelegate::dialog_model().
    //
    // Note that the DialogModel* returned here is only for registering
    // callbacks with the DialogModel::Builder. These callbacks share lifetimes
    // with the DialogModel so uses of it will not result in use-after-frees.
    DialogModel* model() { return model_.get(); }

    // Overrides the close-x use for the dialog. Should be avoided as the
    // close-x is generally derived from dialog modality. Kept to allow
    // conversion of dialogs that currently do not allow style.
    // TODO(pbos): Propose UX updates to existing dialogs that require this,
    // then remove OverrideShowCloseButton().
    Builder& OverrideShowCloseButton(bool show_close_button) {
      model_->override_show_close_button_ = show_close_button;
      return *this;
    }

    Builder& SetInternalName(std::string internal_name) {
      model_->internal_name_ = std::move(internal_name);
      return *this;
    }

    Builder& SetTitle(std::u16string title) {
      model_->title_ = std::move(title);
      return *this;
    }

    Builder& SetAccessibleTitle(std::u16string accessible_title) {
      model_->accessible_title_ = std::move(accessible_title);
      return *this;
    }

    Builder& SetSubtitle(std::u16string subtitle) {
      model_->subtitle_ = std::move(subtitle);
      return *this;
    }

    Builder& SetBannerImage(ImageModel banner,
                            ImageModel dark_mode_banner = ImageModel()) {
      model_->banner_ = std::move(banner);
      model_->dark_mode_banner_ = std::move(dark_mode_banner);
      return *this;
    }

    Builder& SetIcon(ImageModel icon,
                     ImageModel dark_mode_icon = ImageModel()) {
      model_->icon_ = std::move(icon);
      model_->dark_mode_icon_ = std::move(dark_mode_icon);
      return *this;
    }

    Builder& SetMainImage(ImageModel main_image) {
      model_->main_image_ = std::move(main_image);
      return *this;
    }

    // Make screen readers announce the contents of the dialog as it appears.
    // See |ax::mojom::Role::kAlertDialog|.
    Builder& SetIsAlertDialog() {
      model_->is_alert_dialog_ = true;
      return *this;
    }

    // Disables the default behavior that the dialog closes when deactivated.
    Builder& DisableCloseOnDeactivate() {
      model_->close_on_deactivate_ = false;
      return *this;
    }

    // Called when the dialog is explicitly closed (Esc, close-x). Not called
    // during accept/cancel.
    Builder& SetCloseActionCallback(base::OnceClosure callback) {
      model_->close_action_callback_ = std::move(callback);
      return *this;
    }

    // TODO(pbos): Clarify and enforce (through tests) that this is called after
    // {accept,cancel,close} callbacks.
    // Unconditionally called when the dialog destroys. Happens after
    // user-action callbacks (accept, cancel, close), or as a result of dialog
    // destruction. The latter can happen without a user action, for instance as
    // a result of the OS destroying a native Widget in which this dialog is
    // hosted.
    Builder& SetDialogDestroyingCallback(base::OnceClosure callback) {
      model_->dialog_destroying_callback_ = std::move(callback);
      return *this;
    }

    // Adds a dialog button (ok, cancel) to the dialog. The |callback| is called
    // when the dialog is accepted or cancelled, before it closes. Use
    // base::DoNothing() as callback if you want nothing extra to happen as a
    // result, besides the dialog closing.
    // If no |label| is provided, default strings are chosen by the
    // DialogModelHost implementation.
    // TODO(pbos): Reconsider this API, a DialogModelHost does not need to use
    // buttons for accepting/cancelling. Also "ok" should be "accept" to be in
    // sync with other APIs?
    Builder& AddOkButton(ButtonCallbackVariant callback,
                         const Button::Params& params = Button::Params());
    Builder& AddCancelButton(ButtonCallbackVariant callback,
                             const Button::Params& params = Button::Params());

    // Use of the extra button in new dialogs are discouraged. If this is deemed
    // necessary please double-check with UX before adding any new dialogs with
    // them. A button label is required to be set in `params`.
    Builder& AddExtraButton(
        base::RepeatingCallback<void(const Event&)> callback,
        const Button::Params& params);

    // Adds an extra link to the dialog.
    Builder& AddExtraLink(DialogModelLabel::TextReplacement link);

    // Adds a paragraph. See DialogModel::AddParagraph().
    Builder& AddParagraph(const DialogModelLabel& label,
                          std::u16string header = std::u16string(),
                          ElementIdentifier id = ElementIdentifier()) {
      model_->AddParagraph(label, header, id);
      return *this;
    }

    // Adds a checkbox. See DialogModel::AddCheckbox().
    Builder& AddCheckbox(ElementIdentifier id,
                         const DialogModelLabel& label,
                         const DialogModelCheckbox::Params& params =
                             DialogModelCheckbox::Params()) {
      model_->AddCheckbox(id, label, params);
      return *this;
    }

    // Adds a combobox. See DialogModel::AddCombobox().
    Builder& AddCombobox(ElementIdentifier id,
                         std::u16string label,
                         std::unique_ptr<ui::ComboboxModel> combobox_model,
                         const DialogModelCombobox::Params& params =
                             DialogModelCombobox::Params()) {
      model_->AddCombobox(id, std::move(label), std::move(combobox_model),
                          params);
      return *this;
    }

    // Adds a menu item. See DialogModel::AddMenuItem().
    Builder& AddMenuItem(ImageModel icon,
                         std::u16string label,
                         base::RepeatingCallback<void(int)> callback,
                         const DialogModelMenuItem::Params& params =
                             DialogModelMenuItem::Params()) {
      model_->AddMenuItem(std::move(icon), std::move(label),
                          std::move(callback), params);
      return *this;
    }

    // Adds a title item. See DialogModel::AddTitleItem().
    // TODO(pengchaocai): Refactor this method once dialog_model supports
    // multiple DialogModelSection, when the title would be an optional member
    // of DialogModelSection.
    Builder& AddTitleItem(std::u16string label,
                          ElementIdentifier id = ElementIdentifier()) {
      model_->AddTitleItem(std::move(label), id);
      return *this;
    }

    // Adds a separator. See DialogModel::AddSeparator().
    Builder& AddSeparator() {
      model_->AddSeparator();
      return *this;
    }

    // Adds a textfield. See DialogModel::AddTextfield().
    Builder& AddTextfield(ElementIdentifier id,
                          std::u16string label,
                          std::u16string text,
                          const DialogModelTextfield::Params& params =
                              DialogModelTextfield::Params()) {
      model_->AddTextfield(id, std::move(label), std::move(text), params);
      return *this;
    }

    // Adds a password field. See DialogModel::AddPasswordField().
    Builder& AddPasswordField(ElementIdentifier id,
                              std::u16string label,
                              std::u16string accessible_text,
                              std::u16string incorrect_password_text,
                              const DialogModelPasswordField::Params& params =
                                  DialogModelPasswordField::Params()) {
      model_->AddPasswordField(id, std::move(label), std::move(accessible_text),
                               std::move(incorrect_password_text), params);
      return *this;
    }

    // Sets the footnote. See DialogModel::SetFootnote().
    Builder& SetFootnote(const DialogModelLabel& label) {
      model_->SetFootnote(label);
      return *this;
    }

    // Adds a custom field. See DialogModel::AddCustomField().
    Builder& AddCustomField(
        std::unique_ptr<DialogModelCustomField::Field> field,
        ElementIdentifier id = ElementIdentifier()) {
      model_->AddCustomField(std::move(field), id);
      return *this;
    }

    // Overrides default button. Can only be called once. The new default may be
    // set to kNone, but if not, the specified button must already exist in the
    // model.
    Builder& OverrideDefaultButton(mojom::DialogButton button);

    // Sets which field should be initially focused in the dialog model. Must be
    // called after that field has been added. Can only be called once.
    Builder& SetInitiallyFocusedField(ElementIdentifier id);

   private:
    Builder& AddButtonInternal(ButtonCallbackVariant callback,
                               const Button::Params& params,
                               std::optional<Button>& model_button,
                               ButtonCallbackVariant& model_callback);

    std::unique_ptr<DialogModel> model_;
  };

  DialogModel(base::PassKey<DialogModel::Builder>,
              std::unique_ptr<DialogModelDelegate> delegate);

  DialogModel(const DialogModel&) = delete;
  DialogModel& operator=(const DialogModel&) = delete;

  ~DialogModel();

  // The host in which this model is hosted. Set by the Host implementation
  // during Host construction where it takes ownership of |this|.
  DialogModelHost* host() { return host_; }

  // Adds a paragraph at the end of the dialog model. A paragraph consists of a
  // label and an optional header.
  void AddParagraph(const DialogModelLabel& label,
                    std::u16string header = std::u16string(),
                    ElementIdentifier id = ElementIdentifier()) {
    contents_.AddParagraph(label, std::move(header), id);
  }

  // Adds a checkbox ([checkbox] label) at the end of the dialog model.
  void AddCheckbox(ElementIdentifier id,
                   const DialogModelLabel& label,
                   const DialogModelCheckbox::Params& params =
                       DialogModelCheckbox::Params()) {
    contents_.AddCheckbox(id, std::move(label), params);
  }

  // Adds a labeled combobox (label: [model]) at the end of the dialog model.
  void AddCombobox(ElementIdentifier id,
                   std::u16string label,
                   std::unique_ptr<ui::ComboboxModel> combobox_model,
                   const DialogModelCombobox::Params& params =
                       DialogModelCombobox::Params()) {
    contents_.AddCombobox(id, std::move(label), std::move(combobox_model),
                          params);
  }

  // Adds a menu item at the end of the dialog model.
  void AddMenuItem(ImageModel icon,
                   std::u16string label,
                   base::RepeatingCallback<void(int)> callback,
                   const DialogModelMenuItem::Params& params =
                       DialogModelMenuItem::Params()) {
    contents_.AddMenuItem(std::move(icon), std::move(label),
                          std::move(callback), params);
  }

  // Adds a title item at the end of the dialog model.
  // TODO(pengchaocai): Refactor this method once dialog_model supports multiple
  // DialogModelSection, when the title would be an optional member of
  // DialogModelSection and explicitly adding it might not be needed.
  void AddTitleItem(std::u16string label,
                    ElementIdentifier id = ElementIdentifier()) {
    contents_.AddTitleItem(std::move(label), id);
  }

  // Adds a separator at the end of the dialog model.
  void AddSeparator() { contents_.AddSeparator(); }

  // Adds a labeled textfield (label: [text]) at the end of the dialog model.
  void AddTextfield(ElementIdentifier id,
                    std::u16string label,
                    std::u16string text,
                    const DialogModelTextfield::Params& params =
                        DialogModelTextfield::Params()) {
    contents_.AddTextfield(id, std::move(label), std::move(text), params);
  }

  // Adds a labeled password field at the end of the dialog model.
  void AddPasswordField(ElementIdentifier id,
                        std::u16string label,
                        std::u16string accessible_text,
                        std::u16string incorrect_password_text,
                        const DialogModelPasswordField::Params& params =
                            DialogModelField::Params()) {
    contents_.AddPasswordField(id, std::move(label), std::move(accessible_text),
                               std::move(incorrect_password_text), params);
  }

  // Sets the footnote.
  void SetFootnote(const DialogModelLabel& label) {
    // It's not supported to set a footnote while the dialog is already showing.
    CHECK(!host_);

    footnote_label_.emplace(label);
  }

  // Adds a custom field at the end of the dialog model. This is used to inject
  // framework-specific custom UI into dialogs that are otherwise constructed as
  // DialogModels.
  void AddCustomField(std::unique_ptr<DialogModelCustomField::Field> field,
                      ElementIdentifier id = ElementIdentifier()) {
    contents_.AddCustomField(std::move(field), id);
  }

  // Check for the existence of a field. Should not be used if the code path
  // expects the |unique_id| to always be present, as GetFieldByUniqueId() and
  // friends will NOTREACHED() if |unique_id| is not present, detecting the bug.
  bool HasField(ElementIdentifier id) const;

  // Gets DialogModelFields from their unique identifier. |unique_id| is
  // supplied to the ::Params class during construction. Supplying a |unique_id|
  // not present in the model is a bug, and the methods will NOTREACHED(). If
  // you have unique fields that are conditionally present, see HasField().
  DialogModelField* GetFieldByUniqueId(ElementIdentifier id);
  DialogModelCheckbox* GetCheckboxByUniqueId(ElementIdentifier id) {
    return contents_.GetCheckboxByUniqueId(id);
  }
  DialogModelCombobox* GetComboboxByUniqueId(ElementIdentifier id) {
    return contents_.GetComboboxByUniqueId(id);
  }
  DialogModelTextfield* GetTextfieldByUniqueId(ElementIdentifier id) {
    return contents_.GetTextfieldByUniqueId(id);
  }
  DialogModelPasswordField* GetPasswordFieldByUniqueId(ElementIdentifier id) {
    return contents_.GetPasswordFieldByUniqueId(id);
  }
  Button* GetButtonByUniqueId(ElementIdentifier id);

  // Methods with base::PassKey<DialogModelHost> are only intended to be called
  // by the DialogModelHost implementation. The returned boolean is used to
  // indicate whether the dialog should close as a result of the action.
  bool OnDialogAcceptAction(base::PassKey<DialogModelHost>);
  bool OnDialogCancelAction(base::PassKey<DialogModelHost>);
  void OnDialogCloseAction(base::PassKey<DialogModelHost>);

  void OnDialogDestroying(base::PassKey<DialogModelHost>);

  void SetVisible(ElementIdentifier id, bool visible);

  void SetButtonLabel(Button* button, const std::u16string& label);

  void SetButtonEnabled(Button* button, bool enabled);

  // Called when added to a DialogModelHost.
  void set_host(base::PassKey<DialogModelHost>, DialogModelHost* host) {
    host_ = host;
  }

  const std::optional<bool>& override_show_close_button(
      base::PassKey<DialogModelHost>) const {
    return override_show_close_button_;
  }

  const std::string& internal_name(base::PassKey<DialogModelHost>) const {
    return internal_name_;
  }

  const std::u16string& title(base::PassKey<DialogModelHost>) const {
    return title_;
  }

  const std::u16string& accessible_title(base::PassKey<DialogModelHost>) const {
    return accessible_title_;
  }

  const std::u16string& subtitle(base::PassKey<DialogModelHost>) const {
    return subtitle_;
  }

  const ImageModel& main_image(base::PassKey<DialogModelHost>) const {
    return main_image_;
  }

  const ImageModel& icon(base::PassKey<DialogModelHost>) const { return icon_; }

  const ImageModel& dark_mode_icon(base::PassKey<DialogModelHost>) const {
    return dark_mode_icon_;
  }

  const ImageModel& banner(base::PassKey<DialogModelHost>) const {
    return banner_;
  }

  const ImageModel& dark_mode_banner(base::PassKey<DialogModelHost>) const {
    return dark_mode_banner_;
  }

  const std::optional<mojom::DialogButton>& override_default_button(
      base::PassKey<DialogModelHost>) const {
    return override_default_button_;
  }

  ElementIdentifier initially_focused_field(
      base::PassKey<DialogModelHost>) const {
    return initially_focused_field_;
  }

  bool is_alert_dialog(base::PassKey<DialogModelHost>) const {
    return is_alert_dialog_;
  }

  Button* ok_button(base::PassKey<DialogModelHost>) {
    return ok_button_.has_value() ? &ok_button_.value() : nullptr;
  }

  Button* cancel_button(base::PassKey<DialogModelHost>) {
    return cancel_button_.has_value() ? &cancel_button_.value() : nullptr;
  }

  Button* extra_button(base::PassKey<DialogModelHost>) {
    return extra_button_.has_value() ? &extra_button_.value() : nullptr;
  }

  DialogModelLabel::TextReplacement* extra_link(
      base::PassKey<DialogModelHost>) {
    return extra_link_.has_value() ? &extra_link_.value() : nullptr;
  }

  const std::optional<DialogModelLabel>& footnote_label() const {
    return footnote_label_;
  }

  bool close_on_deactivate(base::PassKey<DialogModelHost>) const {
    return close_on_deactivate_;
  }

  DialogModelSection* contents() { return &contents_; }

  // TODO(pbos): Replace this with a section() or something.
  const std::vector<std::unique_ptr<DialogModelField>>& fields(
      base::PassKey<DialogModelHost>) {
    return contents_.fields();
  }

 private:
  // Runs the appropriate variant of the provided ButtonCallbackVariant and
  // returns whether the dialog should close as a result.
  static bool RunButtonCallback(ButtonCallbackVariant& callback_variant);

  Button* MaybeGetButtonByUniqueId(ElementIdentifier id);

  std::unique_ptr<DialogModelDelegate> delegate_;
  raw_ptr<DialogModelHost> host_ = nullptr;

  std::optional<bool> override_show_close_button_;
  bool close_on_deactivate_ = true;
  std::string internal_name_;
  std::u16string title_;
  std::u16string accessible_title_;
  std::u16string subtitle_;
  ImageModel icon_;
  ImageModel dark_mode_icon_;

  ImageModel main_image_;

  ImageModel banner_;
  ImageModel dark_mode_banner_;

  std::optional<mojom::DialogButton> override_default_button_;
  DialogModelSection contents_;
  ElementIdentifier initially_focused_field_;
  bool is_alert_dialog_ = false;

  std::optional<Button> ok_button_;
  std::optional<Button> cancel_button_;
  std::optional<Button> extra_button_;
  std::optional<DialogModelLabel::TextReplacement> extra_link_;
  std::optional<DialogModelLabel> footnote_label_;

  ButtonCallbackVariant accept_action_callback_;
  ButtonCallbackVariant cancel_action_callback_;
  base::OnceClosure close_action_callback_;

  base::OnceClosure dialog_destroying_callback_;
};

}  // namespace ui

#endif  // UI_BASE_MODELS_DIALOG_MODEL_H_