#include "ui/views/window/dialog_client_view.h"
#include <algorithm>
#include <memory>
#include <utility>
#include <vector>
#include "base/auto_reset.h"
#include "base/check.h"
#include "base/memory/raw_ptr.h"
#include "build/build_config.h"
#include "ui/base/interaction/element_identifier.h"
#include "ui/base/metadata/metadata_header_macros.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/base/mojom/dialog_button.mojom.h"
#include "ui/base/ui_base_types.h"
#include "ui/color/color_id.h"
#include "ui/color/color_provider.h"
#include "ui/events/keycodes/keyboard_codes.h"
#include "ui/gfx/geometry/rounded_corners_f.h"
#include "ui/views/background.h"
#include "ui/views/border.h"
#include "ui/views/controls/button/button.h"
#include "ui/views/controls/button/checkbox.h"
#include "ui/views/controls/button/image_button.h"
#include "ui/views/controls/button/md_text_button.h"
#include "ui/views/layout/flex_layout.h"
#include "ui/views/layout/layout_provider.h"
#include "ui/views/layout/table_layout.h"
#include "ui/views/metadata/view_factory.h"
#include "ui/views/style/platform_style.h"
#include "ui/views/view_class_properties.h"
#include "ui/views/view_observer.h"
#include "ui/views/view_tracker.h"
#include "ui/views/view_utils.h"
#include "ui/views/widget/widget.h"
#include "ui/views/window/dialog_delegate.h"
namespace views {
namespace features {
BASE_FEATURE(kDialogVerticalButtonFallback, base::FEATURE_ENABLED_BY_DEFAULT);
}
namespace {
constexpr int kButtonGroup = 6666;
bool ShouldShow(View* view) {
return view && view->GetVisible();
}
gfx::Size GetBoundingSizeForVerticalStack(const gfx::Size& size1,
const gfx::Size& size2) {
return gfx::Size(std::max(size1.width(), size2.width()),
size1.height() + size2.height());
}
constexpr ui::ElementIdentifier kNoElementId;
ui::ElementIdentifier GetButtonId(ui::mojom::DialogButton type) {
switch (type) {
case ui::mojom::DialogButton::kOk:
return DialogClientView::kOkButtonElementId;
case ui::mojom::DialogButton::kCancel:
return DialogClientView::kCancelButtonElementId;
default:
return kNoElementId;
}
}
}
class DialogClientView::ButtonRowContainer : public View {
METADATA_HEADER(ButtonRowContainer, View)
public:
explicit ButtonRowContainer(DialogClientView* owner) : owner_(owner) {}
ButtonRowContainer(const ButtonRowContainer&) = delete;
ButtonRowContainer& operator=(const ButtonRowContainer&) = delete;
void ChildPreferredSizeChanged(View* child) override {
owner_->ChildPreferredSizeChanged(child);
}
void ChildVisibilityChanged(View* child) override {
owner_->OnButtonVisibilityChanged(child);
}
private:
const raw_ptr<DialogClientView> owner_;
};
BEGIN_METADATA(DialogClientView, ButtonRowContainer)
END_METADATA
DialogClientView::DialogClientView(Widget* owner, View* contents_view)
: ClientView(owner, contents_view),
button_row_insets_(
LayoutProvider::Get()->GetInsetsMetric(INSETS_DIALOG_BUTTON_ROW)),
input_protector_(
std::make_unique<views::InputEventActivationProtector>()) {
SetLayoutManager(std::make_unique<DelegatingLayoutManager>(this));
AddAccelerator(ui::Accelerator(ui::VKEY_ESCAPE, ui::EF_NONE));
button_row_container_ =
AddChildView(std::make_unique<ButtonRowContainer>(this));
SetProperty(views::kElementIdentifierKey, kTopViewId);
}
DialogClientView::~DialogClientView() {
DialogDelegate* dialog = GetWidget() ? GetDialogDelegate() : nullptr;
if (dialog) {
dialog->RemoveObserver(this);
}
}
void DialogClientView::SetButtonRowInsets(const gfx::Insets& insets) {
button_row_insets_ = insets;
if (GetWidget()) {
UpdateDialogButtons();
}
}
gfx::Size DialogClientView::CalculatePreferredSize(
const SizeBounds& available_size) const {
const gfx::Insets& content_margins = GetDialogDelegate()->margins();
gfx::Size contents_size;
const int fixed_width = GetDialogDelegate()->fixed_width();
if (fixed_width) {
const int content_width = fixed_width - content_margins.width();
contents_size = ClientView::CalculatePreferredSize(
views::SizeBounds(content_width, {}));
contents_size.set_width(content_width);
} else {
SizeBounds content_available_size(available_size);
content_available_size.Enlarge(-content_margins.width(),
-content_margins.height());
contents_size = ClientView::CalculatePreferredSize(content_available_size);
}
contents_size.Enlarge(content_margins.width(), content_margins.height());
return GetBoundingSizeForVerticalStack(
contents_size, button_row_container_->GetPreferredSize({}));
}
gfx::Size DialogClientView::GetMinimumSize() const {
if (GetDialogDelegate()->fixed_width()) {
return CalculatePreferredSize(
SizeBounds(GetDialogDelegate()->fixed_width(), {}));
}
return GetBoundingSizeForVerticalStack(
ClientView::GetMinimumSize(), button_row_container_->GetMinimumSize());
}
gfx::Size DialogClientView::GetMaximumSize() const {
constexpr int kUnconstrained = 0;
DCHECK(gfx::Size(kUnconstrained, kUnconstrained) ==
button_row_container_->GetMaximumSize());
gfx::Size max_size = ClientView::GetMaximumSize();
if (max_size.height() != kUnconstrained) {
max_size.Enlarge(0, button_row_container_->GetPreferredSize({}).height());
}
return max_size;
}
void DialogClientView::VisibilityChanged(View* starting_from, bool is_visible) {
ClientView::VisibilityChanged(starting_from, is_visible);
input_protector_->VisibilityChanged(is_visible);
}
#if BUILDFLAG(IS_CHROMEOS)
void DialogClientView::UpdateWindowRoundedCorners(
const gfx::RoundedCornersF& window_radii) {
DCHECK(GetWidget());
const gfx::RoundedCornersF background_radii(0, 0, window_radii.lower_right(),
window_radii.lower_left());
SetBackgroundRadii(background_radii);
}
#endif
ProposedLayout DialogClientView::CalculateProposedLayout(
const SizeBounds& size_bounds) const {
ProposedLayout layouts;
DCHECK(size_bounds.is_fully_bounded());
const int container_height =
button_row_container_->GetHeightForWidth(size_bounds.width().value());
const int container_y = size_bounds.height().value() - container_height;
#if defined(__clang__) && (__clang_major__ < 17)
layouts.child_layouts.emplace_back(ChildLayout{
button_row_container_.get(), button_row_container_->GetVisible(),
gfx::Rect(0, container_y, size_bounds.width().value(), container_height),
size_bounds});
#else
layouts.child_layouts.emplace_back(
button_row_container_.get(), button_row_container_->GetVisible(),
gfx::Rect(0, container_y, size_bounds.width().value(), container_height),
size_bounds);
#endif
if (contents_view()) {
gfx::Rect contents_bounds(size_bounds.width().value(), container_y);
contents_bounds.Inset(GetDialogDelegate()->margins());
#if defined(__clang__) && (__clang_major__ < 17)
layouts.child_layouts.emplace_back(ChildLayout{contents_view(),
contents_view()->GetVisible(),
contents_bounds, size_bounds});
#else
layouts.child_layouts.emplace_back(contents_view(),
contents_view()->GetVisible(),
contents_bounds, size_bounds);
#endif
}
layouts.host_size =
gfx::Size(size_bounds.width().value(), size_bounds.height().value());
return layouts;
}
void DialogClientView::SetBackgroundColor(ui::ColorId background_color_id) {
if (background_color_id_ == background_color_id) {
return;
}
background_color_id_ = background_color_id;
UpdateBackground();
}
bool DialogClientView::AcceleratorPressed(const ui::Accelerator& accelerator) {
DCHECK_EQ(accelerator.key_code(), ui::VKEY_ESCAPE);
DialogDelegate* const delegate = GetDialogDelegate();
if (delegate && delegate->EscShouldCancelDialog()) {
delegate->CancelDialog();
return true;
}
GetWidget()->CloseWithReason(Widget::ClosedReason::kEscKeyPressed);
return true;
}
void DialogClientView::ViewHierarchyChanged(
const ViewHierarchyChangedDetails& details) {
View* const child = details.child;
ClientView::ViewHierarchyChanged(details);
if (details.is_add) {
if (child == this) {
UpdateDialogButtons();
GetDialogDelegate()->AddObserver(this);
}
return;
}
if (details.parent != button_row_container_) {
return;
}
if (adding_or_removing_views_) {
return;
}
if (child == ok_button_) {
ok_button_ = nullptr;
} else if (child == cancel_button_) {
cancel_button_ = nullptr;
} else if (child == extra_view_) {
extra_view_ = nullptr;
}
}
void DialogClientView::OnThemeChanged() {
ClientView::OnThemeChanged();
UpdateBackground();
}
void DialogClientView::UpdateInputProtectorTimeStamp() {
input_protector_->MaybeUpdateViewProtectedTimeStamp();
}
void DialogClientView::ResetViewShownTimeStampForTesting() {
input_protector_->ResetForTesting();
}
bool DialogClientView::IsPossiblyUnintendedInteraction(const ui::Event& event,
bool allow_key_events) {
return input_protector_->IsPossiblyUnintendedInteraction(event,
allow_key_events);
}
DialogDelegate* DialogClientView::GetDialogDelegate() const {
return GetWidget()->widget_delegate()
? GetWidget()->widget_delegate()->AsDialogDelegate()
: nullptr;
}
void DialogClientView::SetBackgroundRadii(const gfx::RoundedCornersF& radii) {
if (background_radii_ == radii) {
return;
}
background_radii_ = radii;
UpdateBackground();
}
void DialogClientView::UpdateBackground() {
const DialogDelegate* dialog = GetDialogDelegate();
if (dialog && !dialog->use_custom_frame()) {
SetBackground(views::CreateRoundedRectBackground(
GetColorProvider()->GetColor(background_color_id_), background_radii_));
}
}
void DialogClientView::OnButtonVisibilityChanged(View* child) {
if (child == extra_view_) {
UpdateDialogButtons();
}
InvalidateLayout();
}
void DialogClientView::TriggerInputProtection(bool force_early) {
input_protector_->MaybeUpdateViewProtectedTimeStamp(force_early);
}
void DialogClientView::OnDialogChanged() {
UpdateDialogButtons();
}
void DialogClientView::UpdateDialogButtons() {
SetupLayout();
InvalidateLayout();
}
void DialogClientView::UpdateDialogButton(raw_ptr<MdTextButton>* member,
ui::mojom::DialogButton type) {
DialogDelegate* const delegate = GetDialogDelegate();
if (!(delegate->buttons() & static_cast<int>(type))) {
if (*member) {
button_row_container_->RemoveChildViewT(std::exchange(*member, nullptr));
}
return;
}
const bool is_default = delegate->GetIsDefault(type);
const std::u16string title = delegate->GetDialogButtonLabel(type);
const ui::ButtonStyle style = delegate->GetDialogButtonStyle(type);
if (*member) {
MdTextButton* button = *member;
button->SetEnabled(delegate->IsDialogButtonEnabled(type));
button->SetIsDefault(is_default);
button->SetText(title);
button->SetStyle(style);
return;
}
const int minimum_width = LayoutProvider::Get()->GetDistanceMetric(
views::DISTANCE_DIALOG_BUTTON_MINIMUM_WIDTH);
Builder<View>(button_row_container_)
.AddChild(
Builder<MdTextButton>()
.CopyAddressTo(member)
.SetCallback(base::BindRepeating(&DialogClientView::ButtonPressed,
base::Unretained(this), type))
.SetText(title)
.SetProperty(views::kElementIdentifierKey, GetButtonId(type))
.SetStyle(style)
.SetIsDefault(is_default)
.SetEnabled(delegate->IsDialogButtonEnabled(type))
.SetMinSize(gfx::Size(minimum_width, 0))
.SetGroup(kButtonGroup))
.BuildChildren();
}
void DialogClientView::ButtonPressed(ui::mojom::DialogButton type,
const ui::Event& event) {
DialogDelegate* const delegate = GetDialogDelegate();
if (!delegate ||
input_protector_->IsPossiblyUnintendedInteraction(
event, delegate
->ShouldAllowKeyEventsDuringInputProtection())) {
return;
}
DCHECK(type == ui::mojom::DialogButton::kOk ||
type == ui::mojom::DialogButton::kCancel);
if (type == ui::mojom::DialogButton::kOk &&
!delegate->ShouldIgnoreButtonPressedEventHandling(ok_button_, event)) {
delegate->AcceptDialog();
}
if (type == ui::mojom::DialogButton::kCancel &&
!delegate->ShouldIgnoreButtonPressedEventHandling(cancel_button_,
event)) {
delegate->CancelDialog();
}
}
int DialogClientView::GetExtraViewSpacing() const {
if (!ShouldShow(extra_view_) || !(ok_button_ || cancel_button_)) {
return 0;
}
return LayoutProvider::Get()->GetDistanceMetric(
views::DISTANCE_RELATED_BUTTON_HORIZONTAL);
}
std::array<View*, DialogClientView::kNumButtons>
DialogClientView::GetButtonRowViews() {
View* first = ShouldShow(extra_view_) ? extra_view_.get() : nullptr;
View* second = cancel_button_;
View* third = ok_button_;
if (cancel_button_ && (PlatformStyle::kIsOkButtonLeading == !!ok_button_)) {
std::swap(second, third);
}
return {{first, second, third}};
}
std::vector<View*> DialogClientView::GetButtonColumnViews() const {
std::vector<View*> views;
if (ShouldShow(extra_view_)) {
views.push_back(extra_view_.get());
}
if (cancel_button_) {
views.push_back(cancel_button_);
}
if (ok_button_) {
views.push_back(ok_button_);
}
return views;
}
void DialogClientView::UpdateExtraViewFromDelegate() {
auto maybe_new_extra_view = GetDialogDelegate()->DisownExtraView();
if (!maybe_new_extra_view.has_value()) {
return;
}
if (extra_view_) {
View* old_extra_view = extra_view_.ExtractAsDangling();
CHECK_EQ(old_extra_view->parent(), button_row_container_.get());
button_row_container_->RemoveChildViewT(old_extra_view);
}
auto new_extra_view = std::move(maybe_new_extra_view.value());
if (!new_extra_view) {
return;
}
extra_view_ =
button_row_container_->AddChildViewAt(std::move(new_extra_view), 0);
if (IsViewClass<Button>(extra_view_)) {
extra_view_->SetGroup(kButtonGroup);
}
}
void DialogClientView::SetupLayout() {
base::AutoReset<bool> auto_reset(&adding_or_removing_views_, true);
FocusManager* focus_manager = GetFocusManager();
ViewTracker view_tracker(focus_manager->GetFocusedView());
button_row_container_->SetLayoutManager(nullptr);
UpdateButtonsFromModel();
UpdateExtraViewFromDelegate();
std::array<View*, kNumButtons> views = GetButtonRowViews();
if (std::ranges::count(views, nullptr) == kNumButtons) {
return;
}
if (extra_view_) {
extra_view_->SetProperty(kViewIgnoredByLayoutKey,
!extra_view_->GetVisible());
}
SetupHorizontalLayout();
const int fixed_width = GetDialogDelegate()->fixed_width();
if (GetDialogDelegate()->allow_vertical_buttons() &&
base::FeatureList::IsEnabled(
views::features::kDialogVerticalButtonFallback) &&
ShouldShow(extra_view_) && fixed_width &&
button_row_container_->GetPreferredSize({}).width() > fixed_width) {
SetupVerticalLayout();
}
View* previously_focused_view = view_tracker.view();
if (previously_focused_view && !focus_manager->GetFocusedView() &&
Contains(previously_focused_view)) {
previously_focused_view->RequestFocus();
}
}
void DialogClientView::SetupHorizontalLayout() {
std::array<View*, kNumButtons> views = GetButtonRowViews();
CHECK(std::ranges::count(views, nullptr) != kNumButtons);
auto* layout = button_row_container_->SetLayoutManager(
std::make_unique<views::TableLayout>());
layout->SetMinimumSize(minimum_size_);
constexpr float kFixed = views::TableLayout::kFixedSize;
constexpr float kStretchy = 1.0f;
LayoutProvider* const layout_provider = LayoutProvider::Get();
const int button_spacing = (ok_button_ && cancel_button_)
? layout_provider->GetDistanceMetric(
DISTANCE_RELATED_BUTTON_HORIZONTAL)
: 0;
layout->SetMinimumSize(minimum_size_)
.AddPaddingColumn(kFixed, button_row_insets_.left())
.AddColumn(LayoutAlignment::kStretch, LayoutAlignment::kStretch, kFixed,
TableLayout::ColumnSize::kUsePreferred, 0, 0)
.AddPaddingColumn(kStretchy, GetExtraViewSpacing())
.AddColumn(LayoutAlignment::kStretch, LayoutAlignment::kEnd, kFixed,
TableLayout::ColumnSize::kUsePreferred, 0, 0)
.AddPaddingColumn(kFixed, button_spacing)
.AddColumn(LayoutAlignment::kStretch, LayoutAlignment::kEnd, kFixed,
TableLayout::ColumnSize::kUsePreferred, 0, 0)
.AddPaddingColumn(kFixed, button_row_insets_.right())
.AddPaddingRow(kFixed, button_row_insets_.top())
.AddRows(1, kFixed)
.AddPaddingRow(kFixed, button_row_insets_.bottom())
.SetLinkedColumnSizeLimit(layout_provider->GetDistanceMetric(
DISTANCE_BUTTON_MAX_LINKABLE_WIDTH));
auto should_link = [](views::View* view) {
return IsViewClass<Button>(view) && !IsViewClass<Checkbox>(view) &&
!IsViewClass<ImageButton>(view);
};
for (size_t i = 0; i < kNumButtons; ++i) {
if (views[i]) {
RemoveFillerView(i);
button_row_container_->ReorderChildView(views[i], i);
} else {
AddFillerView(i);
}
}
{
std::vector<size_t> cols;
for (size_t i = 0; i < kNumButtons; ++i) {
if (should_link(views[i])) {
cols.push_back(i * 2 + 1);
}
}
layout->LinkColumnSizes(cols);
}
}
void DialogClientView::SetupVerticalLayout() {
auto views = GetButtonColumnViews();
CHECK_GT(views.size(), 0u);
for (size_t i = 0; i < views.size(); ++i) {
button_row_container_->ReorderChildView(views[i], i);
}
button_row_container_->SetLayoutManager(std::make_unique<views::FlexLayout>())
->SetOrientation(views::LayoutOrientation::kVertical)
.SetMainAxisAlignment(views::LayoutAlignment::kCenter)
.SetCrossAxisAlignment(views::LayoutAlignment::kStretch)
.SetInteriorMargin(button_row_insets_)
.SetCollapseMargins(true)
.SetDefault(kMarginsKey,
gfx::Insets::VH(LayoutProvider::Get()->GetDistanceMetric(
DISTANCE_RELATED_CONTROL_VERTICAL),
0));
}
void DialogClientView::UpdateButtonsFromModel() {
if constexpr (PlatformStyle::kIsOkButtonLeading) {
UpdateDialogButton(&ok_button_, ui::mojom::DialogButton::kOk);
UpdateDialogButton(&cancel_button_, ui::mojom::DialogButton::kCancel);
} else {
UpdateDialogButton(&cancel_button_, ui::mojom::DialogButton::kCancel);
UpdateDialogButton(&ok_button_, ui::mojom::DialogButton::kOk);
}
}
void DialogClientView::AddFillerView(size_t view_index) {
DCHECK_LT(view_index, kNumButtons);
View*& filler = filler_views_[view_index];
if (!filler) {
filler = button_row_container_->AddChildViewAt(
std::make_unique<View>(),
std::min(button_row_container_->children().size(), view_index));
}
}
void DialogClientView::RemoveFillerView(size_t view_index) {
DCHECK_LT(view_index, kNumButtons);
View*& filler = filler_views_[view_index];
if (filler) {
button_row_container_->RemoveChildViewT(filler);
filler = nullptr;
}
}
BEGIN_METADATA(DialogClientView)
END_METADATA
DEFINE_CLASS_ELEMENT_IDENTIFIER_VALUE(DialogClientView, kTopViewId);
DEFINE_CLASS_ELEMENT_IDENTIFIER_VALUE(DialogClientView, kOkButtonElementId);
DEFINE_CLASS_ELEMENT_IDENTIFIER_VALUE(DialogClientView, kCancelButtonElementId);
}