blob: 6b65757348a01f790e760f2abe31836039383386 [file] [log] [blame]
// Copyright 2023 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/organization/tab_organization.h"
#include <optional>
#include <string>
#include <unordered_set>
#include <variant>
#include "base/debug/dump_without_crashing.h"
#include "chrome/browser/ui/tabs/organization/tab_data.h"
#include "chrome/browser/ui/tabs/tab_group_model.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "components/tab_groups/tab_group_id.h"
#include "components/tabs/public/tab_group.h"
#include "components/tabs/public/tab_interface.h"
namespace {
constexpr int kMinValidTabsForOrganizing = 2;
int kNextOrganizationID = 1;
} // namespace
TabOrganization::TabOrganization(
TabDatas tab_datas,
std::vector<std::u16string> names,
int first_new_tab_index,
std::variant<size_t, std::u16string> current_name,
UserChoice choice)
: first_new_tab_index_(first_new_tab_index),
names_(names),
current_name_(current_name),
choice_(choice),
organization_id_(kNextOrganizationID) {
for (auto& tab_data : tab_datas_) {
tab_data->AddObserver(this);
}
kNextOrganizationID++;
// TabDatas must not be duplicates, immediately destroy TabDatas that are.
std::vector<const tabs::TabInterface*> existing_tabs;
for (auto& tab_data : tab_datas) {
if (!base::Contains(existing_tabs, tab_data->tab())) {
existing_tabs.emplace_back(tab_data->tab());
tab_data->AddObserver(this);
tab_datas_.emplace_back(std::move(tab_data));
}
}
}
TabOrganization::~TabOrganization() {
for (auto& tab_data : tab_datas_) {
tab_data->RemoveObserver(this);
}
for (auto& observer : observers_) {
observer.OnTabOrganizationDestroyed(organization_id_);
}
}
const std::u16string TabOrganization::GetDisplayName() const {
if (std::holds_alternative<size_t>(current_name())) {
const size_t index = std::get<size_t>(current_name());
CHECK(index < names_.size());
return names_.at(index);
} else if (std::holds_alternative<std::u16string>(current_name())) {
return std::get<std::u16string>(current_name());
}
return u"";
}
void TabOrganization::AddObserver(TabOrganization::Observer* observer) {
observers_.AddObserver(observer);
}
void TabOrganization::RemoveObserver(TabOrganization::Observer* observer) {
observers_.RemoveObserver(observer);
}
bool TabOrganization::IsValidForOrganizing() const {
if (invalidated_by_tab_change_) {
return false;
}
// There must be at least 1 tab that is new to the group.
if ((tab_datas_.size() - first_new_tab_index_) == 0) {
return false;
}
// There must be at least 2 tabs that are valid for organization.
int valid_tab_count = 0;
for (const std::unique_ptr<TabData>& tab_data : tab_datas_) {
if (tab_data->IsValidForOrganizing(group_id_)) {
valid_tab_count++;
if (valid_tab_count >= kMinValidTabsForOrganizing) {
return true;
}
}
}
return false;
}
// TODO(crbug.com/40925231) Add UKM/UMA Logging on user add.
void TabOrganization::AddTabData(std::unique_ptr<TabData> new_tab_data) {
// Guarantee uniqueness. early return and drop the new tab data if not unique.
for (std::unique_ptr<TabData>& existing_tab_data : tab_datas_) {
if (existing_tab_data->tab() == new_tab_data->tab()) {
return;
}
}
new_tab_data->AddObserver(this);
tab_datas_.emplace_back(std::move(new_tab_data));
NotifyObserversOfUpdate();
}
// TODO(crbug.com/40925231) Add UKM/UMA Logging on user remove.
void TabOrganization::RemoveTabData(TabData::TabID tab_id) {
TabDatas::iterator position =
std::find_if(tab_datas_.begin(), tab_datas_.end(),
[tab_id](const std::unique_ptr<TabData>& tab_data) {
return tab_data->tab_id() == tab_id;
});
CHECK(position != tab_datas_.end());
CHECK(static_cast<size_t>(first_new_tab_index_) < tab_datas_.size());
// If the removed tab is already a part of the tab group (if any)
// corresponding to this organization, decrement |first_new_tab_index_| to
// account for its removal.
if (position < tab_datas_.begin() + first_new_tab_index_) {
first_new_tab_index_--;
}
user_removed_tab_ids_.push_back(tab_id);
tab_datas_.erase(position);
NotifyObserversOfUpdate();
}
void TabOrganization::SetCurrentName(
std::variant<size_t, std::u16string> new_current_name) {
current_name_ = new_current_name;
NotifyObserversOfUpdate();
}
void TabOrganization::Accept() {
CHECK(choice_ == UserChoice::kNoChoice);
CHECK(IsValidForOrganizing());
choice_ = UserChoice::kAccepted;
CHECK(tab_datas_.size() > 0);
TabStripModel* tab_strip_model = tab_datas_[0]->original_tab_strip_model();
CHECK(tab_strip_model);
std::vector<int> valid_indices;
std::unordered_set<raw_ptr<const tabs::TabInterface>> tab_data_tabs;
for (const std::unique_ptr<TabData>& tab_data : tab_datas_) {
// Individual tabs may become invalid. in those cases, where the tab is
// invalid but the organization is not, do not include the tab in the
// organization, but still create the organization.
const tabs::TabInterface* tab = tab_data->tab();
tab_data_tabs.insert(tab);
const int index = tab_strip_model->GetIndexOfTab(tab);
if (tab_data->IsValidForOrganizing() &&
!base::Contains(valid_indices, index)) {
valid_indices.emplace_back(index);
}
}
std::sort(valid_indices.begin(), valid_indices.end());
// TODO(b/319273296): Find a more permanent fix.
// From this point on, we start modifying the tab strip, which
// potentially notifies a large set of observers. TabOrganizationSession
// (which owns |this|) gets destroyed when a tab is added or removed
// from the tab strip. There is a risk that a tab strip observer modifies
// the tab strip and therefore causes |this| to be deleted. So we keep
// a WeakPtr to |this| to detect this case and avoid accessing member
// variables, just in case.
base::WeakPtr<TabOrganization> this_weak_ref = weak_ptr_factory_.GetWeakPtr();
if (group_id_.has_value()) {
CHECK(tab_strip_model->group_model()->ContainsTabGroup(group_id_.value()));
tab_strip_model->AddToExistingGroup(valid_indices, group_id_.value(), true);
// Remove tabs that should not longer be a part of the group. Do this after
// adding new tabs to avoid the group ever becoming empty, which would
// delete the group.
TabGroup* const tab_group =
tab_strip_model->group_model()->GetTabGroup(group_id_.value());
const gfx::Range tab_indices = tab_group->ListTabs();
std::vector<int> indices_to_remove;
for (size_t grouped_tab_index = tab_indices.start();
grouped_tab_index < tab_indices.end(); grouped_tab_index++) {
const tabs::TabInterface* const tab =
tab_strip_model->GetTabAtIndex(grouped_tab_index);
if (!tab_data_tabs.contains(tab)) {
indices_to_remove.emplace_back(grouped_tab_index);
}
}
tab_strip_model->RemoveFromGroup(indices_to_remove);
} else {
group_id_ =
std::make_optional(tab_strip_model->AddToNewGroup(valid_indices));
// Move the entire group to the start left of the tabstrip.
// Iterate through the tabstrip model looking for the first non pinned, non
// grouped tab. If this group is already in the leftmost position then leave
// it there. Else move the group at the index of that tab.
int move_index = tab_strip_model->IndexOfFirstNonPinnedTab();
while (move_index < tab_strip_model->GetTabCount() &&
(tab_strip_model->GetTabGroupForTab(move_index).has_value() &&
tab_strip_model->GetTabGroupForTab(move_index).value() !=
group_id_.value())) {
move_index++;
}
CHECK(move_index < tab_strip_model->GetTabCount());
if (tab_strip_model->GetTabGroupForTab(move_index) != group_id_.value()) {
tab_strip_model->MoveGroupTo(group_id_.value(), move_index);
}
}
TabGroup* const tab_group =
tab_strip_model->group_model()->GetTabGroup(group_id_.value());
tab_groups::TabGroupVisualData new_visual_data(
GetDisplayName(), tab_group->visual_data()->color());
tab_strip_model->ChangeTabGroupVisuals(tab_group->id(), new_visual_data,
tab_group->IsCustomized());
// If |this| has been destroyed, there is no need to notify the observers:
// in practice, the only observer is the TabOrganizationSession which owns
// this object (and therefore has been destroyed) and who will just
// notify WebUI it has been updated (of which there is no need because
// WebUI is now tracking the new TabOrganizationSession which has replaced
// the destroyed one).
if (this_weak_ref) {
NotifyObserversOfUpdate();
} else {
// We'd like to know if this really happens: if so, we should really
// change the ownership model of TabOrganizationSession.
base::debug::DumpWithoutCrashing();
}
}
void TabOrganization::Reject() {
CHECK(choice_ == UserChoice::kNoChoice);
choice_ = UserChoice::kRejected;
NotifyObserversOfUpdate();
}
void TabOrganization::OnTabDataUpdated(const TabData* tab_data) {
if (!tab_data->IsValidForOrganizing(group_id_)) {
invalidated_by_tab_change_ = true;
}
NotifyObserversOfUpdate();
}
void TabOrganization::OnTabDataDestroyed(TabData::TabID tab_id) {
// Only invalidate if RemoveTabData was not previously called on this tab id.
// Closure of a tab that is a part of an organization should invalidate it,
// but removal of the tab from the organization should not.
if (std::find(user_removed_tab_ids_.begin(), user_removed_tab_ids_.end(),
tab_id) == user_removed_tab_ids_.end()) {
invalidated_by_tab_change_ = true;
NotifyObserversOfUpdate();
}
}
void TabOrganization::NotifyObserversOfUpdate() {
for (auto& observer : observers_) {
observer.OnTabOrganizationUpdated(this);
}
}