blob: 52a6d214c2709e8058fc5734e61d52f424819702 [file] [log] [blame]
// Copyright 2021 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/views/tabs/tab_hover_card_controller.h"
#include <algorithm>
#include <optional>
#include "base/callback_list.h"
#include "base/check_is_test.h"
#include "base/command_line.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/memory/memory_pressure_monitor.h"
#include "base/memory/raw_ptr.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/ui/omnibox/omnibox_edit_model.h"
#include "chrome/browser/ui/omnibox/omnibox_popup_view.h"
#include "chrome/browser/ui/tabs/tab_style.h"
#include "chrome/browser/ui/ui_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/location_bar/location_bar_view.h"
#include "chrome/browser/ui/views/omnibox/omnibox_popup_view_views.h"
#include "chrome/browser/ui/views/omnibox/omnibox_view_views.h"
#include "chrome/browser/ui/views/tabs/tab_hover_card_bubble_view.h"
#include "chrome/browser/ui/views/tabs/tab_hover_card_thumbnail_observer.h"
#include "chrome/browser/ui/views/tabs/tab_strip.h"
#include "chrome/browser/ui/views/tabs/tab_strip_controller.h"
#include "chrome/browser/ui/web_applications/app_browser_controller.h"
#include "chrome/common/pref_names.h"
#include "components/user_education/common/help_bubble/help_bubble_factory_registry.h"
#include "components/user_education/views/help_bubble_factory_views.h"
#include "components/user_education/views/help_bubble_view.h"
#include "ui/events/event.h"
#include "ui/events/event_observer.h"
#include "ui/events/types/event_type.h"
#include "ui/views/event_monitor.h"
#include "ui/views/view.h"
#include "ui/views/widget/widget.h"
#include "ui/views/widget/widget_observer.h"
namespace {
constexpr base::TimeDelta kMemoryPressureCaptureDelay = base::Milliseconds(500);
base::TimeDelta GetPreviewImageCaptureDelay(
ThumbnailImage::CaptureReadiness readiness) {
int ms = 0;
switch (readiness) {
case ThumbnailImage::CaptureReadiness::kNotReady: {
static const int not_ready_delay = base::GetFieldTrialParamByFeatureAsInt(
features::kTabHoverCardImages,
features::kTabHoverCardImagesNotReadyDelayParameterName, 800);
ms = not_ready_delay;
break;
}
case ThumbnailImage::CaptureReadiness::kReadyForInitialCapture: {
static const int loading_delay = base::GetFieldTrialParamByFeatureAsInt(
features::kTabHoverCardImages,
features::kTabHoverCardImagesLoadingDelayParameterName, 300);
ms = loading_delay;
break;
}
case ThumbnailImage::CaptureReadiness::kReadyForFinalCapture: {
static const int loaded_delay = base::GetFieldTrialParamByFeatureAsInt(
features::kTabHoverCardImages,
features::kTabHoverCardImagesLoadedDelayParameterName, 300);
ms = loaded_delay;
break;
}
}
DCHECK_GE(ms, 0);
return base::Milliseconds(ms);
}
base::TimeDelta GetShowDelay(int tab_width) {
const TabStyle* tab_style = TabStyle::Get();
static const int max_width_additional_delay =
base::GetFieldTrialParamByFeatureAsInt(
features::kTabHoverCardImages,
features::kTabHoverCardAdditionalMaxWidthDelay, 500);
// Delay is calculated as a logarithmic scale and bounded by a minimum width
// based on the width of a pinned tab and a maximum of the standard width.
//
// delay (ms)
// |
// max delay-| *
// | *
// | *
// | *
// | *
// | *
// | *
// | *
// | *
// min delay-|****
// |___________________________________________ tab width
// | |
// pinned tab width standard tab width
constexpr base::TimeDelta kMinimumTriggerDelay = base::Milliseconds(300);
const int tab_pinned_width = tab_style->GetPinnedWidth(/*is_split=*/false);
const int tab_standard_width =
tab_style->GetStandardWidth(/*is_split=*/false);
if (tab_width < tab_pinned_width) {
return kMinimumTriggerDelay;
}
constexpr base::TimeDelta kMaximumTriggerDelay = base::Milliseconds(800);
double logarithmic_fraction =
std::log(tab_width - tab_pinned_width + 1) /
std::log(tab_standard_width - tab_pinned_width + 1);
base::TimeDelta scaling_factor = kMaximumTriggerDelay - kMinimumTriggerDelay;
base::TimeDelta delay =
logarithmic_fraction * scaling_factor + kMinimumTriggerDelay;
if (tab_width >= tab_standard_width) {
delay += base::Milliseconds(max_width_additional_delay);
}
return delay;
}
bool IsBrowserForSystemWebApp(const Browser* browser) {
#if BUILDFLAG(IS_CHROMEOS)
CHECK(browser);
const auto* const app_controller = browser->app_controller();
if (app_controller && app_controller->system_app()) {
return true;
}
#endif
return false;
}
} // anonymous namespace
//-------------------------------------------------------------------
// TabHoverCardController::EventSniffer
// Listens in on the browser event stream and hides an associated hover card
// on any keypress, mouse click, or gesture.
class TabHoverCardController::EventSniffer : public ui::EventObserver {
public:
explicit EventSniffer(TabHoverCardController* controller)
: controller_(controller) {
// Note that null is a valid value for the second parameter here; if for
// some reason there is no native window it simply falls back to
// application-wide event-sniffing, which for this case is better than not
// watching events at all.
event_monitor_ = views::EventMonitor::CreateWindowMonitor(
this,
controller_->tab_strip_->GetWidget()
->GetTopLevelWidget()
->GetNativeWindow(),
{ui::EventType::kKeyPressed, ui::EventType::kKeyReleased,
ui::EventType::kMousePressed, ui::EventType::kMouseReleased,
ui::EventType::kGestureBegin, ui::EventType::kGestureEnd});
}
~EventSniffer() override = default;
protected:
// ui::EventObserver:
void OnEvent(const ui::Event& event) override {
bool close_hover_card = true;
if (event.IsKeyEvent()) {
// Hover card needs to be dismissed (and regenerated) if the keypress
// would select the tab (this also takes focus out of the tabstrip).
close_hover_card = event.AsKeyEvent()->key_code() == ui::VKEY_RETURN ||
event.AsKeyEvent()->key_code() == ui::VKEY_ESCAPE ||
!controller_->tab_strip_->IsFocusInTabs();
}
if (close_hover_card) {
controller_->UpdateHoverCard(
nullptr, TabSlotController::HoverCardUpdateType::kEvent);
}
}
private:
const raw_ptr<TabHoverCardController> controller_;
std::unique_ptr<views::EventMonitor> event_monitor_;
};
//-------------------------------------------------------------------
// TabHoverCardController
// static
bool TabHoverCardController::disable_animations_for_testing_ = false;
TabHoverCardController::TabHoverCardController(TabStrip* tab_strip)
: tab_strip_(tab_strip),
tab_resource_usage_collector_(TabResourceUsageCollector::Get()) {
if (PrefService* pref_service = g_browser_process->local_state()) {
// Hovercard image previews are still not fully rolled out to all platforms
// so we default the pref to the state of the feature rollout.
pref_service->SetDefaultPrefValue(prefs::kHoverCardImagesEnabled,
base::Value(base::FeatureList::IsEnabled(
features::kTabHoverCardImages)));
pref_change_registrar_.Init(pref_service);
// Register for previews enabled pref change events.
hover_card_image_previews_enabled_ = AreHoverCardImagesEnabled();
pref_change_registrar_.Add(
prefs::kHoverCardImagesEnabled,
base::BindRepeating(
&TabHoverCardController::OnHovercardImagesEnabledChanged,
base::Unretained(this)));
// Register for memory usage enabled pref change events. Exclude
// tracking them for system web apps (e.g. ChromeOS terminal app).
Browser* browser = tab_strip_->GetBrowser();
if (!browser) {
CHECK_IS_TEST();
} else if (!IsBrowserForSystemWebApp(browser)) {
OnHovercardMemoryUsageEnabledChanged();
pref_change_registrar_.Add(
prefs::kHoverCardMemoryUsageEnabled,
base::BindRepeating(
&TabHoverCardController::OnHovercardMemoryUsageEnabledChanged,
base::Unretained(this)));
}
}
}
TabHoverCardController::~TabHoverCardController() = default;
// static
bool TabHoverCardController::AreHoverCardImagesEnabled() {
if (base::FeatureList::IsEnabled(features::kTabHoverCardImages)) {
PrefService* pref_service = g_browser_process->local_state();
return pref_service->GetBoolean(prefs::kHoverCardImagesEnabled);
}
return false;
}
// static
bool TabHoverCardController::UseAnimations() {
return !disable_animations_for_testing_ &&
gfx::Animation::ShouldRenderRichAnimation();
}
bool TabHoverCardController::IsHoverCardVisible() const {
return hover_card_ && GetCardWidget() && !GetCardWidget()->IsClosed();
}
bool TabHoverCardController::IsHoverCardShowingForTab(Tab* tab) const {
return IsHoverCardVisible() && !fade_animator_->IsFadingOut() &&
GetTargetAnchorView() == tab;
}
void TabHoverCardController::UpdateHoverCard(
Tab* tab,
TabSlotController::HoverCardUpdateType update_type) {
// Never display a hover card for a closing tab.
if (tab && tab->closing()) {
tab = nullptr;
}
// Update this ASAP so that if we try to fade-in and we have the wrong target
// then when the fade timer elapses we won't incorrectly try to fade in on the
// wrong tab.
if (target_tab_ != tab) {
delayed_show_timer_.Stop();
target_tab_observation_.Reset();
if (tab) {
target_tab_observation_.Observe(tab);
}
target_tab_ = tab;
}
// If there's nothing to attach to then there's no point in creating a card.
// Note that this includes a check for whether the tab strip widget is
// visible (see crbug.com/454057267).
if (!hover_card_ && (!tab || !tab_strip_->GetWidget() ||
!tab_strip_->GetWidget()->IsVisibleOnScreen())) {
return;
}
switch (update_type) {
case TabSlotController::HoverCardUpdateType::kSelectionChanged:
ResetCardsSeenCount();
break;
case TabSlotController::HoverCardUpdateType::kHover:
if (!tab) {
last_mouse_exit_timestamp_ = base::TimeTicks::Now();
}
break;
case TabSlotController::HoverCardUpdateType::kTabDataChanged:
DCHECK(tab && IsHoverCardShowingForTab(tab));
break;
case TabSlotController::HoverCardUpdateType::kTabRemoved:
case TabSlotController::HoverCardUpdateType::kAnimating:
// Neither of these cases should have a tab associated.
DCHECK(!tab);
break;
case TabSlotController::HoverCardUpdateType::kEvent:
case TabSlotController::HoverCardUpdateType::kFocus:
// No special action taken for this type of even (yet).
break;
}
if (tab) {
UpdateOrShowCard(tab, update_type);
} else {
HideHoverCard();
}
}
void TabHoverCardController::PreventImmediateReshow() {
last_mouse_exit_timestamp_ = base::TimeTicks();
}
void TabHoverCardController::UpdateOrShowCard(
Tab* tab,
TabSlotController::HoverCardUpdateType update_type) {
// Close is asynchronous, so make sure that if we're closing we clear out all
// of our data *now* rather than waiting for the deletion message.
if (hover_card_ && GetCardWidget()->IsClosed()) {
OnViewIsDeleting(hover_card_);
}
// If a hover card is being updated because of a data change, the hover card
// had better already be showing for the affected tab.
if (update_type == TabSlotController::HoverCardUpdateType::kTabDataChanged) {
if (!IsHoverCardShowingForTab(tab)) {
return;
}
UpdateCardContent(tab);
// When a tab has been discarded, the thumbnail is moved to a new
// ThumbnailTabHelper so it must be observed again.
if (tab->data().is_tab_discarded) {
MaybeStartThumbnailObservation(tab, /* is_initial_show */ false);
}
slide_animator_->UpdateTargetBounds();
return;
}
// Cancel any pending fades.
if (hover_card_ && fade_animator_->IsFadingOut()) {
fade_animator_->CancelFadeOut();
}
if (hover_card_) {
// If the card was visible we need to update the card now, before any slide
// or snap occurs.
UpdateCardContent(tab);
MaybeStartThumbnailObservation(tab, /* is_initial_show */ false);
// If widget is already visible and anchored to the correct tab we should
// not try to reset the anchor view or reshow.
if (!UseAnimations() || (hover_card_->GetAnchorView() == tab &&
!slide_animator_->is_animating())) {
slide_animator_->SnapToAnchorView(tab);
} else {
slide_animator_->AnimateToAnchorView(tab);
}
return;
}
// Maybe make hover card visible. Disabling animations for testing also
// eliminates the show timer, lest the tests have to be significantly more
// complex and time-consuming.
const bool is_initial = !ShouldShowImmediately(tab);
if (is_initial) {
ResetCardsSeenCount();
}
if (is_initial && !disable_animations_for_testing_) {
// Use the largest tab in the tab strip when determining the delay so that
// the delay is consistent for all tabs within the tab strip.
int largest_tab = tab->width();
for (int i = 0; i < tab_strip_->GetTabCount(); i++) {
largest_tab = std::max(largest_tab, tab_strip_->tab_at(i)->width());
}
delayed_show_timer_.Start(
FROM_HERE, GetShowDelay(largest_tab),
base::BindOnce(&TabHoverCardController::ShowHoverCard,
weak_ptr_factory_.GetWeakPtr(), true, tab));
} else {
// Just in case, cancel the timer. This shouldn't cancel a delayed capture
// since delayed capture only happens when the hover card already exists,
// and this code is only invoked if there is no hover card yet.
delayed_show_timer_.Stop();
DCHECK_EQ(target_tab_, tab);
ShowHoverCard(is_initial, tab);
}
}
void TabHoverCardController::ShowHoverCard(bool is_initial,
const Tab* intended_tab) {
// Make sure the hover card isn't accidentally shown if it's already visible
// or if the anchor is gone or changed.
if (hover_card_ || target_tab_ != intended_tab || !TargetTabIsValid()) {
return;
}
// Note: `target_tab_` can be nullified via reentreant callbacks invoked
// throughout the HoverCard creation process. The doc mentioned at
// crbug.com/40865488#comment23 discusses proper fixes for this. Until then,
// early-return after vulnerable calls here if `target_tab_` has become null.
// See also: crbug.com/1295601, crbug.com/1322117, crbug.com/1348956
CreateHoverCard(target_tab_);
if (!TargetTabIsValid()) {
HideHoverCard();
return;
}
UpdateCardContent(target_tab_);
if (!TargetTabIsValid()) {
HideHoverCard();
return;
}
slide_animator_->UpdateTargetBounds();
MaybeStartThumbnailObservation(target_tab_, is_initial);
GetCardWidget()->SetZOrderSublevel(ChromeWidgetSublevel::kSublevelHoverable);
if (!is_initial || !UseAnimations()) {
OnCardFullyVisible();
GetCardWidget()->Show();
return;
}
fade_animator_->FadeIn();
}
void TabHoverCardController::HideHoverCard() {
if (!hover_card_ || GetCardWidget()->IsClosed()) {
return;
}
// Required for test metrics.
hover_card_last_seen_on_tab_ = nullptr;
if (thumbnail_observer_) {
thumbnail_observer_->Observe(nullptr);
thumbnail_wait_state_ = ThumbnailWaitState::kNotWaiting;
}
// Cancel any pending fade-in.
if (fade_animator_->IsFadingIn()) {
fade_animator_->CancelFadeIn();
}
// This needs to be called whether we're doing a fade or a pop out.
slide_animator_->StopAnimation();
if (!UseAnimations()) {
CloseCardWidget();
return;
}
if (fade_animator_->IsFadingOut()) {
return;
}
fade_animator_->FadeOut();
}
void TabHoverCardController::OnCardClosing() {
tab_resource_usage_collector_observation_.Reset();
delayed_show_timer_.Stop();
hover_card_observation_.Reset();
event_sniffer_.reset();
slide_progressed_subscription_ = base::CallbackListSubscription();
slide_complete_subscription_ = base::CallbackListSubscription();
fade_complete_subscription_ = base::CallbackListSubscription();
slide_animator_.reset();
fade_animator_.reset();
hover_card_ = nullptr;
}
void TabHoverCardController::OnViewIsDeleting(views::View* observed_view) {
if (hover_card_ == observed_view) {
OnCardClosing();
} else if (target_tab_ == observed_view) {
UpdateHoverCard(nullptr,
TabSlotController::HoverCardUpdateType::kTabRemoved);
// These postconditions should always be met after calling
// UpdateHoverCard(nullptr, ...)
DCHECK(!target_tab_);
DCHECK(!target_tab_observation_.IsObserving());
}
}
void TabHoverCardController::OnViewVisibilityChanged(views::View* observed_view,
views::View* starting_view,
bool visible) {
// Only care about target tab becoming invisible.
if (observed_view != target_tab_) {
return;
}
// If visibility anywhere in the hierarchy changed to false, then the target
// view is not visible, so treat it as if it is going away.
if (!visible) {
OnViewIsDeleting(observed_view);
}
}
void TabHoverCardController::OnTabResourceMetricsRefreshed() {
if (hover_card_ && target_tab_) {
UpdateHoverCard(target_tab_,
TabSlotController::HoverCardUpdateType::kTabDataChanged);
}
}
bool TabHoverCardController::ArePreviewsEnabled() const {
return static_cast<bool>(thumbnail_observer_);
}
void TabHoverCardController::CreateHoverCard(Tab* tab) {
TabHoverCardBubbleView::InitParams params;
params.use_animation = UseAnimations();
// In some browser types (e.g. ChromeOS terminal app) hide the domain label.
params.show_domain = !IsBrowserForSystemWebApp(tab_strip_->GetBrowser());
params.show_memory_usage = hover_card_memory_usage_enabled_;
params.show_image_preview = hover_card_image_previews_enabled_;
hover_card_ = new TabHoverCardBubbleView(tab, params);
hover_card_observation_.Observe(hover_card_.get());
event_sniffer_ = std::make_unique<EventSniffer>(this);
slide_animator_ = std::make_unique<views::BubbleSlideAnimator>(hover_card_);
slide_animator_->SetSlideDuration(
TabHoverCardBubbleView::kHoverCardSlideDuration);
slide_progressed_subscription_ = slide_animator_->AddSlideProgressedCallback(
base::BindRepeating(&TabHoverCardController::OnSlideAnimationProgressed,
weak_ptr_factory_.GetWeakPtr()));
slide_complete_subscription_ = slide_animator_->AddSlideCompleteCallback(
base::BindRepeating(&TabHoverCardController::OnSlideAnimationComplete,
weak_ptr_factory_.GetWeakPtr()));
fade_animator_ = std::make_unique<views::WidgetFadeAnimator>(GetCardWidget());
fade_complete_subscription_ = fade_animator_->AddFadeCompleteCallback(
base::BindRepeating(&TabHoverCardController::OnFadeAnimationEnded,
weak_ptr_factory_.GetWeakPtr()));
if (!thumbnail_observer_ && hover_card_image_previews_enabled_) {
thumbnail_observer_ = std::make_unique<TabHoverCardThumbnailObserver>();
thumbnail_subscription_ = thumbnail_observer_->AddCallback(
base::BindRepeating(&TabHoverCardController::OnPreviewImageAvailable,
weak_ptr_factory_.GetWeakPtr()));
}
tab_resource_usage_collector_observation_.Observe(
tab_resource_usage_collector_);
}
void TabHoverCardController::UpdateCardContent(Tab* tab) {
// If the hover card is transitioning between tabs, we need to do a
// cross-fade.
if (hover_card_->GetAnchorView() != tab) {
hover_card_->SetTextFade(0.0);
}
hover_card_->UpdateCardContent(tab);
}
void TabHoverCardController::MaybeStartThumbnailObservation(
Tab* tab,
bool is_initial_show) {
// If the preview image feature is not enabled, `thumbnail_observer_` will be
// null.
if (!thumbnail_observer_) {
return;
}
// Active tabs don't get thumbnails.
if (tab->IsActive()) {
thumbnail_observer_->Observe(nullptr);
return;
}
// Discarded tabs that don't already have a thumbnail won't get one.
if (tab->IsDiscarded() && !tab->HasThumbnail()) {
thumbnail_observer_->Observe(nullptr);
return;
}
auto thumbnail = tab->data().thumbnail;
if (!thumbnail) {
hover_card_->SetPlaceholderImage();
thumbnail_wait_state_ = ThumbnailWaitState::kNotWaiting;
return;
}
if (thumbnail == thumbnail_observer_->current_image()) {
return;
}
// We're probably going ask for a preview image, so figure out whether we
// want to capture now, later, or at all, and whether to show a placeholder
// in the meantime.
// The crossfade parameter determines when a placeholder image is displayed.
const auto crossfade_at =
TabHoverCardBubbleView::GetPreviewImageCrossfadeStart();
if (UseAnimations() && crossfade_at.has_value() &&
crossfade_at.value() == 0.0) {
hover_card_->SetPlaceholderImage();
thumbnail_wait_state_ = ThumbnailWaitState::kWaitingWithPlaceholder;
} else {
thumbnail_wait_state_ = ThumbnailWaitState::kWaitingWithoutPlaceholder;
}
// For the first show there has already been a delay, so it's fine to ask for
// the image immediately; same is true if we already have a thumbnail.
// Otherwise the delay is based on the capture readiness.
base::TimeDelta capture_delay =
is_initial_show || thumbnail->has_data()
? base::TimeDelta()
: GetPreviewImageCaptureDelay(thumbnail->GetCaptureReadiness());
// Under memory pressure, we will additionally delay the initial capture, so
// that generating the image is a more deliberate choice from the user. The
// memory pressure monitor is disabled in tests.
if (const auto* const monitor = base::MemoryPressureMonitor::Get()) {
switch (monitor->GetCurrentPressureLevel(
base::MemoryPressureMonitorTag::kTabHoverCardController)) {
case base::MEMORY_PRESSURE_LEVEL_CRITICAL:
capture_delay = base::TimeDelta::Max();
break;
case base::MEMORY_PRESSURE_LEVEL_MODERATE:
capture_delay += kMemoryPressureCaptureDelay;
break;
case base::MEMORY_PRESSURE_LEVEL_NONE:
break;
}
}
if (capture_delay.is_zero()) {
thumbnail_observer_->Observe(thumbnail);
return;
}
// If we've already waiting on this tab, we're done.
if (delayed_show_timer_.IsRunning()) {
return;
}
// Stop updating the preview image unless/until we re-enable capture.
thumbnail_observer_->Observe(nullptr);
if (thumbnail_wait_state_ == ThumbnailWaitState::kWaitingWithoutPlaceholder) {
hover_card_->SetPlaceholderImage();
thumbnail_wait_state_ = ThumbnailWaitState::kWaitingWithPlaceholder;
}
// If we've elected to put off capture indefinitely (likely due to memory
// pressure), there's no additional work to do.
if (capture_delay.is_inf()) {
return;
}
// Start a delayed capture.
delayed_show_timer_.Start(
FROM_HERE, capture_delay,
base::BindOnce(&TabHoverCardController::StartThumbnailObservation,
base::Unretained(this), tab));
}
void TabHoverCardController::StartThumbnailObservation(Tab* tab) {
if (tab != target_tab_) {
return;
}
// If the preview image feature is not enabled, `thumbnail_observer_` will be
// null.
if (!thumbnail_observer_) {
return;
}
DCHECK(tab);
DCHECK(hover_card_);
DCHECK(waiting_for_preview());
// Do not capture thumbnails during critical memory pressure.
const auto* const monitor = base::MemoryPressureMonitor::Get();
if (monitor && monitor->GetCurrentPressureLevel(
base::MemoryPressureMonitorTag::kTabHoverCardController) ==
base::MEMORY_PRESSURE_LEVEL_CRITICAL) {
// Because we're blocked, we'll show a placeholder instead of nothing or
// the wrong image.
if (thumbnail_wait_state_ ==
ThumbnailWaitState::kWaitingWithoutPlaceholder) {
hover_card_->SetPlaceholderImage();
thumbnail_wait_state_ = ThumbnailWaitState::kWaitingWithPlaceholder;
}
return;
}
auto thumbnail = tab->data().thumbnail;
if (!thumbnail || thumbnail == thumbnail_observer_->current_image()) {
return;
}
thumbnail_observer_->Observe(thumbnail);
}
bool TabHoverCardController::ShouldShowImmediately(const Tab* tab) const {
// If less than `kShowWithoutDelayTimeBuffer` time has passed since the hover
// card was last visible then it is shown immediately. This is to account for
// if hover unintentionally leaves the tab strip.
constexpr base::TimeDelta kShowWithoutDelayTimeBuffer =
base::Milliseconds(300);
base::TimeDelta elapsed_time =
base::TimeTicks::Now() - last_mouse_exit_timestamp_;
bool within_delay_time_buffer = !last_mouse_exit_timestamp_.is_null() &&
elapsed_time <= kShowWithoutDelayTimeBuffer;
// Hover cards should be shown without delay if triggered within the time
// buffer or if the tab or its children have focus which indicates that the
// tab is keyboard focused.
const views::FocusManager* const tab_focus_manager = tab->GetFocusManager();
return within_delay_time_buffer || tab->HasFocus() ||
(tab_focus_manager &&
tab->Contains(tab_focus_manager->GetFocusedView()));
}
const views::View* TabHoverCardController::GetTargetAnchorView() const {
if (!hover_card_) {
return nullptr;
}
if (slide_animator_->is_animating()) {
return slide_animator_->desired_anchor_view();
}
return hover_card_->GetAnchorView();
}
bool TabHoverCardController::TargetTabIsValid() const {
// There are a bunch of conditions under which a tab may no longer be valid,
// including no longer belonging to the same tabstrip, being dragged or
// detached, or just not being visible. We need to be vigilant about invalid
// tabs due to e.g. crbug.com/1295601.
return target_tab_ && tab_strip_->GetModelIndexOf(target_tab_).has_value() &&
!target_tab_->closing() && !target_tab_->detached() &&
!target_tab_->dragging() && target_tab_->GetVisible();
}
void TabHoverCardController::OnCardFullyVisible() {
DCHECK(target_tab_);
if (target_tab_ == hover_card_last_seen_on_tab_.get()) {
return;
}
hover_card_last_seen_on_tab_ = target_tab_;
++hover_cards_seen_count_;
}
void TabHoverCardController::ResetCardsSeenCount() {
hover_card_last_seen_on_tab_ = nullptr;
hover_cards_seen_count_ = 0;
}
void TabHoverCardController::OnFadeAnimationEnded(
views::WidgetFadeAnimator* animator,
views::WidgetFadeAnimator::FadeType fade_type) {
// There's a potential race condition where we get the fade in complete signal
// just as we've decided to fade out, so check for null.
// See: crbug.com/1192451
if (target_tab_ &&
fade_type == views::WidgetFadeAnimator::FadeType::kFadeIn) {
OnCardFullyVisible();
}
if (fade_type == views::WidgetFadeAnimator::FadeType::kFadeOut) {
CloseCardWidget();
}
}
void TabHoverCardController::OnSlideAnimationProgressed(
views::BubbleSlideAnimator* animator,
double value) {
if (hover_card_) {
hover_card_->SetTextFade(value);
}
if (thumbnail_wait_state_ == ThumbnailWaitState::kWaitingWithoutPlaceholder) {
const auto crossfade_start =
TabHoverCardBubbleView::GetPreviewImageCrossfadeStart();
if (crossfade_start.has_value() && value >= crossfade_start.value()) {
hover_card_->SetPlaceholderImage();
thumbnail_wait_state_ = ThumbnailWaitState::kWaitingWithPlaceholder;
}
}
}
void TabHoverCardController::OnSlideAnimationComplete(
views::BubbleSlideAnimator* animator) {
// Make sure we're displaying the new text at 100% opacity, and none of the
// old text.
hover_card_->SetTextFade(1.0);
// If we were waiting for a preview image with data to load, we don't want to
// keep showing the old image while hovering on the new tab, so clear it. This
// shouldn't happen very often for slide animations, but could on slower
// computers.
if (thumbnail_wait_state_ == ThumbnailWaitState::kWaitingWithoutPlaceholder) {
hover_card_->SetPlaceholderImage();
thumbnail_wait_state_ = ThumbnailWaitState::kWaitingWithPlaceholder;
}
OnCardFullyVisible();
}
void TabHoverCardController::OnPreviewImageAvailable(
TabHoverCardThumbnailObserver* observer,
gfx::ImageSkia thumbnail_image) {
DCHECK_EQ(thumbnail_observer_.get(), observer);
thumbnail_wait_state_ = ThumbnailWaitState::kNotWaiting;
// The hover card could be destroyed before the preview image is delivered.
if (!hover_card_) {
return;
}
// Can still set image on a fading-out hover card (we can change this behavior
// later if we want).
hover_card_->SetTargetTabImage(thumbnail_image);
}
void TabHoverCardController::OnHovercardImagesEnabledChanged() {
hover_card_image_previews_enabled_ = AreHoverCardImagesEnabled();
if (!hover_card_image_previews_enabled_) {
thumbnail_subscription_ = base::CallbackListSubscription();
thumbnail_observer_.reset();
}
}
void TabHoverCardController::OnHovercardMemoryUsageEnabledChanged() {
hover_card_memory_usage_enabled_ =
g_browser_process->local_state()->GetBoolean(
prefs::kHoverCardMemoryUsageEnabled);
}
views::Widget* TabHoverCardController::GetCardWidget() const {
return hover_card_->GetWidget();
}
void TabHoverCardController::CloseCardWidget() {
GetCardWidget()->Close();
}