blob: f8f6a3497390e23f7b5414f07071d1da2a61532b [file] [log] [blame]
// Copyright 2021 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/ui/webui/tab_strip/tab_strip_page_handler.h"
#include <algorithm>
#include <memory>
#include "base/containers/fixed_flat_map.h"
#include "base/containers/span.h"
#include "base/memory/raw_ptr.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "base/trace_event/trace_event.h"
#include "build/chromeos_buildflags.h"
#include "chrome/app/chrome_command_ids.h"
#include "chrome/browser/extensions/extension_tab_util.h"
#include "chrome/browser/favicon/favicon_utils.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/themes/theme_properties.h"
#include "chrome/browser/themes/theme_service.h"
#include "chrome/browser/themes/theme_service_factory.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_commands.h"
#include "chrome/browser/ui/browser_list.h"
#include "chrome/browser/ui/tabs/tab_group.h"
#include "chrome/browser/ui/tabs/tab_group_model.h"
#include "chrome/browser/ui/tabs/tab_group_theme.h"
#include "chrome/browser/ui/tabs/tab_menu_model.h"
#include "chrome/browser/ui/tabs/tab_renderer_data.h"
#include "chrome/browser/ui/tabs/tab_utils.h"
#include "chrome/browser/ui/ui_features.h"
#include "chrome/browser/ui/webui/tab_strip/tab_strip_ui.h"
#include "chrome/browser/ui/webui/tab_strip/tab_strip_ui_embedder.h"
#include "chrome/browser/ui/webui/tab_strip/tab_strip_ui_metrics.h"
#include "chrome/browser/ui/webui/tab_strip/tab_strip_ui_util.h"
#include "chrome/browser/ui/webui/theme_source.h"
#include "chrome/browser/ui/webui/util/image_util.h"
#include "chrome/browser/ui/webui/webui_util.h"
#include "chrome/grit/generated_resources.h"
#include "components/tab_groups/tab_group_color.h"
#include "components/tab_groups/tab_group_id.h"
#include "components/tab_groups/tab_group_visual_data.h"
#include "content/public/common/drop_data.h"
#include "third_party/abseil-cpp/absl/types/optional.h"
#include "third_party/blink/public/common/input/web_gesture_event.h"
#include "ui/aura/window_delegate.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/models/list_selection_model.h"
#include "ui/base/models/simple_menu_model.h"
#include "ui/base/theme_provider.h"
#include "ui/color/color_id.h"
#include "ui/events/event.h"
#include "ui/events/event_utils.h"
#include "ui/events/gesture_event_details.h"
#include "ui/gfx/color_utils.h"
#include "ui/gfx/geometry/point_conversions.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/range/range.h"
#include "url/gurl.h"
// This should be after all other #includes.
#if defined(_WINDOWS_) // Detect whether windows.h was included.
#include "base/win/windows_h_disallowed.h"
#endif // defined(_WINDOWS_)
namespace {
// Delay in milliseconds of when the dragging UI should be shown for touch drag.
// Note: For better user experience, this is made shorter than
// ET_GESTURE_LONG_PRESS delay, which is too long for this case, e.g., about
// 650ms.
constexpr base::TimeDelta kTouchLongpressDelay = base::Milliseconds(300);
class WebUIBackgroundMenuModel : public ui::SimpleMenuModel {
public:
explicit WebUIBackgroundMenuModel(ui::SimpleMenuModel::Delegate* delegate)
: ui::SimpleMenuModel(delegate) {
AddItemWithStringId(IDC_NEW_TAB, IDS_NEW_TAB);
AddItemWithStringId(IDC_RESTORE_TAB, IDS_RESTORE_TAB);
AddItemWithStringId(IDC_BOOKMARK_ALL_TABS, IDS_BOOKMARK_ALL_TABS);
}
};
class WebUIBackgroundContextMenu : public ui::SimpleMenuModel::Delegate,
public WebUIBackgroundMenuModel {
public:
WebUIBackgroundContextMenu(
Browser* browser,
const ui::AcceleratorProvider* accelerator_provider)
: WebUIBackgroundMenuModel(this),
browser_(browser),
accelerator_provider_(accelerator_provider) {}
~WebUIBackgroundContextMenu() override = default;
void ExecuteCommand(int command_id, int event_flags) override {
chrome::ExecuteCommand(browser_, command_id);
}
bool GetAcceleratorForCommandId(int command_id,
ui::Accelerator* accelerator) const override {
return accelerator_provider_->GetAcceleratorForCommandId(command_id,
accelerator);
}
private:
const raw_ptr<Browser> browser_;
const raw_ptr<const ui::AcceleratorProvider> accelerator_provider_;
};
class WebUITabContextMenu : public ui::SimpleMenuModel::Delegate,
public TabMenuModel {
public:
WebUITabContextMenu(Browser* browser,
const ui::AcceleratorProvider* accelerator_provider,
int tab_index)
: TabMenuModel(this,
browser->tab_menu_model_delegate(),
browser->tab_strip_model(),
tab_index),
browser_(browser),
accelerator_provider_(accelerator_provider),
tab_index_(tab_index) {}
~WebUITabContextMenu() override = default;
void ExecuteCommand(int command_id, int event_flags) override {
DCHECK_LT(tab_index_, browser_->tab_strip_model()->count());
browser_->tab_strip_model()->ExecuteContextMenuCommand(
tab_index_, static_cast<TabStripModel::ContextMenuCommand>(command_id));
}
bool GetAcceleratorForCommandId(int command_id,
ui::Accelerator* accelerator) const override {
int real_command = -1;
TabStripModel::ContextMenuCommandToBrowserCommand(command_id,
&real_command);
if (real_command != -1) {
return accelerator_provider_->GetAcceleratorForCommandId(real_command,
accelerator);
} else {
return false;
}
}
private:
const raw_ptr<Browser> browser_;
const raw_ptr<const ui::AcceleratorProvider> accelerator_provider_;
const int tab_index_;
};
bool IsSortedAndContiguous(base::span<const int> sequence) {
if (sequence.size() < 2)
return true;
if (!std::is_sorted(sequence.begin(), sequence.end()))
return false;
return sequence.back() ==
sequence.front() + static_cast<int>(sequence.size()) - 1;
}
} // namespace
TabStripPageHandler::~TabStripPageHandler() = default;
TabStripPageHandler::TabStripPageHandler(
mojo::PendingReceiver<tab_strip::mojom::PageHandler> receiver,
mojo::PendingRemote<tab_strip::mojom::Page> page,
content::WebUI* web_ui,
Browser* browser,
TabStripUIEmbedder* embedder)
: receiver_(this, std::move(receiver)),
page_(std::move(page)),
web_ui_(web_ui),
browser_(browser),
embedder_(embedder),
thumbnail_tracker_(
base::BindRepeating(&TabStripPageHandler::HandleThumbnailUpdate,
base::Unretained(this))),
tab_before_unload_tracker_(
base::BindRepeating(&TabStripPageHandler::OnTabCloseCancelled,
base::Unretained(this))),
context_menu_after_tap_(base::FeatureList::IsEnabled(
features::kWebUITabStripContextMenuAfterTap)),
long_press_timer_(std::make_unique<base::RetainingOneShotTimer>(
FROM_HERE,
kTouchLongpressDelay,
base::BindRepeating(&TabStripPageHandler::OnLongPressTimer,
base::Unretained(this)))) {
DCHECK(browser_);
DCHECK(embedder_);
web_ui_->GetWebContents()->SetDelegate(this);
browser_->tab_strip_model()->AddObserver(this);
// Listen for theme installation.
ThemeServiceFactory::GetForProfile(browser_->profile())->AddObserver(this);
// Or native theme change.
theme_observation_.Observe(webui::GetNativeTheme(web_ui_->GetWebContents()));
}
void TabStripPageHandler::NotifyLayoutChanged() {
TRACE_EVENT0("browser", "TabStripPageHandler:NotifyLayoutChanged");
page_->LayoutChanged(embedder_->GetLayout().AsDictionary());
}
void TabStripPageHandler::NotifyReceivedKeyboardFocus() {
page_->ReceivedKeyboardFocus();
}
void TabStripPageHandler::NotifyContextMenuClosed() {
page_->ContextMenuClosed();
}
// TabStripModelObserver:
void TabStripPageHandler::OnTabGroupChanged(const TabGroupChange& change) {
TRACE_EVENT0("browser", "TabStripPageHandler:OnTabGroupChanged");
switch (change.type) {
case TabGroupChange::kCreated:
case TabGroupChange::kEditorOpened:
case TabGroupChange::kContentsChanged: {
// TabGroupChange::kCreated events are unnecessary as the front-end will
// assume a group was created if there is a tab-group-state-changed event
// with a new group ID. TabGroupChange::kContentsChanged events are
// handled by TabGroupStateChanged.
break;
}
case TabGroupChange::kVisualsChanged: {
page_->TabGroupVisualsChanged(
change.group.ToString(),
GetTabGroupData(
browser_->tab_strip_model()->group_model()->GetTabGroup(
change.group)));
break;
}
case TabGroupChange::kMoved: {
const int start_tab = browser_->tab_strip_model()
->group_model()
->GetTabGroup(change.group)
->ListTabs()
.start();
page_->TabGroupMoved(change.group.ToString(), start_tab);
break;
}
case TabGroupChange::kClosed: {
embedder_->HideEditDialogForGroup();
page_->TabGroupClosed(change.group.ToString());
break;
}
}
}
void TabStripPageHandler::TabGroupedStateChanged(
absl::optional<tab_groups::TabGroupId> group,
content::WebContents* contents,
int index) {
TRACE_EVENT0("browser", "TabStripPageHandler:TabGroupedStateChanged");
const SessionID::id_type tab_id =
extensions::ExtensionTabUtil::GetTabId(contents);
if (group.has_value()) {
page_->TabGroupStateChanged(tab_id, index, group.value().ToString());
} else {
page_->TabGroupStateChanged(tab_id, index, absl::optional<std::string>());
}
}
void TabStripPageHandler::OnTabStripModelChanged(
TabStripModel* tab_strip_model,
const TabStripModelChange& change,
const TabStripSelectionChange& selection) {
TRACE_EVENT0("browser", "TabStripPageHandler:OnTabStripModelChanged");
if (tab_strip_model->empty())
return;
switch (change.type()) {
case TabStripModelChange::kInserted: {
for (const auto& contents : change.GetInsert()->contents) {
page_->TabCreated(GetTabData(contents.contents, contents.index));
}
break;
}
case TabStripModelChange::kRemoved: {
for (const auto& contents : change.GetRemove()->contents) {
page_->TabRemoved(
extensions::ExtensionTabUtil::GetTabId(contents.contents));
}
break;
}
case TabStripModelChange::kMoved: {
auto* move = change.GetMove();
absl::optional<tab_groups::TabGroupId> tab_group_id =
tab_strip_model->GetTabGroupForTab(move->to_index);
if (tab_group_id.has_value()) {
const gfx::Range tabs_in_group = tab_strip_model->group_model()
->GetTabGroup(tab_group_id.value())
->ListTabs();
const ui::ListSelectionModel::SelectedIndices& sel =
selection.new_model.selected_indices();
const auto& selected_tabs = std::vector<int>(sel.begin(), sel.end());
const bool all_tabs_in_group =
IsSortedAndContiguous(base::make_span(selected_tabs)) &&
selected_tabs.front() == static_cast<int>(tabs_in_group.start()) &&
selected_tabs.size() == tabs_in_group.length();
if (all_tabs_in_group) {
// If the selection includes all the tabs within the changed tab's
// group, it is an indication that the entire group is being moved.
// To prevent sending multiple events for each tab in the group,
// ignore these tabs moving as entire group moves will be handled by
// TabGroupChange::kMoved.
break;
}
}
page_->TabMoved(extensions::ExtensionTabUtil::GetTabId(move->contents),
move->to_index,
tab_strip_model->IsTabPinned(move->to_index));
break;
}
case TabStripModelChange::kReplaced: {
auto* replace = change.GetReplace();
page_->TabReplaced(
extensions::ExtensionTabUtil::GetTabId(replace->old_contents),
extensions::ExtensionTabUtil::GetTabId(replace->new_contents));
break;
}
case TabStripModelChange::kSelectionOnly:
// Multi-selection is not supported for touch.
break;
}
if (selection.active_tab_changed()) {
content::WebContents* new_contents = selection.new_contents;
int index = selection.new_model.active();
if (new_contents && index != TabStripModel::kNoTab) {
page_->TabActiveChanged(
extensions::ExtensionTabUtil::GetTabId(new_contents));
}
}
}
void TabStripPageHandler::TabChangedAt(content::WebContents* contents,
int index,
TabChangeType change_type) {
TRACE_EVENT0("browser", "TabStripPageHandler:TabChangedAt");
page_->TabUpdated(GetTabData(contents, index));
}
void TabStripPageHandler::TabPinnedStateChanged(TabStripModel* tab_strip_model,
content::WebContents* contents,
int index) {
page_->TabUpdated(GetTabData(contents, index));
}
void TabStripPageHandler::TabBlockedStateChanged(content::WebContents* contents,
int index) {
page_->TabUpdated(GetTabData(contents, index));
}
bool TabStripPageHandler::PreHandleGestureEvent(
content::WebContents* source,
const blink::WebGestureEvent& event) {
switch (event.GetType()) {
case blink::WebInputEvent::Type::kGestureScrollBegin:
// Drag and drop for the WebUI tab strip is currently only supported for
// Aura platforms.
#if defined(USE_AURA)
// If we are passed the `kTouchLongpressDelay` threshold since the initial
// tap down initiate a drag on scroll start.
if (should_drag_on_gesture_scroll_ && !long_press_timer_->IsRunning()) {
handling_gesture_scroll_ = true;
// If we are about to start a drag ensure the context menu is closed.
embedder_->CloseContextMenu();
// Synthesize a long press event to start the drag and drop session.
// TODO(tluk): Replace this with a better drag and drop trigger when
// available.
ui::GestureEventDetails press_details(ui::ET_GESTURE_LONG_PRESS);
press_details.set_device_type(
ui::GestureDeviceType::DEVICE_TOUCHSCREEN);
ui::GestureEvent press_event(
touch_drag_start_point_.x(), touch_drag_start_point_.y(),
ui::EF_IS_SYNTHESIZED, base::TimeTicks::Now(), press_details);
auto* window = web_ui_->GetWebContents()->GetContentNativeView();
window->delegate()->OnGestureEvent(&press_event);
// Following the long press we need to dispatch a scroll end event to
// ensure the gesture stream is not left in an inconsistent state.
ui::GestureEventDetails scroll_end_details(ui::ET_GESTURE_SCROLL_END);
scroll_end_details.set_device_type(
ui::GestureDeviceType::DEVICE_TOUCHSCREEN);
ui::GestureEvent scroll_end_event(
touch_drag_start_point_.x(), touch_drag_start_point_.y(),
ui::EF_IS_SYNTHESIZED, base::TimeTicks::Now(), scroll_end_details);
window->delegate()->OnGestureEvent(&scroll_end_event);
return true;
}
long_press_timer_->Stop();
#endif // defined(USE_AURA)
return false;
case blink::WebInputEvent::Type::kGestureScrollEnd:
should_drag_on_gesture_scroll_ = false;
handling_gesture_scroll_ = false;
return false;
case blink::WebInputEvent::Type::kGestureTapDown:
// We should only trigger a drag as part of the gesture event stream if
// the stream begins with a tap down gesture event.
should_drag_on_gesture_scroll_ = true;
touch_drag_start_point_ =
gfx::ToRoundedPoint(event.PositionInRootFrame());
long_press_timer_->Reset();
return false;
case blink::WebInputEvent::Type::kGestureLongPress:
// Do not block the long press if handling a scroll gesture. This ensures
// the long press gesture event emitted during a scroll begin event
// reaches the WebContents and triggers a drag session.
if (handling_gesture_scroll_) {
should_drag_on_gesture_scroll_ = false;
return false;
}
if (!context_menu_after_tap_)
page_->ShowContextMenu();
return true;
case blink::WebInputEvent::Type::kGestureTwoFingerTap:
page_->ShowContextMenu();
return true;
case blink::WebInputEvent::Type::kGestureLongTap:
if (context_menu_after_tap_)
page_->ShowContextMenu();
should_drag_on_gesture_scroll_ = false;
long_press_timer_->Stop();
return true;
case blink::WebInputEvent::Type::kGestureTap:
// Ensure that we reset `should_drag_on_gesture_scroll_` when we encounter
// a gesture tap event (i.e. an event triggered after the user lifts their
// finger following a press or long press).
should_drag_on_gesture_scroll_ = false;
long_press_timer_->Stop();
return false;
default:
break;
}
return false;
}
bool TabStripPageHandler::CanDragEnter(
content::WebContents* source,
const content::DropData& data,
blink::DragOperationsMask operations_allowed) {
// TODO(crbug.com/1032592): Prevent dragging across Chromium instances.
if (data.custom_data.find(base::ASCIIToUTF16(kWebUITabIdDataType)) !=
data.custom_data.end()) {
int tab_id;
bool found_tab_id = base::StringToInt(
data.custom_data.at(base::ASCIIToUTF16(kWebUITabIdDataType)), &tab_id);
return found_tab_id && extensions::ExtensionTabUtil::GetTabById(
tab_id, browser_->profile(), false, nullptr);
}
if (data.custom_data.find(base::ASCIIToUTF16(kWebUITabGroupIdDataType)) !=
data.custom_data.end()) {
std::string group_id = base::UTF16ToUTF8(
data.custom_data.at(base::ASCIIToUTF16(kWebUITabGroupIdDataType)));
Browser* found_browser = tab_strip_ui::GetBrowserWithGroupId(
Profile::FromBrowserContext(browser_->profile()), group_id);
return found_browser != nullptr;
}
return false;
}
bool TabStripPageHandler::IsPrivileged() {
return true;
}
void TabStripPageHandler::OnLongPressTimer() {
page_->LongPress();
}
tab_strip::mojom::TabPtr TabStripPageHandler::GetTabData(
content::WebContents* contents,
int index) {
DCHECK(index >= 0);
auto tab_data = tab_strip::mojom::Tab::New();
tab_data->active = browser_->tab_strip_model()->active_index() == index;
tab_data->id = extensions::ExtensionTabUtil::GetTabId(contents);
DCHECK(tab_data->id > 0);
tab_data->index = index;
const absl::optional<tab_groups::TabGroupId> group_id =
browser_->tab_strip_model()->GetTabGroupForTab(index);
if (group_id.has_value()) {
tab_data->group_id = group_id.value().ToString();
}
TabRendererData tab_renderer_data =
TabRendererData::FromTabInModel(browser_->tab_strip_model(), index);
tab_data->pinned = tab_renderer_data.pinned;
tab_data->title = base::UTF16ToUTF8(tab_renderer_data.title);
tab_data->url = tab_renderer_data.visible_url;
if (!tab_renderer_data.favicon.isNull()) {
tab_data->favicon_url = GURL(webui::EncodePNGAndMakeDataURI(
tab_renderer_data.should_themify_favicon
? ThemeFavicon(tab_renderer_data.favicon)
: tab_renderer_data.favicon,
web_ui_->GetDeviceScaleFactor()));
tab_data->is_default_favicon =
tab_renderer_data.favicon.BackedBySameObjectAs(
favicon::GetDefaultFavicon().AsImageSkia());
} else {
tab_data->is_default_favicon = true;
}
tab_data->show_icon = tab_renderer_data.show_icon;
tab_data->network_state = tab_renderer_data.network_state;
tab_data->should_hide_throbber = tab_renderer_data.should_hide_throbber;
tab_data->blocked = tab_renderer_data.blocked;
tab_data->crashed = tab_renderer_data.IsCrashed();
// TODO(johntlee): Add the rest of TabRendererData
auto alert_states = std::make_unique<base::ListValue>();
for (const auto alert_state :
chrome::GetTabAlertStatesForContents(contents)) {
tab_data->alert_states.push_back(alert_state);
}
return tab_data;
}
tab_strip::mojom::TabGroupVisualDataPtr TabStripPageHandler::GetTabGroupData(
TabGroup* group) {
const tab_groups::TabGroupVisualData* visual_data = group->visual_data();
auto tab_group = tab_strip::mojom::TabGroupVisualData::New();
tab_group->title = base::UTF16ToUTF8(visual_data->title());
// TODO the tab strip should support toggles between inactive and active frame
// states. Currently the webui tab strip only uses active frame colors
// (https://crbug.com/1060398).
const int color_id = GetTabGroupTabStripColorId(visual_data->color(), true);
const SkColor group_color = embedder_->GetColor(color_id);
tab_group->color = color_utils::SkColorToRgbString(group_color);
tab_group->text_color = color_utils::SkColorToRgbString(
color_utils::GetColorWithMaxContrast(group_color));
return tab_group;
}
void TabStripPageHandler::GetTabs(GetTabsCallback callback) {
TRACE_EVENT0("browser", "TabStripPageHandler:HandleGetTabs");
std::vector<tab_strip::mojom::TabPtr> tabs;
TabStripModel* tab_strip_model = browser_->tab_strip_model();
for (int i = 0; i < tab_strip_model->count(); ++i) {
tabs.push_back(GetTabData(tab_strip_model->GetWebContentsAt(i), i));
}
std::move(callback).Run(std::move(tabs));
}
void TabStripPageHandler::GetGroupVisualData(
GetGroupVisualDataCallback callback) {
TRACE_EVENT0("browser", "TabStripPageHandler:HandleGetGroupVisualData");
base::flat_map<std::string, tab_strip::mojom::TabGroupVisualDataPtr>
group_visual_datas;
std::vector<tab_groups::TabGroupId> groups =
browser_->tab_strip_model()->group_model()->ListTabGroups();
for (const tab_groups::TabGroupId& group : groups) {
group_visual_datas[group.ToString()] = GetTabGroupData(
browser_->tab_strip_model()->group_model()->GetTabGroup(group));
}
std::move(callback).Run(std::move(group_visual_datas));
}
void TabStripPageHandler::GetThemeColors(GetThemeColorsCallback callback) {
TRACE_EVENT0("browser", "TabStripPageHandler:HandleGetThemeColors");
// This should return an object of CSS variables to rgba values so that
// the WebUI can use the CSS variables to color the tab strip
base::flat_map<std::string, std::string> colors;
colors["--tabstrip-background-color"] = color_utils::SkColorToRgbaString(
embedder_->GetColor(ThemeProperties::COLOR_FRAME_ACTIVE));
colors["--tabstrip-tab-background-color"] = color_utils::SkColorToRgbaString(
embedder_->GetColor(ThemeProperties::COLOR_TOOLBAR));
colors["--tabstrip-tab-text-color"] =
color_utils::SkColorToRgbaString(embedder_->GetColor(
ThemeProperties::COLOR_TAB_FOREGROUND_ACTIVE_FRAME_ACTIVE));
colors["--tabstrip-tab-separator-color"] =
color_utils::SkColorToRgbaString(SkColorSetA(
embedder_->GetColor(
ThemeProperties::COLOR_TAB_FOREGROUND_ACTIVE_FRAME_ACTIVE),
/* 16% opacity */ 0.16 * 255));
std::string throbber_color = color_utils::SkColorToRgbaString(
embedder_->GetColor(ThemeProperties::COLOR_TAB_THROBBER_SPINNING));
colors["--tabstrip-tab-loading-spinning-color"] = throbber_color;
colors["--tabstrip-tab-waiting-spinning-color"] =
color_utils::SkColorToRgbaString(
embedder_->GetColor(ThemeProperties::COLOR_TAB_THROBBER_WAITING));
colors["--tabstrip-indicator-recording-color"] =
color_utils::SkColorToRgbaString(
embedder_->GetColorProviderColor(ui::kColorAlertHighSeverity));
colors["--tabstrip-indicator-pip-color"] = throbber_color;
colors["--tabstrip-indicator-capturing-color"] = throbber_color;
colors["--tabstrip-tab-blocked-color"] = color_utils::SkColorToRgbaString(
embedder_->GetColorProviderColor(ui::kColorButtonBackgroundProminent));
colors["--tabstrip-focus-outline-color"] = color_utils::SkColorToRgbaString(
embedder_->GetColorProviderColor(ui::kColorFocusableBorderFocused));
#if !BUILDFLAG(IS_CHROMEOS_ASH)
colors["--tabstrip-scrollbar-thumb-color-rgb"] =
color_utils::SkColorToRgbString(color_utils::GetColorWithMaxContrast(
embedder_->GetColor(ThemeProperties::COLOR_FRAME_ACTIVE)));
#endif
std::move(callback).Run(std::move(colors));
}
void TabStripPageHandler::GroupTab(int32_t tab_id,
const std::string& group_id_string) {
int tab_index = -1;
bool got_tab = extensions::ExtensionTabUtil::GetTabById(
tab_id, browser_->profile(), /*include_incognito=*/true, nullptr, nullptr,
nullptr, &tab_index);
DCHECK(got_tab);
absl::optional<tab_groups::TabGroupId> group_id =
tab_strip_ui::GetTabGroupIdFromString(
browser_->tab_strip_model()->group_model(), group_id_string);
if (group_id.has_value()) {
browser_->tab_strip_model()->AddToExistingGroup({tab_index},
group_id.value());
}
}
void TabStripPageHandler::UngroupTab(int32_t tab_id) {
int tab_index = -1;
bool got_tab = extensions::ExtensionTabUtil::GetTabById(
tab_id, browser_->profile(), /*include_incognito=*/true, nullptr, nullptr,
nullptr, &tab_index);
DCHECK(got_tab);
browser_->tab_strip_model()->RemoveFromGroup({tab_index});
}
void TabStripPageHandler::MoveGroup(const std::string& group_id_string,
int32_t to_index) {
if (to_index == -1) {
to_index = browser_->tab_strip_model()->count();
}
auto* target_browser = browser_.get();
Browser* source_browser =
tab_strip_ui::GetBrowserWithGroupId(browser_->profile(), group_id_string);
if (!source_browser) {
return;
}
absl::optional<tab_groups::TabGroupId> group_id =
tab_strip_ui::GetTabGroupIdFromString(
source_browser->tab_strip_model()->group_model(), group_id_string);
TabGroup* group =
source_browser->tab_strip_model()->group_model()->GetTabGroup(
group_id.value());
const gfx::Range tabs_in_group = group->ListTabs();
if (source_browser == target_browser) {
if (static_cast<int>(tabs_in_group.start()) == to_index) {
// If the group is already in place, don't move it. This may happen
// if multiple drag events happen while the tab group is still
// being moved.
return;
}
// When a group is moved, all the tabs in it need to be selected at the same
// time. This mimics the way the native tab strip works and also allows
// this handler to ignore the events for each individual tab moving.
int active_index =
target_browser->tab_strip_model()->selection_model().active();
ui::ListSelectionModel group_selection;
group_selection.SetSelectedIndex(tabs_in_group.start());
group_selection.SetSelectionFromAnchorTo(tabs_in_group.end() - 1);
group_selection.set_active(active_index);
target_browser->tab_strip_model()->SetSelectionFromModel(group_selection);
target_browser->tab_strip_model()->MoveGroupTo(group_id.value(), to_index);
return;
}
target_browser->tab_strip_model()->group_model()->AddTabGroup(
group_id.value(),
absl::optional<tab_groups::TabGroupVisualData>{*group->visual_data()});
gfx::Range source_tab_indices = group->ListTabs();
const int tab_count = source_tab_indices.length();
const int from_index = source_tab_indices.start();
for (int i = 0; i < tab_count; i++) {
tab_strip_ui::MoveTabAcrossWindows(source_browser, from_index,
target_browser, to_index + i, group_id);
}
}
void TabStripPageHandler::MoveTab(int32_t tab_id, int32_t to_index) {
if (to_index == -1) {
to_index = browser_->tab_strip_model()->count();
}
Browser* source_browser;
int from_index = -1;
if (!extensions::ExtensionTabUtil::GetTabById(tab_id, browser_->profile(),
true, &source_browser, nullptr,
nullptr, &from_index)) {
return;
}
if (source_browser->profile() != browser_->profile()) {
return;
}
if (source_browser == browser_) {
browser_->tab_strip_model()->MoveWebContentsAt(from_index, to_index, false);
return;
}
tab_strip_ui::MoveTabAcrossWindows(source_browser, from_index, browser_,
to_index);
}
void TabStripPageHandler::CloseContainer() {
// We only autoclose for tab selection.
RecordTabStripUICloseHistogram(TabStripUICloseAction::kTabSelected);
DCHECK(embedder_);
embedder_->CloseContainer();
}
void TabStripPageHandler::CloseTab(int32_t tab_id, bool tab_was_swiped) {
content::WebContents* tab = nullptr;
if (!extensions::ExtensionTabUtil::GetTabById(tab_id, browser_->profile(),
true, &tab)) {
// ID didn't refer to a valid tab.
DVLOG(1) << "Invalid tab ID";
return;
}
if (tab_was_swiped) {
// The unload tracker will automatically unobserve the tab when it
// successfully closes.
tab_before_unload_tracker_.Observe(tab);
}
tab->Close();
}
void TabStripPageHandler::ShowBackgroundContextMenu(int32_t location_x,
int32_t location_y) {
gfx::PointF point(location_x, location_y);
DCHECK(embedder_);
embedder_->ShowContextMenuAtPoint(
gfx::ToRoundedPoint(point),
std::make_unique<WebUIBackgroundContextMenu>(
browser_, embedder_->GetAcceleratorProvider()),
base::BindRepeating(&TabStripPageHandler::NotifyContextMenuClosed,
weak_ptr_factory_.GetWeakPtr()));
}
void TabStripPageHandler::ShowEditDialogForGroup(
const std::string& group_id_string,
int32_t location_x,
int32_t location_y,
int32_t width,
int32_t height) {
absl::optional<tab_groups::TabGroupId> group_id =
tab_strip_ui::GetTabGroupIdFromString(
browser_->tab_strip_model()->group_model(), group_id_string);
if (!group_id.has_value()) {
return;
}
gfx::Point point(location_x, location_y);
gfx::Rect rect(width, height);
DCHECK(embedder_);
embedder_->ShowEditDialogForGroupAtPoint(point, rect, group_id.value());
}
void TabStripPageHandler::ShowTabContextMenu(int32_t tab_id,
int32_t location_x,
int32_t location_y) {
gfx::PointF point(location_x, location_y);
Browser* browser = nullptr;
int tab_index = -1;
const bool got_tab = extensions::ExtensionTabUtil::GetTabById(
tab_id, browser_->profile(), true /* include_incognito */, &browser,
nullptr, nullptr, &tab_index);
CHECK(got_tab);
if (browser != browser_) {
// TODO(crbug.com/1141573): Investigate how a context menu is being opened
// for a tab that is no longer in the tab strip. Until then, fire a
// tab-removed event so the tab is removed from this tab strip.
page_->TabRemoved(tab_id);
return;
}
DCHECK(embedder_);
embedder_->ShowContextMenuAtPoint(
gfx::ToRoundedPoint(point),
std::make_unique<WebUITabContextMenu>(
browser, embedder_->GetAcceleratorProvider(), tab_index),
base::BindRepeating(&TabStripPageHandler::NotifyContextMenuClosed,
weak_ptr_factory_.GetWeakPtr()));
base::UmaHistogramEnumeration(
"TabStrip.Tab.WebUI.ActivationAction",
TabStripModel::TabActivationTypes::kContextMenu);
}
void TabStripPageHandler::GetLayout(GetLayoutCallback callback) {
TRACE_EVENT0("browser", "TabStripPageHandler:HandleGetLayout");
std::move(callback).Run(std::move(embedder_->GetLayout().AsDictionary()));
}
void TabStripPageHandler::SetThumbnailTracked(int32_t tab_id,
bool thumbnail_tracked) {
TRACE_EVENT0("browser", "TabStripPageHandler:HandleSetThumbnailTracked");
content::WebContents* tab = nullptr;
if (!extensions::ExtensionTabUtil::GetTabById(tab_id, browser_->profile(),
true, &tab)) {
// ID didn't refer to a valid tab.
DVLOG(1) << "Invalid tab ID";
return;
}
if (thumbnail_tracked)
thumbnail_tracker_.AddTab(tab);
else
thumbnail_tracker_.RemoveTab(tab);
}
void TabStripPageHandler::ReportTabActivationDuration(uint32_t duration_ms) {
UMA_HISTOGRAM_TIMES("WebUITabStrip.TabActivation",
base::Milliseconds(duration_ms));
base::UmaHistogramEnumeration("TabStrip.Tab.WebUI.ActivationAction",
TabStripModel::TabActivationTypes::kTab);
}
void TabStripPageHandler::ReportTabDataReceivedDuration(uint32_t tab_count,
uint32_t duration_ms) {
ReportTabDurationHistogram("TabDataReceived", tab_count,
base::Milliseconds(duration_ms));
}
void TabStripPageHandler::ReportTabCreationDuration(uint32_t tab_count,
uint32_t duration_ms) {
ReportTabDurationHistogram("TabCreation", tab_count,
base::Milliseconds(duration_ms));
}
// Callback passed to |thumbnail_tracker_|. Called when a tab's thumbnail
// changes, or when we start watching the tab.
void TabStripPageHandler::HandleThumbnailUpdate(
content::WebContents* tab,
ThumbnailTracker::CompressedThumbnailData image) {
// Send base-64 encoded image to JS side. If |image| is blank (i.e.
// there is no data), send a blank URI.
TRACE_EVENT0("browser", "TabStripPageHandler:HandleThumbnailUpdate");
std::string data_uri;
if (image)
data_uri = webui::MakeDataURIForImage(base::make_span(image->data), "jpeg");
const SessionID::id_type tab_id = extensions::ExtensionTabUtil::GetTabId(tab);
page_->TabThumbnailUpdated(tab_id, data_uri);
}
void TabStripPageHandler::OnTabCloseCancelled(content::WebContents* tab) {
tab_before_unload_tracker_.Unobserve(tab);
const SessionID::id_type tab_id = extensions::ExtensionTabUtil::GetTabId(tab);
page_->TabCloseCancelled(tab_id);
}
// Reports a histogram using the format
// WebUITabStrip.|histogram_fragment|.[tab count bucket].
void TabStripPageHandler::ReportTabDurationHistogram(
const char* histogram_fragment,
int tab_count,
base::TimeDelta duration) {
if (tab_count <= 0)
return;
// It isn't possible to report both a number of tabs and duration datapoint
// together in a histogram or to correlate two histograms together. As a
// result the histogram is manually bucketed.
const char* tab_count_bucket = "01_05";
if (6 <= tab_count && tab_count <= 20) {
tab_count_bucket = "06_20";
} else if (20 < tab_count) {
tab_count_bucket = "21_";
}
std::string histogram_name = base::JoinString(
{"WebUITabStrip", histogram_fragment, tab_count_bucket}, ".");
base::UmaHistogramTimes(histogram_name, duration);
}
gfx::ImageSkia TabStripPageHandler::ThemeFavicon(const gfx::ImageSkia& source) {
return favicon::ThemeFavicon(
source, embedder_->GetColor(ThemeProperties::COLOR_TOOLBAR_BUTTON_ICON),
embedder_->GetColor(
ThemeProperties::COLOR_TAB_BACKGROUND_ACTIVE_FRAME_ACTIVE),
embedder_->GetColor(
ThemeProperties::COLOR_TAB_BACKGROUND_INACTIVE_FRAME_ACTIVE));
}
void TabStripPageHandler::ActivateTab(int32_t tab_id) {
TabStripModel* tab_strip_model = browser_->tab_strip_model();
for (int index = 0; index < tab_strip_model->count(); ++index) {
content::WebContents* contents = tab_strip_model->GetWebContentsAt(index);
if (extensions::ExtensionTabUtil::GetTabId(contents) == tab_id) {
tab_strip_model->ActivateTabAt(index);
}
}
}
void TabStripPageHandler::OnThemeChanged() {
page_->ThemeChanged();
}
void TabStripPageHandler::OnNativeThemeUpdated(
ui::NativeTheme* observed_theme) {
// There are two types of theme update. a) The observed theme change. e.g.
// switch between light/dark mode. b) A different theme is enabled. e.g.
// switch between GTK and classic theme on Linux. Reset observer in case b).
ui::NativeTheme* current_theme =
webui::GetNativeTheme(web_ui_->GetWebContents());
if (observed_theme != current_theme) {
theme_observation_.Reset();
theme_observation_.Observe(current_theme);
}
page_->ThemeChanged();
}