| // Copyright 2012 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "ui/views/window/dialog_delegate.h" |
| |
| #include <utility> |
| #include <variant> |
| |
| #include "base/debug/alias.h" |
| #include "base/feature_list.h" |
| #include "base/logging.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/observer_list.h" |
| #include "build/build_config.h" |
| #include "ui/accessibility/ax_enums.mojom.h" |
| #include "ui/accessibility/ax_node_data.h" |
| #include "ui/accessibility/ax_role_properties.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/base/metadata/metadata_impl_macros.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_palette.h" |
| #include "ui/gfx/geometry/rounded_corners_f.h" |
| #include "ui/strings/grit/ui_strings.h" |
| #include "ui/views/bubble/bubble_border.h" |
| #include "ui/views/bubble/bubble_dialog_delegate_view.h" |
| #include "ui/views/bubble/bubble_frame_view.h" |
| #include "ui/views/buildflags.h" |
| #include "ui/views/layout/layout_provider.h" |
| #include "ui/views/style/platform_style.h" |
| #include "ui/views/view_utils.h" |
| #include "ui/views/views_features.h" |
| #include "ui/views/widget/widget.h" |
| #include "ui/views/widget/widget_observer.h" |
| #include "ui/views/window/dialog_client_view.h" |
| #include "ui/views/window/dialog_observer.h" |
| |
| namespace views { |
| |
| namespace { |
| |
| // Class that ensures that dialogs are logically "parented" to their parent for |
| // various purposes, including testing, theming, and Tutorials. |
| class DialogWidget : public Widget { |
| public: |
| DialogWidget() = default; |
| ~DialogWidget() override = default; |
| |
| // Widget: |
| Widget* GetPrimaryWindowWidget() override { |
| // Dialogs are usually parented to another window, so that window should be |
| // the primary window. Only fall back to default Widget behavior if there is |
| // no parent. |
| return parent() ? parent()->GetPrimaryWindowWidget() |
| : Widget::GetPrimaryWindowWidget(); |
| } |
| |
| // TODO(dfried): Possibly also fix the following (possibly in Widget) so they |
| // don't have to be overridden in bubble_dialog_delegate_view.cc: |
| // - GetCustomTheme() |
| // - GetNativeTheme() |
| // - GetColorProvider() |
| }; |
| |
| bool HasCallback( |
| const std::variant<base::OnceClosure, base::RepeatingCallback<bool()>>& |
| callback) { |
| return std::visit( |
| [](const auto& variant) { return static_cast<bool>(variant); }, callback); |
| } |
| |
| } // namespace |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // DialogDelegate::Params: |
| DialogDelegate::Params::Params() = default; |
| DialogDelegate::Params::~Params() = default; |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // DialogDelegate: |
| |
| DialogDelegate::DialogDelegate() { |
| RegisterWindowWillCloseCallback( |
| RegisterWillCloseCallbackPassKey(), |
| base::BindOnce(&DialogDelegate::WindowWillClose, base::Unretained(this))); |
| } |
| |
| // static |
| Widget* DialogDelegate::CreateDialogWidget(WidgetDelegate* delegate, |
| gfx::NativeWindow context, |
| gfx::NativeView parent) { |
| views::Widget* widget = new DialogWidget; |
| views::Widget::InitParams params = |
| GetDialogWidgetInitParams(delegate, context, parent, gfx::Rect()); |
| widget->Init(std::move(params)); |
| return widget; |
| } |
| |
| // static |
| Widget* DialogDelegate::CreateDialogWidget( |
| std::unique_ptr<WidgetDelegate> delegate, |
| gfx::NativeWindow context, |
| gfx::NativeView parent) { |
| return CreateDialogWidget(delegate.release(), context, parent); |
| } |
| |
| // static |
| bool DialogDelegate::CanSupportCustomFrame(gfx::NativeView parent) { |
| #if BUILDFLAG(IS_LINUX) && BUILDFLAG(ENABLE_DESKTOP_AURA) |
| // The new style doesn't support unparented dialogs on Linux desktop. |
| return parent != nullptr; |
| #else |
| return true; |
| #endif |
| } |
| |
| // static |
| Widget::InitParams DialogDelegate::GetDialogWidgetInitParams( |
| WidgetDelegate* delegate, |
| gfx::NativeWindow context, |
| gfx::NativeView parent, |
| const gfx::Rect& bounds) { |
| DialogDelegate* dialog = delegate->AsDialogDelegate(); |
| |
| views::Widget::InitParams params( |
| dialog ? dialog->ownership_of_new_widget_ |
| : Widget::InitParams::NATIVE_WIDGET_OWNS_WIDGET); |
| params.delegate = delegate; |
| params.bounds = bounds; |
| |
| if (dialog) { |
| dialog->params_.custom_frame &= CanSupportCustomFrame(parent); |
| } |
| |
| if (!dialog || dialog->use_custom_frame()) { |
| params.opacity = Widget::InitParams::WindowOpacity::kTranslucent; |
| params.remove_standard_frame = true; |
| #if !BUILDFLAG(IS_MAC) |
| // Except on Mac, the bubble frame includes its own shadow; remove any |
| // native shadowing. On Mac, the window server provides the shadow. |
| params.shadow_type = views::Widget::InitParams::ShadowType::kNone; |
| #endif |
| } |
| params.context = context; |
| params.parent = parent; |
| #if !BUILDFLAG(IS_APPLE) |
| // Web-modal (ui::mojom::ModalType::kChild) dialogs with parents are marked as |
| // child widgets to prevent top-level window behavior (independent movement, |
| // etc). However, on Mac or when forcing to use desktop widget, the |
| // dialog must be considered top-level to gain focus and input method |
| // behaviors. |
| // TODO(crbug.com/346974105): This might be wrong because it implies multiple |
| // focus managers in a widget tree. A widget tree should have a single focus |
| // manager, so that it is impossible for two widgets to have focus |
| // simultaneously. |
| params.child = parent && |
| (delegate->GetModalType() == ui::mojom::ModalType::kChild) && |
| !delegate->use_desktop_widget_override(); |
| #endif |
| |
| if (BubbleDialogDelegate* bubble = delegate->AsBubbleDialogDelegate()) { |
| // TODO(crbug.com/41493925): Remove this CHECK once native frame dialogs |
| // support autosize. |
| CHECK(!bubble->is_autosized() || bubble->use_custom_frame()) |
| << "Autosizing native frame dialogs is not supported."; |
| params.autosize = bubble->is_autosized(); |
| } |
| |
| return params; |
| } |
| |
| int DialogDelegate::GetDefaultDialogButton() const { |
| if (GetParams().default_button.has_value()) { |
| return *GetParams().default_button; |
| } |
| if (buttons() & static_cast<int>(ui::mojom::DialogButton::kOk)) { |
| return static_cast<int>(ui::mojom::DialogButton::kOk); |
| } |
| if (buttons() & static_cast<int>(ui::mojom::DialogButton::kCancel)) { |
| return static_cast<int>(ui::mojom::DialogButton::kCancel); |
| } |
| return static_cast<int>(ui::mojom::DialogButton::kNone); |
| } |
| |
| std::u16string DialogDelegate::GetDialogButtonLabel( |
| ui::mojom::DialogButton button) const { |
| if (!GetParams().button_labels[static_cast<size_t>(button)].empty()) { |
| return GetParams().button_labels[static_cast<size_t>(button)]; |
| } |
| |
| if (button == ui::mojom::DialogButton::kOk) { |
| return l10n_util::GetStringUTF16(IDS_APP_OK); |
| } |
| CHECK_EQ(button, ui::mojom::DialogButton::kCancel); |
| return buttons() & static_cast<int>(ui::mojom::DialogButton::kOk) |
| ? l10n_util::GetStringUTF16(IDS_APP_CANCEL) |
| : l10n_util::GetStringUTF16(IDS_APP_CLOSE); |
| } |
| |
| ui::ButtonStyle DialogDelegate::GetDialogButtonStyle( |
| ui::mojom::DialogButton button) const { |
| std::optional<ui::ButtonStyle> style = |
| GetParams().button_styles[static_cast<size_t>(button)]; |
| if (style.has_value()) { |
| return *style; |
| } |
| |
| return GetIsDefault(button) ? ui::ButtonStyle::kProminent |
| : ui::ButtonStyle::kTonal; |
| } |
| |
| bool DialogDelegate::GetIsDefault(ui::mojom::DialogButton button) const { |
| return GetDefaultDialogButton() == static_cast<int>(button) && |
| (button != ui::mojom::DialogButton::kCancel || |
| PlatformStyle::kDialogDefaultButtonCanBeCancel); |
| } |
| |
| bool DialogDelegate::IsDialogButtonEnabled( |
| ui::mojom::DialogButton button) const { |
| return params_.enabled_buttons & static_cast<int>(button); |
| } |
| |
| bool DialogDelegate::ShouldIgnoreButtonPressedEventHandling( |
| View* button, |
| const ui::Event& event) const { |
| return false; |
| } |
| |
| bool DialogDelegate::ShouldAllowKeyEventsDuringInputProtection() const { |
| return true; |
| } |
| |
| bool DialogDelegate::Cancel() { |
| DCHECK(!already_started_close_); |
| if (HasCallback(cancel_callback_)) { |
| return RunCloseCallback(cancel_callback_); |
| } |
| return true; |
| } |
| |
| bool DialogDelegate::Accept() { |
| DCHECK(!already_started_close_); |
| if (HasCallback(accept_callback_)) { |
| return RunCloseCallback(accept_callback_); |
| } |
| return true; |
| } |
| |
| bool DialogDelegate::RunCloseCallback( |
| std::variant<base::OnceClosure, base::RepeatingCallback<bool()>>& |
| callback) { |
| DCHECK(!already_started_close_); |
| if (std::holds_alternative<base::OnceClosure>(callback)) { |
| already_started_close_ = true; |
| std::get<base::OnceClosure>(std::move(callback)).Run(); |
| return true; |
| } else { |
| base::WeakPtr<Widget> weak_ptr = GetWidget()->GetWeakPtr(); |
| bool already_started_close = |
| std::get<base::RepeatingCallback<bool()>>(callback).Run(); |
| // Widget may get destroyed after the callback is run, this will detect |
| // that condition. |
| if (!weak_ptr) { |
| return false; |
| } |
| already_started_close_ = already_started_close; |
| return already_started_close_; |
| } |
| |
| NOTREACHED(); |
| } |
| |
| View* DialogDelegate::GetInitiallyFocusedView() { |
| if (WidgetDelegate::HasConfiguredInitiallyFocusedView()) { |
| return WidgetDelegate::GetInitiallyFocusedView(); |
| } |
| |
| // Focus the default button if any. |
| const DialogClientView* dcv = GetDialogClientView(); |
| if (!dcv) { |
| return nullptr; |
| } |
| int default_button = GetDefaultDialogButton(); |
| if (default_button == static_cast<int>(ui::mojom::DialogButton::kNone)) { |
| return nullptr; |
| } |
| |
| // The default button should be a button we have. |
| CHECK(default_button & buttons()); |
| |
| if (default_button & static_cast<int>(ui::mojom::DialogButton::kOk)) { |
| return dcv->ok_button(); |
| } |
| if (default_button & static_cast<int>(ui::mojom::DialogButton::kCancel)) { |
| return dcv->cancel_button(); |
| } |
| return nullptr; |
| } |
| |
| DialogDelegate* DialogDelegate::AsDialogDelegate() { |
| return this; |
| } |
| |
| ClientView* DialogDelegate::CreateClientView(Widget* widget) { |
| return new DialogClientView(widget, TransferOwnershipOfContentsView()); |
| } |
| |
| std::unique_ptr<FrameView> DialogDelegate::CreateFrameView(Widget* widget) { |
| return use_custom_frame() ? CreateDialogFrameView(widget) |
| : WidgetDelegate::CreateFrameView(widget); |
| } |
| |
| void DialogDelegate::WindowWillClose() { |
| if (already_started_close_) { |
| return; |
| } |
| |
| const bool new_callback_present = close_callback_ || |
| HasCallback(cancel_callback_) || |
| HasCallback(accept_callback_); |
| |
| if (close_callback_) { |
| // `RunCloseCallback` takes a non-const reference to this variant to support |
| // the accept and cancel callbacks. It doesn't make sense to be storing a |
| // variant for close callbacks, so we construct the variant here instead. |
| std::variant<base::OnceClosure, base::RepeatingCallback<bool()>> |
| close_callback_wrapped(std::move(close_callback_)); |
| RunCloseCallback(close_callback_wrapped); |
| } |
| |
| if (new_callback_present) { |
| return; |
| } |
| |
| // This is set here instead of before the invocations of Accept()/Cancel() so |
| // that those methods can DCHECK that !already_started_close_. Otherwise, |
| // client code could (eg) call Accept() from inside the cancel callback, which |
| // could lead to multiple callbacks being delivered from this class. |
| already_started_close_ = true; |
| } |
| |
| bool DialogDelegate::EscShouldCancelDialog() const { |
| // Use cancel as the Esc action if there's no defined "close" action. If the |
| // delegate has either specified a closing action or a close-x they can expect |
| // it to be called on Esc. |
| return esc_should_cancel_dialog_override_.value_or(!close_callback_ && |
| !ShouldShowCloseButton()); |
| } |
| |
| // static |
| std::unique_ptr<FrameView> DialogDelegate::CreateDialogFrameView( |
| Widget* widget) { |
| LayoutProvider* provider = LayoutProvider::Get(); |
| auto frame = std::make_unique<BubbleFrameView>( |
| provider->GetInsetsMetric(INSETS_DIALOG_TITLE), gfx::Insets()); |
| |
| const BubbleBorder::Shadow kShadow = BubbleBorder::DIALOG_SHADOW; |
| std::unique_ptr<BubbleBorder> border = |
| std::make_unique<BubbleBorder>(BubbleBorder::FLOAT, kShadow); |
| DialogDelegate* delegate = widget->widget_delegate()->AsDialogDelegate(); |
| if (delegate) { |
| if (delegate->GetParams().round_corners) { |
| border->set_rounded_corners( |
| gfx::RoundedCornersF(delegate->GetCornerRadius())); |
| } |
| frame->SetFootnoteView(delegate->DisownFootnoteView()); |
| } |
| frame->SetBubbleBorder(std::move(border)); |
| return frame; |
| } |
| |
| const DialogClientView* DialogDelegate::GetDialogClientView() const { |
| if (!GetWidget()) { |
| return nullptr; |
| } |
| return AsViewClass<DialogClientView>(GetWidget()->client_view()); |
| } |
| |
| DialogClientView* DialogDelegate::GetDialogClientView() { |
| return const_cast<DialogClientView*>( |
| const_cast<const DialogDelegate*>(this)->GetDialogClientView()); |
| } |
| |
| BubbleFrameView* DialogDelegate::GetBubbleFrameView() const { |
| if (!use_custom_frame()) { |
| return nullptr; |
| } |
| |
| const NonClientView* view = |
| GetWidget() ? GetWidget()->non_client_view() : nullptr; |
| return view ? static_cast<BubbleFrameView*>(view->frame_view()) : nullptr; |
| } |
| |
| views::MdTextButton* DialogDelegate::GetOkButton() const { |
| DCHECK(GetWidget()) << "Don't call this before OnWidgetInitialized"; |
| auto* client = GetDialogClientView(); |
| return client ? client->ok_button() : nullptr; |
| } |
| |
| views::MdTextButton* DialogDelegate::GetCancelButton() const { |
| DCHECK(GetWidget()) << "Don't call this before OnWidgetInitialized"; |
| auto* client = GetDialogClientView(); |
| return client ? client->cancel_button() : nullptr; |
| } |
| |
| views::View* DialogDelegate::GetExtraView() const { |
| DCHECK(GetWidget()) << "Don't call this before OnWidgetInitialized"; |
| auto* client = GetDialogClientView(); |
| return client ? client->extra_view() : nullptr; |
| } |
| |
| views::View* DialogDelegate::GetFootnoteViewForTesting() const { |
| if (!GetWidget()) { |
| return footnote_view_.get(); |
| } |
| |
| FrameView* frame = GetWidget()->non_client_view()->frame_view(); |
| |
| // CreateDialogFrameView above always uses BubbleFrameView. There are |
| // subclasses that override CreateDialogFrameView, but none of them override |
| // it to create anything other than a BubbleFrameView. |
| // TODO(crbug.com/40101916): Make CreateDialogFrameView final, then |
| // remove this DCHECK. |
| DCHECK(IsViewClass<BubbleFrameView>(frame)); |
| return static_cast<BubbleFrameView*>(frame)->GetFootnoteView(); |
| } |
| |
| void DialogDelegate::AddObserver(DialogObserver* observer) { |
| observer_list_.AddObserver(observer); |
| } |
| |
| void DialogDelegate::RemoveObserver(DialogObserver* observer) { |
| observer_list_.RemoveObserver(observer); |
| } |
| |
| void DialogDelegate::DialogModelChanged() { |
| observer_list_.Notify(&DialogObserver::OnDialogChanged); |
| } |
| |
| void DialogDelegate::TriggerInputProtection(bool force_early) { |
| GetDialogClientView()->TriggerInputProtection(force_early); |
| } |
| |
| void DialogDelegate::SetDefaultButton(int button) { |
| if (params_.default_button == button) { |
| return; |
| } |
| params_.default_button = button; |
| DialogModelChanged(); |
| } |
| |
| void DialogDelegate::SetButtons(int buttons) { |
| if (params_.buttons == buttons) { |
| return; |
| } |
| params_.buttons = buttons; |
| DialogModelChanged(); |
| } |
| |
| void DialogDelegate::SetButtonEnabled(ui::mojom::DialogButton dialog_button, |
| bool enabled) { |
| int button = static_cast<int>(dialog_button); |
| if (!!(params_.enabled_buttons & button) == enabled) { |
| return; |
| } |
| if (enabled) { |
| params_.enabled_buttons |= button; |
| } else { |
| params_.enabled_buttons &= ~button; |
| } |
| DialogModelChanged(); |
| } |
| |
| void DialogDelegate::SetButtonLabel(ui::mojom::DialogButton button, |
| std::u16string_view label) { |
| if (params_.button_labels[static_cast<size_t>(button)] == label) { |
| return; |
| } |
| params_.button_labels[static_cast<size_t>(button)] = std::u16string(label); |
| DialogModelChanged(); |
| } |
| |
| void DialogDelegate::SetButtonStyle(ui::mojom::DialogButton button, |
| std::optional<ui::ButtonStyle> style) { |
| if (params_.button_styles[static_cast<size_t>(button)] == style) { |
| return; |
| } |
| params_.button_styles[static_cast<size_t>(button)] = style; |
| DialogModelChanged(); |
| } |
| |
| void DialogDelegate::SetAcceptCallback(base::OnceClosure callback) { |
| accept_callback_ = std::move(callback); |
| } |
| |
| void DialogDelegate::SetAcceptCallbackWithClose( |
| base::RepeatingCallback<bool()> callback) { |
| accept_callback_ = std::move(callback); |
| } |
| |
| void DialogDelegate::SetCancelCallback(base::OnceClosure callback) { |
| cancel_callback_ = std::move(callback); |
| } |
| |
| void DialogDelegate::SetCancelCallbackWithClose( |
| base::RepeatingCallback<bool()> callback) { |
| cancel_callback_ = std::move(callback); |
| } |
| |
| void DialogDelegate::SetCloseCallback(base::OnceClosure callback) { |
| close_callback_ = std::move(callback); |
| } |
| |
| void DialogDelegate::SetOwnershipOfNewWidget( |
| Widget::InitParams::Ownership ownership) { |
| CHECK(!GetWidget()); |
| ownership_of_new_widget_ = ownership; |
| } |
| |
| std::optional<std::unique_ptr<View>> DialogDelegate::DisownExtraView() { |
| return std::exchange(extra_view_, std::nullopt); |
| } |
| |
| bool DialogDelegate::Close() { |
| WindowWillClose(); |
| return true; |
| } |
| |
| void DialogDelegate::ResetViewShownTimeStampForTesting() { |
| GetDialogClientView()->ResetViewShownTimeStampForTesting(); |
| } |
| |
| void DialogDelegate::SetButtonRowInsets(const gfx::Insets& insets) { |
| GetDialogClientView()->SetButtonRowInsets(insets); |
| } |
| |
| void DialogDelegate::AcceptDialog() { |
| // Copy the dialog widget name onto the stack so it appears in crash dumps. |
| DEBUG_ALIAS_FOR_CSTR(last_widget_name, GetWidget()->GetName().c_str(), 64); |
| |
| DCHECK(IsDialogButtonEnabled(ui::mojom::DialogButton::kOk)); |
| |
| // Widget (and maybe this delegate) may get destroyed from the Accept() call |
| // below. This will detect that condition. |
| auto weak_widget_ptr = GetWidget()->GetWeakPtr(); |
| |
| if (already_started_close_ || !Accept()) { |
| return; |
| } |
| |
| // `this` may also have been destroyed as a result of the call to Accept(). |
| // Checking whether the widget was destroyed is sufficient for either case. If |
| // only the Widget is destroyed, performing the following actions is |
| // irrelevant (and dangerous). If the widget and this delegate are destroyed |
| // together, then we still need to avoid the following actions. |
| if (!weak_widget_ptr) { |
| return; |
| } |
| already_started_close_ = true; |
| weak_widget_ptr->CloseWithReason( |
| views::Widget::ClosedReason::kAcceptButtonClicked); |
| } |
| |
| void DialogDelegate::CancelDialog() { |
| // If there's a close button available, this callback should only be reachable |
| // if the cancel button is available. Otherwise this can be reached through |
| // closing the dialog via Esc. |
| if (ShouldShowCloseButton()) { |
| DCHECK(IsDialogButtonEnabled(ui::mojom::DialogButton::kCancel)); |
| } |
| |
| // Widget (and maybe this delegate) may get destroyed from the Cancel() call |
| // below. This will detect that condition. |
| auto weak_widget_ptr = GetWidget()->GetWeakPtr(); |
| |
| if (already_started_close_ || !Cancel()) { |
| return; |
| } |
| |
| // See the comment above in AcceptDialog(). |
| if (!weak_widget_ptr) { |
| return; |
| } |
| already_started_close_ = true; |
| weak_widget_ptr->CloseWithReason( |
| views::Widget::ClosedReason::kCancelButtonClicked); |
| } |
| |
| DialogDelegate::~DialogDelegate() = default; |
| |
| ax::mojom::Role DialogDelegate::GetAccessibleWindowRole() { |
| return ax::mojom::Role::kDialog; |
| } |
| |
| int DialogDelegate::GetCornerRadius() const { |
| if (!Widget::IsWindowCompositingSupported()) { |
| return 0; |
| } |
| #if BUILDFLAG(IS_MAC) |
| // TODO(crbug.com/40144839): On Mac MODAL_TYPE_WINDOW is implemented using |
| // sheets which causes visual artifacts when corner radius is increased for |
| // modal types. Remove this after this issue has been addressed. |
| if (GetModalType() == ui::mojom::ModalType::kWindow) { |
| return 2; |
| } |
| #endif |
| if (params_.corner_radius) { |
| return *params_.corner_radius; |
| } |
| return LayoutProvider::Get()->GetCornerRadiusMetric( |
| views::ShapeContextTokens::kDialogRadius); |
| } |
| |
| std::unique_ptr<View> DialogDelegate::DisownFootnoteView() { |
| return std::move(footnote_view_); |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // DialogDelegateView: |
| |
| DialogDelegateView::~DialogDelegateView() = default; |
| |
| Widget* DialogDelegateView::GetWidget() { |
| return View::GetWidget(); |
| } |
| |
| const Widget* DialogDelegateView::GetWidget() const { |
| return View::GetWidget(); |
| } |
| |
| View* DialogDelegateView::GetContentsView() { |
| return this; |
| } |
| |
| DialogDelegateView::DialogDelegateView() = default; |
| |
| BEGIN_METADATA(DialogDelegateView) |
| END_METADATA |
| |
| } // namespace views |