blob: c310805797e3af0bf66de237714be06d0e57d3d6 [file] [log] [blame] [edit]
// 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 button
// must exist.
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_