semantics.dart 25.5 KB
Newer Older
Hixie's avatar
Hixie committed
1 2 3 4
// 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.

5
import 'dart:ui' as ui;
6
import 'dart:ui' show Rect, SemanticsAction, SemanticsFlags;
7
import 'dart:typed_data';
Hixie's avatar
Hixie committed
8

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

import 'node.dart';

16
export 'dart:ui' show SemanticsAction;
Hixie's avatar
Hixie committed
17

18
/// Interface for [RenderObject]s to implement when they want to support
Hixie's avatar
Hixie committed
19 20 21
/// being tapped, etc.
///
/// These handlers will only be called if the relevant flag is set
22 23 24
/// (e.g. [handleSemanticTap]() will only be called if
/// [SemanticsNode.canBeTapped] is true, [handleSemanticScrollDown]() will only
/// be called if [SemanticsNode.canBeScrolledVertically] is true, etc).
25
abstract class SemanticsActionHandler { // ignore: one_member_abstracts
26
  /// Called when the object implementing this interface receives a
27
  /// [SemanticsAction]. For example, if the user of an accessibility tool
28 29
  /// instructs their device that they wish to tap a button, the [RenderObject]
  /// behind that button would have its [performAction] method called with the
30 31
  /// [SemanticsAction.tap] action.
  void performAction(SemanticsAction action);
Hixie's avatar
Hixie committed
32 33
}

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

44 45 46
/// Signature for a function that is called for each [SemanticsNode].
///
/// Return false to stop visiting nodes.
Hixie's avatar
Hixie committed
47 48
typedef bool SemanticsNodeVisitor(SemanticsNode node);

49 50 51 52 53 54 55 56 57 58 59
/// Summary information about a [SemanticsNode] object.
///
/// A semantics node might [SemanticsNode.mergeAllDescendantsIntoThisNode],
/// which means the individual fields on the semantics node don't fully describe
/// the semantics at that node. This data structure contains the full semantics
/// for the node.
///
/// Typically obtained from [SemanticsNode.getSemanticsData].
class SemanticsData {
  /// Creates a semantics data object.
  ///
60 61
  /// The [flags], [actions], [label], and [Rect] arguments must not be null.
  SemanticsData({
62 63 64 65 66
    @required this.flags,
    @required this.actions,
    @required this.label,
    @required this.rect,
    this.transform
67 68 69 70 71 72
  }) {
    assert(flags != null);
    assert(actions != null);
    assert(label != null);
    assert(rect != null);
  }
73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97

  /// A bit field of [SemanticsFlags] that apply to this node.
  final int flags;

  /// A bit field of [SemanticsActions] that apply to this node.
  final int actions;

  /// A textual description of this node.
  final String label;

  /// The bounding box for this node in its coordinate system.
  final Rect rect;

  /// 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).
  final Matrix4 transform;

  /// Whether [flags] contains the given flag.
  bool hasFlag(SemanticsFlags flag) => (flags & flag.index) != 0;

  /// Whether [actions] contains the given action.
  bool hasAction(SemanticsAction action) => (actions & action.index) != 0;
98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132

  @override
  String toString() {
    StringBuffer buffer = new StringBuffer();
    buffer.write('$runtimeType($rect');
    if (transform != null)
      buffer.write('; $transform');
    for (SemanticsAction action in SemanticsAction.values.values) {
      if ((actions & action.index) != 0)
        buffer.write('; $action');
    }
    for (SemanticsFlags flag in SemanticsFlags.values.values) {
      if ((flags & flag.index) != 0)
        buffer.write('; $flag');
    }
    if (label.isNotEmpty)
      buffer.write('; "$label"');
    buffer.write(')');
    return buffer.toString();
  }

  @override
  bool operator ==(dynamic other) {
    if (other is! SemanticsData)
      return false;
    final SemanticsData typedOther = other;
    return typedOther.flags == flags
        && typedOther.actions == actions
        && typedOther.label == label
        && typedOther.rect == rect
        && typedOther.transform == transform;
  }

  @override
  int get hashCode => hashValues(flags, actions, label, rect, transform);
133 134
}

135 136 137 138 139 140
/// 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
141
class SemanticsNode extends AbstractNode {
142 143 144 145
  /// Creates a semantic node.
  ///
  /// Each semantic node has a unique identifier that is assigned when the node
  /// is created.
Hixie's avatar
Hixie committed
146
  SemanticsNode({
147
    SemanticsActionHandler handler
148
  }) : id = _generateNewId(),
Hixie's avatar
Hixie committed
149 150
       _actionHandler = handler;

151 152 153
  /// 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
154
  SemanticsNode.root({
155
    SemanticsActionHandler handler,
156
    SemanticsOwner owner
157
  }) : id = 0,
Hixie's avatar
Hixie committed
158
       _actionHandler = handler {
159
    attach(owner);
Hixie's avatar
Hixie committed
160 161 162 163 164 165 166 167
  }

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

168 169 170 171 172
  /// The unique identifier for this node.
  ///
  /// The root node has an id of zero. Other nodes are given a unique id when
  /// they are created.
  final int id;
Hixie's avatar
Hixie committed
173

174
  final SemanticsActionHandler _actionHandler;
Hixie's avatar
Hixie committed
175 176 177 178

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

179 180 181 182 183
  /// 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
184
  Matrix4 get transform => _transform;
185
  Matrix4 _transform;
186
  set transform (Matrix4 value) {
Hixie's avatar
Hixie committed
187 188 189 190 191 192
    if (!MatrixUtils.matrixEquals(_transform, value)) {
      _transform = value;
      _markDirty();
    }
  }

193
  /// The bounding box for this node in its coordinate system.
Hixie's avatar
Hixie committed
194 195
  Rect get rect => _rect;
  Rect _rect = Rect.zero;
196
  set rect (Rect value) {
Hixie's avatar
Hixie committed
197 198 199 200 201 202 203
    assert(value != null);
    if (_rect != value) {
      _rect = value;
      _markDirty();
    }
  }

204
  /// Whether [rect] might have been influenced by clips applied by ancestors.
Hixie's avatar
Hixie committed
205 206 207 208
  bool wasAffectedByClip = false;


  // FLAGS AND LABELS
209
  // These are supposed to be set by SemanticsAnnotator obtained from getSemanticsAnnotators
Hixie's avatar
Hixie committed
210

211
  int _actions = 0;
212

213 214 215
  /// Adds the given action to the set of semantic actions.
  ///
  /// If the user chooses to perform an action,
216
  /// [SemanticsActionHandler.performAction] will be called with the chosen
217
  /// action.
218
  void addAction(SemanticsAction action) {
219 220 221
    final int index = action.index;
    if ((_actions & index) == 0) {
      _actions |= index;
222
      _markDirty();
223
    }
224 225
  }

226
  /// Adds the [SemanticsAction.scrollLeft] and [SemanticsAction.scrollRight] actions.
227
  void addHorizontalScrollingActions() {
228 229
    addAction(SemanticsAction.scrollLeft);
    addAction(SemanticsAction.scrollRight);
230 231
  }

232
  /// Adds the [SemanticsAction.scrollUp] and [SemanticsAction.scrollDown] actions.
233
  void addVerticalScrollingActions() {
234 235
    addAction(SemanticsAction.scrollUp);
    addAction(SemanticsAction.scrollDown);
236 237
  }

238
  /// Adds the [SemanticsAction.increase] and [SemanticsAction.decrease] actions.
239
  void addAdjustmentActions() {
240 241
    addAction(SemanticsAction.increase);
    addAction(SemanticsAction.decrease);
242 243
  }

244
  bool _canPerformAction(SemanticsAction action) {
245
    return _actionHandler != null && (_actions & action.index) != 0;
246 247
  }

248 249 250 251
  /// Whether all this node and all of its descendants should be treated as one logical entity.
  bool get mergeAllDescendantsIntoThisNode => _mergeAllDescendantsIntoThisNode;
  bool _mergeAllDescendantsIntoThisNode = false;
  set mergeAllDescendantsIntoThisNode(bool value) {
Hixie's avatar
Hixie committed
252
    assert(value != null);
253 254 255 256
    if (_mergeAllDescendantsIntoThisNode == value)
      return;
    _mergeAllDescendantsIntoThisNode = value;
    _markDirty();
Hixie's avatar
Hixie committed
257 258
  }

259 260 261 262 263 264 265 266 267
  bool get _inheritedMergeAllDescendantsIntoThisNode => _inheritedMergeAllDescendantsIntoThisNodeValue;
  bool _inheritedMergeAllDescendantsIntoThisNodeValue = false;
  set _inheritedMergeAllDescendantsIntoThisNode(bool value) {
    assert(value != null);
    if (_inheritedMergeAllDescendantsIntoThisNodeValue == value)
      return;
    _inheritedMergeAllDescendantsIntoThisNodeValue = value;
    _markDirty();
  }
268 269 270

  bool get _shouldMergeAllDescendantsIntoThisNode => mergeAllDescendantsIntoThisNode || _inheritedMergeAllDescendantsIntoThisNode;

271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286
  int _flags = 0;
  void _setFlag(SemanticsFlags flag, bool value) {
    final int index = flag.index;
    if (value) {
      if ((_flags & index) == 0) {
        _flags |= index;
        _markDirty();
      }
    } else {
      if ((_flags & index) != 0) {
        _flags &= ~index;
        _markDirty();
      }
    }
  }

287
  /// Whether this node has Boolean state that can be controlled by the user.
288 289
  bool get hasCheckedState => (_flags & SemanticsFlags.hasCheckedState.index) != 0;
  set hasCheckedState(bool value) => _setFlag(SemanticsFlags.hasCheckedState, value);
Hixie's avatar
Hixie committed
290

291
  /// If this node has Boolean state that can be controlled by the user, whether that state is on or off, corresponding to `true` and `false`, respectively.
292 293
  bool get isChecked => (_flags & SemanticsFlags.isChecked.index) != 0;
  set isChecked(bool value) => _setFlag(SemanticsFlags.isChecked, value);
Hixie's avatar
Hixie committed
294

295
  /// A textual description of this node.
Hixie's avatar
Hixie committed
296 297
  String get label => _label;
  String _label = '';
298
  set label(String value) {
Hixie's avatar
Hixie committed
299 300 301 302 303 304 305
    assert(value != null);
    if (_label != value) {
      _label = value;
      _markDirty();
    }
  }

306
  /// Restore this node to its default state.
Hixie's avatar
Hixie committed
307
  void reset() {
308
    bool hadInheritedMergeAllDescendantsIntoThisNode = _inheritedMergeAllDescendantsIntoThisNode;
309 310
    _actions = 0;
    _flags = 0;
311
    if (hadInheritedMergeAllDescendantsIntoThisNode)
312
      _inheritedMergeAllDescendantsIntoThisNodeValue = true;
Hixie's avatar
Hixie committed
313 314 315 316 317
    _label = '';
    _markDirty();
  }

  List<SemanticsNode> _newChildren;
318 319

  /// Append the given children as children of this node.
Hixie's avatar
Hixie committed
320 321 322 323
  void addChildren(Iterable<SemanticsNode> children) {
    _newChildren ??= <SemanticsNode>[];
    _newChildren.addAll(children);
    // we do the asserts afterwards because children is an Iterable
324
    // and doing the asserts before would mean the behavior is
Hixie's avatar
Hixie committed
325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344
    // 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;
345 346

  /// Whether this node has a non-zero number of children.
Hixie's avatar
Hixie committed
347 348
  bool get hasChildren => _children?.isNotEmpty ?? false;
  bool _dead = false;
349

350 351 352
  /// The number of children this node has.
  int get childrenCount => hasChildren ? _children.length : 0;

353 354 355 356 357 358 359 360 361 362 363 364 365 366
  /// Visits the immediate children of this node.
  ///
  /// This function calls visitor for each child in a pre-order travseral
  /// until visitor returns false. Returns true if all the visitor calls
  /// returned true, otherwise returns false.
  void visitChildren(SemanticsNodeVisitor visitor) {
    if (_children != null) {
      for (SemanticsNode child in _children) {
        if (!visitor(child))
          return;
      }
    }
  }

367 368 369 370 371
  /// 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
372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398
  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
399
            // that our child was, in the last pass, a child of one of our
Hixie's avatar
Hixie committed
400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418
            // 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();
  }

419 420 421
  @override
  SemanticsOwner get owner => super.owner;

422
  @override
Hixie's avatar
Hixie committed
423
  SemanticsNode get parent => super.parent;
424 425

  @override
Hixie's avatar
Hixie committed
426 427 428 429 430 431 432
  void redepthChildren() {
    if (_children != null) {
      for (SemanticsNode child in _children)
        redepthChild(child);
    }
  }

433 434 435 436 437
  /// Visit all the descendants of this node.
  ///
  /// This function calls visitor for each descendant in a pre-order travseral
  /// until visitor returns false. Returns true if all the visitor calls
  /// returned true, otherwise returns false.
Hixie's avatar
Hixie committed
438 439 440 441 442 443 444 445 446 447
  bool _visitDescendants(SemanticsNodeVisitor visitor) {
    if (_children != null) {
      for (SemanticsNode child in _children) {
        if (!visitor(child) || !child._visitDescendants(visitor))
          return false;
      }
    }
    return true;
  }

448
  @override
449
  void attach(SemanticsOwner owner) {
450
    super.attach(owner);
451 452
    assert(!owner._nodes.containsKey(id));
    owner._nodes[id] = this;
453 454 455 456 457
    owner._detachedNodes.remove(this);
    if (_dirty) {
      _dirty = false;
      _markDirty();
    }
458 459
    if (parent != null)
      _inheritedMergeAllDescendantsIntoThisNode = parent._shouldMergeAllDescendantsIntoThisNode;
Hixie's avatar
Hixie committed
460 461
    if (_children != null) {
      for (SemanticsNode child in _children)
462
        child.attach(owner);
Hixie's avatar
Hixie committed
463 464
    }
  }
465 466

  @override
Hixie's avatar
Hixie committed
467
  void detach() {
468
    assert(owner._nodes.containsKey(id));
469
    assert(!owner._detachedNodes.contains(this));
470
    owner._nodes.remove(id);
471
    owner._detachedNodes.add(this);
Hixie's avatar
Hixie committed
472 473 474 475 476 477 478 479 480 481 482 483
    super.detach();
    if (_children != null) {
      for (SemanticsNode child in _children)
        child.detach();
    }
  }

  bool _dirty = false;
  void _markDirty() {
    if (_dirty)
      return;
    _dirty = true;
484 485 486 487
    if (attached) {
      assert(!owner._detachedNodes.contains(this));
      owner._dirtyNodes.add(this);
    }
Hixie's avatar
Hixie committed
488 489
  }

490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522
  /// Returns a summary of the semantics for this node.
  ///
  /// If this node has [mergeAllDescendantsIntoThisNode], then the returned data
  /// includes the information from this node's descendants. Otherwise, the
  /// returned data matches the data on this node.
  SemanticsData getSemanticsData() {
    int flags = _flags;
    int actions = _actions;
    String label = _label;

    if (mergeAllDescendantsIntoThisNode) {
      _visitDescendants((SemanticsNode node) {
        flags |= node._flags;
        actions |= node._actions;
        if (node.label.isNotEmpty) {
          if (label.isEmpty)
            label = node.label;
          else
            label = '$label\n${node.label}';
        }
        return true;
      });
    }

    return new SemanticsData(
      flags: flags,
      actions: actions,
      label: label,
      rect: rect,
      transform: transform
    );
  }

523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540
  static Float64List _initIdentityTransform() {
    return new Matrix4.identity().storage;
  }

  static final Int32List _kEmptyChildList = new Int32List(0);
  static final Float64List _kIdentityTransform = _initIdentityTransform();

  void _addToUpdate(ui.SemanticsUpdateBuilder builder) {
    assert(_dirty);
    final SemanticsData data = getSemanticsData();
    Int32List children;
    if (!hasChildren || mergeAllDescendantsIntoThisNode) {
      children = _kEmptyChildList;
    } else {
      final int childCount = _children.length;
      children = new Int32List(childCount);
      for (int i = 0; i < childCount; ++i)
        children[i] = _children[i].id;
Hixie's avatar
Hixie committed
541
    }
542 543 544 545 546
    builder.updateNode(
      id: id,
      flags: data.flags,
      actions: data.actions,
      rect: data.rect,
547
      label: data.label,
548
      transform: data.transform?.storage ?? _kIdentityTransform,
549
      children: children,
550 551
    );
    _dirty = false;
Hixie's avatar
Hixie committed
552 553
  }

554 555 556
  @override
  String toString() {
    StringBuffer buffer = new StringBuffer();
557
    buffer.write('$runtimeType($id');
558 559 560 561 562 563 564
    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)');
565 566 567
    for (SemanticsAction action in SemanticsAction.values.values) {
      if ((_actions & action.index) != 0)
        buffer.write('; $action');
568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594
    }
    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;
  }
}

595 596 597 598 599
/// 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.
600
class SemanticsOwner extends ChangeNotifier {
601
  final Set<SemanticsNode> _dirtyNodes = new Set<SemanticsNode>();
602 603 604
  final Map<int, SemanticsNode> _nodes = <int, SemanticsNode>{};
  final Set<SemanticsNode> _detachedNodes = new Set<SemanticsNode>();

605 606 607 608 609
  /// The root node of the semantics tree, if any.
  ///
  /// If the semantics tree is empty, returns null.
  SemanticsNode get rootSemanticsNode => _nodes[0];

610
  @override
611 612 613 614
  void dispose() {
    _dirtyNodes.clear();
    _nodes.clear();
    _detachedNodes.clear();
615
    super.dispose();
616
  }
617

618 619
  /// Update the semantics using [ui.window.updateSemantics].
  void sendSemanticsUpdate() {
Hixie's avatar
Hixie committed
620 621 622 623 624 625 626 627 628
    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;
629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660
    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
            }
661 662
          }
        }
663
      }
664
    }
665
    visitedNodes.sort((SemanticsNode a, SemanticsNode b) => a.depth - b.depth);
666
    ui.SemanticsUpdateBuilder builder = new ui.SemanticsUpdateBuilder();
667
    for (SemanticsNode node in visitedNodes) {
Hixie's avatar
Hixie committed
668 669 670 671 672 673 674 675 676 677 678 679
      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)
680
        node._addToUpdate(builder);
Hixie's avatar
Hixie committed
681 682
    }
    _dirtyNodes.clear();
683 684
    ui.window.updateSemantics(builder.build());
    notifyListeners();
Hixie's avatar
Hixie committed
685 686
  }

687
  SemanticsActionHandler _getSemanticsActionHandlerForId(int id, SemanticsAction action) {
Hixie's avatar
Hixie committed
688
    SemanticsNode result = _nodes[id];
689
    if (result != null && result._shouldMergeAllDescendantsIntoThisNode && !result._canPerformAction(action)) {
Hixie's avatar
Hixie committed
690
      result._visitDescendants((SemanticsNode node) {
691
        if (node._canPerformAction(action)) {
Hixie's avatar
Hixie committed
692 693 694 695 696 697
          result = node;
          return false; // found node, abort walk
        }
        return true; // continue walk
      });
    }
698
    if (result == null || !result._canPerformAction(action))
Hixie's avatar
Hixie committed
699 700 701 702
      return null;
    return result._actionHandler;
  }

703 704 705 706
  /// 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.
707
  void performAction(int id, SemanticsAction action) {
708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752
    assert(action != null);
    SemanticsActionHandler handler = _getSemanticsActionHandlerForId(id, action);
    handler?.performAction(action);
  }

  SemanticsActionHandler _getSemanticsActionHandlerForPosition(SemanticsNode node, Point position, SemanticsAction action) {
    if (node.transform != null) {
      Matrix4 inverse = new Matrix4.identity();
      if (inverse.copyInverse(node.transform) == 0.0)
        return null;
      position = MatrixUtils.transformPoint(inverse, position);
    }
    if (!node.rect.contains(position))
      return null;
    if (node.mergeAllDescendantsIntoThisNode) {
      SemanticsNode result;
      node._visitDescendants((SemanticsNode child) {
        if (child._canPerformAction(action)) {
          result = child;
          return false;
        }
        return true;
      });
      return result?._actionHandler;
    }
    if (node.hasChildren) {
      for (SemanticsNode child in node._children.reversed) {
        SemanticsActionHandler handler = _getSemanticsActionHandlerForPosition(child, position, action);
        if (handler != null)
          return handler;
      }
    }
    return node._canPerformAction(action) ? node._actionHandler : null;
  }

  /// Asks the [SemanticsNode] with at the given position to perform the given action.
  ///
  /// If the [SemanticsNode] has not indicated that it can perform the action,
  /// this function does nothing.
  void performActionAt(Point position, SemanticsAction action) {
    assert(action != null);
    final SemanticsNode node = rootSemanticsNode;
    if (node == null)
      return;
    SemanticsActionHandler handler = _getSemanticsActionHandlerForPosition(node, position, action);
753
    handler?.performAction(action);
Hixie's avatar
Hixie committed
754 755
  }
}