910e62b5创建于 1月15日历史提交
// Copyright 2017 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/examples/dialog_example.h"

#include <memory>
#include <string>
#include <string_view>
#include <utility>

#include "base/memory/raw_ptr.h"
#include "base/strings/strcat.h"
#include "base/strings/utf_string_conversions.h"
#include "build/build_config.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/mojom/dialog_button.mojom.h"
#include "ui/base/mojom/ui_base_types.mojom-shared.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/bubble/bubble_dialog_delegate_view.h"
#include "ui/views/controls/button/checkbox.h"
#include "ui/views/controls/button/label_button.h"
#include "ui/views/controls/button/md_text_button.h"
#include "ui/views/controls/combobox/combobox.h"
#include "ui/views/controls/label.h"
#include "ui/views/controls/textfield/textfield.h"
#include "ui/views/examples/examples_window.h"
#include "ui/views/examples/grit/views_examples_resources.h"
#include "ui/views/layout/fill_layout.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/widget/widget.h"
#include "ui/views/window/dialog_delegate.h"

namespace views::examples {
namespace {

constexpr size_t kFakeModeless =
    static_cast<size_t>(ui::mojom::ModalType::kSystem) + 1;

}  // namespace

template <class DialogType>
class DialogExampleDelegate : public DialogType {
 public:
  template <typename... Args>
  explicit DialogExampleDelegate(DialogExample* parent, Args&&... args)
      : DialogType(std::forward<Args>(args)...), parent_(parent) {
    DialogDelegate::SetButtons(parent_->GetDialogButtons());
    DialogDelegate::SetButtonLabel(ui::mojom::DialogButton::kOk,
                                   parent_->ok_button_text());
    DialogDelegate::SetButtonLabel(ui::mojom::DialogButton::kCancel,
                                   parent_->cancel_button_text());
    DialogDelegate::SetCloseCallback(
        base::BindRepeating([](DialogExample* parent) { parent->OnCancel(); },
                            base::Unretained(parent_)));
    WidgetDelegate::SetModalType(parent_->GetModalType());
  }

  DialogExampleDelegate(const DialogExampleDelegate&) = delete;
  DialogExampleDelegate& operator=(const DialogExampleDelegate&) = delete;

  void InitDelegate() {
    this->SetLayoutManager(std::make_unique<FillLayout>());
    auto body = std::make_unique<Label>(parent_->body_text());
    body->SetMultiLine(true);
    body->SetHorizontalAlignment(gfx::ALIGN_LEFT);
    // Give the example code a way to change the body text.
    parent_->set_last_body_label(this->AddChildView(std::move(body)));

    if (parent_->has_extra_button_checked()) {
      DialogDelegate::SetExtraView(std::make_unique<views::MdTextButton>(
          Button::PressedCallback(), parent_->extra_button_text()));
    }
  }

 protected:
  std::u16string GetWindowTitle() const override {
    return std::u16string(parent_->title_text());
  }

  bool Cancel() override { return parent_->OnCancel(); }
  bool Accept() override { return parent_->OnAccept(); }

 private:
  raw_ptr<DialogExample> parent_;
};

class DialogExampleBubble
    : public DialogExampleDelegate<BubbleDialogDelegateView> {
 public:
  DialogExampleBubble(DialogExample* parent, View* anchor)
      : DialogExampleDelegate(parent, anchor, BubbleBorder::TOP_LEFT) {
    set_close_on_deactivate(!parent->persistent_bubble_checked());
  }

  DialogExampleBubble(const DialogExampleBubble&) = delete;
  DialogExampleBubble& operator=(const DialogExampleBubble&) = delete;

  // BubbleDialogDelegateView:
  void Init() override { InitDelegate(); }
};

class DialogExampleDialog : public DialogExampleDelegate<DialogDelegateView> {
 public:
  explicit DialogExampleDialog(DialogExample* parent)
      : DialogExampleDelegate(parent) {
    // Mac supports resizing of modal dialogs (parent or window-modal). On other
    // platforms this will be weird unless the modal type is "none", but helps
    // test layout.
    SetCanResize(true);
  }

  DialogExampleDialog(const DialogExampleDialog&) = delete;
  DialogExampleDialog& operator=(const DialogExampleDialog&) = delete;
};

DialogExample::DialogExample()
    : ExampleBase("Dialog"),
      mode_model_({
          ui::SimpleComboboxModel::Item(u"Modeless"),
          ui::SimpleComboboxModel::Item(u"Window Modal"),
          ui::SimpleComboboxModel::Item(u"Child Modal"),
          ui::SimpleComboboxModel::Item(u"System Modal"),
          ui::SimpleComboboxModel::Item(u"Fake Modeless (non-bubbles)"),
      }) {}

DialogExample::~DialogExample() {
  if (title_) {
    title_->set_controller(nullptr);
  }
  if (body_) {
    body_->set_controller(nullptr);
  }
  if (ok_button_label_) {
    ok_button_label_->set_controller(nullptr);
  }
  if (cancel_button_label_) {
    cancel_button_label_->set_controller(nullptr);
  }
  if (extra_button_label_) {
    extra_button_label_->set_controller(nullptr);
  }
}

void DialogExample::CreateExampleView(View* container) {
  auto* flex_layout =
      container->SetLayoutManager(std::make_unique<FlexLayout>());
  flex_layout->SetOrientation(LayoutOrientation::kVertical);

  auto* table = container->AddChildView(std::make_unique<View>());
  views::LayoutProvider* provider = views::LayoutProvider::Get();
  const int horizontal_spacing =
      provider->GetDistanceMetric(views::DISTANCE_RELATED_BUTTON_HORIZONTAL);
  auto* table_layout = table->SetLayoutManager(std::make_unique<TableLayout>());
  table_layout
      ->AddColumn(LayoutAlignment::kStart, LayoutAlignment::kStretch,
                  TableLayout::kFixedSize,
                  TableLayout::ColumnSize::kUsePreferred, 0, 0)
      .AddPaddingColumn(TableLayout::kFixedSize, horizontal_spacing)
      .AddColumn(LayoutAlignment::kStretch, LayoutAlignment::kStretch, 1.0f,
                 TableLayout::ColumnSize::kUsePreferred, 0, 0)
      .AddPaddingColumn(TableLayout::kFixedSize, horizontal_spacing)
      .AddColumn(LayoutAlignment::kStretch, LayoutAlignment::kStretch,
                 TableLayout::kFixedSize,
                 TableLayout::ColumnSize::kUsePreferred, 0, 0);
  const int vertical_padding =
      provider->GetDistanceMetric(views::DISTANCE_RELATED_CONTROL_VERTICAL);
  for (int i = 0; i < 7; ++i) {
    table_layout->AddPaddingRow(TableLayout::kFixedSize, vertical_padding)
        .AddRows(1, TableLayout::kFixedSize);
  }

  StartTextfieldRow(
      table, &title_, l10n_util::GetStringUTF16(IDS_DIALOG_TITLE_LABEL),
      l10n_util::GetStringUTF16(IDS_DIALOG_TITLE_TEXT), nullptr, true);
  StartTextfieldRow(
      table, &body_, l10n_util::GetStringUTF16(IDS_DIALOG_BODY_LABEL),
      l10n_util::GetStringUTF16(IDS_DIALOG_BODY_LABEL), nullptr, true);

  Label* row_label = nullptr;
  StartTextfieldRow(table, &ok_button_label_,
                    l10n_util::GetStringUTF16(IDS_DIALOG_OK_BUTTON_LABEL),
                    l10n_util::GetStringUTF16(IDS_DIALOG_OK_BUTTON_TEXT),
                    &row_label, false);
  AddCheckbox(table, &has_ok_button_, row_label);

  StartTextfieldRow(table, &cancel_button_label_,
                    l10n_util::GetStringUTF16(IDS_DIALOG_CANCEL_BUTTON_LABEL),
                    l10n_util::GetStringUTF16(IDS_DIALOG_CANCEL_BUTTON_TEXT),
                    &row_label, false);
  AddCheckbox(table, &has_cancel_button_, row_label);

  StartTextfieldRow(table, &extra_button_label_,
                    l10n_util::GetStringUTF16(IDS_DIALOG_EXTRA_BUTTON_LABEL),
                    l10n_util::GetStringUTF16(IDS_DIALOG_EXTRA_BUTTON_TEXT),
                    &row_label, false);
  AddCheckbox(table, &has_extra_button_, row_label);

  std::u16string modal_label =
      l10n_util::GetStringUTF16(IDS_DIALOG_MODAL_TYPE_LABEL);
  table->AddChildView(std::make_unique<Label>(modal_label));
  mode_ = table->AddChildView(std::make_unique<Combobox>(&mode_model_));
  mode_->SetCallback(base::BindRepeating(&DialogExample::OnPerformAction,
                                         base::Unretained(this)));
  mode_->SetSelectedIndex(static_cast<size_t>(ui::mojom::ModalType::kChild));
  mode_->GetViewAccessibility().SetName(modal_label);
  table->AddChildView(std::make_unique<View>());

  Label* bubble_label = table->AddChildView(std::make_unique<Label>(
      l10n_util::GetStringUTF16(IDS_DIALOG_BUBBLE_LABEL)));
  AddCheckbox(table, &bubble_, bubble_label);
  AddCheckbox(table, &persistent_bubble_, nullptr);
  persistent_bubble_->SetText(
      l10n_util::GetStringUTF16(IDS_DIALOG_PERSISTENT_LABEL));

  show_ = container->AddChildView(std::make_unique<views::MdTextButton>(
      base::BindRepeating(&DialogExample::ShowButtonPressed,
                          base::Unretained(this)),
      l10n_util::GetStringUTF16(IDS_DIALOG_SHOW_BUTTON_LABEL)));
  show_->SetProperty(kCrossAxisAlignmentKey, LayoutAlignment::kCenter);
  show_->SetProperty(
      kMarginsKey,
      gfx::Insets::TLBR(provider->GetDistanceMetric(
                            views::DISTANCE_UNRELATED_CONTROL_VERTICAL),
                        0, 0, 0));
}

ui::mojom::ModalType DialogExample::GetModalType() const {
  // "Fake" modeless happens when a DialogDelegate specifies window-modal, but
  // doesn't provide a parent window.
  // TODO(ellyjones): This doesn't work on Mac at all - something should happen
  // other than changing modality on the fly like this. In fact, it should be
  // impossible to change modality in a live dialog at all, and this example
  // should stop doing it.
  if (mode_->GetSelectedIndex() == kFakeModeless) {
    return ui::mojom::ModalType::kWindow;
  }

  return static_cast<ui::mojom::ModalType>(mode_->GetSelectedIndex().value());
}

int DialogExample::GetDialogButtons() const {
  int buttons = 0;
  if (has_ok_button_->GetChecked()) {
    buttons |= static_cast<int>(ui::mojom::DialogButton::kOk);
  }
  if (has_cancel_button_->GetChecked()) {
    buttons |= static_cast<int>(ui::mojom::DialogButton::kCancel);
  }
  return buttons;
}

bool DialogExample::OnCancel() {
  return AllowDialogClose(false);
}

bool DialogExample::OnAccept() {
  return AllowDialogClose(true);
}

void DialogExample::StartTextfieldRow(View* parent,
                                      raw_ptr<Textfield>* member,
                                      std::u16string label,
                                      std::u16string value,
                                      Label** created_label,
                                      bool pad_last_col) {
  Label* row_label = parent->AddChildView(std::make_unique<Label>(label));
  if (created_label) {
    *created_label = row_label;
  }
  auto textfield = std::make_unique<Textfield>();
  textfield->set_controller(this);
  textfield->SetText(value);
  textfield->GetViewAccessibility().SetName(*row_label);
  *member = parent->AddChildView(std::move(textfield));
  if (pad_last_col) {
    parent->AddChildView(std::make_unique<View>());
  }
}

void DialogExample::AddCheckbox(View* parent,
                                raw_ptr<Checkbox>* member,
                                Label* label) {
  auto callback = member == &bubble_ ? &DialogExample::BubbleCheckboxPressed
                                     : &DialogExample::OtherCheckboxPressed;
  auto checkbox = std::make_unique<Checkbox>(
      std::u16string(), base::BindRepeating(callback, base::Unretained(this)));
  checkbox->SetChecked(true);
  if (label) {
    checkbox->GetViewAccessibility().SetName(*label);
  }
  *member = parent->AddChildView(std::move(checkbox));
}

bool DialogExample::AllowDialogClose(bool accept) {
  PrintStatus(
      base::StrCat({"Dialog closed with ", accept ? "Accept." : "Cancel."}));
  last_dialog_ = nullptr;
  last_body_label_ = nullptr;
  return true;
}

void DialogExample::ResizeDialog() {
  DCHECK(last_dialog_);
  Widget* widget = last_dialog_->GetWidget();
  gfx::Rect preferred_bounds(widget->GetRestoredBounds());
  preferred_bounds.set_size(widget->non_client_view()->GetPreferredSize({}));

  // Q: Do we need FrameView::GetWindowBoundsForClientBounds() here?
  // A: When DialogCientView properly feeds back sizes, we do not.
  widget->SetBoundsConstrained(preferred_bounds);

  // For user-resizable dialogs, ensure the window manager enforces any new
  // minimum size.
  widget->OnSizeConstraintsChanged();
}

void DialogExample::ShowButtonPressed() {
  if (bubble_->GetChecked()) {
    // |bubble| will be destroyed by its widget when the widget is destroyed.
    auto* bubble = new DialogExampleBubble(this, show_);
    last_dialog_ = bubble;
    BubbleDialogDelegateView::CreateBubble(bubble);
  } else {
    // |dialog| will be destroyed by its widget when the widget is destroyed.
    auto* dialog = new DialogExampleDialog(this);
    last_dialog_ = dialog;
    dialog->InitDelegate();

    // constrained_window::CreateBrowserModalDialogViews() allows dialogs to
    // be created as MODAL_TYPE_WINDOW without specifying a parent.
    gfx::NativeView parent = gfx::NativeView();
    if (mode_->GetSelectedIndex() != kFakeModeless) {
      parent = example_view()->GetWidget()->GetNativeView();
    }

    DialogDelegate::CreateDialogWidget(
        dialog, example_view()->GetWidget()->GetNativeWindow(), parent);
  }
  last_dialog_->GetWidget()->Show();
}

void DialogExample::BubbleCheckboxPressed() {
  if (bubble_->GetChecked() && GetModalType() != ui::mojom::ModalType::kChild) {
    mode_->SetSelectedIndex(static_cast<size_t>(ui::mojom::ModalType::kChild));
    PrintStatus("You nearly always want Child Modal for bubbles.");
  }
  persistent_bubble_->SetEnabled(bubble_->GetChecked());
  OnPerformAction();  // Validate the modal type.

  if (!bubble_->GetChecked() &&
      GetModalType() == ui::mojom::ModalType::kChild) {
    // Do something reasonable when simply unchecking bubble and re-enable.
    mode_->SetSelectedIndex(static_cast<size_t>(ui::mojom::ModalType::kWindow));
    OnPerformAction();
  }
}

void DialogExample::OtherCheckboxPressed() {
  // Buttons other than show and bubble are pressed. They are all checkboxes.
  // Update the dialog if there is one.
  if (last_dialog_) {
    // TODO(crbug.com/40799020): This can segfault.
    last_dialog_->DialogModelChanged();
    ResizeDialog();
  }
}

void DialogExample::ContentsChanged(Textfield* sender,
                                    const std::u16string& new_contents) {
  if (!last_dialog_) {
    return;
  }

  if (sender == extra_button_label_) {
    PrintStatus("DialogDelegate can never refresh the extra view.");
  }

  if (sender == title_) {
    last_dialog_->GetWidget()->UpdateWindowTitle();
  } else if (sender == body_) {
    last_body_label_->SetText(new_contents);
  } else {
    last_dialog_->DialogModelChanged();
  }

  ResizeDialog();
}

void DialogExample::OnPerformAction() {
  bool enable =
      bubble_->GetChecked() || GetModalType() != ui::mojom::ModalType::kChild;
#if BUILDFLAG(IS_MAC)
  enable = enable && GetModalType() != ui::mojom::ModalType::kSystem;
#endif
  show_->SetEnabled(enable);
  if (!enable && GetModalType() == ui::mojom::ModalType::kChild) {
    PrintStatus("MODAL_TYPE_CHILD can't be used with non-bubbles.");
  }
  if (!enable && GetModalType() == ui::mojom::ModalType::kSystem) {
    PrintStatus("MODAL_TYPE_SYSTEM isn't supported on Mac.");
  }
}

}  // namespace views::examples