flutter_tree_view icon indicating copy to clipboard operation
flutter_tree_view copied to clipboard

add new node and drag this new node will error: Multiple widgets used the same GlobalKey

Open leavky opened this issue 1 year ago • 0 comments

add new node and drag this new node will error: Another exception was thrown: Multiple widgets used the same GlobalKey.

drag_and_drop.dart in example,

import 'dart:math' show Random;

import 'package:flutter/material.dart';
import 'package:flutter_fancy_tree_view/flutter_fancy_tree_view.dart';

import '../shared.dart' show watchAnimationDurationSetting;

//! Disclaimer: This example is very opinionated, it may not work for your usecase.

class Node {
  Node({
    required this.id,
    Iterable<Node>? children,
  }) : _children = <Node>[] {
    if (children == null) return;

    for (final Node child in children) {
      child._parent = this;
      _children.add(child);
    }
  }

  final int id;
  final List<Node> _children;

  Iterable<Node> get children => _children;
  bool get isLeaf => _children.isEmpty;

  Node? get parent => _parent;
  Node? _parent;

  int get index => _parent?._children.indexOf(this) ?? -1;

  void insertChild(int index, Node node) {
    // Adjust the index if necessary when dropping a node at the same parent
    if (node._parent == this && node.index < index) {
      index--;
    }

    // Ensure the node is removed from its previous parent and update it
    node
      .._parent?._children.remove(node)
      .._parent = this;

    _children.insert(index, node);
  }
}

extension on TreeDragAndDropDetails<Node> {
  /// Splits the target node's height in three and checks the vertical offset
  /// of the dragging node, applying the appropriate callback.
  T mapDropPosition<T>({
    required T Function() whenAbove,
    required T Function() whenInside,
    required T Function() whenBelow,
  }) {
    final double oneThirdOfTotalHeight = targetBounds.height * 0.3;
    final double pointerVerticalOffset = dropPosition.dy;

    if (pointerVerticalOffset < oneThirdOfTotalHeight) {
      return whenAbove();
    } else if (pointerVerticalOffset < oneThirdOfTotalHeight * 2) {
      return whenInside();
    } else {
      return whenBelow();
    }
  }
}

class DragAndDropTreeView extends StatefulWidget {
  const DragAndDropTreeView({super.key});

  @override
  State<DragAndDropTreeView> createState() => _DragAndDropTreeViewState();
}

class _DragAndDropTreeViewState extends State<DragAndDropTreeView> {
  late final Node root;
  late final TreeController<Node> treeController;

  @override
  void initState() {
    super.initState();
    root = Node(id: -1);
    populateExampleTree(root);

    treeController = TreeController<Node>(
      roots: root.children,
      childrenProvider: (Node node) => node.children,

      // The parentProvider is extremely important when automatically expanding
      // and collapsing tree nodes on hover, as the [TreeDragTarget] needs to
      // ensure that it doesn't collapse an ancestor of the dragging node as it
      // would be removed from the view stopping the drag updates and callbacks.
      //
      // When not provided, the [TreeController] would need to first locate the
      // target node in the tree and then check its ancestors, which could be
      // very expensive for deep trees.
      parentProvider: (Node node) => node.parent,
    );
  }

  @override
  void dispose() {
    treeController.dispose();
    super.dispose();
  }

  void onNodeAccepted(TreeDragAndDropDetails<Node> details) {
    Node? newParent;
    int newIndex = 0;

    details.mapDropPosition(
      whenAbove: () {
        // Insert the dragged node as the previous sibling of the target node.
        newParent = details.targetNode.parent;
        newIndex = details.targetNode.index;
      },
      whenInside: () {
        // Insert the dragged node as the last child of the target node.
        newParent = details.targetNode;
        newIndex = details.targetNode.children.length;

        // Ensure that the dragged node is visible after reordering.
        treeController.setExpansionState(details.targetNode, true);
      },
      whenBelow: () {
        // Insert the dragged node as the next sibling of the target node.
        newParent = details.targetNode.parent;
        newIndex = details.targetNode.index + 1;
      },
    );

    (newParent ?? root).insertChild(newIndex, details.draggedNode);

    // Rebuild the tree to show the reordered node in its new vicinity.
    treeController.rebuild();
  }

  Duration? longPressDelay;

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    longPressDelay = switch (Theme.of(context).platform) {
      TargetPlatform.android || TargetPlatform.fuchsia || TargetPlatform.iOS => Durations.long2,
      TargetPlatform.linux || TargetPlatform.macOS || TargetPlatform.windows => null,
    };
  }

  @override
  Widget build(BuildContext context) {
    final IndentGuide indentGuide = DefaultIndentGuide.of(context);
    final BorderSide borderSide = BorderSide(
      color: Theme.of(context).colorScheme.outline,
      width: indentGuide is AbstractLineGuide ? indentGuide.thickness : 2.0,
    );

    return AnimatedTreeView<Node>(
      treeController: treeController,
      nodeBuilder: (BuildContext context, TreeEntry<Node> entry) {
        return DragAndDropTreeTile(
          entry: entry,
          treeController: treeController,
          borderSide: borderSide,
          longPressDelay: longPressDelay,
          onNodeAccepted: onNodeAccepted,
          onFolderPressed: () => treeController.toggleExpansion(entry.node),
        );
      },
      duration: watchAnimationDurationSetting(context),
    );
  }
}

class DragAndDropTreeTile extends StatelessWidget {
  const DragAndDropTreeTile({
    super.key,
    required this.entry,
    required this.onNodeAccepted,
    required this.treeController,
    this.borderSide = BorderSide.none,
    this.longPressDelay,
    this.onFolderPressed,
  });

  final TreeEntry<Node> entry;
  final TreeDragTargetNodeAccepted<Node> onNodeAccepted;
  final BorderSide borderSide;
  final Duration? longPressDelay;
  final VoidCallback? onFolderPressed;
  final TreeController<Node> treeController;

  @override
  Widget build(BuildContext context) {
    return TreeDragTarget<Node>(
      node: entry.node,
      onNodeAccepted: onNodeAccepted,
      builder: (BuildContext context, TreeDragAndDropDetails<Node>? details) {
        Decoration? decoration;

        if (details != null) {
          // Add a border to indicate in which portion of the target's height
          // the dragging node will be inserted.
          decoration = BoxDecoration(
            border: details.mapDropPosition(
              whenAbove: () => Border(top: borderSide),
              whenInside: () => Border.fromBorderSide(borderSide),
              whenBelow: () => Border(bottom: borderSide),
            ),
          );
        }

        return TreeDraggable<Node>(
          node: entry.node,
          longPressDelay: longPressDelay,
          childWhenDragging: Opacity(
            opacity: .5,
            child: IgnorePointer(
              child: TreeTile(treeController: treeController, entry: entry),
            ),
          ),
          feedback: IntrinsicWidth(
            child: Material(
              elevation: 4,
              child: TreeTile(
                entry: entry,
                treeController: treeController,
                showIndentation: false,
                onFolderPressed: () {},
              ),
            ),
          ),
          child: TreeTile(
            entry: entry,
            treeController: treeController,
            onFolderPressed: entry.node.isLeaf ? null : onFolderPressed,
            decoration: decoration,
          ),
        );
      },
    );
  }
}

class TreeTile extends StatelessWidget {
  const TreeTile({
    super.key,
    required this.entry,
    required this.treeController,
    this.onFolderPressed,
    this.decoration,
    this.showIndentation = true,
  });

  final TreeEntry<Node> entry;
  final VoidCallback? onFolderPressed;
  final Decoration? decoration;
  final bool showIndentation;
  final TreeController<Node> treeController;

  @override
  Widget build(BuildContext context) {
    Widget content = Padding(
      padding: const EdgeInsetsDirectional.only(end: 8),
      child: Row(
        children: [
          FolderButton(
            isOpen: entry.node.isLeaf ? null : entry.isExpanded,
            onPressed: onFolderPressed,
          ),
          Expanded(
            child: Text('Node ${entry.node.id}'),
          ),
          IconButton(
            onPressed: () {
              entry.node._children.add(Node(id: _uniqueId++));
              treeController.rebuild();
            },
            icon: const Icon(Icons.add, size: 20),
          ),
        ],
      ),
    );

    if (decoration != null) {
      content = DecoratedBox(
        decoration: decoration!,
        child: content,
      );
    }

    if (showIndentation) {
      return TreeIndentation(
        entry: entry,
        child: content,
      );
    }

    return content;
  }
}

int _uniqueId = 0;
void populateExampleTree(
  Node node, [
  int level = 0,
  Random? rng,
  int minChildCount = 3,
]) {
  if (level >= 3) return;

  rng ??= Random();

  for (int index = 0; index <= minChildCount + rng.nextInt(3); ++index) {
    final child = Node(id: _uniqueId++).._parent = node;
    node._children.add(child);
    populateExampleTree(child, level + 1, rng, 1);
  }
}

image

leavky avatar Jun 02 '24 08:06 leavky