* Support tab groups in tree tab strip Integrate tab groups with the tree tab strip so that groups are represented as a single tree node wrapping the group (not one tree node per tab). Tabs inside a group remain direct children of the group collection. Collection and delegate: - BraveTreeTabStripCollectionDelegate: When adding a tab with new_group_id, add to collection without wrapping in a tree node; the group wraps it. MoveTabsRecursive: handle empty tab_indices (no-op when moving within same group), and route move-out-of-group and move-into-group to new helpers. - MoveTabsIntoGroup: unwrap tabs from tree nodes (or detach from other groups), add to target group; when group is detached (new group), wrap group in a TreeTabNode and attach at the correct tree position. - MoveTabsOutOfGroup: move tabs from TabGroupTabCollection back into the tree by wrapping each in a tree node at the destination index. - BraveTabStripCollection: add PopDetachedGroupCollectionForDelegate and GetTreeTabNodeIdForGroup; chromium_src TabStripCollection gains virtual GetTreeTabNodeIdForGroup. BraveTabStripCollectionDelegate implements GetTreeTabNodeIdForGroup. TreeTabNodeTabCollection: - BuildTreeTabs: wrap entire groups in one tree node (processed_groups set); grouped tabs stay as direct children of the group. - New constructor that wraps a TabGroupTabCollection (for creating a group in tree mode). Single-tab constructor allows null for GetEmptyTreeTabNode when a tab was moved into a group and the view still holds the old node id. Model, controller, and UI: - BraveTabStripModel::GetTreeTabNodeIdForGroup returns the tree node id for a group (or nullptr if tree tabs off). Wired through controller and BraveTabStripCollection. - BraveBrowserTabStripController::GetTreeTabNode returns GetEmptyTreeTabNode() when the node is null (e.g. tab just moved into group, before TabGroupedStateChanged/AddTabToGroup updates the view). - OnTreeTabChanged: handle tab index kNoTab and detached state during group creation; avoid double-clearing when AddTabToGroup will clear the node id. - BraveTabStrip::AddTabToGroup override sets the tab's tree_tab_node from GetTreeTabNodeIdForGroup when adding to a group in tree mode.
208 lines
7.0 KiB
C++
208 lines
7.0 KiB
C++
// Copyright (c) 2025 The Brave Authors. All rights reserved.
|
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
|
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
|
|
#include "brave/components/tabs/public/tree_tab_node.h"
|
|
|
|
#include <variant>
|
|
|
|
#include "base/functional/callback_helpers.h"
|
|
#include "base/logging.h"
|
|
#include "base/memory/ptr_util.h"
|
|
#include "base/memory/weak_ptr.h"
|
|
#include "brave/components/tabs/public/tree_tab_node_id.h"
|
|
#include "brave/components/tabs/public/tree_tab_node_tab_collection.h"
|
|
#include "components/tabs/public/split_tab_data.h"
|
|
#include "components/tabs/public/tab_interface.h"
|
|
|
|
namespace tabs {
|
|
|
|
const TreeTabNode& TreeTabNode::GetEmptyTreeTabNode() {
|
|
static base::NoDestructor<TreeTabNodeTabCollection>
|
|
empty_tree_tab_node_tab_collection(
|
|
tree_tab::TreeTabNodeId::GenerateNew(),
|
|
base::WrapUnique<TabInterface>(nullptr), base::DoNothing(),
|
|
base::DoNothing());
|
|
return empty_tree_tab_node_tab_collection->node();
|
|
}
|
|
|
|
TreeTabNode::TreeTabNode(TreeTabNodeTabCollection& collection,
|
|
const tree_tab::TreeTabNodeId& id)
|
|
: collection_(collection), id_(id) {}
|
|
|
|
int TreeTabNode::GetTreeHeight() const {
|
|
return collection_->GetTopLevelAncestor()->node().height();
|
|
}
|
|
|
|
std::vector<const TabInterface*> TreeTabNode::GetTabs() const {
|
|
const auto& value = collection_->current_value();
|
|
if (!value.has_value()) {
|
|
return {};
|
|
}
|
|
if (const base::WeakPtr<TabInterface>* tab_ptr =
|
|
std::get_if<base::WeakPtr<TabInterface>>(&*value)) {
|
|
if (const TabInterface* tab = tab_ptr->get()) {
|
|
return {tab};
|
|
}
|
|
return {};
|
|
}
|
|
if (const raw_ptr<SplitTabCollection>* split_ptr =
|
|
std::get_if<raw_ptr<SplitTabCollection>>(&*value)) {
|
|
SplitTabCollection* split = split_ptr->get();
|
|
if (split && split->data()) {
|
|
std::vector<const TabInterface*> result;
|
|
for (TabInterface* tab : split->data()->ListTabs()) {
|
|
result.push_back(tab);
|
|
}
|
|
return result;
|
|
}
|
|
return {};
|
|
}
|
|
if (const raw_ptr<TabGroupTabCollection>* group_ptr =
|
|
std::get_if<raw_ptr<TabGroupTabCollection>>(&*value)) {
|
|
TabGroupTabCollection* group = group_ptr->get();
|
|
if (group) {
|
|
std::vector<const TabInterface*> result;
|
|
for (TabInterface* tab : group->GetTabsRecursive()) {
|
|
result.push_back(tab);
|
|
}
|
|
return result;
|
|
}
|
|
return {};
|
|
}
|
|
|
|
return {};
|
|
}
|
|
|
|
std::optional<tree_tab::TreeTabNodeId>
|
|
TreeTabNode::GetClosestCollapsedAncestorId() const {
|
|
const TabCollection* current = collection_->GetParentCollection();
|
|
while (current && current->type() == TabCollection::Type::TREE_NODE) {
|
|
const auto* parent_tree =
|
|
static_cast<const TreeTabNodeTabCollection*>(current);
|
|
if (parent_tree->node().collapsed()) {
|
|
return parent_tree->node().id();
|
|
}
|
|
current = current->GetParentCollection();
|
|
}
|
|
return std::nullopt;
|
|
}
|
|
|
|
void TreeTabNode::CollectDescendantIds(
|
|
std::vector<tree_tab::TreeTabNodeId>& out) {
|
|
for (const auto& child : collection_->GetTreeNodeChildren()) {
|
|
if (std::holds_alternative<tabs::TabCollection*>(child)) {
|
|
TabCollection* collection = std::get<tabs::TabCollection*>(child);
|
|
if (collection->type() != TabCollection::Type::TREE_NODE) {
|
|
continue;
|
|
}
|
|
auto* child_tree = static_cast<TreeTabNodeTabCollection*>(collection);
|
|
const tree_tab::TreeTabNodeId& child_id = child_tree->node().id();
|
|
out.push_back(child_id);
|
|
child_tree->node().CollectDescendantIds(out);
|
|
}
|
|
}
|
|
}
|
|
|
|
void TreeTabNode::CollectUncollapsedDescendantIds(
|
|
std::vector<tree_tab::TreeTabNodeId>& out) {
|
|
for (const auto& child : collection_->GetTreeNodeChildren()) {
|
|
if (std::holds_alternative<tabs::TabCollection*>(child)) {
|
|
TabCollection* collection = std::get<tabs::TabCollection*>(child);
|
|
if (collection->type() != TabCollection::Type::TREE_NODE) {
|
|
continue;
|
|
}
|
|
auto* child_tree = static_cast<TreeTabNodeTabCollection*>(collection);
|
|
TreeTabNode& child_node = child_tree->node();
|
|
out.push_back(child_node.id());
|
|
if (!child_node.collapsed()) {
|
|
child_node.CollectUncollapsedDescendantIds(out);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
int TreeTabNode::CalculateLevelAndHeightRecursively(
|
|
base::PassKey<TreeTabNodeTabCollection> pass_key) {
|
|
return CalculateLevelAndHeightRecursivelyImpl();
|
|
}
|
|
|
|
void TreeTabNode::OnChildHeightChanged(
|
|
base::PassKey<TreeTabNodeTabCollection> pass_key) {
|
|
OnChildHeightChangedImpl();
|
|
}
|
|
|
|
int TreeTabNode::CalculateLevelAndHeightRecursivelyImpl() {
|
|
auto* parent_collection = collection_->GetParentCollection();
|
|
if (!parent_collection ||
|
|
parent_collection->type() != TabCollection::Type::TREE_NODE) {
|
|
// If there's no parent or the parent is not a tree node, this is the root.
|
|
level_ = 0;
|
|
} else {
|
|
auto* parent_tree_node =
|
|
static_cast<TreeTabNodeTabCollection*>(parent_collection);
|
|
level_ = parent_tree_node->node().level_ + 1;
|
|
}
|
|
|
|
int max_height = std::numeric_limits<int>::min();
|
|
for (const auto& child : collection_->GetTreeNodeChildren()) {
|
|
if (std::holds_alternative<tabs::TabCollection*>(child)) {
|
|
auto* collection = std::get<tabs::TabCollection*>(child);
|
|
if (collection->type() != TabCollection::Type::TREE_NODE) {
|
|
// If non-tree node child, e.g. split or group, height would be 0 for
|
|
// this node as it should not have descendants.
|
|
continue;
|
|
}
|
|
|
|
max_height = std::max(max_height,
|
|
static_cast<TreeTabNodeTabCollection*>(collection)
|
|
->node()
|
|
.CalculateLevelAndHeightRecursivelyImpl() +
|
|
1);
|
|
}
|
|
}
|
|
|
|
height_ = (max_height == std::numeric_limits<int>::min()) ? 0 : max_height;
|
|
|
|
return height_;
|
|
}
|
|
|
|
void TreeTabNode::OnChildHeightChangedImpl() {
|
|
// Update height of this node.
|
|
int max_height = std::numeric_limits<int>::min();
|
|
for (const auto& child : collection_->GetTreeNodeChildren()) {
|
|
if (std::holds_alternative<tabs::TabCollection*>(child)) {
|
|
auto* collection = std::get<tabs::TabCollection*>(child);
|
|
if (collection->type() != TabCollection::Type::TREE_NODE) {
|
|
// If non-tree node child, height would be 1 for this node
|
|
max_height = std::max(max_height, 1);
|
|
continue;
|
|
}
|
|
|
|
max_height = std::max(
|
|
max_height,
|
|
static_cast<TreeTabNodeTabCollection*>(collection)->node().height_ +
|
|
1);
|
|
}
|
|
}
|
|
|
|
auto new_height =
|
|
(max_height == std::numeric_limits<int>::min()) ? 0 : max_height;
|
|
if (new_height == height_) {
|
|
return;
|
|
}
|
|
|
|
height_ = new_height;
|
|
|
|
if (auto* parent_collection = collection_->GetParentCollection();
|
|
parent_collection &&
|
|
parent_collection->type() == TabCollection::Type::TREE_NODE) {
|
|
static_cast<TreeTabNodeTabCollection*>(parent_collection)
|
|
->node()
|
|
.OnChildHeightChangedImpl();
|
|
}
|
|
}
|
|
|
|
} // namespace tabs
|