blob: 213a14fc50fc69e1d40de2b7aa19a507428e552e [file] [log] [blame]
// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/ui/tabs/tab_group_deletion_dialog_controller.h"
#include <string>
#include "base/check_deref.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/i18n/message_formatter.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/signin/identity_manager_factory.h"
#include "chrome/browser/tab_group_sync/tab_group_sync_service_factory.h"
#include "chrome/browser/ui/browser_dialogs.h"
#include "chrome/browser/ui/browser_element_identifiers.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#include "chrome/browser/ui/tabs/saved_tab_groups/saved_tab_group_pref_names.h"
#include "chrome/browser/ui/tabs/saved_tab_groups/saved_tab_group_utils.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/browser/ui/tabs/tab_strip_model_observer.h"
#include "chrome/common/pref_names.h"
#include "chrome/grit/generated_resources.h"
#include "components/prefs/pref_service.h"
#include "components/saved_tab_groups/public/types.h"
#include "components/signin/public/base/consent_level.h"
#include "components/signin/public/identity_manager/identity_manager.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/models/dialog_model.h"
namespace tab_groups {
namespace {
// The text that shows on the checkbox.
constexpr int kDontAskId = IDS_TAB_GROUP_DELETION_DIALOG_DONT_ASK;
// Body text for all delete actions.
constexpr int kDeleteBodySyncedId =
IDS_TAB_GROUP_DELETION_DIALOG_BODY_SYNCED_DELETE;
constexpr int kDeleteBodyNotSyncedId =
IDS_TAB_GROUP_DELETION_DIALOG_BODY_NOT_SYNCED_DELETE;
// For deletion, the text that shows on the dialog.
constexpr int kDeleteTitleId = IDS_TAB_GROUP_DELETION_DIALOG_TITLE_DELETE;
constexpr int kDeleteOkTextId = IDS_TAB_GROUP_DELETION_DIALOG_OK_TEXT_DELETE;
// For ungrouping, the text that shows on the dialog.
constexpr int kUngroupTitleId = IDS_TAB_GROUP_DELETION_DIALOG_TITLE_UNGROUP;
constexpr int kUngroupBodySyncedId =
IDS_TAB_GROUP_DELETION_DIALOG_BODY_SYNCED_UNGROUP;
constexpr int kUngroupBodyNotSyncedId =
IDS_TAB_GROUP_DELETION_DIALOG_BODY_NOT_SYNCED_UNGROUP;
constexpr int kUngroupOkTextId = IDS_TAB_GROUP_DELETION_DIALOG_OK_TEXT_UNGROUP;
// For closing the last tab, the text that shows on the dialog.
constexpr int kCloseTabAndDeleteTitleId =
IDS_TAB_GROUP_DELETION_DIALOG_TITLE_CLOSE_TAB_AND_DELETE;
// For removing the last tab, the text that shows on the dialog.
constexpr int kRemoveTabAndDeleteTitleId =
IDS_TAB_GROUP_DELETION_DIALOG_TITLE_REMOVE_TAB_AND_DELETE;
struct DialogText {
const std::u16string title;
const std::u16string body;
const std::u16string ok_text;
const std::optional<std::u16string> cancel_text = std::nullopt;
};
// Returns the list of strings that are needed for a given dialog type.
DialogText GetDialogText(
Profile* profile,
const DeletionDialogController::DialogMetadata& dialog_metadata) {
tab_groups::TabGroupSyncService* tab_group_service =
tab_groups::TabGroupSyncServiceFactory::GetForProfile(profile);
const bool is_sync_enabled =
tab_group_service &&
tab_groups::SavedTabGroupUtils::AreSavedTabGroupsSyncedForProfile(
profile);
std::u16string email =
l10n_util::GetStringUTF16(IDS_TAB_GROUP_DELETION_DIALOG_MISSING_EMAIL);
signin::IdentityManager* identity_manager =
IdentityManagerFactory::GetForProfile(profile);
if (profile && identity_manager) {
email = base::UTF8ToUTF16(
identity_manager->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin)
.email);
}
const int closing_group_count = dialog_metadata.closing_group_count;
const int closing_multiple_groups = closing_group_count > 1;
const int plural_type_count =
dialog_metadata.closing_multiple_tabs + closing_multiple_groups;
switch (dialog_metadata.type) {
case DeletionDialogController::DialogType::DeleteSingle: {
return DialogText{
base::i18n::MessageFormatter::FormatWithNumberedArgs(
l10n_util::GetStringUTF16(kDeleteTitleId), closing_group_count),
base::i18n::MessageFormatter::FormatWithNumberedArgs(
is_sync_enabled
? l10n_util::GetStringFUTF16(kDeleteBodySyncedId, email)
: l10n_util::GetStringUTF16(kDeleteBodyNotSyncedId),
closing_group_count),
base::i18n::MessageFormatter::FormatWithNumberedArgs(
l10n_util::GetStringUTF16(kDeleteOkTextId), closing_group_count)};
}
case DeletionDialogController::DialogType::UngroupSingle: {
return DialogText{
base::i18n::MessageFormatter::FormatWithNumberedArgs(
l10n_util::GetStringUTF16(kUngroupTitleId), closing_group_count),
base::i18n::MessageFormatter::FormatWithNumberedArgs(
is_sync_enabled
? l10n_util::GetStringFUTF16(kUngroupBodySyncedId, email)
: l10n_util::GetStringUTF16(kUngroupBodyNotSyncedId),
closing_group_count),
l10n_util::GetStringUTF16(kUngroupOkTextId)};
}
case DeletionDialogController::DialogType::RemoveTabAndDelete: {
return DialogText{
base::i18n::MessageFormatter::FormatWithNumberedArgs(
l10n_util::GetStringUTF16(kRemoveTabAndDeleteTitleId),
plural_type_count),
base::i18n::MessageFormatter::FormatWithNumberedArgs(
is_sync_enabled
? l10n_util::GetStringFUTF16(kDeleteBodySyncedId, email)
: l10n_util::GetStringUTF16(kDeleteBodyNotSyncedId),
closing_group_count),
base::i18n::MessageFormatter::FormatWithNumberedArgs(
l10n_util::GetStringUTF16(kDeleteOkTextId), closing_group_count)};
}
case DeletionDialogController::DialogType::CloseTabAndDelete: {
return DialogText{
base::i18n::MessageFormatter::FormatWithNumberedArgs(
l10n_util::GetStringUTF16(kCloseTabAndDeleteTitleId),
plural_type_count),
base::i18n::MessageFormatter::FormatWithNumberedArgs(
is_sync_enabled
? l10n_util::GetStringFUTF16(kDeleteBodySyncedId, email)
: l10n_util::GetStringUTF16(kDeleteBodyNotSyncedId),
closing_group_count),
base::i18n::MessageFormatter::FormatWithNumberedArgs(
l10n_util::GetStringUTF16(kDeleteOkTextId), closing_group_count)};
}
case DeletionDialogController::DialogType::DeleteSingleShared: {
const bool title_is_empty =
!dialog_metadata.title_of_closing_group.has_value() ||
dialog_metadata.title_of_closing_group->empty();
std::u16string body_text =
title_is_empty
? l10n_util::GetStringUTF16(
IDS_DATA_SHARING_OWNER_DELETE_DIALOG_BODY_NO_GROUP_TITLE)
: l10n_util::GetStringFUTF16(
IDS_DATA_SHARING_OWNER_DELETE_DIALOG_BODY,
dialog_metadata.title_of_closing_group.value());
return DialogText{
l10n_util::GetStringUTF16(IDS_DATA_SHARING_OWNER_DELETE_DIALOG_TITLE),
std::move(body_text),
l10n_util::GetStringUTF16(
IDS_DATA_SHARING_OWNER_DELETE_DIALOG_CONFIRM)};
}
case DeletionDialogController::DialogType::CloseTabAndKeepOrLeaveGroup: {
return DialogText{
l10n_util::GetPluralStringFUTF16(
IDS_DATA_SHARING_DELETE_LAST_TAB_TITLE,
dialog_metadata.closing_group_count),
l10n_util::GetPluralStringFUTF16(
IDS_DATA_SHARING_MEMBER_DELETE_LAST_TAB_BODY,
dialog_metadata.closing_group_count),
l10n_util::GetPluralStringFUTF16(
IDS_DATA_SHARING_DELETE_LAST_TAB_CONFIRM,
dialog_metadata.closing_group_count),
l10n_util::GetPluralStringFUTF16(
IDS_DATA_SHARING_MEMBER_DELETE_LAST_TAB_CANCEL,
dialog_metadata.closing_group_count),
};
}
case DeletionDialogController::DialogType::CloseTabAndKeepOrDeleteGroup: {
return DialogText{
l10n_util::GetPluralStringFUTF16(
IDS_DATA_SHARING_DELETE_LAST_TAB_TITLE,
dialog_metadata.closing_group_count),
l10n_util::GetPluralStringFUTF16(
IDS_DATA_SHARING_OWNER_DELETE_LAST_TAB_BODY,
dialog_metadata.closing_group_count),
l10n_util::GetPluralStringFUTF16(
IDS_DATA_SHARING_DELETE_LAST_TAB_CONFIRM,
dialog_metadata.closing_group_count),
l10n_util::GetPluralStringFUTF16(
IDS_DATA_SHARING_OWNER_DELETE_LAST_TAB_CANCEL,
dialog_metadata.closing_group_count),
};
}
case DeletionDialogController::DialogType::LeaveGroup: {
const bool title_is_empty =
!dialog_metadata.title_of_closing_group.has_value() ||
dialog_metadata.title_of_closing_group->empty();
std::u16string body_text =
title_is_empty
? l10n_util::GetStringUTF16(
IDS_DATA_SHARING_LEAVE_DIALOG_BODY_NO_GROUP_TITLE)
: l10n_util::GetStringFUTF16(
IDS_DATA_SHARING_LEAVE_DIALOG_BODY,
dialog_metadata.title_of_closing_group.value());
return DialogText{
l10n_util::GetStringUTF16(IDS_DATA_SHARING_LEAVE_DIALOG_TITLE),
std::move(body_text),
l10n_util::GetStringUTF16(IDS_DATA_SHARING_LEAVE_DIALOG_CONFIRM)};
}
}
}
bool IsDialogSkippable(DeletionDialogController::DialogType type) {
switch (type) {
// Saved tab group dialogs are skippable.
case DeletionDialogController::DialogType::DeleteSingle:
case DeletionDialogController::DialogType::UngroupSingle:
case DeletionDialogController::DialogType::RemoveTabAndDelete:
case DeletionDialogController::DialogType::CloseTabAndDelete: {
return true;
}
// Shared tab group dialogs aren't skippable.
case DeletionDialogController::DialogType::DeleteSingleShared:
case DeletionDialogController::DialogType::CloseTabAndKeepOrLeaveGroup:
case DeletionDialogController::DialogType::CloseTabAndKeepOrDeleteGroup:
case DeletionDialogController::DialogType::LeaveGroup: {
return false;
}
}
}
// Returns the the value from the settings pref for a given dialog type.
bool IsDialogSkippedByUserSettings(Profile* profile,
DeletionDialogController::DialogType type) {
if (!profile) {
return false;
}
PrefService* pref_service = profile->GetPrefs();
switch (type) {
case DeletionDialogController::DialogType::DeleteSingle: {
return pref_service->GetBoolean(
saved_tab_groups::prefs::kTabGroupsDeletionSkipDialogOnDelete);
}
case DeletionDialogController::DialogType::UngroupSingle: {
return pref_service->GetBoolean(
saved_tab_groups::prefs::kTabGroupsDeletionSkipDialogOnUngroup);
}
case DeletionDialogController::DialogType::RemoveTabAndDelete: {
return pref_service->GetBoolean(
saved_tab_groups::prefs::kTabGroupsDeletionSkipDialogOnRemoveTab);
}
case DeletionDialogController::DialogType::CloseTabAndDelete: {
return pref_service->GetBoolean(
saved_tab_groups::prefs::kTabGroupsDeletionSkipDialogOnCloseTab);
}
// Shared tab groups dialogs aren't skippable.
case DeletionDialogController::DialogType::DeleteSingleShared:
case DeletionDialogController::DialogType::CloseTabAndKeepOrLeaveGroup:
case DeletionDialogController::DialogType::CloseTabAndKeepOrDeleteGroup:
case DeletionDialogController::DialogType::LeaveGroup: {
return false;
}
}
}
void SetSkipDialogForType(Profile* profile,
DeletionDialogController::DialogType type,
bool new_value) {
if (!profile) {
return;
}
PrefService* pref_service = profile->GetPrefs();
switch (type) {
case DeletionDialogController::DialogType::DeleteSingle: {
return pref_service->SetBoolean(
saved_tab_groups::prefs::kTabGroupsDeletionSkipDialogOnDelete,
new_value);
}
case DeletionDialogController::DialogType::UngroupSingle: {
return pref_service->SetBoolean(
saved_tab_groups::prefs::kTabGroupsDeletionSkipDialogOnUngroup,
new_value);
}
case DeletionDialogController::DialogType::RemoveTabAndDelete: {
return pref_service->SetBoolean(
saved_tab_groups::prefs::kTabGroupsDeletionSkipDialogOnRemoveTab,
new_value);
}
case DeletionDialogController::DialogType::CloseTabAndDelete: {
return pref_service->SetBoolean(
saved_tab_groups::prefs::kTabGroupsDeletionSkipDialogOnCloseTab,
new_value);
}
// Shared tab group dialogs aren't skippable.
case DeletionDialogController::DialogType::DeleteSingleShared:
case DeletionDialogController::DialogType::CloseTabAndKeepOrLeaveGroup:
case DeletionDialogController::DialogType::CloseTabAndKeepOrDeleteGroup:
case DeletionDialogController::DialogType::LeaveGroup: {
// We should never try to set the skip pref for these dialog types.
NOTREACHED();
}
}
}
// Keep type dialogs don't let the user cancel their action; instead, they
// choose whether the group should stick around or go away.
bool IsDialogKeepType(DeletionDialogController::DialogType type) {
return type == DeletionDialogController::DialogType::
CloseTabAndKeepOrDeleteGroup ||
type ==
DeletionDialogController::DialogType::CloseTabAndKeepOrLeaveGroup;
}
} // anonymous namespace
// DialogMetadata
DeletionDialogController::DialogMetadata::DialogMetadata(
DialogType type,
int closing_group_count,
bool closing_multiple_tabs)
: type(type),
closing_group_count(closing_group_count),
closing_multiple_tabs(closing_multiple_tabs) {}
DeletionDialogController::DialogMetadata::DialogMetadata(DialogType type)
: type(type) {}
DeletionDialogController::DialogMetadata::~DialogMetadata() = default;
// DialogState
DeletionDialogController::DialogState::DialogState(
DialogType type_,
ui::DialogModel* dialog_model_,
base::OnceCallback<void(DeletionDialogTiming)> callback_,
std::optional<base::OnceCallback<void()>> keep_groups_)
: type(type_),
dialog_model(dialog_model_),
callback(std::move(callback_)),
keep_groups(std::move(keep_groups_)) {}
DeletionDialogController::DialogState::~DialogState() = default;
DeletionDialogController::DeletionDialogController(
BrowserWindowInterface* browser,
Profile* profile,
TabStripModel* tab_strip_model)
: profile_(CHECK_DEREF(profile)),
show_dialog_model_fn_(base::BindRepeating(
&DeletionDialogController::CreateDialogFromBrowser,
base::Unretained(this),
browser)),
tab_strip_model_(CHECK_DEREF(tab_strip_model)) {
tab_strip_model_->AddObserver(this);
}
DeletionDialogController::DeletionDialogController(
BrowserWindowInterface* browser,
Profile* profile,
TabStripModel* tab_strip_model,
ShowDialogModelCallback show_dialog_model_fn)
: profile_(CHECK_DEREF(profile)),
show_dialog_model_fn_(show_dialog_model_fn),
tab_strip_model_(CHECK_DEREF(tab_strip_model)) {}
DeletionDialogController::~DeletionDialogController() = default;
bool DeletionDialogController::CanShowDialog() const {
return !IsShowingDialog();
}
bool DeletionDialogController::IsShowingDialog() const {
return !!state_;
}
void DeletionDialogController::CreateDialogFromBrowser(
BrowserWindowInterface* browser,
std::unique_ptr<ui::DialogModel> dialog_model) {
widget_ = chrome::ShowBrowserModal(browser->GetBrowserForMigrationOnly(),
std::move(dialog_model));
}
bool DeletionDialogController::MaybeShowDialog(
const DialogMetadata& metadata,
base::OnceCallback<void(DeletionDialogTiming)> callback,
std::optional<base::OnceCallback<void()>> keep_groups) {
if (IsDialogKeepType(metadata.type)) {
CHECK(keep_groups.has_value());
} else {
CHECK(!keep_groups.has_value());
}
if (!CanShowDialog()) {
return false;
}
if (IsDialogSkippedByUserSettings(GetProfile(), metadata.type)) {
std::move(callback).Run(DeletionDialogTiming::Synchronous);
return false;
}
std::unique_ptr<ui::DialogModel> dialog_model = BuildDialogModel(metadata);
state_.emplace(metadata.type, dialog_model.get(), std::move(callback),
std::move(keep_groups));
show_dialog_model_fn_.Run(std::move(dialog_model));
return true;
}
void DeletionDialogController::OnTabStripModelChanged(
TabStripModel* tab_strip_model,
const TabStripModelChange& change,
const TabStripSelectionChange& selection) {
if (widget_) {
widget_->Close();
}
}
void DeletionDialogController::SetPrefsPreventShowingDialogForTesting(
bool should_prevent_dialog) {
auto* prefs = profile_->GetPrefs();
prefs->SetBoolean(
saved_tab_groups::prefs::kTabGroupsDeletionSkipDialogOnDelete,
should_prevent_dialog);
prefs->SetBoolean(
saved_tab_groups::prefs::kTabGroupsDeletionSkipDialogOnUngroup,
should_prevent_dialog);
prefs->SetBoolean(
saved_tab_groups::prefs::kTabGroupsDeletionSkipDialogOnRemoveTab,
should_prevent_dialog);
prefs->SetBoolean(
saved_tab_groups::prefs::kTabGroupsDeletionSkipDialogOnCloseTab,
should_prevent_dialog);
}
void DeletionDialogController::OnDialogOk() {
if (state_->dialog_model &&
state_->dialog_model->HasField(kDeletionDialogDontAskCheckboxId) &&
state_->dialog_model
->GetCheckboxByUniqueId(kDeletionDialogDontAskCheckboxId)
->is_checked()) {
SetSkipDialogForType(GetProfile(), state_->type, true);
}
if (IsDialogKeepType(state_->type)) {
std::move(state_->keep_groups.value()).Run();
}
std::move(state_->callback).Run(DeletionDialogTiming::Asynchronous);
state_.reset();
}
void DeletionDialogController::OnDialogCancel() {
if (IsDialogKeepType(state_->type)) {
std::move(state_->callback).Run(DeletionDialogTiming::Asynchronous);
}
state_.reset();
}
std::unique_ptr<ui::DialogModel> DeletionDialogController::BuildDialogModel(
const DialogMetadata& metadata) {
DialogText strings = GetDialogText(GetProfile(), metadata);
ui::DialogModel::Button::Params cancel_button_params;
cancel_button_params.SetEnabled(true).SetId(kDeletionDialogCancelButtonId);
if (strings.cancel_text.has_value()) {
cancel_button_params.SetLabel(strings.cancel_text.value());
}
ui::DialogModel::Builder dialog_builder = ui::DialogModel::Builder();
dialog_builder.SetTitle(strings.title)
.AddParagraph(ui::DialogModelLabel(strings.body))
.AddCancelButton(base::BindOnce(&DeletionDialogController::OnDialogCancel,
base::Unretained(this)),
cancel_button_params)
.AddOkButton(base::BindOnce(&DeletionDialogController::OnDialogOk,
base::Unretained(this)),
ui::DialogModel::Button::Params()
.SetLabel(strings.ok_text)
.SetEnabled(true)
.SetId(kDeletionDialogOkButtonId))
.SetCloseActionCallback(base::BindOnce(
[](DeletionDialogController* dialog_controller) {
dialog_controller->state_.reset();
},
base::Unretained(this)))
.SetDialogDestroyingCallback(base::BindOnce(
[](DeletionDialogController* dialog_controller) {
dialog_controller->widget_ = nullptr;
dialog_controller->state_.reset();
},
base::Unretained(this)));
if (IsDialogSkippable(metadata.type)) {
dialog_builder.AddCheckbox(
kDeletionDialogDontAskCheckboxId,
ui::DialogModelLabel(l10n_util::GetStringUTF16(kDontAskId)));
}
return dialog_builder.Build();
}
Profile* DeletionDialogController::GetProfile() {
return &profile_.get();
}
} // namespace tab_groups