blob: 231ca97ec93a2bbde9e713722e8ebdd43cf0a831 [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/glic/widget/glic_window_controller_impl.h"
#include <algorithm>
#include "base/check.h"
#include "base/check_deref.h"
#include "base/metrics/histogram_functions.h"
#include "base/time/time.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/glic/browser_ui/scoped_glic_button_indicator.h"
#include "chrome/browser/glic/fre/glic_fre_controller.h"
#include "chrome/browser/glic/fre/glic_fre_dialog_view.h"
#include "chrome/browser/glic/glic_enabling.h"
#include "chrome/browser/glic/glic_keyed_service.h"
#include "chrome/browser/glic/glic_metrics.h"
#include "chrome/browser/glic/glic_pref_names.h"
#include "chrome/browser/glic/glic_profile_manager.h"
#include "chrome/browser/glic/host/glic.mojom.h"
#include "chrome/browser/glic/host/host.h"
#include "chrome/browser/glic/host/webui_contents_container.h"
#include "chrome/browser/glic/resources/grit/glic_browser_resources.h"
#include "chrome/browser/glic/widget/browser_conditions.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/profiles/keep_alive/profile_keep_alive_types.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/ui/browser_finder.h"
#include "chrome/browser/ui/browser_list.h"
#include "chrome/browser/ui/layout_constants.h"
#include "chrome/browser/ui/tabs/public/tab_dialog_manager.h"
#include "chrome/browser/ui/tabs/public/tab_features.h"
#include "chrome/browser/ui/views/chrome_widget_sublevel.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
#include "chrome/browser/ui/views/frame/tab_strip_region_view.h"
#include "chrome/browser/ui/views/tabs/glic_button.h"
#include "chrome/browser/ui/views/tabs/tab_strip_action_container.h"
#include "chrome/browser/ui/views/tabs/window_finder.h"
#include "chrome/common/chrome_features.h"
#include "components/tabs/public/tab_interface.h"
#include "components/web_modal/web_contents_modal_dialog_manager.h"
#include "content/public/browser/web_contents.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/ui_base_types.h"
#include "ui/display/display.h"
#include "ui/display/display_finder.h"
#include "ui/display/screen.h"
#include "ui/events/event_observer.h"
#include "ui/views/controls/webview/webview.h"
#include "ui/views/event_monitor.h"
#include "ui/views/interaction/element_tracker_views.h"
#include "ui/views/widget/native_widget.h"
#include "ui/views/widget/widget_observer.h"
#if BUILDFLAG(IS_WIN)
#include "ui/aura/window.h"
#include "ui/aura/window_tree_host.h"
#include "ui/base/win/event_creation_utils.h"
#include "ui/display/win/screen_win.h"
#include "ui/views/win/hwnd_util.h"
#endif // BUILDFLAG(IS_WIN)
namespace glic {
DEFINE_CUSTOM_ELEMENT_EVENT_TYPE(kGlicWidgetAttached);
namespace {
// Default value for adding a buffer to the attachment zone.
constexpr static int kAttachmentBuffer = 20;
constexpr static int kInitialPositionBuffer = 4;
constexpr static int kMaxWidgetSize = 16'384;
constexpr static int kDraggableAreaHeight = 44;
constexpr static base::TimeDelta kAnimationDuration = base::Milliseconds(300);
mojom::PanelState CreatePanelState(bool widget_visible,
Browser* attached_browser) {
mojom::PanelState panel_state;
if (!widget_visible) {
panel_state.kind = mojom::PanelState_Kind::kHidden;
} else if (attached_browser) {
panel_state.kind = mojom::PanelState_Kind::kAttached;
panel_state.window_id = attached_browser->session_id().id();
} else {
panel_state.kind = mojom::PanelState_Kind::kDetached;
}
return panel_state;
}
GlicButton* GetGlicButton(const Browser& browser) {
return browser.window()
->AsBrowserView()
->tab_strip_region_view()
->GetGlicButton();
}
display::Display GetDisplayForOpeningDetached() {
// Get the Display for the most recently active browser. If there was no
// recently active browser, use the primary display.
Browser* last_active_browser = BrowserList::GetInstance()->GetLastActive();
if (last_active_browser) {
std::optional<display::Display> widget_display =
last_active_browser->GetBrowserView().GetWidget()->GetNearestDisplay();
if (widget_display) {
return *widget_display;
}
}
return display::Screen::GetScreen()->GetPrimaryDisplay();
}
// True if |bounds| is an allowed position the Widget can be shown in.
bool IsWidgetLocationAllowed(const gfx::Rect& bounds) {
const std::vector<display::Display>& displays =
display::Screen::GetScreen()->GetAllDisplays();
// Calculate inset corners to allow part of the widget to be off screen.
std::array<gfx::Point, 4> inset_points = {
// top-left: Allow 40% on left and |kInitialPositionBuffer| on top.
gfx::Point(bounds.x() + bounds.width() * .4,
bounds.y() + kInitialPositionBuffer),
// top-right: Allow 40% on right and |kInitialPositionBuffer| on top.
gfx::Point(bounds.right() - bounds.width() * .4,
bounds.y() + kInitialPositionBuffer),
// bottom-left: Allow 40% on left and 70% on bottom.
gfx::Point(bounds.x() + bounds.width() * .4,
bounds.bottom() - bounds.height() * .7),
// bottom-right: Allow 40% on right and 70% on bottom.
gfx::Point(bounds.right() - bounds.width() * .4,
bounds.bottom() - bounds.height() * .7),
};
// Check that all four points are on an existing display.
return std::ranges::all_of(inset_points, [&](gfx::Point p) {
return display::FindDisplayContainingPoint(displays, p) != displays.end();
});
}
std::optional<int> GetOptionalIntPreference(PrefService* prefs,
std::string_view path) {
const PrefService::Preference& pref =
CHECK_DEREF(prefs->FindPreference(path));
if (pref.IsDefaultValue()) {
return std::nullopt;
}
return pref.GetValue()->GetInt();
}
// Get the previous position or none if the window has not been dragged before.
std::optional<gfx::Point> GetPreviousPositionFromPrefs(PrefService* prefs) {
if (!prefs) {
return std::nullopt;
}
auto x_pos = GetOptionalIntPreference(prefs, prefs::kGlicPreviousPositionX);
auto y_pos = GetOptionalIntPreference(prefs, prefs::kGlicPreviousPositionY);
if (!x_pos.has_value() || !y_pos.has_value()) {
return std::nullopt;
}
return gfx::Point(x_pos.value(), y_pos.value());
}
} // namespace
// Helper class for observing mouse and key events from native window.
class GlicWindowControllerImpl::WindowEventObserver : public ui::EventObserver {
public:
WindowEventObserver(glic::GlicWindowController* glic_window_controller,
glic::GlicView* glic_view)
: glic_window_controller_(glic_window_controller), glic_view_(glic_view) {
event_monitor_ = views::EventMonitor::CreateWindowMonitor(
this, glic_view->GetWidget()->GetNativeWindow(),
{
ui::EventType::kMousePressed,
ui::EventType::kMouseReleased,
ui::EventType::kMouseDragged,
ui::EventType::kTouchReleased,
ui::EventType::kTouchPressed,
ui::EventType::kTouchMoved,
ui::EventType::kTouchCancelled,
});
}
WindowEventObserver(const WindowEventObserver&) = delete;
WindowEventObserver& operator=(const WindowEventObserver&) = delete;
~WindowEventObserver() override = default;
void OnEvent(const ui::Event& event) override {
#if BUILDFLAG(IS_WIN)
if (event.IsTouchEvent()) {
// If we get a touch event, send the corresponding mouse event so that
// drag drop of the floaty window will work with touch screens. This is a
// bit hacky; it would be better to have non client hit tests for the
// draggable area return HT_CAPTION but that requires the web client to
// set the draggable areas correctly, and not include the buttons in the
// titlebar. See crbug.com/388000848.
const ui::TouchEvent* touch_event = event.AsTouchEvent();
gfx::Point touch_location = touch_event->location();
auto touch_screen_point =
views::View::ConvertPointToScreen(glic_view_, touch_location);
auto* host = glic_view_->GetWidget()->GetNativeWindow()->GetHost();
host->ConvertDIPToPixels(&touch_screen_point);
if (event.type() == ui::EventType::kTouchPressed) {
POINT cursor_location = touch_screen_point.ToPOINT();
::SetCursorPos(cursor_location.x, cursor_location.y);
touch_down_in_draggable_area_ =
glic_view_->IsPointWithinDraggableArea(touch_location);
if (touch_down_in_draggable_area_) {
ui::SendMouseEvent(touch_screen_point, MOUSEEVENTF_LEFTDOWN);
ui::SendMouseEvent(touch_screen_point, MOUSEEVENTF_MOVE);
}
}
if (!touch_down_in_draggable_area_) {
// If we're not in a potential touch drag of the window, ignore touch
// events.
return;
}
if (event.type() == ui::EventType::kTouchCancelled ||
event.type() == ui::EventType::kTouchReleased) {
touch_down_in_draggable_area_ = false;
ui::SendMouseEvent(touch_screen_point, MOUSEEVENTF_LEFTUP);
}
if (event.type() == ui::EventType::kTouchMoved) {
ui::SendMouseEvent(touch_screen_point, MOUSEEVENTF_MOVE);
}
return;
}
#endif // BUILDFLAG(IS_WIN)
gfx::Point mouse_location = event_monitor_->GetLastMouseLocation();
views::View::ConvertPointFromScreen(glic_view_, &mouse_location);
if (event.type() == ui::EventType::kMousePressed) {
mouse_down_in_draggable_area_ =
glic_view_->IsPointWithinDraggableArea(mouse_location);
initial_press_loc_ = mouse_location;
}
if (event.type() == ui::EventType::kMouseReleased &&
event.AsMouseEvent()->IsRightMouseButton() &&
mouse_down_in_draggable_area_) {
glic_window_controller_->ShowTitleBarContextMenuAt(mouse_location);
}
if (event.type() == ui::EventType::kMouseReleased ||
event.type() == ui::EventType::kMouseExited) {
mouse_down_in_draggable_area_ = false;
initial_press_loc_ = gfx::Point();
}
// Window should only be dragged if a corresponding mouse drag event was
// initiated in the draggable area.
if (mouse_down_in_draggable_area_ &&
event.type() == ui::EventType::kMouseDragged &&
glic_window_controller_->ShouldStartDrag(initial_press_loc_,
mouse_location)) {
glic_window_controller_->HandleWindowDragWithOffset(
initial_press_loc_.OffsetFromOrigin());
}
}
private:
raw_ptr<glic::GlicWindowController> glic_window_controller_;
raw_ptr<glic::GlicView> glic_view_;
std::unique_ptr<views::EventMonitor> event_monitor_;
// Tracks whether the mouse is pressed and was initially within a draggable
// area of the window.
bool mouse_down_in_draggable_area_ = false;
#if BUILDFLAG(IS_WIN)
// Tracks whether a touch pressed event occurred within the draggable area. If
// so, subsequent touch events will trigger corresponding mouse events so that
// window drag works.
bool touch_down_in_draggable_area_ = false;
#endif // BUILDFLAG(IS_WIN)
// Tracks the initial kMousePressed location of a potential drag.
gfx::Point initial_press_loc_;
};
GlicWindowControllerImpl::GlicWindowControllerImpl(
Profile* profile,
signin::IdentityManager* identity_manager,
GlicKeyedService* glic_service,
GlicEnabling* enabling)
: profile_(profile),
fre_controller_(
std::make_unique<GlicFreController>(profile, identity_manager)),
window_finder_(std::make_unique<WindowFinder>()),
glic_service_(glic_service),
enabling_(enabling) {
previous_position_ = GetPreviousPositionFromPrefs(profile_->GetPrefs());
application_hotkey_manager_ = MakeApplicationHotkeyManager(GetWeakPtr());
host_observation_.Observe(&glic_service_->host());
}
GlicWindowControllerImpl::~GlicWindowControllerImpl() = default;
void GlicWindowControllerImpl::WebClientInitializeFailed() {
if (state_ == State::kWaitingForGlicToLoad) {
// TODO(crbug.com/388328847): The web client failed to initialize. Decide
// what the fallback behavior is. Additionally, we probably need some kind
// of timeout and/or loading indicator if loading takes too much time. For
// now, show the UI anyway, which should be helpful in development.
LOG(ERROR)
<< "Glic web client failed to initialize, it won't work properly.";
glic_service_->metrics()->set_show_start_time(base::TimeTicks());
GlicLoadedAndReadyToDisplay();
}
}
void GlicWindowControllerImpl::LoginPageCommitted() {
login_page_committed_ = true;
if (state_ == State::kWaitingForGlicToLoad && !host().IsReady()) {
// TODO(crbug.com/388328847): Temporarily allow showing the UI when a login
// page is reached.
glic_service_->metrics()->set_show_start_time(base::TimeTicks());
GlicLoadedAndReadyToDisplay();
}
}
// Monitoring the glic widget.
void GlicWindowControllerImpl::OnWidgetActivationChanged(views::Widget* widget,
bool active) {
if (GetGlicWidget() != widget) {
return;
}
if (!active && do_focus_loss_announcement_) {
widget->widget_delegate()->SetAccessibleTitle(
l10n_util::GetStringUTF16(IDS_GLIC_WINDOW_TITLE));
GetGlicView()->GetViewAccessibility().AnnounceAlert(
l10n_util::GetStringFUTF16(
IDS_GLIC_WINDOW_FIRST_FOCUS_LOST_ANNOUNCEMENT,
LocalHotkeyManager::GetConfigurableAccelerator(
LocalHotkeyManager::Hotkey::kFocusToggle)
.GetShortcutText()));
do_focus_loss_announcement_ = false;
}
window_activation_callback_list_.Notify(active);
}
// Monitoring the glic widget.
void GlicWindowControllerImpl::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(&GlicWindowControllerImpl::Close,
weak_ptr_factory_.GetWeakPtr()));
}
}
void GlicWindowControllerImpl::OnWidgetBoundsChanged(
views::Widget* widget,
const gfx::Rect& new_bounds) {
if (in_move_loop_ && !AlwaysDetached()) {
// While in a move loop, look for nearby browsers to toggle the drop to
// attach indicator.
HandleGlicButtonIndicator();
}
modal_dialog_host_observers_.Notify(
&web_modal::ModalDialogHostObserver::OnPositionRequiresUpdate);
}
void GlicWindowControllerImpl::OnWidgetUserResizeStarted() {
user_resizing_ = true;
glic_service_->metrics()->OnWidgetUserResizeStarted();
if (GlicWebClientAccess* client = host().GetPrimaryWebClient()) {
client->ManualResizeChanged(true);
}
}
void GlicWindowControllerImpl::OnWidgetUserResizeEnded() {
glic_service_->metrics()->OnWidgetUserResizeEnded();
if (GlicWebClientAccess* client = host().GetPrimaryWebClient()) {
client->ManualResizeChanged(false);
}
if (GetGlicView()) {
GetGlicView()->UpdatePrimaryDraggableAreaOnResize();
}
if (GetGlicWidget()) {
glic_size_ = GetGlicWidget()->GetSize();
}
glic_window_animator_->ResetLastTargetSize();
user_resizing_ = false;
}
void GlicWindowControllerImpl::ShowAfterSignIn(base::WeakPtr<Browser> browser) {
Toggle(browser.get(), true,
// Prefer the source that triggered the sign-in, but if that's not
// available, report it as coming from the sign-in flow.
opening_source_.value_or(mojom::InvocationSource::kAfterSignIn));
}
void GlicWindowControllerImpl::Toggle(BrowserWindowInterface* bwi,
bool prevent_close,
mojom::InvocationSource source) {
// If `bwi` is non-null, the glic button was clicked on a specific window and
// glic should be attached to that window. Otherwise glic was invoked from the
// hotkey or other OS-level entrypoint.
Browser* new_attached_browser =
bwi ? bwi->GetBrowserForMigrationOnly() : nullptr;
// Show the FRE if not yet completed, and if we have a browser to use.
if (fre_controller_->ShouldShowFreDialog()) {
if (!fre_controller_->CanShowFreDialog(new_attached_browser)) {
// If the FRE is blocked because it is already showing, we should instead
// dismiss it. This allows the glic button to be used to toggle the
// presence of the FRE.
fre_controller_->DismissFreIfOpenOnActiveTab(new_attached_browser);
return;
}
fre_controller_->ShowFreDialog(new_attached_browser);
return;
}
mojom::PanelState panel_state = ComputePanelState();
bool is_detached = panel_state.kind == mojom::PanelState_Kind::kDetached;
// In the case where the user invokes the hotkey, or the status tray glic
// icon and the most recently used window for the glic profile is active,
// treat this as if the user clicked the glic button on that window if
// Chrome is currently in the foreground and we aren't in detached state.
if (!new_attached_browser && !is_detached) {
Browser* last_active_browser = BrowserList::GetInstance()->GetLastActive();
if (last_active_browser &&
IsBrowserGlicAttachable(profile_, last_active_browser) &&
IsBrowserInForeground(last_active_browser)) {
new_attached_browser = last_active_browser;
}
}
if (!AlwaysDetached()) {
ToggleWhenNotAlwaysDetached(new_attached_browser, prevent_close, source);
return;
}
auto maybe_close = [this, prevent_close] {
if (!prevent_close) {
Close();
}
};
// If floaty is closed, open floaty
if (state_ == State::kClosed) {
Show(new_attached_browser, source);
return;
}
#if BUILDFLAG(IS_WIN)
// Clicking status tray on Windows makes floaty not active so always close.
if (source == mojom::InvocationSource::kOsButton) {
Close();
return;
}
#endif // BUILDFLAG(IS_WIN)
// If floaty is focused or the source is the top button, close it.
// If floaty is unfocused and open, focus it.
if (IsActive() ||
(source == mojom::InvocationSource::kTopChromeButton &&
!base::FeatureList::IsEnabled(features::kGlicZOrderChanges))) {
maybe_close();
} else if (state_ == State::kOpen) {
// TODO(crbug.com/404601783): Bring focus to the textbox.
GetGlicWidget()->Activate();
GetGlicView()->GetWebContents()->Focus();
}
}
void GlicWindowControllerImpl::ToggleWhenNotAlwaysDetached(
Browser* new_attached_browser,
bool prevent_close,
mojom::InvocationSource source) {
auto maybe_close = [this, prevent_close] {
if (!prevent_close) {
Close();
}
};
// Pressing the button or the hotkey when the window is open, or waiting to
// load should close it. The latter is required because otherwise if there
// were an error loading the backend (or if it just took a long time) then the
// button/hotkey would become unresponsive.
//
// In the future, when the WebUI can send its status back to the controller
// via mojom, we could explicitly restrict the second case to loading,
// offline, and error states.
if (state_ == State::kOpen || state_ == State::kWaitingForGlicToLoad) {
if (new_attached_browser) {
if (new_attached_browser == attached_browser_) {
// Button was clicked on same browser: close.
// There are three ways for this to happen. Normally the glic window
// obscures the glic button. Either the user used keyboard navigation to
// click the glic button, the user clicked the button early and the
// button click was eventually processed asynchronously after the button
// was obscured, or the user invokes the glic hotkey while glic is
// attached to the active window.
maybe_close();
} else {
// Button clicked on a different browser: attach to that one.
MovePositionToBrowserGlicButton(*new_attached_browser,
/*animate=*/true);
AttachToBrowser(*new_attached_browser, AttachChangeReason::kInit);
}
return;
}
// Everything else in this block handles the case where the user invokes the
// hotkey and the most recently used window from the glic profile is not
// active.
// Already attached?
if (attached_browser_) {
bool should_close = IsActive();
#if BUILDFLAG(IS_WIN)
// On Windows, clicking the system tray icon de-activates the active
// window, so fall back to checking if `attached_browser_` is visible for
// both the hot key and system tray click cases.
should_close = attached_browser_->window()
->GetNativeWindow()
->GetHost()
->GetNativeWindowOcclusionState() ==
aura::Window::OcclusionState::VISIBLE;
#endif // BUILDFLAG(IS_WIN)
if (should_close) {
// Hotkey when glic active and attached: close.
maybe_close();
return;
}
// Hotkey when glic is inactive and attached:
if (attached_browser_->IsActive()) {
// Hotkey when glic inactive but attached to active browser: close.
// Note: this should not be possible, since if the attached browser is
// active, new_attached_browser must not have been null.
maybe_close();
} else {
// Hotkey when neither attached browser nor glic are active: open
// detached.
}
return;
}
// Hotkey invoked when glic is already detached.
maybe_close();
} else if (state_ != State::kClosed) {
// Currently in the process of showing the widget, allow that to finish.
return;
} else {
Show(new_attached_browser, source);
}
}
void GlicWindowControllerImpl::FocusIfOpen() {
if (IsShowing() && !IsActive()) {
GetGlicWidget()->Activate();
GetGlicView()->GetWebContents()->Focus();
}
}
void GlicWindowControllerImpl::ShowDetachedForTesting() {
glic::GlicProfileManager::GetInstance()->SetActiveGlic(glic_service_);
Show(nullptr, mojom::InvocationSource::kOsHotkey);
}
void GlicWindowControllerImpl::SetPreviousPositionForTesting(
gfx::Point position) {
previous_position_ = position;
}
Host& GlicWindowControllerImpl::host() const {
return glic_service_->host();
}
void GlicWindowControllerImpl::Show(Browser* browser,
mojom::InvocationSource source) {
// At this point State must be kClosed, and all glic window state must be
// unset.
CHECK(!attached_browser_);
opening_source_ = source;
if (!glic_service_->GetAuthController().CheckAuthBeforeShowSync(
base::BindOnce(&GlicWindowControllerImpl::ShowAfterSignIn,
weak_ptr_factory_.GetWeakPtr(),
browser ? browser->AsWeakPtr() : nullptr))) {
return;
}
glic_window_animator_ = std::make_unique<GlicWindowAnimator>(this);
SetWindowState(State::kWaitingForGlicToLoad);
glic_service_->metrics()->OnGlicWindowOpen(/*attached=*/browser, source);
glic_service_->GetAuthController().OnGlicWindowOpened();
glic_service_->metrics()->set_show_start_time(base::TimeTicks::Now());
host().CreateContents();
host().NotifyWindowIntentToShow();
SetupGlicWidget(browser);
// Notify the web client that the panel will open, and wait for the response
// to actually show the window. Note that we have to call
// `NotifyIfPanelStateChanged()` first, so that the host will receive the
// correct panel state.
NotifyIfPanelStateChanged();
host().PanelWillOpen(source);
if (login_page_committed_) {
// This indicates that we've warmed the web client and it has hit a login
// page. See LoginPageCommitted.
GlicLoadedAndReadyToDisplay();
} else {
// This adds dragging functionality to special case panels (e.g. error,
// offline, loading).
SetDraggingAreasAndWatchForMouseEvents();
}
glic_service_->metrics()->OnGlicWindowShown();
}
void GlicWindowControllerImpl::SetupGlicWidget(Browser* browser) {
auto initial_bounds = GetInitialBounds(browser);
glic_window_hotkey_manager_ = MakeGlicWindowHotkeyManager(GetWeakPtr());
glic_widget_ = GlicWidget::Create(profile_, initial_bounds,
glic_window_hotkey_manager_->GetWeakPtr(),
user_resizable_);
glic_widget_observation_.Observe(glic_widget_.get());
SetupGlicWidgetAccessibilityText();
glic_window_hotkey_manager_->InitializeAccelerators();
if (AlwaysDetached()) {
SetGlicWindowToFloatingMode(true);
} else if (!browser) {
// Detached window when no always detach. Be sure to set its state first
// before showing it.
// TODO(crbug.com/410629338): Reimplement attachment.
}
glic_widget_->Show();
// This is needed in case of theme difference between OS and chrome.
GetGlicWidget()->ThemeChanged();
if (browser && !AlwaysDetached()) {
// Attached window if needed.
AttachToBrowser(*browser, AttachChangeReason::kInit);
}
// This is used to handle the case where the native window is closed
// directly (e.g., Windows context menu close on the title bar). It fixes the
// bug where the window position was not restored after closing with the
// context menu close menu item.
GetGlicWidget()->MakeCloseSynchronous(base::BindOnce(
&GlicWindowControllerImpl::CloseWithReason, base::Unretained(this)));
// Immediately hook up the WebView to the WebContents.
GetGlicView()->SetWebContents(host().webui_contents());
GetGlicView()->UpdateBackgroundColor();
// Add capability to show web modal dialogs (e.g. Data Controls Dialogs for
// enterprise users) via constrained_window APIs.
web_modal::WebContentsModalDialogManager::CreateForWebContents(
host().webui_contents());
web_modal::WebContentsModalDialogManager::FromWebContents(
host().webui_contents())
->SetDelegate(this);
}
void GlicWindowControllerImpl::SetupGlicWidgetAccessibilityText() {
auto* widget_delegate = glic_widget_->widget_delegate();
if (opening_source_ == mojom::InvocationSource::kFre) {
widget_delegate->SetAccessibleTitle(l10n_util::GetStringFUTF16(
IDS_GLIC_WINDOW_TITLE_FIRST_LOAD,
LocalHotkeyManager::GetConfigurableAccelerator(
LocalHotkeyManager::Hotkey::kFocusToggle)
.GetShortcutText()));
do_focus_loss_announcement_ = true;
} else {
widget_delegate->SetAccessibleTitle(
l10n_util::GetStringUTF16(IDS_GLIC_WINDOW_TITLE));
}
}
void GlicWindowControllerImpl::SetGlicWindowToFloatingMode(bool floating) {
GetGlicWidget()->SetZOrderLevel(floating ? ui::ZOrderLevel::kFloatingWindow
: ui::ZOrderLevel::kNormal);
#if BUILDFLAG(IS_MAC)
GetGlicWidget()->SetActivationIndependence(floating);
GetGlicWidget()->SetVisibleOnAllWorkspaces(floating);
GetGlicWidget()->SetCanAppearInExistingFullscreenSpaces(floating);
#endif
}
gfx::Rect GlicWindowControllerImpl::GetInitialBounds(Browser* browser) {
if (browser && !AlwaysDetached()) {
return GetInitialAttachedBounds(*browser);
}
gfx::Size target_size = GetLastRequestedSizeClamped();
// Reset previous position if it results in an invalid location.
if (previous_position_.has_value() &&
!IsWidgetLocationAllowed({previous_position_.value(), target_size})) {
previous_position_.reset();
}
// Use the previous position if there is one.
if (previous_position_.has_value()) {
return {previous_position_.value(), target_size};
}
std::optional<gfx::Rect> bounds_with_browser =
GetInitialDetachedBoundsFromBrowser(browser, target_size);
return bounds_with_browser.value_or(
GetInitialDetachedBoundsNoBrowser(target_size));
}
gfx::Rect GlicWindowControllerImpl::GetInitialDetachedBoundsNoBrowser(
const gfx::Size& target_size) {
// Get the default position offset equal distances from the top right corner
// of the work area (which excludes system UI such as the taskbar).
display::Display display = GetDisplayForOpeningDetached();
gfx::Point top_right = display.work_area().top_right();
int initial_x =
top_right.x() - target_size.width() - kDefaultDetachedTopRightDistance;
int initial_y = top_right.y() + kDefaultDetachedTopRightDistance;
return {{initial_x, initial_y}, target_size};
}
gfx::Rect GlicWindowControllerImpl::GetInitialAttachedBounds(Browser& browser) {
GlicButton* glic_button = GetGlicButton(browser);
CHECK(glic_button);
// If summoned from the tab strip button. This will always show up attached
// because it is tied to a views::View object within the current browser
// window.
gfx::Rect glic_window_widget_initial_rect = glic_button->GetBoundsWithInset();
// Ensure that we start with a non-empty rect (see DCHECK in
// NativeWidgetNSWindowBridge::SetInitialBounds).
if (glic_window_widget_initial_rect.IsEmpty()) {
glic_window_widget_initial_rect.set_width(
std::max(glic_window_widget_initial_rect.width(), 1));
glic_window_widget_initial_rect.set_height(
std::max(glic_window_widget_initial_rect.height(), 1));
}
return glic_window_widget_initial_rect;
}
std::optional<gfx::Rect>
GlicWindowControllerImpl::GetInitialDetachedBoundsFromBrowser(
Browser* browser,
const gfx::Size& target_size) {
if (!browser) {
return std::nullopt;
}
// Set the origin so the top right of glic meets the bottom left of the glic
// button.
GlicButton* glic_button = GetGlicButton(*browser);
CHECK(glic_button);
gfx::Rect glic_button_inset_bounds = glic_button->GetBoundsWithInset();
gfx::Point origin(glic_button_inset_bounds.x() - target_size.width() -
kInitialPositionBuffer,
glic_button_inset_bounds.bottom() + kInitialPositionBuffer);
gfx::Rect bounds = {origin, target_size};
return IsWidgetLocationAllowed(bounds) ? std::make_optional(bounds)
: std::nullopt;
}
void GlicWindowControllerImpl::ClientReadyToShow(
const mojom::OpenPanelInfo& open_info) {
DVLOG(1) << "Glic client ready to show " << open_info.web_client_mode;
glic_service_->metrics()->set_starting_mode(open_info.web_client_mode);
glic_service_->metrics()->OnGlicWindowOpenAndReady();
if (open_info.panelSize.has_value()) {
Resize(*open_info.panelSize, open_info.resizeDuration, base::DoNothing());
}
EnableDragResize(open_info.can_user_resize);
if (state_ == State::kWaitingForGlicToLoad) {
GlicLoadedAndReadyToDisplay();
}
}
void GlicWindowControllerImpl::GlicLoadedAndReadyToDisplay() {
login_page_committed_ = false;
if (state_ == State::kClosed || state_ == State::kOpen) {
return;
}
// Update the background color after showing the webview so the transition
// isn't visible. This will be the widget background color the user sees next
// time.
GetGlicView()->UpdateBackgroundColor();
// In the case that the open animation was skipped, the web view should still
// be visible now.
SetWindowState(State::kOpen);
// Whenever the glic window is shown, it should have focus. The following line
// of code appears to be necessary but not sufficient and there are still some
// edge cases.
// TODO(crbug.com/390637019): Fully fix and remove this comment.
GetGlicView()->GetWebContents()->Focus();
SetDraggingAreasAndWatchForMouseEvents();
NotifyIfPanelStateChanged();
}
void GlicWindowControllerImpl::SetDraggingAreasAndWatchForMouseEvents() {
if (window_event_observer_) {
return;
}
window_event_observer_ =
std::make_unique<WindowEventObserver>(this, GetGlicView());
if (!draggable_area_) {
// Set the draggable area to the top bar of the window.
GetGlicView()->SetDraggableAreas(
{{0, 0, GetGlicView()->width(), kDraggableAreaHeight}});
}
}
GlicView* GlicWindowControllerImpl::GetGlicView() {
if (!GetGlicWidget()) {
return nullptr;
}
return static_cast<GlicView*>(GetGlicWidget()->GetContentsView());
}
base::WeakPtr<views::View> GlicWindowControllerImpl::GetGlicViewAsView() {
if (auto* view = GetGlicView()) {
return view->GetWeakPtr();
}
return nullptr;
}
GlicWidget* GlicWindowControllerImpl::GetGlicWidget() {
return glic_widget_.get();
}
content::WebContents* GlicWindowControllerImpl::GetFreWebContents() {
return fre_controller_->GetWebContents();
}
gfx::Point GlicWindowControllerImpl::GetTopRightPositionForAttachedGlicWindow(
GlicButton* glic_button) {
// The widget should be placed so its top right corner matches the visible top
// right corner of the glic button.
return glic_button->GetBoundsWithInset().top_right();
}
void GlicWindowControllerImpl::AttachedBrowserDidClose(
BrowserWindowInterface* browser) {
Close();
}
void GlicWindowControllerImpl::Attach() {
if (!GetGlicWidget()) {
return;
}
Browser* browser = glic::FindBrowserForAttachment(profile_);
if (!browser) {
return;
}
MovePositionToBrowserGlicButton(*browser, /*animate=*/true);
if (AlwaysDetached()) {
return;
}
AttachToBrowser(*browser, AttachChangeReason::kMenu);
}
void GlicWindowControllerImpl::Detach() {
if (state_ != State::kOpen || !attached_browser_) {
return;
}
if (!AlwaysDetached()) {
// TODO(crbug.com/410629338): Reimplement attachment.
}
NOTIMPLEMENTED();
}
void GlicWindowControllerImpl::DetachFinished() {
SetWindowState(State::kOpen);
}
void GlicWindowControllerImpl::AttachToBrowser(Browser& browser,
AttachChangeReason reason) {
CHECK(!AlwaysDetached());
CHECK(GetGlicWidget());
attached_browser_ = &browser;
glic_service_->metrics()->OnAttachedToBrowser(reason);
BrowserView* browser_view = browser.window()->AsBrowserView();
CHECK(browser_view);
if (!IsActive()) {
GetGlicWidget()->Activate();
}
NotifyIfPanelStateChanged();
SetGlicWindowToFloatingMode(false);
browser_close_subscription_ = browser.RegisterBrowserDidClose(
base::BindRepeating(&GlicWindowControllerImpl::AttachedBrowserDidClose,
base::Unretained(this)));
// Trigger custom event for testing.
views::ElementTrackerViews::GetInstance()->NotifyCustomEvent(
kGlicWidgetAttached, GetGlicButton(browser));
}
void GlicWindowControllerImpl::Resize(const gfx::Size& size,
base::TimeDelta duration,
base::OnceClosure callback) {
glic_size_ = size;
glic_service_->metrics()->OnGlicWindowResize();
const bool in_resizable_state =
state_ == State::kOpen || state_ == State::kWaitingForGlicToLoad;
// TODO(https://crbug.com/379164689): Drive resize animations for error states
// from the browser. For now, we allow animations during the waiting state.
// TODO(https://crbug.com/392668958): If the widget is ready and asks for a
// resize before the opening animation is finished, we will stop the current
// animation and resize to the final size. Investigate a smoother way to
// animate this transition.
if (in_resizable_state && !user_resizing_) {
glic_window_animator_->AnimateSize(GetLastRequestedSizeClamped(), duration,
std::move(callback));
} else {
// If the glic window is closed, or the widget isn't ready (e.g. because
// it's currently still animating closed) immediately post the callback.
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, std::move(callback));
}
}
void GlicWindowControllerImpl::EnableDragResize(bool enabled) {
user_resizable_ = enabled;
if (!GetGlicWidget()) {
return;
}
if (base::FeatureList::IsEnabled(features::kGlicZOrderChanges)) {
// Drag-resizability implies text mode, which isn't floating, while
// non-resizability implies audio mode, which is floating.
SetGlicWindowToFloatingMode(!enabled);
}
GetGlicWidget()->widget_delegate()->SetCanResize(enabled);
GetGlicView()->UpdateBackgroundColor();
glic_window_animator_->MaybeAnimateToTargetSize();
}
gfx::Size GlicWindowControllerImpl::GetSize() {
if (!GetGlicWidget()) {
return gfx::Size();
}
return GetGlicWidget()->GetSize();
}
void GlicWindowControllerImpl::SetDraggableAreas(
const std::vector<gfx::Rect>& draggable_areas) {
GlicView* glic_view = GetGlicView();
if (!glic_view) {
return;
}
glic_view->SetDraggableAreas(draggable_areas);
}
void GlicWindowControllerImpl::SetMinimumWidgetSize(const gfx::Size& size) {
if (!GetGlicWidget()) {
return;
}
glic_widget_->SetMinimumSize(size);
}
void GlicWindowControllerImpl::CloseWithReason(
views::Widget::ClosedReason reason) {
Close();
}
void GlicWindowControllerImpl::Close() {
if (state_ == State::kClosed) {
return;
}
// Save the widge position on close so we can restore in the same position.
SaveWidgetPosition();
glic_window_animator_.reset();
glic_service_->metrics()->OnGlicWindowClose();
base::UmaHistogramEnumeration("Glic.PanelWebUiState.FinishState2",
host().GetPrimaryWebUiState());
SetWindowState(State::kClosed);
attached_browser_ = nullptr;
draggable_area_ = std::nullopt;
window_event_observer_.reset();
browser_close_subscription_.reset();
glic_window_hotkey_manager_.reset();
glic_widget_observation_.Reset();
glic_widget_.reset();
scoped_glic_button_indicator_.reset();
user_resizing_ = false;
NotifyIfPanelStateChanged();
window_activation_callback_list_.Notify(false);
modal_dialog_host_observers_.Notify(
&web_modal::ModalDialogHostObserver::OnHostDestroying);
web_modal::WebContentsModalDialogManager::FromWebContents(
host().webui_contents())
->SetDelegate(nullptr);
host().PanelWasClosed();
if (base::FeatureList::IsEnabled(features::kGlicUnloadOnClose)) {
host().Shutdown();
}
}
void GlicWindowControllerImpl::SaveWidgetPosition() {
if (GetGlicWidget() && GetGlicWidget()->IsVisible()) {
previous_position_ =
GetGlicWidget()->GetClientAreaBoundsInScreen().origin();
profile_->GetPrefs()->SetInteger(prefs::kGlicPreviousPositionX,
previous_position_->x());
profile_->GetPrefs()->SetInteger(prefs::kGlicPreviousPositionY,
previous_position_->y());
}
}
void GlicWindowControllerImpl::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)
}
bool GlicWindowControllerImpl::ShouldStartDrag(
const gfx::Point& initial_press_loc,
const gfx::Point& mouse_location) {
// Determine if the mouse has moved beyond a minimum elasticity distance
// in any direction from the starting point.
static const int kMinimumDragDistance = 10;
int x_offset = abs(mouse_location.x() - initial_press_loc.x());
int y_offset = abs(mouse_location.y() - initial_press_loc.y());
return sqrt(pow(static_cast<float>(x_offset), 2) +
pow(static_cast<float>(y_offset), 2)) > kMinimumDragDistance;
}
void GlicWindowControllerImpl::HandleWindowDragWithOffset(
gfx::Vector2d mouse_offset) {
// This code isn't set up to handle nested run loops. Nested run loops will
// lead to crashes.
if (!in_move_loop_) {
in_move_loop_ = true;
#if BUILDFLAG(IS_MAC)
GetGlicWidget()->SetCapture(nullptr);
#endif
const views::Widget::MoveLoopSource move_loop_source =
views::Widget::MoveLoopSource::kMouse;
if (!AlwaysDetached()) {
// Set glic to a floating z-order while dragging so browsers brought into
// focus by HandleGlicButtonIndicator won't show in front of glic.
GetGlicWidget()->SetZOrderLevel(ui::ZOrderLevel::kFloatingWindow);
}
GetGlicWidget()->RunMoveLoop(
mouse_offset, move_loop_source,
views::Widget::MoveLoopEscapeBehavior::kDontHide);
in_move_loop_ = false;
scoped_glic_button_indicator_.reset();
// Only handle positioning if glic wasn't closed during the drag.
if (state_ == State::kClosed) {
return;
}
// Dragging stops animations. This makes sure we honor the last resize
// request.
glic_window_animator_->MaybeAnimateToTargetSize();
AdjustPositionIfNeeded();
SaveWidgetPosition();
if (!AlwaysDetached()) {
// set glic z-order back to normal after drag is done.
GetGlicWidget()->SetZOrderLevel(ui::ZOrderLevel::kNormal);
// Check whether `GetGlicWidget()` is in a position to attach to a
// browser window.
OnDragComplete();
}
}
}
const mojom::PanelState& GlicWindowControllerImpl::GetPanelState() const {
return panel_state_;
}
void GlicWindowControllerImpl::AdjustPositionIfNeeded() {
// Always have at least `kMinimumVisible` px visible from glic window in
// both vertical and horizontal directions.
constexpr int kMinimumVisible = 40;
const auto widget_size = GetGlicWidget()->GetSize();
const int horizontal_buffer = widget_size.width() - kMinimumVisible;
const int vertical_buffer = widget_size.height() - kMinimumVisible;
// Adjust bounds of visible area screen to allow part of glic to go off
// screen.
auto workarea = GetGlicWidget()->GetWorkAreaBoundsInScreen();
workarea.Outset(gfx::Outsets::VH(vertical_buffer, horizontal_buffer));
auto rect = GetGlicWidget()->GetRestoredBounds();
rect.AdjustToFit(workarea);
GetGlicWidget()->SetBounds(rect);
}
void GlicWindowControllerImpl::OnDragComplete() {
Browser* browser = FindBrowserForAttachment();
// No browser within attachment range.
if (!browser) {
return;
}
// Attach to the found browser.
MovePositionToBrowserGlicButton(*browser, /*animate=*/true);
AttachToBrowser(*browser, AttachChangeReason::kDrag);
}
void GlicWindowControllerImpl::HandleGlicButtonIndicator() {
Browser* browser = FindBrowserForAttachment();
// No browser within attachment range so reset indicators
if (!browser) {
scoped_glic_button_indicator_.reset();
return;
}
GlicButton* glic_button = GetGlicButton(*browser);
// If there isn't an existing scoped indicator for this button, create one.
if (!scoped_glic_button_indicator_ ||
scoped_glic_button_indicator_->GetGlicButton() != glic_button) {
// Bring the browser to the front.
browser->GetBrowserView().GetWidget()->Activate();
scoped_glic_button_indicator_ =
std::make_unique<ScopedGlicButtonIndicator>(glic_button);
}
}
Browser* GlicWindowControllerImpl::FindBrowserForAttachment() {
// The profile must have started off as Glic enabled since a Glic widget is
// open but it may have been disabled at runtime by policy. In this edge-case,
// prevent reattaching back to a window (as it no longer has a GlicButton).
if (!GlicEnabling::IsEnabledForProfile(profile_)) {
return nullptr;
}
gfx::Point glic_top_right =
GetGlicWidget()->GetWindowBoundsInScreen().top_right();
// Loops through all browsers in activation order with the latest accessed
// browser first.
for (Browser* browser : BrowserList::GetInstance()->OrderedByActivation()) {
if (!IsBrowserGlicAttachable(profile_, browser)) {
continue;
}
// If the profile is enabled, the Glic button must be available.
auto* tab_strip_region_view =
browser->GetBrowserView().tab_strip_region_view();
CHECK(tab_strip_region_view);
CHECK(tab_strip_region_view->GetGlicButton());
// Define attachment zone as the right of the tab strip. It either is the
// width of the widget or 1/3 of the tab strip, whichever is smaller.
gfx::Rect attachment_zone = tab_strip_region_view->GetBoundsInScreen();
int width = std::min(attachment_zone.width() / 3,
GlicWidget::GetInitialSize().width());
attachment_zone.SetByBounds(attachment_zone.right() - width,
attachment_zone.y() - kAttachmentBuffer,
attachment_zone.right() + kAttachmentBuffer,
attachment_zone.bottom());
// If both the left center of the attachment zone and glic button right
// center are occluded, don't consider for attachment.
if (IsBrowserOccludedAtPoint(browser, attachment_zone.left_center()) &&
IsBrowserOccludedAtPoint(browser, tab_strip_region_view->GetGlicButton()
->GetBoundsInScreen()
.right_center())) {
continue;
}
if (attachment_zone.Contains(glic_top_right)) {
return browser;
}
}
// No browser found near glic.
return nullptr;
}
void GlicWindowControllerImpl::MovePositionToBrowserGlicButton(
const Browser& browser,
bool animate) {
if (!GetGlicWidget()) {
return;
}
// If the profile's been disabled (e.g. by policy) the window's Glic button
// will be removed so we can't anchor to it. We could work around this by
// keeping the button but disabling and making it invisible but this is an
// edge-case, not sure it's worth the effort.
if (!GlicEnabling::IsEnabledForProfile(browser.profile())) {
return;
}
if (AlwaysDetached() && !IsActive()) {
GetGlicWidget()->Activate();
}
// This will stack with a move animation below.
MaybeAdjustSizeForDisplay(animate);
GlicButton* glic_button = GetGlicButton(browser);
CHECK(glic_button);
gfx::Point window_target_top_left;
if (AlwaysDetached()) {
gfx::Point button_bottom_left =
glic_button->GetBoundsWithInset().bottom_left();
window_target_top_left =
gfx::Point(button_bottom_left.x() -
GetGlicWidget()->GetWindowBoundsInScreen().width(),
button_bottom_left.y());
} else {
gfx::Point top_right =
GetTopRightPositionForAttachedGlicWindow(glic_button);
window_target_top_left = gfx::Point(
top_right.x() - GetGlicWidget()->GetWindowBoundsInScreen().width(),
top_right.y());
}
// Avoid conversions between pixels and DIP on non 1.0 scale factor displays
// changing widget width and height.
base::TimeDelta duration =
animate ? kAnimationDuration : base::Milliseconds(0);
glic_window_animator_->AnimatePosition(window_target_top_left, duration,
base::DoNothing());
NotifyIfPanelStateChanged();
}
void GlicWindowControllerImpl::AddStateObserver(StateObserver* observer) {
state_observers_.AddObserver(observer);
}
void GlicWindowControllerImpl::RemoveStateObserver(StateObserver* observer) {
state_observers_.RemoveObserver(observer);
}
void GlicWindowControllerImpl::NotifyIfPanelStateChanged() {
auto new_state = ComputePanelState();
if (new_state != panel_state_) {
panel_state_ = new_state;
state_observers_.Notify(&StateObserver::PanelStateChanged, panel_state_,
attached_browser_);
}
}
mojom::PanelState GlicWindowControllerImpl::ComputePanelState() const {
return CreatePanelState(IsShowing(), attached_browser_);
}
bool GlicWindowControllerImpl::IsActive() {
return IsShowing() && GetGlicWidget() && GetGlicWidget()->IsActive();
}
bool GlicWindowControllerImpl::IsShowing() const {
return !(state_ == State::kClosed);
}
bool GlicWindowControllerImpl::IsPanelOrFreShowing() const {
return IsShowing() || fre_controller_->IsShowingDialog();
}
bool GlicWindowControllerImpl::IsAttached() const {
return attached_browser_ != nullptr;
}
bool GlicWindowControllerImpl::IsDetached() const {
return IsShowing() && !IsAttached();
}
base::CallbackListSubscription
GlicWindowControllerImpl::AddWindowActivationChangedCallback(
WindowActivationChangedCallback callback) {
return window_activation_callback_list_.Add(std::move(callback));
}
void GlicWindowControllerImpl::Preload() {
if (!host().contents_container()) {
host().CreateContents();
host().webui_contents()->Resize(GetInitialBounds(nullptr));
}
}
void GlicWindowControllerImpl::PreloadFre() {
if (fre_controller_->ShouldShowFreDialog()) {
fre_controller_->TryPreload();
}
}
void GlicWindowControllerImpl::Reload() {
if (GetFreWebContents()) {
GetFreWebContents()->GetController().Reload(
content::ReloadType::BYPASSING_CACHE,
/*check_for_repost=*/false);
}
if (auto* webui_contents = host().webui_contents()) {
webui_contents->GetController().Reload(content::ReloadType::BYPASSING_CACHE,
/*check_for_repost=*/false);
}
}
bool GlicWindowControllerImpl::IsWarmed() const {
return !!host().contents_container();
}
base::WeakPtr<GlicWindowController> GlicWindowControllerImpl::GetWeakPtr() {
return weak_ptr_factory_.GetWeakPtr();
}
void GlicWindowControllerImpl::Shutdown() {
// Hide first, then clean up (but do not animate).
Close();
fre_controller_->Shutdown();
window_activation_callback_list_.Notify(false);
}
bool GlicWindowControllerImpl::IsBrowserOccludedAtPoint(Browser* browser,
gfx::Point point) {
std::set<gfx::NativeWindow> exclude = {
GetGlicView()->GetWidget()->GetNativeWindow()};
gfx::NativeWindow window =
window_finder_->GetLocalProcessWindowAtPoint(point, exclude);
if (browser->GetBrowserView().GetWidget()->GetNativeWindow() != window) {
return true;
}
return false;
}
gfx::Size GlicWindowControllerImpl::GetLastRequestedSizeClamped() const {
gfx::Size min = GlicWidget::GetInitialSize();
if (glic_widget_) {
gfx::Size widget_min = glic_widget_->GetMinimumSize();
if (!widget_min.IsEmpty()) {
min = widget_min;
}
}
gfx::Size max(kMaxWidgetSize, kMaxWidgetSize);
gfx::Size result = glic_size_.value_or(min);
result.SetToMax(min);
result.SetToMin(max);
return result;
}
void GlicWindowControllerImpl::MaybeAdjustSizeForDisplay(bool animate) {
if (state_ == State::kOpen || state_ == State::kWaitingForGlicToLoad) {
const auto target_size = GetLastRequestedSizeClamped();
if (target_size != glic_window_animator_->GetCurrentTargetBounds().size()) {
glic_window_animator_->AnimateSize(
target_size, animate ? kAnimationDuration : base::Milliseconds(0),
base::DoNothing());
}
}
}
void GlicWindowControllerImpl::SetWindowState(State new_state) {
if (state_ == new_state) {
return;
}
state_ = new_state;
if (IsWindowOpenAndReady()) {
glic_service_->metrics()->OnGlicWindowOpenAndReady();
}
}
bool GlicWindowControllerImpl::IsWindowOpenAndReady() {
return host().IsReady() && state_ == State::kOpen;
}
GlicWindowController::State GlicWindowControllerImpl::state() const {
return state_;
}
bool GlicWindowControllerImpl::IsDragging() {
return in_move_loop_;
}
Profile* GlicWindowControllerImpl::profile() {
return profile_;
}
GlicWindowAnimator* GlicWindowControllerImpl::window_animator() {
return glic_window_animator_.get();
}
GlicFreController* GlicWindowControllerImpl::fre_controller() {
return fre_controller_.get();
}
// Return the Browser to which the panel is attached, or null if detached.
Browser* GlicWindowControllerImpl::attached_browser() {
return attached_browser_;
}
web_modal::WebContentsModalDialogHost*
GlicWindowControllerImpl::GetWebContentsModalDialogHost() {
return this;
}
gfx::Size GlicWindowControllerImpl::GetMaximumDialogSize() {
if (!glic_widget_) {
return gfx::Size();
}
return glic_widget_->GetClientAreaBoundsInScreen().size();
}
gfx::NativeView GlicWindowControllerImpl::GetHostView() const {
if (!glic_widget_) {
return gfx::NativeView();
}
return glic_widget_->GetNativeView();
}
gfx::Point GlicWindowControllerImpl::GetDialogPosition(
const gfx::Size& dialog_size) {
if (!glic_widget_) {
return gfx::Point();
}
gfx::Rect client_area_bounds = glic_widget_->GetClientAreaBoundsInScreen();
return gfx::Point((client_area_bounds.width() - dialog_size.width()) / 2, 0);
}
bool GlicWindowControllerImpl::ShouldDialogBoundsConstrainedByHost() {
// 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 GlicWindowControllerImpl::AddObserver(
web_modal::ModalDialogHostObserver* observer) {
modal_dialog_host_observers_.AddObserver(observer);
}
void GlicWindowControllerImpl::RemoveObserver(
web_modal::ModalDialogHostObserver* observer) {
modal_dialog_host_observers_.RemoveObserver(observer);
}
} // namespace glic