blob: c9b52717bf0dbfaac481830de4e4706e9b12a9c1 [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/tab_search_bubble_host.h"
#include "base/functional/bind.h"
#include "base/i18n/rtl.h"
#include "base/metrics/histogram_functions.h"
#include "base/strings/strcat.h"
#include "base/trace_event/named_trigger.h"
#include "base/trace_event/trace_event.h"
#include "chrome/app/vector_icons/vector_icons.h"
#include "chrome/browser/feature_engagement/tracker_factory.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_element_identifiers.h"
#include "chrome/browser/ui/browser_finder.h"
#include "chrome/browser/ui/browser_window/public/browser_window_features.h"
#include "chrome/browser/ui/layout_constants.h"
#include "chrome/browser/ui/tabs/organization/tab_organization_service.h"
#include "chrome/browser/ui/tabs/organization/tab_organization_service_factory.h"
#include "chrome/browser/ui/tabs/organization/tab_organization_utils.h"
#include "chrome/browser/ui/tabs/tab_group_model.h"
#include "chrome/browser/ui/tabs/tab_strip_prefs.h"
#include "chrome/browser/ui/ui_features.h"
#include "chrome/browser/ui/view_ids.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
#include "chrome/browser/ui/views/tab_search_bubble_host_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/webui/tab_search/tab_search_prefs.h"
#include "chrome/browser/ui/webui/tab_search/tab_search_ui.h"
#include "chrome/common/chrome_features.h"
#include "chrome/common/webui_url_constants.h"
#include "chrome/grit/generated_resources.h"
#include "components/feature_engagement/public/event_constants.h"
#include "components/feature_engagement/public/feature_constants.h"
#include "components/feature_engagement/public/tracker.h"
#include "components/prefs/pref_service.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/compositor/compositor.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/views/widget/widget.h"
namespace {
// These values are persisted to logs. Entries should not be renumbered and
// numeric values should never be reused.
enum class TabSearchOpenAction {
kMouseClick = 0,
kKeyboardNavigation = 1,
kKeyboardShortcut = 2,
kTouchGesture = 3,
kMaxValue = kTouchGesture,
};
TabSearchOpenAction GetActionForEvent(const ui::Event& event) {
if (event.IsMouseEvent()) {
return TabSearchOpenAction::kMouseClick;
}
return event.IsKeyEvent() ? TabSearchOpenAction::kKeyboardNavigation
: TabSearchOpenAction::kTouchGesture;
}
} // namespace
TabSearchBubbleHost::TabSearchBubbleHost(
views::Button* button,
BrowserWindowInterface* browser_window_interface)
: button_(button),
profile_(browser_window_interface->GetProfile()),
webui_bubble_manager_(WebUIBubbleManager::Create<TabSearchUI>(
button,
browser_window_interface,
GURL(chrome::kChromeUITabSearchURL),
IDS_ACCNAME_TAB_SEARCH)),
widget_open_timer_(base::BindRepeating([](base::TimeDelta time_elapsed) {
base::UmaHistogramMediumTimes("Tabs.TabSearch.WindowDisplayedDuration3",
time_elapsed);
})) {
auto* const tab_organization_service =
TabOrganizationServiceFactory::GetForProfile(profile_.get());
if (tab_organization_service) {
tab_organization_observation_.Observe(tab_organization_service);
}
auto menu_button_controller = std::make_unique<views::MenuButtonController>(
button,
base::BindRepeating(&TabSearchBubbleHost::ButtonPressed,
base::Unretained(this)),
std::make_unique<views::Button::DefaultButtonControllerDelegate>(button));
menu_button_controller_ = menu_button_controller.get();
button->SetButtonController(std::move(menu_button_controller));
webui_bubble_manager_observer_.Observe(webui_bubble_manager_.get());
}
TabSearchBubbleHost::~TabSearchBubbleHost() = default;
void TabSearchBubbleHost::OnWidgetVisibilityChanged(views::Widget* widget,
bool visible) {
CHECK_EQ(webui_bubble_manager_->GetBubbleWidget(), widget);
if (visible && widget && bubble_created_time_.has_value()) {
widget->GetCompositor()->RequestSuccessfulPresentationTimeForNextFrame(
base::BindOnce(
[](base::TimeTicks bubble_created_time,
bool bubble_using_cached_web_contents,
WebUIContentsWarmupLevel contents_warmup_level,
const viz::FrameTimingDetails& frame_timing_details) {
base::TimeTicks presentation_timestamp =
frame_timing_details.presentation_feedback.timestamp;
base::TimeDelta time_to_show =
presentation_timestamp - bubble_created_time;
base::UmaHistogramMediumTimes(
bubble_using_cached_web_contents
? "Tabs.TabSearch.WindowTimeToShowCachedWebView2"
: "Tabs.TabSearch.WindowTimeToShowUncachedWebView2",
time_to_show);
base::UmaHistogramMediumTimes(
base::StrCat({"Tabs.TabSearch.TimeToShow.",
ToString(contents_warmup_level)}),
time_to_show);
},
*bubble_created_time_,
webui_bubble_manager_->bubble_using_cached_web_contents(),
webui_bubble_manager_->contents_warmup_level()));
const PrefService* prefs = profile_->GetPrefs();
const auto section = tab_search_prefs::GetTabSearchSectionFromInt(
prefs->GetInteger(tab_search_prefs::kTabSearchTabIndex));
const auto organization_feature =
tab_search_prefs::GetTabOrganizationFeatureFromInt(
prefs->GetInteger(tab_search_prefs::kTabOrganizationFeature));
if (section == tab_search::mojom::TabSearchSection::kSearch) {
return;
}
if (organization_feature ==
tab_search::mojom::TabOrganizationFeature::kSelector ||
organization_feature ==
tab_search::mojom::TabOrganizationFeature::kNone) {
base::UmaHistogramEnumeration(
"Tab.Organization.SelectorCTR",
tab_search::mojom::SelectorCTREvent::kSelectorShown);
} else if (organization_feature ==
tab_search::mojom::TabOrganizationFeature::kDeclutter) {
base::UmaHistogramEnumeration(
"Tab.Organization.DeclutterCTR",
tab_search::mojom::DeclutterCTREvent::kDeclutterShown);
}
} else if (!visible && bubble_created_time_.has_value()) {
const base::TimeDelta time_to_close =
base::TimeTicks::Now() - bubble_created_time_.value();
base::UmaHistogramMediumTimes("Tabs.TabSearch.TimeToClose", time_to_close);
bubble_created_time_.reset();
}
}
void TabSearchBubbleHost::OnWidgetDestroying(views::Widget* widget) {
DCHECK_EQ(webui_bubble_manager_->GetBubbleWidget(), widget);
DCHECK(bubble_widget_observation_.IsObservingSource(
webui_bubble_manager_->GetBubbleWidget()));
bubble_widget_observation_.Reset();
pressed_lock_.reset();
for (auto& observer : observers_) {
observer.OnBubbleDestroying();
}
}
void TabSearchBubbleHost::OnOrganizationAccepted(Browser* browser) {
if (browser != GetBrowser()) {
return;
}
// Don't show IPH if the user already has other tab groups.
if (browser->tab_strip_model()->group_model()->ListTabGroups().size() > 1) {
return;
}
BrowserUserEducationInterface::From(browser)->MaybeShowFeaturePromo(
feature_engagement::kIPHTabOrganizationSuccessFeature);
}
void TabSearchBubbleHost::OnUserInvokedFeature(const Browser* browser) {
if (browser == GetBrowser()) {
ShowTabSearchBubble(
false, tab_search::mojom::TabSearchSection::kOrganize,
tab_search::mojom::TabOrganizationFeature::kAutoTabGroups);
}
}
void TabSearchBubbleHost::BeforeBubbleWidgetShowed(views::Widget* widget) {
CHECK_EQ(widget, webui_bubble_manager_->GetBubbleWidget());
// There should only ever be a single bubble widget active for the
// TabSearchBubbleHost.
DCHECK(!bubble_widget_observation_.IsObserving());
bubble_widget_observation_.Observe(widget);
widget_open_timer_.Reset(widget);
widget->GetCompositor()->RequestSuccessfulPresentationTimeForNextFrame(
base::BindOnce(
[](base::TimeTicks button_pressed_time,
const viz::FrameTimingDetails& frame_timing_details) {
base::TimeTicks presentation_timestamp =
frame_timing_details.presentation_feedback.timestamp;
base::UmaHistogramMediumTimes(
"Tabs.TabSearch."
"ButtonPressedToNextFramePresented",
presentation_timestamp - button_pressed_time);
},
base::TimeTicks::Now()));
}
void TabSearchBubbleHost::AddObserver(TabSearchBubbleHostObserver* observer) {
observers_.AddObserver(observer);
}
void TabSearchBubbleHost::RemoveObserver(
TabSearchBubbleHostObserver* observer) {
observers_.RemoveObserver(observer);
}
bool TabSearchBubbleHost::ShowTabSearchBubble(
bool triggered_by_keyboard_shortcut,
tab_search::mojom::TabSearchSection section,
tab_search::mojom::TabOrganizationFeature organization_feature) {
TRACE_EVENT0("ui", "TabSearchBubbleHost::ShowTabSearchBubble");
base::trace_event::EmitNamedTrigger("show-tab-search-bubble");
if (section != tab_search::mojom::TabSearchSection::kNone) {
profile_->GetPrefs()->SetInteger(
tab_search_prefs::kTabSearchTabIndex,
tab_search_prefs::GetIntFromTabSearchSection(section));
}
if (organization_feature !=
tab_search::mojom::TabOrganizationFeature::kNone) {
profile_->GetPrefs()->SetInteger(
tab_search_prefs::kTabOrganizationFeature,
tab_search_prefs::GetIntFromTabOrganizationFeature(
organization_feature));
}
if (webui_bubble_manager_->GetBubbleWidget()) {
return false;
}
for (auto& observer : observers_) {
observer.OnBubbleInitializing();
}
if (auto* const browser = GetBrowser()) {
// Close the Tab Search IPH if it is showing.
BrowserUserEducationInterface::From(browser)->NotifyFeaturePromoFeatureUsed(
feature_engagement::kIPHTabSearchFeature,
FeaturePromoFeatureUsedAction::kClosePromoIfPresent);
}
bubble_created_time_ = base::TimeTicks::Now();
webui_bubble_manager_->set_widget_initialization_callback(base::BindOnce(
[](base::TimeTicks bubble_init_start_time) {
base::UmaHistogramMediumTimes(
"Tabs.TabSearch.BubbleWidgetInitializationTime",
base::TimeTicks::Now() - bubble_init_start_time);
},
*bubble_created_time_));
webui_bubble_manager_->ShowBubble(std::nullopt,
tabs::GetTabSearchTrailingTabstrip(profile_)
? views::BubbleBorder::TOP_RIGHT
: views::BubbleBorder::TOP_LEFT,
kTabSearchBubbleElementId);
auto* tracker =
feature_engagement::TrackerFactory::GetForBrowserContext(profile_);
if (tracker) {
tracker->NotifyEvent(feature_engagement::events::kTabSearchOpened);
}
if (triggered_by_keyboard_shortcut) {
base::UmaHistogramEnumeration("Tabs.TabSearch.OpenAction",
TabSearchOpenAction::kKeyboardShortcut);
}
// Hold the pressed lock while the |bubble_| is active.
pressed_lock_ = menu_button_controller_->TakeLock();
return true;
}
void TabSearchBubbleHost::CloseTabSearchBubble() {
webui_bubble_manager_->CloseBubble();
}
Browser* TabSearchBubbleHost::GetBrowser() {
for (Browser* browser : chrome::FindAllBrowsersWithProfile(profile_)) {
BrowserView* browser_view = BrowserView::GetBrowserViewForBrowser(browser);
if (browser_view->GetTabSearchBubbleHost() == this) {
return browser;
}
}
return nullptr;
}
void TabSearchBubbleHost::ButtonPressed(const ui::Event& event) {
if (ShowTabSearchBubble()) {
// Only log the open action if it resulted in creating a new instance of the
// Tab Search bubble.
base::UmaHistogramEnumeration("Tabs.TabSearch.OpenAction",
GetActionForEvent(event));
return;
}
CloseTabSearchBubble();
}