Unverified Commit 333c9618 authored by Chris Yang's avatar Chris Yang Committed by GitHub

Extract common PlatformView functionality: Gesture and PointerEvent (#37497)

parent 0cd0c660
......@@ -75,12 +75,12 @@ Set<Type> _factoriesTypeSet<T>(Set<Factory<T>> factories) {
///
/// * [AndroidView] which is a widget that is used to show an Android view.
/// * [PlatformViewsService] which is a service for controlling platform views.
class RenderAndroidView extends RenderBox {
class RenderAndroidView extends RenderBox with _PlatformViewGestureMixin {
/// Creates a render object for an Android view.
RenderAndroidView({
@required AndroidViewController viewController,
@required this.hitTestBehavior,
@required PlatformViewHitTestBehavior hitTestBehavior,
@required Set<Factory<OneSequenceGestureRecognizer>> gestureRecognizers,
}) : assert(viewController != null),
assert(hitTestBehavior != null),
......@@ -89,6 +89,7 @@ class RenderAndroidView extends RenderBox {
_motionEventsDispatcher = _MotionEventsDispatcher(globalToLocal, viewController);
updateGestureRecognizers(gestureRecognizers);
_viewController.addOnPlatformViewCreatedListener(_onPlatformViewCreated);
this.hitTestBehavior = hitTestBehavior;
}
_PlatformViewState _state = _PlatformViewState.uninitialized;
......@@ -117,11 +118,6 @@ class RenderAndroidView extends RenderBox {
markNeedsSemanticsUpdate();
}
/// How to behave during hit testing.
// The implicit setter is enough here as changing this value will just affect
// any newly arriving events there's nothing we need to invalidate.
PlatformViewHitTestBehavior hitTestBehavior;
/// {@template flutter.rendering.platformView.updateGestureRecognizers}
/// Updates which gestures should be forwarded to the platform view.
///
......@@ -139,16 +135,7 @@ class RenderAndroidView extends RenderBox {
/// Any active gesture arena the Android view participates in is rejected when the
/// set of gesture recognizers is changed.
void updateGestureRecognizers(Set<Factory<OneSequenceGestureRecognizer>> gestureRecognizers) {
assert(gestureRecognizers != null);
assert(
_factoriesTypeSet(gestureRecognizers).length == gestureRecognizers.length,
'There were multiple gesture recognizer factories for the same type, there must only be a single '
'gesture recognizer factory for each gesture recognizer type.',);
if (_factoryTypesSetEquals(gestureRecognizers, _gestureRecognizer?.gestureRecognizerFactories)) {
return;
}
_gestureRecognizer?.dispose();
_gestureRecognizer = _AndroidViewGestureRecognizer(_motionEventsDispatcher, gestureRecognizers);
_updateGestureRecognizersWithCallBack(gestureRecognizers, _motionEventsDispatcher.handlePointerEvent);
}
@override
......@@ -162,8 +149,6 @@ class RenderAndroidView extends RenderBox {
_MotionEventsDispatcher _motionEventsDispatcher;
_AndroidViewGestureRecognizer _gestureRecognizer;
@override
void performResize() {
size = constraints.biggest;
......@@ -229,24 +214,6 @@ class RenderAndroidView extends RenderBox {
));
}
@override
bool hitTest(BoxHitTestResult result, { Offset position }) {
if (hitTestBehavior == PlatformViewHitTestBehavior.transparent || !size.contains(position))
return false;
result.add(BoxHitTestEntry(this, position));
return hitTestBehavior == PlatformViewHitTestBehavior.opaque;
}
@override
bool hitTestSelf(Offset position) => hitTestBehavior != PlatformViewHitTestBehavior.transparent;
@override
void handleEvent(PointerEvent event, HitTestEntry entry) {
if (event is PointerDownEvent) {
_gestureRecognizer.addPointer(event);
}
}
@override
void describeSemanticsConfiguration (SemanticsConfiguration config) {
super.describeSemanticsConfiguration(config);
......@@ -257,12 +224,6 @@ class RenderAndroidView extends RenderBox {
config.platformViewId = _viewController.id;
}
}
@override
void detach() {
_gestureRecognizer.reset();
super.detach();
}
}
/// A render object for an iOS UIKit UIView.
......@@ -486,15 +447,17 @@ class _UiKitViewGestureRecognizer extends OneSequenceGestureRecognizer {
}
}
typedef _HandlePointerEvent = void Function(PointerEvent event);
// This recognizer constructs gesture recognizers from a set of gesture recognizer factories
// it was give, adds all of them to a gesture arena team with the _AndroidViewGestureRecognizer
// it was give, adds all of them to a gesture arena team with the _PlatformViewGestureRecognizer
// as the team captain.
// As long as ta gesture arena is unresolved the recognizer caches all pointer events.
// When the team wins the recognizer sends all the cached point events to the embedded Android view, and
// sets itself to a "forwarding mode" where it will forward any new pointer event to the Android view.
class _AndroidViewGestureRecognizer extends OneSequenceGestureRecognizer {
_AndroidViewGestureRecognizer(
this.dispatcher,
// As long as the gesture arena is unresolved, the recognizer caches all pointer events.
// When the team wins, the recognizer sends all the cached pointer events to `_handlePointerEvent`, and
// sets itself to a "forwarding mode" where it will forward any new pointer event to `_handlePointerEvent`.
class _PlatformViewGestureRecognizer extends OneSequenceGestureRecognizer {
_PlatformViewGestureRecognizer(
_HandlePointerEvent handlePointerEvent,
this.gestureRecognizerFactories, {
PointerDeviceKind kind,
}) : super(kind: kind) {
......@@ -505,18 +468,19 @@ class _AndroidViewGestureRecognizer extends OneSequenceGestureRecognizer {
return recognizerFactory.constructor()..team = team;
},
).toSet();
_handlePointerEvent = handlePointerEvent;
}
final _MotionEventsDispatcher dispatcher;
_HandlePointerEvent _handlePointerEvent;
// Maps a pointer to a list of its cached pointer events.
// Before the arena for a pointer is resolved all events are cached here, if we win the arena
// the cached events are dispatched to the view, if we lose the arena we clear the cache for
// the cached events are dispatched to `_handlePointerEvent`, if we lose the arena we clear the cache for
// the pointer.
final Map<int, List<PointerEvent>> cachedEvents = <int, List<PointerEvent>>{};
// Pointer for which we have already won the arena, events for pointers in this set are
// immediately dispatched to the Android view.
// immediately dispatched to `_handlePointerEvent`.
final Set<int> forwardedPointers = <int>{};
// We use OneSequenceGestureRecognizers as they support gesture arena teams.
......@@ -534,7 +498,7 @@ class _AndroidViewGestureRecognizer extends OneSequenceGestureRecognizer {
}
@override
String get debugDescription => 'Android view';
String get debugDescription => 'Platform view';
@override
void didStopTrackingLastPointer(int pointer) { }
......@@ -542,16 +506,16 @@ class _AndroidViewGestureRecognizer extends OneSequenceGestureRecognizer {
@override
void handleEvent(PointerEvent event) {
if (!forwardedPointers.contains(event.pointer)) {
cacheEvent(event);
_cacheEvent(event);
} else {
dispatcher.handlePointerEvent(event);
_handlePointerEvent(event);
}
stopTrackingIfPointerNoLongerDown(event);
}
@override
void acceptGesture(int pointer) {
flushPointerCache(pointer);
_flushPointerCache(pointer);
forwardedPointers.add(pointer);
}
......@@ -561,15 +525,15 @@ class _AndroidViewGestureRecognizer extends OneSequenceGestureRecognizer {
cachedEvents.remove(pointer);
}
void cacheEvent(PointerEvent event) {
void _cacheEvent(PointerEvent event) {
if (!cachedEvents.containsKey(event.pointer)) {
cachedEvents[event.pointer] = <PointerEvent> [];
}
cachedEvents[event.pointer].add(event);
}
void flushPointerCache(int pointer) {
cachedEvents.remove(pointer)?.forEach(dispatcher.handlePointerEvent);
void _flushPointerCache(int pointer) {
cachedEvents.remove(pointer)?.forEach(_handlePointerEvent);
}
@override
......@@ -728,18 +692,24 @@ class _MotionEventsDispatcher {
/// A render object for embedding a platform view.
///
/// [PlatformViewRenderBox] presents a platform view by adding a [PlatformViewLayer] layer, integrates it with the gesture arenas system
/// and adds relevant semantic nodes to the semantics tree.
class PlatformViewRenderBox extends RenderBox {
/// [PlatformViewRenderBox] presents a platform view by adding a [PlatformViewLayer] layer,
/// integrates it with the gesture arenas system and adds relevant semantic nodes to the semantics tree.
class PlatformViewRenderBox extends RenderBox with _PlatformViewGestureMixin {
/// Creating a render object for a [PlatformViewSurface].
///
/// The `controller` parameter must not be null.
PlatformViewRenderBox({
@required PlatformViewController controller,
}) : assert(controller != null && controller.viewId != null && controller.viewId > -1),
_controller = controller;
@required PlatformViewHitTestBehavior hitTestBehavior,
@required Set<Factory<OneSequenceGestureRecognizer>> gestureRecognizers,
}) : assert(controller != null && controller.viewId != null && controller.viewId > -1),
assert(hitTestBehavior != null),
assert(gestureRecognizers != null),
_controller = controller {
this.hitTestBehavior = hitTestBehavior;
updateGestureRecognizers(gestureRecognizers);
}
/// Sets the [controller] for this render object.
///
......@@ -759,6 +729,19 @@ class PlatformViewRenderBox extends RenderBox {
}
}
/// How to behave during hit testing.
// The implicit setter is enough here as changing this value will just affect
// any newly arriving events there's nothing we need to invalidate.
// PlatformViewHitTestBehavior hitTestBehavior;
/// {@macro flutter.rendering.platformView.updateGestureRecognizers}
///
/// Any active gesture arena the `PlatformView` participates in is rejected when the
/// set of gesture recognizers is changed.
void updateGestureRecognizers(Set<Factory<OneSequenceGestureRecognizer>> gestureRecognizers) {
_updateGestureRecognizersWithCallBack(gestureRecognizers, _controller.dispatchPointerEvent);
}
PlatformViewController _controller;
@override
......@@ -791,3 +774,56 @@ class PlatformViewRenderBox extends RenderBox {
config.platformViewId = _controller.viewId;
}
}
/// The Mixin handling the pointer events and gestures of a platform view render box.
mixin _PlatformViewGestureMixin on RenderBox {
/// How to behave during hit testing.
// The implicit setter is enough here as changing this value will just affect
// any newly arriving events there's nothing we need to invalidate.
PlatformViewHitTestBehavior hitTestBehavior;
/// {@macro flutter.rendering.platformView.updateGestureRecognizers}
///
/// Any active gesture arena the `PlatformView` participates in is rejected when the
/// set of gesture recognizers is changed.
void _updateGestureRecognizersWithCallBack(Set<Factory<OneSequenceGestureRecognizer>> gestureRecognizers, _HandlePointerEvent _handlePointerEvent) {
assert(gestureRecognizers != null);
assert(
_factoriesTypeSet(gestureRecognizers).length == gestureRecognizers.length,
'There were multiple gesture recognizer factories for the same type, there must only be a single '
'gesture recognizer factory for each gesture recognizer type.',);
if (_factoryTypesSetEquals(gestureRecognizers, _gestureRecognizer?.gestureRecognizerFactories)) {
return;
}
_gestureRecognizer?.dispose();
_gestureRecognizer = _PlatformViewGestureRecognizer(_handlePointerEvent, gestureRecognizers);
}
_PlatformViewGestureRecognizer _gestureRecognizer;
@override
bool hitTest(BoxHitTestResult result, { Offset position }) {
if (hitTestBehavior == PlatformViewHitTestBehavior.transparent || !size.contains(position)) {
return false;
}
result.add(BoxHitTestEntry(this, position));
return hitTestBehavior == PlatformViewHitTestBehavior.opaque;
}
@override
bool hitTestSelf(Offset position) => hitTestBehavior != PlatformViewHitTestBehavior.transparent;
@override
void handleEvent(PointerEvent event, HitTestEntry entry) {
if (event is PointerDownEvent) {
_gestureRecognizer.addPointer(event);
}
}
@override
void detach() {
_gestureRecognizer.reset();
super.detach();
}
}
......@@ -7,6 +7,7 @@ import 'dart:typed_data';
import 'dart:ui';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'message_codec.dart';
import 'system_channels.dart';
......@@ -725,4 +726,7 @@ abstract class PlatformViewController {
///
/// See also [PlatformViewRegistry] which is a helper for managing platform view ids.
int get viewId;
/// Dispatches the `event` to the platform view.
void dispatchPointerEvent(PointerEvent event);
}
......@@ -603,7 +603,11 @@ class PlatformViewSurface extends LeafRenderObjectWidget {
/// The [controller] must not be null.
const PlatformViewSurface({
@required this.controller,
}) : assert(controller != null);
@required this.hitTestBehavior,
@required this.gestureRecognizers,
}) : assert(controller != null),
assert(hitTestBehavior != null),
assert(gestureRecognizers != null);
/// The controller for the platform view integrated by this [PlatformViewSurface].
///
......@@ -611,14 +615,60 @@ class PlatformViewSurface extends LeafRenderObjectWidget {
/// [PlatformViewController.viewId] identifies the platform view whose contents are painted by this widget.
final PlatformViewController controller;
/// Which gestures should be forwarded to the PlatformView.
///
/// {@macro flutter.widgets.platformViews.gestureRecognizersDescHead}
///
/// For example, with the following setup vertical drags will not be dispatched to the platform view
/// as the vertical drag gesture is claimed by the parent [GestureDetector].
///
/// ```dart
/// GestureDetector(
/// onVerticalDragStart: (DragStartDetails details) {},
/// child: PlatformViewSurface(
/// ),
/// )
/// ```
///
/// To get the [PlatformViewSurface] to claim the vertical drag gestures we can pass a vertical drag
/// gesture recognizer factory in [gestureRecognizers] e.g:
///
/// ```dart
/// GestureDetector(
/// onVerticalDragStart: (DragStartDetails details) {},
/// child: SizedBox(
/// width: 200.0,
/// height: 100.0,
/// child: PlatformViewSurface(
/// gestureRecognizers: <Factory<OneSequenceGestureRecognizer>>[
/// new Factory<OneSequenceGestureRecognizer>(
/// () => new EagerGestureRecognizer(),
/// ),
/// ].toSet(),
/// ),
/// ),
/// )
/// ```
///
/// {@macro flutter.widgets.platformViews.gestureRecognizersDescFoot}
// We use OneSequenceGestureRecognizers as they support gesture arena teams.
// TODO(amirh): get a list of GestureRecognizers here.
// https://github.com/flutter/flutter/issues/20953
final Set<Factory<OneSequenceGestureRecognizer>> gestureRecognizers;
/// {@macro flutter.widgets.platformViews.hittestParam}
final PlatformViewHitTestBehavior hitTestBehavior;
@override
RenderObject createRenderObject(BuildContext context) {
return PlatformViewRenderBox(controller: controller);
return PlatformViewRenderBox(controller: controller, gestureRecognizers: gestureRecognizers, hitTestBehavior: hitTestBehavior);
}
@override
void updateRenderObject(BuildContext context, PlatformViewRenderBox renderObject) {
renderObject
..controller = controller;
..controller = controller
..hitTestBehavior = hitTestBehavior
..updateGestureRecognizers(gestureRecognizers);
}
}
......@@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart';
......@@ -15,7 +17,16 @@ void main() {
PlatformViewRenderBox platformViewRenderBox;
setUp((){
fakePlatformViewController = FakePlatformViewController(0);
platformViewRenderBox = PlatformViewRenderBox(controller: fakePlatformViewController);
platformViewRenderBox = PlatformViewRenderBox(
controller: fakePlatformViewController,
hitTestBehavior: PlatformViewHitTestBehavior.opaque,
gestureRecognizers: <Factory<OneSequenceGestureRecognizer>>{
Factory<VerticalDragGestureRecognizer>(
() {
return VerticalDragGestureRecognizer();
},
),
},);
});
test('layout should size to max constraint', () {
......
......@@ -8,6 +8,7 @@ import 'package:collection/collection.dart';
import 'package:flutter/painting.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
/// Used in internal testing.
class FakePlatformViewController extends PlatformViewController {
......@@ -16,10 +17,22 @@ class FakePlatformViewController extends PlatformViewController {
_id = id;
}
/// Events that are dispatched;
List<PointerEvent> dispatchedPointerEvents = <PointerEvent>[];
int _id;
@override
int get viewId => _id;
@override
void dispatchPointerEvent(PointerEvent event) {
dispatchedPointerEvents.add(event);
}
void clearTestingVariables() {
dispatchedPointerEvents.clear();
}
}
class FakeAndroidPlatformViewsController {
......
......@@ -1676,12 +1676,261 @@ void main() {
});
testWidgets('PlatformViewSurface should create platform view layer', (WidgetTester tester) async {
final PlatformViewSurface surface = PlatformViewSurface(controller: controller);
final PlatformViewSurface surface = PlatformViewSurface(
controller: controller,
hitTestBehavior: PlatformViewHitTestBehavior.opaque,
gestureRecognizers: const <Factory<OneSequenceGestureRecognizer>>{},);
await tester.pumpWidget(surface);
final PlatformViewLayer layer = tester.layers.firstWhere((Layer layer){
return layer is PlatformViewLayer;
});
expect(layer, isNotNull);
});
testWidgets('PlatformViewSurface can lose gesture arenas', (WidgetTester tester) async {
bool verticalDragAcceptedByParent = false;
await tester.pumpWidget(
Align(
alignment: Alignment.topLeft,
child: Container(
margin: const EdgeInsets.all(10.0),
child: GestureDetector(
onVerticalDragStart: (DragStartDetails d) {
verticalDragAcceptedByParent = true;
},
child: SizedBox(
width: 200.0,
height: 100.0,
child: PlatformViewSurface(
controller: controller,
gestureRecognizers: const <Factory<OneSequenceGestureRecognizer>>{},
hitTestBehavior: PlatformViewHitTestBehavior.opaque),
),
),
),
),
);
final TestGesture gesture = await tester.startGesture(const Offset(50.0, 50.0));
await gesture.moveBy(const Offset(0.0, 100.0));
await gesture.up();
expect(verticalDragAcceptedByParent, true);
expect(
controller.dispatchedPointerEvents,
isEmpty,
);
});
testWidgets('PlatformViewSurface gesture recognizers dispatch events', (WidgetTester tester) async {
bool verticalDragAcceptedByParent = false;
await tester.pumpWidget(
Align(
alignment: Alignment.topLeft,
child: GestureDetector(
onVerticalDragStart: (DragStartDetails d) {
verticalDragAcceptedByParent = true;
},
child: SizedBox(
width: 200.0,
height: 100.0,
child: PlatformViewSurface(
controller: controller,
hitTestBehavior: PlatformViewHitTestBehavior.opaque,
gestureRecognizers: <Factory<OneSequenceGestureRecognizer>>{
Factory<VerticalDragGestureRecognizer>(
() {
return VerticalDragGestureRecognizer()
..onStart = (_) {}; // Add callback to enable recognizer
},
),
},
),
),
),
),
);
final TestGesture gesture = await tester.startGesture(const Offset(50.0, 50.0));
await gesture.moveBy(const Offset(0.0, 100.0));
await gesture.up();
expect(verticalDragAcceptedByParent, false);
expect(
controller.dispatchedPointerEvents.length,
3,
);
});
testWidgets(
'PlatformViewSurface can claim gesture after all pointers are up', (WidgetTester tester) async {
bool verticalDragAcceptedByParent = false;
// The long press recognizer rejects the gesture after the PlatformViewSurface gets the pointer up event.
// This test makes sure that the PlatformViewSurface can win the gesture after it got the pointer up event.
await tester.pumpWidget(
Align(
alignment: Alignment.topLeft,
child: GestureDetector(
onVerticalDragStart: (DragStartDetails d) {
verticalDragAcceptedByParent = true;
},
onLongPress: () { },
child: SizedBox(
width: 200.0,
height: 100.0,
child: PlatformViewSurface(
controller: controller,
hitTestBehavior: PlatformViewHitTestBehavior.opaque,
gestureRecognizers: const <Factory<OneSequenceGestureRecognizer>>{},
),
),
),
),
);
final TestGesture gesture = await tester.startGesture(const Offset(50.0, 50.0));
await gesture.up();
expect(verticalDragAcceptedByParent, false);
expect(
controller.dispatchedPointerEvents.length,
2,
);
});
testWidgets('PlatformViewSurface rebuilt during gesture', (WidgetTester tester) async {
await tester.pumpWidget(
Align(
alignment: Alignment.topLeft,
child: SizedBox(
width: 200.0,
height: 100.0,
child: PlatformViewSurface(
controller: controller,
hitTestBehavior: PlatformViewHitTestBehavior.opaque,
gestureRecognizers: const <Factory<OneSequenceGestureRecognizer>>{},
),
),
),
);
final TestGesture gesture = await tester.startGesture(const Offset(50.0, 50.0));
await gesture.moveBy(const Offset(0.0, 100.0));
await tester.pumpWidget(
Align(
alignment: Alignment.topLeft,
child: SizedBox(
width: 200.0,
height: 100.0,
child: PlatformViewSurface(
controller: controller,
hitTestBehavior: PlatformViewHitTestBehavior.opaque,
gestureRecognizers: const <Factory<OneSequenceGestureRecognizer>>{},
),
),
),
);
await gesture.up();
expect(
controller.dispatchedPointerEvents.length,
3,
);
});
testWidgets('PlatformViewSurface with eager gesture recognizer', (WidgetTester tester) async {
await tester.pumpWidget(
Align(
alignment: Alignment.topLeft,
child: GestureDetector(
onVerticalDragStart: (DragStartDetails d) { },
child: SizedBox(
width: 200.0,
height: 100.0,
child: PlatformViewSurface(
controller: controller,
hitTestBehavior: PlatformViewHitTestBehavior.opaque,
gestureRecognizers: <Factory<OneSequenceGestureRecognizer>>{
Factory<OneSequenceGestureRecognizer>(
() => EagerGestureRecognizer(),
),
},
),
),
),
),
);
await tester.startGesture(const Offset(50.0, 50.0));
// Normally (without the eager gesture recognizer) after just the pointer down event
// no gesture arena member will claim the arena (so no motion events will be dispatched to
// the PlatformViewSurface). Here we assert that with the eager recognizer in the gesture team the
// pointer down event is immediately dispatched.
expect(
controller.dispatchedPointerEvents.length,
1,
);
});
testWidgets('PlatformViewRenderBox reconstructed with same gestureRecognizers', (WidgetTester tester) async {
int factoryInvocationCount = 0;
final ValueGetter<EagerGestureRecognizer> constructRecognizer = () {
++ factoryInvocationCount;
return EagerGestureRecognizer();
};
final PlatformViewSurface platformViewSurface = PlatformViewSurface(
controller: controller,
hitTestBehavior: PlatformViewHitTestBehavior.opaque,
gestureRecognizers: <Factory<OneSequenceGestureRecognizer>>{
Factory<OneSequenceGestureRecognizer>(
constructRecognizer,
),
});
await tester.pumpWidget(platformViewSurface);
await tester.pumpWidget(const SizedBox.shrink());
await tester.pumpWidget(platformViewSurface);
expect(factoryInvocationCount, 2);
});
testWidgets('PlatformViewSurface rebuilt with same gestureRecognizers', (WidgetTester tester) async {
int factoryInvocationCount = 0;
final ValueGetter<EagerGestureRecognizer> constructRecognizer = () {
++ factoryInvocationCount;
return EagerGestureRecognizer();
};
await tester.pumpWidget(
PlatformViewSurface(
controller: controller,
hitTestBehavior: PlatformViewHitTestBehavior.opaque,
gestureRecognizers: <Factory<OneSequenceGestureRecognizer>>{
Factory<OneSequenceGestureRecognizer>(
constructRecognizer,
),
})
);
await tester.pumpWidget(
PlatformViewSurface(
controller: controller,
hitTestBehavior: PlatformViewHitTestBehavior.opaque,
gestureRecognizers: <Factory<OneSequenceGestureRecognizer>>{
Factory<OneSequenceGestureRecognizer>(
constructRecognizer,
),
})
);
expect(factoryInvocationCount, 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