// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ash/style/system_dialog_delegate_view.h"
#include <memory>
#include "ash/public/cpp/ash_view_ids.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/style/typography.h"
#include "base/functional/bind.h"
#include "ui/aura/window.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/base/models/image_model.h"
#include "ui/chromeos/styles/cros_tokens_color_mappings.h"
#include "ui/color/color_id.h"
#include "ui/compositor/layer.h"
#include "ui/events/event.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/strings/grit/ui_strings.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/background.h"
#include "ui/views/border.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/controls/label.h"
#include "ui/views/highlight_border.h"
#include "ui/views/layout/flex_layout.h"
#include "ui/views/layout/flex_layout_types.h"
#include "ui/views/layout/flex_layout_view.h"
#include "ui/views/layout/layout_types.h"
#include "ui/views/view_class_properties.h"
#include "ui/wm/core/window_util.h"
namespace ash {
namespace {
// The default color IDs of the dialog.
constexpr ui::ColorId kBackgroundColorId = cros_tokens::kCrosSysBaseElevated;
constexpr ui::ColorId kBodyColorId = cros_tokens::kCrosSysOnSurfaceVariant;
constexpr ui::ColorId kIconColorId = cros_tokens::kCrosSysPrimary;
constexpr ui::ColorId kTitleColorId = cros_tokens::kCrosSysOnSurface;
// The default layout parameters of the dialog.
constexpr gfx::Size kMinimumDialogSize = gfx::Size(296, 144);
constexpr gfx::Size kMaximumDialogSize = gfx::Size(512, 600);
constexpr int kRoundedCornerRadius = 20;
constexpr gfx::Insets kBorderInsets = gfx::Insets::TLBR(32, 32, 28, 32);
constexpr int kIconSize = 32;
constexpr int kIconBottomPadding = 20;
constexpr int kTitleBottomPadding = 16;
constexpr int kDefaultContentPadding = 32;
constexpr int kButtonContainerTopPadding = 32;
constexpr int kButtonSpacing = 8;
constexpr int kMinimumAdditionalButtonPadding = 80;
// Typical sizes of a dialog.
constexpr int kDialogWidthLarge = 512;
constexpr int kDialogWidthMedium = 359;
constexpr int kDialogWidthSmall = 296;
// The host window sizes that will change the resizing rule of the dialog.
constexpr int kHostWidthLarge = 672;
constexpr int kHostWidthMedium = 520;
constexpr int kHostWidthSmall = 424;
constexpr int kHostWidthXSmall = 400;
// Padding between the dialog and the host window.
constexpr int kDialogHostPaddingLarge = 80;
constexpr int kDialogHostPaddingSmall = 32;
// The default fonts of the title and description.
constexpr TypographyToken kTitleFont = TypographyToken::kCrosDisplay7;
constexpr TypographyToken kBodyFont = TypographyToken::kCrosBody1;
// Sets margins and flex layout specs to the view.
void SetViewLayoutSpecs(
views::View* view,
const gfx::Insets& margins = gfx::Insets(),
const views::FlexSpecification flex_spec = views::FlexSpecification()) {
view->SetProperty(views::kMarginsKey, margins);
view->SetProperty(views::kFlexBehaviorKey, flex_spec);
}
// Sets the cross alignment to the given view.
void SetViewCrossAxisAlignment(views::View* view,
views::LayoutAlignment alignment) {
CHECK(view);
auto* cross_alignment = view->GetProperty(views::kCrossAxisAlignmentKey);
if (!cross_alignment || *cross_alignment != alignment) {
view->SetProperty(views::kCrossAxisAlignmentKey, alignment);
}
}
// Gets the host window of the dialog.
aura::Window* GetDialogHostWindow(const views::Widget* dialog_widget) {
if (!dialog_widget) {
return nullptr;
}
// Return transient parent as the host window if exists. Otherwise, return the
// default parent.
auto* dialog_window = dialog_widget->GetNativeWindow();
auto* transient_parent = wm::GetTransientParent(dialog_window);
return transient_parent ? transient_parent : dialog_window->parent();
}
} // namespace
DEFINE_CLASS_ELEMENT_IDENTIFIER_VALUE(SystemDialogDelegateView,
kAcceptButtonIdForTesting);
DEFINE_CLASS_ELEMENT_IDENTIFIER_VALUE(SystemDialogDelegateView,
kCancelButtonIdForTesting);
DEFINE_CLASS_ELEMENT_IDENTIFIER_VALUE(SystemDialogDelegateView,
kDescriptionTextIdForTesting);
DEFINE_CLASS_ELEMENT_IDENTIFIER_VALUE(SystemDialogDelegateView,
kTitleTextIdForTesting);
//------------------------------------------------------------------------------
// SystemDialogDelegateView::ButtonContainer:
// The container includes an accept button and a cancel button. The buttons are
// awlays at the right bottom corner of the dialog. The container also allows an
// additional view to be added at the left side. Please refer to the example in
// the header file for the container layout.
class SystemDialogDelegateView::ButtonContainer : public views::FlexLayoutView {
METADATA_HEADER(ButtonContainer, views::FlexLayoutView)
public:
explicit ButtonContainer(SystemDialogDelegateView* dialog_view)
: cancel_button_(AddChildView(std::make_unique<PillButton>(
base::BindRepeating(&SystemDialogDelegateView::Cancel,
base::Unretained(dialog_view)),
l10n_util::GetStringUTF16(IDS_APP_CANCEL),
PillButton::Type::kSecondaryWithoutIcon))),
accept_button_(AddChildView(std::make_unique<PillButton>(
base::BindRepeating(&SystemDialogDelegateView::Accept,
base::Unretained(dialog_view)),
l10n_util::GetStringUTF16(IDS_APP_OK),
PillButton::Type::kPrimaryWithoutIcon))) {
SetOrientation(views::LayoutOrientation::kHorizontal);
SetMainAxisAlignment(views::LayoutAlignment::kEnd);
SetCrossAxisAlignment(views::LayoutAlignment::kCenter);
SetViewLayoutSpecs(cancel_button_,
gfx::Insets::TLBR(0, 0, 0, kButtonSpacing));
cancel_button_->SetID(
ViewID::VIEW_ID_STYLE_SYSTEM_DIALOG_DELEGATE_CANCEL_BUTTON);
cancel_button_->SetProperty(views::kElementIdentifierKey,
kCancelButtonIdForTesting);
cancel_button_->SetBackgroundColorId(cros_tokens::kCrosSysPrimaryContainer);
cancel_button_->SetButtonTextColorId(
cros_tokens::kCrosSysOnPrimaryContainer);
cancel_button_->SetIconColorId(cros_tokens::kCrosSysOnPrimaryContainer);
accept_button_->SetID(
ViewID::VIEW_ID_STYLE_SYSTEM_DIALOG_DELEGATE_ACCEPT_BUTTON);
accept_button_->SetProperty(views::kElementIdentifierKey,
kAcceptButtonIdForTesting);
accept_button_->SetIsDefault(true);
}
ButtonContainer(const ButtonContainer&) = delete;
ButtonContainer& operator=(const ButtonContainer&) = delete;
~ButtonContainer() override = default;
const PillButton* accept_button() const { return accept_button_; }
PillButton* accept_button() { return accept_button_; }
const PillButton* cancel_button() const { return cancel_button_; }
PillButton* cancel_button() { return cancel_button_; }
void SetAcceptText(const std::u16string& accept_text) {
accept_button_->SetText(accept_text);
}
void SetCancelText(const std::u16string& cancel_text) {
cancel_button_->SetText(cancel_text);
}
void SetAdditionalView(std::unique_ptr<views::View> additional_view) {
if (additional_view_) {
RemoveChildViewT(additional_view_);
}
// Create a place holder view to fill the space between the cancel button
// and the additional view.
if (!place_holder_view_) {
place_holder_view_ = AddChildViewAt(std::make_unique<views::View>(), 0);
SetViewLayoutSpecs(
place_holder_view_,
gfx::Insets::TLBR(0, 0, 0, kMinimumAdditionalButtonPadding),
views::FlexSpecification(views::MinimumFlexSizeRule::kPreferred,
views::MaximumFlexSizeRule::kUnbounded));
}
additional_view_ = AddChildViewAt(std::move(additional_view), 0);
}
private:
// Owned by the container.
raw_ptr<PillButton> cancel_button_ = nullptr;
raw_ptr<PillButton> accept_button_ = nullptr;
raw_ptr<views::View> additional_view_ = nullptr;
// The view used to fill the free spaces between the additional view and
// cancel button.
raw_ptr<views::View> place_holder_view_ = nullptr;
};
BEGIN_METADATA(SystemDialogDelegateView, ButtonContainer)
END_METADATA
//------------------------------------------------------------------------------
// SystemDialogDelegateView:
SystemDialogDelegateView::SystemDialogDelegateView() {
// Set border and background.
SetBorder(views::CreatePaddedBorder(
std::make_unique<views::HighlightBorder>(
kRoundedCornerRadius,
views::HighlightBorder::Type::kHighlightBorderOnShadow),
kBorderInsets));
SetBackground(views::CreateThemedRoundedRectBackground(kBackgroundColorId,
kRoundedCornerRadius));
// Set shadow.
shadow_ = SystemShadow::CreateShadowOnNinePatchLayerForView(
this, SystemShadow::Type::kElevation12);
shadow_->SetRoundedCornerRadius(kRoundedCornerRadius);
// Use flex layout.
SetLayoutManager(std::make_unique<views::FlexLayout>())
->SetOrientation(views::LayoutOrientation::kVertical)
.SetMainAxisAlignment(views::LayoutAlignment::kStart)
.SetCrossAxisAlignment(views::LayoutAlignment::kStretch)
.SetCollapseMargins(true);
icon_ = AddChildView(std::make_unique<views::ImageView>());
SetViewLayoutSpecs(icon_, gfx::Insets::TLBR(0, 0, kIconBottomPadding, 0));
icon_->SetProperty(views::kCrossAxisAlignmentKey,
views::LayoutAlignment::kStart);
icon_->SetVisible(false);
// Configure icon, title, description, and button container with pre-defined
// layout.
auto* typography_provider = TypographyProvider::Get();
title_ = AddChildView(std::make_unique<views::Label>());
SetViewLayoutSpecs(title_, gfx::Insets::TLBR(0, 0, kTitleBottomPadding, 0));
typography_provider->StyleLabel(kTitleFont, *title_);
title_->SetHorizontalAlignment(gfx::ALIGN_LEFT);
title_->SetAutoColorReadabilityEnabled(false);
title_->SetEnabledColorId(kTitleColorId);
title_->SetVisible(false);
title_->GetViewAccessibility().SetRole(ax::mojom::Role::kHeading);
title_->SetProperty(views::kElementIdentifierKey, kTitleTextIdForTesting);
description_ = AddChildView(std::make_unique<views::Label>());
SetViewLayoutSpecs(
description_, gfx::Insets(),
views::FlexSpecification(views::LayoutOrientation::kHorizontal,
views::MinimumFlexSizeRule::kScaleToMinimum,
views::MaximumFlexSizeRule::kUnbounded, true,
views::MinimumFlexSizeRule::kPreferred));
typography_provider->StyleLabel(kBodyFont, *description_);
description_->SetHorizontalAlignment(gfx::ALIGN_LEFT);
description_->SetMultiLine(true);
description_->SetAllowCharacterBreak(true);
description_->SetAutoColorReadabilityEnabled(false);
description_->SetEnabledColorId(kBodyColorId);
description_->SetVisible(false);
description_->SetProperty(views::kElementIdentifierKey,
kDescriptionTextIdForTesting);
button_container_ = AddChildView(std::make_unique<ButtonContainer>(this));
SetViewLayoutSpecs(
button_container_, gfx::Insets::TLBR(kButtonContainerTopPadding, 0, 0, 0),
views::FlexSpecification(views::LayoutOrientation::kHorizontal,
views::MinimumFlexSizeRule::kScaleToMinimum,
views::MaximumFlexSizeRule::kUnbounded));
SetAccessibleWindowRole(ax::mojom::Role::kDialog);
// Make dialog initially focus on the accept button.
SetInitiallyFocusedView(button_container_->accept_button());
// Register the close callback.
RegisterWindowClosingCallback(
base::BindOnce(&SystemDialogDelegateView::Close, base::Unretained(this)));
}
SystemDialogDelegateView::~SystemDialogDelegateView() = default;
void SystemDialogDelegateView::SetIcon(const gfx::VectorIcon& icon) {
icon_->SetImage(
ui::ImageModel::FromVectorIcon(icon, kIconColorId, kIconSize));
icon_->SetVisible(true);
}
void SystemDialogDelegateView::SetTitleText(const std::u16string& title) {
title_->SetText(title);
title_->SetVisible(!title.empty());
SetAccessibleTitle(title);
}
void SystemDialogDelegateView::SetDescription(
const std::u16string& description) {
description_->SetText(description);
description_->SetVisible(!description.empty());
}
void SystemDialogDelegateView::SetDescriptionAccessibleName(
const std::u16string& accessible_name) {
description_->GetViewAccessibility().SetName(accessible_name);
}
void SystemDialogDelegateView::SetAcceptButtonVisible(bool visible) {
button_container_->accept_button()->SetVisible(visible);
}
void SystemDialogDelegateView::SetCancelButtonVisible(bool visible) {
button_container_->cancel_button()->SetVisible(visible);
}
void SystemDialogDelegateView::SetAcceptButtonText(
const std::u16string& accept_text) {
button_container_->SetAcceptText(accept_text);
}
void SystemDialogDelegateView::SetCancelButtonText(
const std::u16string& cancel_text) {
button_container_->SetCancelText(cancel_text);
}
void SystemDialogDelegateView::SetButtonContainerAlignment(
views::LayoutAlignment alignment) {
button_container_->SetMainAxisAlignment(alignment);
}
void SystemDialogDelegateView::SetTopContentAlignment(
views::LayoutAlignment alignment) {
SetViewCrossAxisAlignment(contents_[ContentType::kTop], alignment);
}
void SystemDialogDelegateView::SetMiddleContentAlignment(
views::LayoutAlignment alignment) {
SetViewCrossAxisAlignment(contents_[ContentType::kMiddle], alignment);
}
void SystemDialogDelegateView::SetTitleMargins(const gfx::Insets& margins) {
SetViewLayoutSpecs(title_, margins);
}
gfx::Size SystemDialogDelegateView::CalculatePreferredSize(
const views::SizeBounds& available_size) const {
auto* host_window = GetDialogHostWindow(GetWidget());
// If the delegate view is not added to a widget or parented to a host window,
// return the default preferred size.
if (!host_window) {
return views::WidgetDelegateView::CalculatePreferredSize(available_size);
}
// Otherwise, calculate the preferred size according to its host window size.
const int host_width = host_window->GetBoundsInScreen().width();
// The resizing rules of the dialog are as follows:
// - When the host window width is larger than `kHostWidthLarge`, the dialog
// width would remain at `kDialogWidthLarge`.
// - When the host window width is between `kHostWidthMedium` and
// `kHostWidthLarge`, the dialog width will decrease but maintain a padding
// of `kDialogHostPaddingLarge` on both sides.
// - When the host window width is between `kHostWidthSmall` and
// `kHostWidthMedium`, the dialog width would remain at `kDialogWidthMedium`.
// - When the host window width is less than `kHostWidthXSmall`, the dialog
// width will decrease but maintain a padding of `kDialogHostPaddingSmall` on
// both sides.
// - The dialog minimum width is `kDialogWidthSmall`.
int dialog_width = kDialogWidthSmall;
if (host_width >= kHostWidthLarge) {
dialog_width = kDialogWidthLarge;
} else if (host_width >= kHostWidthMedium) {
dialog_width = host_width - kDialogHostPaddingLarge * 2;
} else if (host_width >= kHostWidthSmall) {
dialog_width = kDialogWidthMedium;
} else if (host_width >= kHostWidthXSmall) {
dialog_width = host_width - kDialogHostPaddingSmall * 2;
}
return gfx::Size(dialog_width, GetLayoutManager()->GetPreferredHeightForWidth(
this, dialog_width));
}
gfx::Size SystemDialogDelegateView::GetMinimumSize() const {
return kMinimumDialogSize;
}
gfx::Size SystemDialogDelegateView::GetMaximumSize() const {
return kMaximumDialogSize;
}
void SystemDialogDelegateView::OnWidgetInitialized() {
UpdateDialogSize();
}
void SystemDialogDelegateView::OnWorkAreaChanged() {
UpdateDialogSize();
}
const PillButton* SystemDialogDelegateView::GetAcceptButtonForTesting() const {
return button_container_->accept_button();
}
const PillButton* SystemDialogDelegateView::GetCancelButtonForTesting() const {
return button_container_->cancel_button();
}
void SystemDialogDelegateView::UpdateDialogSize() {
if (auto* widget = GetWidget()) {
widget->CenterWindow(GetPreferredSize());
}
}
size_t SystemDialogDelegateView::GetContentIndex(ContentType type) const {
switch (type) {
case ContentType::kTop:
return 0u;
case ContentType::kMiddle:
// The middle content is right after the description.
return GetIndexOf(description_).value() + 1u;
}
}
void SystemDialogDelegateView::SetContentInternal(
std::unique_ptr<views::View> view,
ContentType type) {
// If there is an existing content, remove it.
views::View* content = contents_[type];
if (content) {
contents_[type] = nullptr;
RemoveChildViewT(content);
}
// Add content and move it to the specific position.
content = AddChildViewAt(std::move(view), GetContentIndex(type));
// Set default bottom/top margins to the top/middle content if there is no
// preset margins or the corresponding margins are 0.
auto* margins = content->GetProperty(views::kMarginsKey);
switch (type) {
case ContentType::kTop:
if (!margins) {
content->SetProperty(
views::kMarginsKey,
gfx::Insets::TLBR(0, 0, kDefaultContentPadding, 0));
} else if (!margins->bottom()) {
margins->set_bottom(kDefaultContentPadding);
}
break;
case ContentType::kMiddle:
if (!margins) {
content->SetProperty(
views::kMarginsKey,
gfx::Insets::TLBR(kDefaultContentPadding, 0, 0, 0));
} else if (!margins->top()) {
margins->set_top(kDefaultContentPadding);
}
break;
}
content->SetProperty(views::kCrossAxisAlignmentKey,
views::LayoutAlignment::kCenter);
contents_[type] = content;
}
void SystemDialogDelegateView::SetAdditionalViewInButtonRowInternal(
std::unique_ptr<views::View> view) {
button_container_->SetAdditionalView(std::move(view));
}
void SystemDialogDelegateView::Accept() {
if (!closing_dialog_) {
RunCallbackAndCloseDialog(
std::move(accept_callback_),
views::Widget::ClosedReason::kAcceptButtonClicked);
}
}
void SystemDialogDelegateView::Cancel() {
if (!closing_dialog_) {
RunCallbackAndCloseDialog(
std::move(cancel_callback_),
views::Widget::ClosedReason::kCancelButtonClicked);
}
}
void SystemDialogDelegateView::Close() {
if (!closing_dialog_ && close_callback_) {
std::move(close_callback_).Run();
}
closing_dialog_ = true;
}
void SystemDialogDelegateView::RunCallbackAndCloseDialog(
base::OnceClosure callback,
views::Widget::ClosedReason closed_reason) {
CHECK(!closing_dialog_);
if (callback) {
std::move(callback).Run();
}
if (auto* widget = GetWidget()) {
// Update the `closing_dialog_` before closing the widget.
closing_dialog_ = true;
widget->CloseWithReason(closed_reason);
}
}
BEGIN_METADATA(SystemDialogDelegateView)
END_METADATA
} // namespace ash