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 {
config.isSemanticBoundary = isInteractive;
if (isInteractive) {
config.addAction(SemanticsAction.increase, _increaseAction);
config.addAction(SemanticsAction.decrease, _decreaseAction);
config.onIncrease = _increaseAction;
config.onDecrease = _decreaseAction;
}
}
......
......@@ -380,7 +380,7 @@ class _RenderCupertinoSwitch extends RenderConstrainedBox {
config.isSemanticBoundary = isInteractive;
if (isInteractive)
config.addAction(SemanticsAction.tap, _handleTap);
config.onTap = _handleTap;
config.isChecked = _value;
}
......
......@@ -713,8 +713,8 @@ class _RenderSlider extends RenderBox {
config.isSemanticBoundary = isInteractive;
if (isInteractive) {
config.addAction(SemanticsAction.increase, _increaseAction);
config.addAction(SemanticsAction.decrease, _decreaseAction);
config.onIncrease = _increaseAction;
config.onDecrease = _decreaseAction;
}
}
......
......@@ -288,7 +288,7 @@ abstract class RenderToggleable extends RenderConstrainedBox {
config.isSemanticBoundary = isInteractive;
if (isInteractive)
config.addAction(SemanticsAction.tap, _handleTap);
config.onTap = _handleTap;
config.isChecked = _value;
}
......
......@@ -4,6 +4,7 @@
import 'dart:async';
import 'dart:developer';
import 'dart:typed_data';
import 'dart:ui' as ui show window;
import 'package:flutter/foundation.dart';
......@@ -187,8 +188,12 @@ abstract class RendererBinding extends BindingBase with ServicesBinding, Schedul
}
}
void _handleSemanticsAction(int id, SemanticsAction action) {
_pipelineOwner.semanticsOwner?.performAction(id, action);
void _handleSemanticsAction(int id, SemanticsAction action, ByteData args) {
_pipelineOwner.semanticsOwner?.performAction(
id,
action,
args != null ? const StandardMessageCodec().decodeMessage(args) : null,
);
}
void _handleSemanticsOwnerCreated() {
......
......@@ -823,34 +823,34 @@ class RenderCustomPaint extends RenderProxyBox {
config.textDirection = properties.textDirection;
}
if (properties.onTap != null) {
config.addAction(SemanticsAction.tap, properties.onTap);
config.onTap = properties.onTap;
}
if (properties.onLongPress != null) {
config.addAction(SemanticsAction.longPress, properties.onLongPress);
config.onLongPress = properties.onLongPress;
}
if (properties.onScrollLeft != null) {
config.addAction(SemanticsAction.scrollLeft, properties.onScrollLeft);
config.onScrollLeft = properties.onScrollLeft;
}
if (properties.onScrollRight != null) {
config.addAction(SemanticsAction.scrollRight, properties.onScrollRight);
config.onScrollRight = properties.onScrollRight;
}
if (properties.onScrollUp != null) {
config.addAction(SemanticsAction.scrollUp, properties.onScrollUp);
config.onScrollUp = properties.onScrollUp;
}
if (properties.onScrollDown != null) {
config.addAction(SemanticsAction.scrollDown, properties.onScrollDown);
config.onScrollDown = properties.onScrollDown;
}
if (properties.onIncrease != null) {
config.addAction(SemanticsAction.increase, properties.onIncrease);
config.onIncrease = properties.onIncrease;
}
if (properties.onDecrease != null) {
config.addAction(SemanticsAction.decrease, properties.onDecrease);
config.onDecrease = properties.onDecrease;
}
if (properties.onMoveCursorForwardByCharacter != null) {
config.addAction(SemanticsAction.moveCursorForwardByCharacter, properties.onMoveCursorForwardByCharacter);
config.onMoveCursorForwardByCharacter = properties.onMoveCursorForwardByCharacter;
}
if (properties.onMoveCursorBackwardByCharacter != null) {
config.addAction(SemanticsAction.moveCursorBackwardByCharacter, properties.onMoveCursorBackwardByCharacter);
config.onMoveCursorBackwardByCharacter = properties.onMoveCursorBackwardByCharacter;
}
newChild.updateWith(
......
......@@ -349,28 +349,31 @@ class RenderEditable extends RenderBox {
..isTextField = true;
if (_selection?.isValid == true) {
if (_textPainter.getOffsetBefore(_selection.extentOffset) != null) {
config.addAction(SemanticsAction.moveCursorBackwardByCharacter, () {
final int offset = _textPainter.getOffsetBefore(_selection.extentOffset);
if (offset == null)
if (_textPainter.getOffsetBefore(_selection.extentOffset) != null)
config.onMoveCursorBackwardByCharacter = _handleMoveCursorBackwardByCharacter;
if (_textPainter.getOffsetAfter(_selection.extentOffset) != null)
config.onMoveCursorForwardByCharacter = _handleMoveCursorForwardByCharacter;
}
}
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.collapsed(offset: offset), this, SelectionChangedCause.keyboard,
new TextSelection(baseOffset: baseOffset, extentOffset: extentOffset), this, SelectionChangedCause.keyboard,
);
});
}
if (_textPainter.getOffsetAfter(_selection.extentOffset) != null) {
config.addAction(SemanticsAction.moveCursorForwardByCharacter, () {
final int offset = _textPainter.getOffsetAfter(_selection.extentOffset);
if (offset == null)
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.collapsed(offset: offset), this, SelectionChangedCause.keyboard,
new TextSelection(baseOffset: baseOffset, extentOffset: extentOffset), this, SelectionChangedCause.keyboard,
);
});
}
}
}
@override
......
......@@ -2140,7 +2140,7 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
/// void describeSemanticsConfiguration(SemanticsConfiguration config) {
/// super.describeSemanticsConfiguration(config);
/// config
/// ..addAction(SemanticsAction.tap, _handleTap)
/// ..onTap = _handleTap
/// ..label = 'I am a button'
/// ..isButton = true;
/// }
......
......@@ -2685,27 +2685,26 @@ class RenderSemanticsGestureHandler extends RenderProxyBox {
config.explicitChildNodes = onHorizontalDragUpdate != null
|| onVerticalDragUpdate != null;
final Map<SemanticsAction, VoidCallback> actions = <SemanticsAction, VoidCallback>{};
if (onTap != null)
actions[SemanticsAction.tap] = onTap;
if (onLongPress != null)
actions[SemanticsAction.longPress] = onLongPress;
if (onTap != null && _isValidAction(SemanticsAction.tap))
config.onTap = onTap;
if (onLongPress != null && _isValidAction(SemanticsAction.longPress))
config.onLongPress = onLongPress;
if (onHorizontalDragUpdate != null) {
actions[SemanticsAction.scrollRight] = _performSemanticScrollRight;
actions[SemanticsAction.scrollLeft] = _performSemanticScrollLeft;
if (_isValidAction(SemanticsAction.scrollRight))
config.onScrollRight = _performSemanticScrollRight;
if (_isValidAction(SemanticsAction.scrollLeft))
config.onScrollLeft = _performSemanticScrollLeft;
}
if (onVerticalDragUpdate != null) {
actions[SemanticsAction.scrollUp] = _performSemanticScrollUp;
actions[SemanticsAction.scrollDown] = _performSemanticScrollDown;
if (_isValidAction(SemanticsAction.scrollUp))
config.onScrollUp = _performSemanticScrollUp;
if (_isValidAction(SemanticsAction.scrollDown))
config.onScrollDown = _performSemanticScrollDown;
}
final Iterable<SemanticsAction> actionsToAdd = validActions ?? actions.keys;
for (SemanticsAction action in actionsToAdd) {
final VoidCallback handler = actions[action];
if (handler != null)
config.addAction(action, handler);
}
bool _isValidAction(SemanticsAction action) {
return validActions == null || validActions.contains(action);
}
SemanticsNode _innerNode;
......@@ -2830,8 +2829,8 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
VoidCallback onScrollDown,
VoidCallback onIncrease,
VoidCallback onDecrease,
VoidCallback onMoveCursorForwardByCharacter,
VoidCallback onMoveCursorBackwardByCharacter,
MoveCursorHandler onMoveCursorForwardByCharacter,
MoveCursorHandler onMoveCursorBackwardByCharacter,
}) : assert(container != null),
_container = container,
_explicitChildNodes = explicitChildNodes,
......@@ -3173,9 +3172,9 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
///
/// TalkBack users can trigger this by pressing the volume up key while the
/// input focus is in a text field.
VoidCallback get onMoveCursorForwardByCharacter => _onMoveCursorForwardByCharacter;
VoidCallback _onMoveCursorForwardByCharacter;
set onMoveCursorForwardByCharacter(VoidCallback handler) {
MoveCursorHandler get onMoveCursorForwardByCharacter => _onMoveCursorForwardByCharacter;
MoveCursorHandler _onMoveCursorForwardByCharacter;
set onMoveCursorForwardByCharacter(MoveCursorHandler handler) {
if (_onMoveCursorForwardByCharacter == handler)
return;
final bool hadValue = _onMoveCursorForwardByCharacter != null;
......@@ -3191,9 +3190,9 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
///
/// TalkBack users can trigger this by pressing the volume down key while the
/// input focus is in a text field.
VoidCallback get onMoveCursorBackwardByCharacter => _onMoveCursorBackwardByCharacter;
VoidCallback _onMoveCursorBackwardByCharacter;
set onMoveCursorBackwardByCharacter(VoidCallback handler) {
MoveCursorHandler get onMoveCursorBackwardByCharacter => _onMoveCursorBackwardByCharacter;
MoveCursorHandler _onMoveCursorBackwardByCharacter;
set onMoveCursorBackwardByCharacter(MoveCursorHandler handler) {
if (_onMoveCursorBackwardByCharacter == handler)
return;
final bool hadValue = _onMoveCursorBackwardByCharacter != null;
......@@ -3230,25 +3229,25 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
// ones to ensure that changing a user provided handler from a non-null to
// another non-null value doesn't require a semantics update.
if (onTap != null)
config.addAction(SemanticsAction.tap, _performTap);
config.onTap = _performTap;
if (onLongPress != null)
config.addAction(SemanticsAction.longPress, _performLongPress);
config.onLongPress = _performLongPress;
if (onScrollLeft != null)
config.addAction(SemanticsAction.scrollLeft, _performScrollLeft);
config.onScrollLeft = _performScrollLeft;
if (onScrollRight != null)
config.addAction(SemanticsAction.scrollRight, _performScrollRight);
config.onScrollRight = _performScrollRight;
if (onScrollUp != null)
config.addAction(SemanticsAction.scrollUp, _performScrollUp);
config.onScrollUp = _performScrollUp;
if (onScrollDown != null)
config.addAction(SemanticsAction.scrollDown, _performScrollDown);
config.onScrollDown = _performScrollDown;
if (onIncrease != null)
config.addAction(SemanticsAction.increase, _performIncrease);
config.onIncrease = _performIncrease;
if (onDecrease != null)
config.addAction(SemanticsAction.decrease, _performDecrease);
config.onDecrease = _performDecrease;
if (onMoveCursorForwardByCharacter != null)
config.addAction(SemanticsAction.moveCursorForwardByCharacter, _performMoveCursorForwardByCharacter);
config.onMoveCursorForwardByCharacter = _performMoveCursorForwardByCharacter;
if (onMoveCursorBackwardByCharacter != null)
config.addAction(SemanticsAction.moveCursorBackwardByCharacter, _performMoveCursorBackwardByCharacter);
config.onMoveCursorBackwardByCharacter = _performMoveCursorBackwardByCharacter;
}
void _performTap() {
......@@ -3291,14 +3290,14 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
onDecrease();
}
void _performMoveCursorForwardByCharacter() {
void _performMoveCursorForwardByCharacter(bool extendSelection) {
if (onMoveCursorForwardByCharacter != null)
onMoveCursorForwardByCharacter();
onMoveCursorForwardByCharacter(extendSelection);
}
void _performMoveCursorBackwardByCharacter() {
void _performMoveCursorBackwardByCharacter(bool extendSelection) {
if (onMoveCursorBackwardByCharacter != null)
onMoveCursorBackwardByCharacter();
onMoveCursorBackwardByCharacter(extendSelection);
}
}
......
......@@ -25,6 +25,14 @@ export 'semantics_event.dart';
/// Used by [SemanticsNode.visitChildren].
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].
///
/// Tags can be interpreted by the parent of a [SemanticsNode]
......@@ -445,7 +453,7 @@ class SemanticsProperties extends DiagnosticableTree {
///
/// TalkBack users can trigger this by pressing the volume up key while the
/// input focus is in a text field.
final VoidCallback onMoveCursorForwardByCharacter;
final MoveCursorHandler onMoveCursorForwardByCharacter;
/// The handler for [SemanticsAction.onMoveCursorBackwardByCharacter].
///
......@@ -454,7 +462,7 @@ class SemanticsProperties extends DiagnosticableTree {
///
/// TalkBack users can trigger this by pressing the volume down key while the
/// input focus is in a text field.
final VoidCallback onMoveCursorBackwardByCharacter;
final MoveCursorHandler onMoveCursorBackwardByCharacter;
@override
void debugFillProperties(DiagnosticPropertiesBuilder description) {
......@@ -824,7 +832,7 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
// TAGS, LABELS, ACTIONS
Map<SemanticsAction, VoidCallback> _actions = _kEmptyConfig._actions;
Map<SemanticsAction, _SemanticsActionHandler> _actions = _kEmptyConfig._actions;
int _actionsAsBits = _kEmptyConfig._actionsAsBits;
......@@ -912,7 +920,7 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
_hint = config.hint;
_flags = config._flags;
_textDirection = config.textDirection;
_actions = new Map<SemanticsAction, VoidCallback>.from(config._actions);
_actions = new Map<SemanticsAction, _SemanticsActionHandler>.from(config._actions);
_actionsAsBits = config._actionsAsBits;
_mergeAllDescendantsIntoThisNode = config.isMergingSemanticsOfDescendants;
_replaceChildren(childrenInInversePaintOrder ?? const <SemanticsNode>[]);
......@@ -1213,7 +1221,7 @@ class SemanticsOwner extends ChangeNotifier {
notifyListeners();
}
VoidCallback _getSemanticsActionHandlerForId(int id, SemanticsAction action) {
_SemanticsActionHandler _getSemanticsActionHandlerForId(int id, SemanticsAction action) {
SemanticsNode result = _nodes[id];
if (result != null && result.isPartOfNodeMerging && !result._canPerformAction(action)) {
result._visitDescendants((SemanticsNode node) {
......@@ -1233,11 +1241,14 @@ class SemanticsOwner extends ChangeNotifier {
///
/// If the [SemanticsNode] has not indicated that it can perform the action,
/// 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);
final VoidCallback handler = _getSemanticsActionHandlerForId(id, action);
final _SemanticsActionHandler handler = _getSemanticsActionHandlerForId(id, action);
if (handler != null) {
handler();
handler(args);
return;
}
......@@ -1246,7 +1257,7 @@ class SemanticsOwner extends ChangeNotifier {
_nodes[id]._showOnScreen();
}
VoidCallback _getSemanticsActionHandlerForPosition(SemanticsNode node, Offset position, SemanticsAction action) {
_SemanticsActionHandler _getSemanticsActionHandlerForPosition(SemanticsNode node, Offset position, SemanticsAction action) {
if (node.transform != null) {
final Matrix4 inverse = new Matrix4.identity();
if (inverse.copyInverse(node.transform) == 0.0)
......@@ -1268,7 +1279,7 @@ class SemanticsOwner extends ChangeNotifier {
}
if (node.hasChildren) {
for (SemanticsNode child in node._children.reversed) {
final VoidCallback handler = _getSemanticsActionHandlerForPosition(child, position, action);
final _SemanticsActionHandler handler = _getSemanticsActionHandlerForPosition(child, position, action);
if (handler != null)
return handler;
}
......@@ -1280,14 +1291,17 @@ class SemanticsOwner extends ChangeNotifier {
///
/// If the [SemanticsNode] has not indicated that it can perform the action,
/// 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);
final SemanticsNode node = rootSemanticsNode;
if (node == null)
return;
final VoidCallback handler = _getSemanticsActionHandlerForPosition(node, position, action);
final _SemanticsActionHandler handler = _getSemanticsActionHandlerForPosition(node, position, action);
if (handler != null)
handler();
handler(args);
}
@override
......@@ -1366,7 +1380,7 @@ class SemanticsConfiguration {
bool _isMergingDescendantsIntoOneNode = false;
set isMergingDescendantsIntoOneNode(bool value) {
assert(isSemanticBoundary);
_isMergingDescendantsIntoOneNode = isMergingDescendantsIntoOneNode;
_isMergingDescendantsIntoOneNode = value;
}
// SEMANTIC ANNOTATIONS
......@@ -1386,27 +1400,233 @@ class SemanticsConfiguration {
/// See also:
///
/// * [addAction] to add an action.
final Map<SemanticsAction, VoidCallback> _actions = <SemanticsAction, VoidCallback>{};
final Map<SemanticsAction, _SemanticsActionHandler> _actions = <SemanticsAction, _SemanticsActionHandler>{};
int _actionsAsBits = 0;
/// Adds an `action` to the semantics tree.
///
/// Whenever the user performs `action` the provided `handler` is called.
void addAction(SemanticsAction action, VoidCallback handler) {
/// The provided `handler` is called to respond to the user triggered
/// `action`.
void _addAction(SemanticsAction action, _SemanticsActionHandler handler) {
assert(handler != null);
_actions[action] = handler;
_actionsAsBits |= action.index;
_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
/// registered.
///
/// See also:
///
/// * [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
/// all of its descendants should be treated as one logical entity.
......@@ -1516,6 +1736,7 @@ class SemanticsConfiguration {
}
/// Whether the owning [RenderObject] is selected (true) or not (false).
bool get isSelected => _hasFlag(SemanticsFlags.isSelected);
set isSelected(bool value) {
_setFlag(SemanticsFlags.isSelected, value);
}
......@@ -1525,22 +1746,26 @@ class SemanticsConfiguration {
///
/// Do not set this to any value if the owning [RenderObject] doesn't have
/// Booleans state that can be controlled by the user.
bool get isChecked => _hasFlag(SemanticsFlags.hasCheckedState) && _hasFlag(SemanticsFlags.isChecked);
set isChecked(bool value) {
_setFlag(SemanticsFlags.hasCheckedState, true);
_setFlag(SemanticsFlags.isChecked, value);
}
/// Whether the owning [RenderObject] currently holds the user's focus.
bool get isFocused => _hasFlag(SemanticsFlags.isFocused);
set isFocused(bool value) {
_setFlag(SemanticsFlags.isFocused, value);
}
/// Whether the owning [RenderObject] is a button (true) or not (false).
bool get isButton => _hasFlag(SemanticsFlags.isButton);
set isButton(bool value) {
_setFlag(SemanticsFlags.isButton, value);
}
/// Whether the owning [RenderObject] is a text field.
bool get isTextField => _hasFlag(SemanticsFlags.isTextField);
set isTextField(bool value) {
_setFlag(SemanticsFlags.isTextField, value);
}
......@@ -1589,6 +1814,8 @@ class SemanticsConfiguration {
_hasBeenAnnotated = true;
}
bool _hasFlag(SemanticsFlags flag) => (_flags & flag.index) != 0;
// CONFIGURATION COMBINATION LOGIC
/// Whether this configuration is compatible with the provided `other`
......
......@@ -265,11 +265,18 @@ TextEditingValue _selectionAwareTextManipulation(
value.text.substring(selectionEndIndex)
);
manipulatedText = beforeSelection + inSelection + afterSelection;
if (value.selection.baseOffset > value.selection.extentOffset) {
manipulatedSelection = value.selection.copyWith(
baseOffset: beforeSelection.length + inSelection.length,
extentOffset: beforeSelection.length,
);
} else {
manipulatedSelection = value.selection.copyWith(
baseOffset: beforeSelection.length,
extentOffset: beforeSelection.length + inSelection.length,
);
}
}
return new TextEditingValue(
text: manipulatedText,
selection: manipulatedSelection ?? const TextSelection.collapsed(offset: -1),
......
......@@ -4726,8 +4726,8 @@ class Semantics extends SingleChildRenderObjectWidget {
VoidCallback onScrollDown,
VoidCallback onIncrease,
VoidCallback onDecrease,
VoidCallback onMoveCursorForwardByCharacter,
VoidCallback onMoveCursorBackwardByCharacter,
MoveCursorHandler onMoveCursorForwardByCharacter,
MoveCursorHandler onMoveCursorBackwardByCharacter,
}) : this.fromProperties(
key: key,
child: child,
......
......@@ -65,19 +65,19 @@ void main() {
TestRender middle;
final TestRender root = new TestRender(
action: SemanticsAction.tap,
hasTapAction: true,
isSemanticBoundary: true,
child: new TestRender(
action: SemanticsAction.longPress,
hasLongPressAction: true,
isSemanticBoundary: false,
child: middle = new TestRender(
action: SemanticsAction.scrollLeft,
hasScrollLeftAction: true,
isSemanticBoundary: false,
child: new TestRender(
action: SemanticsAction.scrollRight,
hasScrollRightAction: true,
isSemanticBoundary: false,
child: new TestRender(
action: SemanticsAction.scrollUp,
hasScrollUpAction: true,
isSemanticBoundary: true,
)
)
......@@ -91,7 +91,9 @@ void main() {
int expectedActions = SemanticsAction.tap.index | SemanticsAction.longPress.index | SemanticsAction.scrollLeft.index | SemanticsAction.scrollRight.index;
expect(root.debugSemantics.getSemanticsData().actions, expectedActions);
middle.action = SemanticsAction.scrollDown;
middle
..hasScrollLeftAction = false
..hasScrollDownAction = true;
middle.markNeedsSemanticsUpdate();
pumpFrame(phase: EnginePhase.flushSemantics);
......@@ -204,9 +206,9 @@ void main() {
final SemanticsConfiguration config = new SemanticsConfiguration()
..isMergingSemanticsOfDescendants = true
..addAction(SemanticsAction.scrollUp, () { })
..addAction(SemanticsAction.longPress, () { })
..addAction(SemanticsAction.showOnScreen, () { })
..onScrollUp = () { }
..onLongPress = () { }
..onShowOnScreen = () { }
..isChecked = false
..isSelected = true
..isButton = true
......@@ -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])',
);
});
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 {
TestRender({ this.action, this.isSemanticBoundary, RenderObject child }) : super(child);
final bool isSemanticBoundary;
TestRender({
this.hasTapAction: false,
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
void describeSemanticsConfiguration(SemanticsConfiguration config) {
super.describeSemanticsConfiguration(config);
config
..isSemanticBoundary = isSemanticBoundary
..addAction(action, () { });
config.isSemanticBoundary = isSemanticBoundary;
if (hasTapAction)
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() {
testWidgets('can move cursor with a11y means', (WidgetTester tester) async {
final SemanticsTester semantics = new SemanticsTester(tester);
const bool doNotExtendSelection = false;
controller.text = 'test';
controller.selection = new TextSelection.collapsed(offset: controller.text.length);
......@@ -440,7 +441,7 @@ void main() {
expect(controller.selection.baseOffset, 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();
expect(controller.selection.baseOffset, 3);
......@@ -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();
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId, SemanticsAction.moveCursorBackwardByCharacter);
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId, SemanticsAction.moveCursorBackwardByCharacter, doNotExtendSelection);
await tester.pumpAndSettle();
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId, SemanticsAction.moveCursorBackwardByCharacter);
tester.binding.pipelineOwner.semanticsOwner.performAction(semanticsId, SemanticsAction.moveCursorBackwardByCharacter, doNotExtendSelection);
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 0);
......@@ -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();
});
}
......@@ -388,8 +388,8 @@ void main() {
onScrollDown: () => performedActions.add(SemanticsAction.scrollDown),
onIncrease: () => performedActions.add(SemanticsAction.increase),
onDecrease: () => performedActions.add(SemanticsAction.decrease),
onMoveCursorForwardByCharacter: () => performedActions.add(SemanticsAction.moveCursorForwardByCharacter),
onMoveCursorBackwardByCharacter: () => performedActions.add(SemanticsAction.moveCursorBackwardByCharacter),
onMoveCursorForwardByCharacter: (bool _) => performedActions.add(SemanticsAction.moveCursorForwardByCharacter),
onMoveCursorBackwardByCharacter: (bool _) => performedActions.add(SemanticsAction.moveCursorBackwardByCharacter),
)
);
......@@ -412,7 +412,14 @@ void main() {
final SemanticsOwner semanticsOwner = tester.binding.pipelineOwner.semanticsOwner;
int expectedLength = 1;
for (SemanticsAction action in allActions) {
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.last, action);
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