semantics.dart 21.8 KB
Newer Older
Hixie's avatar
Hixie committed
1 2 3 4 5 6 7
// Copyright 2015 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:math' as math;
import 'dart:ui' show Rect;

8
import 'package:flutter/foundation.dart';
Hixie's avatar
Hixie committed
9
import 'package:flutter/painting.dart';
10
import 'package:meta/meta.dart';
Hixie's avatar
Hixie committed
11 12 13 14 15
import 'package:sky_services/semantics/semantics.mojom.dart' as mojom;
import 'package:vector_math/vector_math_64.dart';

import 'node.dart';

16 17 18 19 20 21 22 23 24 25
enum SemanticAction {
  tap,
  longPress,
  scrollLeft,
  scrollRight,
  scrollUp,
  scrollDown,
  increase,
  decrease,
}
Hixie's avatar
Hixie committed
26

27
/// Interface for [RenderObject]s to implement when they want to support
Hixie's avatar
Hixie committed
28 29 30
/// being tapped, etc.
///
/// These handlers will only be called if the relevant flag is set
31 32 33
/// (e.g. [handleSemanticTap]() will only be called if
/// [SemanticsNode.canBeTapped] is true, [handleSemanticScrollDown]() will only
/// be called if [SemanticsNode.canBeScrolledVertically] is true, etc).
34 35
abstract class SemanticActionHandler { // ignore: one_member_abstracts
  void performAction(SemanticAction action);
Hixie's avatar
Hixie committed
36 37
}

38 39 40 41 42 43 44 45 46 47
/// The type of function returned by [RenderObject.getSemanticAnnotators()].
///
/// These callbacks are called with the [SemanticsNode] object that
/// corresponds to the [RenderObject]. (One [SemanticsNode] can
/// correspond to multiple [RenderObject] objects.)
///
/// See [RenderObject.getSemanticAnnotators()] for details on the
/// contract that semantic annotators must follow.
typedef void SemanticAnnotator(SemanticsNode semantics);

Hixie's avatar
Hixie committed
48 49
enum _SemanticFlags {
  mergeAllDescendantsIntoThisNode,
50
  inheritedMergeAllDescendantsIntoThisNode, // whether an ancestor had mergeAllDescendantsIntoThisNode set
Hixie's avatar
Hixie committed
51 52 53 54
  hasCheckedState,
  isChecked,
}

55 56 57
/// Signature for a function that is called for each [SemanticsNode].
///
/// Return false to stop visiting nodes.
Hixie's avatar
Hixie committed
58 59
typedef bool SemanticsNodeVisitor(SemanticsNode node);

60 61 62 63 64 65
/// A node that represents some semantic data.
///
/// The semantics tree is maintained during the semantics phase of the pipeline
/// (i.e., during [PipelineOwner.flushSemantics]), which happens after
/// compositing. The semantics tree is then uploaded into the engine for use
/// by assistive technology.
Hixie's avatar
Hixie committed
66
class SemanticsNode extends AbstractNode {
67 68 69 70
  /// Creates a semantic node.
  ///
  /// Each semantic node has a unique identifier that is assigned when the node
  /// is created.
Hixie's avatar
Hixie committed
71 72 73 74 75
  SemanticsNode({
    SemanticActionHandler handler
  }) : _id = _generateNewId(),
       _actionHandler = handler;

76 77 78
  /// Creates a semantic node to represent the root of the semantics tree.
  ///
  /// The root node is assigned an identifier of zero.
Hixie's avatar
Hixie committed
79
  SemanticsNode.root({
80
    SemanticActionHandler handler,
81
    SemanticsOwner owner
Hixie's avatar
Hixie committed
82 83
  }) : _id = 0,
       _actionHandler = handler {
84
    attach(owner);
Hixie's avatar
Hixie committed
85 86 87 88 89 90 91 92 93 94 95 96 97 98 99
  }

  static int _lastIdentifier = 0;
  static int _generateNewId() {
    _lastIdentifier += 1;
    return _lastIdentifier;
  }

  final int _id;
  final SemanticActionHandler _actionHandler;


  // GEOMETRY
  // These are automatically handled by RenderObject's own logic

100 101 102 103 104
  /// The transform from this node's coordinate system to its parent's coordinate system.
  ///
  /// By default, the transform is null, which represents the identity
  /// transformation (i.e., that this node has the same coorinate system as its
  /// parent).
Hixie's avatar
Hixie committed
105
  Matrix4 get transform => _transform;
106
  Matrix4 _transform;
107
  set transform (Matrix4 value) {
Hixie's avatar
Hixie committed
108 109 110 111 112 113
    if (!MatrixUtils.matrixEquals(_transform, value)) {
      _transform = value;
      _markDirty();
    }
  }

114
  /// The bounding box for this node in its coordinate system.
Hixie's avatar
Hixie committed
115 116
  Rect get rect => _rect;
  Rect _rect = Rect.zero;
117
  set rect (Rect value) {
Hixie's avatar
Hixie committed
118 119 120 121 122 123 124
    assert(value != null);
    if (_rect != value) {
      _rect = value;
      _markDirty();
    }
  }

125
  /// Whether [rect] might have been influenced by clips applied by ancestors.
Hixie's avatar
Hixie committed
126 127 128 129 130 131
  bool wasAffectedByClip = false;


  // FLAGS AND LABELS
  // These are supposed to be set by SemanticAnnotator obtained from getSemanticAnnotators

132 133
  final Set<SemanticAction> _actions = new Set<SemanticAction>();

134 135 136 137 138
  /// Adds the given action to the set of semantic actions.
  ///
  /// If the user chooses to perform an action,
  /// [SemanticActionHandler.performAction] will be called with the chosen
  /// action.
139 140 141 142 143
  void addAction(SemanticAction action) {
    if (_actions.add(action))
      _markDirty();
  }

144
  /// Adds the [SemanticAction.scrollLeft] and [SemanticAction.scrollRight] actions.
145 146 147 148 149
  void addHorizontalScrollingActions() {
    addAction(SemanticAction.scrollLeft);
    addAction(SemanticAction.scrollRight);
  }

150
  /// Adds the [SemanticAction.scrollUp] and [SemanticAction.scrollDown] actions.
151 152 153 154 155
  void addVerticalScrollingActions() {
    addAction(SemanticAction.scrollUp);
    addAction(SemanticAction.scrollDown);
  }

156 157 158 159 160 161
  /// Adds the [SemanticAction.increase] and [SemanticAction.decrease] actions.
  void addAdjustmentActions() {
    addAction(SemanticAction.increase);
    addAction(SemanticAction.decrease);
  }

162 163 164 165
  bool _hasAction(SemanticAction action) {
    return _actionHandler != null && _actions.contains(action);
  }

Hixie's avatar
Hixie committed
166 167 168 169 170 171 172 173 174 175 176
  BitField<_SemanticFlags> _flags = new BitField<_SemanticFlags>.filled(_SemanticFlags.values.length, false);

  void _setFlag(_SemanticFlags flag, bool value, { bool needsHandler: false }) {
    assert(value != null);
    assert((!needsHandler) || (_actionHandler != null) || (value == false));
    if (_flags[flag] != value) {
      _flags[flag] = value;
      _markDirty();
    }
  }

177
  /// Whether all this node and all of its descendants should be treated as one logical entity.
Hixie's avatar
Hixie committed
178
  bool get mergeAllDescendantsIntoThisNode => _flags[_SemanticFlags.mergeAllDescendantsIntoThisNode];
179
  set mergeAllDescendantsIntoThisNode(bool value) => _setFlag(_SemanticFlags.mergeAllDescendantsIntoThisNode, value);
Hixie's avatar
Hixie committed
180

181
  bool get _inheritedMergeAllDescendantsIntoThisNode => _flags[_SemanticFlags.inheritedMergeAllDescendantsIntoThisNode];
182
  set _inheritedMergeAllDescendantsIntoThisNode(bool value) => _setFlag(_SemanticFlags.inheritedMergeAllDescendantsIntoThisNode, value);
183 184 185

  bool get _shouldMergeAllDescendantsIntoThisNode => mergeAllDescendantsIntoThisNode || _inheritedMergeAllDescendantsIntoThisNode;

186
  /// Whether this node has Boolean state that can be controlled by the user.
Hixie's avatar
Hixie committed
187
  bool get hasCheckedState => _flags[_SemanticFlags.hasCheckedState];
188
  set hasCheckedState(bool value) => _setFlag(_SemanticFlags.hasCheckedState, value);
Hixie's avatar
Hixie committed
189

190
  /// If this node has Boolean state that can be controlled by the user, whether that state is on or off, cooresponding to `true` and `false`, respectively.
Hixie's avatar
Hixie committed
191
  bool get isChecked => _flags[_SemanticFlags.isChecked];
192
  set isChecked(bool value) => _setFlag(_SemanticFlags.isChecked, value);
Hixie's avatar
Hixie committed
193

194
  /// A textual description of this node.
Hixie's avatar
Hixie committed
195 196
  String get label => _label;
  String _label = '';
197
  set label(String value) {
Hixie's avatar
Hixie committed
198 199 200 201 202 203 204
    assert(value != null);
    if (_label != value) {
      _label = value;
      _markDirty();
    }
  }

205
  /// Restore this node to its default state.
Hixie's avatar
Hixie committed
206
  void reset() {
207
    bool hadInheritedMergeAllDescendantsIntoThisNode = _inheritedMergeAllDescendantsIntoThisNode;
208
    _actions.clear();
Hixie's avatar
Hixie committed
209
    _flags.reset();
210 211
    if (hadInheritedMergeAllDescendantsIntoThisNode)
      _inheritedMergeAllDescendantsIntoThisNode = true;
Hixie's avatar
Hixie committed
212 213 214 215 216
    _label = '';
    _markDirty();
  }

  List<SemanticsNode> _newChildren;
217 218

  /// Append the given children as children of this node.
Hixie's avatar
Hixie committed
219 220 221 222
  void addChildren(Iterable<SemanticsNode> children) {
    _newChildren ??= <SemanticsNode>[];
    _newChildren.addAll(children);
    // we do the asserts afterwards because children is an Iterable
223
    // and doing the asserts before would mean the behavior is
Hixie's avatar
Hixie committed
224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243
    // different in checked mode vs release mode (if you walk an
    // iterator after having reached the end, it'll just start over;
    // the values are not cached).
    assert(!_newChildren.any((SemanticsNode child) => child == this));
    assert(() {
      SemanticsNode ancestor = this;
      while (ancestor.parent is SemanticsNode)
        ancestor = ancestor.parent;
      assert(!_newChildren.any((SemanticsNode child) => child == ancestor));
      return true;
    });
    assert(() {
      Set<SemanticsNode> seenChildren = new Set<SemanticsNode>();
      for (SemanticsNode child in _newChildren)
        assert(seenChildren.add(child)); // check for duplicate adds
      return true;
    });
  }

  List<SemanticsNode> _children;
244 245

  /// Whether this node has a non-zero number of children.
Hixie's avatar
Hixie committed
246 247
  bool get hasChildren => _children?.isNotEmpty ?? false;
  bool _dead = false;
248 249 250 251 252 253

  /// Called during the compilation phase after all the children of this node have been compiled.
  ///
  /// This function lets the semantic node respond to all the changes to its
  /// child list for the given frame at once instead of needing to process the
  /// changes incrementally as new children are compiled.
Hixie's avatar
Hixie committed
254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280
  void finalizeChildren() {
    if (_children != null) {
      for (SemanticsNode child in _children)
        child._dead = true;
    }
    if (_newChildren != null) {
      for (SemanticsNode child in _newChildren)
        child._dead = false;
    }
    bool sawChange = false;
    if (_children != null) {
      for (SemanticsNode child in _children) {
        if (child._dead) {
          if (child.parent == this) {
            // we might have already had our child stolen from us by
            // another node that is deeper in the tree.
            dropChild(child);
          }
          sawChange = true;
        }
      }
    }
    if (_newChildren != null) {
      for (SemanticsNode child in _newChildren) {
        if (child.parent != this) {
          if (child.parent != null) {
            // we're rebuilding the tree from the bottom up, so it's possible
281
            // that our child was, in the last pass, a child of one of our
Hixie's avatar
Hixie committed
282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300
            // ancestors. In that case, we drop the child eagerly here.
            // TODO(ianh): Find a way to assert that the same node didn't
            // actually appear in the tree in two places.
            child.parent?.dropChild(child);
          }
          assert(!child.attached);
          adoptChild(child);
          sawChange = true;
        }
      }
    }
    List<SemanticsNode> oldChildren = _children;
    _children = _newChildren;
    oldChildren?.clear();
    _newChildren = oldChildren;
    if (sawChange)
      _markDirty();
  }

301 302 303
  @override
  SemanticsOwner get owner => super.owner;

304
  @override
Hixie's avatar
Hixie committed
305
  SemanticsNode get parent => super.parent;
306 307

  @override
Hixie's avatar
Hixie committed
308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327
  void redepthChildren() {
    if (_children != null) {
      for (SemanticsNode child in _children)
        redepthChild(child);
    }
  }

  // Visits all the descendants of this node, calling visitor for each one, until
  // visitor returns false. Returns true if all the visitor calls returned true,
  // otherwise returns false.
  bool _visitDescendants(SemanticsNodeVisitor visitor) {
    if (_children != null) {
      for (SemanticsNode child in _children) {
        if (!visitor(child) || !child._visitDescendants(visitor))
          return false;
      }
    }
    return true;
  }

328
  @override
329
  void attach(SemanticsOwner owner) {
330
    super.attach(owner);
331 332 333 334 335 336 337
    assert(!owner._nodes.containsKey(_id));
    owner._nodes[_id] = this;
    owner._detachedNodes.remove(this);
    if (_dirty) {
      _dirty = false;
      _markDirty();
    }
338 339
    if (parent != null)
      _inheritedMergeAllDescendantsIntoThisNode = parent._shouldMergeAllDescendantsIntoThisNode;
Hixie's avatar
Hixie committed
340 341
    if (_children != null) {
      for (SemanticsNode child in _children)
342
        child.attach(owner);
Hixie's avatar
Hixie committed
343 344
    }
  }
345 346

  @override
Hixie's avatar
Hixie committed
347
  void detach() {
348 349 350 351
    assert(owner._nodes.containsKey(_id));
    assert(!owner._detachedNodes.contains(this));
    owner._nodes.remove(_id);
    owner._detachedNodes.add(this);
Hixie's avatar
Hixie committed
352 353 354 355 356 357 358 359 360 361 362 363
    super.detach();
    if (_children != null) {
      for (SemanticsNode child in _children)
        child.detach();
    }
  }

  bool _dirty = false;
  void _markDirty() {
    if (_dirty)
      return;
    _dirty = true;
364 365 366 367
    if (attached) {
      assert(!owner._detachedNodes.contains(this));
      owner._dirtyNodes.add(this);
    }
Hixie's avatar
Hixie committed
368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388
  }

  mojom.SemanticsNode _serialize() {
    mojom.SemanticsNode result = new mojom.SemanticsNode();
    result.id = _id;
    if (_dirty) {
      // We could be even more efficient about not sending data here, by only
      // sending the bits that are dirty (tracking the geometry, flags, strings,
      // and children separately). For now, we send all or nothing.
      result.geometry = new mojom.SemanticGeometry();
      result.geometry.transform = transform?.storage;
      result.geometry.top = rect.top;
      result.geometry.left = rect.left;
      result.geometry.width = math.max(rect.width, 0.0);
      result.geometry.height = math.max(rect.height, 0.0);
      result.flags = new mojom.SemanticFlags();
      result.flags.hasCheckedState = hasCheckedState;
      result.flags.isChecked = isChecked;
      result.strings = new mojom.SemanticStrings();
      result.strings.label = label;
      List<mojom.SemanticsNode> children = <mojom.SemanticsNode>[];
389 390
      Set<SemanticAction> mergedActions = new Set<SemanticAction>();
      mergedActions.addAll(_actions);
391
      if (_shouldMergeAllDescendantsIntoThisNode) {
Hixie's avatar
Hixie committed
392
        _visitDescendants((SemanticsNode node) {
393
          mergedActions.addAll(node._actions);
Hixie's avatar
Hixie committed
394 395 396 397
          result.flags.hasCheckedState = result.flags.hasCheckedState || node.hasCheckedState;
          result.flags.isChecked = result.flags.isChecked || node.isChecked;
          if (node.label != '')
            result.strings.label = result.strings.label.isNotEmpty ? '${result.strings.label}\n${node.label}' : node.label;
398
          node._dirty = false;
Hixie's avatar
Hixie committed
399 400 401 402 403 404 405 406 407 408
          return true; // continue walk
        });
        // and we pretend to have no children
      } else {
        if (_children != null) {
          for (SemanticsNode child in _children)
            children.add(child._serialize());
        }
      }
      result.children = children;
409 410 411
      result.actions = <int>[];
      for (SemanticAction action in mergedActions)
        result.actions.add(action.index);
Hixie's avatar
Hixie committed
412 413 414 415 416
      _dirty = false;
    }
    return result;
  }

417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456
  @override
  String toString() {
    StringBuffer buffer = new StringBuffer();
    buffer.write('$runtimeType($_id');
    if (_dirty)
      buffer.write(" (${ owner != null && owner._dirtyNodes.contains(this) ? 'dirty' : 'STALE' })");
    if (_shouldMergeAllDescendantsIntoThisNode)
      buffer.write(' (leaf merge)');
    buffer.write('; $rect');
    if (wasAffectedByClip)
      buffer.write(' (clipped)');
    for (SemanticAction action in _actions) {
      buffer.write('; $action');
    }
    if (hasCheckedState) {
      if (isChecked)
        buffer.write('; checked');
      else
        buffer.write('; unchecked');
    }
    if (label.isNotEmpty)
      buffer.write('; "$label"');
    buffer.write(')');
    return buffer.toString();
  }

  /// Returns a string representation of this node and its descendants.
  String toStringDeep([String prefixLineOne = '', String prefixOtherLines = '']) {
    String result = '$prefixLineOne$this\n';
    if (_children != null && _children.isNotEmpty) {
      for (int index = 0; index < _children.length - 1; index += 1) {
        SemanticsNode child = _children[index];
        result += '${child.toStringDeep("$prefixOtherLines \u251C", "$prefixOtherLines \u2502")}';
      }
      result += '${_children.last.toStringDeep("$prefixOtherLines \u2514", "$prefixOtherLines  ")}';
    }
    return result;
  }
}

457 458 459 460 461 462 463 464
/// Signature for functions that receive updates about render tree semantics.
typedef void SemanticsListener(List<mojom.SemanticsNode> nodes);

/// Owns [SemanticsNode] objects and notifies listeners of changes to the
/// render tree semantics.
///
/// To listen for semantic updates, call [PipelineOwner.addSemanticsListener],
/// which will create a [SemanticsOwner] if necessary.
465
class SemanticsOwner {
466 467 468 469 470 471 472 473 474 475 476 477 478 479
  /// Creates a [SemanticsOwner].
  ///
  /// The `onLastListenerRemoved` argument must not be null and will be called
  /// when the last listener is removed from this object.
  SemanticsOwner({
    @required SemanticsListener initialListener,
    @required VoidCallback onLastListenerRemoved
  }) : _onLastListenerRemoved = onLastListenerRemoved {
    assert(_onLastListenerRemoved != null);
    addListener(initialListener);
  }

  final VoidCallback _onLastListenerRemoved;

480
  final Set<SemanticsNode> _dirtyNodes = new Set<SemanticsNode>();
481 482 483
  final Map<int, SemanticsNode> _nodes = <int, SemanticsNode>{};
  final Set<SemanticsNode> _detachedNodes = new Set<SemanticsNode>();

484
  final List<SemanticsListener> _listeners = <SemanticsListener>[];
485

486
  /// Releases any resources retained by this object.
487
  ///
488 489 490 491 492 493 494
  /// Requires that there are no listeners registered with [addListener].
  void dispose() {
    assert(_listeners.isEmpty);
    _dirtyNodes.clear();
    _nodes.clear();
    _detachedNodes.clear();
  }
495 496 497 498 499 500

  /// Add a consumer of semantic data.
  ///
  /// After the [PipelineOwner] updates the semantic data for a given frame, it
  /// calls [sendSemanticsTree], which uploads the data to each listener
  /// registered with this function.
501 502 503
  ///
  /// Listeners can be removed with [removeListener].
  void addListener(SemanticsListener listener) {
504 505 506
    _listeners.add(listener);
  }

507 508 509 510 511 512 513 514 515
  /// Removes a consumer of semantic data.
  ///
  /// Listeners can be added with [addListener].
  void removeListener(SemanticsListener listener) {
    _listeners.remove(listener);
    if (_listeners.isEmpty)
      _onLastListenerRemoved();
  }

516
  /// Uploads the semantics tree to the listeners registered with [addListener].
517
  void sendSemanticsTree() {
518
    assert(_listeners.isNotEmpty);
Hixie's avatar
Hixie committed
519 520 521 522 523 524 525 526 527
    for (SemanticsNode oldNode in _detachedNodes) {
      // The other side will have forgotten this node if we even send
      // it again, so make sure to mark it dirty so that it'll get
      // sent if it is resurrected.
      oldNode._dirty = true;
    }
    _detachedNodes.clear();
    if (_dirtyNodes.isEmpty)
      return;
528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559
    List<SemanticsNode> visitedNodes = <SemanticsNode>[];
    while (_dirtyNodes.isNotEmpty) {
      List<SemanticsNode> localDirtyNodes = _dirtyNodes.toList();
      _dirtyNodes.clear();
      localDirtyNodes.sort((SemanticsNode a, SemanticsNode b) => a.depth - b.depth);
      visitedNodes.addAll(localDirtyNodes);
      for (SemanticsNode node in localDirtyNodes) {
        assert(node._dirty);
        assert(node.parent == null || !node.parent._shouldMergeAllDescendantsIntoThisNode || node._inheritedMergeAllDescendantsIntoThisNode);
        if (node._shouldMergeAllDescendantsIntoThisNode) {
          assert(node.mergeAllDescendantsIntoThisNode || node.parent != null);
          if (node.mergeAllDescendantsIntoThisNode ||
              node.parent != null && node.parent._shouldMergeAllDescendantsIntoThisNode) {
            // if we're merged into our parent, make sure our parent is added to the list
            if (node.parent != null && node.parent._shouldMergeAllDescendantsIntoThisNode)
              node.parent._markDirty(); // this can add the node to the dirty list
            // make sure all the descendants are also marked, so that if one gets marked dirty later we know to walk up then too
            if (node._children != null) {
              for (SemanticsNode child in node._children)
                child._inheritedMergeAllDescendantsIntoThisNode = true; // this can add the node to the dirty list
            }
          } else {
            // we previously were being merged but aren't any more
            // update our bits and all our descendants'
            assert(node._inheritedMergeAllDescendantsIntoThisNode);
            assert(!node.mergeAllDescendantsIntoThisNode);
            assert(node.parent == null || !node.parent._shouldMergeAllDescendantsIntoThisNode);
            node._inheritedMergeAllDescendantsIntoThisNode = false;
            if (node._children != null) {
              for (SemanticsNode child in node._children)
                child._inheritedMergeAllDescendantsIntoThisNode = false; // this can add the node to the dirty list
            }
560 561
          }
        }
562
      }
563
    }
564
    visitedNodes.sort((SemanticsNode a, SemanticsNode b) => a.depth - b.depth);
Hixie's avatar
Hixie committed
565
    List<mojom.SemanticsNode> updatedNodes = <mojom.SemanticsNode>[];
566
    for (SemanticsNode node in visitedNodes) {
Hixie's avatar
Hixie committed
567 568 569 570 571 572 573 574 575 576 577 578 579 580
      assert(node.parent?._dirty != true); // could be null (no parent) or false (not dirty)
      // 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 && node.attached)
        updatedNodes.add(node._serialize());
    }
581 582
    for (SemanticsListener listener in new List<SemanticsListener>.from(_listeners))
      listener(updatedNodes);
Hixie's avatar
Hixie committed
583 584 585
    _dirtyNodes.clear();
  }

586
  SemanticActionHandler _getSemanticActionHandlerForId(int id, { @required SemanticAction action }) {
587
    assert(action != null);
Hixie's avatar
Hixie committed
588
    SemanticsNode result = _nodes[id];
589
    if (result != null && result._shouldMergeAllDescendantsIntoThisNode && !result._hasAction(action)) {
Hixie's avatar
Hixie committed
590
      result._visitDescendants((SemanticsNode node) {
591
        if (node._actionHandler != null && node._hasAction(action)) {
Hixie's avatar
Hixie committed
592 593 594 595 596 597
          result = node;
          return false; // found node, abort walk
        }
        return true; // continue walk
      });
    }
598
    if (result == null || !result._hasAction(action))
Hixie's avatar
Hixie committed
599 600 601 602
      return null;
    return result._actionHandler;
  }

603 604 605 606
  /// Asks the [SemanticsNode] with the given id to perform the given action.
  ///
  /// If the [SemanticsNode] has not indicated that it can perform the action,
  /// this function does nothing.
607 608 609
  void performAction(int id, SemanticAction action) {
    SemanticActionHandler handler = _getSemanticActionHandlerForId(id, action: action);
    handler?.performAction(action);
Hixie's avatar
Hixie committed
610 611
  }
}