// 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 #include #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 "third_party/abseil-cpp/absl/types/optional.h" #include "third_party/abseil-cpp/absl/types/variant.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/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 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. // // 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(); // 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(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(absl::optional 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_; absl::optional style_; bool is_enabled_ = true; base::flat_set accelerators_; }; Button(base::RepeatingCallback callback, const Params& params); Button(const Button&) = delete; Button& operator=(const Button&) = delete; ~Button() override; const std::u16string& label() const { return label_; } const absl::optional style() const { return style_; } bool is_enabled() const { return is_enabled_; } void OnPressed(base::PassKey, const Event& event); private: friend class DialogModel; std::u16string label_; const absl::optional style_; const 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 callback_; }; // A variant for button callbacks that allows different behavior to be // specified when a button is pressed. using ButtonCallbackVariant = absl::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>; // 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 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 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 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 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 callback, const DialogModelMenuItem::Params& params = DialogModelMenuItem::Params()) { model_->AddMenuItem(std::move(icon), std::move(label), std::move(callback), params); 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 custom field. See DialogModel::AddCustomField(). Builder& AddCustomField( std::unique_ptr 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(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, absl::optional