sendSemanticsUpdate method

void sendSemanticsUpdate()

Update the semantics using onSemanticsUpdate.

Implementation

void sendSemanticsUpdate() {
  // Once the tree is up-to-date, verify that every node is visible.
  assert(() {
    final invisibleNodes = <SemanticsNode>[];
    // Finds the invisible nodes in the tree rooted at `node` and adds them to
    // the invisibleNodes list. If a node is itself invisible, all its
    // descendants will be skipped.
    bool findInvisibleNodes(SemanticsNode node) {
      if (node.rect.isEmpty) {
        invisibleNodes.add(node);
      } else if (!node.mergeAllDescendantsIntoThisNode) {
        node.visitChildren(findInvisibleNodes);
      }
      return true;
    }

    final SemanticsNode? rootSemanticsNode = this.rootSemanticsNode;
    if (rootSemanticsNode != null) {
      // The root node is allowed to be invisible when it has no children.
      if (rootSemanticsNode.childrenCount > 0 && rootSemanticsNode.rect.isEmpty) {
        invisibleNodes.add(rootSemanticsNode);
      } else if (!rootSemanticsNode.mergeAllDescendantsIntoThisNode) {
        rootSemanticsNode.visitChildren(findInvisibleNodes);
      }
    }

    if (invisibleNodes.isEmpty) {
      return true;
    }

    List<DiagnosticsNode> nodeToMessage(SemanticsNode invisibleNode) {
      final SemanticsNode? parent = invisibleNode.parent;
      return <DiagnosticsNode>[
        invisibleNode.toDiagnosticsNode(style: DiagnosticsTreeStyle.errorProperty),
        parent?.toDiagnosticsNode(
              name: 'which was added as a child of',
              style: DiagnosticsTreeStyle.errorProperty,
            ) ??
            ErrorDescription('which was added as the root SemanticsNode'),
      ];
    }

    throw FlutterError.fromParts(<DiagnosticsNode>[
      ErrorSummary('Invisible SemanticsNodes should not be added to the tree.'),
      ErrorDescription('The following invisible SemanticsNodes were added to the tree:'),
      ...invisibleNodes.expand(nodeToMessage),
      ErrorHint(
        'An invisible SemanticsNode is one whose rect is not on screen hence not reachable for users, '
        'and its semantic information is not merged into a visible parent.',
      ),
      ErrorHint(
        'An invisible SemanticsNode makes the accessibility experience confusing, '
        'as it does not provide any visual indication when the user selects it '
        'via accessibility technologies.',
      ),
      ErrorHint(
        'Consider removing the above invisible SemanticsNodes if they were added by your '
        'RenderObject.assembleSemanticsNode implementation, or filing a bug on GitHub:\n'
        '  https://github.com/flutter/flutter/issues/new?template=02_bug.yml',
      ),
    ]);
  }());

  if (_dirtyNodes.isEmpty) {
    return;
  }
  final customSemanticsActionIds = <int>{};
  final visitedNodes = <SemanticsNode>[];
  while (_dirtyNodes.isNotEmpty) {
    final List<SemanticsNode> localDirtyNodes = _dirtyNodes
        .where((SemanticsNode node) => !_detachedNodes.contains(node))
        .toList();
    _dirtyNodes.clear();
    _detachedNodes.clear();
    localDirtyNodes.sort((SemanticsNode a, SemanticsNode b) => a.depth - b.depth);
    visitedNodes.addAll(localDirtyNodes);
    for (final node in localDirtyNodes) {
      assert(node._dirty);
      assert(node.parent == null || !node.parent!.isPartOfNodeMerging || node.isMergedIntoParent);
      if (node.isPartOfNodeMerging) {
        assert(node.mergeAllDescendantsIntoThisNode || node.parent != null);
        // If child node is merged into its parent, make sure the parent is marked as dirty
        if (node.parent != null && node.parent!.isPartOfNodeMerging) {
          node.parent!._markDirty(); // this can add the node to the dirty list
          node._dirty = false; // Do not send update for this node, as it's now part of its parent
        }
      }

      // Clean up the dirty entry in owner._traversalParentNodes map because it
      // will be updated later.
      _traversalParentNodes.removeWhere((Object key, SemanticsNode oldNode) => node == oldNode);
      // Clean up the node from the value set in owner._traversalChildNodes.
      for (final Set<SemanticsNode> childSet in _traversalChildNodes.values) {
        childSet.removeWhere((SemanticsNode oldNode) => node == oldNode);
      }
    }
  }

  visitedNodes.sort((SemanticsNode a, SemanticsNode b) => a.depth - b.depth);
  final SemanticsUpdateBuilder builder = SemanticsBinding.instance.createSemanticsUpdateBuilder();

  final updatedVisitedNodes = <SemanticsNode>[];

  for (final node in visitedNodes) {
    final bool isTraversalParent = node._isTraversalParent;
    final bool isTraversalChild = node._isTraversalChild;

    if (kIsWeb) {
      updatedVisitedNodes.add(node);
    } else {
      if (!isTraversalParent && !isTraversalChild) {
        updatedVisitedNodes.add(node);
        continue;
      }

      if (isTraversalChild) {
        // If the node has a non-null `_traversalChildIdentifier`, it indicates
        // that its hit-test parent and traversal parent are different, and
        // its traversal parent should update its children to include this node.
        // Therefore, its traversal parent node should be added to the
        // `updatedVisitedNodes` list for later grafting, in order to generate
        // a correct `childrenIntraversalOrder`. This is typically used in
        // `OverlayPortal` widget.
        final SemanticsNode? parentNode = _traversalParentNodes[node.traversalChildIdentifier];
        if (parentNode != null && !updatedVisitedNodes.contains(parentNode)) {
          updatedVisitedNodes.add(parentNode);
        }
      }

      updatedVisitedNodes.add(node);
    }

    // If the node is a traversal parent, then add it to the
    // _traversalParentNodes map for later grafting. Similarly, add the node
    // to the _traversalChildNodes map if it is a traversal child.
    if (isTraversalParent) {
      assert(
        !_traversalParentNodes.containsKey(node._traversalParentIdentifier) ||
            _traversalParentNodes[node.traversalParentIdentifier!] == node,
        'The traversalParentIdentifier must be unique. No two semantics nodes can share the same traversalParentIdentifier.',
      );
      _traversalParentNodes[node.traversalParentIdentifier!] = node;
    } else if (isTraversalChild) {
      _traversalChildNodes[node.traversalChildIdentifier!] ??= <SemanticsNode>{};
      _traversalChildNodes[node.traversalChildIdentifier!]!.add(node);
    }
  }

  for (final node in updatedVisitedNodes) {
    assert(
      node.parent?._dirty != true || node._isTraversalParent,
    ); // could be null (no parent) or false (not dirty)

    // The traversalParentNode is added to updatedVisitedNodes for later
    // grafting; its traversalChildren should be grafted to its children in
    // the traversal order. This grafting process is skipped on web because
    // the traversal order will be handled in the web engine.
    final bool needUpdateTraversalParent = !kIsWeb && node._isTraversalParent;
    // The _serialize() method marks the node as not dirty, and
    // recurses through the tree to do a deep serialization of all
    // contiguous dirty nodes. This means that when we return here,
    // it's quite possible that subsequent nodes are no longer
    // dirty. We skip these here.
    // We also skip any nodes that were reset and subsequently
    // dropped entirely (RenderObject.markNeedsSemanticsUpdate()
    // calls reset() on its SemanticsNode if onlyChanges isn't set,
    // which happens e.g. when the node is no longer contributing
    // semantics).
    if ((node._dirty || needUpdateTraversalParent) && node.attached) {
      node._addToUpdate(builder, customSemanticsActionIds);
    }
  }
  _dirtyNodes.clear();
  for (final actionId in customSemanticsActionIds) {
    final CustomSemanticsAction action = CustomSemanticsAction.getAction(actionId)!;
    builder.updateCustomAction(
      id: actionId,
      label: action.label,
      hint: action.hint,
      overrideId: action.action?.index ?? -1,
    );
  }
  onSemanticsUpdate(builder.build());
  notifyListeners();
}