Unverified Commit 671c1101 authored by Michael Goderbauer's avatar Michael Goderbauer Committed by GitHub

Change text selection (or cursor position) via a11y (#14275)

Roll engine to 7c34dfafc9acece1a9438f206bfbb0a9bedba3bf
parent c23509e9
4c82c566edf394a5cfc237a266aea5bd37a6c172 7c34dfafc9acece1a9438f206bfbb0a9bedba3bf
...@@ -865,6 +865,9 @@ class RenderCustomPaint extends RenderProxyBox { ...@@ -865,6 +865,9 @@ class RenderCustomPaint extends RenderProxyBox {
if (properties.onMoveCursorBackwardByCharacter != null) { if (properties.onMoveCursorBackwardByCharacter != null) {
config.onMoveCursorBackwardByCharacter = properties.onMoveCursorBackwardByCharacter; config.onMoveCursorBackwardByCharacter = properties.onMoveCursorBackwardByCharacter;
} }
if (properties.onSetSelection != null) {
config.onSetSelection = properties.onSetSelection;
}
newChild.updateWith( newChild.updateWith(
config: config, config: config,
......
...@@ -356,6 +356,9 @@ class RenderEditable extends RenderBox { ...@@ -356,6 +356,9 @@ class RenderEditable extends RenderBox {
..isFocused = hasFocus ..isFocused = hasFocus
..isTextField = true; ..isTextField = true;
if (hasFocus)
config.onSetSelection = _handleSetSelection;
if (_selection?.isValid == true) { if (_selection?.isValid == true) {
config.textSelection = _selection; config.textSelection = _selection;
if (_textPainter.getOffsetBefore(_selection.extentOffset) != null) if (_textPainter.getOffsetBefore(_selection.extentOffset) != null)
...@@ -365,6 +368,10 @@ class RenderEditable extends RenderBox { ...@@ -365,6 +368,10 @@ class RenderEditable extends RenderBox {
} }
} }
void _handleSetSelection(TextSelection selection) {
onSelectionChanged(selection, this, SelectionChangedCause.keyboard);
}
void _handleMoveCursorForwardByCharacter(bool extentSelection) { void _handleMoveCursorForwardByCharacter(bool extentSelection) {
final int extentOffset = _textPainter.getOffsetAfter(_selection.extentOffset); final int extentOffset = _textPainter.getOffsetAfter(_selection.extentOffset);
if (extentOffset == null) if (extentOffset == null)
......
...@@ -3017,6 +3017,7 @@ class RenderSemanticsAnnotations extends RenderProxyBox { ...@@ -3017,6 +3017,7 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
VoidCallback onDecrease, VoidCallback onDecrease,
MoveCursorHandler onMoveCursorForwardByCharacter, MoveCursorHandler onMoveCursorForwardByCharacter,
MoveCursorHandler onMoveCursorBackwardByCharacter, MoveCursorHandler onMoveCursorBackwardByCharacter,
SetSelectionHandler onSetSelection,
}) : assert(container != null), }) : assert(container != null),
_container = container, _container = container,
_explicitChildNodes = explicitChildNodes, _explicitChildNodes = explicitChildNodes,
...@@ -3040,6 +3041,7 @@ class RenderSemanticsAnnotations extends RenderProxyBox { ...@@ -3040,6 +3041,7 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
_onDecrease = onDecrease, _onDecrease = onDecrease,
_onMoveCursorForwardByCharacter = onMoveCursorForwardByCharacter, _onMoveCursorForwardByCharacter = onMoveCursorForwardByCharacter,
_onMoveCursorBackwardByCharacter = onMoveCursorBackwardByCharacter, _onMoveCursorBackwardByCharacter = onMoveCursorBackwardByCharacter,
_onSetSelection = onSetSelection,
super(child); super(child);
/// If 'container' is true, this [RenderObject] will introduce a new /// If 'container' is true, this [RenderObject] will introduce a new
...@@ -3399,6 +3401,24 @@ class RenderSemanticsAnnotations extends RenderProxyBox { ...@@ -3399,6 +3401,24 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
markNeedsSemanticsUpdate(); markNeedsSemanticsUpdate();
} }
/// The handler for [SemanticsAction.setSelection].
///
/// This handler is invoked when the user either wants to change the currently
/// selected text in a text field or change the position of the cursor.
///
/// TalkBack users can trigger this handler by selecting "Move cursor to
/// beginning/end" or "Select all" from the local context menu.
SetSelectionHandler get onSetSelection => _onSetSelection;
SetSelectionHandler _onSetSelection;
set onSetSelection(SetSelectionHandler handler) {
if (_onSetSelection == handler)
return;
final bool hadValue = _onSetSelection != null;
_onSetSelection = handler;
if ((handler != null) != hadValue)
markNeedsSemanticsUpdate();
}
@override @override
void describeSemanticsConfiguration(SemanticsConfiguration config) { void describeSemanticsConfiguration(SemanticsConfiguration config) {
super.describeSemanticsConfiguration(config); super.describeSemanticsConfiguration(config);
...@@ -3448,6 +3468,8 @@ class RenderSemanticsAnnotations extends RenderProxyBox { ...@@ -3448,6 +3468,8 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
config.onMoveCursorForwardByCharacter = _performMoveCursorForwardByCharacter; config.onMoveCursorForwardByCharacter = _performMoveCursorForwardByCharacter;
if (onMoveCursorBackwardByCharacter != null) if (onMoveCursorBackwardByCharacter != null)
config.onMoveCursorBackwardByCharacter = _performMoveCursorBackwardByCharacter; config.onMoveCursorBackwardByCharacter = _performMoveCursorBackwardByCharacter;
if (onSetSelection != null)
config.onSetSelection = _performSetSelection;
} }
void _performTap() { void _performTap() {
...@@ -3499,6 +3521,11 @@ class RenderSemanticsAnnotations extends RenderProxyBox { ...@@ -3499,6 +3521,11 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
if (onMoveCursorBackwardByCharacter != null) if (onMoveCursorBackwardByCharacter != null)
onMoveCursorBackwardByCharacter(extendSelection); onMoveCursorBackwardByCharacter(extendSelection);
} }
void _performSetSelection(TextSelection selection) {
if (onSetSelection != null)
onSetSelection(selection);
}
} }
/// Causes the semantics of all earlier render objects below the same semantic /// Causes the semantics of all earlier render objects below the same semantic
......
...@@ -31,6 +31,10 @@ typedef bool SemanticsNodeVisitor(SemanticsNode node); ...@@ -31,6 +31,10 @@ typedef bool SemanticsNodeVisitor(SemanticsNode node);
/// current selection or (if nothing is currently selected) start a selection. /// current selection or (if nothing is currently selected) start a selection.
typedef void MoveCursorHandler(bool extendSelection); typedef void MoveCursorHandler(bool extendSelection);
/// Signature for the [SemanticsAction.setSelection] handlers to change the
/// text selection (or re-position the cursor) to `selection`.
typedef void SetSelectionHandler(TextSelection selection);
typedef void _SemanticsActionHandler(dynamic args); typedef void _SemanticsActionHandler(dynamic args);
/// A tag for a [SemanticsNode]. /// A tag for a [SemanticsNode].
...@@ -275,6 +279,7 @@ class SemanticsProperties extends DiagnosticableTree { ...@@ -275,6 +279,7 @@ class SemanticsProperties extends DiagnosticableTree {
this.onDecrease, this.onDecrease,
this.onMoveCursorForwardByCharacter, this.onMoveCursorForwardByCharacter,
this.onMoveCursorBackwardByCharacter, this.onMoveCursorBackwardByCharacter,
this.onSetSelection,
}); });
/// If non-null, indicates that this subtree represents something that can be /// If non-null, indicates that this subtree represents something that can be
...@@ -485,6 +490,15 @@ class SemanticsProperties extends DiagnosticableTree { ...@@ -485,6 +490,15 @@ class SemanticsProperties extends DiagnosticableTree {
/// input focus is in a text field. /// input focus is in a text field.
final MoveCursorHandler onMoveCursorBackwardByCharacter; final MoveCursorHandler onMoveCursorBackwardByCharacter;
/// The handler for [SemanticsAction.setSelection].
///
/// This handler is invoked when the user either wants to change the currently
/// selected text in a text field or change the position of the cursor.
///
/// TalkBack users can trigger this handler by selecting "Move cursor to
/// beginning/end" or "Select all" from the local context menu.
final SetSelectionHandler onSetSelection;
@override @override
void debugFillProperties(DiagnosticPropertiesBuilder description) { void debugFillProperties(DiagnosticPropertiesBuilder description) {
super.debugFillProperties(description); super.debugFillProperties(description);
...@@ -1658,6 +1672,28 @@ class SemanticsConfiguration { ...@@ -1658,6 +1672,28 @@ class SemanticsConfiguration {
_onMoveCursorBackwardByCharacter = value; _onMoveCursorBackwardByCharacter = value;
} }
/// The handler for [SemanticsAction.setSelection].
///
/// This handler is invoked when the user either wants to change the currently
/// selected text in a text field or change the position of the cursor.
///
/// TalkBack users can trigger this handler by selecting "Move cursor to
/// beginning/end" or "Select all" from the local context menu.
SetSelectionHandler get onSetSelection => _onSetSelection;
SetSelectionHandler _onSetSelection;
set onSetSelection(SetSelectionHandler value) {
assert(value != null);
_addAction(SemanticsAction.setSelection, (dynamic args) {
final Map<String, int> selection = args;
assert(selection != null && selection['base'] != null && selection['extent'] != null);
value(new TextSelection(
baseOffset: selection['base'],
extentOffset: selection['extent'],
));
});
_onSetSelection = 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.
/// ///
......
...@@ -4854,6 +4854,7 @@ class Semantics extends SingleChildRenderObjectWidget { ...@@ -4854,6 +4854,7 @@ class Semantics extends SingleChildRenderObjectWidget {
VoidCallback onDecrease, VoidCallback onDecrease,
MoveCursorHandler onMoveCursorForwardByCharacter, MoveCursorHandler onMoveCursorForwardByCharacter,
MoveCursorHandler onMoveCursorBackwardByCharacter, MoveCursorHandler onMoveCursorBackwardByCharacter,
SetSelectionHandler onSetSelection,
}) : this.fromProperties( }) : this.fromProperties(
key: key, key: key,
child: child, child: child,
...@@ -4880,6 +4881,7 @@ class Semantics extends SingleChildRenderObjectWidget { ...@@ -4880,6 +4881,7 @@ class Semantics extends SingleChildRenderObjectWidget {
onDecrease: onDecrease, onDecrease: onDecrease,
onMoveCursorForwardByCharacter: onMoveCursorForwardByCharacter, onMoveCursorForwardByCharacter: onMoveCursorForwardByCharacter,
onMoveCursorBackwardByCharacter: onMoveCursorBackwardByCharacter, onMoveCursorBackwardByCharacter: onMoveCursorBackwardByCharacter,
onSetSelection: onSetSelection,
), ),
); );
...@@ -4948,6 +4950,7 @@ class Semantics extends SingleChildRenderObjectWidget { ...@@ -4948,6 +4950,7 @@ class Semantics extends SingleChildRenderObjectWidget {
onDecrease: properties.onDecrease, onDecrease: properties.onDecrease,
onMoveCursorForwardByCharacter: properties.onMoveCursorForwardByCharacter, onMoveCursorForwardByCharacter: properties.onMoveCursorForwardByCharacter,
onMoveCursorBackwardByCharacter: properties.onMoveCursorBackwardByCharacter, onMoveCursorBackwardByCharacter: properties.onMoveCursorBackwardByCharacter,
onSetSelection: properties.onSetSelection,
); );
} }
...@@ -4986,7 +4989,8 @@ class Semantics extends SingleChildRenderObjectWidget { ...@@ -4986,7 +4989,8 @@ class Semantics extends SingleChildRenderObjectWidget {
..onIncrease = properties.onIncrease ..onIncrease = properties.onIncrease
..onDecrease = properties.onDecrease ..onDecrease = properties.onDecrease
..onMoveCursorForwardByCharacter = properties.onMoveCursorForwardByCharacter ..onMoveCursorForwardByCharacter = properties.onMoveCursorForwardByCharacter
..onMoveCursorBackwardByCharacter = properties.onMoveCursorForwardByCharacter; ..onMoveCursorBackwardByCharacter = properties.onMoveCursorForwardByCharacter
..onSetSelection = properties.onSetSelection;
} }
@override @override
......
...@@ -1761,7 +1761,7 @@ void main() { ...@@ -1761,7 +1761,7 @@ void main() {
child: new TextField( child: new TextField(
key: key, key: key,
controller: controller, controller: controller,
) ),
), ),
); );
...@@ -1812,6 +1812,7 @@ void main() { ...@@ -1812,6 +1812,7 @@ void main() {
actions: <SemanticsAction>[ actions: <SemanticsAction>[
SemanticsAction.tap, SemanticsAction.tap,
SemanticsAction.moveCursorBackwardByCharacter, SemanticsAction.moveCursorBackwardByCharacter,
SemanticsAction.setSelection,
], ],
flags: <SemanticsFlag>[ flags: <SemanticsFlag>[
SemanticsFlag.isTextField, SemanticsFlag.isTextField,
...@@ -1835,6 +1836,7 @@ void main() { ...@@ -1835,6 +1836,7 @@ void main() {
SemanticsAction.tap, SemanticsAction.tap,
SemanticsAction.moveCursorBackwardByCharacter, SemanticsAction.moveCursorBackwardByCharacter,
SemanticsAction.moveCursorForwardByCharacter, SemanticsAction.moveCursorForwardByCharacter,
SemanticsAction.setSelection,
], ],
flags: <SemanticsFlag>[ flags: <SemanticsFlag>[
SemanticsFlag.isTextField, SemanticsFlag.isTextField,
...@@ -1858,6 +1860,7 @@ void main() { ...@@ -1858,6 +1860,7 @@ void main() {
actions: <SemanticsAction>[ actions: <SemanticsAction>[
SemanticsAction.tap, SemanticsAction.tap,
SemanticsAction.moveCursorForwardByCharacter, SemanticsAction.moveCursorForwardByCharacter,
SemanticsAction.setSelection,
], ],
flags: <SemanticsFlag>[ flags: <SemanticsFlag>[
SemanticsFlag.isTextField, SemanticsFlag.isTextField,
...@@ -1881,7 +1884,7 @@ void main() { ...@@ -1881,7 +1884,7 @@ void main() {
child: new TextField( child: new TextField(
key: key, key: key,
controller: controller, controller: controller,
) ),
), ),
); );
...@@ -1915,6 +1918,7 @@ void main() { ...@@ -1915,6 +1918,7 @@ void main() {
actions: <SemanticsAction>[ actions: <SemanticsAction>[
SemanticsAction.tap, SemanticsAction.tap,
SemanticsAction.moveCursorBackwardByCharacter, SemanticsAction.moveCursorBackwardByCharacter,
SemanticsAction.setSelection,
], ],
flags: <SemanticsFlag>[ flags: <SemanticsFlag>[
SemanticsFlag.isTextField, SemanticsFlag.isTextField,
...@@ -1938,6 +1942,96 @@ void main() { ...@@ -1938,6 +1942,96 @@ void main() {
SemanticsAction.tap, SemanticsAction.tap,
SemanticsAction.moveCursorBackwardByCharacter, SemanticsAction.moveCursorBackwardByCharacter,
SemanticsAction.moveCursorForwardByCharacter, SemanticsAction.moveCursorForwardByCharacter,
SemanticsAction.setSelection,
],
flags: <SemanticsFlag>[
SemanticsFlag.isTextField,
SemanticsFlag.isFocused,
],
),
],
), ignoreTransform: true, ignoreRect: true));
semantics.dispose();
});
testWidgets('TextField change selection with semantics', (WidgetTester tester) async {
final SemanticsTester semantics = new SemanticsTester(tester);
final SemanticsOwner semanticsOwner = tester.binding.pipelineOwner.semanticsOwner;
final TextEditingController controller = new TextEditingController()
..text = 'Hello';
final Key key = new UniqueKey();
await tester.pumpWidget(
overlay(
child: new TextField(
key: key,
controller: controller,
),
),
);
// Focus the text field
await tester.tap(find.byKey(key));
await tester.pump();
const int inputFieldId = 2;
expect(controller.selection, const TextSelection.collapsed(offset: 5, affinity: TextAffinity.upstream));
expect(semantics, hasSemantics(new TestSemantics.root(
children: <TestSemantics>[
new TestSemantics.rootChild(
id: inputFieldId,
value: 'Hello',
textSelection: const TextSelection.collapsed(offset: 5),
textDirection: TextDirection.ltr,
actions: <SemanticsAction>[
SemanticsAction.tap,
SemanticsAction.moveCursorBackwardByCharacter,
SemanticsAction.setSelection,
],
flags: <SemanticsFlag>[
SemanticsFlag.isTextField,
SemanticsFlag.isFocused,
],
),
],
), ignoreTransform: true, ignoreRect: true));
// move cursor back once
semanticsOwner.performAction(inputFieldId, SemanticsAction.setSelection, <String, int>{
'base': 4,
'extent': 4,
});
await tester.pump();
expect(controller.selection, const TextSelection.collapsed(offset: 4));
// move cursor to front
semanticsOwner.performAction(inputFieldId, SemanticsAction.setSelection, <String, int>{
'base': 0,
'extent': 0,
});
await tester.pump();
expect(controller.selection, const TextSelection.collapsed(offset: 0));
// select all
semanticsOwner.performAction(inputFieldId, SemanticsAction.setSelection, <String, int>{
'base': 0,
'extent': 5,
});
await tester.pump();
expect(controller.selection, const TextSelection(baseOffset: 0, extentOffset: 5));
expect(semantics, hasSemantics(new TestSemantics.root(
children: <TestSemantics>[
new TestSemantics.rootChild(
id: inputFieldId,
value: 'Hello',
textSelection: const TextSelection(baseOffset: 0, extentOffset: 5),
textDirection: TextDirection.ltr,
actions: <SemanticsAction>[
SemanticsAction.tap,
SemanticsAction.moveCursorBackwardByCharacter,
SemanticsAction.setSelection,
], ],
flags: <SemanticsFlag>[ flags: <SemanticsFlag>[
SemanticsFlag.isTextField, SemanticsFlag.isTextField,
...@@ -1950,4 +2044,5 @@ void main() { ...@@ -1950,4 +2044,5 @@ void main() {
semantics.dispose(); semantics.dispose();
}); });
} }
...@@ -383,6 +383,7 @@ void main() { ...@@ -383,6 +383,7 @@ void main() {
value: 'test', value: 'test',
actions: <SemanticsAction>[ actions: <SemanticsAction>[
SemanticsAction.moveCursorBackwardByCharacter, SemanticsAction.moveCursorBackwardByCharacter,
SemanticsAction.setSelection,
], ],
)); ));
...@@ -395,6 +396,7 @@ void main() { ...@@ -395,6 +396,7 @@ void main() {
actions: <SemanticsAction>[ actions: <SemanticsAction>[
SemanticsAction.moveCursorBackwardByCharacter, SemanticsAction.moveCursorBackwardByCharacter,
SemanticsAction.moveCursorForwardByCharacter, SemanticsAction.moveCursorForwardByCharacter,
SemanticsAction.setSelection,
], ],
)); ));
...@@ -406,6 +408,7 @@ void main() { ...@@ -406,6 +408,7 @@ void main() {
value: 'test', value: 'test',
actions: <SemanticsAction>[ actions: <SemanticsAction>[
SemanticsAction.moveCursorForwardByCharacter, SemanticsAction.moveCursorForwardByCharacter,
SemanticsAction.setSelection,
], ],
)); ));
...@@ -452,6 +455,7 @@ void main() { ...@@ -452,6 +455,7 @@ void main() {
actions: <SemanticsAction>[ actions: <SemanticsAction>[
SemanticsAction.moveCursorBackwardByCharacter, SemanticsAction.moveCursorBackwardByCharacter,
SemanticsAction.moveCursorForwardByCharacter, SemanticsAction.moveCursorForwardByCharacter,
SemanticsAction.setSelection,
], ],
)); ));
...@@ -470,6 +474,7 @@ void main() { ...@@ -470,6 +474,7 @@ void main() {
value: 'test', value: 'test',
actions: <SemanticsAction>[ actions: <SemanticsAction>[
SemanticsAction.moveCursorForwardByCharacter, SemanticsAction.moveCursorForwardByCharacter,
SemanticsAction.setSelection,
], ],
)); ));
...@@ -523,6 +528,7 @@ void main() { ...@@ -523,6 +528,7 @@ void main() {
actions: <SemanticsAction>[ actions: <SemanticsAction>[
SemanticsAction.moveCursorBackwardByCharacter, SemanticsAction.moveCursorBackwardByCharacter,
SemanticsAction.moveCursorForwardByCharacter, SemanticsAction.moveCursorForwardByCharacter,
SemanticsAction.setSelection,
], ],
)); ));
...@@ -541,6 +547,7 @@ void main() { ...@@ -541,6 +547,7 @@ void main() {
value: 'test', value: 'test',
actions: <SemanticsAction>[ actions: <SemanticsAction>[
SemanticsAction.moveCursorForwardByCharacter, SemanticsAction.moveCursorForwardByCharacter,
SemanticsAction.setSelection,
], ],
)); ));
......
...@@ -11,6 +11,10 @@ import 'package:flutter_test/flutter_test.dart'; ...@@ -11,6 +11,10 @@ import 'package:flutter_test/flutter_test.dart';
import 'semantics_tester.dart'; import 'semantics_tester.dart';
void main() { void main() {
setUp(() {
debugResetSemanticsIdCounter();
});
testWidgets('Semantics shutdown and restart', (WidgetTester tester) async { testWidgets('Semantics shutdown and restart', (WidgetTester tester) async {
SemanticsTester semantics = new SemanticsTester(tester); SemanticsTester semantics = new SemanticsTester(tester);
...@@ -390,13 +394,14 @@ void main() { ...@@ -390,13 +394,14 @@ void main() {
onDecrease: () => performedActions.add(SemanticsAction.decrease), onDecrease: () => performedActions.add(SemanticsAction.decrease),
onMoveCursorForwardByCharacter: (bool _) => performedActions.add(SemanticsAction.moveCursorForwardByCharacter), onMoveCursorForwardByCharacter: (bool _) => performedActions.add(SemanticsAction.moveCursorForwardByCharacter),
onMoveCursorBackwardByCharacter: (bool _) => performedActions.add(SemanticsAction.moveCursorBackwardByCharacter), onMoveCursorBackwardByCharacter: (bool _) => performedActions.add(SemanticsAction.moveCursorBackwardByCharacter),
onSetSelection: (TextSelection _) => performedActions.add(SemanticsAction.setSelection),
) )
); );
final Set<SemanticsAction> allActions = SemanticsAction.values.values.toSet() final Set<SemanticsAction> allActions = SemanticsAction.values.values.toSet()
..remove(SemanticsAction.showOnScreen); // showOnScreen is non user-exposed. ..remove(SemanticsAction.showOnScreen); // showOnScreen is non user-exposed.
final int expectedId = 32; const int expectedId = 2;
final TestSemantics expectedSemantics = new TestSemantics.root( final TestSemantics expectedSemantics = new TestSemantics.root(
children: <TestSemantics>[ children: <TestSemantics>[
new TestSemantics.rootChild( new TestSemantics.rootChild(
...@@ -417,6 +422,12 @@ void main() { ...@@ -417,6 +422,12 @@ void main() {
case SemanticsAction.moveCursorForwardByCharacter: case SemanticsAction.moveCursorForwardByCharacter:
semanticsOwner.performAction(expectedId, action, true); semanticsOwner.performAction(expectedId, action, true);
break; break;
case SemanticsAction.setSelection:
semanticsOwner.performAction(expectedId, action, <String, int>{
'base': 4,
'extent': 5,
});
break;
default: default:
semanticsOwner.performAction(expectedId, action); semanticsOwner.performAction(expectedId, action);
} }
...@@ -446,7 +457,7 @@ void main() { ...@@ -446,7 +457,7 @@ void main() {
), ),
); );
final int expectedId = 35; const int expectedId = 2;
final TestSemantics expectedSemantics = new TestSemantics.root( final TestSemantics expectedSemantics = new TestSemantics.root(
children: <TestSemantics>[ children: <TestSemantics>[
new TestSemantics.rootChild( new TestSemantics.rootChild(
......
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