| // 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 |