collapseAll method

void collapseAll({
  1. bool animate = true,
  2. int? maxDepth,
})

Collapses all nodes in the tree.

Uses batch operations for better performance with large trees.

Implementation

void collapseAll({bool animate = true, int? maxDepth}) {
  if (animationDuration == Duration.zero) animate = false;
  // Collect all expanded nodes and their visible descendants
  final nodesToCollapse = <TKey>[];
  final nodesToHide = <TKey>[];
  for (final rootId in _roots) {
    if (_isExpandedKey(rootId)) {
      nodesToCollapse.add(rootId);
      nodesToHide.addAll(_getVisibleDescendants(rootId));
    }
  }
  // Also check for nodes that are entering (from an interrupted expandAll)
  final nodesToHideSet = nodesToHide.toSet();

  // Check standalone entering animations
  for (final nid in _activeStandaloneNids) {
    final state = _standaloneByNid[nid]!;
    if (state.type != AnimationType.entering) continue;
    final key = _nids.keyOfUnchecked(nid);
    if (nodesToHideSet.contains(key)) continue;
    if (_parentKeyOfKey(key) != null) {
      nodesToHide.add(key);
      nodesToHideSet.add(key);
    }
  }

  // Check operation group members (expanding)
  for (final group in _operationGroups.values) {
    if (group.pendingRemoval.isEmpty) {
      // Group is expanding
      for (final key in group.members.keys) {
        if (!nodesToHideSet.contains(key)) {
          if (_parentKeyOfKey(key) != null) {
            nodesToHide.add(key);
            nodesToHideSet.add(key);
          }
        }
      }
    }
  }

  // Check bulk group members (expanding nodes)
  if (_bulkAnimationGroup != null) {
    for (final key in _bulkAnimationGroup!.members) {
      if (!nodesToHideSet.contains(key)) {
        if (_parentKeyOfKey(key) != null) {
          nodesToHide.add(key);
          nodesToHideSet.add(key);
        }
      }
    }
  }

  if (nodesToHide.isEmpty) {
    if (nodesToCollapse.isNotEmpty) {
      _collapseAllInRegistry(maxDepth);
      // Bulk expansion-state clear — see main collapseAll branch below.
      _notifyStructural();
    }
    return;
  }
  // Clear expansion state for ALL nodes within depth limit,
  // not just visible ones.
  _collapseAllInRegistry(maxDepth);
  _structureGeneration++;
  if (animate) {
    // Reverse expanding operation groups
    bool opGroupReversed = false;
    for (final entry in _operationGroups.entries) {
      final group = entry.value;
      if (group.pendingRemoval.isEmpty) {
        // Group is expanding — reverse it
        for (final nodeId in group.members.keys) {
          if (!_isPendingDeletion(nodeId)) {
            group.pendingRemoval.add(nodeId);
            opGroupReversed = true;
          }
        }
        // Normalize startExtent to 0 so the reversal terminates at
        // zero instead of at a captured mid-flight start value.
        for (final member in group.members.entries) {
          member.value.startExtent = 0.0;
        }
        group.controller.reverse();
      }
    }
    if (opGroupReversed) _bumpAnimGen();

    // Check if there's an expanding bulk animation we can reverse
    if (_bulkAnimationGroup != null &&
        _bulkAnimationGroup!.members.isNotEmpty &&
        _bulkAnimationGroup!.pendingRemoval.isEmpty) {
      // Mark all members for removal when animation completes at 0
      for (final key in _bulkAnimationGroup!.members) {
        if (!_isPendingDeletion(key)) {
          _addBulkPending(key);
        }
      }

      // Handle additional nodes not in any group
      for (final key in nodesToHide) {
        if (_isPendingDeletion(key)) continue;
        if (!_bulkAnimationGroup!.members.contains(key) &&
            !_hasOperationGroup(key)) {
          _startStandaloneExitAnimation(key);
        }
      }

      // Reverse the controller direction
      _bulkAnimationGroup!.controller.reverse();
      _bumpBulkGen();
    } else {
      // Dispose old group and create fresh with value=1.0
      _disposeBulkAnimationGroup();
      _bulkAnimationGroup = _createBulkAnimationGroup(initialValue: 1.0);

      // Add nodes to the bulk group, keeping individually-animating
      // nodes on their own timeline for smooth transitions.
      for (final key in nodesToHide) {
        if (_isPendingDeletion(key)) continue;
        if (_hasOperationGroup(key)) continue;
        if (_hasStandalone(key)) {
          // Reverse standalone animation smoothly
          _startStandaloneExitAnimation(key);
        } else {
          _removeAnimation(key);
          _addBulkMember(key);
          _addBulkPending(key);
        }
      }

      // Start collapsing (value 1 -> 0)
      if (_bulkAnimationGroup!.members.isNotEmpty) {
        _bulkAnimationGroup!.controller.reverse();
      }
      _bumpBulkGen();
    }
  } else {
    // Remove immediately
    final toRemove = <TKey>{};
    for (final key in nodesToHide) {
      if (!_isPendingDeletion(key)) {
        toRemove.add(key);
        _removeAnimation(key);
      }
    }
    if (toRemove.isNotEmpty) {
      _removeFromVisibleOrder(toRemove);
    }
  }
  // Bulk expansion-state clear: every node whose isExpanded state flipped
  // may render differently (chevron rotation, etc.). The set can span
  // arbitrary subtrees; fall back to a full refresh.
  _notifyStructural();
}