From 5ed8f1a1fdf50192dab6be49904c8c56389e84d1 Mon Sep 17 00:00:00 2001 From: Adam Barth Date: Fri, 1 Jul 2016 13:38:24 -0700 Subject: [PATCH] Add semantics for Sliders (#4808) Also, make SemanticsOwner into a real class and use it instead of a static in several places. --- packages/flutter/lib/src/material/slider.dart | 42 ++++- .../flutter/lib/src/rendering/binding.dart | 15 +- .../flutter/lib/src/rendering/object.dart | 94 +++++----- .../flutter/lib/src/rendering/semantics.dart | 166 ++++++++++-------- .../lib/src/widgets/semantics_debugger.dart | 56 +++--- .../flutter/test/material/tooltip_test.dart | 2 +- .../flutter/test/rendering/reattach_test.dart | 7 +- .../test/rendering/rendering_tester.dart | 18 +- .../flutter/test/widget/buttons_test.dart | 2 +- .../independent_widget_layout_test.dart | 5 +- .../flutter/test/widget/semantics_1_test.dart | 2 +- .../flutter/test/widget/semantics_2_test.dart | 2 +- .../flutter/test/widget/semantics_3_test.dart | 2 +- .../flutter/test/widget/semantics_4_test.dart | 2 +- .../flutter/test/widget/semantics_5_test.dart | 2 +- .../flutter/test/widget/semantics_7_test.dart | 2 +- .../flutter/test/widget/semantics_8_test.dart | 2 +- .../flutter/test/widget/test_semantics.dart | 7 +- packages/flutter_test/lib/src/binding.dart | 9 +- 19 files changed, 254 insertions(+), 183 deletions(-) diff --git a/packages/flutter/lib/src/material/slider.dart b/packages/flutter/lib/src/material/slider.dart index ba72f23424b..4e4119aa4e4 100644 --- a/packages/flutter/lib/src/material/slider.dart +++ b/packages/flutter/lib/src/material/slider.dart @@ -180,6 +180,8 @@ final Tween _kLabelBalloonRadiusTween = new Tween(begin: _kThumb final Tween _kLabelBalloonTipTween = new Tween(begin: 0.0, end: -8.0); final double _kLabelBalloonTipAttachmentRatio = math.sin(math.PI / 4.0); +const double _kAdjustmentUnit = 0.1; // Matches iOS implementation of material slider. + double _getAdditionalHeightForLabel(String label) { return label == null ? 0.0 : _kLabelBalloonRadius * 2.0; } @@ -191,7 +193,7 @@ BoxConstraints _getAdditionalConstraints(String label) { ); } -class _RenderSlider extends RenderConstrainedBox { +class _RenderSlider extends RenderConstrainedBox implements SemanticActionHandler { _RenderSlider({ double value, int divisions, @@ -291,8 +293,10 @@ class _RenderSlider extends RenderConstrainedBox { return dragValue; } + bool get isInteractive => onChanged != null; + void _handleDragStart(DragStartDetails details) { - if (onChanged != null) { + if (isInteractive) { _active = true; _currentDragValue = (globalToLocal(details.globalPosition).x - _kReactionRadius) / _trackLength; onChanged(_discretizedCurrentDragValue); @@ -302,7 +306,7 @@ class _RenderSlider extends RenderConstrainedBox { } void _handleDragUpdate(DragUpdateDetails details) { - if (onChanged != null) { + if (isInteractive) { _currentDragValue += details.primaryDelta / _trackLength; onChanged(_discretizedCurrentDragValue); } @@ -322,7 +326,7 @@ class _RenderSlider extends RenderConstrainedBox { @override void handleEvent(PointerEvent event, BoxHitTestEntry entry) { - if (event is PointerDownEvent && onChanged != null) + if (event is PointerDownEvent && isInteractive) _drag.addPointer(event); } @@ -331,7 +335,7 @@ class _RenderSlider extends RenderConstrainedBox { final Canvas canvas = context.canvas; final double trackLength = _trackLength; - final bool enabled = onChanged != null; + final bool enabled = isInteractive; final double value = _position.value; final double additionalHeightForLabel = _getAdditionalHeightForLabel(label); @@ -417,4 +421,32 @@ class _RenderSlider extends RenderConstrainedBox { } canvas.drawCircle(thumbCenter, thumbRadius + thumbRadiusDelta, thumbPaint); } + + @override + bool get hasSemantics => isInteractive; + + @override + Iterable getSemanticAnnotators() sync* { + yield (SemanticsNode semantics) { + if (isInteractive) + semantics.addAdjustmentActions(); + }; + } + + @override + void performAction(SemanticAction action) { + switch (action) { + case SemanticAction.increase: + if (isInteractive) + onChanged((value + _kAdjustmentUnit).clamp(0.0, 1.0)); + break; + case SemanticAction.decrease: + if (isInteractive) + onChanged((value - _kAdjustmentUnit).clamp(0.0, 1.0)); + break; + default: + assert(false); + break; + } + } } diff --git a/packages/flutter/lib/src/rendering/binding.dart b/packages/flutter/lib/src/rendering/binding.dart index 14416535ab8..b3e00b9a87e 100644 --- a/packages/flutter/lib/src/rendering/binding.dart +++ b/packages/flutter/lib/src/rendering/binding.dart @@ -133,13 +133,19 @@ abstract class RendererBinding extends BindingBase implements SchedulerBinding, /// /// Called automatically when the binding is created. void initSemantics() { - SemanticsNode.onSemanticsEnabled = renderView.scheduleInitialSemantics; shell.provideService(mojom.SemanticsServer.serviceName, (core.MojoMessagePipeEndpoint endpoint) { + ensureSemantics(); mojom.SemanticsServerStub server = new mojom.SemanticsServerStub.fromEndpoint(endpoint); - server.impl = new SemanticsServer(); + server.impl = new SemanticsServer(semanticsOwner: pipelineOwner.semanticsOwner); }); } + void ensureSemantics() { + if (pipelineOwner.semanticsOwner == null) + renderView.scheduleInitialSemantics(); + assert(pipelineOwner.semanticsOwner != null); + } + void _handlePersistentFrameCallback(Duration timeStamp) { beginFrame(); } @@ -153,10 +159,7 @@ abstract class RendererBinding extends BindingBase implements SchedulerBinding, pipelineOwner.flushCompositingBits(); pipelineOwner.flushPaint(); renderView.compositeFrame(); // this sends the bits to the GPU - if (SemanticsNode.hasListeners) { - pipelineOwner.flushSemantics(); - SemanticsNode.sendSemanticsTree(); - } + pipelineOwner.flushSemantics(); } @override diff --git a/packages/flutter/lib/src/rendering/object.dart b/packages/flutter/lib/src/rendering/object.dart index e8d7264992d..26c1298de23 100644 --- a/packages/flutter/lib/src/rendering/object.dart +++ b/packages/flutter/lib/src/rendering/object.dart @@ -507,12 +507,12 @@ class _SemanticsGeometry { abstract class _SemanticsFragment { _SemanticsFragment({ - RenderObject owner, + RenderObject renderObjectOwner, Iterable annotators, List<_SemanticsFragment> children }) { - assert(owner != null); - _ancestorChain = [owner]; + assert(renderObjectOwner != null); + _ancestorChain = [renderObjectOwner]; if (annotators != null) addAnnotators(annotators); assert(() { @@ -531,7 +531,7 @@ abstract class _SemanticsFragment { _ancestorChain.add(ancestor); } - RenderObject get owner => _ancestorChain.first; + RenderObject get renderObjectOwner => _ancestorChain.first; List _annotators; void addAnnotators(Iterable moreAnnotators) { @@ -555,20 +555,20 @@ abstract class _SemanticsFragment { /// that comes from the (dirty) ancestors.) class _CleanSemanticsFragment extends _SemanticsFragment { _CleanSemanticsFragment({ - RenderObject owner - }) : super(owner: owner) { - assert(owner._semantics != null); + RenderObject renderObjectOwner + }) : super(renderObjectOwner: renderObjectOwner) { + assert(renderObjectOwner._semantics != null); } @override Iterable compile({ _SemanticsGeometry geometry, SemanticsNode currentSemantics, SemanticsNode parentSemantics }) sync* { assert(!_debugCompiled); assert(() { _debugCompiled = true; return true; }); - SemanticsNode node = owner._semantics; + SemanticsNode node = renderObjectOwner._semantics; assert(node != null); if (geometry != null) { geometry.applyAncestorChain(_ancestorChain); - geometry.updateSemanticsNode(rendering: owner, semantics: node, parentSemantics: parentSemantics); + geometry.updateSemanticsNode(rendering: renderObjectOwner, semantics: node, parentSemantics: parentSemantics); } else { assert(_ancestorChain.length == 1); } @@ -578,10 +578,10 @@ class _CleanSemanticsFragment extends _SemanticsFragment { abstract class _InterestingSemanticsFragment extends _SemanticsFragment { _InterestingSemanticsFragment({ - RenderObject owner, + RenderObject renderObjectOwner, Iterable annotators, Iterable<_SemanticsFragment> children - }) : super(owner: owner, annotators: annotators, children: children); + }) : super(renderObjectOwner: renderObjectOwner, annotators: annotators, children: children); bool get haveConcreteNode => true; @@ -593,7 +593,7 @@ abstract class _InterestingSemanticsFragment extends _SemanticsFragment { for (SemanticAnnotator annotator in _annotators) annotator(node); for (_SemanticsFragment child in _children) { - assert(child._ancestorChain.last == owner); + assert(child._ancestorChain.last == renderObjectOwner); node.addChildren(child.compile( geometry: createSemanticsGeometryForChild(geometry), currentSemantics: _children.length > 1 ? null : node, @@ -612,10 +612,10 @@ abstract class _InterestingSemanticsFragment extends _SemanticsFragment { class _RootSemanticsFragment extends _InterestingSemanticsFragment { _RootSemanticsFragment({ - RenderObject owner, + RenderObject renderObjectOwner, Iterable annotators, Iterable<_SemanticsFragment> children - }) : super(owner: owner, annotators: annotators, children: children); + }) : super(renderObjectOwner: renderObjectOwner, annotators: annotators, children: children); @override SemanticsNode establishSemanticsNode(_SemanticsGeometry geometry, SemanticsNode currentSemantics, SemanticsNode parentSemantics) { @@ -623,14 +623,14 @@ class _RootSemanticsFragment extends _InterestingSemanticsFragment { assert(geometry == null); assert(currentSemantics == null); assert(parentSemantics == null); - owner._semantics ??= new SemanticsNode.root( - handler: owner is SemanticActionHandler ? owner as dynamic : null, - owner: owner.owner + renderObjectOwner._semantics ??= new SemanticsNode.root( + handler: renderObjectOwner is SemanticActionHandler ? renderObjectOwner as dynamic : null, + owner: renderObjectOwner.owner.semanticsOwner ); - SemanticsNode node = owner._semantics; + SemanticsNode node = renderObjectOwner._semantics; assert(MatrixUtils.matrixEquals(node.transform, new Matrix4.identity())); assert(!node.wasAffectedByClip); - node.rect = owner.semanticBounds; + node.rect = renderObjectOwner.semanticBounds; return node; } @@ -642,20 +642,20 @@ class _RootSemanticsFragment extends _InterestingSemanticsFragment { class _ConcreteSemanticsFragment extends _InterestingSemanticsFragment { _ConcreteSemanticsFragment({ - RenderObject owner, + RenderObject renderObjectOwner, Iterable annotators, Iterable<_SemanticsFragment> children - }) : super(owner: owner, annotators: annotators, children: children); + }) : super(renderObjectOwner: renderObjectOwner, annotators: annotators, children: children); @override SemanticsNode establishSemanticsNode(_SemanticsGeometry geometry, SemanticsNode currentSemantics, SemanticsNode parentSemantics) { - owner._semantics ??= new SemanticsNode( - handler: owner is SemanticActionHandler ? owner as dynamic : null + renderObjectOwner._semantics ??= new SemanticsNode( + handler: renderObjectOwner is SemanticActionHandler ? renderObjectOwner as dynamic : null ); - SemanticsNode node = owner._semantics; + SemanticsNode node = renderObjectOwner._semantics; if (geometry != null) { geometry.applyAncestorChain(_ancestorChain); - geometry.updateSemanticsNode(rendering: owner, semantics: node, parentSemantics: parentSemantics); + geometry.updateSemanticsNode(rendering: renderObjectOwner, semantics: node, parentSemantics: parentSemantics); } else { assert(_ancestorChain.length == 1); } @@ -670,10 +670,10 @@ class _ConcreteSemanticsFragment extends _InterestingSemanticsFragment { class _ImplicitSemanticsFragment extends _InterestingSemanticsFragment { _ImplicitSemanticsFragment({ - RenderObject owner, + RenderObject renderObjectOwner, Iterable annotators, Iterable<_SemanticsFragment> children - }) : super(owner: owner, annotators: annotators, children: children); + }) : super(renderObjectOwner: renderObjectOwner, annotators: annotators, children: children); @override bool get haveConcreteNode => _haveConcreteNode; @@ -685,18 +685,18 @@ class _ImplicitSemanticsFragment extends _InterestingSemanticsFragment { assert(_haveConcreteNode == null); _haveConcreteNode = currentSemantics == null && _annotators.isNotEmpty; if (haveConcreteNode) { - owner._semantics ??= new SemanticsNode( - handler: owner is SemanticActionHandler ? owner as dynamic : null + renderObjectOwner._semantics ??= new SemanticsNode( + handler: renderObjectOwner is SemanticActionHandler ? renderObjectOwner as dynamic : null ); - node = owner._semantics; + node = renderObjectOwner._semantics; } else { - owner._semantics = null; + renderObjectOwner._semantics = null; node = currentSemantics; } if (geometry != null) { geometry.applyAncestorChain(_ancestorChain); if (haveConcreteNode) - geometry.updateSemanticsNode(rendering: owner, semantics: node, parentSemantics: parentSemantics); + geometry.updateSemanticsNode(rendering: renderObjectOwner, semantics: node, parentSemantics: parentSemantics); } else { assert(_ancestorChain.length == 1); } @@ -713,9 +713,9 @@ class _ImplicitSemanticsFragment extends _InterestingSemanticsFragment { class _ForkingSemanticsFragment extends _SemanticsFragment { _ForkingSemanticsFragment({ - RenderObject owner, + RenderObject renderObjectOwner, Iterable<_SemanticsFragment> children - }) : super(owner: owner, children: children) { + }) : super(renderObjectOwner: renderObjectOwner, children: children) { assert(children != null); assert(children.length > 1); } @@ -727,7 +727,7 @@ class _ForkingSemanticsFragment extends _SemanticsFragment { assert(geometry != null); geometry.applyAncestorChain(_ancestorChain); for (_SemanticsFragment child in _children) { - assert(child._ancestorChain.last == owner); + assert(child._ancestorChain.last == renderObjectOwner); yield* child.compile( geometry: new _SemanticsGeometry.copy(geometry), currentSemantics: null, @@ -880,7 +880,8 @@ class PipelineOwner { } } - bool _semanticsEnabled = false; + SemanticsOwner get semanticsOwner => _semanticsOwner; + SemanticsOwner _semanticsOwner; bool _debugDoingSemantics = false; List _nodesNeedingSemantics = []; @@ -892,8 +893,10 @@ class PipelineOwner { /// /// See [RendererBinding] for an example of how this function is used. void flushSemantics() { + if (_semanticsOwner == null) + return; Timeline.startSync('Semantics'); - assert(_semanticsEnabled); + assert(_semanticsOwner != null); assert(() { _debugDoingSemantics = true; return true; }); try { _nodesNeedingSemantics.sort((RenderObject a, RenderObject b) => a.depth - b.depth); @@ -906,6 +909,7 @@ class PipelineOwner { assert(() { _debugDoingSemantics = false; return true; }); Timeline.finishSync(); } + _semanticsOwner.sendSemanticsTree(); } /// Cause the entire subtree rooted at the given [RenderObject] to @@ -1858,8 +1862,8 @@ abstract class RenderObject extends AbstractNode implements HitTestTarget { assert(!owner._debugDoingSemantics); assert(_semantics == null); assert(_needsSemanticsUpdate); - assert(owner._semanticsEnabled == false); - owner._semanticsEnabled = true; + assert(owner._semanticsOwner == null); + owner._semanticsOwner = new SemanticsOwner(); owner._nodesNeedingSemantics.add(this); owner.requestVisualUpdate(); } @@ -1917,7 +1921,7 @@ abstract class RenderObject extends AbstractNode implements HitTestTarget { /// tree will be out of date. void markNeedsSemanticsUpdate({ bool onlyChanges: false, bool noGeometry: false }) { assert(!attached || !owner._debugDoingSemantics); - if ((attached && !owner._semanticsEnabled) || (_needsSemanticsUpdate && onlyChanges && (_needsSemanticsGeometryUpdate || noGeometry))) + if ((attached && owner._semanticsOwner == null) || (_needsSemanticsUpdate && onlyChanges && (_needsSemanticsGeometryUpdate || noGeometry))) return; if (!noGeometry && (_semantics == null || (_semantics.hasChildren && _semantics.wasAffectedByClip))) { // Since the geometry might have changed, we need to make sure to reapply any clips. @@ -1981,7 +1985,7 @@ abstract class RenderObject extends AbstractNode implements HitTestTarget { // early-exit if we're not dirty and have our own semantics if (!_needsSemanticsUpdate && hasSemantics) { assert(_semantics != null); - return new _CleanSemanticsFragment(owner: this); + return new _CleanSemanticsFragment(renderObjectOwner: this); } List<_SemanticsFragment> children; visitChildrenForSemantics((RenderObject child) { @@ -2004,16 +2008,16 @@ abstract class RenderObject extends AbstractNode implements HitTestTarget { _needsSemanticsGeometryUpdate = false; Iterable annotators = getSemanticAnnotators(); if (parent is! RenderObject) - return new _RootSemanticsFragment(owner: this, annotators: annotators, children: children); + return new _RootSemanticsFragment(renderObjectOwner: this, annotators: annotators, children: children); if (hasSemantics) - return new _ConcreteSemanticsFragment(owner: this, annotators: annotators, children: children); + return new _ConcreteSemanticsFragment(renderObjectOwner: this, annotators: annotators, children: children); if (annotators.isNotEmpty) - return new _ImplicitSemanticsFragment(owner: this, annotators: annotators, children: children); + return new _ImplicitSemanticsFragment(renderObjectOwner: this, annotators: annotators, children: children); _semantics = null; if (children == null) return null; if (children.length > 1) - return new _ForkingSemanticsFragment(owner: this, children: children); + return new _ForkingSemanticsFragment(renderObjectOwner: this, children: children); assert(children.length == 1); return children.single; } diff --git a/packages/flutter/lib/src/rendering/semantics.dart b/packages/flutter/lib/src/rendering/semantics.dart index 945bd0daacf..75cfde68223 100644 --- a/packages/flutter/lib/src/rendering/semantics.dart +++ b/packages/flutter/lib/src/rendering/semantics.dart @@ -78,7 +78,7 @@ class SemanticsNode extends AbstractNode { /// The root node is assigned an identifier of zero. SemanticsNode.root({ SemanticActionHandler handler, - Object owner + SemanticsOwner owner }) : _id = 0, _actionHandler = handler { attach(owner); @@ -131,21 +131,34 @@ class SemanticsNode extends AbstractNode { final Set _actions = new Set(); + /// 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. void addAction(SemanticAction action) { if (_actions.add(action)) _markDirty(); } + /// Adds the [SemanticAction.scrollLeft] and [SemanticAction.scrollRight] actions. void addHorizontalScrollingActions() { addAction(SemanticAction.scrollLeft); addAction(SemanticAction.scrollRight); } + /// Adds the [SemanticAction.scrollUp] and [SemanticAction.scrollDown] actions. void addVerticalScrollingActions() { addAction(SemanticAction.scrollUp); addAction(SemanticAction.scrollDown); } + /// Adds the [SemanticAction.increase] and [SemanticAction.decrease] actions. + void addAdjustmentActions() { + addAction(SemanticAction.increase); + addAction(SemanticAction.decrease); + } + bool _hasAction(SemanticAction action) { return _actionHandler != null && _actions.contains(action); } @@ -285,6 +298,9 @@ class SemanticsNode extends AbstractNode { _markDirty(); } + @override + SemanticsOwner get owner => super.owner; + @override SemanticsNode get parent => super.parent; @@ -309,15 +325,16 @@ class SemanticsNode extends AbstractNode { return true; } - static Map _nodes = {}; - static Set _detachedNodes = new Set(); - @override - void attach(Object owner) { + void attach(SemanticsOwner owner) { super.attach(owner); - assert(!_nodes.containsKey(_id)); - _nodes[_id] = this; - _detachedNodes.remove(this); + assert(!owner._nodes.containsKey(_id)); + owner._nodes[_id] = this; + owner._detachedNodes.remove(this); + if (_dirty) { + _dirty = false; + _markDirty(); + } if (parent != null) _inheritedMergeAllDescendantsIntoThisNode = parent._shouldMergeAllDescendantsIntoThisNode; if (_children != null) { @@ -328,26 +345,27 @@ class SemanticsNode extends AbstractNode { @override void detach() { + assert(owner._nodes.containsKey(_id)); + assert(!owner._detachedNodes.contains(this)); + owner._nodes.remove(_id); + owner._detachedNodes.add(this); 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 _dirtyNodes = []; bool _dirty = false; void _markDirty() { if (_dirty) return; _dirty = true; - assert(!_dirtyNodes.contains(this)); - assert(!_detachedNodes.contains(this)); - _dirtyNodes.add(this); + if (attached) { + assert(!owner._dirtyNodes.contains(this)); + assert(!owner._detachedNodes.contains(this)); + owner._dirtyNodes.add(this); + } } mojom.SemanticsNode _serialize() { @@ -397,35 +415,71 @@ class SemanticsNode extends AbstractNode { return result; } - static List _listeners; + @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; + } +} + +class SemanticsOwner { + final List _dirtyNodes = []; + final Map _nodes = {}; + final Set _detachedNodes = new Set(); + + List _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; + bool get hasListeners => _listeners != null && _listeners.length > 0; /// 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(); - } + void addListener(mojom.SemanticsListener listener) { _listeners ??= []; _listeners.add(listener); } /// Uploads the semantics tree to the listeners registered with [addListener]. - static void sendSemanticsTree() { + void sendSemanticsTree() { assert(hasListeners); for (SemanticsNode oldNode in _detachedNodes) { // The other side will have forgotten this node if we even send @@ -491,7 +545,7 @@ class SemanticsNode extends AbstractNode { _dirtyNodes.clear(); } - static SemanticActionHandler _getSemanticActionHandlerForId(int id, { @required SemanticAction action }) { + SemanticActionHandler _getSemanticActionHandlerForId(int id, { @required SemanticAction action }) { assert(action != null); SemanticsNode result = _nodes[id]; if (result != null && result._shouldMergeAllDescendantsIntoThisNode && !result._hasAction(action)) { @@ -508,59 +562,29 @@ class SemanticsNode extends AbstractNode { return result._actionHandler; } - @override - String toString() { - StringBuffer buffer = new StringBuffer(); - buffer.write('$runtimeType($_id'); - if (_dirty) - buffer.write(" (${ _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; + void performAction(int id, SemanticAction action) { + SemanticActionHandler handler = _getSemanticActionHandlerForId(id, action: action); + handler?.performAction(action); } } /// Exposes the [SemanticsNode] tree to the underlying platform. class SemanticsServer extends mojom.SemanticsServer { + SemanticsServer({ @required this.semanticsOwner }) { + assert(semanticsOwner != null); + } + + final SemanticsOwner semanticsOwner; + @override void addSemanticsListener(mojom.SemanticsListenerProxy listener) { // TODO(abarth): We should remove the listener when this pipe closes. // See . - SemanticsNode.addListener(listener); + semanticsOwner.addListener(listener); } @override void performAction(int id, mojom.SemanticAction encodedAction) { - SemanticAction action = SemanticAction.values[encodedAction.mojoEnumValue]; - SemanticActionHandler node = SemanticsNode._getSemanticActionHandlerForId(id, action: action); - node?.performAction(action); + semanticsOwner.performAction(id, SemanticAction.values[encodedAction.mojoEnumValue]); } } diff --git a/packages/flutter/lib/src/widgets/semantics_debugger.dart b/packages/flutter/lib/src/widgets/semantics_debugger.dart index 367d0040660..5a73b1b5813 100644 --- a/packages/flutter/lib/src/widgets/semantics_debugger.dart +++ b/packages/flutter/lib/src/widgets/semantics_debugger.dart @@ -9,6 +9,7 @@ import 'package:flutter/rendering.dart'; import 'package:sky_services/semantics/semantics.mojom.dart' as mojom; import 'basic.dart'; +import 'binding.dart'; import 'framework.dart'; import 'gesture_detector.dart'; @@ -30,16 +31,23 @@ class SemanticsDebugger extends StatefulWidget { } class _SemanticsDebuggerState extends State { + _SemanticsClient _client; + @override void initState() { super.initState(); - _SemanticsDebuggerListener.ensureInstantiated(); - _SemanticsDebuggerListener.instance.addListener(_update); + // TODO(abarth): We shouldn't reach out to the WidgetsBinding.instance + // static here because we might not be in a tree that's attached to that + // binding. Instead, we should find a way to get to the PipelineOwner from + // the BuildContext. + WidgetsBinding.instance.ensureSemantics(); + _client = new _SemanticsClient(WidgetsBinding.instance.pipelineOwner.semanticsOwner) + ..addListener(_update); } @override void dispose() { - _SemanticsDebuggerListener.instance.removeListener(_update); + _client.removeListener(_update); super.dispose(); } @@ -58,21 +66,21 @@ class _SemanticsDebuggerState extends State { void _handleTap() { assert(_lastPointerDownLocation != null); - _SemanticsDebuggerListener.instance._performAction(_lastPointerDownLocation, SemanticAction.tap); + _client._performAction(_lastPointerDownLocation, SemanticAction.tap); setState(() { _lastPointerDownLocation = null; }); } void _handleLongPress() { assert(_lastPointerDownLocation != null); - _SemanticsDebuggerListener.instance._performAction(_lastPointerDownLocation, SemanticAction.longPress); + _client._performAction(_lastPointerDownLocation, SemanticAction.longPress); setState(() { _lastPointerDownLocation = null; }); } void _handlePanEnd(DragEndDetails details) { assert(_lastPointerDownLocation != null); - _SemanticsDebuggerListener.instance.handlePanEnd(_lastPointerDownLocation, details.velocity); + _client.handlePanEnd(_lastPointerDownLocation, details.velocity); setState(() { _lastPointerDownLocation = null; }); @@ -81,7 +89,7 @@ class _SemanticsDebuggerState extends State { @override Widget build(BuildContext context) { return new CustomPaint( - foregroundPainter: new _SemanticsDebuggerPainter(_SemanticsDebuggerListener.instance.generation, _lastPointerDownLocation), + foregroundPainter: new _SemanticsDebuggerPainter(_client.generation, _client, _lastPointerDownLocation), child: new GestureDetector( behavior: HitTestBehavior.opaque, onTap: _handleTap, @@ -196,6 +204,11 @@ class _SemanticsDebuggerEntry { || actions.contains(SemanticAction.scrollDown); } + bool get _isAdjustable { + return actions.contains(SemanticAction.increase) + || actions.contains(SemanticAction.decrease); + } + TextPainter textPainter; void _updateMessage() { List annotations = []; @@ -215,6 +228,8 @@ class _SemanticsDebuggerEntry { annotations.add('long-pressable'); if (_isScrollable) annotations.add('scrollable'); + if (_isAdjustable) + annotations.add('adjustable'); String message; if (annotations.isEmpty) { assert(label != null); @@ -295,16 +310,12 @@ class _SemanticsDebuggerEntry { } } -class _SemanticsDebuggerListener extends ChangeNotifier implements mojom.SemanticsListener { - _SemanticsDebuggerListener._() { - SemanticsNode.addListener(this); +class _SemanticsClient extends ChangeNotifier implements mojom.SemanticsListener { + _SemanticsClient(this.semanticsOwner) { + semanticsOwner.addListener(this); } - static _SemanticsDebuggerListener instance; - static final SemanticsServer _server = new SemanticsServer(); - static void ensureInstantiated() { - instance ??= new _SemanticsDebuggerListener._(); - } + final SemanticsOwner semanticsOwner; _SemanticsDebuggerEntry get rootNode => _nodes[0]; final Map _nodes = {}; @@ -357,7 +368,7 @@ class _SemanticsDebuggerListener extends ChangeNotifier implements mojom.Semanti void _performAction(Point position, SemanticAction action) { _SemanticsDebuggerEntry entry = _hitTest(position, (_SemanticsDebuggerEntry entry) => entry.actions.contains(action)); - _server.performAction(entry?.id ?? 0, mojom.SemanticAction.values[action.index]); + semanticsOwner.performAction(entry?.id ?? 0, action); } void handlePanEnd(Point position, Velocity velocity) { @@ -366,10 +377,13 @@ class _SemanticsDebuggerListener extends ChangeNotifier implements mojom.Semanti if (vx.abs() == vy.abs()) return; if (vx.abs() > vy.abs()) { - if (vx.sign < 0) + if (vx.sign < 0) { + _performAction(position, SemanticAction.decrease); _performAction(position, SemanticAction.scrollLeft); - else + } else { + _performAction(position, SemanticAction.increase); _performAction(position, SemanticAction.scrollRight); + } } else { if (vy.sign < 0) _performAction(position, SemanticAction.scrollUp); @@ -380,14 +394,15 @@ class _SemanticsDebuggerListener extends ChangeNotifier implements mojom.Semanti } class _SemanticsDebuggerPainter extends CustomPainter { - const _SemanticsDebuggerPainter(this.generation, this.pointerPosition); + const _SemanticsDebuggerPainter(this.generation, this.client, this.pointerPosition); final int generation; + final _SemanticsClient client; final Point pointerPosition; @override void paint(Canvas canvas, Size size) { - _SemanticsDebuggerEntry rootNode = _SemanticsDebuggerListener.instance.rootNode; + _SemanticsDebuggerEntry rootNode = client.rootNode; rootNode?.paint(canvas, rootNode.findDepth()); if (pointerPosition != null) { Paint paint = new Paint(); @@ -399,6 +414,7 @@ class _SemanticsDebuggerPainter extends CustomPainter { @override bool shouldRepaint(_SemanticsDebuggerPainter oldDelegate) { return generation != oldDelegate.generation + || client != oldDelegate.client || pointerPosition != oldDelegate.pointerPosition; } } diff --git a/packages/flutter/test/material/tooltip_test.dart b/packages/flutter/test/material/tooltip_test.dart index 20f70f1201e..8e231e88d14 100644 --- a/packages/flutter/test/material/tooltip_test.dart +++ b/packages/flutter/test/material/tooltip_test.dart @@ -394,7 +394,7 @@ void main() { }); testWidgets('Does tooltip contribute semantics', (WidgetTester tester) async { - TestSemanticsListener client = new TestSemanticsListener(); + TestSemanticsListener client = new TestSemanticsListener(tester); GlobalKey key = new GlobalKey(); await tester.pumpWidget( new Overlay( diff --git a/packages/flutter/test/rendering/reattach_test.dart b/packages/flutter/test/rendering/reattach_test.dart index 477de1b2d1e..c264bf6433b 100644 --- a/packages/flutter/test/rendering/reattach_test.dart +++ b/packages/flutter/test/rendering/reattach_test.dart @@ -136,9 +136,10 @@ void main() { test('objects can be detached and re-attached: semantics', () { TestTree testTree = new TestTree(); TestSemanticsListener listener = new TestSemanticsListener(); - SemanticsNode.addListener(listener); + renderer.ensureSemantics(); + renderer.pipelineOwner.semanticsOwner.addListener(listener); // Lay out, composite, paint, and update semantics - layout(testTree.root, phase: EnginePhase.sendSemanticsTree); + layout(testTree.root, phase: EnginePhase.flushSemantics); expect(listener.updates.length, equals(1)); // Remove testTree from the custom render view renderer.renderView.child = null; @@ -148,7 +149,7 @@ void main() { testTree.child.markNeedsSemanticsUpdate(); expect(listener.updates.length, equals(0)); // Lay out, composite, paint, and update semantics again - layout(testTree.root, phase: EnginePhase.sendSemanticsTree); + layout(testTree.root, phase: EnginePhase.flushSemantics); expect(listener.updates.length, equals(1)); }); } diff --git a/packages/flutter/test/rendering/rendering_tester.dart b/packages/flutter/test/rendering/rendering_tester.dart index ca6971c0949..eed173f6bee 100644 --- a/packages/flutter/test/rendering/rendering_tester.dart +++ b/packages/flutter/test/rendering/rendering_tester.dart @@ -12,8 +12,7 @@ enum EnginePhase { compositingBits, paint, composite, - flushSemantics, - sendSemanticsTree + flushSemantics } class TestRenderingFlutterBinding extends BindingBase with SchedulerBinding, ServicesBinding, RendererBinding, GestureBinding { @@ -33,24 +32,21 @@ class TestRenderingFlutterBinding extends BindingBase with SchedulerBinding, Ser renderView.compositeFrame(); if (phase == EnginePhase.composite) return; - if (SemanticsNode.hasListeners) { - pipelineOwner.flushSemantics(); - if (phase == EnginePhase.flushSemantics) - return; - SemanticsNode.sendSemanticsTree(); - } + pipelineOwner.flushSemantics(); + assert(phase == EnginePhase.flushSemantics); } } TestRenderingFlutterBinding _renderer; -TestRenderingFlutterBinding get renderer => _renderer; +TestRenderingFlutterBinding get renderer { + _renderer ??= new TestRenderingFlutterBinding(); + return _renderer; +} void layout(RenderBox box, { BoxConstraints constraints, EnginePhase phase: EnginePhase.layout }) { assert(box != null); // If you want to just repump the last box, call pumpFrame(). assert(box.parent == null); // We stick the box in another, so you can't reuse it easily, sorry. - _renderer ??= new TestRenderingFlutterBinding(); - renderer.renderView.child = null; if (constraints != null) { box = new RenderPositionedBox( diff --git a/packages/flutter/test/widget/buttons_test.dart b/packages/flutter/test/widget/buttons_test.dart index 408abe49481..863aa14e8d5 100644 --- a/packages/flutter/test/widget/buttons_test.dart +++ b/packages/flutter/test/widget/buttons_test.dart @@ -11,7 +11,7 @@ import 'test_semantics.dart'; void main() { testWidgets('Does FlatButton contribute semantics', (WidgetTester tester) async { - TestSemanticsListener client = new TestSemanticsListener(); + TestSemanticsListener client = new TestSemanticsListener(tester); await tester.pumpWidget( new Material( child: new Center( diff --git a/packages/flutter/test/widget/independent_widget_layout_test.dart b/packages/flutter/test/widget/independent_widget_layout_test.dart index a41d7d0e68a..54661bc9208 100644 --- a/packages/flutter/test/widget/independent_widget_layout_test.dart +++ b/packages/flutter/test/widget/independent_widget_layout_test.dart @@ -43,10 +43,7 @@ class OffscreenWidgetTree { pipelineOwner.flushCompositingBits(); pipelineOwner.flushPaint(); renderView.compositeFrame(); - if (SemanticsNode.hasListeners) { - pipelineOwner.flushSemantics(); - SemanticsNode.sendSemanticsTree(); - } + pipelineOwner.flushSemantics(); buildOwner.finalizeTree(); } diff --git a/packages/flutter/test/widget/semantics_1_test.dart b/packages/flutter/test/widget/semantics_1_test.dart index 1f7c44330b9..d58f71c9a47 100644 --- a/packages/flutter/test/widget/semantics_1_test.dart +++ b/packages/flutter/test/widget/semantics_1_test.dart @@ -11,7 +11,7 @@ import 'test_semantics.dart'; void main() { testWidgets('Semantics 1', (WidgetTester tester) async { - TestSemanticsListener client = new TestSemanticsListener(); + TestSemanticsListener client = new TestSemanticsListener(tester); // smoketest await tester.pumpWidget( diff --git a/packages/flutter/test/widget/semantics_2_test.dart b/packages/flutter/test/widget/semantics_2_test.dart index 596273ff157..954c8657179 100644 --- a/packages/flutter/test/widget/semantics_2_test.dart +++ b/packages/flutter/test/widget/semantics_2_test.dart @@ -11,7 +11,7 @@ import 'test_semantics.dart'; void main() { testWidgets('Semantics 2', (WidgetTester tester) async { - TestSemanticsListener client = new TestSemanticsListener(); + TestSemanticsListener client = new TestSemanticsListener(tester); // this test is the same as the test in Semantics 1, but // starting with the second branch being ignored and then diff --git a/packages/flutter/test/widget/semantics_3_test.dart b/packages/flutter/test/widget/semantics_3_test.dart index f1646b4a4dc..92ca2fc6a65 100644 --- a/packages/flutter/test/widget/semantics_3_test.dart +++ b/packages/flutter/test/widget/semantics_3_test.dart @@ -10,7 +10,7 @@ import 'test_semantics.dart'; void main() { testWidgets('Semantics 3', (WidgetTester tester) async { - TestSemanticsListener client = new TestSemanticsListener(); + TestSemanticsListener client = new TestSemanticsListener(tester); // implicit annotators await tester.pumpWidget( diff --git a/packages/flutter/test/widget/semantics_4_test.dart b/packages/flutter/test/widget/semantics_4_test.dart index cd016a5dfc6..f677312c773 100644 --- a/packages/flutter/test/widget/semantics_4_test.dart +++ b/packages/flutter/test/widget/semantics_4_test.dart @@ -10,7 +10,7 @@ import 'test_semantics.dart'; void main() { testWidgets('Semantics 4', (WidgetTester tester) async { - TestSemanticsListener client = new TestSemanticsListener(); + TestSemanticsListener client = new TestSemanticsListener(tester); // O // / \ O=root diff --git a/packages/flutter/test/widget/semantics_5_test.dart b/packages/flutter/test/widget/semantics_5_test.dart index ceb6759fb62..7399040651c 100644 --- a/packages/flutter/test/widget/semantics_5_test.dart +++ b/packages/flutter/test/widget/semantics_5_test.dart @@ -10,7 +10,7 @@ import 'test_semantics.dart'; void main() { testWidgets('Semantics 5', (WidgetTester tester) async { - TestSemanticsListener client = new TestSemanticsListener(); + TestSemanticsListener client = new TestSemanticsListener(tester); await tester.pumpWidget( new Stack( diff --git a/packages/flutter/test/widget/semantics_7_test.dart b/packages/flutter/test/widget/semantics_7_test.dart index 069b525608f..2e38118efd6 100644 --- a/packages/flutter/test/widget/semantics_7_test.dart +++ b/packages/flutter/test/widget/semantics_7_test.dart @@ -11,7 +11,7 @@ import 'package:sky_services/semantics/semantics.mojom.dart' as mojom; void main() { testWidgets('Semantics 7 - Merging', (WidgetTester tester) async { - TestSemanticsListener client = new TestSemanticsListener(); + TestSemanticsListener client = new TestSemanticsListener(tester); String label; diff --git a/packages/flutter/test/widget/semantics_8_test.dart b/packages/flutter/test/widget/semantics_8_test.dart index 05bd88d0ff8..d9244046c7b 100644 --- a/packages/flutter/test/widget/semantics_8_test.dart +++ b/packages/flutter/test/widget/semantics_8_test.dart @@ -10,7 +10,7 @@ import 'test_semantics.dart'; void main() { testWidgets('Semantics 8 - Merging with reset', (WidgetTester tester) async { - TestSemanticsListener client = new TestSemanticsListener(); + TestSemanticsListener client = new TestSemanticsListener(tester); await tester.pumpWidget( new MergeSemantics( diff --git a/packages/flutter/test/widget/test_semantics.dart b/packages/flutter/test/widget/test_semantics.dart index 9f399a3f386..294d1d3e444 100644 --- a/packages/flutter/test/widget/test_semantics.dart +++ b/packages/flutter/test/widget/test_semantics.dart @@ -2,12 +2,13 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:flutter/rendering.dart'; +import 'package:flutter_test/flutter_test.dart'; import 'package:sky_services/semantics/semantics.mojom.dart' as mojom; class TestSemanticsListener implements mojom.SemanticsListener { - TestSemanticsListener() { - SemanticsNode.addListener(this); + TestSemanticsListener(WidgetTester tester) { + tester.binding.ensureSemantics(); + tester.binding.pipelineOwner.semanticsOwner.addListener(this); } final List updates = []; diff --git a/packages/flutter_test/lib/src/binding.dart b/packages/flutter_test/lib/src/binding.dart index dd9de5b6ea3..4d4d5907ae9 100644 --- a/packages/flutter_test/lib/src/binding.dart +++ b/packages/flutter_test/lib/src/binding.dart @@ -463,12 +463,9 @@ class AutomatedTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding { renderView.compositeFrame(); // this sends the bits to the GPU if (_phase == EnginePhase.composite) return; - if (SemanticsNode.hasListeners) { - pipelineOwner.flushSemantics(); - if (_phase == EnginePhase.flushSemantics) - return; - SemanticsNode.sendSemanticsTree(); - } + pipelineOwner.flushSemantics(); + if (_phase == EnginePhase.flushSemantics) + return; buildOwner.finalizeTree(); }