blob: d13cb6f579cd8be68572bd5bdf34ea741b773ab8 [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/host/context/glic_focused_tab_manager.h"
#include <optional>
#include "base/callback_list.h"
#include "base/functional/bind.h"
#include "chrome/browser/glic/host/context/glic_focused_browser_manager.h"
#include "chrome/browser/glic/host/context/glic_sharing_utils.h"
#include "chrome/browser/glic/host/context/glic_tab_data.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_finder.h"
#include "chrome/browser/ui/browser_list.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/browser_window/public/desktop_browser_window_capabilities.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/common/webui_url_constants.h"
#include "content/public/common/url_constants.h"
#include "ui/base/base_window.h"
#include "ui/views/widget/widget.h"
#if BUILDFLAG(IS_MAC)
#include "ui/base/cocoa/appkit_utils.h"
#endif
namespace glic {
namespace {
// Returns whether `a` and `b` both point to the same object.
// Note that if both `a` and `b` are invalidated, this returns true, even if
// the object they once pointed to is different. For our purposes, this is OK.
// This code helps address focus state changes from an old state that's since
// been invalidated to a new state that is now nullptr (we want to treat this
// as a "focus changed" scenario and notify).
template <typename T>
bool IsWeakPtrSame(const base::WeakPtr<T>& a, const base::WeakPtr<T>& b) {
return std::make_pair(a.get(), a.WasInvalidated()) ==
std::make_pair(b.get(), b.WasInvalidated());
}
} // namespace
GlicFocusedTabManager::GlicFocusedTabManager(
GlicFocusedBrowserManager* focused_browser_manager)
: focused_browser_manager_(focused_browser_manager) {
focused_browser_subscription_ =
focused_browser_manager_->AddFocusedBrowserChangedCallback(
base::BindRepeating(&GlicFocusedTabManager::OnFocusedBrowserChanged,
base::Unretained(this)));
}
GlicFocusedTabManager::~GlicFocusedTabManager() = default;
base::CallbackListSubscription
GlicFocusedTabManager::AddFocusedTabChangedCallback(
FocusedTabChangedCallback callback) {
return focused_callback_list_.Add(std::move(callback));
}
base::CallbackListSubscription
GlicFocusedTabManager::AddFocusedTabInstanceChangedCallback(
FocusedTabInstanceChangedCallback callback) {
return focused_instance_callback_list_.Add(std::move(callback));
}
base::CallbackListSubscription
GlicFocusedTabManager::AddFocusedTabOrCandidateInstanceChangedCallback(
FocusedTabOrCandidateInstanceChangedCallback callback) {
return focused_or_candidate_instance_callback_list_.Add(std::move(callback));
}
base::CallbackListSubscription
GlicFocusedTabManager::AddFocusedTabDataChangedCallback(
FocusedTabDataChangedCallback callback) {
return focused_data_callback_list_.Add(std::move(callback));
}
void GlicFocusedTabManager::OnFocusedBrowserChanged(
BrowserWindowInterface* candidate,
BrowserWindowInterface* focused) {
BrowserWindowInterface* previously_subscribed = subscribed_browser_.get();
if (previously_subscribed) {
previously_subscribed->GetTabStripModel()->RemoveObserver(this);
}
if (focused) {
focused->GetTabStripModel()->AddObserver(this);
subscribed_browser_ = focused->GetWeakPtr();
active_tab_subscription_ = focused->RegisterActiveTabDidChange(
base::BindRepeating(&GlicFocusedTabManager::OnActiveTabChanged,
base::Unretained(this)));
} else {
subscribed_browser_.reset();
active_tab_subscription_ = base::CallbackListSubscription();
}
// We need to force-notify because even if the focused tab doesn't change, it
// can be in a different browser window (i.e., the user drag-n-drop the
// focused tab into a new window). Let the subscribers to decide what to do in
// this case.
//
// TODO(crbug.com/393578218): We should have dedicated subscription lists for
// different types of notifications.
MaybeUpdateFocusedTab(/*force_notify=*/true);
}
void GlicFocusedTabManager::OnActiveTabChanged(
BrowserWindowInterface* browser) {
MaybeUpdateFocusedTab();
}
void GlicFocusedTabManager::OnSplitTabChanged(const SplitTabChange& change) {
if (change.type == SplitTabChange::Type::kContentsChanged) {
MaybeUpdateFocusedTab(/*force_notify=*/true);
}
}
void GlicFocusedTabManager::PrimaryPageChanged(content::Page& page) {
// We always want to trigger our notify callback here (even if focused tab
// remains the same) so that subscribers can update if they care about primary
// page changed events.
MaybeUpdateFocusedTab(/*force_notify=*/true);
}
void GlicFocusedTabManager::FocusedTabDataChanged(TabDataChange change) {
// `TabDataObserver` is responsible for firing this when appropriate, we just
// forward events along.
// Note: we omit calling `MaybeUpdateFocusedTab()` here because observing web
// contents for changes that might impact focused tab container or candidate
// are handled separately.
NotifyFocusedTabDataChanged(std::move(change));
}
void GlicFocusedTabManager::MaybeUpdateFocusedTab(bool force_notify) {
struct FocusedTabState new_focused_tab_state = ComputeFocusedTabState();
bool focus_changed = !focused_tab_state_.IsSame(new_focused_tab_state);
bool focused_instance_changed = !IsWeakPtrSame(
focused_tab_state_.focused_tab, new_focused_tab_state.focused_tab);
bool focused_or_candidate_instance_changed =
focused_instance_changed ||
!IsWeakPtrSame(focused_tab_state_.candidate_tab,
new_focused_tab_state.candidate_tab);
if (focus_changed) {
focused_tab_state_ = new_focused_tab_state;
focused_tab_data_ = GetFocusedTabData(new_focused_tab_state);
}
// If we have one, observe tab candidate. If not, whether that's because there
// was never one, or because it's been invalidated, turn off tab candidate
// observation.
Observe(focused_tab_state_.candidate_tab.get());
// Similarly set up or turn off tab data observation for the focused tab.
focused_tab_data_observer_ = std::make_unique<TabDataObserver>(
focused_tab_state_.focused_tab.get(),
base::BindRepeating(&GlicFocusedTabManager::FocusedTabDataChanged,
base::Unretained(this)));
if (focused_instance_changed) {
NotifyFocusedTabInstanceChanged(focused_tab_state_.focused_tab.get());
NotifyFocusedTabDataChanged(
{{TabDataChangeCause::kTabChanged},
CreateTabData(focused_tab_state_.focused_tab.get())});
}
if (focused_or_candidate_instance_changed) {
NotifyFocusedTabOrCandidateInstanceChanged(ImplToPublic(focused_tab_data_));
}
if (focus_changed || force_notify) {
NotifyFocusedTabChanged();
}
}
struct GlicFocusedTabManager::FocusedTabState
GlicFocusedTabManager::ComputeFocusedTabState() {
struct FocusedTabState focused_tab_state = FocusedTabState();
BrowserWindowInterface* candidate_browser =
focused_browser_manager_->GetCandidateBrowser();
if (!candidate_browser) {
return focused_tab_state;
}
focused_tab_state.candidate_browser = candidate_browser->GetWeakPtr();
BrowserWindowInterface* focused_browser =
focused_browser_manager_->GetFocusedBrowser();
if (focused_browser) {
focused_tab_state.focused_browser = focused_browser->GetWeakPtr();
CHECK_EQ(focused_browser, candidate_browser);
}
content::WebContents* active_contents =
candidate_browser->GetActiveTabInterface()
? candidate_browser->GetActiveTabInterface()->GetContents()
: nullptr;
if (active_contents) {
focused_tab_state.candidate_tab = active_contents->GetWeakPtr();
}
if (candidate_browser == focused_browser && active_contents &&
IsTabValidForSharing(active_contents)) {
focused_tab_state.focused_tab = focused_tab_state.candidate_tab;
}
return focused_tab_state;
}
void GlicFocusedTabManager::NotifyFocusedTabChanged() {
focused_callback_list_.Notify(GetFocusedTabData());
}
void GlicFocusedTabManager::NotifyFocusedTabInstanceChanged(
content::WebContents* web_contents) {
focused_instance_callback_list_.Notify(web_contents);
}
void GlicFocusedTabManager::NotifyFocusedTabOrCandidateInstanceChanged(
const FocusedTabData& focused_tab_data) {
focused_or_candidate_instance_callback_list_.Notify(focused_tab_data);
}
void GlicFocusedTabManager::NotifyFocusedTabDataChanged(TabDataChange change) {
focused_data_callback_list_.Notify(change.tab_data.get());
}
bool GlicFocusedTabManager::IsTabFocused(tabs::TabHandle tab_handle) const {
auto* tab = tab_handle.Get();
if (!tab) {
return false;
}
content::WebContents* web_contents = focused_tab_data_.focus();
if (!web_contents) {
return false;
}
return tab->GetContents() == web_contents;
}
GlicFocusedTabManager::FocusedTabDataImpl
GlicFocusedTabManager::GetFocusedTabData(
const GlicFocusedTabManager::FocusedTabState& focused_state) {
if (focused_state.focused_tab) {
return FocusedTabDataImpl(focused_state.focused_tab);
}
if (focused_state.candidate_tab) {
return FocusedTabDataImpl(NoFocusedTabData(
"no focusable tab", focused_state.candidate_tab.get()));
}
if (focused_state.focused_browser) {
return FocusedTabDataImpl(NoFocusedTabData("no focusable tab"));
}
if (focused_state.candidate_browser) {
return FocusedTabDataImpl(NoFocusedTabData("no focusable browser window"));
}
return FocusedTabDataImpl(NoFocusedTabData("no browser window"));
}
FocusedTabData GlicFocusedTabManager::GetFocusedTabData() {
return ImplToPublic(focused_tab_data_);
}
FocusedTabData GlicFocusedTabManager::ImplToPublic(FocusedTabDataImpl impl) {
if (impl.is_focus()) {
content::WebContents* contents = impl.focus();
if (!contents) {
return FocusedTabData(std::string("focused tab disappeared"),
/*unfocused_tab=*/nullptr);
}
return FocusedTabData(tabs::TabInterface::GetFromContents(contents));
}
const NoFocusedTabData* no_focus = impl.no_focus();
CHECK(no_focus);
content::WebContents* contents = no_focus->active_tab.get();
tabs::TabInterface* tab =
contents ? tabs::TabInterface::GetFromContents(contents) : nullptr;
return FocusedTabData(std::string(no_focus->no_focus_reason), tab);
}
GlicFocusedTabManager::FocusedTabState::FocusedTabState() = default;
GlicFocusedTabManager::FocusedTabState::~FocusedTabState() = default;
GlicFocusedTabManager::FocusedTabState::FocusedTabState(
const GlicFocusedTabManager::FocusedTabState& src) = default;
GlicFocusedTabManager::FocusedTabState&
GlicFocusedTabManager::FocusedTabState::operator=(
const GlicFocusedTabManager::FocusedTabState& src) = default;
bool GlicFocusedTabManager::FocusedTabState::IsSame(
const FocusedTabState& other) const {
return IsWeakPtrSame(candidate_browser, other.candidate_browser) &&
IsWeakPtrSame(focused_browser, other.focused_browser) &&
IsWeakPtrSame(candidate_tab, other.candidate_tab) &&
IsWeakPtrSame(focused_tab, other.focused_tab);
}
GlicFocusedTabManager::FocusedTabDataImpl::FocusedTabDataImpl(
base::WeakPtr<content::WebContents> contents)
: data_(std::move(contents)) {}
GlicFocusedTabManager::FocusedTabDataImpl::FocusedTabDataImpl(
const NoFocusedTabData& no_focused_tab_data)
: data_(no_focused_tab_data) {}
GlicFocusedTabManager::FocusedTabDataImpl::~FocusedTabDataImpl() = default;
GlicFocusedTabManager::FocusedTabDataImpl::FocusedTabDataImpl(
const FocusedTabDataImpl& other) = default;
bool GlicFocusedTabManager::FocusedTabDataImpl::IsSame(
const FocusedTabDataImpl& new_data) const {
if (data_.index() != new_data.data_.index()) {
return false;
}
switch (data_.index()) {
case 0:
return IsWeakPtrSame(std::get<0>(data_), std::get<0>(new_data.data_));
case 1:
return std::get<1>(data_).IsSame(std::get<1>(new_data.data_));
}
NOTREACHED();
}
bool GlicFocusedTabManager::NoFocusedTabData::IsSame(
const NoFocusedTabData& other) const {
return IsWeakPtrSame(active_tab, other.active_tab) &&
no_focus_reason == other.no_focus_reason;
}
GlicFocusedTabManager::NoFocusedTabData::NoFocusedTabData() = default;
GlicFocusedTabManager::NoFocusedTabData::NoFocusedTabData(
std::string_view reason,
content::WebContents* tab)
: active_tab(tab ? tab->GetWeakPtr() : nullptr), no_focus_reason(reason) {}
GlicFocusedTabManager::NoFocusedTabData::~NoFocusedTabData() = default;
GlicFocusedTabManager::NoFocusedTabData::NoFocusedTabData(
const NoFocusedTabData& other) = default;
GlicFocusedTabManager::NoFocusedTabData&
GlicFocusedTabManager::NoFocusedTabData::operator=(
const NoFocusedTabData& other) = default;
} // namespace glic