blob: f2d2533b27f269d796dc960d0dfc89570cfab68b [file] [log] [blame]
// Copyright 2025 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/glic/widget/glic_floating_ui.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/weak_ptr.h"
#include "base/notimplemented.h"
#include "base/time/time.h"
#include "chrome/browser/glic/glic_profile_manager.h"
#include "chrome/browser/glic/widget/application_hotkey_delegate.h"
#include "chrome/browser/glic/widget/glic_inactive_floating_ui.h"
#include "chrome/browser/glic/widget/glic_panel_hotkey_delegate.h"
#include "chrome/browser/glic/widget/glic_view.h"
#include "chrome/browser/glic/widget/glic_widget.h"
#include "chrome/browser/glic/widget/glic_window_animator.h"
#include "chrome/browser/picture_in_picture/picture_in_picture_occlusion_tracker.h"
#include "chrome/browser/picture_in_picture/picture_in_picture_window_manager.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface_iterator.h"
#include "chrome/common/chrome_features.h"
#include "components/web_modal/web_contents_modal_dialog_manager.h"
#include "ui/views/widget/widget_delegate.h"
#if BUILDFLAG(IS_WIN)
#include "ui/display/win/screen_win.h"
#include "ui/views/win/hwnd_util.h"
#endif // BUILDFLAG(IS_WIN)
namespace glic {
// static
gfx::Size GlicFloatingUi::GetDefaultSize() {
return {features::kGlicMultiInstanceFloatyWidth.Get(),
features::kGlicMultiInstanceFloatyHeight.Get()};
}
// end static
GlicFloatingUi::GlicFloatingUi(Profile* profile,
gfx::Rect initial_bounds,
GlicUiEmbedder::Delegate& delegate)
: profile_(profile), delegate_(delegate) {
application_hotkey_manager_ =
MakeApplicationHotkeyManager(weak_ptr_factory_.GetWeakPtr());
glic_panel_hotkey_manager_ =
MakeGlicWindowHotkeyManager(weak_ptr_factory_.GetWeakPtr());
CreateAndSetupWidget(initial_bounds);
panel_state_.kind = mojom::PanelStateKind::kDetached;
PictureInPictureOcclusionTracker* tracker =
PictureInPictureWindowManager::GetInstance()->GetOcclusionTracker();
tracker->OnPictureInPictureWidgetOpened(glic_widget_.get());
}
GlicFloatingUi::~GlicFloatingUi() {
GlicProfileManager::GetInstance()->SetCurrentDetachedGlic(nullptr);
PictureInPictureOcclusionTracker* tracker =
PictureInPictureWindowManager::GetInstance()->GetOcclusionTracker();
tracker->RemovePictureInPictureWidget(glic_widget_.get());
}
Host::EmbedderDelegate* GlicFloatingUi::GetHostEmbedderDelegate() {
return this;
}
mojom::PanelState GlicFloatingUi::GetPanelState() const {
return panel_state_;
}
gfx::Size GlicFloatingUi::GetPanelSize() {
if (auto* glic_widget = GetGlicWidget()) {
return glic_widget->GetSize();
}
return gfx::Size();
}
GlicWidget* GlicFloatingUi::GetGlicWidget() const {
return glic_widget_.get();
}
GlicView* GlicFloatingUi::GetGlicView() const {
if (auto* glic_widget = GetGlicWidget()) {
return glic_widget->GetGlicView();
}
return nullptr;
}
void GlicFloatingUi::CreateAndSetupWidget(gfx::Rect initial_bounds) {
glic_widget_ = GlicWidget::Create(profile_, initial_bounds,
glic_panel_hotkey_manager_->GetWeakPtr(),
user_resizable_);
// TODO: Setup AccessibilityText.
GetGlicWidget()->SetZOrderLevel(ui::ZOrderLevel::kFloatingWindow);
#if BUILDFLAG(IS_MAC)
GetGlicWidget()->SetActivationIndependence(true);
GetGlicWidget()->SetVisibleOnAllWorkspaces(true);
GetGlicWidget()->SetCanAppearInExistingFullscreenSpaces(true);
#endif
glic_window_animator_ = std::make_unique<GlicWindowAnimator>(
glic_widget_->GetWeakPtr(),
base::BindRepeating(&GlicFloatingUi::MaybeSetWidgetCanResize,
weak_ptr_factory_.GetWeakPtr()));
window_event_observer_ = std::make_unique<GlicWindowEventObserver>(
glic_widget_->GetWeakPtr(), this);
glic_widget_observation_.Observe(GetGlicWidget());
}
void GlicFloatingUi::Resize(const gfx::Size& size,
base::TimeDelta duration,
base::OnceClosure callback) {
if (!user_resizing_ && glic_window_animator_ && IsShowing()) {
glic_window_animator_->AnimateSize(
GlicWidget::ClampSize(size, GetGlicWidget()), duration,
std::move(callback));
} else {
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, std::move(callback));
}
}
void GlicFloatingUi::SetDraggableAreas(
const std::vector<gfx::Rect>& draggable_areas) {
if (auto* glic_view = GetGlicView()) {
glic_view->SetDraggableAreas(draggable_areas);
}
}
GlicWindowAnimator* GlicFloatingUi::window_animator() {
return glic_window_animator_.get();
}
void GlicFloatingUi::OnDragComplete() {
NOTIMPLEMENTED();
}
void GlicFloatingUi::FocusIfOpen() {
if (!IsShowing() || HasFocus()) {
return;
}
GetGlicWidget()->Activate();
GetGlicView()->GetWebContents()->Focus();
}
bool GlicFloatingUi::HasFocus() {
return IsShowing() && GetGlicWidget()->IsActive();
}
bool GlicFloatingUi::ActivateBrowser() {
if (auto* const last_active_bwi =
GetLastActiveBrowserWindowInterfaceWithAnyProfile()) {
last_active_bwi->GetWindow()->Activate();
return true;
}
return false;
}
void GlicFloatingUi::ShowTitleBarContextMenuAt(gfx::Point event_loc) {
#if BUILDFLAG(IS_WIN)
views::View::ConvertPointToScreen(GetGlicView(), &event_loc);
event_loc = display::win::GetScreenWin()->DIPToScreenPoint(event_loc);
views::ShowSystemMenuAtScreenPixelLocation(views::HWNDForView(GetGlicView()),
event_loc);
#endif // BUILDFLAG(IS_WIN)
}
void GlicFloatingUi::EnableDragResize(bool enabled) {
user_resizable_ = enabled;
MaybeSetWidgetCanResize();
GetGlicView()->UpdateBackgroundColor();
glic_window_animator_->MaybeAnimateToTargetSize();
}
void GlicFloatingUi::MaybeSetWidgetCanResize() {
if (GetGlicWidget()->widget_delegate()->CanResize() == user_resizable_ ||
glic_window_animator_->IsAnimating()) {
// If the resize state is already correct or the widget is animating do not
// update the resize state.
return;
}
#if BUILDFLAG(IS_WIN)
// On Windows when resize is enabled there is an invisible border added
// around the client area. We need to make the widget larger or smaller to
// keep the visible client area the same size.
gfx::Rect previous_client_bounds =
GetGlicWidget()->GetClientAreaBoundsInScreen();
#endif // BUILDFLAG(IS_WIN)
// Update resize state on widget delegate.
GetGlicWidget()->widget_delegate()->SetCanResize(user_resizable_);
#if BUILDFLAG(IS_WIN)
if (user_resizable_) {
// Resizable so the widget area is larger than the client area.
gfx::Rect new_widget_bounds =
GetGlicWidget()->VisibleToWidgetBounds(previous_client_bounds);
GetGlicWidget()->SetBoundsConstrained(new_widget_bounds);
} else {
// Not resizable so the client and widget areas are the same.
GetGlicWidget()->SetBoundsConstrained(previous_client_bounds);
}
#endif // BUILDFLAG(IS_WIN)
}
void GlicFloatingUi::Attach() {
NOTIMPLEMENTED();
}
void GlicFloatingUi::Detach() {
// Floaty UI is already detached.
NOTREACHED();
}
void GlicFloatingUi::SetMinimumWidgetSize(const gfx::Size& size) {
GetGlicWidget()->SetMinimumSize(size);
}
bool GlicFloatingUi::IsShowing() const {
return glic_widget_ != nullptr;
}
void GlicFloatingUi::Show() {
GlicProfileManager::GetInstance()->SetCurrentDetachedGlic(profile_);
GetGlicWidget()->Show();
GetGlicView()->SetWebContents(delegate_->host().webui_contents());
GetGlicView()->UpdateBackgroundColor();
application_hotkey_manager_->InitializeAccelerators();
glic_panel_hotkey_manager_->InitializeAccelerators();
// TODO: Set up manual resize.
window_event_observer_->SetDraggingAreasAndWatchForMouseEvents();
// Add capability to show web modal dialogs (e.g. Data Controls Dialogs for
// enterprise users) via constrained_window APIs.
web_modal::WebContentsModalDialogManager::CreateForWebContents(
delegate_->host().webui_contents());
web_modal::WebContentsModalDialogManager::FromWebContents(
delegate_->host().webui_contents())
->SetDelegate(this);
}
void GlicFloatingUi::Close() {
if (IsShowing()) {
modal_dialog_host_observers_.Notify(
&web_modal::ModalDialogHostObserver::OnHostDestroying);
if (auto* web_contents = delegate_->host().webui_contents()) {
web_modal::WebContentsModalDialogManager::FromWebContents(web_contents)
->SetDelegate(nullptr);
}
}
if (screenshot_capturer_) {
screenshot_capturer_->CloseScreenPicker();
}
window_event_observer_.reset();
glic_window_animator_.reset();
glic_widget_observation_.Reset();
glic_widget_.reset();
user_resizable_ = false;
// NOTE: `this` will be destroyed after this call.
delegate_->WillCloseFor(FloatingEmbedderKey{});
}
void GlicFloatingUi::ClosePanel() {
Close();
}
void GlicFloatingUi::Focus() {
if (auto* web_contents = delegate_->host().webui_contents()) {
web_contents->Focus();
}
}
void GlicFloatingUi::OnWidgetActivationChanged(views::Widget* widget,
bool active) {
delegate_->OnEmbedderWindowActivationChanged(active);
}
void GlicFloatingUi::OnWidgetDestroyed(views::Widget* widget) {
// This is used to handle the case where the native window is closed
// directly (e.g., Windows context menu close on the title bar).
// Conceptually this should synchronously call Close(), but the Widget
// implementation currently does not support this.
if (GetGlicWidget() == widget) {
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(&GlicFloatingUi::Close, weak_ptr_factory_.GetWeakPtr()));
}
}
void GlicFloatingUi::OnWidgetBoundsChanged(views::Widget* widget,
const gfx::Rect& new_bounds) {
modal_dialog_host_observers_.Notify(
&web_modal::ModalDialogHostObserver::OnPositionRequiresUpdate);
}
void GlicFloatingUi::OnWidgetUserResizeStarted() {
user_resizing_ = true;
if (GlicWebClientAccess* client = delegate_->host().GetPrimaryWebClient()) {
client->ManualResizeChanged(true);
}
}
void GlicFloatingUi::OnWidgetUserResizeEnded() {
if (GlicWebClientAccess* client = delegate_->host().GetPrimaryWebClient()) {
client->ManualResizeChanged(false);
}
if (GetGlicView()) {
GetGlicView()->UpdatePrimaryDraggableAreaOnResize();
}
glic_window_animator_->ResetLastTargetSize();
user_resizing_ = false;
}
// web_modal::WebContentsModalDialogManagerDelegate
// web_modal::WebContentsModalDialogHost
web_modal::WebContentsModalDialogHost*
GlicFloatingUi::GetWebContentsModalDialogHost(
content::WebContents* web_contents) {
return this;
}
gfx::Size GlicFloatingUi::GetMaximumDialogSize() {
return GetGlicWidget()->GetClientAreaBoundsInScreen().size();
}
gfx::NativeView GlicFloatingUi::GetHostView() const {
return GetGlicWidget()->GetNativeView();
}
gfx::Point GlicFloatingUi::GetDialogPosition(const gfx::Size& dialog_size) {
gfx::Rect client_area_bounds = GetGlicWidget()->GetClientAreaBoundsInScreen();
return gfx::Point((client_area_bounds.width() - dialog_size.width()) / 2, 0);
}
bool GlicFloatingUi::ShouldConstrainDialogBoundsByHost() {
// Allows web modal dialogs to extend beyond the boundary of glic window.
// These web modals are usually larger than the glic window.
return false;
}
void GlicFloatingUi::AddObserver(web_modal::ModalDialogHostObserver* observer) {
modal_dialog_host_observers_.AddObserver(observer);
}
void GlicFloatingUi::RemoveObserver(
web_modal::ModalDialogHostObserver* observer) {
modal_dialog_host_observers_.RemoveObserver(observer);
}
std::unique_ptr<GlicUiEmbedder> GlicFloatingUi::CreateInactiveEmbedder() const {
return GlicInactiveFloatingUi::From(*this);
}
base::WeakPtr<views::View> GlicFloatingUi::GetView() {
if (auto* glic_view = GetGlicView()) {
return glic_view->GetWeakPtr();
}
return nullptr;
}
void GlicFloatingUi::SwitchConversation(
glic::mojom::ConversationInfoPtr info,
mojom::WebClientHandler::SwitchConversationCallback callback) {
// NOTE: `this` may be destroyed after this call.
delegate_->SwitchConversation(
ShowOptions::ForFloating(GetGlicWidget()->GetWindowBoundsInScreen()),
std::move(info), std::move(callback));
}
void GlicFloatingUi::CaptureScreenshot(
glic::mojom::WebClientHandler::CaptureScreenshotCallback callback) {
if (!screenshot_capturer_) {
screenshot_capturer_ = std::make_unique<GlicScreenshotCapturer>();
}
screenshot_capturer_->CaptureScreenshot(GetGlicWidget()->GetNativeWindow(),
std::move(callback));
}
} // namespace glic