Unverified Commit d04c906e authored by Michael Goderbauer's avatar Michael Goderbauer Committed by GitHub

Add arguments to SemanticsActions; implement extend selection for a11y (#13490)

**This PR contains a breaking API change:**
`SemanticsConfiguration.addAction()` has been removed and replaces by action-specific setters (`onTap`, `onLongPress`, etc.) to take care of the fact that some action handlers (those, who take arguments) have different signatures.
parent 1dd68d5d
2bdb21985c5eb9250a7b744b9b4d606c5ba30945 e07eafae1d14b454c33b1ae68d14b7a5694c22b6
...@@ -383,8 +383,8 @@ class _RenderCupertinoSlider extends RenderConstrainedBox { ...@@ -383,8 +383,8 @@ class _RenderCupertinoSlider extends RenderConstrainedBox {
config.isSemanticBoundary = isInteractive; config.isSemanticBoundary = isInteractive;
if (isInteractive) { if (isInteractive) {
config.addAction(SemanticsAction.increase, _increaseAction); config.onIncrease = _increaseAction;
config.addAction(SemanticsAction.decrease, _decreaseAction); config.onDecrease = _decreaseAction;
} }
} }
......
...@@ -380,7 +380,7 @@ class _RenderCupertinoSwitch extends RenderConstrainedBox { ...@@ -380,7 +380,7 @@ class _RenderCupertinoSwitch extends RenderConstrainedBox {
config.isSemanticBoundary = isInteractive; config.isSemanticBoundary = isInteractive;
if (isInteractive) if (isInteractive)
config.addAction(SemanticsAction.tap, _handleTap); config.onTap = _handleTap;
config.isChecked = _value; config.isChecked = _value;
} }
......
...@@ -713,8 +713,8 @@ class _RenderSlider extends RenderBox { ...@@ -713,8 +713,8 @@ class _RenderSlider extends RenderBox {
config.isSemanticBoundary = isInteractive; config.isSemanticBoundary = isInteractive;
if (isInteractive) { if (isInteractive) {
config.addAction(SemanticsAction.increase, _increaseAction); config.onIncrease = _increaseAction;
config.addAction(SemanticsAction.decrease, _decreaseAction); config.onDecrease = _decreaseAction;
} }
} }
......
...@@ -288,7 +288,7 @@ abstract class RenderToggleable extends RenderConstrainedBox { ...@@ -288,7 +288,7 @@ abstract class RenderToggleable extends RenderConstrainedBox {
config.isSemanticBoundary = isInteractive; config.isSemanticBoundary = isInteractive;
if (isInteractive) if (isInteractive)
config.addAction(SemanticsAction.tap, _handleTap); config.onTap = _handleTap;
config.isChecked = _value; config.isChecked = _value;
} }
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
import 'dart:async'; import 'dart:async';
import 'dart:developer'; import 'dart:developer';
import 'dart:typed_data';
import 'dart:ui' as ui show window; import 'dart:ui' as ui show window;
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
...@@ -187,8 +188,12 @@ abstract class RendererBinding extends BindingBase with ServicesBinding, Schedul ...@@ -187,8 +188,12 @@ abstract class RendererBinding extends BindingBase with ServicesBinding, Schedul
} }
} }
void _handleSemanticsAction(int id, SemanticsAction action) { void _handleSemanticsAction(int id, SemanticsAction action, ByteData args) {
_pipelineOwner.semanticsOwner?.performAction(id, action); _pipelineOwner.semanticsOwner?.performAction(
id,
action,
args != null ? const StandardMessageCodec().decodeMessage(args) : null,
);
} }
void _handleSemanticsOwnerCreated() { void _handleSemanticsOwnerCreated() {
......
...@@ -823,34 +823,34 @@ class RenderCustomPaint extends RenderProxyBox { ...@@ -823,34 +823,34 @@ class RenderCustomPaint extends RenderProxyBox {
config.textDirection = properties.textDirection; config.textDirection = properties.textDirection;
} }
if (properties.onTap != null) { if (properties.onTap != null) {
config.addAction(SemanticsAction.tap, properties.onTap); config.onTap = properties.onTap;
} }
if (properties.onLongPress != null) { if (properties.onLongPress != null) {
config.addAction(SemanticsAction.longPress, properties.onLongPress); config.onLongPress = properties.onLongPress;
} }
if (properties.onScrollLeft != null) { if (properties.onScrollLeft != null) {
config.addAction(SemanticsAction.scrollLeft, properties.onScrollLeft); config.onScrollLeft = properties.onScrollLeft;
} }
if (properties.onScrollRight != null) { if (properties.onScrollRight != null) {
config.addAction(SemanticsAction.scrollRight, properties.onScrollRight); config.onScrollRight = properties.onScrollRight;
} }
if (properties.onScrollUp != null) { if (properties.onScrollUp != null) {
config.addAction(SemanticsAction.scrollUp, properties.onScrollUp); config.onScrollUp = properties.onScrollUp;
} }
if (properties.onScrollDown != null) { if (properties.onScrollDown != null) {
config.addAction(SemanticsAction.scrollDown, properties.onScrollDown); config.onScrollDown = properties.onScrollDown;
} }
if (properties.onIncrease != null) { if (properties.onIncrease != null) {
config.addAction(SemanticsAction.increase, properties.onIncrease); config.onIncrease = properties.onIncrease;
} }
if (properties.onDecrease != null) { if (properties.onDecrease != null) {
config.addAction(SemanticsAction.decrease, properties.onDecrease); config.onDecrease = properties.onDecrease;
} }
if (properties.onMoveCursorForwardByCharacter != null) { if (properties.onMoveCursorForwardByCharacter != null) {
config.addAction(SemanticsAction.moveCursorForwardByCharacter, properties.onMoveCursorForwardByCharacter); config.onMoveCursorForwardByCharacter = properties.onMoveCursorForwardByCharacter;
} }
if (properties.onMoveCursorBackwardByCharacter != null) { if (properties.onMoveCursorBackwardByCharacter != null) {
config.addAction(SemanticsAction.moveCursorBackwardByCharacter, properties.onMoveCursorBackwardByCharacter); config.onMoveCursorBackwardByCharacter = properties.onMoveCursorBackwardByCharacter;
} }
newChild.updateWith( newChild.updateWith(
......
...@@ -349,30 +349,33 @@ class RenderEditable extends RenderBox { ...@@ -349,30 +349,33 @@ class RenderEditable extends RenderBox {
..isTextField = true; ..isTextField = true;
if (_selection?.isValid == true) { if (_selection?.isValid == true) {
if (_textPainter.getOffsetBefore(_selection.extentOffset) != null) { if (_textPainter.getOffsetBefore(_selection.extentOffset) != null)
config.addAction(SemanticsAction.moveCursorBackwardByCharacter, () { config.onMoveCursorBackwardByCharacter = _handleMoveCursorBackwardByCharacter;
final int offset = _textPainter.getOffsetBefore(_selection.extentOffset); if (_textPainter.getOffsetAfter(_selection.extentOffset) != null)
if (offset == null) config.onMoveCursorForwardByCharacter = _handleMoveCursorForwardByCharacter;
return;
onSelectionChanged(
new TextSelection.collapsed(offset: offset), this, SelectionChangedCause.keyboard,
);
});
}
if (_textPainter.getOffsetAfter(_selection.extentOffset) != null) {
config.addAction(SemanticsAction.moveCursorForwardByCharacter, () {
final int offset = _textPainter.getOffsetAfter(_selection.extentOffset);
if (offset == null)
return;
onSelectionChanged(
new TextSelection.collapsed(offset: offset), this, SelectionChangedCause.keyboard,
);
});
}
} }
} }
void _handleMoveCursorForwardByCharacter(bool extentSelection) {
final int extentOffset = _textPainter.getOffsetAfter(_selection.extentOffset);
if (extentOffset == null)
return;
final int baseOffset = !extentSelection ? extentOffset : _selection.baseOffset;
onSelectionChanged(
new TextSelection(baseOffset: baseOffset, extentOffset: extentOffset), this, SelectionChangedCause.keyboard,
);
}
void _handleMoveCursorBackwardByCharacter(bool extentSelection) {
final int extentOffset = _textPainter.getOffsetBefore(_selection.extentOffset);
if (extentOffset == null)
return;
final int baseOffset = !extentSelection ? extentOffset : _selection.baseOffset;
onSelectionChanged(
new TextSelection(baseOffset: baseOffset, extentOffset: extentOffset), this, SelectionChangedCause.keyboard,
);
}
@override @override
void attach(PipelineOwner owner) { void attach(PipelineOwner owner) {
super.attach(owner); super.attach(owner);
......
...@@ -2140,7 +2140,7 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im ...@@ -2140,7 +2140,7 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
/// void describeSemanticsConfiguration(SemanticsConfiguration config) { /// void describeSemanticsConfiguration(SemanticsConfiguration config) {
/// super.describeSemanticsConfiguration(config); /// super.describeSemanticsConfiguration(config);
/// config /// config
/// ..addAction(SemanticsAction.tap, _handleTap) /// ..onTap = _handleTap
/// ..label = 'I am a button' /// ..label = 'I am a button'
/// ..isButton = true; /// ..isButton = true;
/// } /// }
......
...@@ -2685,27 +2685,26 @@ class RenderSemanticsGestureHandler extends RenderProxyBox { ...@@ -2685,27 +2685,26 @@ class RenderSemanticsGestureHandler extends RenderProxyBox {
config.explicitChildNodes = onHorizontalDragUpdate != null config.explicitChildNodes = onHorizontalDragUpdate != null
|| onVerticalDragUpdate != null; || onVerticalDragUpdate != null;
final Map<SemanticsAction, VoidCallback> actions = <SemanticsAction, VoidCallback>{}; if (onTap != null && _isValidAction(SemanticsAction.tap))
if (onTap != null) config.onTap = onTap;
actions[SemanticsAction.tap] = onTap; if (onLongPress != null && _isValidAction(SemanticsAction.longPress))
if (onLongPress != null) config.onLongPress = onLongPress;
actions[SemanticsAction.longPress] = onLongPress;
if (onHorizontalDragUpdate != null) { if (onHorizontalDragUpdate != null) {
actions[SemanticsAction.scrollRight] = _performSemanticScrollRight; if (_isValidAction(SemanticsAction.scrollRight))
actions[SemanticsAction.scrollLeft] = _performSemanticScrollLeft; config.onScrollRight = _performSemanticScrollRight;
if (_isValidAction(SemanticsAction.scrollLeft))
config.onScrollLeft = _performSemanticScrollLeft;
} }
if (onVerticalDragUpdate != null) { if (onVerticalDragUpdate != null) {
actions[SemanticsAction.scrollUp] = _performSemanticScrollUp; if (_isValidAction(SemanticsAction.scrollUp))
actions[SemanticsAction.scrollDown] = _performSemanticScrollDown; config.onScrollUp = _performSemanticScrollUp;
if (_isValidAction(SemanticsAction.scrollDown))
config.onScrollDown = _performSemanticScrollDown;
} }
}
final Iterable<SemanticsAction> actionsToAdd = validActions ?? actions.keys; bool _isValidAction(SemanticsAction action) {
return validActions == null || validActions.contains(action);
for (SemanticsAction action in actionsToAdd) {
final VoidCallback handler = actions[action];
if (handler != null)
config.addAction(action, handler);
}
} }
SemanticsNode _innerNode; SemanticsNode _innerNode;
...@@ -2830,8 +2829,8 @@ class RenderSemanticsAnnotations extends RenderProxyBox { ...@@ -2830,8 +2829,8 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
VoidCallback onScrollDown, VoidCallback onScrollDown,
VoidCallback onIncrease, VoidCallback onIncrease,
VoidCallback onDecrease, VoidCallback onDecrease,
VoidCallback onMoveCursorForwardByCharacter, MoveCursorHandler onMoveCursorForwardByCharacter,
VoidCallback onMoveCursorBackwardByCharacter, MoveCursorHandler onMoveCursorBackwardByCharacter,
}) : assert(container != null), }) : assert(container != null),
_container = container, _container = container,
_explicitChildNodes = explicitChildNodes, _explicitChildNodes = explicitChildNodes,
...@@ -3173,9 +3172,9 @@ class RenderSemanticsAnnotations extends RenderProxyBox { ...@@ -3173,9 +3172,9 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
/// ///
/// TalkBack users can trigger this by pressing the volume up key while the /// TalkBack users can trigger this by pressing the volume up key while the
/// input focus is in a text field. /// input focus is in a text field.
VoidCallback get onMoveCursorForwardByCharacter => _onMoveCursorForwardByCharacter; MoveCursorHandler get onMoveCursorForwardByCharacter => _onMoveCursorForwardByCharacter;
VoidCallback _onMoveCursorForwardByCharacter; MoveCursorHandler _onMoveCursorForwardByCharacter;
set onMoveCursorForwardByCharacter(VoidCallback handler) { set onMoveCursorForwardByCharacter(MoveCursorHandler handler) {
if (_onMoveCursorForwardByCharacter == handler) if (_onMoveCursorForwardByCharacter == handler)
return; return;
final bool hadValue = _onMoveCursorForwardByCharacter != null; final bool hadValue = _onMoveCursorForwardByCharacter != null;
...@@ -3191,9 +3190,9 @@ class RenderSemanticsAnnotations extends RenderProxyBox { ...@@ -3191,9 +3190,9 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
/// ///
/// TalkBack users can trigger this by pressing the volume down key while the /// TalkBack users can trigger this by pressing the volume down key while the
/// input focus is in a text field. /// input focus is in a text field.
VoidCallback get onMoveCursorBackwardByCharacter => _onMoveCursorBackwardByCharacter; MoveCursorHandler get onMoveCursorBackwardByCharacter => _onMoveCursorBackwardByCharacter;
VoidCallback _onMoveCursorBackwardByCharacter; MoveCursorHandler _onMoveCursorBackwardByCharacter;
set onMoveCursorBackwardByCharacter(VoidCallback handler) { set onMoveCursorBackwardByCharacter(MoveCursorHandler handler) {
if (_onMoveCursorBackwardByCharacter == handler) if (_onMoveCursorBackwardByCharacter == handler)
return; return;
final bool hadValue = _onMoveCursorBackwardByCharacter != null; final bool hadValue = _onMoveCursorBackwardByCharacter != null;
...@@ -3230,25 +3229,25 @@ class RenderSemanticsAnnotations extends RenderProxyBox { ...@@ -3230,25 +3229,25 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
// ones to ensure that changing a user provided handler from a non-null to // ones to ensure that changing a user provided handler from a non-null to
// another non-null value doesn't require a semantics update. // another non-null value doesn't require a semantics update.
if (onTap != null) if (onTap != null)
config.addAction(SemanticsAction.tap, _performTap); config.onTap = _performTap;
if (onLongPress != null) if (onLongPress != null)
config.addAction(SemanticsAction.longPress, _performLongPress); config.onLongPress = _performLongPress;
if (onScrollLeft != null) if (onScrollLeft != null)
config.addAction(SemanticsAction.scrollLeft, _performScrollLeft); config.onScrollLeft = _performScrollLeft;
if (onScrollRight != null) if (onScrollRight != null)
config.addAction(SemanticsAction.scrollRight, _performScrollRight); config.onScrollRight = _performScrollRight;
if (onScrollUp != null) if (onScrollUp != null)
config.addAction(SemanticsAction.scrollUp, _performScrollUp); config.onScrollUp = _performScrollUp;
if (onScrollDown != null) if (onScrollDown != null)
config.addAction(SemanticsAction.scrollDown, _performScrollDown); config.onScrollDown = _performScrollDown;
if (onIncrease != null) if (onIncrease != null)
config.addAction(SemanticsAction.increase, _performIncrease); config.onIncrease = _performIncrease;
if (onDecrease != null) if (onDecrease != null)
config.addAction(SemanticsAction.decrease, _performDecrease); config.onDecrease = _performDecrease;
if (onMoveCursorForwardByCharacter != null) if (onMoveCursorForwardByCharacter != null)
config.addAction(SemanticsAction.moveCursorForwardByCharacter, _performMoveCursorForwardByCharacter); config.onMoveCursorForwardByCharacter = _performMoveCursorForwardByCharacter;
if (onMoveCursorBackwardByCharacter != null) if (onMoveCursorBackwardByCharacter != null)
config.addAction(SemanticsAction.moveCursorBackwardByCharacter, _performMoveCursorBackwardByCharacter); config.onMoveCursorBackwardByCharacter = _performMoveCursorBackwardByCharacter;
} }
void _performTap() { void _performTap() {
...@@ -3291,14 +3290,14 @@ class RenderSemanticsAnnotations extends RenderProxyBox { ...@@ -3291,14 +3290,14 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
onDecrease(); onDecrease();
} }
void _performMoveCursorForwardByCharacter() { void _performMoveCursorForwardByCharacter(bool extendSelection) {
if (onMoveCursorForwardByCharacter != null) if (onMoveCursorForwardByCharacter != null)
onMoveCursorForwardByCharacter(); onMoveCursorForwardByCharacter(extendSelection);
} }
void _performMoveCursorBackwardByCharacter() { void _performMoveCursorBackwardByCharacter(bool extendSelection) {
if (onMoveCursorBackwardByCharacter != null) if (onMoveCursorBackwardByCharacter != null)
onMoveCursorBackwardByCharacter(); onMoveCursorBackwardByCharacter(extendSelection);
} }
} }
......
...@@ -25,6 +25,14 @@ export 'semantics_event.dart'; ...@@ -25,6 +25,14 @@ export 'semantics_event.dart';
/// Used by [SemanticsNode.visitChildren]. /// Used by [SemanticsNode.visitChildren].
typedef bool SemanticsNodeVisitor(SemanticsNode node); typedef bool SemanticsNodeVisitor(SemanticsNode node);
/// Signature for [SemanticsAction]s that move the cursor.
///
/// If `extendSelection` is set to true the cursor movement should extend the
/// current selection or (if nothing is currently selected) start a selection.
typedef void MoveCursorHandler(bool extendSelection);
typedef void _SemanticsActionHandler(dynamic args);
/// A tag for a [SemanticsNode]. /// A tag for a [SemanticsNode].
/// ///
/// Tags can be interpreted by the parent of a [SemanticsNode] /// Tags can be interpreted by the parent of a [SemanticsNode]
...@@ -445,7 +453,7 @@ class SemanticsProperties extends DiagnosticableTree { ...@@ -445,7 +453,7 @@ class SemanticsProperties extends DiagnosticableTree {
/// ///
/// TalkBack users can trigger this by pressing the volume up key while the /// TalkBack users can trigger this by pressing the volume up key while the
/// input focus is in a text field. /// input focus is in a text field.
final VoidCallback onMoveCursorForwardByCharacter; final MoveCursorHandler onMoveCursorForwardByCharacter;
/// The handler for [SemanticsAction.onMoveCursorBackwardByCharacter]. /// The handler for [SemanticsAction.onMoveCursorBackwardByCharacter].
/// ///
...@@ -454,7 +462,7 @@ class SemanticsProperties extends DiagnosticableTree { ...@@ -454,7 +462,7 @@ class SemanticsProperties extends DiagnosticableTree {
/// ///
/// TalkBack users can trigger this by pressing the volume down key while the /// TalkBack users can trigger this by pressing the volume down key while the
/// input focus is in a text field. /// input focus is in a text field.
final VoidCallback onMoveCursorBackwardByCharacter; final MoveCursorHandler onMoveCursorBackwardByCharacter;
@override @override
void debugFillProperties(DiagnosticPropertiesBuilder description) { void debugFillProperties(DiagnosticPropertiesBuilder description) {
...@@ -824,7 +832,7 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin { ...@@ -824,7 +832,7 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
// TAGS, LABELS, ACTIONS // TAGS, LABELS, ACTIONS
Map<SemanticsAction, VoidCallback> _actions = _kEmptyConfig._actions; Map<SemanticsAction, _SemanticsActionHandler> _actions = _kEmptyConfig._actions;
int _actionsAsBits = _kEmptyConfig._actionsAsBits; int _actionsAsBits = _kEmptyConfig._actionsAsBits;
...@@ -912,7 +920,7 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin { ...@@ -912,7 +920,7 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
_hint = config.hint; _hint = config.hint;
_flags = config._flags; _flags = config._flags;
_textDirection = config.textDirection; _textDirection = config.textDirection;
_actions = new Map<SemanticsAction, VoidCallback>.from(config._actions); _actions = new Map<SemanticsAction, _SemanticsActionHandler>.from(config._actions);
_actionsAsBits = config._actionsAsBits; _actionsAsBits = config._actionsAsBits;
_mergeAllDescendantsIntoThisNode = config.isMergingSemanticsOfDescendants; _mergeAllDescendantsIntoThisNode = config.isMergingSemanticsOfDescendants;
_replaceChildren(childrenInInversePaintOrder ?? const <SemanticsNode>[]); _replaceChildren(childrenInInversePaintOrder ?? const <SemanticsNode>[]);
...@@ -1213,7 +1221,7 @@ class SemanticsOwner extends ChangeNotifier { ...@@ -1213,7 +1221,7 @@ class SemanticsOwner extends ChangeNotifier {
notifyListeners(); notifyListeners();
} }
VoidCallback _getSemanticsActionHandlerForId(int id, SemanticsAction action) { _SemanticsActionHandler _getSemanticsActionHandlerForId(int id, SemanticsAction action) {
SemanticsNode result = _nodes[id]; SemanticsNode result = _nodes[id];
if (result != null && result.isPartOfNodeMerging && !result._canPerformAction(action)) { if (result != null && result.isPartOfNodeMerging && !result._canPerformAction(action)) {
result._visitDescendants((SemanticsNode node) { result._visitDescendants((SemanticsNode node) {
...@@ -1233,11 +1241,14 @@ class SemanticsOwner extends ChangeNotifier { ...@@ -1233,11 +1241,14 @@ class SemanticsOwner extends ChangeNotifier {
/// ///
/// If the [SemanticsNode] has not indicated that it can perform the action, /// If the [SemanticsNode] has not indicated that it can perform the action,
/// this function does nothing. /// this function does nothing.
void performAction(int id, SemanticsAction action) { ///
/// If the given `action` requires arguments they need to be passed in via
/// the `args` parameter.
void performAction(int id, SemanticsAction action, [dynamic args]) {
assert(action != null); assert(action != null);
final VoidCallback handler = _getSemanticsActionHandlerForId(id, action); final _SemanticsActionHandler handler = _getSemanticsActionHandlerForId(id, action);
if (handler != null) { if (handler != null) {
handler(); handler(args);
return; return;
} }
...@@ -1246,7 +1257,7 @@ class SemanticsOwner extends ChangeNotifier { ...@@ -1246,7 +1257,7 @@ class SemanticsOwner extends ChangeNotifier {
_nodes[id]._showOnScreen(); _nodes[id]._showOnScreen();
} }
VoidCallback _getSemanticsActionHandlerForPosition(SemanticsNode node, Offset position, SemanticsAction action) { _SemanticsActionHandler _getSemanticsActionHandlerForPosition(SemanticsNode node, Offset position, SemanticsAction action) {
if (node.transform != null) { if (node.transform != null) {
final Matrix4 inverse = new Matrix4.identity(); final Matrix4 inverse = new Matrix4.identity();
if (inverse.copyInverse(node.transform) == 0.0) if (inverse.copyInverse(node.transform) == 0.0)
...@@ -1268,7 +1279,7 @@ class SemanticsOwner extends ChangeNotifier { ...@@ -1268,7 +1279,7 @@ class SemanticsOwner extends ChangeNotifier {
} }
if (node.hasChildren) { if (node.hasChildren) {
for (SemanticsNode child in node._children.reversed) { for (SemanticsNode child in node._children.reversed) {
final VoidCallback handler = _getSemanticsActionHandlerForPosition(child, position, action); final _SemanticsActionHandler handler = _getSemanticsActionHandlerForPosition(child, position, action);
if (handler != null) if (handler != null)
return handler; return handler;
} }
...@@ -1280,14 +1291,17 @@ class SemanticsOwner extends ChangeNotifier { ...@@ -1280,14 +1291,17 @@ class SemanticsOwner extends ChangeNotifier {
/// ///
/// If the [SemanticsNode] has not indicated that it can perform the action, /// If the [SemanticsNode] has not indicated that it can perform the action,
/// this function does nothing. /// this function does nothing.
void performActionAt(Offset position, SemanticsAction action) { ///
/// If the given `action` requires arguments they need to be passed in via
/// the `args` parameter.
void performActionAt(Offset position, SemanticsAction action, [dynamic args]) {
assert(action != null); assert(action != null);
final SemanticsNode node = rootSemanticsNode; final SemanticsNode node = rootSemanticsNode;
if (node == null) if (node == null)
return; return;
final VoidCallback handler = _getSemanticsActionHandlerForPosition(node, position, action); final _SemanticsActionHandler handler = _getSemanticsActionHandlerForPosition(node, position, action);
if (handler != null) if (handler != null)
handler(); handler(args);
} }
@override @override
...@@ -1366,7 +1380,7 @@ class SemanticsConfiguration { ...@@ -1366,7 +1380,7 @@ class SemanticsConfiguration {
bool _isMergingDescendantsIntoOneNode = false; bool _isMergingDescendantsIntoOneNode = false;
set isMergingDescendantsIntoOneNode(bool value) { set isMergingDescendantsIntoOneNode(bool value) {
assert(isSemanticBoundary); assert(isSemanticBoundary);
_isMergingDescendantsIntoOneNode = isMergingDescendantsIntoOneNode; _isMergingDescendantsIntoOneNode = value;
} }
// SEMANTIC ANNOTATIONS // SEMANTIC ANNOTATIONS
...@@ -1386,27 +1400,233 @@ class SemanticsConfiguration { ...@@ -1386,27 +1400,233 @@ class SemanticsConfiguration {
/// See also: /// See also:
/// ///
/// * [addAction] to add an action. /// * [addAction] to add an action.
final Map<SemanticsAction, VoidCallback> _actions = <SemanticsAction, VoidCallback>{}; final Map<SemanticsAction, _SemanticsActionHandler> _actions = <SemanticsAction, _SemanticsActionHandler>{};
int _actionsAsBits = 0; int _actionsAsBits = 0;
/// Adds an `action` to the semantics tree. /// Adds an `action` to the semantics tree.
/// ///
/// Whenever the user performs `action` the provided `handler` is called. /// The provided `handler` is called to respond to the user triggered
void addAction(SemanticsAction action, VoidCallback handler) { /// `action`.
void _addAction(SemanticsAction action, _SemanticsActionHandler handler) {
assert(handler != null); assert(handler != null);
_actions[action] = handler; _actions[action] = handler;
_actionsAsBits |= action.index; _actionsAsBits |= action.index;
_hasBeenAnnotated = true; _hasBeenAnnotated = true;
} }
/// Adds an `action` to the semantics tree, whose `handler` does not expect
/// any arguments.
///
/// The provided `handler` is called to respond to the user triggered
/// `action`.
void _addArgumentlessAction(SemanticsAction action, VoidCallback handler) {
assert(handler != null);
_addAction(action, (dynamic args) {
assert(args == null);
handler();
});
}
/// The handler for [SemanticsAction.tap].
///
/// This is the semantic equivalent of a user briefly tapping the screen with
/// the finger without moving it. For example, a button should implement this
/// action.
///
/// VoiceOver users on iOS and TalkBack users on Android can trigger this
/// action by double-tapping the screen while an element is focused.
VoidCallback get onTap => _onTap;
VoidCallback _onTap;
set onTap(VoidCallback value) {
_addArgumentlessAction(SemanticsAction.tap, value);
_onTap = value;
}
/// The handler for [SemanticsAction.longPress].
///
/// This is the semantic equivalent of a user pressing and holding the screen
/// with the finger for a few seconds without moving it.
///
/// VoiceOver users on iOS and TalkBack users on Android can trigger this
/// action by double-tapping the screen without lifting the finger after the
/// second tap.
VoidCallback get onLongPress => _onLongPress;
VoidCallback _onLongPress;
set onLongPress(VoidCallback value) {
_addArgumentlessAction(SemanticsAction.longPress, value);
_onLongPress = value;
}
/// The handler for [SemanticsAction.scrollLeft].
///
/// This is the semantic equivalent of a user moving their finger across the
/// screen from right to left. It should be recognized by controls that are
/// horizontally scrollable.
///
/// VoiceOver users on iOS can trigger this action by swiping left with three
/// fingers. TalkBack users on Android can trigger this action by swiping
/// right and then left in one motion path. On Android, [onScrollUp] and
/// [onScrollLeft] share the same gesture. Therefore, only on of them should
/// be provided.
VoidCallback get onScrollLeft => _onScrollLeft;
VoidCallback _onScrollLeft;
set onScrollLeft(VoidCallback value) {
_addArgumentlessAction(SemanticsAction.scrollLeft, value);
_onScrollLeft = value;
}
/// The handler for [SemanticsAction.scrollRight].
///
/// This is the semantic equivalent of a user moving their finger across the
/// screen from left to right. It should be recognized by controls that are
/// horizontally scrollable.
///
/// VoiceOver users on iOS can trigger this action by swiping right with three
/// fingers. TalkBack users on Android can trigger this action by swiping
/// left and then right in one motion path. On Android, [onScrollDown] and
/// [onScrollRight] share the same gesture. Therefore, only on of them should
/// be provided.
VoidCallback get onScrollRight => _onScrollRight;
VoidCallback _onScrollRight;
set onScrollRight(VoidCallback value) {
_addArgumentlessAction(SemanticsAction.scrollRight, value);
_onScrollRight = value;
}
/// The handler for [SemanticsAction.scrollUp].
///
/// This is the semantic equivalent of a user moving their finger across the
/// screen from bottom to top. It should be recognized by controls that are
/// vertically scrollable.
///
/// VoiceOver users on iOS can trigger this action by swiping up with three
/// fingers. TalkBack users on Android can trigger this action by swiping
/// right and then left in one motion path. On Android, [onScrollUp] and
/// [onScrollLeft] share the same gesture. Therefore, only on of them should
/// be provided.
VoidCallback get onScrollUp => _onScrollUp;
VoidCallback _onScrollUp;
set onScrollUp(VoidCallback value) {
_addArgumentlessAction(SemanticsAction.scrollUp, value);
_onScrollUp = value;
}
/// The handler for [SemanticsAction.scrollDown].
///
/// This is the semantic equivalent of a user moving their finger across the
/// screen from top to bottom. It should be recognized by controls that are
/// vertically scrollable.
///
/// VoiceOver users on iOS can trigger this action by swiping down with three
/// fingers. TalkBack users on Android can trigger this action by swiping
/// left and then right in one motion path. On Android, [onScrollDown] and
/// [onScrollRight] share the same gesture. Therefore, only on of them should
/// be provided.
VoidCallback get onScrollDown => _onScrollDown;
VoidCallback _onScrollDown;
set onScrollDown(VoidCallback value) {
_addArgumentlessAction(SemanticsAction.scrollDown, value);
_onScrollDown = value;
}
/// The handler for [SemanticsAction.increase].
///
/// This is a request to increase the value represented by the widget. For
/// example, this action might be recognized by a slider control.
///
/// If a [value] is set, [increasedValue] must also be provided and
/// [onIncrease] must ensure that [value] will be set to [increasedValue].
///
/// VoiceOver users on iOS can trigger this action by swiping up with one
/// finger. TalkBack users on Android can trigger this action by pressing the
/// volume up button.
VoidCallback get onIncrease => _onIncrease;
VoidCallback _onIncrease;
set onIncrease(VoidCallback value) {
_addArgumentlessAction(SemanticsAction.increase, value);
_onIncrease = value;
}
/// The handler for [SemanticsAction.decrease].
///
/// This is a request to decrease the value represented by the widget. For
/// example, this action might be recognized by a slider control.
///
/// If a [value] is set, [decreasedValue] must also be provided and
/// [onDecrease] must ensure that [value] will be set to [decreasedValue].
///
/// VoiceOver users on iOS can trigger this action by swiping down with one
/// finger. TalkBack users on Android can trigger this action by pressing the
/// volume down button.
VoidCallback get onDecrease => _onDecrease;
VoidCallback _onDecrease;
set onDecrease(VoidCallback value) {
_addArgumentlessAction(SemanticsAction.decrease, value);
_onDecrease = value;
}
/// The handler for [SemanticsAction.showOnScreen].
///
/// A request to fully show the semantics node on screen. For example, this
/// action might be send to a node in a scrollable list that is partially off
/// screen to bring it on screen.
///
/// For elements in a scrollable list the framework provides a default
/// implementation for this action and it is not advised to provide a
/// custom one via this setter.
VoidCallback get onShowOnScreen => _onShowOnScreen;
VoidCallback _onShowOnScreen;
set onShowOnScreen(VoidCallback value) {
_addArgumentlessAction(SemanticsAction.showOnScreen, value);
_onShowOnScreen = value;
}
/// The handler for [SemanticsAction.onMoveCursorForwardByCharacter].
///
/// This handler is invoked when the user wants to move the cursor in a
/// text field forward by one character.
///
/// TalkBack users can trigger this by pressing the volume up key while the
/// input focus is in a text field.
MoveCursorHandler get onMoveCursorForwardByCharacter => _onMoveCursorForwardByCharacter;
MoveCursorHandler _onMoveCursorForwardByCharacter;
set onMoveCursorForwardByCharacter(MoveCursorHandler value) {
assert(value != null);
_addAction(SemanticsAction.moveCursorForwardByCharacter, (dynamic args) {
final bool extentSelection = args;
assert(extentSelection != null);
value(extentSelection);
});
_onMoveCursorForwardByCharacter = value;
}
/// The handler for [SemanticsAction.onMoveCursorBackwardByCharacter].
///
/// This handler is invoked when the user wants to move the cursor in a
/// text field backward by one character.
///
/// TalkBack users can trigger this by pressing the volume down key while the
/// input focus is in a text field.
MoveCursorHandler get onMoveCursorBackwardByCharacter => _onMoveCursorBackwardByCharacter;
MoveCursorHandler _onMoveCursorBackwardByCharacter;
set onMoveCursorBackwardByCharacter(MoveCursorHandler value) {
assert(value != null);
_addAction(SemanticsAction.moveCursorBackwardByCharacter, (dynamic args) {
final bool extentSelection = args;
assert(extentSelection != null);
value(extentSelection);
});
_onMoveCursorBackwardByCharacter = value;
}
/// Returns the action handler registered for [action] or null if none was /// Returns the action handler registered for [action] or null if none was
/// registered. /// registered.
/// ///
/// See also: /// See also:
/// ///
/// * [addAction] to add an action. /// * [addAction] to add an action.
VoidCallback getActionHandler(SemanticsAction action) => _actions[action]; _SemanticsActionHandler getActionHandler(SemanticsAction action) => _actions[action];
/// Whether the semantic information provided by the owning [RenderObject] and /// Whether the semantic information provided by the owning [RenderObject] and
/// all of its descendants should be treated as one logical entity. /// all of its descendants should be treated as one logical entity.
...@@ -1516,6 +1736,7 @@ class SemanticsConfiguration { ...@@ -1516,6 +1736,7 @@ class SemanticsConfiguration {
} }
/// Whether the owning [RenderObject] is selected (true) or not (false). /// Whether the owning [RenderObject] is selected (true) or not (false).
bool get isSelected => _hasFlag(SemanticsFlags.isSelected);
set isSelected(bool value) { set isSelected(bool value) {
_setFlag(SemanticsFlags.isSelected, value); _setFlag(SemanticsFlags.isSelected, value);
} }
...@@ -1525,22 +1746,26 @@ class SemanticsConfiguration { ...@@ -1525,22 +1746,26 @@ class SemanticsConfiguration {
/// ///
/// Do not set this to any value if the owning [RenderObject] doesn't have /// Do not set this to any value if the owning [RenderObject] doesn't have
/// Booleans state that can be controlled by the user. /// Booleans state that can be controlled by the user.
bool get isChecked => _hasFlag(SemanticsFlags.hasCheckedState) && _hasFlag(SemanticsFlags.isChecked);
set isChecked(bool value) { set isChecked(bool value) {
_setFlag(SemanticsFlags.hasCheckedState, true); _setFlag(SemanticsFlags.hasCheckedState, true);
_setFlag(SemanticsFlags.isChecked, value); _setFlag(SemanticsFlags.isChecked, value);
} }
/// Whether the owning [RenderObject] currently holds the user's focus. /// Whether the owning [RenderObject] currently holds the user's focus.
bool get isFocused => _hasFlag(SemanticsFlags.isFocused);
set isFocused(bool value) { set isFocused(bool value) {
_setFlag(SemanticsFlags.isFocused, value); _setFlag(SemanticsFlags.isFocused, value);
} }
/// Whether the owning [RenderObject] is a button (true) or not (false). /// Whether the owning [RenderObject] is a button (true) or not (false).
bool get isButton => _hasFlag(SemanticsFlags.isButton);
set isButton(bool value) { set isButton(bool value) {
_setFlag(SemanticsFlags.isButton, value); _setFlag(SemanticsFlags.isButton, value);
} }
/// Whether the owning [RenderObject] is a text field. /// Whether the owning [RenderObject] is a text field.
bool get isTextField => _hasFlag(SemanticsFlags.isTextField);
set isTextField(bool value) { set isTextField(bool value) {
_setFlag(SemanticsFlags.isTextField, value); _setFlag(SemanticsFlags.isTextField, value);
} }
...@@ -1589,6 +1814,8 @@ class SemanticsConfiguration { ...@@ -1589,6 +1814,8 @@ class SemanticsConfiguration {
_hasBeenAnnotated = true; _hasBeenAnnotated = true;
} }
bool _hasFlag(SemanticsFlags flag) => (_flags & flag.index) != 0;
// CONFIGURATION COMBINATION LOGIC // CONFIGURATION COMBINATION LOGIC
/// Whether this configuration is compatible with the provided `other` /// Whether this configuration is compatible with the provided `other`
......
...@@ -265,10 +265,17 @@ TextEditingValue _selectionAwareTextManipulation( ...@@ -265,10 +265,17 @@ TextEditingValue _selectionAwareTextManipulation(
value.text.substring(selectionEndIndex) value.text.substring(selectionEndIndex)
); );
manipulatedText = beforeSelection + inSelection + afterSelection; manipulatedText = beforeSelection + inSelection + afterSelection;
manipulatedSelection = value.selection.copyWith( if (value.selection.baseOffset > value.selection.extentOffset) {
baseOffset: beforeSelection.length, manipulatedSelection = value.selection.copyWith(
extentOffset: beforeSelection.length + inSelection.length, baseOffset: beforeSelection.length + inSelection.length,
); extentOffset: beforeSelection.length,
);
} else {
manipulatedSelection = value.selection.copyWith(
baseOffset: beforeSelection.length,
extentOffset: beforeSelection.length + inSelection.length,
);
}
} }
return new TextEditingValue( return new TextEditingValue(
text: manipulatedText, text: manipulatedText,
......
...@@ -4726,8 +4726,8 @@ class Semantics extends SingleChildRenderObjectWidget { ...@@ -4726,8 +4726,8 @@ class Semantics extends SingleChildRenderObjectWidget {
VoidCallback onScrollDown, VoidCallback onScrollDown,
VoidCallback onIncrease, VoidCallback onIncrease,
VoidCallback onDecrease, VoidCallback onDecrease,
VoidCallback onMoveCursorForwardByCharacter, MoveCursorHandler onMoveCursorForwardByCharacter,
VoidCallback onMoveCursorBackwardByCharacter, MoveCursorHandler onMoveCursorBackwardByCharacter,
}) : this.fromProperties( }) : this.fromProperties(
key: key, key: key,
child: child, child: child,
......
...@@ -65,19 +65,19 @@ void main() { ...@@ -65,19 +65,19 @@ void main() {
TestRender middle; TestRender middle;
final TestRender root = new TestRender( final TestRender root = new TestRender(
action: SemanticsAction.tap, hasTapAction: true,
isSemanticBoundary: true, isSemanticBoundary: true,
child: new TestRender( child: new TestRender(
action: SemanticsAction.longPress, hasLongPressAction: true,
isSemanticBoundary: false, isSemanticBoundary: false,
child: middle = new TestRender( child: middle = new TestRender(
action: SemanticsAction.scrollLeft, hasScrollLeftAction: true,
isSemanticBoundary: false, isSemanticBoundary: false,
child: new TestRender( child: new TestRender(
action: SemanticsAction.scrollRight, hasScrollRightAction: true,
isSemanticBoundary: false, isSemanticBoundary: false,
child: new TestRender( child: new TestRender(
action: SemanticsAction.scrollUp, hasScrollUpAction: true,
isSemanticBoundary: true, isSemanticBoundary: true,
) )
) )
...@@ -91,7 +91,9 @@ void main() { ...@@ -91,7 +91,9 @@ void main() {
int expectedActions = SemanticsAction.tap.index | SemanticsAction.longPress.index | SemanticsAction.scrollLeft.index | SemanticsAction.scrollRight.index; int expectedActions = SemanticsAction.tap.index | SemanticsAction.longPress.index | SemanticsAction.scrollLeft.index | SemanticsAction.scrollRight.index;
expect(root.debugSemantics.getSemanticsData().actions, expectedActions); expect(root.debugSemantics.getSemanticsData().actions, expectedActions);
middle.action = SemanticsAction.scrollDown; middle
..hasScrollLeftAction = false
..hasScrollDownAction = true;
middle.markNeedsSemanticsUpdate(); middle.markNeedsSemanticsUpdate();
pumpFrame(phase: EnginePhase.flushSemantics); pumpFrame(phase: EnginePhase.flushSemantics);
...@@ -204,9 +206,9 @@ void main() { ...@@ -204,9 +206,9 @@ void main() {
final SemanticsConfiguration config = new SemanticsConfiguration() final SemanticsConfiguration config = new SemanticsConfiguration()
..isMergingSemanticsOfDescendants = true ..isMergingSemanticsOfDescendants = true
..addAction(SemanticsAction.scrollUp, () { }) ..onScrollUp = () { }
..addAction(SemanticsAction.longPress, () { }) ..onLongPress = () { }
..addAction(SemanticsAction.showOnScreen, () { }) ..onShowOnScreen = () { }
..isChecked = false ..isChecked = false
..isSelected = true ..isSelected = true
..isButton = true ..isButton = true
...@@ -237,22 +239,128 @@ void main() { ...@@ -237,22 +239,128 @@ void main() {
'SemanticsData(Rect.fromLTRB(50.0, 10.0, 70.0, 40.0), [10.0,0.0,0.0,0.0; 0.0,10.0,0.0,0.0; 0.0,0.0,1.0,0.0; 0.0,0.0,0.0,1.0])', 'SemanticsData(Rect.fromLTRB(50.0, 10.0, 70.0, 40.0), [10.0,0.0,0.0,0.0; 0.0,10.0,0.0,0.0; 0.0,0.0,1.0,0.0; 0.0,0.0,0.0,1.0])',
); );
}); });
test('SemanticsConfiguration getter/setter', () {
final SemanticsConfiguration config = new SemanticsConfiguration();
expect(config.isSemanticBoundary, isFalse);
expect(config.isButton, isFalse);
expect(config.isMergingSemanticsOfDescendants, isFalse);
expect(config.isChecked, isFalse);
expect(config.isSelected, isFalse);
expect(config.isBlockingSemanticsOfPreviouslyPaintedNodes, isFalse);
expect(config.isFocused, isFalse);
expect(config.isMergingDescendantsIntoOneNode, isFalse);
expect(config.isTextField, isFalse);
expect(config.onShowOnScreen, isNull);
expect(config.onScrollDown, isNull);
expect(config.onScrollUp, isNull);
expect(config.onScrollLeft, isNull);
expect(config.onScrollRight, isNull);
expect(config.onLongPress, isNull);
expect(config.onDecrease, isNull);
expect(config.onIncrease, isNull);
expect(config.onMoveCursorForwardByCharacter, isNull);
expect(config.onMoveCursorBackwardByCharacter, isNull);
expect(config.onTap, isNull);
config.isSemanticBoundary = true;
config.isButton = true;
config.isMergingSemanticsOfDescendants = true;
config.isChecked = true;
config.isSelected = true;
config.isBlockingSemanticsOfPreviouslyPaintedNodes = true;
config.isFocused = true;
config.isMergingDescendantsIntoOneNode = true;
config.isTextField = true;
final VoidCallback onShowOnScreen = () { };
final VoidCallback onScrollDown = () { };
final VoidCallback onScrollUp = () { };
final VoidCallback onScrollLeft = () { };
final VoidCallback onScrollRight = () { };
final VoidCallback onLongPress = () { };
final VoidCallback onDecrease = () { };
final VoidCallback onIncrease = () { };
final MoveCursorHandler onMoveCursorForwardByCharacter = (bool _) { };
final MoveCursorHandler onMoveCursorBackwardByCharacter = (bool _) { };
final VoidCallback onTap = () { };
config.onShowOnScreen = onShowOnScreen;
config.onScrollDown = onScrollDown;
config.onScrollUp = onScrollUp;
config.onScrollLeft = onScrollLeft;
config.onScrollRight = onScrollRight;
config.onLongPress = onLongPress;
config.onDecrease = onDecrease;
config.onIncrease = onIncrease;
config.onMoveCursorForwardByCharacter = onMoveCursorForwardByCharacter;
config.onMoveCursorBackwardByCharacter = onMoveCursorBackwardByCharacter;
config.onTap = onTap;
expect(config.isSemanticBoundary, isTrue);
expect(config.isButton, isTrue);
expect(config.isMergingSemanticsOfDescendants, isTrue);
expect(config.isChecked, isTrue);
expect(config.isSelected, isTrue);
expect(config.isBlockingSemanticsOfPreviouslyPaintedNodes, isTrue);
expect(config.isFocused, isTrue);
expect(config.isMergingDescendantsIntoOneNode, isTrue);
expect(config.isTextField, isTrue);
expect(config.onShowOnScreen, same(onShowOnScreen));
expect(config.onScrollDown, same(onScrollDown));
expect(config.onScrollUp, same(onScrollUp));
expect(config.onScrollLeft, same(onScrollLeft));
expect(config.onScrollRight, same(onScrollRight));
expect(config.onLongPress, same(onLongPress));
expect(config.onDecrease, same(onDecrease));
expect(config.onIncrease, same(onIncrease));
expect(config.onMoveCursorForwardByCharacter, same(onMoveCursorForwardByCharacter));
expect(config.onMoveCursorBackwardByCharacter, same(onMoveCursorBackwardByCharacter));
expect(config.onTap, same(onTap));
});
} }
class TestRender extends RenderProxyBox { class TestRender extends RenderProxyBox {
TestRender({ this.action, this.isSemanticBoundary, RenderObject child }) : super(child); TestRender({
this.hasTapAction: false,
final bool isSemanticBoundary; this.hasLongPressAction: false,
this.hasScrollLeftAction: false,
this.hasScrollRightAction: false,
this.hasScrollUpAction: false,
this.hasScrollDownAction: false,
this.isSemanticBoundary,
RenderObject child
}) : super(child);
bool hasTapAction;
bool hasLongPressAction;
bool hasScrollLeftAction;
bool hasScrollRightAction;
bool hasScrollUpAction;
bool hasScrollDownAction;
bool isSemanticBoundary;
SemanticsAction action;
@override @override
void describeSemanticsConfiguration(SemanticsConfiguration config) { void describeSemanticsConfiguration(SemanticsConfiguration config) {
super.describeSemanticsConfiguration(config); super.describeSemanticsConfiguration(config);
config config.isSemanticBoundary = isSemanticBoundary;
..isSemanticBoundary = isSemanticBoundary if (hasTapAction)
..addAction(action, () { }); config.onTap = () { };
if (hasLongPressAction)
config.onLongPress = () { };
if (hasScrollLeftAction)
config.onScrollLeft = () { };
if (hasScrollRightAction)
config.onScrollRight = () { };
if (hasScrollUpAction)
config.onScrollUp = () { };
if (hasScrollDownAction)
config.onScrollDown = () { };
} }
} }
...@@ -414,6 +414,7 @@ void main() { ...@@ -414,6 +414,7 @@ void main() {
testWidgets('can move cursor with a11y means', (WidgetTester tester) async { testWidgets('can move cursor with a11y means', (WidgetTester tester) async {
final SemanticsTester semantics = new SemanticsTester(tester); final SemanticsTester semantics = new SemanticsTester(tester);
const bool doNotExtendSelection = false;
controller.text = 'test'; controller.text = 'test';
controller.selection = new TextSelection.collapsed(offset: controller.text.length); controller.selection = new TextSelection.collapsed(offset: controller.text.length);
...@@ -440,7 +441,7 @@ void main() { ...@@ -440,7 +441,7 @@ void main() {
expect(controller.selection.baseOffset, 4); expect(controller.selection.baseOffset, 4);
expect(controller.selection.extentOffset, 4); expect(controller.selection.extentOffset, 4);
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId, SemanticsAction.moveCursorBackwardByCharacter); tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId, SemanticsAction.moveCursorBackwardByCharacter, doNotExtendSelection);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 3); expect(controller.selection.baseOffset, 3);
...@@ -454,11 +455,11 @@ void main() { ...@@ -454,11 +455,11 @@ void main() {
], ],
)); ));
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId, SemanticsAction.moveCursorBackwardByCharacter); tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId, SemanticsAction.moveCursorBackwardByCharacter, doNotExtendSelection);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId, SemanticsAction.moveCursorBackwardByCharacter); tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId, SemanticsAction.moveCursorBackwardByCharacter, doNotExtendSelection);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId, SemanticsAction.moveCursorBackwardByCharacter); tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId, SemanticsAction.moveCursorBackwardByCharacter, doNotExtendSelection);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 0); expect(controller.selection.baseOffset, 0);
...@@ -472,6 +473,89 @@ void main() { ...@@ -472,6 +473,89 @@ void main() {
], ],
)); ));
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId, SemanticsAction.moveCursorForwardByCharacter, doNotExtendSelection);
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 1);
expect(controller.selection.extentOffset, 1);
semantics.dispose();
});
testWidgets('can extend selection with a11y means', (WidgetTester tester) async {
final SemanticsTester semantics = new SemanticsTester(tester);
const bool extendSelection = true;
const bool doNotExtendSelection = false;
controller.text = 'test';
controller.selection = new TextSelection.collapsed(offset: controller.text.length);
await tester.pumpWidget(new MaterialApp(
home: new EditableText(
controller: controller,
focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
),
));
expect(semantics, includesNodeWith(
value: 'test',
actions: <SemanticsAction>[
SemanticsAction.moveCursorBackwardByCharacter,
],
));
final RenderEditable render = tester.allRenderObjects.firstWhere((RenderObject o) => o.runtimeType == RenderEditable);
final int semanticsId = render.debugSemantics.id;
expect(controller.selection.baseOffset, 4);
expect(controller.selection.extentOffset, 4);
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId, SemanticsAction.moveCursorBackwardByCharacter, extendSelection);
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 4);
expect(controller.selection.extentOffset, 3);
expect(semantics, includesNodeWith(
value: 'test',
actions: <SemanticsAction>[
SemanticsAction.moveCursorBackwardByCharacter,
SemanticsAction.moveCursorForwardByCharacter,
],
));
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId, SemanticsAction.moveCursorBackwardByCharacter, extendSelection);
await tester.pumpAndSettle();
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId, SemanticsAction.moveCursorBackwardByCharacter, extendSelection);
await tester.pumpAndSettle();
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId, SemanticsAction.moveCursorBackwardByCharacter, extendSelection);
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 4);
expect(controller.selection.extentOffset, 0);
await tester.pumpAndSettle();
expect(semantics, includesNodeWith(
value: 'test',
actions: <SemanticsAction>[
SemanticsAction.moveCursorForwardByCharacter,
],
));
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId, SemanticsAction.moveCursorForwardByCharacter, doNotExtendSelection);
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 1);
expect(controller.selection.extentOffset, 1);
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId, SemanticsAction.moveCursorForwardByCharacter, extendSelection);
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 1);
expect(controller.selection.extentOffset, 2);
semantics.dispose(); semantics.dispose();
}); });
} }
...@@ -388,8 +388,8 @@ void main() { ...@@ -388,8 +388,8 @@ void main() {
onScrollDown: () => performedActions.add(SemanticsAction.scrollDown), onScrollDown: () => performedActions.add(SemanticsAction.scrollDown),
onIncrease: () => performedActions.add(SemanticsAction.increase), onIncrease: () => performedActions.add(SemanticsAction.increase),
onDecrease: () => performedActions.add(SemanticsAction.decrease), onDecrease: () => performedActions.add(SemanticsAction.decrease),
onMoveCursorForwardByCharacter: () => performedActions.add(SemanticsAction.moveCursorForwardByCharacter), onMoveCursorForwardByCharacter: (bool _) => performedActions.add(SemanticsAction.moveCursorForwardByCharacter),
onMoveCursorBackwardByCharacter: () => performedActions.add(SemanticsAction.moveCursorBackwardByCharacter), onMoveCursorBackwardByCharacter: (bool _) => performedActions.add(SemanticsAction.moveCursorBackwardByCharacter),
) )
); );
...@@ -412,7 +412,14 @@ void main() { ...@@ -412,7 +412,14 @@ void main() {
final SemanticsOwner semanticsOwner = tester.binding.pipelineOwner.semanticsOwner; final SemanticsOwner semanticsOwner = tester.binding.pipelineOwner.semanticsOwner;
int expectedLength = 1; int expectedLength = 1;
for (SemanticsAction action in allActions) { for (SemanticsAction action in allActions) {
semanticsOwner.performAction(expectedId, action); switch (action) {
case SemanticsAction.moveCursorBackwardByCharacter:
case SemanticsAction.moveCursorForwardByCharacter:
semanticsOwner.performAction(expectedId, action, true);
break;
default:
semanticsOwner.performAction(expectedId, action);
}
expect(performedActions.length, expectedLength); expect(performedActions.length, expectedLength);
expect(performedActions.last, action); expect(performedActions.last, action);
expectedLength += 1; expectedLength += 1;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment