
The new mojom.dart code makes mocking services a bit tricky. I've filed https://github.com/domokit/mojo/issues/786 about improving that.
588 lines
23 KiB
Dart
588 lines
23 KiB
Dart
// 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;
|
|
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter/painting.dart';
|
|
import 'package:sky_services/semantics/semantics.mojom.dart' as mojom;
|
|
import 'package:vector_math/vector_math_64.dart';
|
|
|
|
import 'node.dart';
|
|
|
|
/// 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);
|
|
|
|
/// Interface for [RenderObject]s to implement when they want to support
|
|
/// being tapped, etc.
|
|
///
|
|
/// These handlers will only be called if the relevant flag is set
|
|
/// (e.g. [handleSemanticTap]() will only be called if
|
|
/// [SemanticsNode.canBeTapped] is true, [handleSemanticScrollDown]() will only
|
|
/// be called if [SemanticsNode.canBeScrolledVertically] is true, etc).
|
|
abstract class SemanticActionHandler {
|
|
/// Called when the user taps on the render object.
|
|
void handleSemanticTap() { }
|
|
|
|
/// Called when the user presses on the render object for a long period of time.
|
|
void handleSemanticLongPress() { }
|
|
|
|
/// Called when the user scrolls to the left.
|
|
void handleSemanticScrollLeft() { }
|
|
|
|
/// Called when the user scrolls to the right.
|
|
void handleSemanticScrollRight() { }
|
|
|
|
/// Called when the user scrolls up.
|
|
void handleSemanticScrollUp() { }
|
|
|
|
/// Called when the user scrolls down.
|
|
void handleSemanticScrollDown() { }
|
|
}
|
|
|
|
enum _SemanticFlags {
|
|
mergeAllDescendantsIntoThisNode,
|
|
inheritedMergeAllDescendantsIntoThisNode, // whether an ancestor had mergeAllDescendantsIntoThisNode set
|
|
canBeTapped,
|
|
canBeLongPressed,
|
|
canBeScrolledHorizontally,
|
|
canBeScrolledVertically,
|
|
hasCheckedState,
|
|
isChecked,
|
|
}
|
|
|
|
/// Signature for a function that is called for each [SemanticsNode].
|
|
///
|
|
/// Return false to stop visiting nodes.
|
|
typedef bool SemanticsNodeVisitor(SemanticsNode node);
|
|
|
|
/// 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.
|
|
class SemanticsNode extends AbstractNode {
|
|
/// Creates a semantic node.
|
|
///
|
|
/// Each semantic node has a unique identifier that is assigned when the node
|
|
/// is created.
|
|
SemanticsNode({
|
|
SemanticActionHandler handler
|
|
}) : _id = _generateNewId(),
|
|
_actionHandler = handler;
|
|
|
|
/// Creates a semantic node to represent the root of the semantics tree.
|
|
///
|
|
/// The root node is assigned an identifier of zero.
|
|
SemanticsNode.root({
|
|
SemanticActionHandler handler,
|
|
Object owner
|
|
}) : _id = 0,
|
|
_actionHandler = handler {
|
|
attach(owner);
|
|
}
|
|
|
|
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
|
|
|
|
/// 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).
|
|
Matrix4 get transform => _transform;
|
|
Matrix4 _transform;
|
|
set transform (Matrix4 value) {
|
|
if (!MatrixUtils.matrixEquals(_transform, value)) {
|
|
_transform = value;
|
|
_markDirty();
|
|
}
|
|
}
|
|
|
|
/// The bounding box for this node in its coordinate system.
|
|
Rect get rect => _rect;
|
|
Rect _rect = Rect.zero;
|
|
set rect (Rect value) {
|
|
assert(value != null);
|
|
if (_rect != value) {
|
|
_rect = value;
|
|
_markDirty();
|
|
}
|
|
}
|
|
|
|
/// Whether [rect] might have been influenced by clips applied by ancestors.
|
|
bool wasAffectedByClip = false;
|
|
|
|
|
|
// FLAGS AND LABELS
|
|
// These are supposed to be set by SemanticAnnotator obtained from getSemanticAnnotators
|
|
|
|
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();
|
|
}
|
|
}
|
|
|
|
bool _canHandle(_SemanticFlags flag) {
|
|
return _actionHandler != null && _flags[flag];
|
|
}
|
|
|
|
/// Whether all this node and all of its descendants should be treated as one logical entity.
|
|
bool get mergeAllDescendantsIntoThisNode => _flags[_SemanticFlags.mergeAllDescendantsIntoThisNode];
|
|
set mergeAllDescendantsIntoThisNode(bool value) => _setFlag(_SemanticFlags.mergeAllDescendantsIntoThisNode, value);
|
|
|
|
bool get _inheritedMergeAllDescendantsIntoThisNode => _flags[_SemanticFlags.inheritedMergeAllDescendantsIntoThisNode];
|
|
set _inheritedMergeAllDescendantsIntoThisNode(bool value) => _setFlag(_SemanticFlags.inheritedMergeAllDescendantsIntoThisNode, value);
|
|
|
|
bool get _shouldMergeAllDescendantsIntoThisNode => mergeAllDescendantsIntoThisNode || _inheritedMergeAllDescendantsIntoThisNode;
|
|
|
|
/// Whether this node responds to tap gestures.
|
|
bool get canBeTapped => _flags[_SemanticFlags.canBeTapped];
|
|
set canBeTapped(bool value) => _setFlag(_SemanticFlags.canBeTapped, value, needsHandler: true);
|
|
|
|
/// Whether this node responds to long-press gestures.
|
|
bool get canBeLongPressed => _flags[_SemanticFlags.canBeLongPressed];
|
|
set canBeLongPressed(bool value) => _setFlag(_SemanticFlags.canBeLongPressed, value, needsHandler: true);
|
|
|
|
/// Whether this node responds to horizontal scrolling.
|
|
bool get canBeScrolledHorizontally => _flags[_SemanticFlags.canBeScrolledHorizontally];
|
|
set canBeScrolledHorizontally(bool value) => _setFlag(_SemanticFlags.canBeScrolledHorizontally, value, needsHandler: true);
|
|
|
|
/// Whether this node responds to vertical scrolling.
|
|
bool get canBeScrolledVertically => _flags[_SemanticFlags.canBeScrolledVertically];
|
|
set canBeScrolledVertically(bool value) => _setFlag(_SemanticFlags.canBeScrolledVertically, value, needsHandler: true);
|
|
|
|
/// Whether this node has Boolean state that can be controlled by the user.
|
|
bool get hasCheckedState => _flags[_SemanticFlags.hasCheckedState];
|
|
set hasCheckedState(bool value) => _setFlag(_SemanticFlags.hasCheckedState, value);
|
|
|
|
/// 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.
|
|
bool get isChecked => _flags[_SemanticFlags.isChecked];
|
|
set isChecked(bool value) => _setFlag(_SemanticFlags.isChecked, value);
|
|
|
|
/// A textual description of this node.
|
|
String get label => _label;
|
|
String _label = '';
|
|
set label(String value) {
|
|
assert(value != null);
|
|
if (_label != value) {
|
|
_label = value;
|
|
_markDirty();
|
|
}
|
|
}
|
|
|
|
/// Restore this node to its default state.
|
|
void reset() {
|
|
bool hadInheritedMergeAllDescendantsIntoThisNode = _inheritedMergeAllDescendantsIntoThisNode;
|
|
_flags.reset();
|
|
if (hadInheritedMergeAllDescendantsIntoThisNode)
|
|
_inheritedMergeAllDescendantsIntoThisNode = true;
|
|
_label = '';
|
|
_markDirty();
|
|
}
|
|
|
|
List<SemanticsNode> _newChildren;
|
|
|
|
/// Append the given children as children of this node.
|
|
void addChildren(Iterable<SemanticsNode> children) {
|
|
_newChildren ??= <SemanticsNode>[];
|
|
_newChildren.addAll(children);
|
|
// we do the asserts afterwards because children is an Iterable
|
|
// and doing the asserts before would mean the behavior is
|
|
// 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;
|
|
|
|
/// Whether this node has a non-zero number of children.
|
|
bool get hasChildren => _children?.isNotEmpty ?? false;
|
|
bool _dead = false;
|
|
|
|
/// 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.
|
|
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
|
|
// that our child was, in the last pass, a child of one of our
|
|
// 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();
|
|
}
|
|
|
|
@override
|
|
SemanticsNode get parent => super.parent;
|
|
|
|
@override
|
|
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;
|
|
}
|
|
|
|
static Map<int, SemanticsNode> _nodes = <int, SemanticsNode>{};
|
|
static Set<SemanticsNode> _detachedNodes = new Set<SemanticsNode>();
|
|
|
|
@override
|
|
void attach(Object owner) {
|
|
super.attach(owner);
|
|
assert(!_nodes.containsKey(_id));
|
|
_nodes[_id] = this;
|
|
_detachedNodes.remove(this);
|
|
if (parent != null)
|
|
_inheritedMergeAllDescendantsIntoThisNode = parent._shouldMergeAllDescendantsIntoThisNode;
|
|
if (_children != null) {
|
|
for (SemanticsNode child in _children)
|
|
child.attach(owner);
|
|
}
|
|
}
|
|
|
|
@override
|
|
void detach() {
|
|
super.detach();
|
|
assert(_nodes.containsKey(_id));
|
|
assert(!_detachedNodes.contains(this));
|
|
_nodes.remove(_id);
|
|
_detachedNodes.add(this);
|
|
if (_children != null) {
|
|
for (SemanticsNode child in _children)
|
|
child.detach();
|
|
}
|
|
}
|
|
|
|
static List<SemanticsNode> _dirtyNodes = <SemanticsNode>[];
|
|
bool _dirty = false;
|
|
void _markDirty() {
|
|
if (_dirty)
|
|
return;
|
|
_dirty = true;
|
|
assert(!_dirtyNodes.contains(this));
|
|
assert(!_detachedNodes.contains(this));
|
|
_dirtyNodes.add(this);
|
|
}
|
|
|
|
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.canBeTapped = canBeTapped;
|
|
result.flags.canBeLongPressed = canBeLongPressed;
|
|
result.flags.canBeScrolledHorizontally = canBeScrolledHorizontally;
|
|
result.flags.canBeScrolledVertically = canBeScrolledVertically;
|
|
result.flags.hasCheckedState = hasCheckedState;
|
|
result.flags.isChecked = isChecked;
|
|
result.strings = new mojom.SemanticStrings();
|
|
result.strings.label = label;
|
|
List<mojom.SemanticsNode> children = <mojom.SemanticsNode>[];
|
|
if (_shouldMergeAllDescendantsIntoThisNode) {
|
|
_visitDescendants((SemanticsNode node) {
|
|
result.flags.canBeTapped = result.flags.canBeTapped || node.canBeTapped;
|
|
result.flags.canBeLongPressed = result.flags.canBeLongPressed || node.canBeLongPressed;
|
|
result.flags.canBeScrolledHorizontally = result.flags.canBeScrolledHorizontally || node.canBeScrolledHorizontally;
|
|
result.flags.canBeScrolledVertically = result.flags.canBeScrolledVertically || node.canBeScrolledVertically;
|
|
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;
|
|
node._dirty = false;
|
|
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;
|
|
_dirty = false;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
static List<mojom.SemanticsListener> _listeners;
|
|
|
|
/// Whether there are currently any consumers of semantic data.
|
|
///
|
|
/// If there are no consumers of semantic data, there is no need to compile
|
|
/// semantic data into a [SemanticsNode] tree.
|
|
static bool get hasListeners => _listeners != null && _listeners.length > 0;
|
|
|
|
/// Called when the first consumer of semantic data arrives.
|
|
///
|
|
/// Typically set by [RendererBinding].
|
|
static VoidCallback onSemanticsEnabled;
|
|
|
|
/// 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.
|
|
static void addListener(mojom.SemanticsListener listener) {
|
|
if (!hasListeners) {
|
|
assert(onSemanticsEnabled != null); // initialise the binding _before_ adding listeners
|
|
onSemanticsEnabled();
|
|
}
|
|
_listeners ??= <mojom.SemanticsListener>[];
|
|
_listeners.add(listener);
|
|
}
|
|
|
|
/// Uploads the semantics tree to the listeners registered with [addListener].
|
|
static void sendSemanticsTree() {
|
|
assert(hasListeners);
|
|
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;
|
|
_dirtyNodes.sort((SemanticsNode a, SemanticsNode b) => a.depth - b.depth);
|
|
for (int index = 0; index < _dirtyNodes.length; index += 1) {
|
|
// we mutate the list as we walk it here, which is why we use an index instead of an iterator
|
|
SemanticsNode node = _dirtyNodes[index];
|
|
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
|
|
}
|
|
}
|
|
}
|
|
assert(_dirtyNodes[index] == node); // make sure nothing went in front of us in the list
|
|
}
|
|
_dirtyNodes.sort((SemanticsNode a, SemanticsNode b) => a.depth - b.depth);
|
|
List<mojom.SemanticsNode> updatedNodes = <mojom.SemanticsNode>[];
|
|
for (SemanticsNode node in _dirtyNodes) {
|
|
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());
|
|
}
|
|
for (mojom.SemanticsListener listener in _listeners)
|
|
listener.updateSemanticsTree(updatedNodes);
|
|
_dirtyNodes.clear();
|
|
}
|
|
|
|
static SemanticActionHandler _getSemanticActionHandlerForId(int id, { _SemanticFlags neededFlag }) {
|
|
assert(neededFlag != null);
|
|
SemanticsNode result = _nodes[id];
|
|
if (result != null && result._shouldMergeAllDescendantsIntoThisNode && !result._canHandle(neededFlag)) {
|
|
result._visitDescendants((SemanticsNode node) {
|
|
if (node._actionHandler != null && node._flags[neededFlag]) {
|
|
result = node;
|
|
return false; // found node, abort walk
|
|
}
|
|
return true; // continue walk
|
|
});
|
|
}
|
|
if (result == null || !result._canHandle(neededFlag))
|
|
return null;
|
|
return result._actionHandler;
|
|
}
|
|
|
|
@override
|
|
String toString() {
|
|
return '$runtimeType($_id'
|
|
'${_dirty ? " (${ _dirtyNodes.contains(this) ? 'dirty' : 'STALE' })" : ""}'
|
|
'${_shouldMergeAllDescendantsIntoThisNode ? " (leaf merge)" : ""}'
|
|
'; $rect'
|
|
'${wasAffectedByClip ? " (clipped)" : ""}'
|
|
'${canBeTapped ? "; canBeTapped" : ""}'
|
|
'${canBeLongPressed ? "; canBeLongPressed" : ""}'
|
|
'${canBeScrolledHorizontally ? "; canBeScrolledHorizontally" : ""}'
|
|
'${canBeScrolledVertically ? "; canBeScrolledVertically" : ""}'
|
|
'${hasCheckedState ? (isChecked ? "; checked" : "; unchecked") : ""}'
|
|
'${label != "" ? "; \"$label\"" : ""}'
|
|
')';
|
|
}
|
|
|
|
/// 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;
|
|
}
|
|
}
|
|
|
|
/// Exposes the [SemanticsNode] tree to the underlying platform.
|
|
class SemanticsServer extends mojom.SemanticsServer {
|
|
@override
|
|
void addSemanticsListener(mojom.SemanticsListenerProxy listener) {
|
|
// TODO(abarth): We should remove the listener when this pipe closes.
|
|
// See <https://github.com/flutter/flutter/issues/3342>.
|
|
SemanticsNode.addListener(listener);
|
|
}
|
|
|
|
@override
|
|
void tap(int nodeID) {
|
|
SemanticsNode._getSemanticActionHandlerForId(nodeID, neededFlag: _SemanticFlags.canBeTapped)?.handleSemanticTap();
|
|
}
|
|
|
|
@override
|
|
void longPress(int nodeID) {
|
|
SemanticsNode._getSemanticActionHandlerForId(nodeID, neededFlag: _SemanticFlags.canBeLongPressed)?.handleSemanticLongPress();
|
|
}
|
|
|
|
@override
|
|
void scrollLeft(int nodeID) {
|
|
SemanticsNode._getSemanticActionHandlerForId(nodeID, neededFlag: _SemanticFlags.canBeScrolledHorizontally)?.handleSemanticScrollLeft();
|
|
}
|
|
|
|
@override
|
|
void scrollRight(int nodeID) {
|
|
SemanticsNode._getSemanticActionHandlerForId(nodeID, neededFlag: _SemanticFlags.canBeScrolledHorizontally)?.handleSemanticScrollRight();
|
|
}
|
|
|
|
@override
|
|
void scrollUp(int nodeID) {
|
|
SemanticsNode._getSemanticActionHandlerForId(nodeID, neededFlag: _SemanticFlags.canBeScrolledVertically)?.handleSemanticScrollUp();
|
|
}
|
|
|
|
@override
|
|
void scrollDown(int nodeID) {
|
|
SemanticsNode._getSemanticActionHandlerForId(nodeID, neededFlag: _SemanticFlags.canBeScrolledVertically)?.handleSemanticScrollDown();
|
|
}
|
|
}
|