blob: 178b0376b5fbc33708fb1aedc184a9ed0c5c0d34 [file] [log] [blame]
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:collection';
import 'dart:math';
/// Non-UI specific tree code should live in this file.
///
/// This file does not have direct dependencies on dart:html and therefore
/// allows for testing to be done on the VM.
// TODO(kenz): look into consolidating logic between this file and
// ui/trees.dart, which houses generic tree types vs the base classes in this
// file.
abstract class TreeNode<T extends TreeNode<T>> {
T? parent;
final List<T> children = [];
// TODO(jacobr) should impact depth.
bool indentChildren = true;
/// Index in [parent.children].
int index = -1;
/// Depth of this tree, including [this].
///
/// We assume that TreeNodes are not modified after the first time [depth] is
/// accessed. We would need to clear the cache before accessing, otherwise.
int get depth {
if (_depth != 0) {
return _depth;
}
for (T child in children) {
_depth = max(_depth, child.depth);
}
return _depth = _depth + 1;
}
int _depth = 0;
bool get isRoot => parent == null;
bool get isSelected => _selected;
bool _selected = false;
T get root {
if (_root != null) {
return _root!;
}
if (parent == null) return _root = this as T;
return _root = parent!.root;
}
T? _root;
/// The level (0-based) of this tree node in the tree.
int get level {
if (_level != null) {
return _level!;
}
if (parent == null) return _level = 0;
return _level = 1 + parent!.level;
}
int? _level;
/// Whether the tree table node is expandable.
bool get isExpandable => children.isNotEmpty;
/// Whether the node is currently expanded in the tree table.
bool get isExpanded => _isExpanded;
bool _isExpanded = false;
// TODO(gmoothart): expand does not check isExpandable, which can lead to
// inconsistent state, particular in combination with expandCascading. We
// should clean this up.
void expand() {
_isExpanded = true;
}
void select() {
_selected = true;
}
void unselect() {
_selected = false;
}
// TODO(jacobr): cache the value of whether the node should be shown
// so that lookups on this class are O(1) invalidating the cache when nodes
// up the tree are expanded and collapsed.
/// Whether this node should be shown in the tree.
///
/// When using this, consider caching the value. It is O([level]) to compute.
bool shouldShow() =>
parent == null || (parent!.isExpanded && parent!.shouldShow());
void collapse() {
_isExpanded = false;
}
void toggleExpansion() {
_isExpanded = !_isExpanded;
}
/// Override to handle pressing on a Leaf node.
void leaf() {}
void addChild(T child, {int? index}) {
index ??= children.length;
assert(index <= children.length);
children.insert(index, child);
child.parent = this as T?;
child.index = index;
for (int i = index + 1; i < children.length; ++i) {
children[i].index++;
}
}
T removeChildAtIndex(int index) {
assert(index < children.length);
for (int i = index + 1; i < children.length; ++i) {
children[i].index--;
}
return children.removeAt(index);
}
void addAllChildren(List<T> children) {
children.forEach(addChild);
}
/// Expands this node and all of its children (cascading).
void expandCascading() {
breadthFirstTraversal<T>(
this as T,
action: (T node) {
node.expand();
},
);
}
/// Expands this node and each parent node recursively.
void expandAscending() {
expand();
parent?.expandAscending();
}
/// Collapses this node and all of its children (cascading).
void collapseCascading() {
breadthFirstTraversal<T>(
this as T,
action: (T node) {
node.collapse();
},
);
}
void removeLastChild() {
children.removeLast();
}
bool subtreeHasNodeWithCondition(bool condition(T node)) {
final T? childWithCondition = firstChildWithCondition(condition);
return childWithCondition != null;
}
/// Returns a list of nodes in this tree that match [condition].
///
/// This list may include the root.
List<T> nodesWithCondition(bool condition(T node)) {
final _nodes = <T>[];
breadthFirstTraversal<T>(
this as T,
action: (node) {
if (condition(node)) {
_nodes.add(node);
}
},
);
return _nodes;
}
/// Returns a list of shallow nodes that match [condition], meaning that if
/// a node matches [condition], none of its children will be included in the
/// returned list, even if those children happen to match [condition].
///
/// In other words, only the top-most node in each tree branch that matches
/// [condition] will be included in the returned list. This list may include
/// the root.
List<T> shallowNodesWithCondition(bool condition(T node)) {
final _nodes = <T>[];
depthFirstTraversal<T>(
this as T,
action: (T node) {
if (condition(node)) {
_nodes.add(node);
}
},
exploreChildrenCondition: (T node) => !condition(node),
);
return _nodes;
}
T? firstChildWithCondition(bool condition(T node)) {
return breadthFirstTraversal<T>(
this as T,
returnCondition: condition,
);
}
/// Locates the first sub-node in the tree at level [level].
///
/// [level] is relative to the subtree root [this].
///
/// For example:
///
/// [level 0] A
/// / \
/// [level 1] B E
/// / / \
/// [level 2] C F G
/// /
/// [level 3] D
///
/// E.firstSubNodeAtLevel(1) => F
T? firstChildNodeAtLevel(int level) {
return _childNodeAtLevelWithCondition(
level,
// When this condition is called, we have already ensured that
// [level] < [depth], so at least one child is guaranteed to meet the
// firstWhere condition.
(currentNode, levelWithOffset) => currentNode.children
.firstWhere((n) => n.depth + n.level > levelWithOffset),
);
}
/// Locates the last sub-node in the tree at level [level].
///
/// [level] is relative to the subtree root [this].
///
/// For example:
///
/// [level 0] A
/// / \
/// [level 1] B E
/// / / \
/// [level 2] C F G
/// /
/// [level 3] D
///
/// E.lastSubNodeAtLevel(1) => G
T? lastChildNodeAtLevel(int level) {
return _childNodeAtLevelWithCondition(
level,
// When this condition is called, we have already ensured that
// [level] < [depth], so at least one child is guaranteed to meet the
// lastWhere condition.
(currentNode, levelWithOffset) => currentNode.children
.lastWhere((n) => n.depth + n.level > levelWithOffset),
);
}
// TODO(kenz): We should audit this method with a very large tree:
// https://github.com/flutter/devtools/issues/1480.
/// Finds a child node at [level] where traversal order is determined by
/// [traversalCondition].
///
/// The runtime of this method is O(level * tree width). The worst case
/// scenario is searching for a very deep level in a very wide tree.
T? _childNodeAtLevelWithCondition(
int level,
T traversalCondition(T currentNode, int levelWithOffset),
) {
if (level >= depth) return null;
// The current node [this] is not guaranteed to be at level 0, so we need
// to account for the level offset of [this].
final levelWithOffset = level + this.level;
TreeNode<T> currentNode = this;
while (currentNode.level < levelWithOffset) {
// Walk down the tree until we find the node at [level].
if (currentNode.children.isNotEmpty) {
currentNode = traversalCondition(currentNode as T, levelWithOffset);
}
}
return currentNode as T?;
}
TreeNode<T> shallowCopy();
/// Filters a tree starting at this node and returns a list of new roots after
/// filtering, where all nodes in the new tree(s) meet the condition `filter`.
///
/// If the root [this] should be included in the filtered results, the list
/// will contain one node. If the root [this] should not be included in the
/// filtered results, the list may contain one or more nodes.
List<T> filterWhere(bool filter(T node)) {
List<T> walkAndCopy(T node) {
if (filter(node)) {
final copy = node.shallowCopy();
for (final child in node.children) {
copy.addAllChildren(walkAndCopy(child));
}
return [copy as T];
}
return [for (final child in node.children) ...walkAndCopy(child)];
}
return walkAndCopy(this as T);
}
int childCountToMatchingNode({
bool matchingNodeCondition(T node)?,
bool includeCollapsedNodes = true,
}) {
var index = 0;
final matchingNode = depthFirstTraversal<T>(
root,
returnCondition: matchingNodeCondition,
exploreChildrenCondition:
includeCollapsedNodes ? null : (T node) => node.isExpanded,
action: (T _) => index++,
);
if (matchingNode != null) return index;
return -1;
}
}
/// Traverses a tree in breadth-first order.
///
/// [returnCondition] specifies the condition for which we should stop
/// traversing the tree. For example, if we are calling this method to perform
/// BFS, [returnCondition] would specify when we have found the node we are
/// searching for. [action] specifies an action that we will execute on each
/// node. For example, if we need to traverse a tree and change a property on
/// every single node, we would do this through the [action] param.
T? breadthFirstTraversal<T extends TreeNode<T>>(
T root, {
bool returnCondition(T node)?,
void action(T node)?,
}) {
return _treeTraversal(
root,
bfs: true,
returnCondition: returnCondition,
action: action,
);
}
/// Traverses a tree in depth-first preorder order.
///
/// [returnCondition] specifies the condition for which we should stop
/// traversing the tree. For example, if we are calling this method to perform
/// DFS, [returnCondition] would specify when we have found the node we are
/// searching for. [action] specifies an action that we will execute on each
/// node. For example, if we need to traverse a tree and change a property on
/// every single node, we would do this through the [action] param.
/// [exploreChildrenCondition] specifies the condition for which we should
/// explore the children of a node. By default, all children are explored.
T? depthFirstTraversal<T extends TreeNode<T>>(
T root, {
bool returnCondition(T node)?,
void action(T node)?,
bool exploreChildrenCondition(T node)?,
}) {
return _treeTraversal(
root,
bfs: false,
returnCondition: returnCondition,
action: action,
exploreChildrenCondition: exploreChildrenCondition,
);
}
T? _treeTraversal<T extends TreeNode<T>>(
T root, {
bool bfs = true,
bool returnCondition(T node)?,
void action(T node)?,
bool exploreChildrenCondition(T node)?,
}) {
final toVisit = Queue.of([root]);
while (toVisit.isNotEmpty) {
final node = bfs ? toVisit.removeFirst() : toVisit.removeLast();
if (returnCondition != null && returnCondition(node)) {
return node;
}
if (action != null) {
action(node);
}
if (exploreChildrenCondition == null || exploreChildrenCondition(node)) {
// For DFS, reverse the children to gaurantee preorder traversal.
final children = bfs ? node.children : node.children.reversed;
children.forEach(toVisit.add);
}
}
return null;
}