blob: 4e5d2d8afec7e4de1fa6eaccaaea484d36807b01 [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/toasts/toast_controller.h"
#include <optional>
#include <string>
#include <utility>
#include <vector>
#include "base/check.h"
#include "base/check_is_test.h"
#include "base/check_op.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/location.h"
#include "base/time/time.h"
#include "base/timer/timer.h"
#include "chrome/browser/ui/browser_window/public/browser_window_features.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#include "chrome/browser/ui/exclusive_access/exclusive_access_manager.h"
#include "chrome/browser/ui/omnibox/omnibox_tab_helper.h"
#include "chrome/browser/ui/tabs/public/tab_interface.h"
#include "chrome/browser/ui/toasts/api/toast_id.h"
#include "chrome/browser/ui/toasts/api/toast_registry.h"
#include "chrome/browser/ui/toasts/api/toast_specification.h"
#include "chrome/browser/ui/toasts/toast_features.h"
#include "chrome/browser/ui/toasts/toast_view.h"
#include "components/omnibox/common/omnibox_focus_state.h"
#include "content/public/browser/web_contents.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/views/bubble/bubble_dialog_delegate_view.h"
#include "ui/views/focus/focus_manager.h"
#include "ui/views/view.h"
#include "ui/views/widget/widget.h"
ToastParams::ToastParams(ToastId id) : toast_id_(id) {}
ToastParams::ToastParams(ToastParams&& other) noexcept = default;
ToastParams& ToastParams::operator=(ToastParams&& other) noexcept = default;
ToastParams::~ToastParams() = default;
ToastController::ToastController(
BrowserWindowInterface* browser_window_interface,
const ToastRegistry* toast_registry)
: browser_window_interface_(browser_window_interface),
toast_registry_(toast_registry) {
}
ToastController::~ToastController() = default;
void ToastController::Init() {
CHECK(browser_window_interface_);
CHECK(browser_subscriptions_.empty());
browser_subscriptions_.push_back(
browser_window_interface_->RegisterActiveTabDidChange(base::BindRepeating(
&ToastController::OnActiveTabChanged, base::Unretained(this))));
}
bool ToastController::IsShowingToast() const {
return GetCurrentToastId().has_value();
}
bool ToastController::CanShowToast(ToastId id) const {
if (!base::FeatureList::IsEnabled(toast_features::kToastFramework)) {
return false;
}
if (!IsShowingToast()) {
return true;
}
const ToastSpecification* potential_toast_spec =
toast_registry_->GetToastSpecification(id);
return !(persistent_params_.has_value() &&
potential_toast_spec->is_persistent_toast());
}
std::optional<ToastId> ToastController::GetCurrentToastId() const {
return currently_showing_toast_id_;
}
bool ToastController::MaybeShowToast(ToastParams params) {
if (!CanShowToast(params.toast_id_)) {
return false;
}
CloseToast(toasts::ToastCloseReason::kPreempted);
if (IsShowingToast()) {
QueueToast(std::move(params));
} else {
ShowToast(std::move(params));
}
return true;
}
void ToastController::ClosePersistentToast(ToastId id) {
CHECK(persistent_params_.has_value());
CHECK_EQ(persistent_params_.value().toast_id_, id);
std::optional<ToastId> current_toast_id = GetCurrentToastId();
persistent_params_ = std::nullopt;
// Close the toast if we are currently showing a persistent toast.
if (current_toast_id.has_value() &&
toast_registry_->GetToastSpecification(current_toast_id.value())
->is_persistent_toast()) {
CloseToast(toasts::ToastCloseReason::kFeatureDismiss);
}
}
#if BUILDFLAG(IS_MAC)
void ToastController::OnWidgetActivationChanged(views::Widget* widget,
bool active) {
if (active) {
// Clears the stored focus view so that after widget activation occurs,
// focus will not advance out of the widget and into the ContentsWebView.
toast_widget_->GetFocusManager()->SetStoredFocusView(nullptr);
} else {
// On Mac, traversing out of the toast widget and into the browser causes
// the browser to advance focus twice so we clear the focus to achieve the
// expected focus behavior.
browser_window_interface_->TopContainer()
->GetWidget()
->GetFocusManager()
->ClearFocus();
}
}
#endif
void ToastController::OnWidgetDestroyed(views::Widget* widget) {
current_ephemeral_params_ = std::nullopt;
currently_showing_toast_id_ = std::nullopt;
toast_view_ = nullptr;
toast_widget_ = nullptr;
toast_observer_.Reset();
fullscreen_observation_.Reset();
toast_close_timer_.Stop();
if (browser_window_interface_ &&
browser_window_interface_->IsAttemptingToCloseBrowser()) {
// Clear any queued toasts to prevent them from showing
// after an existing toast is destroyed while the browser is trying to
// close.
next_ephemeral_params_ = std::nullopt;
persistent_params_ = std::nullopt;
omnibox_helper_observer_.Reset();
}
if (next_ephemeral_params_.has_value()) {
ShowToast(std::move(next_ephemeral_params_.value()));
next_ephemeral_params_ = std::nullopt;
} else if (persistent_params_.has_value()) {
ShowToast(std::move(persistent_params_.value()));
}
}
void ToastController::PrimaryPageChanged(content::Page& page) {
ClearTabScopedToasts();
}
base::OneShotTimer* ToastController::GetToastCloseTimerForTesting() {
return &toast_close_timer_;
}
void ToastController::OnActiveTabChanged(
BrowserWindowInterface* browser_interface) {
tabs::TabInterface* const tab_interface =
browser_interface->GetActiveTabInterface();
content::WebContents* const web_contents = tab_interface->GetContents();
OmniboxTabHelper* const tab_helper =
OmniboxTabHelper::FromWebContents(web_contents);
CHECK(tab_helper);
omnibox_helper_observer_.Reset();
omnibox_helper_observer_.Observe(tab_helper);
Observe(web_contents);
ClearTabScopedToasts();
}
void ToastController::QueueToast(ToastParams params) {
if (next_ephemeral_params_.has_value()) {
// TODO(crbug.com/358610190): Record that next_ephemeral_params_ was
// preempted.
next_ephemeral_params_ = std::nullopt;
} else if (persistent_params_.has_value()) {
// TODO(crbug.com/358610190): Record that persistent_params_ was
// preempted.
} else {
// Since we are queuing a toast, current_ephemeral_params must have a value
// if we do not already have another ephemeral toast queued up.
CHECK(current_ephemeral_params_.has_value());
// TODO(crbug.com/358610190): Record that current_ephemeral_params was
// preempted.
}
if (toast_registry_->GetToastSpecification(params.toast_id_)
->is_persistent_toast()) {
CHECK(!persistent_params_.has_value());
persistent_params_ = std::move(params);
} else {
next_ephemeral_params_ = std::move(params);
}
}
void ToastController::OnOmniboxInputInProgress(bool in_progress) {
if (in_progress) {
UpdateToastWidgetVisibility(false);
}
}
void ToastController::OnOmniboxFocusChanged(OmniboxFocusState state,
OmniboxFocusChangeReason reason) {
UpdateToastWidgetVisibility(state == OmniboxFocusState::OMNIBOX_FOCUS_NONE);
}
void ToastController::OnOmniboxPopupVisibilityChanged(bool popup_is_open) {
is_omnibox_popup_showing_ = popup_is_open;
UpdateToastWidgetVisibility(!popup_is_open);
}
void ToastController::UpdateToastWidgetVisibility(bool show_toast_widget) {
if (toast_widget_) {
if (show_toast_widget) {
toast_widget_->ShowInactive();
} else {
toast_widget_->Hide();
}
}
}
void ToastController::WebContentsDestroyed() {
omnibox_helper_observer_.Reset();
Observe(nullptr);
}
void ToastController::ShowToast(ToastParams params) {
// TODO(crbug.com/367755347): Remove check when test is fixed.
CHECK(!toast_registry_->IsEmpty());
const ToastSpecification* current_toast_spec =
toast_registry_->GetToastSpecification(params.toast_id_);
CHECK(current_toast_spec);
currently_showing_toast_id_ = params.toast_id_;
if (current_toast_spec->is_persistent_toast()) {
persistent_params_ = std::move(params);
} else {
current_ephemeral_params_ = std::move(params);
base::TimeDelta timeout =
current_toast_spec->action_button_string_id().has_value()
? toast_features::kToastTimeout.Get()
: toast_features::kToastWithoutActionTimeout.Get();
toast_close_timer_.Start(
FROM_HERE, timeout,
base::BindOnce(&ToastController::CloseToast, base::Unretained(this),
toasts::ToastCloseReason::kAutoDismissed));
}
CreateToast(current_toast_spec->is_persistent_toast()
? persistent_params_.value()
: current_ephemeral_params_.value(),
current_toast_spec);
}
void ToastController::CloseToast(toasts::ToastCloseReason reason) {
if (toast_view_) {
toast_view_->Close(reason);
}
}
void ToastController::CreateToast(const ToastParams& params,
const ToastSpecification* spec) {
views::View* anchor_view = browser_window_interface_->TopContainer();
CHECK(anchor_view);
auto toast_view = std::make_unique<toasts::ToastView>(
anchor_view,
FormatString(spec->body_string_id(),
params.body_string_replacement_params_),
spec->icon(), spec->has_close_button(),
browser_window_interface_->ShouldHideUIForFullscreen());
if (spec->action_button_string_id().has_value()) {
toast_view->AddActionButton(
FormatString(spec->action_button_string_id().value(),
params.action_button_string_replacement_params_),
spec->action_button_callback());
}
toast_view_ = toast_view.get();
toast_widget_ =
views::BubbleDialogDelegateView::CreateBubble(std::move(toast_view));
// Get rid of the border that is drawn by default when we set the toast to
// have a shadow.
toast_view_->GetBubbleFrameView()->bubble_border()->set_draw_border_stroke(
false);
toast_observer_.Observe(toast_widget_);
fullscreen_observation_.Observe(
browser_window_interface_->GetExclusiveAccessManager()
->fullscreen_controller());
toast_widget_->SetVisibilityChangedAnimationsEnabled(false);
// Set the the focus traversable parent of the toast widget to be the parent
// of the anchor view, so that when focus leaves the toast, the search for the
// next focusable view will start from the right place. However, does not set
// the anchor view's focus traversable to be the toast widget, because when
// focus leaves the toast widget it will go into the anchor view's focus
// traversable if it exists, so doing that would trap focus inside of the
// toast widget.
toast_widget_->SetFocusTraversableParent(
anchor_view->parent()->GetWidget()->GetFocusTraversable());
toast_widget_->SetFocusTraversableParentView(anchor_view);
if (!is_omnibox_popup_showing_) {
toast_widget_->ShowInactive();
toast_view_->AnimateIn();
} else {
toast_widget_->Hide();
}
}
std::u16string ToastController::FormatString(
int string_id,
std::vector<std::u16string> replacements) {
return l10n_util::GetStringFUTF16(string_id, replacements, nullptr);
}
void ToastController::OnFullscreenStateChanged() {
toast_view_->UpdateRenderToastOverWebContentsAndPaint(
browser_window_interface_->ShouldHideUIForFullscreen());
}
void ToastController::ClearTabScopedToasts() {
toast_close_timer_.Stop();
if (next_ephemeral_params_.has_value()) {
const ToastSpecification* const specification =
toast_registry_->GetToastSpecification(
next_ephemeral_params_.value().toast_id_);
if (!specification->is_global_scope()) {
next_ephemeral_params_ = std::nullopt;
}
}
if (current_ephemeral_params_.has_value()) {
const ToastSpecification* const specification =
toast_registry_->GetToastSpecification(
current_ephemeral_params_.value().toast_id_);
if (!specification->is_global_scope()) {
CloseToast(toasts::ToastCloseReason::kAbort);
}
}
}