// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "ios/chrome/browser/shared/model/web_state_list/web_state_list.h"
#import <algorithm>
#import <utility>
#import "base/auto_reset.h"
#import "base/check_op.h"
#import "base/containers/adapters.h"
#import "base/containers/contains.h"
#import "base/memory/raw_ptr.h"
#import "components/tab_groups/tab_group_id.h"
#import "ios/chrome/browser/shared/model/web_state_list/order_controller.h"
#import "ios/chrome/browser/shared/model/web_state_list/order_controller_source_from_web_state_list.h"
#import "ios/chrome/browser/shared/model/web_state_list/removing_indexes.h"
#import "ios/chrome/browser/shared/model/web_state_list/tab_group.h"
#import "ios/chrome/browser/shared/model/web_state_list/tab_group_range.h"
#import "ios/chrome/browser/shared/model/web_state_list/web_state_list_delegate.h"
#import "ios/chrome/browser/shared/model/web_state_list/web_state_list_observer.h"
#import "ios/chrome/browser/shared/model/web_state_list/web_state_opener.h"
#import "ios/web/public/web_state.h"
namespace {
// Returns whether the given flag is set in a flagset.
bool IsClosingFlagSet(int flagset, WebStateList::ClosingFlags flag) {
return (flagset & flag) == flag;
}
} // namespace
WebStateList::ScopedBatchOperation::ScopedBatchOperation(
WebStateList* web_state_list)
: web_state_list_(web_state_list) {
DCHECK(web_state_list_);
DCHECK(!web_state_list_->batch_operation_in_progress_);
web_state_list_->batch_operation_in_progress_ = true;
for (auto& observer : web_state_list_->observers_) {
observer.WillBeginBatchOperation(web_state_list_.get());
}
}
WebStateList::ScopedBatchOperation::~ScopedBatchOperation() {
if (web_state_list_) {
DCHECK(web_state_list_->batch_operation_in_progress_);
web_state_list_->batch_operation_in_progress_ = false;
for (auto& observer : web_state_list_->observers_) {
observer.BatchOperationEnded(web_state_list_.get());
}
}
}
// Used by DetachWebStateAtImpl() and DetachWebStatesAtIndicesImpl() as
// parameter. There are 3 situations of
// 1. a WebState is detached.
// 2. a WebState is detached and closed.
// 3. a WebState is detached and closed due to an user action.
// The static helper method helps construct a object that represents
// a valid state.
struct WebStateList::DetachParams {
static DetachParams Detaching();
static DetachParams Closing(bool is_user_action);
const bool is_closing;
const bool is_user_action;
};
WebStateList::DetachParams WebStateList::DetachParams::Detaching() {
return {.is_closing = false, .is_user_action = false};
}
WebStateList::DetachParams WebStateList::DetachParams::Closing(
bool is_user_action) {
return {.is_closing = true, .is_user_action = is_user_action};
}
// Wrapper around a WebState stored in a WebStateList.
class WebStateList::WebStateWrapper {
public:
explicit WebStateWrapper(std::unique_ptr<web::WebState> web_state);
WebStateWrapper(const WebStateWrapper&) = delete;
WebStateWrapper& operator=(const WebStateWrapper&) = delete;
~WebStateWrapper();
web::WebState* web_state() const { return web_state_.get(); }
// Returns ownership of the wrapped WebState.
std::unique_ptr<web::WebState> ReleaseWebState();
// Replaces the wrapped WebState and returns the old WebState after forfeiting
// ownership. The opener is cleared, but the group is kept.
std::unique_ptr<web::WebState> ReplaceWebState(
std::unique_ptr<web::WebState> web_state);
// Gets and sets information about this WebState opener. The navigation index
// is used to detect navigation changes during the same session.
WebStateOpener opener() const { return opener_; }
void SetOpener(WebStateOpener opener);
// Gets and sets whether this WebState opener must be clear when the active
// WebState changes.
bool ShouldResetOpenerOnActiveWebStateChange() const;
void SetShouldResetOpenerOnActiveWebStateChange(bool should_reset_opener);
// Gets and sets information about this WebState group.
const TabGroup* group() const { return group_; }
void SetGroup(const TabGroup* group) { group_ = group; }
private:
std::unique_ptr<web::WebState> web_state_;
WebStateOpener opener_;
raw_ptr<const TabGroup> group_ = nullptr;
bool should_reset_opener_ = false;
};
WebStateList::WebStateWrapper::WebStateWrapper(
std::unique_ptr<web::WebState> web_state)
: web_state_(std::move(web_state)), opener_(nullptr) {
DCHECK(web_state_);
}
WebStateList::WebStateWrapper::~WebStateWrapper() = default;
std::unique_ptr<web::WebState>
WebStateList::WebStateWrapper::ReleaseWebState() {
std::unique_ptr<web::WebState> web_state;
std::swap(web_state, web_state_);
opener_ = WebStateOpener();
group_ = nullptr;
return web_state;
}
std::unique_ptr<web::WebState> WebStateList::WebStateWrapper::ReplaceWebState(
std::unique_ptr<web::WebState> web_state) {
DCHECK_NE(web_state.get(), web_state_.get());
DCHECK_NE(web_state.get(), nullptr);
std::swap(web_state, web_state_);
opener_ = WebStateOpener();
return web_state;
}
void WebStateList::WebStateWrapper::SetOpener(WebStateOpener opener) {
DCHECK_NE(web_state_.get(), opener.opener);
should_reset_opener_ = false;
opener_ = opener;
}
bool WebStateList::WebStateWrapper::ShouldResetOpenerOnActiveWebStateChange()
const {
return should_reset_opener_;
}
void WebStateList::WebStateWrapper::SetShouldResetOpenerOnActiveWebStateChange(
bool should_reset_opener) {
should_reset_opener_ = should_reset_opener;
}
WebStateList::InsertionParams::InsertionParams() = default;
WebStateList::InsertionParams::InsertionParams(const InsertionParams& other) =
default;
WebStateList::InsertionParams& WebStateList::InsertionParams::operator=(
const InsertionParams& other) = default;
WebStateList::InsertionParams::InsertionParams(InsertionParams&& other) =
default;
WebStateList::InsertionParams& WebStateList::InsertionParams::operator=(
InsertionParams&& other) = default;
WebStateList::WebStateList(WebStateListDelegate* delegate)
: delegate_(delegate) {
DCHECK(delegate_);
}
WebStateList::~WebStateList() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(!locked_);
DCHECK(!batch_operation_in_progress_);
for (auto& observer : observers_) {
observer.WebStateListDestroyed(this);
}
DCHECK(!locked_);
DCHECK(!batch_operation_in_progress_);
}
base::WeakPtr<WebStateList> WebStateList::AsWeakPtr() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return weak_factory_.GetWeakPtr();
}
bool WebStateList::ContainsIndex(int index) const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return 0 <= index && index < count();
}
bool WebStateList::IsMutating() const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return locked_;
}
bool WebStateList::IsBatchInProgress() const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return batch_operation_in_progress_;
}
web::WebState* WebStateList::GetActiveWebState() const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
WebStateWrapper* wrapper = GetActiveWebStateWrapper();
return wrapper ? wrapper->web_state() : nullptr;
}
web::WebState* WebStateList::GetWebStateAt(int index) const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return GetWebStateWrapperAt(index)->web_state();
}
int WebStateList::GetIndexOfWebState(const web::WebState* web_state) const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
for (int index = 0; index < count(); ++index) {
if (web_state_wrappers_[index]->web_state() == web_state) {
return index;
}
}
return kInvalidIndex;
}
int WebStateList::GetIndexOfWebStateWithURL(const GURL& url) const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
for (int index = 0; index < count(); ++index) {
if (web_state_wrappers_[index]->web_state()->GetVisibleURL() == url) {
return index;
}
}
return kInvalidIndex;
}
int WebStateList::GetIndexOfInactiveWebStateWithURL(const GURL& url) const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
for (int index = 0; index < count(); ++index) {
if (index == active_index_) {
continue;
}
if (web_state_wrappers_[index]->web_state()->GetVisibleURL() == url) {
return index;
}
}
return kInvalidIndex;
}
WebStateOpener WebStateList::GetOpenerOfWebStateAt(int index) const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(ContainsIndex(index));
return web_state_wrappers_[index]->opener();
}
void WebStateList::SetOpenerOfWebStateAt(int index, WebStateOpener opener) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(ContainsIndex(index));
DCHECK(ContainsIndex(GetIndexOfWebState(opener.opener)));
web_state_wrappers_[index]->SetOpener(opener);
}
int WebStateList::SetWebStatePinnedAt(int index, bool pinned) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(ContainsIndex(index));
auto lock = LockForMutation();
return SetWebStatePinnedAtImpl(index, pinned);
}
bool WebStateList::IsWebStatePinnedAt(int index) const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(ContainsIndex(index));
return index < pinned_tabs_count_;
}
int WebStateList::InsertWebState(std::unique_ptr<web::WebState> web_state,
InsertionParams params) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
auto lock = LockForMutation();
return InsertWebStateImpl(std::move(web_state), params);
}
void WebStateList::MoveWebStateAt(int from_index, int to_index) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
auto lock = LockForMutation();
return MoveWebStateAtImpl(from_index, to_index);
}
std::unique_ptr<web::WebState> WebStateList::ReplaceWebStateAt(
int index,
std::unique_ptr<web::WebState> web_state) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK_NE(web_state.get(), nullptr);
auto lock = LockForMutation();
return ReplaceWebStateAtImpl(index, std::move(web_state));
}
std::unique_ptr<web::WebState> WebStateList::DetachWebStateAt(int index) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
auto lock = LockForMutation();
OrderControllerSourceFromWebStateList source(*this);
OrderController order_controller(source);
const int new_active_index =
order_controller.DetermineNewActiveIndex(active_index_, {index});
return DetachWebStateAtImpl(index, new_active_index,
DetachParams::Detaching());
}
void WebStateList::CloseWebStateAt(int index, int close_flags) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
auto lock = LockForMutation();
OrderControllerSourceFromWebStateList source(*this);
OrderController order_controller(source);
const int new_active_index =
order_controller.DetermineNewActiveIndex(active_index_, {index});
const DetachParams detach_params =
DetachParams::Closing(IsClosingFlagSet(close_flags, CLOSE_USER_ACTION));
std::unique_ptr<web::WebState> detached_web_state =
DetachWebStateAtImpl(index, new_active_index, detach_params);
// Dropping detached_web_state will destroy it.
}
void WebStateList::ActivateWebStateAt(int index) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(ContainsIndex(index) || index == kInvalidIndex);
auto lock = LockForMutation();
return ActivateWebStateAtImpl(index);
}
void WebStateList::CloseWebStatesAtIndices(int close_flags,
RemovingIndexes removing_indexes) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
auto lock = LockForMutation();
const DetachParams detach_params =
DetachParams::Closing(IsClosingFlagSet(close_flags, CLOSE_USER_ACTION));
// Detach all web states in a first pass, before destroying them at once
// later. This avoids odd side effects as a result of WebStateImpl's
// destructor notifying observers, including slowness during shutdown due to
// quadratic behavior if observers iterate the WebStateList.
std::vector<std::unique_ptr<web::WebState>> detached_web_states =
DetachWebStatesAtIndicesImpl(removing_indexes, detach_params);
detached_web_states.clear();
}
const TabGroup* WebStateList::GetGroupOfWebStateAt(int index) const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(ContainsIndex(index));
const TabGroup* group = web_state_wrappers_[index]->group();
DCHECK(!group || ContainsGroup(group));
return group;
}
std::set<const TabGroup*> WebStateList::GetGroups() const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
std::set<const TabGroup*> groups;
for (const auto& group : groups_) {
groups.insert(group.get());
}
return groups;
}
const TabGroup* WebStateList::CreateGroup(
const std::set<int>& indices,
const tab_groups::TabGroupVisualData& visual_data,
tab_groups::TabGroupId tab_group_id) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
auto lock = LockForMutation();
return CreateGroupImpl(indices, visual_data, tab_group_id);
}
bool WebStateList::ContainsGroup(const TabGroup* group) const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return groups_.contains(group);
}
void WebStateList::UpdateGroupVisualData(
const TabGroup* group,
const tab_groups::TabGroupVisualData& visual_data) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
auto lock = LockForMutation();
UpdateGroupVisualDataImpl(group, visual_data);
}
void WebStateList::MoveToGroup(const std::set<int>& indices,
const TabGroup* group) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
auto lock = LockForMutation();
MoveToGroupImpl(indices, group);
}
void WebStateList::RemoveFromGroups(const std::set<int>& indices) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
auto lock = LockForMutation();
RemoveFromGroupsImpl(indices);
}
void WebStateList::MoveGroup(const TabGroup* group, int before_index) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
auto lock = LockForMutation();
MoveGroupImpl(group, before_index);
}
void WebStateList::DeleteGroup(const TabGroup* group) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
auto lock = LockForMutation();
DeleteGroupImpl(group);
}
base::AutoReset<bool> WebStateList::LockForMutation() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
CHECK(!locked_) << "WebStateList is not re-entrant; it is an error to try to "
<< "mutate it from one of the observers (even indirectly).";
return base::AutoReset<bool>(&locked_, /*locked=*/true);
}
int WebStateList::InsertWebStateImpl(std::unique_ptr<web::WebState> web_state,
InsertionParams params) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(locked_);
DCHECK(web_state);
WebStateOpener opener = params.opener;
const bool inheriting = params.inherit_opener;
const bool activating = params.activate;
int index = params.desired_index;
CHECK(!params.in_group || ContainsGroup(params.in_group));
CHECK(!params.in_group || !params.pinned);
const TabGroup* group = params.in_group;
if (inheriting) {
for (const auto& wrapper : web_state_wrappers_) {
wrapper->SetOpener(WebStateOpener());
}
opener = WebStateOpener(GetActiveWebState());
}
// If group is not set but opener has a group, inherit the group.
int opener_index = kInvalidIndex;
if (opener.opener) {
opener_index = GetIndexOfWebState(opener.opener);
CHECK_NE(opener_index, kInvalidIndex);
if (!group) {
group = web_state_wrappers_[opener_index]->group();
}
}
// Tab will be pinned if asked explicitly and no group could be deduced.
const bool pinned = params.pinned && !group;
int range_begin = 0;
int range_end = count();
if (group) {
const TabGroupRange tab_group_range = group->range();
range_begin = tab_group_range.range_begin();
range_end = tab_group_range.range_end();
} else if (pinned) {
range_end = pinned_tabs_count_;
} else {
range_begin = pinned_tabs_count_;
}
const OrderController::Range range{.begin = range_begin, .end = range_end};
const OrderControllerSourceFromWebStateList source(*this);
const OrderController order_controller(source);
if (index != WebStateList::kInvalidIndex) {
index = order_controller.DetermineInsertionIndex(
OrderController::InsertionParams::ForceIndex(index, range));
} else if (opener.opener) {
index = order_controller.DetermineInsertionIndex(
OrderController::InsertionParams::WithOpener(opener_index, range));
} else {
index = order_controller.DetermineInsertionIndex(
OrderController::InsertionParams::Automatic(range));
}
DCHECK(ContainsIndex(index) || index == count());
delegate_->WillAddWebState(web_state.get());
web::WebState* web_state_ptr = web_state.get();
web_state_wrappers_.insert(
web_state_wrappers_.begin() + index,
std::make_unique<WebStateWrapper>(std::move(web_state)));
if (pinned) {
DCHECK_LE(index, pinned_tabs_count_);
pinned_tabs_count_++;
} else {
DCHECK_GE(index, pinned_tabs_count_);
}
if (active_index_ >= index) {
++active_index_;
}
if (inheriting) {
const auto& wrapper = web_state_wrappers_[index];
wrapper->SetShouldResetOpenerOnActiveWebStateChange(true);
}
web::WebState* old_active_web_state = GetActiveWebState();
if (activating) {
SetActiveIndex(index);
}
if (opener.opener) {
SetOpenerOfWebStateAt(index, opener);
}
// Ensure group contiguity: if the WebStates before and after are part of the
// same group, honor that group instead.
const TabGroup* prev_group =
ContainsIndex(index - 1) ? GetGroupOfWebStateAt(index - 1) : nullptr;
const TabGroup* next_group =
ContainsIndex(index + 1) ? GetGroupOfWebStateAt(index + 1) : nullptr;
if (prev_group == next_group && prev_group != nullptr) {
group = prev_group;
}
// Shift groups on the right of `index` towards the right.
web_state_wrappers_[index]->SetGroup(group);
for (const auto& current_group : groups_) {
TabGroupRange& current_range = current_group->range();
if (current_group.get() == group) {
current_range.ExpandRight();
} else if (current_range.range_begin() >= index) {
current_range.MoveRight();
}
}
const WebStateListChangeInsert insert_change(web_state_ptr, index, group);
const WebStateListStatus status = {
.old_active_web_state = old_active_web_state,
.new_active_web_state = GetActiveWebState()};
for (auto& observer : observers_) {
observer.WebStateListDidChange(this, insert_change, status);
}
return index;
}
void WebStateList::MoveWebStateAtImpl(int from_index, int to_index) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(locked_);
// Moves via `MoveWebStateAt` are constrained to keep their pinned state.
const bool pinned = IsWebStatePinnedAt(from_index);
to_index = ConstrainMoveIndex(to_index, pinned);
DCHECK(ContainsIndex(from_index));
DCHECK(ContainsIndex(to_index));
// Moves via `MoveWebStateAt` should update the group to ensure contiguity.
const TabGroup* old_group = GetGroupOfWebStateAt(from_index);
const TabGroup* new_group = nullptr;
if (old_group && old_group->range().contains(to_index)) {
// The move stays within the same group.
new_group = old_group;
} else {
// If the WebStates before and after are part of the same group, add to that
// group.
const TabGroup* prev_group = nullptr;
const TabGroup* next_group = nullptr;
if (from_index < to_index) {
prev_group = GetGroupOfWebStateAt(to_index);
if (ContainsIndex(to_index + 1)) {
next_group = GetGroupOfWebStateAt(to_index + 1);
}
} else {
if (ContainsIndex(to_index - 1)) {
prev_group = GetGroupOfWebStateAt(to_index - 1);
}
next_group = GetGroupOfWebStateAt(to_index);
}
if (prev_group && prev_group == next_group) {
new_group = prev_group;
}
}
MoveWebStateWrapperAt(from_index, to_index, pinned, new_group);
}
std::unique_ptr<web::WebState> WebStateList::ReplaceWebStateAtImpl(
int index,
std::unique_ptr<web::WebState> web_state) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(locked_);
DCHECK(web_state);
DCHECK(ContainsIndex(index));
delegate_->WillAddWebState(web_state.get());
ClearOpenersReferencing(index);
web::WebState* web_state_ptr = web_state.get();
std::unique_ptr<web::WebState> replaced_web_state =
web_state_wrappers_[index]->ReplaceWebState(std::move(web_state));
if (index == active_index_) {
// The active WebState was replaced.
OnActiveWebStateChanged();
}
const WebStateListChangeReplace replace_change(replaced_web_state.get(),
web_state_ptr, index);
const WebStateListStatus status = {
.old_active_web_state = (index == active_index_)
? replaced_web_state.get()
: GetActiveWebState(),
.new_active_web_state = GetActiveWebState()};
for (auto& observer : observers_) {
observer.WebStateListDidChange(this, replace_change, status);
}
return replaced_web_state;
}
std::unique_ptr<web::WebState> WebStateList::DetachWebStateAtImpl(
int index,
int new_active_index,
DetachParams params) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(locked_);
DCHECK(ContainsIndex(index));
const bool is_active_web_state_detached = (index == active_index_);
web::WebState* web_state = web_state_wrappers_[index]->web_state();
const TabGroup* group = web_state_wrappers_[index]->group();
const WebStateListChangeDetach detach_change(
web_state, index, params.is_closing, params.is_user_action, group);
// `new_active_index` may be invalid e.g. when closing all the WebStates,
// so use `ContainsIndex(...)` to avoid crashing in `GetWebStateAt(...)`.
web::WebState* old_active_web_state = GetActiveWebState();
web::WebState* new_active_web_state = ContainsIndex(new_active_index)
? GetWebStateAt(new_active_index)
: nullptr;
const WebStateListStatus status = {
.old_active_web_state = old_active_web_state,
.new_active_web_state = new_active_web_state};
for (auto& observer : observers_) {
observer.WebStateListWillChange(this, detach_change, status);
}
ClearOpenersReferencing(index);
std::unique_ptr<web::WebState> detached_web_state =
web_state_wrappers_[index]->ReleaseWebState();
web_state_wrappers_.erase(web_state_wrappers_.begin() + index);
// Update the number of pinned tabs if necessary.
if (index < pinned_tabs_count_) {
CHECK_GT(pinned_tabs_count_, 0);
--pinned_tabs_count_;
}
// Update the span of the group containing the detached WebState and the
// starting index of all groups located after the detached WebState.
for (const auto& current_group : groups_) {
TabGroupRange& current_range = current_group->range();
if (current_group.get() == group) {
current_range.ContractRight();
} else if (current_range.range_begin() >= index) {
current_range.MoveLeft();
}
}
// Update the active index to prevent observer from seeing an invalid WebState
// as the active one but only send the WebStateActivatedAt notification after
// the WebStateListDidChange with kDetach.
active_index_ = new_active_index;
if (index < active_index_) {
CHECK_GT(active_index_, 0);
--active_index_;
}
// Check that the active element (if there is one) is valid and expected.
DCHECK(active_index_ == kInvalidIndex || ContainsIndex(active_index_));
DCHECK_EQ(GetActiveWebState(), new_active_web_state);
// Inform the delegate that the active WebState changed (it may decide to
// force its realization, ...).
if (is_active_web_state_detached) {
OnActiveWebStateChanged();
}
for (auto& observer : observers_) {
observer.WebStateListDidChange(this, detach_change, status);
}
// If the group is now empty, delete it.
DeleteGroupIfEmpty(group);
return detached_web_state;
}
std::vector<std::unique_ptr<web::WebState>>
WebStateList::DetachWebStatesAtIndicesImpl(RemovingIndexes removing_indexes,
DetachParams detach_params) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(locked_);
// Immediately determine the new active index to avoid sending multiple
// notification about changing active WebState (as they could force the
// realization of the activated WebStates).
OrderControllerSourceFromWebStateList source(*this);
OrderController order_controller(source);
int old_active_index = active_index_;
int new_active_index =
order_controller.DetermineNewActiveIndex(active_index_, removing_indexes);
// Store the detached WebStates to allow the caller to delete them after
// they have all been detached.
std::vector<std::unique_ptr<web::WebState>> detached_web_states;
RemovingIndexes::Range span = removing_indexes.span();
for (int i = 0; i < span.count; ++i) {
const int index = span.start + span.count - i - 1;
if (!removing_indexes.Contains(index)) {
continue;
}
int active_index = active_index_;
if (index == old_active_index) {
active_index = new_active_index;
} else {
if (index < new_active_index) {
CHECK_GT(new_active_index, 0);
--new_active_index;
}
}
detached_web_states.push_back(
DetachWebStateAtImpl(index, active_index, detach_params));
}
return detached_web_states;
}
void WebStateList::ActivateWebStateAtImpl(int index) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(locked_);
DCHECK(ContainsIndex(index) || index == kInvalidIndex);
// Do nothing when the target WebState is already activated.
if (active_index_ == index) {
return;
}
web::WebState* old_active_web_state = GetActiveWebState();
SetActiveIndex(index);
web::WebState* new_active_web_state = GetActiveWebState();
const TabGroup* group =
index != kInvalidIndex ? GetGroupOfWebStateAt(index) : nullptr;
const WebStateListChangeStatusOnly status_only_change(
new_active_web_state, index, /*pinned_state_changed=*/false, group,
group);
const WebStateListStatus status = {
.old_active_web_state = old_active_web_state,
.new_active_web_state = new_active_web_state};
for (auto& observer : observers_) {
observer.WebStateListDidChange(this, status_only_change, status);
}
}
int WebStateList::SetWebStatePinnedAtImpl(int index, bool pinned) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(locked_);
if (pinned == IsWebStatePinnedAt(index)) {
// The pinned state is not changed, nothing to do.
return index;
}
// When pinning, move to the end of the pinned tabs. When unpinning, move to
// the end of the WebStateList. May end up identical to `index`.
int new_index = pinned ? pinned_tabs_count_ : count() - 1;
MoveWebStateWrapperAt(index, new_index, pinned, /*new_group=*/nullptr);
return new_index;
}
const TabGroup* WebStateList::CreateGroupImpl(
const std::set<int>& indices,
const tab_groups::TabGroupVisualData& visual_data,
tab_groups::TabGroupId tab_group_id) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(locked_);
DCHECK(!indices.empty());
// Figure out the pivot index.
int pivot_index = kInvalidIndex;
const int first_index = *indices.begin();
CHECK(ContainsIndex(first_index), base::NotFatalUntil::M128);
if (IsWebStatePinnedAt(first_index)) {
// Move to the last pinned tab.
pivot_index = pinned_tabs_count_;
} else {
const TabGroup* group = GetGroupOfWebStateAt(first_index);
const TabGroup* group_before = ContainsIndex(first_index - 1)
? GetGroupOfWebStateAt(first_index - 1)
: nullptr;
if (group && group == group_before) {
pivot_index = group->range().range_end();
} else {
pivot_index = first_index;
}
}
DCHECK_NE(pivot_index, kInvalidIndex);
// Create the group.
auto group = std::make_unique<TabGroup>(tab_group_id, visual_data,
TabGroupRange(pivot_index, 0));
const TabGroup* new_group = group.get();
groups_.insert(std::move(group));
// Notify the observers of the group creation.
// The creation didn't change the active WebState.
web::WebState* const active_web_state = GetActiveWebState();
const WebStateListStatus status = {.old_active_web_state = active_web_state,
.new_active_web_state = active_web_state};
const WebStateListChangeGroupCreate group_create_change(new_group);
for (auto& observer : observers_) {
observer.WebStateListDidChange(this, group_create_change, status);
}
// Move the WebStates to the group.
MoveToGroupImpl(indices, new_group);
return new_group;
}
void WebStateList::UpdateGroupVisualDataImpl(
const TabGroup* group,
const tab_groups::TabGroupVisualData& visual_data) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(group);
DCHECK(ContainsGroup(group));
// `TabGroupVisualData` comparison does not account for `is_collapsed()` but
// this API should allow modifying `is_collapsed()` and notify observers
// accordingly.
if (group->visual_data() == visual_data &&
group->visual_data().is_collapsed() == visual_data.is_collapsed()) {
return;
}
// Update the visual data on the group. Find it in `groups_`, to get a
// non-const pointer.
const auto old_visual_data = group->visual_data();
groups_.find(group)->get()->SetVisualData(visual_data);
// Notify the observers.
// The update didn't change the active WebState.
web::WebState* const active_web_state = GetActiveWebState();
const WebStateListStatus status = {.old_active_web_state = active_web_state,
.new_active_web_state = active_web_state};
const WebStateListChangeGroupVisualDataUpdate group_visual_data_update_change(
group, old_visual_data);
for (auto& observer : observers_) {
observer.WebStateListDidChange(this, group_visual_data_update_change,
status);
}
}
void WebStateList::MoveToGroupImpl(const std::set<int>& indices,
const TabGroup* group) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(locked_);
DCHECK(group);
DCHECK(ContainsGroup(group));
DCHECK(!indices.empty());
const TabGroupRange group_range = group->range();
// Split indices between WebStates left of the group moving to their right and
// WebStates right of the group moving to their left. This is to keep indices
// valid during the moves.
std::vector<int> before_group;
std::vector<int> after_group;
for (const auto& index : indices) {
if (index < group_range.range_begin()) {
before_group.push_back(index);
} else if (index >= group_range.range_end()) {
after_group.push_back(index);
} else {
// Indices already in the group range are not updated.
}
}
// Iterate over the WebStates on the left of the group.
// Reverse `before_group` to start from the rightmost, to keep indices valid.
std::reverse(before_group.begin(), before_group.end());
int to_index = group_range.range_end() - 1;
for (int index : before_group) {
MoveWebStateWrapperAt(index, to_index, /*pinned=*/false, group);
--to_index;
}
// Iterate over the WebStates on the right of the group.
to_index = group_range.range_end();
for (int index : after_group) {
MoveWebStateWrapperAt(index, to_index, /*pinned=*/false, group);
++to_index;
}
}
void WebStateList::RemoveFromGroupsImpl(const std::set<int>& indices) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(locked_);
// Ungrouped WebStates are moved after the group. Iterate from the end to
// keep ungrouped WebStates in the order they were in the group.
for (auto it = indices.rbegin(); it != indices.rend(); ++it) {
const int index = *it;
CHECK(ContainsIndex(index), base::NotFatalUntil::M128);
const TabGroup* group = GetGroupOfWebStateAt(index);
if (group) {
const int to_index = group->range().range_end() - 1;
MoveWebStateWrapperAt(index, to_index, /*pinned=*/false,
/*new_group=*/nullptr);
}
}
}
void WebStateList::DeleteGroupImpl(const TabGroup* group) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(locked_);
DCHECK(group);
for (int index : group->range()) {
MoveWebStateWrapperAt(index, index, /*pinned=*/false,
/*new_group=*/nullptr);
}
}
void WebStateList::MoveGroupImpl(const TabGroup* group, int before_index) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(locked_);
DCHECK(group);
// Groups can't move to the pinned tabs section. Keep `count` in, to be able
// to move a group at the end.
int to_index = std::clamp(before_index, pinned_tabs_count_, count());
// Destination can't be in the middle of a group. Update `to_index` to the
// beginning of the group at the destination, if any.
const TabGroup* group_at_destination =
ContainsIndex(to_index) ? GetGroupOfWebStateAt(to_index) : nullptr;
if (group_at_destination) {
to_index = group_at_destination->range().range_begin();
}
// Compute the stride, i.e. by how much to change the start of the moving
// group's range.
const TabGroupRange prior_range = group->range();
const int from_index = prior_range.range_begin();
int stride = to_index - from_index;
if (stride > 0) {
// When moving to the right, account for the length of the moving group.
CHECK_GE(stride, prior_range.count());
stride -= prior_range.count();
}
// Early return if the group doesn't need to move.
if (stride == 0) {
return;
}
// Update the groups ranges.
const int group_count = prior_range.count();
for (auto& some_group : groups_) {
TabGroupRange& some_group_range = some_group->range();
if (some_group.get() == group) {
// Update the moved group range.
some_group_range.Move(stride);
} else {
// Slide all groups after the removed group to the left.
if (from_index < some_group_range.range_begin()) {
some_group_range.MoveLeft(group_count);
}
// Slide all groups at or after the added group to the right.
if (from_index + stride <= some_group_range.range_begin()) {
some_group_range.MoveRight(group_count);
}
}
}
// Update the active index if needed.
if (prior_range.contains(active_index_)) {
active_index_ += stride;
} else {
if (from_index < active_index_) {
active_index_ -= group_count;
}
if (from_index + stride <= active_index_) {
active_index_ += group_count;
}
}
// Move the wrappers.
if (from_index < to_index) {
std::rotate(web_state_wrappers_.begin() + from_index,
web_state_wrappers_.begin() + from_index + group_count,
web_state_wrappers_.begin() + to_index);
} else {
std::rotate(web_state_wrappers_.begin() + to_index,
web_state_wrappers_.begin() + from_index,
web_state_wrappers_.begin() + from_index + group_count);
}
// Notify the observers of the change.
// The move didn't change the active WebState.
web::WebState* const active_web_state = GetActiveWebState();
const WebStateListStatus status = {.old_active_web_state = active_web_state,
.new_active_web_state = active_web_state};
const WebStateListChangeGroupMove group_move_change(group, prior_range,
group->range());
for (auto& observer : observers_) {
observer.WebStateListDidChange(this, group_move_change, status);
}
}
void WebStateList::AddObserver(WebStateListObserver* observer) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
observers_.AddObserver(observer);
}
void WebStateList::RemoveObserver(WebStateListObserver* observer) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
observers_.RemoveObserver(observer);
}
WebStateList::ScopedBatchOperation WebStateList::StartBatchOperation() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(!batch_operation_in_progress_);
return ScopedBatchOperation(this);
}
void WebStateList::ClearOpenersReferencing(int index) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
web::WebState* old_web_state = web_state_wrappers_[index]->web_state();
for (auto& web_state_wrapper : web_state_wrappers_) {
if (web_state_wrapper->opener().opener == old_web_state) {
web_state_wrapper->SetOpener(WebStateOpener());
}
}
}
int WebStateList::ConstrainMoveIndex(int index, bool pinned) const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return pinned ? std::clamp(index, 0, pinned_tabs_count_ - 1)
: std::clamp(index, pinned_tabs_count_, count() - 1);
}
WebStateList::WebStateWrapper* WebStateList::GetActiveWebStateWrapper() const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (active_index_ != kInvalidIndex) {
return GetWebStateWrapperAt(active_index_);
}
return nullptr;
}
WebStateList::WebStateWrapper* WebStateList::GetWebStateWrapperAt(
int index) const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(ContainsIndex(index));
return web_state_wrappers_[index].get();
}
void WebStateList::MoveWebStateWrapperAt(int from_index,
int to_index,
bool pinned,
const TabGroup* new_group) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(locked_);
DCHECK(ContainsIndex(from_index));
DCHECK(ContainsIndex(to_index));
DCHECK(!(pinned && new_group));
// Prepare info about the move.
const bool index_changed = from_index != to_index;
const bool pinned_state_changed = IsWebStatePinnedAt(from_index) != pinned;
const TabGroup* old_group = GetGroupOfWebStateAt(from_index);
const bool group_changed = old_group != new_group;
// Return early if nothing has changed.
if (!index_changed && !pinned_state_changed && !group_changed) {
return;
}
// Update the pinned tabs count.
if (pinned_state_changed) {
if (pinned) {
CHECK_LT(pinned_tabs_count_, count());
pinned_tabs_count_++;
} else {
CHECK_GT(pinned_tabs_count_, 0);
pinned_tabs_count_--;
}
}
// Update the wrapper's group tag.
web_state_wrappers_[from_index]->SetGroup(new_group);
// Update the groups ranges.
for (auto& group : groups_) {
TabGroupRange& group_range = group->range();
// Remove the item from the old group.
if (group.get() == old_group) {
group_range.ContractRight();
}
// Slide all groups after the removed tab to the left.
if (from_index < group_range.range_begin()) {
group_range.MoveLeft();
}
// Add the item to the new group.
if (group.get() == new_group) {
group_range.ExpandRight();
} else if (to_index <= group_range.range_begin()) {
// Slide all groups at or after the added tab to the right.
group_range.MoveRight();
}
}
// Move the wrapper.
std::unique_ptr<WebStateWrapper> web_state_wrapper =
std::move(web_state_wrappers_[from_index]);
web_state_wrappers_.erase(web_state_wrappers_.begin() + from_index);
web_state_wrappers_.insert(web_state_wrappers_.begin() + to_index,
std::move(web_state_wrapper));
// Update the active index if needed.
if (active_index_ == from_index) {
active_index_ = to_index;
} else {
int min = std::min(from_index, to_index);
int max = std::max(from_index, to_index);
int delta = from_index < to_index ? -1 : +1;
if (min <= active_index_ && active_index_ <= max) {
active_index_ += delta;
}
}
// Notify the observers of the change.
// The move didn't change the active WebState.
web::WebState* const active_web_state = GetActiveWebState();
const WebStateListStatus status = {.old_active_web_state = active_web_state,
.new_active_web_state = active_web_state};
if (index_changed) {
const WebStateListChangeMove move_change(
GetWebStateAt(to_index), from_index, to_index, pinned_state_changed,
old_group, new_group);
for (auto& observer : observers_) {
observer.WebStateListDidChange(this, move_change, status);
}
} else {
DCHECK(pinned_state_changed || group_changed);
const WebStateListChangeStatusOnly status_only_change(
GetWebStateAt(to_index), to_index, pinned_state_changed, old_group,
new_group);
for (auto& observer : observers_) {
observer.WebStateListDidChange(this, status_only_change, status);
}
}
// If the old group is now empty, delete it.
DeleteGroupIfEmpty(old_group);
}
void WebStateList::DeleteGroupIfEmpty(const TabGroup* group) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(locked_);
const auto iter = groups_.find(group);
if (iter != groups_.end() && group->range().count() == 0) {
// Notify observers of the imminent deletion of the group.
// The deletion doesn't change the active WebState.
web::WebState* const active_web_state = GetActiveWebState();
const WebStateListStatus status = {
.old_active_web_state = active_web_state,
.new_active_web_state = active_web_state};
const WebStateListChangeGroupDelete group_delete_change(group);
for (auto& observer : observers_) {
observer.WebStateListDidChange(this, group_delete_change, status);
}
// Actually delete the group.
groups_.erase(iter);
}
}
void WebStateList::SetActiveIndex(int active_index) {
if (active_index_ == active_index) {
return;
}
WebStateWrapper* old_active_web_state_wrapper = GetActiveWebStateWrapper();
if (old_active_web_state_wrapper &&
old_active_web_state_wrapper->ShouldResetOpenerOnActiveWebStateChange()) {
// Clear the opener when the active WebState changes.
old_active_web_state_wrapper->SetOpener(WebStateOpener());
}
active_index_ = active_index;
OnActiveWebStateChanged();
}
void WebStateList::OnActiveWebStateChanged() {
web::WebState* active_web_state = GetActiveWebState();
if (active_web_state) {
delegate_->WillActivateWebState(active_web_state);
}
}
void CloseAllWebStates(WebStateList& web_state_list, int close_flags) {
const int count = web_state_list.count();
const WebStateList::ScopedBatchOperation batch =
web_state_list.StartBatchOperation();
web_state_list.CloseWebStatesAtIndices(close_flags, RemovingIndexes({
.start = 0,
.count = count,
}));
}
void CloseAllNonPinnedWebStates(WebStateList& web_state_list, int close_flags) {
const int pinned_tabs_count = web_state_list.pinned_tabs_count();
const int regular_tabs_count = web_state_list.count() - pinned_tabs_count;
const WebStateList::ScopedBatchOperation batch =
web_state_list.StartBatchOperation();
web_state_list.CloseWebStatesAtIndices(close_flags,
RemovingIndexes({
.start = pinned_tabs_count,
.count = regular_tabs_count,
}));
}
void CloseAllWebStatesInGroup(WebStateList& web_state_list,
const TabGroup* group,
int close_flags) {
const TabGroupRange range = group->range();
const WebStateList::ScopedBatchOperation batch =
web_state_list.StartBatchOperation();
web_state_list.CloseWebStatesAtIndices(close_flags,
RemovingIndexes({
.start = range.range_begin(),
.count = range.count(),
}));
}
void CloseOtherWebStates(WebStateList& web_state_list,
int index_to_keep,
int close_flags) {
const int count = web_state_list.count();
const int pinned_count = web_state_list.pinned_tabs_count();
std::vector<int> indexes_to_close;
indexes_to_close.reserve(count - pinned_count);
for (int index = pinned_count; index < count; ++index) {
if (index == index_to_keep) {
continue;
}
indexes_to_close.push_back(index);
}
const WebStateList::ScopedBatchOperation batch =
web_state_list.StartBatchOperation();
web_state_list.CloseWebStatesAtIndices(
close_flags, RemovingIndexes(std::move(indexes_to_close)));
}