blob: 32fed42b46992d3895501f219a578173290b47c3 [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/public/tab_dialog_manager.h"
#include <algorithm>
#include <memory>
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/memory/ptr_util.h"
#include "base/scoped_observation.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#include "chrome/browser/ui/browser_window/public/desktop_browser_window_capabilities.h"
#include "chrome/browser/ui/tabs/public/tab_features.h"
#include "chrome/browser/ui/ui_features.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
#include "components/back_forward_cache/back_forward_cache_disable.h"
#include "components/tabs/public/tab_interface.h"
#include "components/web_modal/modal_dialog_host.h"
#include "components/web_modal/web_contents_modal_dialog_host.h"
#include "content/public/browser/back_forward_cache.h"
#include "content/public/browser/navigation_handle.h"
#include "net/base/registry_controlled_domains/registry_controlled_domain.h"
#include "ui/base/base_window.h"
#include "ui/display/display.h"
#include "ui/display/screen.h"
#include "ui/gfx/animation/animation.h"
#include "ui/gfx/animation/linear_animation.h"
#include "ui/gfx/geometry/point.h"
#include "ui/gfx/native_ui_types.h"
#include "ui/views/widget/native_widget.h"
#include "ui/views/widget/widget.h"
#include "ui/views/widget/widget_delegate.h"
#include "ui/views/widget/widget_observer.h"
#include "ui/views/window/dialog_delegate.h"
#if BUILDFLAG(IS_OZONE)
#include "ui/ozone/public/ozone_platform.h"
#endif
namespace constrained_window {
extern const void* kConstrainedWindowWidgetIdentifier;
} // namespace constrained_window
namespace tabs {
class TabDialogWidgetObserver : public views::WidgetObserver {
public:
TabDialogWidgetObserver(TabDialogManager* tab_dialog_manager,
views::Widget* widget);
TabDialogWidgetObserver(const TabDialogWidgetObserver&) = delete;
TabDialogWidgetObserver& operator=(const TabDialogWidgetObserver&) = delete;
~TabDialogWidgetObserver() override = default;
private:
// Overridden from WidgetObserver:
void OnWidgetDestroyed(views::Widget* widget) override;
raw_ptr<TabDialogManager> tab_dialog_manager_ = nullptr;
base::ScopedObservation<views::Widget, views::WidgetObserver>
tab_dialog_scoped_observation_{this};
};
TabDialogWidgetObserver::TabDialogWidgetObserver(
TabDialogManager* tab_dialog_manager,
views::Widget* widget)
: tab_dialog_manager_(tab_dialog_manager) {
tab_dialog_scoped_observation_.Observe(widget);
}
void TabDialogWidgetObserver::OnWidgetDestroyed(views::Widget* widget) {
tab_dialog_scoped_observation_.Reset();
tab_dialog_manager_->WidgetDestroyed(widget);
}
namespace {
bool SupportsGlobalScreenCoordinates() {
#if !BUILDFLAG(IS_OZONE)
return true;
#else
return ui::OzonePlatform::GetInstance()
->GetPlatformProperties()
.supports_global_screen_coordinates;
#endif
}
bool PlatformClipsChildrenToViewport() {
#if BUILDFLAG(IS_LINUX)
return true;
#else
return false;
#endif
}
gfx::Rect GetModalDialogBounds(views::Widget* widget,
TabInterface* tab_interface,
const gfx::Size& size) {
BrowserWindowInterface* const host_browser_window =
tab_interface->GetBrowserWindowInterface();
gfx::Point position =
host_browser_window->GetWebContentsModalDialogHostForTab(tab_interface)
->GetDialogPosition(size);
if (widget->non_client_view()) {
// Align the first row of pixels inside the border. This is the apparent top
// of the dialog.
position.set_y(position.y() -
widget->non_client_view()->frame_view()->GetInsets().top());
}
gfx::Rect dialog_bounds(position, size);
if (widget->is_top_level() && SupportsGlobalScreenCoordinates()) {
views::Widget* const host_widget =
host_browser_window->TopContainer()->GetWidget();
gfx::Rect dialog_screen_bounds =
dialog_bounds +
host_widget->GetClientAreaBoundsInScreen().OffsetFromOrigin();
const gfx::Rect host_screen_bounds = host_widget->GetWindowBoundsInScreen();
// The requested dialog bounds should never fall outside the bounds of the
// transient parent.
DCHECK(dialog_screen_bounds.Intersects(host_screen_bounds));
// Adjust the dialog bound to ensure it remains visible on the display.
const gfx::Rect display_work_area =
host_widget->GetNearestDisplay().value().work_area();
if (!display_work_area.Contains(dialog_screen_bounds)) {
dialog_screen_bounds.AdjustToFit(display_work_area);
}
// For platforms that clip transient children to the viewport we must
// maximize its bounds on the display whilst keeping it within the host
// bounds to avoid viewport clipping.
// In the case that the host window bounds do not have sufficient overlap
// with the display, and the dialog cannot be shown in its entirety, this is
// a recoverable state as users are still able to reposition the host window
// back onto the display.
if (PlatformClipsChildrenToViewport() &&
!host_screen_bounds.Contains(dialog_screen_bounds)) {
dialog_screen_bounds.AdjustToFit(host_screen_bounds);
}
// Readjust the position of the dialog.
dialog_bounds.set_origin(dialog_screen_bounds.origin());
}
return dialog_bounds;
}
void ConfigureDesiredBoundsDelegate(views::Widget* widget,
TabInterface* tab_interface) {
views::WidgetDelegate* delegate = widget->widget_delegate();
// This callback is invoked in two cases:
// 1. by auto-resizing (Widget::is_autosized()) widgets when the layout is
// invalidated.
// 2. by BubbleDialogDelegate::SizeToContents().
delegate->set_desired_bounds_delegate(base::BindRepeating(
[](views::Widget* widget, TabInterface* tab_interface) -> gfx::Rect {
return GetModalDialogBounds(
widget, tab_interface, widget->GetRootView()->GetPreferredSize({}));
},
widget, tab_interface));
}
// The dialog widget should be visible if and only if the tab is in the
// foreground and activated, the host window is not minimized and the client
// also indicates visibility.
bool GetWidgetVisibility(
bool activated,
bool minimized,
TabDialogManager::ShouldShowCallback& should_show_callback) {
bool should_show = true;
if (activated && !minimized && should_show_callback) {
should_show_callback.Run(should_show);
}
return activated && !minimized && should_show;
}
} // namespace
// Applies positioning changes from the browser window widget to the tracked
// Widget. This class relies on the assumption that it is scoped to the lifetime
// of a single tab, in a single browser, and that it will be destroyed
// before the tab moves between browser windows.
class TabDialogManager::BrowserWindowWidgetObserver
: public views::WidgetObserver {
public:
BrowserWindowWidgetObserver(TabDialogManager* tab_dialog_manager,
TabInterface* tab_interface,
views::Widget* dialog_widget)
: tab_dialog_manager_(tab_dialog_manager),
tab_(tab_interface),
dialog_widget_(dialog_widget) {
CHECK(dialog_widget_);
browser_window_widget_observation_.Observe(
tab_dialog_manager_->GetHostWidget());
}
BrowserWindowWidgetObserver(const BrowserWindowWidgetObserver&) = delete;
BrowserWindowWidgetObserver& operator=(const BrowserWindowWidgetObserver&) =
delete;
~BrowserWindowWidgetObserver() override = default;
// WidgetObserver:
void OnWidgetBoundsChanged(views::Widget* widget,
const gfx::Rect& new_bounds) override {
if (dialog_widget_->IsVisible()) {
tab_dialog_manager_->UpdateModalDialogBounds();
}
}
void OnWidgetShowStateChanged(views::Widget* widget) override {
bool minimized = widget->IsMinimized();
bool activated = tab_->IsActivated();
auto* tab_dialog_manager = tab_->GetTabFeatures()->tab_dialog_manager();
dialog_widget_->SetVisible(
GetWidgetVisibility(activated, minimized,
tab_dialog_manager->params_->should_show_callback));
}
private:
const raw_ptr<TabDialogManager> tab_dialog_manager_;
// The tab that owns this dialog manager.
raw_ptr<TabInterface> tab_;
// The widget being tracked.
raw_ptr<views::Widget> dialog_widget_;
base::ScopedObservation<views::Widget, views::WidgetObserver>
browser_window_widget_observation_{this};
};
TabDialogManager::Params::Params() = default;
TabDialogManager::Params::~Params() = default;
TabDialogManager::TabDialogManager(TabInterface* tab_interface)
: content::WebContentsObserver(tab_interface->GetContents()),
tab_interface_(tab_interface) {
tab_subscriptions_.push_back(
tab_interface_->RegisterDidBecomeVisible(base::BindRepeating(
&TabDialogManager::TabDidEnterForeground, base::Unretained(this))));
tab_subscriptions_.push_back(
tab_interface_->RegisterWillBecomeHidden(base::BindRepeating(
&TabDialogManager::TabWillEnterBackground, base::Unretained(this))));
tab_subscriptions_.push_back(
tab_interface_->RegisterWillDetach(base::BindRepeating(
&TabDialogManager::TabWillDetach, base::Unretained(this))));
}
TabDialogManager::~TabDialogManager() = default;
std::unique_ptr<views::Widget> TabDialogManager::CreateTabScopedDialog(
views::DialogDelegate* delegate) {
DCHECK_EQ(ui::mojom::ModalType::kChild, delegate->GetModalType());
views::Widget* host = GetHostWidget();
CHECK(host);
if (base::FeatureList::IsEnabled(features::kTabModalUsesDesktopWidget)) {
delegate->set_use_desktop_widget_override(true);
}
return base::WrapUnique(views::DialogDelegate::CreateDialogWidget(
delegate, gfx::NativeWindow(), host->GetNativeView()));
}
void TabDialogManager::ShowDialog(views::Widget* widget,
std::unique_ptr<Params> params) {
// An autosized widget handles its own bounds, while `animated` bounds changes
// are handled by `TabDialogManager` and not the widget. They are not
// compatible.
// TODO(crbug.com/427759111): allow animated autosizing widgets.
CHECK(!(params->animated && widget->is_autosized()))
<< "Animated widgets are not compatible with autosized.";
if (params_ && !params_->block_new_modal && widget_) {
CloseDialog();
}
widget_ = widget;
params_ = std::move(params);
if (!params_->get_dialog_bounds) {
ConfigureDesiredBoundsDelegate(widget_.get(), tab_interface_);
}
UpdateModalDialogBounds();
widget_->SetNativeWindowProperty(
views::kWidgetIdentifierKey,
const_cast<void*>(
constrained_window::kConstrainedWindowWidgetIdentifier));
if (params_->disable_input) {
scoped_ignore_input_events_ =
tab_interface_->GetContents()->IgnoreInputEvents(std::nullopt);
tab_interface_->GetBrowserWindowInterface()
->capabilities()
->SetWebContentsBlocked(tab_interface_->GetContents(),
/*blocked=*/true);
}
tab_dialog_widget_observer_ =
std::make_unique<TabDialogWidgetObserver>(this, widget_.get());
if (params_->block_new_modal) {
showing_modal_ui_ = tab_interface_->ShowModalUI();
}
browser_window_widget_observer_ =
std::make_unique<BrowserWindowWidgetObserver>(this, tab_interface_,
widget_.get());
if (params_->should_show_inactive) {
widget_->ShowInactive();
} else {
widget_->Show();
}
UpdateDialogVisibility();
}
std::unique_ptr<views::Widget> TabDialogManager::CreateAndShowDialog(
views::DialogDelegate* delegate,
std::unique_ptr<Params> params) {
auto widget = CreateTabScopedDialog(delegate);
ShowDialog(widget.get(), std::move(params));
return widget;
}
void TabDialogManager::CloseDialog() {
if (widget_) {
views::Widget* widget = widget_;
// First reset all state tracked by this class.
WidgetDestroyed(widget_);
// Now destroy the Widget. We don't know whether destruction will be
// synchronous or asynchronous, but we no longer hold any state at this
// point so it doesn't matter.
widget->Close();
}
}
bool TabDialogManager::MaybeActivateDialog() {
// Also test whether the widget is in the closed state and in the middle of
// being torn down (Widget::CloseNow() or Widget::Close() called)
if (!widget_ || widget_->IsClosed()) {
return false;
}
if (UpdateDialogVisibility()) {
widget_->Activate();
return true;
}
return false;
}
void TabDialogManager::WidgetDestroyed(views::Widget* widget) {
CHECK_EQ(widget, widget_.get());
widget_ = nullptr;
params_.reset();
showing_modal_ui_.reset();
tab_dialog_widget_observer_.reset();
scoped_ignore_input_events_.reset();
browser_window_widget_observer_.reset();
bounds_animation_.reset();
tab_interface_->GetBrowserWindowInterface()
->capabilities()
->SetWebContentsBlocked(tab_interface_->GetContents(), /*blocked=*/false);
}
views::Widget* TabDialogManager::GetHostWidget() const {
return tab_interface_->GetBrowserWindowInterface()
->TopContainer()
->GetWidget();
}
void TabDialogManager::UpdateModalDialogBounds() {
if (bounds_animation_) {
bounds_animation_->Stop();
}
if (!widget_) {
return;
}
// Do not forcibly update the dialog widget position if it is being dragged.
if (widget_->HasCapture()) {
return;
}
auto* host_widget = GetHostWidget();
const gfx::Size size = widget_->GetRootView()->GetPreferredSize({});
if (!host_widget) {
widget_->SetSize(size);
return;
}
// If the host view's widget is minimized, don't update any positions.
if (host_widget->IsMinimized()) {
return;
}
gfx::Rect target_bounds;
if (params_->get_dialog_bounds) {
target_bounds = params_->get_dialog_bounds.Run();
} else {
target_bounds = GetModalDialogBounds(widget_.get(), tab_interface_, size);
}
if (params_->animated && gfx::Animation::ShouldRenderRichAnimation() &&
widget_->IsVisible()) {
if (!bounds_animation_) {
bounds_animation_ = std::make_unique<gfx::LinearAnimation>(this);
bounds_animation_->SetDuration(
gfx::Animation::RichAnimationDuration(base::Milliseconds(120)));
}
animation_start_bounds_ = widget_->GetWindowBoundsInScreen();
animation_target_bounds_ = target_bounds;
bounds_animation_->Start();
} else {
widget_->SetBounds(target_bounds);
}
}
bool TabDialogManager::UpdateDialogVisibility(
std::optional<bool> requested_visibility) {
if (!widget_) {
return false;
}
const bool should_be_visible =
GetDialogWidgetVisibility() && requested_visibility.value_or(true);
if (should_be_visible) {
params_->should_show_inactive ? widget_->ShowInactive() : widget_->Show();
} else {
widget_->Hide();
}
return widget_->IsVisible();
}
bool TabDialogManager::IsDialogManaged(views::Widget* widget) {
return widget_ && widget == widget_.get();
}
void TabDialogManager::DidFinishNavigation(
content::NavigationHandle* navigation_handle) {
if (!widget_) {
return;
}
if (!navigation_handle->IsInPrimaryMainFrame() ||
!navigation_handle->HasCommitted()) {
return;
}
// Disable BFCache for the page which had any modal dialog open.
// This prevents the page which has print, confirm form resubmission, http
// password dialogs, etc. to go in to BFCache. We can't simply dismiss the
// dialogs in the case, since they are requesting meaningful input from the
// user that affects the loading or display of the content.
content::BackForwardCache::DisableForRenderFrameHost(
navigation_handle->GetPreviousRenderFrameHostId(),
back_forward_cache::DisabledReason(
back_forward_cache::DisabledReasonId::kModalDialog));
// Close modal dialogs if necessary.
bool different_site_navigation =
!net::registry_controlled_domains::SameDomainOrHost(
navigation_handle->GetPreviousPrimaryMainFrameURL(),
navigation_handle->GetURL(),
net::registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES);
if (params_->close_on_navigate && different_site_navigation) {
CloseDialog();
}
}
void TabDialogManager::PrimaryMainFrameWasResized(bool width_changed) {
if (base::FeatureList::IsEnabled(features::kSideBySide)) {
UpdateModalDialogBounds();
}
}
void TabDialogManager::TabDidEnterForeground(TabInterface* tab_interface) {
if (widget_) {
browser_window_widget_observer_ =
std::make_unique<BrowserWindowWidgetObserver>(this, tab_interface_,
widget_.get());
// Check if the tab was detached and dragged to a new browser window. This
// ensures the widget is properly reparented.
auto* parent_widget = GetHostWidget();
if (parent_widget != widget_->parent()) {
widget_->Reparent(parent_widget);
}
UpdateDialogVisibility();
UpdateModalDialogBounds();
}
}
void TabDialogManager::TabWillEnterBackground(TabInterface* tab_interface) {
if (widget_) {
if (bounds_animation_ && bounds_animation_->is_animating()) {
bounds_animation_->Stop();
}
widget_->SetVisible(false);
browser_window_widget_observer_.reset();
}
}
void TabDialogManager::TabWillDetach(TabInterface* tab_interface,
TabInterface::DetachReason reason) {
if (widget_ && params_->close_on_detach) {
CloseDialog();
}
}
bool TabDialogManager::GetDialogWidgetVisibility() {
// The dialog widget should be visible if and only if the tab is in the
// foreground and activated, and the host window is not minimized. For split
// view, a tab must just be in the foreground because if both tabs have
// modals, one won't be activated.
return GetWidgetVisibility(
tab_interface_->IsVisible(),
tab_interface_->GetBrowserWindowInterface()->GetWindow()->IsMinimized(),
params_->should_show_callback);
}
void TabDialogManager::AnimationProgressed(const gfx::Animation* animation) {
if (animation == bounds_animation_.get()) {
gfx::Rect new_bounds = animation->CurrentValueBetween(
animation_start_bounds_, animation_target_bounds_);
widget_->SetBounds(new_bounds);
}
}
void TabDialogManager::AnimationEnded(const gfx::Animation* animation) {
if (animation == bounds_animation_.get()) {
widget_->SetBounds(animation_target_bounds_);
}
}
} // namespace tabs