blob: 92250688f58fbeb67a4b9e004c14777ffc004dd5 [file] [log] [blame]
// Copyright 2012 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/tabs/tab_strip_model.h"
#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <iterator>
#include <map>
#include <memory>
#include <optional>
#include <set>
#include <string>
#include <unordered_set>
#include <utility>
#include <vector>
#include "base/check.h"
#include "base/check_op.h"
#include "base/containers/adapters.h"
#include "base/containers/flat_map.h"
#include "base/containers/span.h"
#include "base/dcheck_is_on.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/weak_ptr.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "base/metrics/user_metrics.h"
#include "base/metrics/user_metrics_action.h"
#include "base/notimplemented.h"
#include "base/notreached.h"
#include "base/observer_list.h"
#include "base/trace_event/common/trace_event_common.h"
#include "base/trace_event/trace_event.h"
#include "base/types/pass_key.h"
#include "build/build_config.h"
#include "chrome/app/chrome_command_ids.h"
#include "chrome/browser/commerce/browser_utils.h"
#include "chrome/browser/content_settings/host_content_settings_map_factory.h"
#include "chrome/browser/extensions/tab_helper.h"
#include "chrome/browser/lifetime/browser_shutdown.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/reading_list/reading_list_model_factory.h"
#include "chrome/browser/resource_coordinator/tab_helper.h"
#include "chrome/browser/send_tab_to_self/send_tab_to_self_util.h"
#include "chrome/browser/ui/bookmarks/bookmark_utils.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_commands.h"
#include "chrome/browser/ui/browser_finder.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/commerce/ui_utils.h"
#include "chrome/browser/ui/send_tab_to_self/send_tab_to_self_bubble.h"
#include "chrome/browser/ui/tabs/features.h"
#include "chrome/browser/ui/tabs/organization/metrics.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_session.h"
#include "chrome/browser/ui/tabs/split_tab_metrics.h"
#include "chrome/browser/ui/tabs/tab_change_type.h"
#include "chrome/browser/ui/tabs/tab_enums.h"
#include "chrome/browser/ui/tabs/tab_group_desktop.h"
#include "chrome/browser/ui/tabs/tab_group_model.h"
#include "chrome/browser/ui/tabs/tab_model.h"
#include "chrome/browser/ui/tabs/tab_strip_model_delegate.h"
#include "chrome/browser/ui/tabs/tab_strip_model_observer.h"
#include "chrome/browser/ui/tabs/tab_strip_user_gesture_details.h"
#include "chrome/browser/ui/tabs/tab_utils.h"
#include "chrome/browser/ui/thumbnails/thumbnail_tab_helper.h"
#include "chrome/browser/ui/ui_features.h"
#include "chrome/browser/ui/user_education/browser_user_education_interface.h"
#include "chrome/browser/ui/views/tabs/dragging/tab_drag_controller.h"
#include "chrome/browser/ui/web_applications/web_app_dialog_utils.h"
#include "chrome/browser/ui/web_applications/web_app_launch_utils.h"
#include "chrome/browser/ui/web_applications/web_app_tabbed_utils.h"
#include "chrome/browser/web_applications/policy/web_app_policy_manager.h"
#include "chrome/browser/web_applications/web_app_provider.h"
#include "chrome/browser/web_applications/web_app_tab_helper.h"
#include "chrome/common/webui_url_constants.h"
#include "components/commerce/core/commerce_utils.h"
#include "components/content_settings/core/browser/host_content_settings_map.h"
#include "components/content_settings/core/common/content_settings.h"
#include "components/content_settings/core/common/content_settings_types.h"
#include "components/feature_engagement/public/feature_constants.h"
#include "components/reading_list/core/reading_list_model.h"
#include "components/tab_groups/tab_group_id.h"
#include "components/tab_groups/tab_group_visual_data.h"
#include "components/tabs/public/pinned_tab_collection.h"
#include "components/tabs/public/split_tab_collection.h"
#include "components/tabs/public/split_tab_data.h"
#include "components/tabs/public/split_tab_id.h"
#include "components/tabs/public/split_tab_visual_data.h"
#include "components/tabs/public/tab_group_tab_collection.h"
#include "components/tabs/public/tab_interface.h"
#include "components/tabs/public/tab_strip_collection.h"
#include "components/tabs/public/unpinned_tab_collection.h"
#include "components/web_modal/web_contents_modal_dialog_manager.h"
#include "components/webapps/common/web_app_id.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/reload_type.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/render_widget_host.h"
#include "content/public/browser/render_widget_host_view.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/url_constants.h"
#include "media/base/media_switches.h"
#include "third_party/perfetto/include/perfetto/tracing/traced_value.h"
#include "ui/base/models/list_selection_model.h"
#include "ui/base/page_transition_types.h"
#include "ui/gfx/range/range.h"
#if BUILDFLAG(ENABLE_GLIC)
#include "chrome/browser/glic/public/glic_enabling.h"
#endif
using base::UserMetricsAction;
using content::WebContents;
namespace {
TabGroupModelFactory* factory_instance = nullptr;
// Works similarly to base::AutoReset but checks for access from the wrong
// thread as well as ensuring that the previous value of the re-entrancy guard
// variable was false.
class ReentrancyCheck {
public:
explicit ReentrancyCheck(bool* guard_flag) : guard_flag_(guard_flag) {
CHECK_CURRENTLY_ON(content::BrowserThread::UI);
CHECK(!*guard_flag_);
*guard_flag_ = true;
}
~ReentrancyCheck() { *guard_flag_ = false; }
private:
const raw_ptr<bool> guard_flag_;
};
// Returns true if the specified transition is one of the types that cause the
// opener relationships for the tab in which the transition occurred to be
// forgotten. This is generally any navigation that isn't a link click (i.e.
// any navigation that can be considered to be the start of a new task distinct
// from what had previously occurred in that tab).
bool ShouldForgetOpenersForTransition(ui::PageTransition transition) {
return ui::PageTransitionCoreTypeIs(transition, ui::PAGE_TRANSITION_TYPED) ||
ui::PageTransitionCoreTypeIs(transition,
ui::PAGE_TRANSITION_AUTO_BOOKMARK) ||
ui::PageTransitionCoreTypeIs(transition,
ui::PAGE_TRANSITION_GENERATED) ||
ui::PageTransitionCoreTypeIs(transition,
ui::PAGE_TRANSITION_KEYWORD) ||
ui::PageTransitionCoreTypeIs(transition,
ui::PAGE_TRANSITION_AUTO_TOPLEVEL);
}
std::vector<int> RangeToVector(gfx::Range range) {
std::vector<int> indices;
indices.reserve(range.length());
for (size_t i = range.start(); i < range.end(); ++i) {
indices.push_back(i);
}
return indices;
}
} // namespace
TabGroupModelFactory::TabGroupModelFactory() {
DCHECK(!factory_instance);
factory_instance = this;
}
// static
TabGroupModelFactory* TabGroupModelFactory::GetInstance() {
if (!factory_instance) {
factory_instance = new TabGroupModelFactory();
}
return factory_instance;
}
std::unique_ptr<TabGroupModel> TabGroupModelFactory::Create() {
return std::make_unique<TabGroupModel>();
}
DetachedTabCollection::DetachedTabCollection(
std::variant<std::unique_ptr<tabs::TabGroupTabCollection>,
std::unique_ptr<tabs::SplitTabCollection>> collection,
std::optional<int> active_index,
bool pinned)
: collection_(std::move(collection)),
active_index_(active_index),
pinned_(pinned) {}
DetachedTabCollection::~DetachedTabCollection() = default;
DetachedTabCollection::DetachedTabCollection(DetachedTabCollection&&) = default;
DetachedTab::DetachedTab(int index_before_any_removals,
int index_at_time_of_removal,
bool was_pinned_at_time_of_removal,
std::unique_ptr<tabs::TabModel> tab,
TabStripModelChange::RemoveReason remove_reason,
tabs::TabInterface::DetachReason tab_detach_reason,
std::optional<SessionID> id)
: tab(std::move(tab)),
index_before_any_removals(index_before_any_removals),
index_at_time_of_removal(index_at_time_of_removal),
was_pinned_at_time_of_removal(was_pinned_at_time_of_removal),
remove_reason(remove_reason),
tab_detach_reason(tab_detach_reason),
id(id) {}
DetachedTab::~DetachedTab() = default;
DetachedTab::DetachedTab(DetachedTab&&) = default;
// Holds all state necessary to send notifications for detached tabs.
struct TabStripModel::DetachNotifications {
DetachNotifications(tabs::TabInterface* initially_active_tab,
const ui::ListSelectionModel& selection_model)
: initially_active_tab(initially_active_tab),
selection_model(selection_model) {}
DetachNotifications(const DetachNotifications&) = delete;
DetachNotifications& operator=(const DetachNotifications&) = delete;
~DetachNotifications() = default;
// The tab that was active prior to any detaches happening. If this
// is nullptr, the active tab was not removed.
//
// It's safe to use a raw pointer here because the active tab, if
// detached, is owned by `detached_tab`.
//
// Once the notification for change of active tab has been sent,
// this field is set to nullptr.
raw_ptr<tabs::TabInterface> initially_active_tab = nullptr;
// The WebContents that were recently detached. Observers need to be notified
// about these. These must be updated after construction.
std::vector<std::unique_ptr<DetachedTab>> detached_tab;
// The selection model prior to any tabs being detached.
const ui::ListSelectionModel selection_model;
};
///////////////////////////////////////////////////////////////////////////////
// TabStripModel, public:
constexpr int TabStripModel::kNoTab;
TabStripModel::TabStripModel(TabStripModelDelegate* delegate,
Profile* profile,
TabGroupModelFactory* group_model_factory)
: delegate_(delegate),
profile_(profile),
selection_model_(std::make_unique<ui::ListSelectionModel>()) {
DCHECK(delegate_);
contents_data_ = std::make_unique<tabs::TabStripCollection>();
if (group_model_factory) {
group_model_ = group_model_factory->Create();
}
scrubbing_metrics_.Init();
}
TabStripModel::~TabStripModel() {
for (auto& observer : observers_) {
observer.ModelDestroyed(TabStripModelObserver::ModelPasskey(), this);
}
}
void TabStripModel::SetTabStripUI(TabStripModelObserver* observer) {
DCHECK(!tab_strip_ui_was_set_);
std::vector<TabStripModelObserver*> new_observers{observer};
for (auto& old_observer : observers_) {
new_observers.push_back(&old_observer);
}
observers_.Clear();
for (auto* new_observer : new_observers) {
observers_.AddObserver(new_observer);
}
observer->StartedObserving(TabStripModelObserver::ModelPasskey(), this);
tab_strip_ui_was_set_ = true;
}
void TabStripModel::AddObserver(TabStripModelObserver* observer) {
observers_.AddObserver(observer);
observer->StartedObserving(TabStripModelObserver::ModelPasskey(), this);
}
void TabStripModel::RemoveObserver(TabStripModelObserver* observer) {
observer->StoppedObserving(TabStripModelObserver::ModelPasskey(), this);
observers_.RemoveObserver(observer);
}
int TabStripModel::count() const {
return contents_data_->TabCountRecursive();
}
bool TabStripModel::empty() const {
return contents_data_->TabCountRecursive() == 0;
}
bool TabStripModel::ContainsIndex(int index) const {
return index >= 0 && index < count();
}
void TabStripModel::AppendWebContents(std::unique_ptr<WebContents> contents,
bool foreground) {
InsertWebContentsAt(
count(), std::move(contents),
foreground ? (ADD_INHERIT_OPENER | ADD_ACTIVE) : ADD_NONE);
}
void TabStripModel::AppendTab(std::unique_ptr<tabs::TabModel> tab,
bool foreground) {
InsertDetachedTabAt(
count(), std::move(tab),
foreground ? (ADD_INHERIT_OPENER | ADD_ACTIVE) : ADD_NONE);
}
int TabStripModel::InsertWebContentsAt(
int index,
std::unique_ptr<WebContents> contents,
int add_types,
std::optional<tab_groups::TabGroupId> group) {
return InsertDetachedTabAt(
index, std::make_unique<tabs::TabModel>(std::move(contents), this),
add_types, group);
}
int TabStripModel::InsertDetachedTabAt(
int index,
std::unique_ptr<tabs::TabModel> tab,
int add_types,
std::optional<tab_groups::TabGroupId> group) {
ReentrancyCheck reentrancy_check(&reentrancy_guard_);
tab->OnAddedToModel(this);
return InsertTabAtImpl(index, std::move(tab), add_types, group);
}
std::unique_ptr<content::WebContents> TabStripModel::DiscardWebContentsAt(
int index,
std::unique_ptr<WebContents> new_contents) {
ReentrancyCheck reentrancy_check(&reentrancy_guard_);
delegate()->WillAddWebContents(new_contents.get());
CHECK(ContainsIndex(index));
FixOpeners(index);
TabStripSelectionChange selection(GetActiveTab(), selection_model());
WebContents* raw_new_contents = new_contents.get();
std::unique_ptr<WebContents> old_contents =
GetTabModelAtIndex(index)->DiscardContents(std::move(new_contents));
// When the active WebContents is replaced send out a selection notification
// too. We do this as nearly all observers need to treat a replacement of the
// selected contents as the selection changing.
if (active_index() == index) {
selection.new_contents = raw_new_contents;
selection.reason = TabStripModelObserver::CHANGE_REASON_REPLACED;
}
TabStripModelChange::Replace replace;
replace.tab = GetTabAtIndex(index);
replace.old_contents = old_contents.get();
replace.new_contents = raw_new_contents;
replace.index = index;
TabStripModelChange change(replace);
OnChange(change, selection);
return old_contents;
}
std::unique_ptr<tabs::TabModel> TabStripModel::DetachTabAtForInsertion(
int index) {
auto dt = DetachTabWithReasonAt(
index, TabStripModelChange::RemoveReason::kInsertedIntoOtherTabStrip,
tabs::TabInterface::DetachReason::kInsertIntoOtherWindow);
return std::move(dt->tab);
}
std::unique_ptr<content::WebContents>
TabStripModel::DetachWebContentsAtForInsertion(int index) {
auto dt = DetachTabWithReasonAt(
index, TabStripModelChange::RemoveReason::kInsertedIntoOtherTabStrip,
tabs::TabInterface::DetachReason::kDelete);
return tabs::TabModel::DestroyAndTakeWebContents(std::move(dt->tab));
}
void TabStripModel::DetachAndDeleteWebContentsAt(int index) {
// Drops the returned unique pointer.
DetachTabWithReasonAt(index, TabStripModelChange::RemoveReason::kDeleted,
tabs::TabInterface::DetachReason::kDelete);
}
std::vector<std::variant<std::unique_ptr<DetachedTab>,
std::unique_ptr<DetachedTabCollection>>>
TabStripModel::DetachTabsAndCollectionsForInsertion(
const std::vector<int>& tab_indices) {
const std::vector<tab_groups::TabGroupId> groups_to_move =
GetGroupsDestroyedFromRemovingIndices(tab_indices);
std::vector<tabs::TabInterface*> tab_interfaces;
for (const int index : tab_indices) {
tab_interfaces.push_back(GetTabAtIndex(index));
}
std::vector<std::variant<std::unique_ptr<DetachedTab>,
std::unique_ptr<DetachedTabCollection>>>
owned_tabs_and_collections;
for (const tabs::TabInterface* tab_interface : tab_interfaces) {
const int index = GetIndexOfTab(tab_interface);
if (index == TabStripModel::kNoTab) {
// If this is a tab, we already moved it as part of its group.
// If this is a header, we will move it when we get to its first tab.
continue;
}
const std::optional<tab_groups::TabGroupId> group =
tab_interface->GetGroup();
if (std::find(groups_to_move.begin(), groups_to_move.end(), group) !=
groups_to_move.end()) {
owned_tabs_and_collections.emplace_back(
DetachTabGroupForInsertion(group.value()));
} else if (tab_interface->IsSplit()) {
owned_tabs_and_collections.emplace_back(
DetachSplitTabForInsertion(tab_interface->GetSplit().value()));
} else {
owned_tabs_and_collections.emplace_back(DetachTabWithReasonAt(
index, TabStripModelChange::RemoveReason::kInsertedIntoOtherTabStrip,
tabs::TabInterface::DetachReason::kInsertIntoOtherWindow));
}
}
return owned_tabs_and_collections;
}
std::unique_ptr<DetachedTab> TabStripModel::DetachTabWithReasonAt(
int index,
TabStripModelChange::RemoveReason web_contents_remove_reason,
tabs::TabInterface::DetachReason tab_detach_reason) {
ReentrancyCheck reentrancy_check(&reentrancy_guard_);
CHECK_NE(active_index(), kNoTab) << "Activate the TabStripModel by "
"selecting at least one tab before "
"trying to detach web contents.";
tabs::TabModel* active_tab_model = GetTabModelAtIndex(active_index());
tabs::TabModel* tab_model = GetTabModelAtIndex(index);
if (index == active_index() && !closing_all_) {
tab_model->WillDeactivate(base::PassKey<TabStripModel>());
}
if (tab_model->IsVisible()) {
tab_model->WillBecomeHidden(base::PassKey<TabStripModel>());
}
tab_model->WillDetach(base::PassKey<TabStripModel>(), tab_detach_reason);
DetachNotifications notifications(active_tab_model, selection_model());
auto dt = DetachTabImpl(index, index,
/*create_historical_tab=*/false,
web_contents_remove_reason, tab_detach_reason);
notifications.detached_tab.push_back(std::move(dt));
SendDetachWebContentsNotifications(&notifications);
return std::move(notifications.detached_tab[0]);
}
std::unique_ptr<DetachedTabCollection>
TabStripModel::DetachTabGroupForInsertion(
const tab_groups::TabGroupId group_id) {
ReentrancyCheck reentrancy_check(&reentrancy_guard_);
CHECK(group_model_);
CHECK(group_model_->ContainsTabGroup(group_id));
std::map<split_tabs::SplitTabId,
std::vector<std::pair<tabs::TabInterface*, int>>>
splits_in_group;
std::optional<int> active_index_in_collection = std::nullopt;
int index = 0;
for (tabs::TabInterface* tab :
*contents_data_->GetTabGroupCollection(group_id)) {
if (tab->IsActivated()) {
active_index_in_collection = index;
}
if (tab->IsSplit()) {
split_tabs::SplitTabId split_id = tab->GetSplit().value();
if (!splits_in_group.contains(split_id)) {
splits_in_group[split_id] = GetTabsAndIndicesInSplit(split_id);
}
}
index++;
}
std::unique_ptr<tabs::TabCollection> detached_collection =
DetachTabCollectionImpl(
contents_data_->GetTabGroupCollection(group_id),
base::BindOnce(&tabs::TabStripCollection::RemoveTabCollection,
base::Unretained(contents_data_.get()),
contents_data_->GetTabGroupCollection(group_id)),
base::BindOnce(&TabStripModel::NotifyTabGroupDetached,
base::Unretained(this),
contents_data_->GetTabGroupCollection(group_id),
splits_in_group));
return std::make_unique<DetachedTabCollection>(
base::WrapUnique(static_cast<tabs::TabGroupTabCollection*>(
detached_collection.release())),
active_index_in_collection, false);
}
std::unique_ptr<DetachedTabCollection>
TabStripModel::DetachSplitTabForInsertion(
const split_tabs::SplitTabId split_id) {
ReentrancyCheck reentrancy_check(&reentrancy_guard_);
CHECK(base::FeatureList::IsEnabled(features::kSideBySide));
CHECK(GetSplitData(split_id));
std::vector<std::pair<tabs::TabInterface*, int>> tabs_in_split =
GetTabsAndIndicesInSplit(split_id);
const bool previous_pinned_state = tabs_in_split[0].first->IsPinned();
const std::optional<tab_groups::TabGroupId> previous_group_state =
tabs_in_split[0].first->GetGroup();
std::optional<int> active_index_in_collection = std::nullopt;
int index = 0;
for (tabs::TabInterface* tab :
*contents_data_->GetSplitTabCollection(split_id)) {
if (tab->IsActivated()) {
active_index_in_collection = index;
break;
}
index++;
}
std::unique_ptr<tabs::TabCollection> detached_collection =
DetachTabCollectionImpl(
contents_data_->GetSplitTabCollection(split_id),
base::BindOnce(&tabs::TabStripCollection::RemoveTabCollection,
base::Unretained(contents_data_.get()),
contents_data_->GetSplitTabCollection(split_id)),
base::BindOnce(&TabStripModel::NotifySplitTabDetached,
base::Unretained(this),
contents_data_->GetSplitTabCollection(split_id),
tabs_in_split, previous_group_state));
return std::make_unique<DetachedTabCollection>(
base::WrapUnique(static_cast<tabs::SplitTabCollection*>(
detached_collection.release())),
active_index_in_collection, previous_pinned_state);
}
gfx::Range TabStripModel::InsertDetachedSplitTabAt(
std::unique_ptr<DetachedTabCollection> split,
int index,
bool pinned,
std::optional<tab_groups::TabGroupId> group_id) {
ReentrancyCheck reentrancy_check(&reentrancy_guard_);
CHECK(std::holds_alternative<std::unique_ptr<tabs::SplitTabCollection>>(
split->collection_));
std::unique_ptr<tabs::SplitTabCollection> split_collection_unique_ptr =
std::move(std::get<std::unique_ptr<tabs::SplitTabCollection>>(
split->collection_));
tabs::SplitTabCollection* split_collection =
split_collection_unique_ptr.get();
// Check a split with the same id is not present in the `contents_data_`.
CHECK(!contents_data_->GetSplitTabCollection(
split_collection->GetSplitTabId()));
// Notify tab is added to model.
for (tabs::TabInterface* tab : *split_collection) {
static_cast<tabs::TabModel*>(tab)->OnAddedToModel(this);
}
return InsertDetachedCollectionImpl(
split_collection, split->active_index_,
base::BindOnce(&tabs::TabStripCollection::InsertTabCollectionAt,
base::Unretained(contents_data_.get()),
std::move(split_collection_unique_ptr), index, pinned,
group_id),
base::BindOnce(&TabStripModel::NotifySplitTabAttached,
base::Unretained(this), split_collection));
}
gfx::Range TabStripModel::InsertDetachedTabGroupAt(
std::unique_ptr<DetachedTabCollection> group,
int index) {
ReentrancyCheck reentrancy_check(&reentrancy_guard_);
CHECK(group_model_);
CHECK(std::holds_alternative<std::unique_ptr<tabs::TabGroupTabCollection>>(
group->collection_));
std::unique_ptr<tabs::TabGroupTabCollection> group_collection_unique_ptr =
std::move(std::get<std::unique_ptr<tabs::TabGroupTabCollection>>(
group->collection_));
tabs::TabGroupTabCollection* group_collection =
group_collection_unique_ptr.get();
CHECK(!group_model_->ContainsTabGroup(group_collection->GetTabGroupId()));
// Notify tab is added to model.
for (tabs::TabInterface* tab : *(group_collection)) {
static_cast<tabs::TabModel*>(tab)->OnAddedToModel(this);
}
index = ConstrainInsertionIndex(index, false);
return InsertDetachedCollectionImpl(
group_collection, group->active_index_,
base::BindOnce(&TabStripModel::InsertDetachedTabGroupImpl,
base::Unretained(this),
std::move(group_collection_unique_ptr), index),
base::BindOnce(&TabStripModel::NotifyTabGroupAttached,
base::Unretained(this), group_collection));
}
tabs::TabModel* TabStripModel::GetTabModelAtIndex(int index) const {
return static_cast<tabs::TabModel*>(GetTabAtIndex(index));
}
void TabStripModel::OnChange(const TabStripModelChange& change,
const TabStripSelectionChange& selection) {
OnActiveTabChanged(selection);
for (auto& observer : observers_) {
observer.OnTabStripModelChanged(this, change, selection);
}
}
TabStripModelChange::Remove TabStripModel::ProcessTabsForDetach(
gfx::Range tab_indices) {
TabStripModelChange::Remove remove;
tabs::TabModel* active_tab_model = GetTabModelAtIndex(active_index());
for (int index = tab_indices.end() - 1;
index >= static_cast<int>(tab_indices.start()); index--) {
tabs::TabModel* tab = GetTabModelAtIndex(index);
// If the tab is active, notify it that it's going to be deactivated:
if (tab == active_tab_model) {
tab->WillDeactivate(base::PassKey<TabStripModel>());
}
// If the tab is visible, notify it that it's going to be hidden:
if (tab->IsVisible()) {
tab->WillBecomeHidden(base::PassKey<TabStripModel>());
}
// Tell the tab it’s being detached (inserted into another window).
tab->WillDetach(base::PassKey<TabStripModel>(),
tabs::TabInterface::DetachReason::kInsertIntoOtherWindow);
// Notify observers that the tab will be removed.
for (auto& observer : observers_) {
observer.OnTabWillBeRemoved(tab->GetContents(), index);
}
// Fix opener relationships before removing the tab.
FixOpeners(index);
// Record this removal in the `Remove` event payload.
remove.contents.emplace_back(
tab, index,
TabStripModelChange::RemoveReason::kInsertedIntoOtherTabStrip,
tabs::TabInterface::DetachReason::kInsertIntoOtherWindow, std::nullopt);
}
return remove;
}
void TabStripModel::UpdateSelectionModelForDetach(
gfx::Range tab_indices,
std::optional<int> next_selected_index) {
const bool closed_all_tabs = (GetTabCount() == 0);
bool active_tab_removed = tab_indices.Contains(gfx::Range(active_index()));
if (closed_all_tabs) {
selection_model_->Clear();
} else {
// Remove all the selected tabs from the model.
for (int index = static_cast<int>(tab_indices.end()) - 1;
index >= static_cast<int>(tab_indices.start()); --index) {
selection_model_->DecrementFrom(index);
}
if (active_tab_removed) {
if (!selection_model_->empty()) {
selection_model_->set_active(
*selection_model_->selected_indices().begin());
selection_model_->set_anchor(selection_model_->active());
} else {
SetSelectedIndex(selection_model_.get(), next_selected_index.value());
}
}
}
}
std::unique_ptr<tabs::TabCollection> TabStripModel::DetachTabCollectionImpl(
tabs::TabCollection* collection,
base::OnceCallback<std::unique_ptr<tabs::TabCollection>()>
execute_detach_collection_operation,
base::OnceClosure execute_tabs_notify_observer_operation) {
// Get Tabs and Indices in collection
std::vector<int> tabs_in_collection;
const int collection_start_index =
GetIndexOfTab(collection->GetTabAtIndexRecursive(0));
gfx::Range tab_indices =
gfx::Range(collection_start_index,
collection_start_index + collection->TabCountRecursive());
std::optional<int> next_selected_index =
DetermineNewSelectedIndex(collection);
tabs::TabModel* active_tab_model = GetTabModelAtIndex(active_index());
const ui::ListSelectionModel old_selection_model = selection_model();
bool selected_tabs_removed = std::any_of(
selection_model_->selected_indices().begin(),
selection_model_->selected_indices().end(),
[&](int sel) { return tab_indices.Contains(gfx::Range(sel)); });
// Pass the indices vector from above.
TabStripModelChange::Remove remove = ProcessTabsForDetach(tab_indices);
// Call the callback for detaching collection.
std::unique_ptr<tabs::TabCollection> detached_collection =
std::move(execute_detach_collection_operation).Run();
// Pass the indices vector from above.
UpdateSelectionModelForDetach(tab_indices, next_selected_index);
ValidateTabStripModel();
// Call the callback for collection detached.
std::move(execute_tabs_notify_observer_operation).Run();
// Notify tab is removed from model
for (tabs::TabInterface* tab : *collection) {
static_cast<tabs::TabModel*>(tab)->OnRemovedFromModel();
}
// TODO(crbug.com/418764233): Integrate with
// SendDetachWebContentsNotifications
TabStripModelChange change(std::move(remove));
TabStripSelectionChange selection(active_tab_model, old_selection_model);
selection.new_tab = GetActiveTab();
selection.new_contents = GetActiveWebContents();
selection.new_model = selection_model();
selection.reason = TabStripModelObserver::CHANGE_REASON_NONE;
selection.selected_tabs_were_removed = selected_tabs_removed;
OnChange(change, selection);
if (empty()) {
for (auto& observer : observers_) {
observer.TabStripEmpty();
}
}
return detached_collection;
}
gfx::Range TabStripModel::InsertDetachedCollectionImpl(
tabs::TabCollection* collection,
std::optional<int> active_index,
base::OnceClosure execute_insert_detached_tabs_operation,
base::OnceClosure execute_tabs_notify_observer_operation) {
for (const tabs::TabInterface* tab : *collection) {
delegate()->WillAddWebContents(tab->GetContents());
}
tabs::TabInterface* old_active_tab = GetActiveTab();
const bool tab_strip_empty_initially = empty();
// Add the collection.
std::move(execute_insert_detached_tabs_operation).Run();
int collection_insertion_index =
GetIndexOfTab(collection->GetTabAtIndexRecursive(0));
// Update selection model.
for (int i = collection_insertion_index;
i < collection_insertion_index +
static_cast<int>(collection->TabCountRecursive());
i++) {
selection_model_->IncrementFrom(collection_insertion_index);
}
TabStripSelectionChange selection(old_active_tab, selection_model());
if (active_index.has_value()) {
SetSelectedIndex(selection_model_.get(),
collection_insertion_index + active_index.value());
} else if (tab_strip_empty_initially) {
SetSelectedIndex(selection_model_.get(), collection_insertion_index);
}
ValidateTabStripModel();
for (tabs::TabInterface* tab : *collection) {
static_cast<tabs::TabModel*>(tab)->DidInsert(
base::PassKey<TabStripModel>());
}
// Send add notifications for tabs.
selection.new_model = selection_model();
selection.new_tab = GetActiveTab();
selection.new_contents = GetActiveWebContents();
TabStripModelChange::Insert insert;
for (int index_of_tab = GetIndexOfTab(*(collection->begin()));
tabs::TabInterface* tab : *collection) {
insert.contents.push_back({tab, tab->GetContents(), index_of_tab});
index_of_tab++;
}
TabStripModelChange change(std::move(insert));
OnChange(change, selection);
// observer callback
std::move(execute_tabs_notify_observer_operation).Run();
return gfx::Range(
collection_insertion_index,
collection_insertion_index + collection->TabCountRecursive());
}
void TabStripModel::InsertDetachedTabGroupImpl(
std::unique_ptr<tabs::TabGroupTabCollection> group_collection,
int index) {
group_model_->AddTabGroup(group_collection->GetTabGroup(),
base::PassKey<TabStripModel>());
contents_data_->InsertTabCollectionAt(std::move(group_collection), index,
false, std::nullopt);
}
std::unique_ptr<DetachedTab> TabStripModel::DetachTabImpl(
int index_before_any_removals,
int index_at_time_of_removal,
bool create_historical_tab,
TabStripModelChange::RemoveReason web_contents_remove_reason,
tabs::TabInterface::DetachReason tab_detach_reason) {
if (empty()) {
return nullptr;
}
CHECK(ContainsIndex(index_at_time_of_removal));
tabs::TabModel* tab = GetTabModelAtIndex(index_at_time_of_removal);
const bool was_pinned_at_time_of_removal = tab->IsPinned();
for (auto& observer : observers_) {
observer.OnTabWillBeRemoved(tab->GetContents(), index_at_time_of_removal);
}
FixOpeners(index_at_time_of_removal);
// Ask the delegate to save an entry for this tab in the historical tab
// database.
std::optional<SessionID> id = std::nullopt;
if (create_historical_tab) {
id = delegate_->CreateHistoricalTab(tab->GetContents());
}
std::unique_ptr<tabs::TabModel> old_tab_model =
RemoveTabFromIndexImpl(index_at_time_of_removal, tab_detach_reason);
old_tab_model->OnRemovedFromModel();
return std::make_unique<DetachedTab>(
index_before_any_removals, index_at_time_of_removal,
was_pinned_at_time_of_removal, std::move(old_tab_model),
web_contents_remove_reason, tab_detach_reason, id);
}
void TabStripModel::SendDetachWebContentsNotifications(
DetachNotifications* notifications) {
// Sort the DetachedTab in decreasing order of
// |index_before_any_removals|. This is because |index_before_any_removals| is
// used by observers to update their own copy of TabStripModel state, and each
// removal affects subsequent removals of higher index.
std::sort(
notifications->detached_tab.begin(), notifications->detached_tab.end(),
[](const std::unique_ptr<DetachedTab>& dt1,
const std::unique_ptr<DetachedTab>& dt2) {
return dt1->index_before_any_removals > dt2->index_before_any_removals;
});
// `change` must be deleted before the unique_ptr<Tab>s in `notifications` are
// reset, or their raw_ptr<Tab>s will dangle.
{
TabStripModelChange::Remove remove;
for (auto& dt : notifications->detached_tab) {
remove.contents.emplace_back(dt->tab.get(), dt->index_before_any_removals,
dt->remove_reason, dt->tab_detach_reason,
dt->id);
}
TabStripModelChange change(std::move(remove));
TabStripSelectionChange selection;
selection.old_tab = notifications->initially_active_tab;
selection.new_tab = GetActiveTab();
selection.old_contents =
selection.old_tab ? selection.old_tab->GetContents() : nullptr;
selection.new_contents = GetActiveWebContents();
selection.old_model = notifications->selection_model;
selection.new_model = selection_model();
selection.reason = TabStripModelObserver::CHANGE_REASON_NONE;
selection.selected_tabs_were_removed = std::ranges::any_of(
notifications->detached_tab, [&notifications](auto& dt) {
return notifications->selection_model.IsSelected(
dt->index_before_any_removals);
});
OnChange(change, selection);
// Prevent this from dangling in case a detached tab was initially active.
notifications->initially_active_tab = nullptr;
}
for (auto& dt : notifications->detached_tab) {
if (dt->remove_reason == TabStripModelChange::RemoveReason::kDeleted) {
// This destroys the WebContents, which will also send
// WebContentsDestroyed notifications.
dt->tab.reset();
}
}
if (empty()) {
for (auto& observer : observers_) {
observer.TabStripEmpty();
}
}
}
void TabStripModel::ActivateTabAt(int index,
TabStripUserGestureDetails user_gesture) {
ReentrancyCheck reentrancy_check(&reentrancy_guard_);
CHECK(ContainsIndex(index));
TRACE_EVENT0("ui", "TabStripModel::ActivateTabAt");
scrubbing_metrics_.IncrementPressCount(user_gesture);
ui::ListSelectionModel new_model(*selection_model_.get());
SetSelectedIndex(&new_model, index);
SetSelection(
std::move(new_model),
user_gesture.type != TabStripUserGestureDetails::GestureType::kNone
? TabStripModelObserver::CHANGE_REASON_USER_GESTURE
: TabStripModelObserver::CHANGE_REASON_NONE,
/*triggered_by_other_operation=*/false);
}
int TabStripModel::MoveWebContentsAt(int index,
int to_position,
bool select_after_move) {
ReentrancyCheck reentrancy_check(&reentrancy_guard_);
CHECK(ContainsIndex(index));
const bool pinned = IsTabPinned(index);
to_position = ConstrainMoveIndex(to_position, pinned);
if (index == to_position) {
return to_position;
}
std::optional<tab_groups::TabGroupId> group =
GetGroupToAssign(index, to_position);
MoveTabToIndexImpl(index, to_position, group, pinned, select_after_move);
return to_position;
}
int TabStripModel::MoveWebContentsAt(
int index,
int to_position,
bool select_after_move,
std::optional<tab_groups::TabGroupId> group) {
ReentrancyCheck reentrancy_check(&reentrancy_guard_);
CHECK(ContainsIndex(index));
bool pinned = IsTabPinned(index);
to_position = ConstrainMoveIndex(to_position, pinned);
MoveTabToIndexImpl(index, to_position, group, pinned, select_after_move);
return to_position;
}
void TabStripModel::MoveSelectedTabsTo(
int index,
std::optional<tab_groups::TabGroupId> into_group) {
ReentrancyCheck reentrancy_check(&reentrancy_guard_);
const int pinned_tab_count = IndexOfFirstNonPinnedTab();
const std::vector<int> pinned_selected_indices = GetSelectedPinnedTabs();
const std::vector<int> unpinned_selected_indices = GetSelectedUnpinnedTabs();
const int last_pinned_index =
std::clamp(index + static_cast<int>(pinned_selected_indices.size()) - 1,
static_cast<int>(pinned_selected_indices.size()) - 1,
pinned_tab_count - 1);
MoveTabsToIndexImpl(
pinned_selected_indices,
last_pinned_index - static_cast<int>(pinned_selected_indices.size()) + 1,
std::nullopt);
const int first_unpinned_index =
std::clamp(index + static_cast<int>(pinned_selected_indices.size()),
pinned_tab_count,
count() - static_cast<int>(unpinned_selected_indices.size()));
MoveTabsToIndexImpl(unpinned_selected_indices, first_unpinned_index,
into_group);
}
void TabStripModel::MoveGroupTo(const tab_groups::TabGroupId& group,
int to_index) {
ReentrancyCheck reentrancy_check(&reentrancy_guard_);
CHECK_NE(to_index, kNoTab);
to_index = ConstrainMoveIndex(to_index, false /* pinned tab */);
if (!group_model_) {
return;
}
const gfx::Range tabs_in_group = group_model_->GetTabGroup(group)->ListTabs();
if (static_cast<int>(tabs_in_group.start()) == to_index) {
return;
}
std::optional<split_tabs::SplitTabId> destination_split =
MoveBreaksSplitContiguity(static_cast<int>(tabs_in_group.start()),
tabs_in_group.length(), to_index);
if (destination_split.has_value()) {
RemoveSplitImpl(destination_split.value(),
SplitTabChange::SplitTabRemoveReason::kSplitTabRemoved);
}
MoveGroupToImpl(group, to_index);
}
void TabStripModel::MoveSplitTo(
const split_tabs::SplitTabId& split_id,
int to_index,
bool pinned,
std::optional<tab_groups::TabGroupId> group_id) {
ReentrancyCheck reentrancy_check(&reentrancy_guard_);
static const std::set<tabs::TabCollection::Type> retain_collection_types =
std::set<tabs::TabCollection::Type>({tabs::TabCollection::Type::SPLIT});
CHECK_NE(to_index, kNoTab);
if (!group_model_ && group_id.has_value()) {
return;
}
to_index = ConstrainMoveIndex(to_index, pinned);
std::vector<std::pair<tabs::TabInterface*, int>> tabs_with_indices =
GetTabsAndIndicesInSplit(split_id);
// Invalid to move an to an index that breaks another split.
std::optional<split_tabs::SplitTabId> destination_split =
MoveBreaksSplitContiguity(tabs_with_indices[0].second,
tabs_with_indices.size(), to_index);
CHECK(!destination_split.has_value());
std::vector<int> tab_indices;
for (const auto& tab_pair : tabs_with_indices) {
tab_indices.push_back(tab_pair.second);
}
MoveTabsWithNotifications(
tab_indices, to_index,
base::BindOnce(&tabs::TabStripCollection::MoveTabsRecursive,
base::Unretained(contents_data_.get()), tab_indices,
to_index, group_id, pinned, retain_collection_types));
}
void TabStripModel::MoveGroupToImpl(const tab_groups::TabGroupId& group,
int to_index) {
const gfx::Range tabs_in_group = group_model_->GetTabGroup(group)->ListTabs();
CHECK_GT(tabs_in_group.length(), 0u);
std::vector<int> tab_indices;
for (size_t i = tabs_in_group.start(); i < tabs_in_group.end(); ++i) {
tab_indices.push_back(i);
}
// Remove all the tabs from the model.
MoveTabsWithNotifications(
tab_indices, to_index,
base::BindOnce(&tabs::TabStripCollection::MoveTabGroupTo,
base::Unretained(contents_data_.get()), group, to_index));
NotifyTabGroupMoved(group);
}
WebContents* TabStripModel::GetActiveWebContents() const {
return GetWebContentsAt(active_index());
}
tabs::TabInterface* TabStripModel::GetActiveTab() const {
const int index = active_index();
if (ContainsIndex(index)) {
return GetTabAtIndex(index);
}
return nullptr;
}
std::vector<tabs::TabInterface*> TabStripModel::GetForegroundTabs() const {
tabs::TabInterface* active_tab = GetActiveTab();
if (!active_tab) {
return std::vector<tabs::TabInterface*>();
}
if (active_tab->IsSplit()) {
return GetSplitData(active_tab->GetSplit().value())->ListTabs();
}
return std::vector<tabs::TabInterface*>{active_tab};
}
WebContents* TabStripModel::GetWebContentsAt(int index) const {
if (ContainsIndex(index)) {
return GetTabAtIndex(index)->GetContents();
}
return nullptr;
}
int TabStripModel::GetIndexOfWebContents(const WebContents* contents) const {
int index = 0;
for (const tabs::TabInterface* tab : *this) {
if (tab->GetContents() == contents) {
return index;
}
index++;
}
return kNoTab;
}
void TabStripModel::NotifyTabChanged(const tabs::TabInterface* const tab,
TabChangeType change_type) {
const int index = GetIndexOfTab(tab);
for (auto& observer : observers_) {
observer.TabChangedAt(tab->GetContents(), index, change_type);
}
}
void TabStripModel::UpdateWebContentsStateAt(int index,
TabChangeType change_type) {
const tabs::TabInterface* const tab = GetTabAtIndex(index);
for (auto& observer : observers_) {
observer.TabChangedAt(tab->GetContents(), index, change_type);
}
}
void TabStripModel::SetTabNeedsAttentionAt(int index, bool attention) {
CHECK(ContainsIndex(index));
for (auto& observer : observers_) {
observer.SetTabNeedsAttentionAt(index, attention);
}
}
void TabStripModel::SetTabGroupNeedsAttention(
const tab_groups::TabGroupId& group,
bool attention) {
CHECK(group_model_->ContainsTabGroup(group));
for (auto& observer : observers_) {
observer.SetTabGroupNeedsAttention(group, attention);
}
}
void TabStripModel::CloseAllTabs() {
ReentrancyCheck reentrancy_check(&reentrancy_guard_);
// Set state so that observers can adjust their behavior to suit this
// specific condition when CloseWebContentsAt causes a flurry of
// Close/Detach/Select notifications to be sent.
closing_all_ = true;
std::vector<content::WebContents*> closing_tabs;
closing_tabs.reserve(count());
for (std::vector<tabs::TabInterface*> tabs =
contents_data_->GetTabsRecursive();
tabs::TabInterface* tab : base::Reversed(tabs)) {
closing_tabs.push_back(tab->GetContents());
}
CloseTabs(closing_tabs, TabCloseTypes::CLOSE_CREATE_HISTORICAL_TAB);
}
void TabStripModel::CloseAllTabsInGroup(const tab_groups::TabGroupId& group) {
ReentrancyCheck reentrancy_check(&reentrancy_guard_);
if (!group_model_) {
return;
}
delegate_->WillCloseGroup(group);
for (TabStripModelObserver& observer : observers_) {
observer.OnTabGroupWillBeRemoved(group);
}
TabGroup* const tab_group = group_model_->GetTabGroup(group);
tab_group->SetGroupIsClosing(/*is_closing=*/true);
gfx::Range tabs_in_group = tab_group->ListTabs();
if (static_cast<int>(tabs_in_group.length()) == count()) {
closing_all_ = true;
}
std::vector<content::WebContents*> closing_tabs;
closing_tabs.reserve(tabs_in_group.length());
for (uint32_t i = tabs_in_group.end(); i > tabs_in_group.start(); --i) {
closing_tabs.push_back(GetWebContentsAt(i - 1));
}
CloseTabs(closing_tabs, TabCloseTypes::CLOSE_CREATE_HISTORICAL_TAB);
}
void TabStripModel::CloseWebContentsAt(int index, uint32_t close_types) {
CHECK(ContainsIndex(index));
CloseTabs({GetWebContentsAt(index)}, close_types);
}
bool TabStripModel::TabsNeedLoadingUI() const {
for (const tabs::TabInterface* tab : *this) {
if (tab->GetContents()->ShouldShowLoadingUI()) {
return true;
}
}
return false;
}
tabs::TabInterface* TabStripModel::GetOpenerOfTabAt(const int index) const {
CHECK(ContainsIndex(index));
const tabs::TabModel* const tab = GetTabModelAtIndex(index);
return tab->opener();
}
void TabStripModel::SetOpenerOfWebContentsAt(int index, WebContents* opener) {
CHECK(ContainsIndex(index));
// The TabStripModel only maintains the references to openers that it itself
// owns; trying to set an opener to an external WebContents can result in
// the opener being used after its freed. See crbug.com/698681.
DCHECK(!opener || GetIndexOfWebContents(opener) != kNoTab)
<< "Cannot set opener to a web contents not owned by this tab strip.";
GetTabModelAtIndex(index)->set_opener(GetTabForWebContents(opener));
}
int TabStripModel::GetIndexOfLastWebContentsOpenedBy(const WebContents* opener,
int start_index) const {
DCHECK(opener);
CHECK(ContainsIndex(start_index));
std::set<const WebContents*> opener_and_descendants;
opener_and_descendants.insert(opener);
int last_index = kNoTab;
for (int i = start_index + 1; i < count(); ++i) {
tabs::TabModel* tab = GetTabModelAtIndex(i);
// Test opened by transitively, i.e. include tabs opened by tabs opened by
// opener, etc. Stop when we find the first non-descendant.
if (!opener_and_descendants.count(
tab->opener() ? tab->opener()->GetContents() : nullptr)) {
// Skip over pinned tabs as new tabs are added after pinned tabs.
if (tab->IsPinned()) {
continue;
}
break;
}
opener_and_descendants.insert(tab->GetContents());
last_index = i;
}
return last_index;
}
void TabStripModel::TabNavigating(WebContents* contents,
ui::PageTransition transition) {
if (ShouldForgetOpenersForTransition(transition)) {
// Don't forget the openers if this tab is a New Tab page opened at the
// end of the TabStrip (e.g. by pressing Ctrl+T). Give the user one
// navigation of one of these transition types before resetting the
// opener relationships (this allows for the use case of opening a new
// tab to do a quick look-up of something while viewing a tab earlier in
// the strip). We can make this heuristic more permissive if need be.
if (!IsNewTabAtEndOfTabStrip(contents)) {
// If the user navigates the current tab to another page in any way
// other than by clicking a link, we want to pro-actively forget all
// TabStrip opener relationships since we assume they're beginning a
// different task by reusing the current tab.
ForgetAllOpeners();
}
}
}
void TabStripModel::SetTabBlocked(int index, bool blocked) {
CHECK(ContainsIndex(index));
tabs::TabModel* tab_model = GetTabModelAtIndex(index);
if (tab_model->blocked() == blocked) {
return;
}
tab_model->set_blocked(blocked);
for (auto& observer : observers_) {
observer.TabBlockedStateChanged(tab_model->GetContents(), index);
}
}
int TabStripModel::SetTabPinned(int index, bool pinned) {
ReentrancyCheck reentrancy_check(&reentrancy_guard_);
CHECK(ContainsIndex(index));
if (IsTabPinned(index) == pinned) {
return index;
}
return SetTabPinnedImpl(index, pinned);
}
bool TabStripModel::IsTabPinned(int index) const {
CHECK(ContainsIndex(index)) << index;
return index < IndexOfFirstNonPinnedTab();
}
bool TabStripModel::IsTabCollapsed(int index) const {
std::optional<tab_groups::TabGroupId> group = GetTabGroupForTab(index);
return group.has_value() && IsGroupCollapsed(group.value());
}
bool TabStripModel::IsGroupCollapsed(
const tab_groups::TabGroupId& group) const {
DCHECK(group_model_);
return group_model()->ContainsTabGroup(group) &&
group_model()->GetTabGroup(group)->visual_data()->is_collapsed();
}
std::optional<split_tabs::SplitTabId> TabStripModel::GetSplitForTab(
int index) const {
CHECK(ContainsIndex(index));
return GetTabAtIndex(index)->GetSplit();
}
bool TabStripModel::IsTabBlocked(int index) const {
CHECK(ContainsIndex(index)) << index;
return GetTabModelAtIndex(index)->blocked();
}
bool TabStripModel::IsTabInForeground(int index) const {
if (!ContainsIndex(index)) {
return false;
}
const tabs::TabInterface *active_tab = GetActiveTab();
if (!active_tab) {
return false;
}
if (active_tab->IsSplit()) {
const gfx::Range index_range =
GetIndexRangeOfSplit(active_tab->GetSplit().value());
// If the active tab is a split, check if the index is within the range of
// the split since all of these tabs are in the foreground.
return (index >= static_cast<int>(index_range.GetMin()) &&
index < static_cast<int>(index_range.GetMax()));
}
return active_index() == index;
}
bool TabStripModel::IsTabClosable(int index) const {
return PolicyAllowsTabClosing(GetWebContentsAt(index));
}
bool TabStripModel::IsTabClosable(const content::WebContents* contents) const {
return IsTabClosable(GetIndexOfWebContents(contents));
}
std::optional<tab_groups::TabGroupId> TabStripModel::GetTabGroupForTab(
int index) const {
return ContainsIndex(index) ? GetTabAtIndex(index)->GetGroup() : std::nullopt;
}
std::optional<tab_groups::TabGroupId> TabStripModel::GetActiveTabGroupId()
const {
return GetTabGroupForTab(active_index());
}
std::optional<tab_groups::TabGroupId> TabStripModel::GetSurroundingTabGroup(
int index) const {
if (!ContainsIndex(index - 1) || !ContainsIndex(index)) {
return std::nullopt;
}
// If the tab before is not in a group, a tab inserted at |index|
// wouldn't be surrounded by one group.
std::optional<tab_groups::TabGroupId> group = GetTabGroupForTab(index - 1);
if (!group) {
return std::nullopt;
}
// If the tab after is in a different (or no) group, a new tab at
// |index| isn't surrounded.
if (group != GetTabGroupForTab(index)) {
return std::nullopt;
}
return group;
}
int TabStripModel::IndexOfFirstNonPinnedTab() const {
return contents_data_->IndexOfFirstNonPinnedTab();
}
void TabStripModel::ExtendSelectionTo(int index) {
CHECK(ContainsIndex(index));
ui::ListSelectionModel new_model(*selection_model_.get());
if (!selection_model().anchor().has_value()) {
SetSelectedIndex(&new_model, index);
} else {
new_model.SetSelectionFromAnchorTo(index);
// Potentially expand the initial selection to capture any split tabs at the
// anchor or index.
std::pair<int, int> selection_range =
GetSelectionRangeFromAnchorToIndex(index);
new_model.AddIndexRangeToSelection(selection_range.first,
selection_range.second);
}
SetSelection(std::move(new_model), TabStripModelObserver::CHANGE_REASON_NONE,
/*triggered_by_other_operation=*/false);
}
void TabStripModel::SelectTabAt(int index) {
if (!delegate()->IsTabStripEditable()) {
return;
}
CHECK(ContainsIndex(index));
const size_t selection_index = static_cast<size_t>(index);
ui::ListSelectionModel new_model = selection_model();
if (std::optional<split_tabs::SplitTabId> split_id = GetSplitForTab(index);
split_id.has_value()) {
gfx::Range index_range = GetIndexRangeOfSplit(split_id.value());
new_model.AddIndexRangeToSelection(index_range.start(),
index_range.end() - 1);
} else {
new_model.AddIndexToSelection(selection_index);
}
new_model.set_anchor(selection_index);
new_model.set_active(selection_index);
SetSelection(std::move(new_model), TabStripModelObserver::CHANGE_REASON_NONE,
/*triggered_by_other_operation=*/false);
}
void TabStripModel::DeselectTabAt(int index) {
if (!delegate()->IsTabStripEditable()) {
return;
} else if (!IsTabSelected(index)) {
// If the tab is already deselected, no need to do anything.
return;
} else if (selection_model().size() == 1 ||
(selection_model().size() == 2 &&
GetSplitForTab(index).has_value())) {
// One tab must be selected and this tab is currently selected so we can't
// unselect it.
return;
}
CHECK(ContainsIndex(index));
const size_t selection_index = static_cast<size_t>(index);
ui::ListSelectionModel new_model = selection_model();
if (std::optional<split_tabs::SplitTabId> split_id = GetSplitForTab(index);
split_id.has_value()) {
for (auto [_, i] : GetTabsAndIndicesInSplit(split_id.value())) {
new_model.RemoveIndexFromSelection(i);
}
} else {
new_model.RemoveIndexFromSelection(selection_index);
}
new_model.set_anchor(selection_index);
if (!new_model.active().has_value() ||
new_model.active() == selection_index) {
new_model.set_active(*new_model.selected_indices().begin());
}
SetSelection(std::move(new_model), TabStripModelObserver::CHANGE_REASON_NONE,
/*triggered_by_other_operation=*/false);
}
void TabStripModel::AddSelectionFromAnchorTo(int index) {
ui::ListSelectionModel new_model(*selection_model_.get());
if (!selection_model().anchor().has_value()) {
SetSelectedIndex(&new_model, index);
} else {
std::pair<int, int> selection_range =
GetSelectionRangeFromAnchorToIndex(index);
new_model.AddIndexRangeToSelection(selection_range.first,
selection_range.second);
new_model.set_active(index);
}
SetSelection(std::move(new_model), TabStripModelObserver::CHANGE_REASON_NONE,
/*triggered_by_other_operation=*/false);
}
bool TabStripModel::IsTabSelected(int index) const {
CHECK(ContainsIndex(index));
return selection_model().IsSelected(index);
}
void TabStripModel::SetSelectionFromModel(ui::ListSelectionModel source) {
CHECK(source.active().has_value());
const ui::ListSelectionModel::SelectedIndices sel = source.selected_indices();
for (auto& source_sel_index : sel) {
if (std::optional<split_tabs::SplitTabId> split_id =
GetSplitForTab(source_sel_index);
split_id.has_value()) {
gfx::Range index_range = GetIndexRangeOfSplit(split_id.value());
source.AddIndexRangeToSelection(index_range.start(),
index_range.end() - 1);
}
}
SetSelection(std::move(source), TabStripModelObserver::CHANGE_REASON_NONE,
/*triggered_by_other_operation=*/false);
}
const ui::ListSelectionModel& TabStripModel::selection_model() const {
return *selection_model_.get();
}
bool TabStripModel::CanShowModalUI() const {
return !showing_modal_ui_;
}
std::unique_ptr<ScopedTabStripModalUI> TabStripModel::ShowModalUI() {
return std::make_unique<ScopedTabStripModalUIImpl>(this);
}
void TabStripModel::ForceShowingModalUIForTesting(bool showing) {
showing_modal_ui_ = showing;
}
void TabStripModel::AddWebContents(
std::unique_ptr<WebContents> contents,
int index,
ui::PageTransition transition,
int add_types,
std::optional<tab_groups::TabGroupId> group) {
auto tab = std::make_unique<tabs::TabModel>(std::move(contents), this);
AddTab(std::move(tab), index, transition, add_types, group);
}
void TabStripModel::AddTab(std::unique_ptr<tabs::TabModel> tab,
int index,
ui::PageTransition transition,
int add_types,
std::optional<tab_groups::TabGroupId> group) {
for (auto& observer : observers_) {
observer.OnTabWillBeAdded();
}
ReentrancyCheck reentrancy_check(&reentrancy_guard_);
// If the newly-opened tab is part of the same task as the parent tab, we want
// to inherit the parent's opener attribute, so that if this tab is then
// closed we'll jump back to the parent tab.
bool inherit_opener = (add_types & ADD_INHERIT_OPENER) == ADD_INHERIT_OPENER;
if (ui::PageTransitionTypeIncludingQualifiersIs(transition,
ui::PAGE_TRANSITION_LINK) &&
(add_types & ADD_FORCE_INDEX) == 0) {
// We assume tabs opened via link clicks are part of the same task as their
// parent. Note that when |force_index| is true (e.g. when the user
// drag-and-drops a link to the tab strip), callers aren't really handling
// link clicks, they just want to score the navigation like a link click in
// the history backend, so we don't inherit the opener in this case.
index = DetermineInsertionIndex(transition, add_types & ADD_ACTIVE);
inherit_opener = true;
// The current active index is our opener. If the tab we are adding is not
// in a group, set the group of the tab to that of its opener.
if (!group.has_value()) {
group = GetTabGroupForTab(active_index());
}
} else {
// For all other types, respect what was passed to us, normalizing -1s and
// values that are too large.
if (index < 0 || index > count()) {
index = count();
}
}
// Prevent the tab from being inserted at an index that would make the group
// non-contiguous. Most commonly, the new-tab button always attempts to insert
// at the end of the tab strip. Extensions can insert at an arbitrary index,
// so we have to handle the general case.
if (group_model_) {
if (group.has_value()) {
gfx::Range grouped_tabs =
group_model_->GetTabGroup(group.value())->ListTabs();
if (grouped_tabs.length() > 0) {
index = std::clamp(index, static_cast<int>(grouped_tabs.start()),
static_cast<int>(grouped_tabs.end()));
}
} else if (GetTabGroupForTab(index - 1) == GetTabGroupForTab(index)) {
group = GetTabGroupForTab(index);
}
// Pinned tabs cannot be added to a group.
if (add_types & ADD_PINNED) {
group = std::nullopt;
}
} else {
group = std::nullopt;
}
// Move insertion index after the split group if it breaks contiguity.
if (std::optional<split_tabs::SplitTabId> split_id =
InsertionBreaksSplitContiguity(index);
split_id.has_value()) {
index = GetIndexRangeOfSplit(split_id.value()).GetMax();
}
if (ui::PageTransitionTypeIncludingQualifiersIs(transition,
ui::PAGE_TRANSITION_TYPED) &&
index == count()) {
// Also, any tab opened at the end of the TabStrip with a "TYPED"
// transition inherit opener as well. This covers the cases where the user
// creates a New Tab (e.g. Ctrl+T, or clicks the New Tab button), or types
// in the address bar and presses Alt+Enter. This allows for opening a new
// Tab to quickly look up something. When this Tab is closed, the old one
// is re-activated, not the next-adjacent.
inherit_opener = true;
}
tabs::TabModel* tab_ptr = tab.get();
tab->OnAddedToModel(this);
InsertTabAtImpl(index, std::move(tab),
add_types | (inherit_opener ? ADD_INHERIT_OPENER : 0), group);
// Reset the index, just in case insert ended up moving it on us.
index = GetIndexOfTab(tab_ptr);
// In the "quick look-up" case detailed above, we want to reset the opener
// relationship on any active tab change, even to another tab in the same tree
// of openers. A jump would be too confusing at that point.
if (inherit_opener && ui::PageTransitionTypeIncludingQualifiersIs(
transition, ui::PAGE_TRANSITION_TYPED)) {
tab_ptr->set_reset_opener_on_active_tab_change(true);
}
// TODO(sky): figure out why this is here and not in InsertWebContentsAt. When
// here we seem to get failures in startup perf tests.
// Ensure that the new WebContentsView begins at the same size as the
// previous WebContentsView if it existed. Otherwise, the initial WebKit
// layout will be performed based on a width of 0 pixels, causing a
// very long, narrow, inaccurate layout. Because some scripts on pages (as
// well as WebKit's anchor link location calculation) are run on the
// initial layout and not recalculated later, we need to ensure the first
// layout is performed with sane view dimensions even when we're opening a
// new background tab.
if (WebContents* old_contents = GetActiveWebContents()) {
if ((add_types & ADD_ACTIVE) == 0) {
tab_ptr->GetContents()->Resize(
gfx::Rect(old_contents->GetContainerBounds().size()));
}
}
}
void TabStripModel::CloseSelectedTabs() {
auto get_indices = base::BindRepeating(
[](const ui::ListSelectionModel& selection_model) {
const ui::ListSelectionModel::SelectedIndices& sel =
selection_model.selected_indices();
return std::vector<int>(sel.begin(), sel.end());
},
selection_model());
ExecuteCloseTabsByIndicesCommand(std::move(get_indices),
/*delete_groups=*/true);
}
void TabStripModel::SelectNextTab(TabStripUserGestureDetails detail) {
SelectRelativeTab(TabRelativeDirection::kNext, detail);
}
void TabStripModel::SelectPreviousTab(TabStripUserGestureDetails detail) {
SelectRelativeTab(TabRelativeDirection::kPrevious, detail);
}
void TabStripModel::SelectLastTab(TabStripUserGestureDetails detail) {
ActivateTabAt(count() - 1, detail);
}
void TabStripModel::MoveTabNext() {
MoveTabRelative(TabRelativeDirection::kNext);
}
void TabStripModel::MoveTabPrevious() {
MoveTabRelative(TabRelativeDirection::kPrevious);
}
split_tabs::SplitTabData* TabStripModel::GetSplitData(
split_tabs::SplitTabId split_id) const {
const tabs::SplitTabCollection* split =
contents_data_->GetSplitTabCollection(split_id);
CHECK(split);
return split->data();
}
std::set<split_tabs::SplitTabId> TabStripModel::ListSplits() const {
return contents_data_->ListSplits();
}
bool TabStripModel::ContainsSplit(split_tabs::SplitTabId split_id) const {
return contents_data_->GetSplitTabCollection(split_id);
}
bool TabStripModel::IsActiveTabSplit() const {
const tabs::TabInterface* active_tab = GetActiveTab();
return active_tab && active_tab->IsSplit();
}
std::optional<split_tabs::SplitTabId>
TabStripModel::InsertionBreaksSplitContiguity(int index) {
CHECK(index >= 0 && index <= count());
if (!ContainsIndex(index)) {
return std::nullopt;
}
tabs::TabInterface* tab = GetTabAtIndex(index);
if (tab->IsSplit() &&
contents_data_->GetSplitTabCollection(tab->GetSplit().value())
->GetIndexOfTab(tab) > 0) {
return tab->GetSplit();
}
return std::nullopt;
}
std::optional<split_tabs::SplitTabId> TabStripModel::MoveBreaksSplitContiguity(
int start_index,
int length,
int final_index) {
// The logic for finding the previous and next tabs depends on
// the relative position of the start_index and final_index as the indices of
// the previous tab and next tab get updated if start_index < final_index but
// otherwise the ordering is the same.
const int previous_tab_index =
start_index < final_index ? final_index - 1 + length : final_index - 1;
const int next_tab_index = previous_tab_index + 1;
if (!ContainsIndex(previous_tab_index) || !ContainsIndex(next_tab_index)) {
return std::nullopt;
}
std::optional<split_tabs::SplitTabId> previous_split =
GetSplitForTab(previous_tab_index);
std::optional<split_tabs::SplitTabId> next_split =
GetSplitForTab(next_tab_index);
// If both previous and next splits are nullopt this will return nullopt.
return (previous_split == next_split) ? previous_split : std::nullopt;
}
void TabStripModel::MaybeRemoveSplitsForMove(
int initial_index,
int final_index,
const std::optional<tab_groups::TabGroupId> group,
bool pin) {
tabs::TabInterface* const tab = GetTabAtIndex(initial_index);
const bool pinned_state_changed = tab->IsPinned() != pin;
const bool group_state_changed = tab->GetGroup() != group;
// This expects the tab should move in the collection hierarchy tree.
CHECK((initial_index != final_index) || pinned_state_changed ||
group_state_changed);
// If the move is within a split collection there is no need to remove any
// split.
if (tab->IsSplit() &&
tab->GetSplit() == GetTabAtIndex(final_index)->GetSplit() &&
!pinned_state_changed && !group_state_changed) {
return;
}
// Remove the split of the origin tab if it is not moving within the
// split collection.
if (tab->IsSplit()) {
RemoveSplitImpl(tab->GetSplit().value(),
SplitTabChange::SplitTabRemoveReason::kSplitTabRemoved);
}
// Maybe remove the split tab of the destination if it results in
// discontiguity.
std::optional<split_tabs::SplitTabId> destination_split =
MoveBreaksSplitContiguity(initial_index, 1, final_index);
if (destination_split.has_value()) {
RemoveSplitImpl(destination_split.value(),
SplitTabChange::SplitTabRemoveReason::kSplitTabRemoved);
}
}
void TabStripModel::UpdateSplitLayout(split_tabs::SplitTabId split_id,
split_tabs::SplitTabLayout tab_layout) {
ReentrancyCheck reentrancy_check(&reentrancy_guard_);
split_tabs::SplitTabData* split_data = GetSplitData(split_id);
if (split_data->visual_data()->split_layout() == tab_layout) {
return;
}
split_tabs::SplitTabVisualData old_visual_data =
*GetSplitData(split_id)->visual_data();
split_data->visual_data()->set_split_layout(tab_layout);
NotifySplitTabVisualsChanged(
split_id, old_visual_data, *split_data->visual_data(),
SplitTabChange::SplitVisualChangeReason::kLayoutUpdated);
}
void TabStripModel::UpdateSplitRatio(split_tabs::SplitTabId split_id,
double split_ratio) {
ReentrancyCheck reentrancy_check(&reentrancy_guard_);
split_tabs::SplitTabData* split_data = GetSplitData(split_id);
if (split_data->visual_data()->split_ratio() == split_ratio) {
return;
}
split_tabs::SplitTabVisualData old_visual_data = *split_data->visual_data();
split_data->visual_data()->set_split_ratio(split_ratio);
NotifySplitTabVisualsChanged(
split_id, old_visual_data, *split_data->visual_data(),
SplitTabChange::SplitVisualChangeReason::kRatioUpdated);
}
void TabStripModel::UpdateTabInSplit(tabs::TabInterface* split_tab,
int update_index,
SplitUpdateType update_type) {
ReentrancyCheck reentrancy_check(&reentrancy_guard_);
CHECK(base::FeatureList::IsEnabled(features::kSideBySide));
tabs::TabInterface* update_tab = GetTabAtIndex(update_index);
// Show the deletion dialog if the group is being deleted. In `kSwap` case the
// group should be retained for the `update_index` since the active tab will
// be swapped with it into the group.
if (update_type == SplitUpdateType::kReplace &&
update_tab->GetGroup().has_value() &&
group_model_->GetTabGroup(update_tab->GetGroup().value())->tab_count() ==
1) {
std::vector<tab_groups::TabGroupId> groups_to_delete = {
update_tab->GetGroup().value()};
MarkTabGroupsForClosing(groups_to_delete);
base::OnceCallback<void()> callback = base::BindOnce(
&TabStripModel::UpdateTabInSplitImpl, base::Unretained(this), split_tab,
update_index, update_type);
return delegate_->OnRemovingAllTabsFromGroups(groups_to_delete,
std::move(callback));
}
UpdateTabInSplitImpl(split_tab, update_index, update_type);
}
void TabStripModel::ReverseTabsInSplit(split_tabs::SplitTabId split_id) {
ReentrancyCheck reentrancy_check(&reentrancy_guard_);
tabs::TabInterface* first_tab = GetSplitData(split_id)->ListTabs()[0];
const int index_of_first_tab_in_split = GetIndexOfTab(first_tab);
MoveTabToIndexImpl(index_of_first_tab_in_split,
index_of_first_tab_in_split + 1, first_tab->GetGroup(),
first_tab->IsPinned(), false);
}
split_tabs::SplitTabId TabStripModel::AddToNewSplit(
std::vector<int> indices,
split_tabs::SplitTabVisualData visual_data,
split_tabs::SplitTabCreatedSource source) {
ReentrancyCheck reentrancy_check(&reentrancy_guard_);
// Ensure that there is only one index. This will be split with the active
// tab.
CHECK_EQ(indices.size(), 1u);
CHECK(std::ranges::is_sorted(indices));
CHECK(active_index() != kNoTab);
CHECK(active_index() != indices[0]);
split_tabs::RecordSplitTabCreated(source);
split_tabs::SplitTabId split_id = split_tabs::SplitTabId::GenerateNew();
// Insert the active index into the sorted `indices`.
auto position = lower_bound(indices.begin(), indices.end(), active_index());
indices.insert(position, active_index());
AddToSplitImpl(split_id, indices, active_index(), visual_data,
SplitTabChange::SplitTabAddReason::kNewSplitTabAdded);
split_tabs::LogSplitViewCreatedUKM(this, split_id);
return split_id;
}
void TabStripModel::RestoreSplit(split_tabs::SplitTabId split_id,
const std::vector<int>& indices,
split_tabs::SplitTabVisualData visual_data) {
ReentrancyCheck reentrancy_check(&reentrancy_guard_);
CHECK(std::ranges::is_sorted(indices));
CHECK_EQ(indices.size(), 2u);
CHECK(features::IsRestoringSplitViewEnabled());
// Ideally these are consecutive indices from the restore flow and the pivot
// index does not matter. However, given there are numerous steps in restore
// and split is the last step, the API should be resilient to potential
// changes.
AddToSplitImpl(split_id, indices, indices[0], visual_data,
SplitTabChange::SplitTabAddReason::kNewSplitTabAdded);
}
tab_groups::TabGroupId TabStripModel::AddToNewGroup(
const std::vector<int> indices) {
ReentrancyCheck reentrancy_check(&reentrancy_guard_);
CHECK(SupportsTabGroups());
// Ensure that the indices are nonempty, sorted, and unique.
CHECK_GT(indices.size(), 0u);
CHECK(std::ranges::is_sorted(indices));
CHECK(std::ranges::adjacent_find(indices) == indices.end());
// The odds of |new_group| colliding with an existing group are astronomically
// low. If there is a collision, a DCHECK will fail in |AddToNewGroupImpl()|,
// in which case there is probably something wrong with
// |tab_groups::TabGroupId::GenerateNew()|.
const tab_groups::TabGroupId new_group =
tab_groups::TabGroupId::GenerateNew();
AddToNewGroupImpl(indices, new_group);
// TODO(crbug.com/339858272) : Consolidate all default save logic to
// TabStripModel::AddToNewGroupImpl.
delegate_->GroupAdded(new_group);
for (TabStripModelObserver& observer : observers_) {
observer.OnTabGroupAdded(new_group);
}
return new_group;
}
void TabStripModel::AddToExistingGroup(const std::vector<int> indices,
const tab_groups::TabGroupId group,
const bool add_to_end) {
ReentrancyCheck reentrancy_check(&reentrancy_guard_);
CHECK(SupportsTabGroups());
// Ensure that the indices are sorted and unique.
DCHECK(std::ranges::is_sorted(indices));
DCHECK(std::ranges::adjacent_find(indices) == indices.end());
CHECK(ContainsIndex(*(indices.begin())));
CHECK(ContainsIndex(*(indices.rbegin())));
AddToExistingGroupImpl(indices, group, add_to_end);
}
void TabStripModel::AddToGroupForRestore(const std::vector<int>& indices,
const tab_groups::TabGroupId& group) {
ReentrancyCheck reentrancy_check(&reentrancy_guard_);
DCHECK(group_model_);
if (!group_model_) {
return;
}
const bool group_exists = group_model_->ContainsTabGroup(group);
if (group_exists) {
AddToExistingGroupImpl(indices, group);
} else {
AddToNewGroupImpl(indices, group);
}
}
void TabStripModel::RemoveFromGroup(const std::vector<int>& indices) {
ReentrancyCheck reentrancy_check(&reentrancy_guard_);
if (!group_model_) {
return;
}
std::map<tab_groups::TabGroupId, std::vector<int>> indices_per_tab_group;
for (int index : indices) {
std::optional<tab_groups::TabGroupId> old_group = GetTabGroupForTab(index);
if (old_group.has_value()) {
indices_per_tab_group[old_group.value()].push_back(index);
}
}
for (const auto& [immutable_group_id, immutable_group_indices] :
indices_per_tab_group) {
auto group_indices = immutable_group_indices;
const TabGroup* group = group_model_->GetTabGroup(immutable_group_id);
CHECK(group);
tabs::TabInterface* first_tab_in_group = group->GetFirstTab();
CHECK(first_tab_in_group);
int first_tab_index = GetIndexOfTab(first_tab_in_group);
tabs::TabInterface* last_tab_in_group = group->GetLastTab();
int last_tab_index = GetIndexOfTab(last_tab_in_group);
// TabGroupTabCollection::SeparateTabsByVisualPosition uses recursive
// indices with respect to the group. Transpose the input by subtracting the
// index of the first tab, and do the reverse on the output.
std::transform(
group_indices.begin(), group_indices.end(), group_indices.begin(),
[first_tab_index](int index) { return index - first_tab_index; });
auto [left_of_group, right_of_group] =
contents_data_->GetTabGroupCollection(immutable_group_id)
->SeparateTabsByVisualPosition(group_indices);
for (auto partition : {&left_of_group, &right_of_group}) {
std::transform(
partition->begin(), partition->end(), partition->begin(),
[first_tab_index](int index) { return index + first_tab_index; });
}
MoveTabsAndSetPropertiesImpl(left_of_group, first_tab_index, std::nullopt,
false);
MoveTabsAndSetPropertiesImpl(right_of_group, last_tab_index + 1,
std::nullopt, false);
}
}
void TabStripModel::RemoveSplit(split_tabs::SplitTabId split_id) {
ReentrancyCheck reentrancy_check(&reentrancy_guard_);
for (tabs::TabInterface* foreground_tab : GetForegroundTabs()) {
if (!foreground_tab->IsActivated()) {
static_cast<tabs::TabModel*>(foreground_tab)
->WillBecomeHidden(base::PassKey<TabStripModel>());
}
}
RemoveSplitImpl(split_id,
SplitTabChange::SplitTabRemoveReason::kSplitTabRemoved);
}
bool TabStripModel::IsReadLaterSupportedForAny(
const std::vector<int>& indices) {
if (!delegate_->SupportsReadLater()) {
return false;
}
ReadingListModel* model =
ReadingListModelFactory::GetForBrowserContext(profile_);
if (!model || !model->loaded()) {
return false;
}
for (int index : indices) {
if (model->IsUrlSupported(
chrome::GetURLToBookmark(GetWebContentsAt(index)))) {
return true;
}
}
return false;
}
void TabStripModel::AddToReadLater(const std::vector<int>& indices) {
ReentrancyCheck reentrancy_check(&reentrancy_guard_);
AddToReadLaterImpl(indices);
}
void TabStripModel::OpenTabGroupEditor(const tab_groups::TabGroupId& group) {
TabGroupChange change(this, group, TabGroupChange::kEditorOpened);
for (auto& observer : observers_) {
observer.OnTabGroupChanged(change);
}
}
void TabStripModel::ChangeTabGroupVisuals(
const tab_groups::TabGroupId& group_id,
tab_groups::TabGroupVisualData visual_data,
bool is_customized) {
TabGroup* tab_group = group_model_->GetTabGroup(group_id);
// Move current visuals to old_visuals before updating
tab_groups::TabGroupVisualData old_visuals = *tab_group->visual_data();
TabGroupChange::VisualsChange visuals;
visuals.old_visuals = &old_visuals;
visuals.new_visuals = &visual_data;
tab_group->SetVisualData(visual_data, is_customized);
NotifyTabGroupVisualsChanged(group_id, visuals);
}
void TabStripModel::NotifyTabGroupVisualsChanged(
const tab_groups::TabGroupId& group_id,
TabGroupChange::VisualsChange visuals) {
// Notify the controller of the visual change
TabGroupChange change(this, group_id, visuals);
for (auto& observer : observers_) {
observer.OnTabGroupChanged(change);
}
}
void TabStripModel::NotifyTabGroupMoved(const tab_groups::TabGroupId& group) {
TabGroupChange change(this, group, TabGroupChange::kMoved);
for (auto& observer : observers_) {
observer.OnTabGroupChanged(change);
}
}
void TabStripModel::NotifyTabGroupCreated(const tab_groups::TabGroupId& group) {
TabGroupChange change(
this, group,
TabGroupChange::CreateChange(
TabGroupChange::TabGroupCreationReason::kNewGroupCreated, nullptr));
for (auto& observer : observers_) {
observer.OnTabGroupChanged(change);
}
}
void TabStripModel::NotifyTabGroupClosed(const tab_groups::TabGroupId& group) {
TabGroupChange change(
this, group,
TabGroupChange::CloseChange(
TabGroupChange::TabGroupClosureReason::kGroupClosed, nullptr));
for (auto& observer : observers_) {
observer.OnTabGroupChanged(change);
}
}
void TabStripModel::NotifyTabGroupDetached(
tabs::TabGroupTabCollection* group_collection,
std::map<split_tabs::SplitTabId,
std::vector<std::pair<tabs::TabInterface*, int>>>
splits_in_group) {
TabGroupChange change(
this, group_collection->GetTabGroupId(),
TabGroupChange::CloseChange(
TabGroupChange::TabGroupClosureReason::kDetachedToAnotherTabstrip,
group_collection));
for (auto& observer : observers_) {
observer.OnTabGroupChanged(change);
}
group_model_->RemoveTabGroup(group_collection->GetTabGroupId(),
base::PassKey<TabStripModel>());
for (auto const& [split_id, tabs_with_indices] : splits_in_group) {
NotifySplitTabRemoved(
split_id, tabs_with_indices,
SplitTabChange::SplitTabRemoveReason::kDetachedToAnotherTabstrip);
}
}
void TabStripModel::NotifyTabGroupAttached(
tabs::TabGroupTabCollection* group_collection) {
TabGroupChange change(
this, group_collection->GetTabGroupId(),
TabGroupChange::CreateChange(
TabGroupChange::TabGroupCreationReason::kInsertedFromAnotherTabstrip,
group_collection));
for (auto& observer : observers_) {
observer.OnTabGroupChanged(change);
}
std::set<split_tabs::SplitTabId> splits_in_group;
for (tabs::TabInterface* tab : *group_collection) {
if (tab->IsSplit()) {
splits_in_group.insert(tab->GetSplit().value());
}
}
for (const split_tabs::SplitTabId& split_id : splits_in_group) {
NotifySplitTabCreated(
split_id, GetTabsAndIndicesInSplit(split_id),
SplitTabChange::SplitTabAddReason::kInsertedFromAnotherTabstrip,
*GetSplitData(split_id)->visual_data());
}
}
void TabStripModel::NotifySplitTabCreated(
split_tabs::SplitTabId split_id,
const std::vector<std::pair<tabs::TabInterface*, int>>& tabs_with_indices,
SplitTabChange::SplitTabAddReason reason,
const split_tabs::SplitTabVisualData& visual_data) {
SplitTabChange change(
this, split_id,
SplitTabChange::AddedChange(tabs_with_indices, reason, visual_data));
for (auto& observer : observers_) {
observer.OnSplitTabChanged(change);
}
}
void TabStripModel::NotifySplitTabVisualsChanged(
split_tabs::SplitTabId split_id,
const split_tabs::SplitTabVisualData& old_visual_data,
const split_tabs::SplitTabVisualData& new_visual_data,
const SplitTabChange::SplitVisualChangeReason reason) {
SplitTabChange change(
this, split_id,
SplitTabChange::VisualsChange(old_visual_data, new_visual_data, reason));
for (auto& observer : observers_) {
observer.OnSplitTabChanged(change);
}
}
void TabStripModel::NotifySplitTabContentsUpdated(
split_tabs::SplitTabId split_id,
const std::vector<std::pair<tabs::TabInterface*, int>>& prev_tabs,
const std::vector<std::pair<tabs::TabInterface*, int>>& new_tabs) {
SplitTabChange change(this, split_id,
SplitTabChange::ContentsChange(prev_tabs, new_tabs));
for (auto& observer : observers_) {
observer.OnSplitTabChanged(change);
}
}
void TabStripModel::NotifySplitTabRemoved(
split_tabs::SplitTabId split_id,
const std::vector<std::pair<tabs::TabInterface*, int>>& tabs_with_indices,
SplitTabChange::SplitTabRemoveReason reason) {
SplitTabChange change(
this, split_id, SplitTabChange::RemovedChange(tabs_with_indices, reason));
for (auto& observer : observers_) {
observer.OnSplitTabChanged(change);
}
}
void TabStripModel::NotifySplitTabDetached(
tabs::SplitTabCollection* split_collection,
std::vector<std::pair<tabs::TabInterface*, int>> tabs_in_split,
std::optional<tab_groups::TabGroupId> previous_group_state) {
// Send possible group notification of removal of grouped tabs.
if (group_model_ && previous_group_state) {
for (auto [tab, index] : tabs_in_split) {
TabGroupStateChanged(index, tab, previous_group_state, std::nullopt);
}
}
// Send split tab notification of removal.
NotifySplitTabRemoved(
split_collection->GetSplitTabId(), tabs_in_split,
SplitTabChange::SplitTabRemoveReason::kDetachedToAnotherTabstrip);
}
void TabStripModel::NotifySplitTabAttached(
tabs::SplitTabCollection* split_collection) {
std::optional<tab_groups::TabGroupId> group_id =
split_collection->GetTabAtIndexRecursive(0)->GetGroup();
const split_tabs::SplitTabId& split_id = split_collection->GetSplitTabId();
std::vector<std::pair<tabs::TabInterface*, int>> tabs_in_split =
GetTabsAndIndicesInSplit(split_id);
if (group_model_ && group_id.has_value()) {
for (auto [tab, i] : tabs_in_split) {
TabGroupStateChanged(i, tab, std::nullopt, group_id);
}
}
// Send split attach notification
NotifySplitTabCreated(
split_id, tabs_in_split,
SplitTabChange::SplitTabAddReason::kInsertedFromAnotherTabstrip,
*GetSplitData(split_id)->visual_data());
}
int TabStripModel::GetTabCount() const {
return contents_data_->TabCountRecursive();
}
TabStripModel::TabIterator TabStripModel::begin() const {
return contents_data_->begin();
}
TabStripModel::TabIterator TabStripModel::end() const {
return contents_data_->end();
}
const tabs::TabCollection* TabStripModel::Root(
base::PassKey<tabs_api::MojoTreeBuilder> key) const {
return contents_data_.get();
}
std::optional<const tab_groups::TabGroupId> TabStripModel::FindGroupIdFor(
const tabs::TabCollection::Handle& collection_handle,
base::PassKey<tabs_api::TabStripModelAdapterImpl>) const {
return FindGroupIdFor(collection_handle);
}
tabs::TabCollectionHandle TabStripModel::GetPinnedTabsCollectionHandle(
base::PassKey<tabs_api::TabStripModelAdapterImpl>) const {
return contents_data_->pinned_collection()->GetHandle();
}
tabs::TabCollectionHandle TabStripModel::GetUnpinnedTabsCollectionHandle(
base::PassKey<tabs_api::TabStripModelAdapterImpl>) const {
return contents_data_->unpinned_collection()->GetHandle();
}
// Context menu functions.
bool TabStripModel::IsContextMenuCommandEnabled(
int context_index,
ContextMenuCommand command_id) const {
// Command must be valid.
DCHECK(command_id > CommandFirst && command_id < CommandLast);
// Context Index having an index greater than tab strip model doesnt make
// sense since this context menu must target a tab.
if (!ContainsIndex(context_index)) {
return false;
}
switch (command_id) {
case CommandNewTabToRight:
case CommandCloseTab:
return true;
case CommandReload:
return delegate_->CanReload();
case CommandCloseOtherTabs:
case CommandCloseTabsToRight: {
return !GetIndicesClosedByCommand(context_index, command_id).empty();
}
case CommandDuplicate: {
std::vector<int> indices = GetIndicesForCommand(context_index);
for (int index : indices) {
if (delegate()->CanDuplicateContentsAt(index)) {
return true;
}
}
return false;
}
case CommandToggleSiteMuted: {
std::vector<int> indices = GetIndicesForCommand(context_index);
for (int index : indices) {
if (!GetWebContentsAt(index)->GetLastCommittedURL().is_empty()) {
return true;
}
}
return false;
}
case CommandTogglePinned:
return true;
case CommandToggleGrouped:
return SupportsTabGroups();
case CommandSendTabToSelf:
return true;
case CommandAddToReadLater:
return true;
case CommandAddToNewGroup:
return SupportsTabGroups();
case CommandAddToExistingGroup:
return SupportsTabGroups();
case CommandAddToSplit:
case CommandSwapWithActiveSplit:
case CommandArrangeSplit:
return true;
case CommandRemoveFromGroup:
return SupportsTabGroups();
case CommandMoveToExistingWindow:
return true;
case CommandMoveTabsToNewWindow: {
std::vector<int> indices = GetIndicesForCommand(context_index);
const bool would_leave_strip_empty =
static_cast<int>(indices.size()) == count();
return !would_leave_strip_empty &&
delegate()->CanMoveTabsToWindow(indices);
}
case CommandOrganizeTabs:
return true;
case CommandCommerceProductSpecifications: {
auto selected_web_contents =
GetWebContentsesByIndices(GetIndicesForCommand(context_index));
return commerce::IsProductSpecsMultiSelectMenuEnabled(
profile_, GetWebContentsAt(context_index)) &&
commerce::IsWebContentsListEligibleForProductSpecs(
selected_web_contents);
}
#if BUILDFLAG(ENABLE_GLIC)
case CommandGlicShareLimit:
return false;
case CommandGlicStartShare:
return true;
case CommandGlicStopShare:
return true;
#endif
case CommandAddToNewComparisonTable:
case CommandAddToExistingComparisonTable:
return commerce::IsUrlEligibleForProductSpecs(
GetWebContentsAt(context_index)->GetLastCommittedURL());
case CommandCopyURL:
DCHECK(delegate()->IsForWebApp());
return true;
case CommandGoBack:
DCHECK(delegate()->IsForWebApp());
return delegate()->CanGoBack(GetWebContentsAt(context_index));
case CommandCloseAllTabs:
DCHECK(delegate()->IsForWebApp());
DCHECK(web_app::HasPinnedHomeTab(this));
return true;
default:
NOTREACHED();
}
}
void TabStripModel::ExecuteContextMenuCommand(int context_index,
ContextMenuCommand command_id) {
// This should have been tested by IsContextMenuCommandEnabled.
CHECK(command_id > CommandFirst && command_id < CommandLast);
// The tab strip may have been modified while the context menu was open,
// including closing the tab originally at |context_index|.
if (!ContainsIndex(context_index)) {
return;
}
switch (command_id) {
case CommandNewTabToRight: {
base::UmaHistogramCounts1000(
"Tab.ContextMenu.NewTabToRight.SelectedTabsCount",
selection_model().selected_indices().size());
base::RecordAction(UserMetricsAction("TabContextMenu_NewTab"));
UMA_HISTOGRAM_ENUMERATION("Tab.NewTab", NewTabTypes::kNewTabContextMenu,
NewTabTypes::kNewTabEnumCount);
delegate()->AddTabAt(GURL(), context_index + 1, true,
GetTabGroupForTab(context_index));
break;
}
case CommandReload: {
base::UmaHistogramCounts1000("Tab.ContextMenu.Reload.SelectedTabsCount",
selection_model().selected_indices().size());
base::RecordAction(UserMetricsAction("TabContextMenu_Reload"));
if (!delegate()->CanReload()) {
break;
}
const std::vector<int> indices = GetIndicesForCommand(context_index);
base::UmaHistogramCounts100("TabStrip.Tab.ContextMenuReloadCount",
indices.size());
for (int index : indices) {
WebContents* tab = GetWebContentsAt(index);
if (tab) {
tab->GetController().Reload(content::ReloadType::NORMAL, true);
}
}
break;
}
case CommandDuplicate: {
base::UmaHistogramCounts1000(
"Tab.ContextMenu.Duplicate.SelectedTabsCount",
selection_model().selected_indices().size());
base::RecordAction(UserMetricsAction("TabContextMenu_Duplicate"));
std::vector<int> indices = GetIndicesForCommand(context_index);
// Copy the tabs off as the indices will change as tabs are duplicated.
std::vector<tabs::TabInterface*> tabs;
for (int index : indices) {
tabs.push_back(GetTabAtIndex(index));
}
for (size_t i = 0; i < tabs.size();) {
tabs::TabInterface* tab = tabs[i];
if (tab->IsSplit()) {
split_tabs::SplitTabId split_id = tab->GetSplit().value();
delegate()->DuplicateSplit(split_id);
i += contents_data_->GetSplitTabCollection(split_id)
->TabCountRecursive();
} else {
// Need to reacquire the index of the tab as that could have changed
// since we got the tab from the index due to a previous tab being
// duplicated.
delegate()->DuplicateContentsAt(GetIndexOfTab(tab));
i++;
}
}
break;
}
case CommandCloseTab: {
base::UmaHistogramCounts1000("Tab.ContextMenu.CloseTab.SelectedTabsCount",
selection_model().selected_indices().size());
base::RecordAction(UserMetricsAction("TabContextMenu_CloseTab"));
ExecuteCloseTabsByIndicesCommand(
base::BindRepeating(&TabStripModel::GetIndicesForCommand,
base::Unretained(this), context_index),
/*delete_groups=*/true);
break;
}
case CommandCloseOtherTabs: {
base::UmaHistogramCounts1000(
"Tab.ContextMenu.CloseOtherTabs.SelectedTabsCount",
selection_model().selected_indices().size());
base::RecordAction(UserMetricsAction("TabContextMenu_CloseOtherTabs"));
ExecuteCloseTabsByIndicesCommand(
base::BindRepeating(&TabStripModel::GetIndicesClosedByCommand,
base::Unretained(this), context_index,
command_id),
/*delete_groups=*/false);
break;
}
case CommandCloseTabsToRight: {
base::UmaHistogramCounts1000(
"Tab.ContextMenu.CloseTabsToRight.SelectedTabsCount",
selection_model().selected_indices().size());
base::RecordAction(UserMetricsAction("TabContextMenu_CloseTabsToRight"));
ExecuteCloseTabsByIndicesCommand(
base::BindRepeating(&TabStripModel::GetIndicesClosedByCommand,
base::Unretained(this), context_index,
command_id),
/*delete_groups=*/false);
break;
}
case CommandSendTabToSelf: {
base::UmaHistogramCounts1000(
"Tab.ContextMenu.SendTabToSelf.SelectedTabsCount",
selection_model().selected_indices().size());
send_tab_to_self::ShowBubble(GetWebContentsAt(context_index));
break;
}
case CommandTogglePinned: {
base::UmaHistogramCounts1000(
"Tab.ContextMenu.TogglePinned.SelectedTabsCount",
selection_model().selected_indices().size());
ReentrancyCheck reentrancy_check(&reentrancy_guard_);
base::RecordAction(UserMetricsAction("TabContextMenu_TogglePinned"));
std::vector<int> indices = GetIndicesForCommand(context_index);
std::vector<tab_groups::TabGroupId> groups_to_delete =
GetGroupsDestroyedFromRemovingIndices(indices);
MarkTabGroupsForClosing(groups_to_delete);
bool pin = WillContextMenuPin(context_index);
// If there are groups that will be deleted by closing tabs from the
// context menu, confirm the group deletion first, and then perform the
// close, either through the callback provided to confirm, or directly if
// the Confirm is allowing a synchronous delete.
base::OnceCallback<void()> callback = base::BindOnce(
[](TabStripModel* model, std::vector<int> indices, bool pin_indices) {
model->SetTabsPinned(indices, pin_indices);
},
base::Unretained(this), indices, pin);
if (pin && !groups_to_delete.empty()) {
// If the delegate returns false for confirming the destroy of groups
// that means that the user needs to make a decision about the
// destruction first. prevent CloseTabs from being called.
return delegate_->OnRemovingAllTabsFromGroups(groups_to_delete,
std::move(callback));
} else {
std::move(callback).Run();
}
break;
}
case CommandToggleGrouped: {
base::UmaHistogramCounts1000(
"Tab.ContextMenu.ToggleGrouped.SelectedTabsCount",
selection_model().selected_indices().size());
if (!group_model_) {
break;
}
std::vector<int> indices = GetIndicesForCommand(context_index);
if (WillContextMenuGroup(context_index)) {
std::optional<tab_groups::TabGroupId> new_group_id =
AddToNewGroup(indices);
if (new_group_id.has_value()) {
OpenTabGroupEditor(new_group_id.value());
}
} else {
std::vector<tab_groups::TabGroupId> groups_to_delete =
GetGroupsDestroyedFromRemovingIndices(indices);
MarkTabGroupsForClosing(groups_to_delete);
base::OnceCallback<void()> callback = base::BindOnce(
&TabStripModel::RemoveFromGroup, base::Unretained(this), indices);
if (!groups_to_delete.empty()) {
delegate_->OnRemovingAllTabsFromGroups(groups_to_delete,
std::move(callback));
} else {
std::move(callback).Run();
}
}
break;
}
case CommandToggleSiteMuted: {
base::UmaHistogramCounts1000(
"Tab.ContextMenu.ToggleSiteMuted.SelectedTabsCount",
selection_model().selected_indices().size());
const bool mute = WillContextMenuMuteSites(context_index);
if (mute) {
base::RecordAction(
UserMetricsAction("SoundContentSetting.MuteBy.TabStrip"));
} else {
base::RecordAction(
UserMetricsAction("SoundContentSetting.UnmuteBy.TabStrip"));
}
SetSitesMuted(GetIndicesForCommand(context_index), mute);
break;
}
case CommandAddToReadLater: {
base::UmaHistogramCounts1000(
"Tab.ContextMenu.AddToReadLater.SelectedTabsCount",
selection_model().selected_indices().size());
base::RecordAction(
UserMetricsAction("DesktopReadingList.AddItem.FromTabContextMenu"));
AddToReadLater(GetIndicesForCommand(context_index));
break;
}
case CommandAddToNewGroup: {
base::UmaHistogramCounts1000(
"Tab.ContextMenu.AddToNewGroup.SelectedTabsCount",
selection_model().selected_indices().size());
if (!group_model_) {
break;
}
base::RecordAction(UserMetricsAction("TabContextMenu_AddToNewGroup"));
AddToNewGroupFromContextIndex(context_index);
break;
}
case CommandAddToExistingGroup: {
base::UmaHistogramCounts1000(
"Tab.ContextMenu.AddToExistingGroup.SelectedTabsCount",
selection_model().selected_indices().size());
// Do nothing. The submenu's delegate will invoke
// ExecuteAddToExistingGroupCommand with the correct group later.
break;
}
case CommandAddToSplit: {
base::UmaHistogramCounts1000(
"Tab.ContextMenu.AddToSplit.SelectedTabsCount",
selection_model().selected_indices().size());
CHECK(base::FeatureList::IsEnabled(features::kSideBySide));
std::vector<int> indices = GetIndicesForCommand(context_index);
// There are three cases for adding to a split.
// 1. Selecting an inactive tab and making it a split with the active.
// 2. Selecting active and inactive tab and creating a split
// 3. Splitting the active tab with itself.
// Remove the active tab from the indices first since splitting is done
// with the active tab. Case 3 is a special zero split case that creates a
// new split tab and is inferred by the delegate.
std::erase_if(indices, [this](int tab_index) {
return tab_index == active_index();
});
// This callback results in creating a split. It is either sent to the
// deletion dialog that owns it and is responsible for calling it or if no
// group is deleted it is simply called here.
base::OnceCallback<void()> callback = base::BindOnce(
&TabStripModelDelegate::NewSplitTab, base::Unretained(delegate_),
indices, split_tabs::SplitTabCreatedSource::kTabContextMenu);
// If we are splitting the active tab no group can be deleted.
if (!indices.empty()) {
std::vector<tab_groups::TabGroupId> groups_to_delete =
GetGroupsDestroyedFromRemovingIndices(indices);
if (!groups_to_delete.empty()) {
MarkTabGroupsForClosing(groups_to_delete);
return delegate_->OnRemovingAllTabsFromGroups(groups_to_delete,
std::move(callback));
}
}
std::move(callback).Run();
break;
}
case CommandSwapWithActiveSplit: {
base::UmaHistogramCounts1000(
"Tab.ContextMenu.SwapWithActiveSplit.SelectedTabsCount",
selection_model().selected_indices().size());
// Do nothing. The submenu's delegate will invoke the correct subcommand
// later.
break;
}
case CommandArrangeSplit: {
base::UmaHistogramCounts1000(
"Tab.ContextMenu.ArrangeSplit.SelectedTabsCount",
selection_model().selected_indices().size());
// Do nothing. The submenu's delegate will invoke the correct subcommand
// later.
break;
}
case CommandRemoveFromGroup: {
base::UmaHistogramCounts1000(
"Tab.ContextMenu.RemoveFromGroup.SelectedTabsCount",
selection_model().selected_indices().size());
if (!group_model_) {
break;
}
base::RecordAction(UserMetricsAction("TabContextMenu_RemoveFromGroup"));
std::vector<int> indices_to_remove = GetIndicesForCommand(context_index);
std::vector<tab_groups::TabGroupId> groups_to_delete =
GetGroupsDestroyedFromRemovingIndices(indices_to_remove);
MarkTabGroupsForClosing(groups_to_delete);
base::OnceCallback<void()> callback =
base::BindOnce(&TabStripModel::RemoveFromGroup,
base::Unretained(this), indices_to_remove);
if (!groups_to_delete.empty()) {
return delegate_->OnRemovingAllTabsFromGroups(groups_to_delete,
std::move(callback));
} else {
std::move(callback).Run();
}
break;
}
case CommandMoveToExistingWindow: {
base::UmaHistogramCounts1000(
"Tab.ContextMenu.MoveToExistingWindow.SelectedTabsCount",
selection_model().selected_indices().size());
// Do nothing. The submenu's delegate will invoke
// ExecuteAddToExistingWindowCommand with the correct window later.
break;
}
case CommandMoveTabsToNewWindow: {
base::UmaHistogramCounts1000(
"Tab.ContextMenu.MoveTabsToNewWindow.SelectedTabsCount",
selection_model().selected_indices().size());
base::RecordAction(
UserMetricsAction("TabContextMenu_MoveTabToNewWindow"));
std::vector<int> indices_to_move = GetIndicesForCommand(context_index);
std::vector<tab_groups::TabGroupId> groups_to_delete =
GetGroupsDestroyedFromRemovingIndices(indices_to_move);
MarkTabGroupsForClosing(groups_to_delete);
base::OnceCallback<void()> callback =
base::BindOnce(&TabStripModelDelegate::MoveTabsToNewWindow,
base::Unretained(delegate()), indices_to_move);
if (!groups_to_delete.empty()) {
return delegate_->OnRemovingAllTabsFromGroups(groups_to_delete,
std::move(callback));
} else {
std::move(callback).Run();
}
break;
}
case CommandOrganizeTabs: {
base::UmaHistogramCounts1000(
"Tab.ContextMenu.OrganizeTabs.SelectedTabsCount",
selection_model().selected_indices().size());
base::RecordAction(UserMetricsAction("TabContextMenu_OrganizeTabs"));
const Browser* const browser =
chrome::FindBrowserWithTab(GetWebContentsAt(context_index));
TabOrganizationService* const service =
TabOrganizationServiceFactory::GetForProfile(profile_);
CHECK(service);
UMA_HISTOGRAM_BOOLEAN("Tab.Organization.AllEntrypoints.Clicked", true);
UMA_HISTOGRAM_BOOLEAN("Tab.Organization.TabContextMenu.Clicked", true);
service->RestartSessionAndShowUI(
browser, TabOrganizationEntryPoint::kTabContextMenu,
GetTabAtIndex(context_index));
break;
}
case CommandCommerceProductSpecifications: {
base::UmaHistogramCounts1000(
"Tab.ContextMenu.CommerceProductSpecifications.SelectedTabsCount",
selection_model().selected_indices().size());
// ProductSpecs can only be triggered on non-incognito profiles.
DCHECK(!profile_->IsIncognitoProfile());
auto indices = GetIndicesForCommand(context_index);
auto selected_web_contents =
GetWebContentsesByIndices(GetIndicesForCommand(context_index));
auto eligible_urls =
commerce::GetListOfProductSpecsEligibleUrls(selected_web_contents);
Browser* browser =
chrome::FindBrowserWithTab(GetWebContentsAt(context_index));
chrome::OpenCommerceProductSpecificationsTab(browser, eligible_urls,
indices.back());
break;
}
#if BUILDFLAG(ENABLE_GLIC)
case CommandGlicShareLimit:
break;
case CommandGlicStopShare:
case CommandGlicStartShare: {
std::vector<int> indices = GetIndicesForCommand(context_index);
std::vector<tabs::TabHandle> tab_handles;
for (const auto& selection : indices) {
tabs::TabInterface* tab = GetTabAtIndex(selection);
if (command_id == CommandGlicStartShare &&
delegate_->IsTabGlicPinned(tab->GetHandle())) {
continue;
}
tab_handles.push_back(tab->GetHandle());
}
if (command_id == CommandGlicStartShare) {
CHECK(delegate_->GlicPinTabs(tab_handles));
if (!glic::GlicEnabling::IsMultiInstanceEnabledByFlags()) {
delegate_->OpenGlicWindowFromSharedTab();
}
} else {
CHECK(delegate_->GlicUnpinTabs(tab_handles));
}
break;
}
#endif
case CommandAddToNewComparisonTable: {
base::UmaHistogramCounts1000(
"Tab.ContextMenu.AddToNewComparisonTable.SelectedTabsCount",
selection_model().selected_indices().size());
const auto& tab_url =
GetWebContentsAt(context_index)->GetLastCommittedURL();
commerce::OpenProductSpecsTabForUrls({tab_url}, this, context_index);
break;
}
case CommandAddToExistingComparisonTable: {
base::UmaHistogramCounts1000(
"Tab.ContextMenu.AddToExistingComparisonTable.SelectedTabsCount",
selection_model().selected_indices().size());
// Handled by the existing comparison table submenu model.
break;
}
case CommandCopyURL: {
base::UmaHistogramCounts1000("Tab.ContextMenu.CopyURL.SelectedTabsCount",
selection_model().selected_indices().size());
base::RecordAction(UserMetricsAction("TabContextMenu_CopyURL"));
delegate()->CopyURL(GetWebContentsAt(context_index));
break;
}
case CommandGoBack: {
base::UmaHistogramCounts1000("Tab.ContextMenu.GoBack.SelectedTabsCount",
selection_model().selected_indices().size());
base::RecordAction(UserMetricsAction("TabContextMenu_Back"));
delegate()->GoBack(GetWebContentsAt(context_index));
break;
}
case CommandCloseAllTabs: {
base::UmaHistogramCounts1000(
"Tab.ContextMenu.CloseAllTabs.SelectedTabsCount",
selection_model().selected_indices().size());
// Closes all tabs except the pinned home tab.
base::RecordAction(UserMetricsAction("TabContextMenu_CloseAllTabs"));
base::RepeatingCallback<std::vector<int>()> get_indices =
base::BindRepeating(
[](base::RepeatingCallback<int()> count) {
std::vector<int> indices;
for (int i = count.Run() - 1; i > 0; --i) {
indices.push_back(i);
}
return indices;
},
base::BindRepeating(&TabStripModel::count,
base::Unretained(this)));
// Because no tabs will remain in the tab strip after this command ensure
// the groups are also deleted.
ExecuteCloseTabsByIndicesCommand(get_indices,
/*delete_groups=*/true);
break;
}
case CommandAddToNewGroupFromMenuItem: {
base::UmaHistogramCounts1000(
"Tab.ContextMenu.AddToNewGroupFromMenuItem.SelectedTabsCount",
selection_model().selected_indices().size());
if (!group_model_) {
break;
}
AddToNewGroupFromContextIndex(context_index);
break;
}
case CommandFirst:
case CommandAddNote:
case CommandLast:
NOTREACHED();
}
}
void TabStripModel::AddToNewGroupFromContextIndex(int context_index) {
std::vector<int> indices_to_add = GetIndicesForCommand(context_index);
CHECK(!indices_to_add.empty());
std::vector<tabs::TabInterface*> tabs_to_add;
for (const int index : indices_to_add) {
tabs_to_add.push_back(GetTabAtIndex(index));
}
std::vector<tab_groups::TabGroupId> groups_to_delete =
GetGroupsDestroyedFromRemovingIndices(indices_to_add);
MarkTabGroupsForClosing(groups_to_delete);
base::OnceCallback<void()> callback = base::BindOnce(
[](TabStripModel* model, std::vector<tabs::TabInterface*> tabs) {
std::vector<int> indices;
for (tabs::TabInterface* tab : tabs) {
const int index = model->GetIndexOfTab(tab);
if (index == kNoTab) {
continue;
}
indices.push_back(index);
}
if (indices.empty()) {
return;
}
std::optional<tab_groups::TabGroupId> new_group_id =
model->AddToNewGroup(indices);
model->OpenTabGroupEditor(new_group_id.value());
},
base::Unretained(this), tabs_to_add);
if (groups_to_delete.empty()) {
std::move(callback).Run();
} else {
delegate_->OnRemovingAllTabsFromGroups(groups_to_delete,
std::move(callback));
}
}
void TabStripModel::ExecuteAddToExistingGroupCommand(
int context_index,
const tab_groups::TabGroupId& group) {
if (!group_model_ || !group_model_->ContainsTabGroup(group)) {
return;
}
base::RecordAction(UserMetricsAction("TabContextMenu_AddToExistingGroup"));
if (!ContainsIndex(context_index)) {
return;
}
std::vector<int> indices = GetIndicesForCommand(context_index);
CHECK(!indices.empty());
std::vector<tabs::TabInterface*> tabs;
for (const int index : indices) {
tabs.push_back(GetTabAtIndex(index));
}
std::vector<tab_groups::TabGroupId> groups_to_delete =
GetGroupsDestroyedFromRemovingIndices(indices);
MarkTabGroupsForClosing(groups_to_delete);
// If there are no groups to delete OR there is only one group that was found
// to be deleted, but it is the group that is being added to then the there
// are no actual deletions occuring. Otherwise the group deletion must be
// confirmed.
base::OnceCallback<void()> callback = base::BindOnce(
[](TabStripModel* model, std::vector<tabs::TabInterface*> tabs,
const tab_groups::TabGroupId& group) {
if (!model->group_model()->ContainsTabGroup(group)) {
return;
}
std::vector<int> indices;
for (tabs::TabInterface* tab : tabs) {
const int index = model->GetIndexOfTab(tab);
if (index == kNoTab) {
continue;
}
indices.push_back(index);
}
model->AddToExistingGroup(indices, group, false);
},
base::Unretained(this), tabs, group);
if (!groups_to_delete.empty() &&
!(groups_to_delete.size() == 1 && groups_to_delete[0] == group)) {
delegate_->OnRemovingAllTabsFromGroups(groups_to_delete,
std::move(callback));
} else {
std::move(callback).Run();
}
}
void TabStripModel::ExecuteAddToExistingWindowCommand(int context_index,
int browser_index) {
base::RecordAction(UserMetricsAction("TabContextMenu_AddToExistingWindow"));
if (!ContainsIndex(context_index)) {
return;
}
delegate()->MoveToExistingWindow(GetIndicesForCommand(context_index),
browser_index);
}
std::vector<tab_groups::TabGroupId>
TabStripModel::GetGroupsDestroyedFromRemovingIndices(
const std::vector<int>& indices) const {
if (!SupportsTabGroups()) {
return std::vector<tab_groups::TabGroupId>();
}
// Collect indices of tabs in each group.
std::map<tab_groups::TabGroupId, std::vector<int>> group_indices_map;
for (const int index : indices) {
std::optional<tab_groups::TabGroupId> tab_group = GetTabGroupForTab(index);
if (!tab_group.has_value()) {
continue;
}
if (!group_indices_map.contains(tab_group.value())) {
group_indices_map.emplace(tab_group.value(), std::vector<int>{});
}
group_indices_map[tab_group.value()].emplace_back(index);
}
// collect the groups that are going to be destoyed because all tabs are
// closing.
std::vector<tab_groups::TabGroupId> groups_to_delete;
for (const auto& [group, group_indices] : group_indices_map) {
if (group_model_->GetTabGroup(group)->tab_count() ==
static_cast<int>(group_indices.size())) {
groups_to_delete.emplace_back(group);
}
}
return groups_to_delete;
}
void TabStripModel::ExecuteCloseTabsByIndices(
base::RepeatingCallback<std::vector<int>()> get_indices_to_close,
uint32_t close_types) {
ReentrancyCheck reentrancy_check(&reentrancy_guard_);
const std::vector<int> indices_to_close =
std::move(get_indices_to_close).Run();
CloseTabs(GetWebContentsesByIndices(indices_to_close), close_types);
}
void TabStripModel::MarkTabGroupsForClosing(
const std::vector<tab_groups::TabGroupId> group_ids) {
for (const tab_groups::TabGroupId& group_id : group_ids) {
TabGroup* const tab_group = group_model()->GetTabGroup(group_id);
CHECK(tab_group);
tab_group->SetGroupIsClosing(true);
}
}
void TabStripModel::ExecuteCloseTabsByIndicesCommand(
base::RepeatingCallback<std::vector<int>()> get_indices_to_close,
bool delete_groups) {
std::vector<tab_groups::TabGroupId> groups_to_delete =
GetGroupsDestroyedFromRemovingIndices(get_indices_to_close.Run());
MarkTabGroupsForClosing(groups_to_delete);
// If there are groups that will be deleted by closing tabs from the context
// menu, confirm the group deletion first, and then perform the close, either
// through the callback provided to confirm, or directly if the Confirm is
// allowing a synchronous delete. The delegate gets to decide if the
// groups will be deleted or closed based on where this is a bulk
// operation.
base::OnceCallback<void()> close_callback =
base::BindOnce(&TabStripModel::ExecuteCloseTabsByIndices,
base::Unretained(this), get_indices_to_close,
TabCloseTypes::CLOSE_CREATE_HISTORICAL_TAB |
TabCloseTypes::CLOSE_USER_GESTURE);
if (!groups_to_delete.empty()) {
// The delegate decides whether to close or delete the groups,
// potentially prompting the user to decide what action to take.
// ExecuteCloseTabs may or may not be called as a result.
delegate_->OnGroupsDestruction(groups_to_delete, std::move(close_callback),
delete_groups);
} else {
std::move(close_callback).Run();
}
}
bool TabStripModel::WillContextMenuMuteSites(int index) {
return !AreAllSitesMuted(*this, GetIndicesForCommand(index));
}
bool TabStripModel::WillContextMenuPin(int index) {
std::vector<int> indices = GetIndicesForCommand(index);
// If all tabs are pinned, then we unpin, otherwise we pin.
bool all_pinned = true;
for (size_t i = 0; i < indices.size() && all_pinned; ++i) {
all_pinned = IsTabPinned(indices[i]);
}
return !all_pinned;
}
bool TabStripModel::WillContextMenuGroup(int index) {
if (!group_model_) {
return false;
}
std::vector<int> indices = GetIndicesForCommand(index);
DCHECK(!indices.empty());
// If all tabs are in the same group, then we ungroup, otherwise we group.
std::optional<tab_groups::TabGroupId> group = GetTabGroupForTab(indices[0]);
if (!group.has_value()) {
return true;
}
for (size_t i = 1; i < indices.size(); ++i) {
if (GetTabGroupForTab(indices[i]) != group) {
return true;
}
}
return false;
}
// static
bool TabStripModel::ContextMenuCommandToBrowserCommand(int cmd_id,
int* browser_cmd) {
switch (cmd_id) {
case CommandReload:
*browser_cmd = IDC_RELOAD;
break;
case CommandDuplicate:
*browser_cmd = IDC_DUPLICATE_TAB;
break;
case CommandSendTabToSelf:
*browser_cmd = IDC_SEND_TAB_TO_SELF;
break;
case CommandCloseTab:
*browser_cmd = IDC_CLOSE_TAB;
break;
case CommandOrganizeTabs:
*browser_cmd = IDC_ORGANIZE_TABS;
break;
default:
*browser_cmd = 0;
return false;
}
return true;
}
int TabStripModel::GetIndexOfNextWebContentsOpenedBy(
const gfx::Range& block_tab_range) const {
CHECK(ContainsIndex(block_tab_range.start()));
CHECK(ContainsIndex(block_tab_range.end() - 1));
std::set<tabs::TabInterface*> block_tabs;
for (size_t i = block_tab_range.start(); i < block_tab_range.end(); i++) {
block_tabs.insert(GetTabModelAtIndex(i));
}
for (size_t i = block_tab_range.end(); i < static_cast<size_t>(count());
i++) {
if (block_tabs.find(GetTabModelAtIndex(i)->opener()) != block_tabs.end()) {
return i;
}
}
for (int i = block_tab_range.start() - 1; i >= 0; i--) {
if (block_tabs.find(GetTabModelAtIndex(i)->opener()) != block_tabs.end()) {
return i;
}
}
return kNoTab;
}
int TabStripModel::GetIndexOfNextWebContentsOpenedByOpenerOf(
const gfx::Range& block_tab_range) const {
CHECK(ContainsIndex(block_tab_range.start()));
CHECK(ContainsIndex(block_tab_range.end() - 1));
std::set<tabs::TabInterface*> block_openers;
for (size_t i = block_tab_range.start(); i < block_tab_range.end(); ++i) {
tabs::TabModel* tab = GetTabModelAtIndex(i);
if (tab->opener()) {
block_openers.insert(tab->opener());
}
}
if (block_openers.empty()) {
return kNoTab;
}
for (size_t i = block_tab_range.end(); i < static_cast<size_t>(count());
i++) {
if (block_openers.find(GetTabModelAtIndex(i)->opener()) !=
block_openers.end()) {
return i;
}
}
for (int i = block_tab_range.start() - 1; i >= 0; i--) {
if (block_openers.find(GetTabModelAtIndex(i)->opener()) !=
block_openers.end()) {
return i;
}
}
return kNoTab;
}
std::optional<int> TabStripModel::GetNextExpandedActiveTab(
const gfx::Range& block_tab_range) const {
// Check tabs from the end of the block.
for (int i = block_tab_range.end(); i < count(); ++i) {
std::optional<tab_groups::TabGroupId> current_group = GetTabGroupForTab(i);
if (!current_group.has_value() ||
(!IsGroupCollapsed(current_group.value()))) {
return i;
}
}
// Then check tabs before start_index, iterating backwards.
for (int i = block_tab_range.start() - 1; i >= 0; --i) {
std::optional<tab_groups::TabGroupId> current_group = GetTabGroupForTab(i);
if (!current_group.has_value() ||
(!IsGroupCollapsed(current_group.value()))) {
return i;
}
}
return std::nullopt;
}
std::optional<int> TabStripModel::GetNextExpandedActiveTab(
tab_groups::TabGroupId collapsing_group) const {
CHECK(group_model()->ContainsTabGroup(collapsing_group));
gfx::Range group_tab_indices =
group_model()->GetTabGroup(collapsing_group)->ListTabs();
return GetNextExpandedActiveTab(group_tab_indices);
}
void TabStripModel::ForgetAllOpeners() {
for (tabs::TabInterface* tab : *this) {
static_cast<tabs::TabModel*>(tab)->set_opener(nullptr);
}
}
void TabStripModel::ForgetOpener(WebContents* contents) {
const int index = GetIndexOfWebContents(contents);
CHECK(ContainsIndex(index));
GetTabModelAtIndex(index)->set_opener(nullptr);
}
void TabStripModel::WriteIntoTrace(perfetto::TracedValue context) const {
auto dict = std::move(context).WriteDictionary();
dict.Add("active_index", active_index());
dict.Add("tab_count", count());
}
///////////////////////////////////////////////////////////////////////////////
// TabStripModel, private:
bool TabStripModel::RunUnloadListenerBeforeClosing(
content::WebContents* contents) {
return delegate_->RunUnloadListenerBeforeClosing(contents);
}
bool TabStripModel::ShouldRunUnloadListenerBeforeClosing(
content::WebContents* contents) {
return contents->NeedToFireBeforeUnloadOrUnloadEvents() ||
delegate_->ShouldRunUnloadListenerBeforeClosing(contents);
}
int TabStripModel::ConstrainInsertionIndex(int index, bool pinned_tab) const {
return pinned_tab ? std::clamp(index, 0, IndexOfFirstNonPinnedTab())
: std::clamp(index, IndexOfFirstNonPinnedTab(), count());
}
int TabStripModel::ConstrainMoveIndex(int index, bool pinned_tab) const {
return pinned_tab
? std::clamp(index, 0, IndexOfFirstNonPinnedTab() - 1)
: std::clamp(index, IndexOfFirstNonPinnedTab(), count() - 1);
}
std::vector<int> TabStripModel::GetIndicesForCommand(int index) const {
if (!IsTabSelected(index)) {
// When the context menu is triggered on an unselected tab that is part of
// the split, return all the tabs in that split so that context menu
// actions can operate on the entire split.
std::optional<split_tabs::SplitTabId> split = GetSplitForTab(index);
if (split.has_value()) {
gfx::Range index_range = GetIndexRangeOfSplit(split.value());
std::vector<int> split_indices(index_range.length());
std::iota(split_indices.begin(), split_indices.end(),
static_cast<int>(index_range.start()));
return split_indices;
}
return {index};
}
const ui::ListSelectionModel::SelectedIndices& sel =
selection_model().selected_indices();
return std::vector<int>(sel.begin(), sel.end());
}
std::vector<int> TabStripModel::GetIndicesClosedByCommand(
int index,
ContextMenuCommand id) const {
std::vector<int> indices;
if (!ContainsIndex(index)) {
return indices;
}
DCHECK(id == CommandCloseTabsToRight || id == CommandCloseOtherTabs);
bool is_selected = IsTabSelected(index);
int last_unclosed_tab = -1;
if (id == CommandCloseTabsToRight) {
last_unclosed_tab =
is_selected ? *selection_model().selected_indices().rbegin() : index;
}
// If the tab that the context menu command is invoked on is not selected and
// also in a split, also exclude tabs from that split from being closed. We
// don't have to worry about the case when a split is selected, because all
// indices in that split are guaranteed to be part of the selection model.
tabs::TabInterface* invoked_tab = GetTabAtIndex(index);
gfx::Range indices_to_exclude =
invoked_tab->IsSplit()
? GetIndexRangeOfSplit(invoked_tab->GetSplit().value())
: gfx::Range(index, index + 1);
// NOTE: callers expect the vector to be sorted in descending order.
for (int i = count() - 1; i > last_unclosed_tab; --i) {
if (!indices_to_exclude.Contains(gfx::Range(i, i + 1)) && !IsTabPinned(i) &&
(!is_selected || !IsTabSelected(i))) {
indices.push_back(i);
}
}
return indices;
}
bool TabStripModel::IsNewTabAtEndOfTabStrip(WebContents* contents) const {
const GURL& url = contents->GetLastCommittedURL();
return url.SchemeIs(content::kChromeUIScheme) &&
url.host() == chrome::kChromeUINewTabHost &&
contents == GetTabAtIndex(count() - 1)->GetContents() &&
contents->GetController().GetEntryCount() == 1;
}
std::vector<content::WebContents*> TabStripModel::GetWebContentsesByIndices(
const std::vector<int>& indices) const {
std::vector<content::WebContents*> items;
items.reserve(indices.size());
for (int index : indices) {
items.push_back(GetTabAtIndex(index)->GetContents());
}
return items;
}
int TabStripModel::InsertTabAtImpl(
int index,
std::unique_ptr<tabs::TabModel> tab,
int add_types,
std::optional<tab_groups::TabGroupId> group) {
if (group_model_ && group.has_value()) {
CHECK(group_model_->ContainsTabGroup(group.value()));
}
delegate()->WillAddWebContents(tab->GetContents());
const bool active = (add_types & ADD_ACTIVE) != 0 || empty();
const bool pin = (add_types & ADD_PINNED) != 0;
index = ConstrainInsertionIndex(index, pin);
tabs::TabModel* const active_tab_model =
selection_model().active().has_value()
? GetTabModelAtIndex(active_index())
: nullptr;
// If there's already an active tab, and the new tab will become active, send
// a notification.
if (active_tab_model && active && !closing_all_) {
NotifyForegroundTabsWillEnterBackground();
}
// Have to get the active contents before we monkey with the contents
// otherwise we run into problems when we try to change the active contents
// since the old contents and the new contents will be the same...
CHECK_EQ(this, tab->owning_model());
if ((add_types & ADD_INHERIT_OPENER) && active_tab_model) {
if (active) {
// Forget any existing relationships, we don't want to make things too
// confusing by having multiple openers active at the same time.
ForgetAllOpeners();
}
tab->set_opener(active_tab_model);
}
// TODO(gbillock): Ask the modal dialog manager whether the WebContents should
// be blocked, or just let the modal dialog manager make the blocking call
// directly and not use this at all.
const web_modal::WebContentsModalDialogManager* manager =
web_modal::WebContentsModalDialogManager::FromWebContents(
tab->GetContents());
if (manager) {
tab->set_blocked(manager->IsDialogActive());
}
InsertTabAtIndexImpl(std::move(tab), index, group, pin, active);
return index;
}
int TabStripModel::GetIndexOfTab(const tabs::TabInterface* tab) const {
if (tab == nullptr) {
return kNoTab;
}
std::optional<size_t> index_of_tab =
contents_data_->GetIndexOfTabRecursive(tab);
return index_of_tab.value_or(kNoTab);
}
tabs::TabInterface* TabStripModel::GetTabAtIndex(int index) const {
return contents_data_->GetTabAtIndexRecursive(index);
}
tabs::TabInterface* TabStripModel::GetTabForWebContents(
const content::WebContents* contents) const {
return GetTabAtIndex(GetIndexOfWebContents(contents));
}
void TabStripModel::CloseTabs(base::span<content::WebContents* const> items,
uint32_t close_types) {
std::vector<content::WebContents*> filtered_items;
for (content::WebContents* contents : items) {
if (IsTabClosable(contents)) {
filtered_items.push_back(contents);
} else {
for (auto& observer : observers_) {
observer.TabCloseCancelled(contents);
}
}
}
if (filtered_items.empty()) {
return;
}
const bool closing_all = static_cast<int>(filtered_items.size()) == count();
base::WeakPtr<TabStripModel> ref = weak_factory_.GetWeakPtr();
if (closing_all) {
for (auto& observer : observers_) {
observer.WillCloseAllTabs(this);
}
}
DetachNotifications notifications(GetActiveTab(), selection_model());
const bool closed_all =
CloseWebContentses(filtered_items, close_types, &notifications);
// When unload handler is triggered for all items, we should wait for the
// result.
if (!notifications.detached_tab.empty()) {
SendDetachWebContentsNotifications(&notifications);
}
if (!ref) {
return;
}
if (closing_all) {
// CloseAllTabsStopped is sent with reason kCloseAllCompleted if
// closed_all; otherwise kCloseAllCanceled is sent.
for (auto& observer : observers_) {
observer.CloseAllTabsStopped(
this, closed_all ? TabStripModelObserver::kCloseAllCompleted
: TabStripModelObserver::kCloseAllCanceled);
}
}
}
bool TabStripModel::CloseWebContentses(
base::span<content::WebContents* const> items,
uint32_t close_types,
DetachNotifications* notifications) {
if (items.empty()) {
return true;
}
for (WebContents* contents : items) {
const int index = GetIndexOfWebContents(contents);
tabs::TabModel* tab_model = GetTabModelAtIndex(index);
if (index == active_index() && !closing_all_) {
tab_model->WillDeactivate(base::PassKey<TabStripModel>());
}
if (tab_model->IsVisible() && !closing_all_) {
tab_model->WillBecomeHidden(base::PassKey<TabStripModel>());
}
tab_model->WillDetach(base::PassKey<TabStripModel>(),
tabs::TabInterface::DetachReason::kDelete);
}
// We only try the fast shutdown path if the whole browser process is *not*
// shutting down. Fast shutdown during browser termination is handled in
// browser_shutdown::OnShutdownStarting.
if (!browser_shutdown::HasShutdownStarted()) {
// Construct a map of processes to the number of associated tabs that are
// closing.
base::flat_map<content::RenderProcessHost*, size_t> processes;
for (content::WebContents* contents : items) {
if (ShouldRunUnloadListenerBeforeClosing(contents)) {
continue;
}
content::RenderProcessHost* process =
contents->GetPrimaryMainFrame()->GetProcess();
++processes[process];
}
// Try to fast shutdown the tabs that can close.
for (const auto& pair : processes) {
pair.first->FastShutdownIfPossible(pair.second, false);
}
}
// We now return to our regularly scheduled shutdown procedure.
bool closed_all = true;
// The indices of WebContents prior to any modification of the internal state.
std::vector<int> original_indices;
original_indices.resize(items.size());
for (size_t i = 0; i < items.size(); ++i) {
original_indices[i] = GetIndexOfWebContents(items[i]);
}
std::vector<std::unique_ptr<DetachedTab>> detached_tab;
for (size_t i = 0; i < items.size(); ++i) {
WebContents* closing_contents = items[i];
// The index into contents_data_.
int current_index = GetIndexOfWebContents(closing_contents);
CHECK_NE(current_index, kNoTab);
// Update the explicitly closed state. If the unload handlers cancel the
// close the state is reset in Browser. We don't update the explicitly
// closed state if already marked as explicitly closed as unload handlers
// call back to this if the close is allowed.
if (!closing_contents->GetClosedByUserGesture()) {
closing_contents->SetClosedByUserGesture(
close_types & TabCloseTypes::CLOSE_USER_GESTURE);
}
if (RunUnloadListenerBeforeClosing(closing_contents)) {
closed_all = false;
continue;
}
bool create_historical_tab =
close_types & TabCloseTypes::CLOSE_CREATE_HISTORICAL_TAB;
auto dt =
DetachTabImpl(original_indices[i], current_index, create_historical_tab,
TabStripModelChange::RemoveReason::kDeleted,
tabs::TabInterface::DetachReason::kDelete);
detached_tab.push_back(std::move(dt));
}
for (auto& dt : detached_tab) {
notifications->detached_tab.push_back(std::move(dt));
}
return closed_all;
}
TabStripSelectionChange TabStripModel::SetSelection(
ui::ListSelectionModel new_model,
TabStripModelObserver::ChangeReason reason,
bool triggered_by_other_operation) {
TabStripSelectionChange selection;
selection.old_model = selection_model();
selection.old_tab = GetActiveTab();
selection.old_contents = GetActiveWebContents();
selection.new_model = new_model;
selection.reason = reason;
if (selection_model().active().has_value() &&
new_model.active().has_value() &&
selection_model().active().value() != new_model.active().value()) {
if (GetActiveTab()->IsSplit() &&
GetActiveTab()->GetSplit() ==
GetSplitForTab(new_model.active().value())) {
// When switching between two tabs in a split, neither enters the
// background but one becomes deactivated.
static_cast<tabs::TabModel*>(GetActiveTab())
->WillDeactivate(base::PassKey<TabStripModel>());
} else {
NotifyForegroundTabsWillEnterBackground();
}
}
// This is done after notifying TabDeactivated() because caller can assume
// that TabStripModel::active_index() would return the index for
// |selection.old_contents|.
selection_model_ = std::make_unique<ui::ListSelectionModel>(new_model);
selection.new_tab = GetActiveTab();
selection.new_contents = GetActiveWebContents();
if (!triggered_by_other_operation &&
(selection.active_tab_changed() || selection.selection_changed())) {
if (selection.active_tab_changed()) {
// Start measuring the tab switch compositing time. This must be the first
// thing in this block so that the start time is saved before any changes
// that might affect compositing.
if (selection.new_contents) {
// Don't record the time if the old and new tabs are in the same split.
CHECK(selection.new_tab);
const auto new_split_id = selection.new_tab->GetSplit();
const auto old_split_id =
selection.old_tab ? selection.old_tab->GetSplit() : std::nullopt;
if (!new_split_id || !old_split_id || new_split_id != old_split_id) {
selection.new_contents->SetTabSwitchStartTime(
base::TimeTicks::Now(),
resource_coordinator::ResourceCoordinatorTabHelper::IsLoaded(
selection.new_contents));
}
}
if (base::FeatureList::IsEnabled(media::kEnableTabMuting)) {
// Show the in-product help dialog pointing users to the tab mute button
// if the user backgrounds an audible tab.
if (selection.old_contents &&
selection.old_contents->IsCurrentlyAudible()) {
if (auto* const user_ed =
BrowserUserEducationInterface::MaybeGetForWebContentsInTab(
selection.old_contents)) {
user_ed->MaybeShowFeaturePromo(
feature_engagement::kIPHTabAudioMutingFeature);
}
}
}
}
ValidateTabStripModel();
TabStripModelChange change;
OnChange(change, selection);
}
return selection;
}
void TabStripModel::SelectRelativeTab(TabRelativeDirection direction,
TabStripUserGestureDetails detail) {
// This may happen during automated testing or if a user somehow buffers
// many key accelerators.
if (empty()) {
return;
}
const int start_index = active_index();
std::optional<tab_groups::TabGroupId> start_group =
GetTabGroupForTab(start_index);
// Ensure the active tab is not in a collapsed group so the while loop can
// fallback on activating the active tab.
DCHECK(!start_group.has_value() || !IsGroupCollapsed(start_group.value()));
const int delta = direction == TabRelativeDirection::kNext ? 1 : -1;
int index = (start_index + count() + delta) % count();
std::optional<tab_groups::TabGroupId> group = GetTabGroupForTab(index);
while (group.has_value() && IsGroupCollapsed(group.value())) {
index = (index + count() + delta) % count();
group = GetTabGroupForTab(index);
}
ActivateTabAt(index, detail);
}
void TabStripModel::MoveTabRelative(TabRelativeDirection direction) {
ReentrancyCheck reentrancy_check(&reentrancy_guard_);
CHECK(active_index() != kNoTab);
const size_t active_tab_index = static_cast<size_t>(active_index());
tabs::TabInterface* active_tab = GetTabAtIndex(active_tab_index);
// The range of indices being moved. This will either be the active tab, or
// all the tabs in the same split as the active tab. These are guaranteed to
// be all have the same pinned state and group membership.
// TODO: this needs to be updated for multi-selection.
const gfx::Range moving_index_range =
active_tab->IsSplit()
? GetIndexRangeOfSplit(active_tab->GetSplit().value())
: gfx::Range{active_tab_index, active_tab_index + 1};
// Calculate the target index the tabs needs to moved to. This will be the
// destination index of the current tab at moving_index_range.start().
int target_index = moving_index_range.start();
int neighbor_index = direction == TabRelativeDirection::kNext
? moving_index_range.end()
: moving_index_range.start() - 1;
if (ContainsIndex(neighbor_index) &&
IsTabPinned(moving_index_range.start()) == IsTabPinned(neighbor_index)) {
int offset = 1;
if (tabs::TabInterface* neighbor = GetTabAtIndex(neighbor_index);
neighbor->IsSplit()) {
offset = GetIndexRangeOfSplit(neighbor->GetSplit().value()).length();
}
target_index +=
(direction == TabRelativeDirection::kNext ? 1 : -1) * offset;
}
std::optional<tab_groups::TabGroupId> current_group =
GetTabGroupForTab(moving_index_range.start());
// If the target index is the same as the current index, then the tab is at a
// min/max boundary and being moved further in that direction. In that case,
// the tab could still be ungrouped to move one more slot.
std::optional<tab_groups::TabGroupId> target_group =
(target_index == static_cast<int>(moving_index_range.start()))
? std::nullopt
: GetTabGroupForTab(neighbor_index);
// If the tab is at a group boundary and the group is expanded, instead of
// actually moving the tab just change its group membership.
if (group_model_ && current_group != target_group) {
if (current_group.has_value()) {
target_index = moving_index_range.start();
target_group = std::nullopt;
} else if (target_group.has_value()) {
// If the tab is at a group boundary and the group is collapsed, treat the
// collapsed group as a tab and find the next available slot for the tab
// to move to.
const TabGroup* group = group_model_->GetTabGroup(target_group.value());
if (group->visual_data()->is_collapsed()) {
const gfx::Range tabs_in_group = group->ListTabs();
target_index = direction == TabRelativeDirection::kNext
? tabs_in_group.end() - moving_index_range.length()
: tabs_in_group.start();
target_group = std::nullopt;
} else {
target_index = moving_index_range.start();
}
}
}
MoveTabsToIndexImpl(RangeToVector(moving_index_range), target_index,
target_group);
}
std::pair<std::optional<int>, std::optional<int>>
TabStripModel::GetAdjacentTabsAfterSelectedMove(
base::PassKey<DraggingTabsSession>,
int destination_index) {
const int pinned_tab_count = IndexOfFirstNonPinnedTab();
const std::vector<int> pinned_selected_indices = GetSelectedPinnedTabs();
const std::vector<int> unpinned_selected_indices = GetSelectedUnpinnedTabs();
std::pair<std::optional<int>, std::optional<int>> adjacent_tabs(std::nullopt,
std::nullopt);
// If `unpinned_selected_indices` is empty there are no adjacent tabs.
if (unpinned_selected_indices.empty()) {
return adjacent_tabs;
}
// The index should be clamped between the first possible unpinned tab
// position and the end of the tabstrip.
const int first_unpinned_selected_dst_index = std::clamp(
destination_index + static_cast<int>(pinned_selected_indices.size()),
pinned_tab_count,
count() - static_cast<int>(unpinned_selected_indices.size()));
// Get the left adjacent if the first unpinned selected is not in the start of
// the unpinned container.
if (first_unpinned_selected_dst_index > pinned_tab_count) {
int non_selected_index = pinned_tab_count;
for (int i = pinned_tab_count; i < count(); ++i) {
if (!IsTabSelected(i)) {
if (non_selected_index == first_unpinned_selected_dst_index - 1) {
adjacent_tabs.first = i;
break;
}
++non_selected_index;
}
}
} else {
// Maybe the left adjacent is the last pinned tab.
const int is_last_pinned_tab_selected =
!pinned_selected_indices.empty() &&
(destination_index + static_cast<int>(pinned_selected_indices.size()) -
1 >=
pinned_tab_count - 1);
for (int i = pinned_tab_count - 1; i >= 0; i--) {
if (IsTabSelected(i) == is_last_pinned_tab_selected) {
adjacent_tabs.first = i;
break;
}
}
}
const int last_unpinned_selected_dst_index =
first_unpinned_selected_dst_index + unpinned_selected_indices.size() - 1;
// Get the right adjacent if the last unpinned selected is not in the end of
// the tabstrip.
if (last_unpinned_selected_dst_index < count() - 1) {
int non_selected_index = count() - 1;
for (int i = count() - 1; i >= pinned_tab_count; i--) {
if (!IsTabSelected(i)) {
if (non_selected_index == last_unpinned_selected_dst_index + 1) {
adjacent_tabs.second = i;
break;
}
--non_selected_index;
}
}
}
return adjacent_tabs;
}
std::vector<int> TabStripModel::GetSelectedPinnedTabs() {
const int pinned_tab_count = IndexOfFirstNonPinnedTab();
const ui::ListSelectionModel::SelectedIndices& selected_indices =
selection_model().selected_indices();
std::vector<int> indices;
for (int selected_index : selected_indices) {
if (selected_index < pinned_tab_count) {
indices.push_back(selected_index);
} else {
// Since selected_indices are sorted, no more pinned tabs will be found
break;
}
}
return indices;
}
std::vector<int> TabStripModel::GetSelectedUnpinnedTabs() {
const int pinned_tab_count = IndexOfFirstNonPinnedTab();
const ui::ListSelectionModel::SelectedIndices& selected_indices =
selection_model().selected_indices();
std::vector<int> indices;
for (int selected_index : base::Reversed(selected_indices)) {
if (selected_index >= pinned_tab_count) {
// Insert to the start so it is in ascending order.
indices.insert(indices.begin(), selected_index);
} else {
// Since selected_indices are sorted, no more unpinned tabs will be found
break;
}
}
return indices;
}
split_tabs::SplitTabId TabStripModel::AddToSplitImpl(
split_tabs::SplitTabId split_id,
const std::vector<int>& indices,
int pivot_index,
split_tabs::SplitTabVisualData visual_data,
SplitTabChange::SplitTabAddReason reason) {
std::vector<tabs::TabInterface*> tabs = {};
for (int i : indices) {
tabs::TabInterface* tab = GetTabAtIndex(i);
CHECK(!tab->IsSplit());
tabs.push_back(tab);
}
// Add the tabs to a split with the active index.
MoveTabsAndSetPropertiesImpl(indices, pivot_index,
GetTabGroupForTab(pivot_index),
IsTabPinned(pivot_index));
contents_data_->CreateSplit(split_id, tabs, visual_data);
std::vector<std::pair<tabs::TabInterface*, int>> tabs_with_indices;
for (tabs::TabInterface* tab : tabs) {
tabs_with_indices.emplace_back(tab, GetIndexOfTab(tab));
}
bool add_to_selection = std::any_of(
contents_data_->GetSplitTabCollection(split_id)->begin(),
contents_data_->GetSplitTabCollection(split_id)->end(),
[this](tabs::TabInterface* tab) {
return IsTabSelected(GetIndexOfTab(tab)) || tab->IsActivated();
});
const ui::ListSelectionModel old_selection_model = selection_model();
if (add_to_selection) {
for (auto split_tab : tabs_with_indices) {
selection_model_->AddIndexToSelection(split_tab.second);
}
}
ValidateTabStripModel();
if (old_selection_model != selection_model()) {
TabStripSelectionChange selection(GetActiveTab(), old_selection_model);
selection.new_model = selection_model();
TabStripModelChange change;
OnChange(change, selection);
}
NotifySplitTabCreated(split_id, tabs_with_indices, reason, visual_data);
return split_id;
}
void TabStripModel::RemoveSplitImpl(
split_tabs::SplitTabId split_id,
SplitTabChange::SplitTabRemoveReason reason) {
std::vector<std::pair<tabs::TabInterface*, int>> tabs_with_indices =
GetTabsAndIndicesInSplit(split_id);
contents_data_->Unsplit(split_id);
const ui::ListSelectionModel old_selection_model = selection_model();
for (const auto& [_, i] : tabs_with_indices) {
if (selection_model().IsSelected(i) && i != active_index()) {
selection_model_->RemoveIndexFromSelection(i);
}
}
ValidateTabStripModel();
// If there was an update to the selection model, notify observers.
if (old_selection_model != selection_model()) {
TabStripSelectionChange selection(GetActiveTab(), old_selection_model);
selection.new_model = selection_model();
TabStripModelChange change;
OnChange(change, selection);
}
NotifySplitTabRemoved(split_id, tabs_with_indices, reason);
}
void TabStripModel::UpdateTabInSplitImpl(tabs::TabInterface* split_tab,
int update_index,
SplitUpdateType update_type) {
CHECK(split_tab->IsSplit());
const split_tabs::SplitTabId split_id = split_tab->GetSplit().value();
std::vector<tabs::TabInterface*> tabs_to_split =
GetSplitData(split_id)->ListTabs();
split_tabs::SplitTabVisualData split_visual_data =
*GetSplitData(split_id)->visual_data();
const bool initial_split_active = split_tab->IsActivated();
// Require that one of the tabs in the split must be active.
CHECK(std::any_of(tabs_to_split.begin(), tabs_to_split.end(),
[](tabs::TabInterface* t) { return t->IsActivated(); }));
// Remove `split_tab` from `tabs_to_split` as it will be replaced or swapped
// out of the split and remove the active tab.
std::erase_if(tabs_to_split, [split_tab](tabs::TabInterface* tab) {
return tab == split_tab || tab->IsActivated();
});
// If the initial split isn't active, add the tab at `update_index` since it
// will be added to the split.
if (!initial_split_active) {
tabs_to_split.push_back(GetTabAtIndex(update_index));
}
// This operation is a bulk operation and is done in multiple steps.
// 1. Unsplit the collection so we can perform close and move to correct
// index.
// 2. Move the tab to replace to the correct index and make it active.
// 3. Close the previous active tab (if we are replacing the tab).
// 4. Re-split the other tabs that were a part of the split collection with
// the new active tab (the initial tab at `update_index`)
RemoveSplitImpl(split_id,
SplitTabChange::SplitTabRemoveReason::kSplitTabUpdated);
if (update_type == SplitUpdateType::kReplace) {
const int split_index = GetIndexOfTab(split_tab);
MoveTabToIndexImpl(update_index, split_index, split_tab->GetGroup(),
split_tab->IsPinned(), initial_split_active);
CloseWebContentsAt(GetIndexOfTab(split_tab),
TabCloseTypes::CLOSE_USER_GESTURE);
} else {
tabs::TabInterface* update_tab = GetTabAtIndex(update_index);
std::optional<tab_groups::TabGroupId> initial_split_group =
split_tab->GetGroup();
const bool initial_split_pinned = split_tab->IsPinned();
const int split_index = GetIndexOfTab(split_tab);
// The `split_tab` will be replaced in the split so notify observers that it
// will be moving to the background.
if (split_tab->IsActivated()) {
static_cast<tabs::TabModel*>(split_tab)->WillDeactivate(
base::PassKey<TabStripModel>());
}
static_cast<tabs::TabModel*>(split_tab)->WillBecomeHidden(
base::PassKey<TabStripModel>());
// Move the split index first so the group is not possibly destroyed at the
// update index. This can happen when the update index is the only member of
// a group. Note that the split tab cannot be the only member of a group
// since it is a split tab.
//
// Adjust the `final_index` location by shifting it one towards the
// `update_index`. When `split_index` and `update_index` are adjacent, the
// second `MoveTabToIndexImpl` can be a no-op if the tab at `update_index`
// isn't grouped or pinned. This results in the active state not being
// updated properly. Instead make the first move be to the same location by
// shifting the indices so the tab at `update_index` can pick up any
// pin/group state changes so the second move is guaranteed to apply the
// selection update.
MoveTabToIndexImpl(
split_index,
update_index > split_index ? update_index - 1 : update_index + 1,
update_tab->GetGroup(), update_tab->IsPinned(), false);
MoveTabToIndexImpl(GetIndexOfTab(update_tab), split_index,
initial_split_group, initial_split_pinned,
initial_split_active);
}
std::vector<int> split_indices;
for (tabs::TabInterface* tab : tabs_to_split) {
split_indices.emplace_back(GetIndexOfTab(tab));
}
// Insert the active index into the sorted `indices`.
auto position =
lower_bound(split_indices.begin(), split_indices.end(), active_index());
split_indices.insert(position, active_index());
AddToSplitImpl(split_id, split_indices, active_index(), split_visual_data,
SplitTabChange::SplitTabAddReason::kSplitTabUpdated);
split_tabs::LogSplitViewUpdatedUKM(this, split_id);
}
void TabStripModel::AddToNewGroupImpl(
const std::vector<int>& indices,
const tab_groups::TabGroupId& new_group,
std::optional<tab_groups::TabGroupVisualData> visual_data) {
if (!group_model_) {
return;
}
// Verify that the group id is not being used by any existing group. Group ids
// are generated randomly but a conflict should be almost impossible in
// practice.
DCHECK([&]() {
for (const tabs::TabInterface* tab : *this) {
if (tab->GetGroup() == new_group) {
return false;
}
}
return true;
}());
TabGroupDesktop::Factory factory(profile());
std::unique_ptr<tabs::TabGroupTabCollection> group_collection =
std::make_unique<tabs::TabGroupTabCollection>(
factory, new_group,
tab_groups::TabGroupVisualData(
std::u16string(),
group_model_->GetNextColor(base::PassKey<TabStripModel>())));
group_model_->AddTabGroup(group_collection->GetTabGroup(),
base::PassKey<TabStripModel>());
contents_data_->CreateTabGroup(std::move(group_collection));
// Find a destination for the first tab that's not pinned or inside another
// group. We will stack the rest of the tabs up to its right.
int destination_index = -1;
for (int i = indices[0]; i <= count(); i++) {
// Grouping at the end of the tabstrip is always valid.
if (!ContainsIndex(i)) {
destination_index = i;
break;
}
// Grouping in the middle of pinned tabs is never valid.
if (IsTabPinned(i)) {
continue;
}
// Otherwise, grouping is valid if the destination is not in the middle of a
// different group.
std::optional<tab_groups::TabGroupId> destination_group =
GetTabGroupForTab(i);
if (!destination_group.has_value() ||
destination_group != GetTabGroupForTab(indices[0])) {
destination_index = i;
break;
}
}
MoveTabsAndSetPropertiesImpl(indices, destination_index, new_group, false);
// Excluding the active tab, deselect all tabs being added to the group.
// See crbug/1301846 for more info.
const gfx::Range tab_indices =
group_model()->GetTabGroup(new_group)->ListTabs();
for (auto index = tab_indices.start(); index < tab_indices.end(); ++index) {
if (active_index() != static_cast<int>(index) && IsTabSelected(index)) {
DeselectTabAt(index);
}
}
}
void TabStripModel::AddToExistingGroupImpl(const std::vector<int>& indices,
const tab_groups::TabGroupId& group,
const bool add_to_end) {
if (!group_model_) {
return;
}
// Do nothing if the "existing" group can't be found. This would only happen
// if the existing group is closed programmatically while the user is
// interacting with the UI - e.g. if a group close operation is started by an
// extension while the user clicks "Add to existing group" in the context
// menu.
// If this happens, the browser should not crash. So here we just make it a
// no-op, since we don't want to create unintended side effects in this rare
// corner case.
if (!group_model_->ContainsTabGroup(group)) {
return;
}
const TabGroup* group_object = group_model_->GetTabGroup(group);
tabs::TabInterface* first_tab_in_group = group_object->GetFirstTab();
CHECK(first_tab_in_group);
int first_tab_index = GetIndexOfTab(first_tab_in_group);
tabs::TabInterface* last_tab_in_group = group_object->GetLastTab();
int last_tab_index = GetIndexOfTab(last_tab_in_group);
// Split |new_indices| into |tabs_left_of_group| and |tabs_right_of_group| to
// be moved to proper destination index. Directly set the group for indices
// that are inside the group.
std::vector<int> tabs_left_of_group;
std::vector<int> tabs_right_of_group;
for (int index : indices) {
if (index < first_tab_index) {
tabs_left_of_group.push_back(index);
} else if (index > last_tab_index) {
tabs_right_of_group.push_back(index);
}
}
if (add_to_end) {
std::vector<int> all_tabs = tabs_left_of_group;
all_tabs.insert(all_tabs.end(), tabs_right_of_group.begin(),
tabs_right_of_group.end());
MoveTabsAndSetPropertiesImpl(all_tabs, last_tab_index + 1, group, false);
} else {
MoveTabsAndSetPropertiesImpl(tabs_left_of_group, first_tab_index, group,
false);
MoveTabsAndSetPropertiesImpl(tabs_right_of_group, last_tab_index + 1, group,
false);
}
}
void TabStripModel::MoveTabsAndSetPropertiesImpl(
const std::vector<int>& indices,
int destination_index,
std::optional<tab_groups::TabGroupId> group,
bool pinned) {
if (!group_model_) {
return;
}
static const std::set<tabs::TabCollection::Type> retain_collection_types =
std::set<tabs::TabCollection::Type>({tabs::TabCollection::Type::SPLIT});
// TabStripCollection::MoveTabsRecursive moves tabs to the destination index
// after the tabs are removed, so adjust `destination_index` by subtracting
// the number of tabs to the left of it.
size_t num_tabs_to_left_of_destination = 0;
for (auto i : indices) {
if (i >= destination_index) {
break;
}
num_tabs_to_left_of_destination++;
}
destination_index -= num_tabs_to_left_of_destination;
MoveTabsWithNotifications(
indices, destination_index,
base::BindOnce(&tabs::TabStripCollection::MoveTabsRecursive,
base::Unretained(contents_data_.get()), indices,
destination_index, group, pinned,
retain_collection_types));
}
void TabStripModel::AddToReadLaterImpl(const std::vector<int>& indices) {
std::vector<WebContents*> web_contentses;
for (int index : indices) {
web_contentses.push_back(GetWebContentsAt(index));
}
delegate_->AddToReadLater(web_contentses);
}
void TabStripModel::InsertTabAtIndexImpl(
std::unique_ptr<tabs::TabModel> tab_model,
int index,
std::optional<tab_groups::TabGroupId> group,
bool pin,
bool active) {
tabs::TabModel* const tab_ptr = tab_model.get();
if (std::optional<split_tabs::SplitTabId> split_id =
InsertionBreaksSplitContiguity(index);
split_id.has_value()) {
RemoveSplitImpl(split_id.value(),
SplitTabChange::SplitTabRemoveReason::kSplitTabRemoved);
}
tabs::TabInterface* old_active_tab = GetActiveTab();
contents_data_->AddTabRecursive(std::move(tab_model), index, group, pin);
// Update selection model and send the notification.
selection_model_->IncrementFrom(index);
// Start computing selection change after updating the indices in
// `selection_model_`.
TabStripSelectionChange selection(old_active_tab, selection_model());
if (active) {
ui::ListSelectionModel new_model(*selection_model_.get());
SetSelectedIndex(&new_model, index);
SetSelection(std::move(new_model),
TabStripModelObserver::CHANGE_REASON_NONE,
/*triggered_by_other_operation=*/true);
}
ValidateTabStripModel();
tab_ptr->DidInsert(base::PassKey<TabStripModel>());
selection.new_model = selection_model();
selection.new_tab = GetActiveTab();
selection.new_contents = GetActiveWebContents();
TabStripModelChange::Insert insert;
insert.contents.push_back({tab_ptr, tab_ptr->GetContents(), index});
TabStripModelChange change(std::move(insert));
OnChange(change, selection);
if (group_model_ && group.has_value()) {
TabGroupStateChanged(index, tab_ptr, std::nullopt, group);
}
}
std::unique_ptr<tabs::TabModel> TabStripModel::RemoveTabFromIndexImpl(
int index,
tabs::TabInterface::DetachReason tab_detach_reason) {
tabs::TabModel* const tab = GetTabModelAtIndex(index);
const std::optional<tab_groups::TabGroupId> old_group = tab->GetGroup();
std::optional<int> next_selected_index = DetermineNewSelectedIndex(tab);
const bool removed_tab_is_split = tab->IsSplit();
if (removed_tab_is_split) {
RemoveSplitImpl(tab->GetSplit().value(),
SplitTabChange::SplitTabRemoveReason::kSplitTabRemoved);
}
if (tab_detach_reason == tabs::TabInterface::DetachReason::kDelete) {
tab->DestroyTabFeatures();
}
// Remove the tab.
std::unique_ptr<tabs::TabModel> old_data =
base::WrapUnique(static_cast<tabs::TabModel*>(
contents_data_->RemoveTabAtIndexRecursive(index).release()));
if (empty()) {
selection_model_->Clear();
} else {
int old_active = active_index();
selection_model_->DecrementFrom(index);
if (index == old_active) {
if (removed_tab_is_split) {
// If the removed tab was part of a split, we should go to the first tab
// in the split.
selection_model_->set_active(next_selected_index);
selection_model_->set_anchor(next_selected_index);
if (next_selected_index.has_value()) {
selection_model_->AddIndexToSelection(next_selected_index.value());
}
} else if (!selection_model_->empty()) {
// The active tab was removed, but there is still something selected.
// Move the active and anchor to the first selected index.
selection_model_->set_active(
*selection_model_->selected_indices().begin());
selection_model_->set_anchor(selection_model_->active());
} else {
// The active tab was removed and nothing is selected. Reset the
// selection and send out notification.
SetSelectedIndex(selection_model_.get(), next_selected_index.value());
}
}
}
ValidateTabStripModel();
if (group_model_ && old_group) {
TabGroupStateChanged(index, tab, old_group, std::nullopt);
}
return old_data;
}
void TabStripModel::MoveTabToIndexImpl(
int initial_index,
int final_index,
const std::optional<tab_groups::TabGroupId> group,
bool pin,
bool select_after_move) {
CHECK(ContainsIndex(initial_index));
CHECK_LT(initial_index, count());
CHECK_LT(final_index, count());
tabs::TabInterface* const tab = GetTabAtIndex(initial_index);
const bool initial_pinned_state = tab->IsPinned();
const std::optional<tab_groups::TabGroupId> initial_group = tab->GetGroup();
// If nothing has changed, noop.
if (initial_index == final_index && group == initial_group &&
initial_pinned_state == pin) {
return;
}
MaybeRemoveSplitsForMove(initial_index, final_index, group, pin);
// If the tab still has a split id after MaybeRemoveSplitsForMove, then it
// must be a move within a split.
bool move_within_split =
initial_index != final_index && tab->GetSplit().has_value();
std::vector<std::pair<tabs::TabInterface*, int>> initial_split_tabs;
if (move_within_split) {
initial_split_tabs = GetTabsAndIndicesInSplit(tab->GetSplit().value());
}
if (initial_index != final_index) {
FixOpeners(initial_index);
}
TabStripSelectionChange selection(GetActiveTab(), selection_model());
if (move_within_split) {
int index_of_first_tab_in_split = initial_split_tabs[0].second;
CHECK(final_index >= index_of_first_tab_in_split);
contents_data_->GetSplitTabCollection(tab->GetSplit().value())
->MoveTab(tab, final_index - index_of_first_tab_in_split);
} else {
contents_data_->MoveTabRecursive(initial_index, final_index, group, pin);
}
UpdateSelectionModelForMove(initial_index, final_index, select_after_move);
ValidateTabStripModel();
selection.new_model = selection_model();
selection.new_tab = GetActiveTab();
selection.new_contents = GetActiveWebContents();
// Send all the notifications.
if (initial_index != final_index) {
SendMoveNotificationForTab(initial_index, final_index, tab, selection);
}
if (move_within_split) {
NotifySplitTabContentsUpdated(
tab->GetSplit().value(), initial_split_tabs,
GetTabsAndIndicesInSplit(tab->GetSplit().value()));
}
if (initial_pinned_state != tab->IsPinned()) {
for (auto& observer : observers_) {
observer.TabPinnedStateChanged(this, tab->GetContents(), final_index);
}
}
if (group_model_) {
if (initial_group != tab->GetGroup()) {
TabGroupStateChanged(final_index, tab, initial_group, tab->GetGroup());
}
}
}
void TabStripModel::MoveTabsToIndexImpl(
const std::vector<int>& tab_indices,
int destination_index,
const std::optional<tab_groups::TabGroupId> group) {
if (tab_indices.empty()) {
return;
}
static const std::set<tabs::TabCollection::Type> retain_collection_types =
std::set<tabs::TabCollection::Type>(
{tabs::TabCollection::Type::SPLIT, tabs::TabCollection::Type::GROUP});
const int pinned_tab_count = IndexOfFirstNonPinnedTab();
const bool pin = IsTabPinned(tab_indices[0]);
const bool all_tabs_pinned = std::all_of(
tab_indices.begin(), tab_indices.end(),
[pinned_tab_count](int index) { return index < pinned_tab_count; });
const bool all_tabs_unpinned = std::all_of(
tab_indices.begin(), tab_indices.end(),
[pinned_tab_count](int index) { return index >= pinned_tab_count; });
CHECK(all_tabs_pinned || all_tabs_unpinned);
CHECK(std::ranges::is_sorted(tab_indices));
// Update `contents_data`.
MoveTabsWithNotifications(
tab_indices, destination_index,
base::BindOnce(&tabs::TabStripCollection::MoveTabsRecursive,
base::Unretained(contents_data_.get()), tab_indices,
destination_index, group, pin, retain_collection_types));
}
void TabStripModel::TabGroupStateChanged(
int index,
tabs::TabInterface* tab,
const std::optional<tab_groups::TabGroupId> initial_group,
const std::optional<tab_groups::TabGroupId> new_group) {
if (!group_model_) {
return;
}
if (initial_group == new_group) {
return;
}
if (initial_group.has_value()) {
TabGroup* tab_group = group_model_->GetTabGroup(initial_group.value());
tab_group->RemoveTab();
// Send the observation
for (auto& observer : observers_) {
observer.TabGroupedStateChanged(this, initial_group, std::nullopt, tab,
index);
}
// If the group model must be deleted, then do that at this point.
if (tab_group->IsEmpty()) {
NotifyTabGroupClosed(initial_group.value());
group_model_->RemoveTabGroup(initial_group.value(),
base::PassKey<TabStripModel>());
contents_data_->CloseDetachedTabGroup(initial_group.value());
}
}
if (new_group.has_value()) {
// Use IsEmpty() method as it relies on the tab_count_ maintained by the
// TabGroup object. Any method that relies on the model for this would be
// wrong since the model is already updated.
const bool is_group_empty =
group_model_->GetTabGroup(new_group.value())->IsEmpty();
// Update the group model.
AddTabToGroupModel(new_group.value());
// Send the observation
for (auto& observer : observers_) {
observer.TabGroupedStateChanged(this, std::nullopt, new_group, tab,
index);
}
// TODO(398256328): Look into replacing the empty visual change with
// providing the right initial value or migrating clients to working with
// TabGroupChange::kCreated.
if (is_group_empty) {
TabGroupChange::VisualsChange visuals;
NotifyTabGroupVisualsChanged(new_group.value(), visuals);
}
}
}
void TabStripModel::AddTabToGroupModel(const tab_groups::TabGroupId& group) {
if (!group_model_) {
return;
}
TabGroup* tab_group = group_model_->GetTabGroup(group);
if (tab_group->IsEmpty()) {
NotifyTabGroupCreated(group);
}
tab_group->AddTab();
}
void TabStripModel::ValidateTabStripModel() {
if (empty()) {
return;
}
CHECK(selection_model().active().has_value());
CHECK(ContainsIndex(selection_model().active().value()));
CHECK(GetTabAtIndex(selection_model().active().value()));
// Check if the selected tab indices are valid.
const ui::ListSelectionModel::SelectedIndices& selected_indices =
selection_model().selected_indices();
std::set<split_tabs::SplitTabId> selected_splits;
for (auto selection : selected_indices) {
// Check if the selected tab indices are valid.
const tabs::TabInterface* tab = GetTabAtIndex(selection);
CHECK(tab);
if (tab->IsSplit()) {
selected_splits.insert(tab->GetSplit().value());
}
}
// For all splits that have at least one tab selected, check that all tabs are
// selected.
for (split_tabs::SplitTabId split_id : selected_splits) {
std::vector<std::pair<tabs::TabInterface*, int>> tabs_in_split =
GetTabsAndIndicesInSplit(split_id);
CHECK(std::all_of(tabs_in_split.begin(), tabs_in_split.end(),
[&](std::pair<tabs::TabInterface*, int> tab) {
return IsTabSelected(tab.second);
}));
}
contents_data_->ValidateData();
}
void TabStripModel::SendMoveNotificationForTab(
int index,
int to_position,
tabs::TabInterface* tab,
const TabStripSelectionChange& selection_change) {
TabStripModelChange::Move move;
move.tab = tab;
move.contents = tab->GetContents();
move.from_index = index;
move.to_index = to_position;
TabStripModelChange change(move);
OnChange(change, selection_change);
}
void TabStripModel::UpdateSelectionModelForMove(int initial_index,
int final_index,
bool select_after_move) {
if (initial_index == final_index) {
return;
}
selection_model_->Move(initial_index, final_index, 1);
if (!selection_model().IsSelected(final_index) && select_after_move) {
SetSelectedIndex(selection_model_.get(), final_index);
}
}
void TabStripModel::UpdateSelectionModelForMoves(
const std::vector<int>& tab_indices,
int destination_index) {
const std::vector<std::pair<int, int>> moved_indices =
CalculateIncrementalTabMoves(tab_indices, destination_index);
for (std::pair<int, int> move : moved_indices) {
if (move.first != move.second) {
selection_model_->Move(move.first, move.second, 1);
}
}
}
void TabStripModel::SetSelectedIndex(ui::ListSelectionModel* selection,
int index) {
selection->SetSelectedIndex(index);
if (std::optional<split_tabs::SplitTabId> split_id = GetSplitForTab(index);
split_id.has_value()) {
gfx::Range index_range = GetIndexRangeOfSplit(split_id.value());
selection->AddIndexRangeToSelection(index_range.start(),
index_range.end() - 1);
}
}
std::pair<int, int> TabStripModel::GetSelectionRangeFromAnchorToIndex(
int index) {
if (!selection_model().anchor().has_value()) {
if (std::optional<split_tabs::SplitTabId> split_id = GetSplitForTab(index);
split_id.has_value()) {
gfx::Range index_range = GetIndexRangeOfSplit(split_id.value());
return std::pair(index_range.start(), index_range.end() - 1);
} else {
return std::pair(index, index);
}
}
const int anchor_index = static_cast<int>(selection_model().anchor().value());
// If the start index is part of a split, find the leftmost index in that
// split.
int start_index = std::min(index, anchor_index);
if (std::optional<split_tabs::SplitTabId> split_id =
GetSplitForTab(start_index);
split_id.has_value()) {
start_index = GetIndexRangeOfSplit(split_id.value()).GetMin();
}
// If the end index is part of a split, find the rightmost index in that
// split.
int end_index = std::max(index, anchor_index);
if (std::optional<split_tabs::SplitTabId> split_id =
GetSplitForTab(end_index);
split_id.has_value()) {
end_index = GetIndexRangeOfSplit(split_id.value()).GetMax() - 1;
}
return std::pair(start_index, end_index);
}
std::vector<std::pair<int, int>> TabStripModel::CalculateIncrementalTabMoves(
const std::vector<int>& tab_indices,
int destination_index) const {
std::vector<std::pair<int, int>> source_and_target_indices_to_move_left;
std::vector<std::pair<int, int>> source_and_target_indices_to_move_right;
// We want a sequence of moves that moves each tab directly from its
// initial index to its final index. This is possible if and only if
// every move maintains the same relative order of the moving tabs.
// We do this by splitting the tabs based on which direction they're
// moving, then moving them in the correct order within each group.
int tab_destination_index = destination_index;
for (int source_index : tab_indices) {
if (source_index < tab_destination_index) {
source_and_target_indices_to_move_right.emplace_back(
source_index, tab_destination_index++);
} else {
source_and_target_indices_to_move_left.emplace_back(
source_index, tab_destination_index++);
}
}
std::vector<std::pair<int, int>> moved_indices;
std::copy(source_and_target_indices_to_move_right.rbegin(),
source_and_target_indices_to_move_right.rend(),
std::back_inserter(moved_indices));
std::copy(source_and_target_indices_to_move_left.begin(),
source_and_target_indices_to_move_left.end(),
std::back_inserter(moved_indices));
return moved_indices;
}
std::vector<TabStripModel::MoveNotification>
TabStripModel::PrepareTabsToMoveToIndex(const std::vector<int>& tab_indices,
int destination_index) {
const std::vector<std::pair<int, int>> moved_indices =
CalculateIncrementalTabMoves(tab_indices, destination_index);
std::vector<MoveNotification> notifications;
ui::ListSelectionModel old_selection_model = selection_model();
for (std::pair<int, int> move : moved_indices) {
if (move.first != move.second) {
FixOpeners(move.first);
}
// Update the `old_selection_model`
TabStripSelectionChange selection;
if (move.first == move.second) {
selection = TabStripSelectionChange();
} else {
selection = TabStripSelectionChange(GetActiveTab(), old_selection_model);
old_selection_model.Move(move.first, move.second, 1);
selection.new_model = old_selection_model;
}
const tabs::TabInterface* const tab = GetTabAtIndex(move.first);
const MoveNotification notification = {move.first, tab->GetGroup(),
tab->IsPinned(), tab, selection};
notifications.push_back(notification);
}
return notifications;
}
void TabStripModel::SetTabsPinned(std::vector<int> indices, bool pinned) {
// `indices` are given in ascending order. If pinning, process the indices as
// is, since when moving the tab at `index` to the left, this will not change
// the tabs that are pointed to by indices larger than `index`. Similarly, if
// unpinning, process the indices in descending order.
if (!pinned) {
std::ranges::reverse(indices);
}
// When we see a tab that is part of a split, do not move it until we look
// forward and see if all the tabs in the split are in indices. If so, move
// the whole split, otherwise move the tabs individually. Splits are
// contiguous, so once we stop seeing a split, we will not see it again,
// therefore we dont have to worry about processing the same split twice.
size_t next_i;
for (size_t i = 0; i < indices.size(); i = next_i) {
next_i = i + 1;
int index = indices[i];
if (IsTabPinned(index) == pinned) {
continue;
}
tabs::TabInterface* tab = GetTabAtIndex(index);
if (tab->IsSplit()) {
tabs::SplitTabCollection* split =
contents_data_->GetSplitTabCollection(tab->GetSplit().value());
// Fast forward until we are no longer in the split.
while (next_i < indices.size() &&
next_i < i + split->TabCountRecursive() &&
GetTabAtIndex(indices[next_i])->GetSplit() == tab->GetSplit()) {
next_i++;
}
if (next_i == i + split->TabCountRecursive()) {
SetSplitPinnedImpl(split, pinned);
} else {
for (size_t j = i; j < next_i; j++) {
SetTabPinnedImpl(indices[j], pinned);
}
}
} else {
SetTabPinnedImpl(index, pinned);
}
}
}
int TabStripModel::SetTabPinnedImpl(int index, bool pinned) {
const int final_index =
pinned ? IndexOfFirstNonPinnedTab() : IndexOfFirstNonPinnedTab() - 1;
MoveTabToIndexImpl(index, final_index, std::nullopt, pinned, false);
return final_index;
}
void TabStripModel::SetSplitPinnedImpl(tabs::SplitTabCollection* split,
bool pinned) {
static const std::set<tabs::TabCollection::Type> retain_collection_types =
std::set<tabs::TabCollection::Type>({tabs::TabCollection::Type::SPLIT});
std::vector<tabs::TabInterface*> tabs = split->GetTabsRecursive();
std::vector<int> tab_indices = {};
for (size_t index = GetIndexOfTab(tabs[0]); tabs::TabInterface* _ : tabs) {
tab_indices.push_back(index++);
}
const int destination_index = pinned
? IndexOfFirstNonPinnedTab()
: IndexOfFirstNonPinnedTab() - tabs.size();
MoveTabsWithNotifications(
tab_indices, destination_index,
base::BindOnce(&tabs::TabStripCollection::MoveTabsRecursive,
base::Unretained(contents_data_.get()), tab_indices,
destination_index, std::nullopt, pinned,
retain_collection_types));
}
void TabStripModel::MoveTabsWithNotifications(
std::vector<int> tab_indices,
int destination_index,
base::OnceClosure execute_tabs_move_operation) {
const std::vector<MoveNotification> notifications =
PrepareTabsToMoveToIndex(tab_indices, destination_index);
std::move(execute_tabs_move_operation).Run();
UpdateSelectionModelForMoves(tab_indices, destination_index);
ValidateTabStripModel();
for (const auto& notification : notifications) {
const int final_index = GetIndexOfTab(notification.tab);
tabs::TabInterface* tab = GetTabAtIndex(final_index);
if (notification.initial_index != final_index) {
SendMoveNotificationForTab(notification.initial_index, final_index, tab,
notification.selection_change);
}
if (group_model_) {
if (notification.intial_group != tab->GetGroup()) {
TabGroupStateChanged(final_index, tab, notification.intial_group,
tab->GetGroup());
}
}
if (notification.initial_pinned != tab->IsPinned()) {
for (auto& observer : observers_) {
observer.TabPinnedStateChanged(this, tab->GetContents(), final_index);
}
}
}
}
// Sets the sound content setting for each site at the |indices|.
void TabStripModel::SetSitesMuted(const std::vector<int>& indices,
bool mute) const {
for (int tab_index : indices) {
content::WebContents* web_contents = GetWebContentsAt(tab_index);
GURL url = web_contents->GetLastCommittedURL();
// `GetLastCommittedURL` could return an empty URL if no navigation has
// occurred yet.
if (url.is_empty()) {
continue;
}
if (url.SchemeIs(content::kChromeUIScheme)) {
// chrome:// URLs don't have content settings but can be muted, so just
// mute the WebContents.
SetTabAudioMuted(web_contents, mute,
TabMutedReason::kContentSettingChrome, std::string());
} else {
Profile* profile =
Profile::FromBrowserContext(web_contents->GetBrowserContext());
HostContentSettingsMap* map =
HostContentSettingsMapFactory::GetForProfile(profile);
ContentSetting setting =
mute ? CONTENT_SETTING_BLOCK : CONTENT_SETTING_ALLOW;
// The goal is to only add the site URL to the exception list if
// the request behavior differs from the default value or if there is an
// existing less specific rule (i.e. wildcards) in the exception list.
if (!profile->IsIncognitoProfile()) {
// Using default setting value below clears the setting from the
// exception list for the site URL if it exists.
map->SetContentSettingDefaultScope(url, url, ContentSettingsType::SOUND,
CONTENT_SETTING_DEFAULT);
// If the current setting matches the desired setting after clearing the
// site URL from the exception list we can simply skip otherwise we
// will add the site URL to the exception list.
if (setting ==
map->GetContentSetting(url, url, ContentSettingsType::SOUND)) {
continue;
}
}
// Adds the site URL to the exception list for the setting.
map->SetContentSettingDefaultScope(url, url, ContentSettingsType::SOUND,
setting);
}
}
}
void TabStripModel::FixOpeners(int index) {
tabs::TabModel* old_tab = GetTabModelAtIndex(index);
tabs::TabInterface* new_opener = old_tab ? old_tab->opener() : nullptr;
for (tabs::TabInterface* tab : *this) {
auto* tab_model = static_cast<tabs::TabModel*>(tab);
if (tab_model->opener() != old_tab) {
continue;
}
// Ensure a tab isn't its own opener.
tab_model->set_opener(new_opener == tab_model ? nullptr : new_opener);
}
// Sanity check that none of the tabs' openers refer |old_tab| or
// themselves.
DCHECK([&]() {
return std::none_of(begin(), end(), [&](tabs::TabInterface* tab) {
tabs::TabInterface* opener = static_cast<tabs::TabModel*>(tab)->opener();
return opener == old_tab || opener == tab;
});
}());
}
std::optional<tab_groups::TabGroupId> TabStripModel::GetGroupToAssign(
int index,
int to_position) {
CHECK(ContainsIndex(index));
CHECK(ContainsIndex(to_position));
tabs::TabInterface* tab_to_move = GetTabAtIndex(index);
if (!group_model_) {
return std::nullopt;
}
std::optional<tab_groups::TabGroupId> new_left_group;
std::optional<tab_groups::TabGroupId> new_right_group;
if (to_position > index) {
new_left_group = GetTabGroupForTab(to_position);
new_right_group = GetTabGroupForTab(to_position + 1);
} else if (to_position < index) {
new_left_group = GetTabGroupForTab(to_position - 1);
new_right_group = GetTabGroupForTab(to_position);
}
if (tab_to_move->GetGroup() != new_left_group &&
tab_to_move->GetGroup() != new_right_group) {
if (new_left_group == new_right_group && new_left_group.has_value()) {
// The tab is in the middle of an existing group, so add it to that group.
return new_left_group;
} else if (tab_to_move->GetGroup().has_value() &&
group_model_->GetTabGroup(tab_to_move->GetGroup().value())
->tab_count() > 1) {
// The tab is between groups and its group is non-contiguous, so clear
// this tab's group.
return std::nullopt;
}
}
return tab_to_move->GetGroup();
}
std::optional<const tab_groups::TabGroupId> TabStripModel::FindGroupIdFor(
const tabs::TabCollection::Handle& collection_handle) const {
return contents_data_->FindGroupIdFor(collection_handle,
base::PassKey<TabStripModel>());
}
int TabStripModel::GetTabIndexAfterClosing(int index,
const gfx::Range& block_tabs) const {
CHECK(!block_tabs.Contains(gfx::Range(index)));
const int last_tab_in_block = static_cast<int>(block_tabs.end() - 1);
if (index > last_tab_in_block) {
index = index - static_cast<int>(block_tabs.length());
CHECK(index >= 0);
}
return index;
}
void TabStripModel::OnActiveTabChanged(
const TabStripSelectionChange& selection) {
if (!selection.active_tab_changed() || empty()) {
return;
}
tabs::TabInterface* const old_tab = selection.old_tab;
const tabs::TabInterface* const new_tab = selection.new_tab;
const tabs::TabInterface* old_opener = nullptr;
int reason = selection.reason;
if (new_tab->GetGroup()) {
group_model_->OnTabGroupActivated(*(new_tab->GetGroup()),
base::PassKey<TabStripModel>());
}
if (old_tab) {
const int index = GetIndexOfTab(old_tab);
if (index != TabStripModel::kNoTab) {
// When switching away from a tab, the tab preview system may want to
// capture an updated preview image. This must be done before any changes
// are made to the old contents, and while the contents are still visible.
//
// It's possible this could be done with a separate TabStripModelObserver,
// but then it would be possible for a different observer to jump in front
// and modify the WebContents, so for now, do it here.
auto* const thumbnail_helper = ThumbnailTabHelper::From(old_tab);
if (thumbnail_helper) {
thumbnail_helper->CaptureThumbnailOnTabBackgrounded();
}
old_opener = GetOpenerOfTabAt(index);
// Forget the opener relationship if it needs to be reset whenever the
// active tab changes (see comment in TabStripModel::AddWebContents, where
// the flag is set).
if (GetTabModelAtIndex(index)->reset_opener_on_active_tab_change()) {
ForgetOpener(old_tab->GetContents());
}
}
}
DCHECK(selection.new_model.active().has_value());
const tabs::TabInterface* const new_opener =
GetOpenerOfTabAt(selection.new_model.active().value());
if ((reason & TabStripModelObserver::CHANGE_REASON_USER_GESTURE) &&
new_opener != old_opener &&
((old_tab == nullptr && new_opener == nullptr) ||
new_opener != old_tab) &&
((new_tab == nullptr && old_opener == nullptr) ||
old_opener != new_tab)) {
ForgetAllOpeners();
}
}
bool TabStripModel::PolicyAllowsTabClosing(
content::WebContents* contents) const {
if (!contents) {
return true;
}
web_app::WebAppProvider* provider =
web_app::WebAppProvider::GetForWebContents(contents);
// Can be null if there is no tab helper or app id.
const webapps::AppId* app_id = web_app::WebAppTabHelper::GetAppId(contents);
if (!app_id) {
return true;
}
return !delegate()->IsForWebApp() ||
!provider->policy_manager().IsPreventCloseEnabled(*app_id);
}
int TabStripModel::DetermineInsertionIndex(ui::PageTransition transition,
bool foreground) {
int tab_count = count();
if (!tab_count) {
return 0;
}
if (ui::PageTransitionCoreTypeIs(transition, ui::PAGE_TRANSITION_LINK) &&
active_index() != -1) {
if (foreground) {
// If the page was opened in the foreground by a link click in another
// tab, insert it adjacent to the tab that opened that link.
return active_index() + 1;
}
content::WebContents* const opener = GetActiveWebContents();
// Figure out the last tab opened by the current tab.
const int index = GetIndexOfLastWebContentsOpenedBy(opener, active_index());
// If no such tab exists, simply place next to the current tab.
if (index == TabStripModel::kNoTab) {
return active_index() + 1;
}
// Normally we'd add the tab immediately after the most recent tab
// associated with `opener`. However, if there is a group discontinuity
// between the active tab and where we'd like to place the tab, we'll place
// it just before the discontinuity instead (see crbug.com/1246421).
const auto opener_group = GetTabGroupForTab(active_index());
for (int i = active_index() + 1; i <= index; ++i) {
// Insert before the first tab that differs in group.
if (GetTabGroupForTab(i) != opener_group) {
return i;
}
}
// If there is no discontinuity, add after the last tab already associated
// with the opener.
return index + 1;
}
// In other cases, such as Ctrl+T, open at the end of the strip.
return count();
}
void TabStripModel::GroupCloseStopped(const tab_groups::TabGroupId& group) {
delegate_->GroupCloseStopped(group);
gfx::Range tabs_in_group = group_model_->GetTabGroup(group)->ListTabs();
RemoveFromGroup(RangeToVector(tabs_in_group));
}
std::optional<int> TabStripModel::DetermineNewSelectedIndex(
std::variant<tabs::TabInterface*, tabs::TabCollection*> tab_or_collection)
const {
int start_index;
int block_size;
if (std::holds_alternative<tabs::TabInterface*>(tab_or_collection)) {
if (count() == 1) {
return std::nullopt;
}
tabs::TabInterface* tab = std::get<tabs::TabInterface*>(tab_or_collection);
start_index = GetIndexOfTab(tab);
block_size = 1;
} else {
tabs::TabCollection* collection =
std::get<tabs::TabCollection*>(tab_or_collection);
if (count() == static_cast<int>(collection->TabCountRecursive())) {
return std::nullopt;
}
CHECK(collection && collection->TabCountRecursive() > 0);
start_index = GetIndexOfTab(collection->GetTabAtIndexRecursive(0));
block_size = collection->TabCountRecursive();
}
gfx::Range block_tabs = gfx::Range(start_index, start_index + block_size);
// First preference is a tab the block opened.
int new_selected_index = GetIndexOfNextWebContentsOpenedBy(block_tabs);
if (new_selected_index != TabStripModel::kNoTab &&
!IsTabCollapsed(new_selected_index)) {
return GetTabIndexAfterClosing(new_selected_index, block_tabs);
}
// Second preference is a tab the block's opener opened.
new_selected_index = GetIndexOfNextWebContentsOpenedByOpenerOf(block_tabs);
if (new_selected_index != TabStripModel::kNoTab &&
!IsTabCollapsed(new_selected_index)) {
return GetTabIndexAfterClosing(new_selected_index, block_tabs);
}
// Third preference is the block's opener.
for (size_t i = block_tabs.start(); i < block_tabs.end(); ++i) {
tabs::TabInterface* opener = GetTabModelAtIndex(i)->opener();
std::optional<int> opener_index =
opener ? std::make_optional(GetIndexOfTab(opener)) : std::nullopt;
if (opener && !block_tabs.Contains(gfx::Range(opener_index.value())) &&
!IsTabCollapsed(opener_index.value())) {
return GetTabIndexAfterClosing(opener_index.value(), block_tabs);
}
}
// Fourth preference is a tab that belongs in the same parent collection as
// `tab_or_collection`.
const tabs::TabCollection* parent_collection_detached_object = nullptr;
if (std::holds_alternative<tabs::TabInterface*>(tab_or_collection)) {
tabs::TabInterface* tab = std::get<tabs::TabInterface*>(tab_or_collection);
parent_collection_detached_object = tab->GetParentCollection();
} else {
tabs::TabCollection* collection =
std::get<tabs::TabCollection*>(tab_or_collection);
parent_collection_detached_object = collection->GetParentCollection();
}
// Check if either the right of the block is present in
// `parent_collection_range` or the left of the block.
if (parent_collection_detached_object->type() ==
tabs::TabCollection::Type::GROUP ||
parent_collection_detached_object->type() ==
tabs::TabCollection::Type::SPLIT) {
const int first_tab_index = GetIndexOfTab(
parent_collection_detached_object->GetTabAtIndexRecursive(0));
const gfx::Range parent_collection_range =
gfx::Range(first_tab_index,
first_tab_index +
parent_collection_detached_object->TabCountRecursive());
if (parent_collection_range.end() != block_tabs.end()) {
return GetTabIndexAfterClosing(start_index + block_size, block_tabs);
}
if (parent_collection_range.start() != block_tabs.start()) {
return GetTabIndexAfterClosing(start_index - 1, block_tabs);
}
}
// Try to pick an uncollapsed index.
std::optional<int> next_available = GetNextExpandedActiveTab(block_tabs);
if (next_available.has_value()) {
return GetTabIndexAfterClosing(next_available.value(), block_tabs);
}
// Otherwise, prefer picking the tab after the last tab in the block.
const int first_tab_in_block = static_cast<int>(block_tabs.start());
const int last_tab_in_block = static_cast<int>(block_tabs.end() - 1);
if (last_tab_in_block >= (count() - 1)) {
return first_tab_in_block - 1;
}
return last_tab_in_block + 1 - block_tabs.length();
}
std::vector<std::pair<tabs::TabInterface*, int>>
TabStripModel::GetTabsAndIndicesInSplit(split_tabs::SplitTabId split_id) {
std::vector<std::pair<tabs::TabInterface*, int>> split_tabs_with_indices;
split_tabs::SplitTabData* split_data = GetSplitData(split_id);
std::vector<tabs::TabInterface*> split_tabs = split_data->ListTabs();
if (split_tabs.empty()) {
return split_tabs_with_indices;
}
// All the tabs in a split should be contiguous. Instead of using
// GetIndexOfTab multiple times, call it on the first tab, then increment by
// one for each subsequent tab.
for (size_t index = GetIndexOfTab(split_tabs[0]);
tabs::TabInterface* split_tab : split_tabs) {
split_tabs_with_indices.emplace_back(split_tab, index++);
}
return split_tabs_with_indices;
}
gfx::Range TabStripModel::GetIndexRangeOfSplit(
split_tabs::SplitTabId split_id) const {
split_tabs::SplitTabData* split_data = GetSplitData(split_id);
return split_data->GetIndexRange();
}
void TabStripModel::NotifyForegroundTabsWillEnterBackground() {
for (tabs::TabInterface* tab : GetForegroundTabs()) {
if (tab->IsActivated()) {
static_cast<tabs::TabModel*>(GetActiveTab())
->WillDeactivate(base::PassKey<TabStripModel>());
}
static_cast<tabs::TabModel*>(tab)->WillBecomeHidden(
base::PassKey<TabStripModel>());
}
}
TabStripModel::ScopedTabStripModalUIImpl::ScopedTabStripModalUIImpl(
TabStripModel* model)
: model_(model) {
CHECK(!model_->showing_modal_ui_);
model_->showing_modal_ui_ = true;
}
TabStripModel::ScopedTabStripModalUIImpl::~ScopedTabStripModalUIImpl() {
model_->showing_modal_ui_ = false;
}