Commit 5f9cb4aa authored by Hixie's avatar Hixie

Port ScrollableMixedWidgetListState to fn3.

parent 06ffa759
...@@ -4,7 +4,8 @@ ...@@ -4,7 +4,8 @@
import 'package:sky/animation.dart'; import 'package:sky/animation.dart';
import 'package:sky/material.dart'; import 'package:sky/material.dart';
import 'package:sky/widgets.dart'; import 'package:sky/painting.dart';
import 'package:sky/src/fn3.dart';
class CardModel { class CardModel {
CardModel(this.value, this.height, this.color); CardModel(this.value, this.height, this.color);
...@@ -15,7 +16,11 @@ class CardModel { ...@@ -15,7 +16,11 @@ class CardModel {
Key get key => new ObjectKey(this); Key get key => new ObjectKey(this);
} }
class CardCollectionApp extends App { class CardCollectionApp extends StatefulComponent {
CardCollectionAppState createState() => new CardCollectionAppState();
}
class CardCollectionAppState extends State<CardCollectionApp> {
static const TextStyle cardLabelStyle = static const TextStyle cardLabelStyle =
const TextStyle(color: Colors.white, fontSize: 18.0, fontWeight: bold); const TextStyle(color: Colors.white, fontSize: 18.0, fontWeight: bold);
...@@ -23,14 +28,14 @@ class CardCollectionApp extends App { ...@@ -23,14 +28,14 @@ class CardCollectionApp extends App {
final TextStyle backgroundTextStyle = final TextStyle backgroundTextStyle =
Typography.white.title.copyWith(textAlign: TextAlign.center); Typography.white.title.copyWith(textAlign: TextAlign.center);
MixedViewportLayoutState _layoutState = new MixedViewportLayoutState();
List<CardModel> _cardModels; List<CardModel> _cardModels;
DismissDirection _dismissDirection = DismissDirection.horizontal; DismissDirection _dismissDirection = DismissDirection.horizontal;
bool _drawerShowing = false; bool _drawerShowing = false;
AnimationStatus _drawerStatus = AnimationStatus.dismissed; AnimationStatus _drawerStatus = AnimationStatus.dismissed;
InvalidatorCallback _invalidator;
void initState(BuildContext context) {
void initState() { super.initState(context);
List<double> cardHeights = <double>[ List<double> cardHeights = <double>[
48.0, 63.0, 82.0, 146.0, 60.0, 55.0, 84.0, 96.0, 50.0, 48.0, 63.0, 82.0, 146.0, 60.0, 55.0, 84.0, 96.0, 50.0,
48.0, 63.0, 82.0, 146.0, 60.0, 55.0, 84.0, 96.0, 50.0, 48.0, 63.0, 82.0, 146.0, 60.0, 55.0, 84.0, 96.0, 50.0,
...@@ -40,7 +45,6 @@ class CardCollectionApp extends App { ...@@ -40,7 +45,6 @@ class CardCollectionApp extends App {
Color color = Color.lerp(Colors.red[300], Colors.blue[900], i / cardHeights.length); Color color = Color.lerp(Colors.red[300], Colors.blue[900], i / cardHeights.length);
return new CardModel(i, cardHeights[i], color); return new CardModel(i, cardHeights[i], color);
}); });
super.initState();
} }
void dismissCard(CardModel card) { void dismissCard(CardModel card) {
...@@ -121,14 +125,14 @@ class CardCollectionApp extends App { ...@@ -121,14 +125,14 @@ class CardCollectionApp extends App {
); );
} }
Widget buildCard(int index) { Widget buildCard(BuildContext context, int index) {
if (index >= _cardModels.length) if (index >= _cardModels.length)
return null; return null;
CardModel cardModel = _cardModels[index]; CardModel cardModel = _cardModels[index];
Widget card = new Dismissable( Widget card = new Dismissable(
direction: _dismissDirection, direction: _dismissDirection,
onResized: () { _layoutState.invalidate([index]); }, onResized: () { _invalidator([index]); },
onDismissed: () { dismissCard(cardModel); }, onDismissed: () { dismissCard(cardModel); },
child: new Card( child: new Card(
color: cardModel.color, color: cardModel.color,
...@@ -178,7 +182,7 @@ class CardCollectionApp extends App { ...@@ -178,7 +182,7 @@ class CardCollectionApp extends App {
child: new Viewport( child: new Viewport(
child: new Container( child: new Container(
height: cardModel.height, height: cardModel.height,
decoration: new BoxDecoration(backgroundColor: Theme.of(this).primaryColor), decoration: new BoxDecoration(backgroundColor: Theme.of(context).primaryColor),
child: new Row([ child: new Row([
leftArrowIcon, leftArrowIcon,
new Flexible(child: new Text(backgroundMessage, style: backgroundTextStyle)), new Flexible(child: new Text(backgroundMessage, style: backgroundTextStyle)),
...@@ -196,14 +200,14 @@ class CardCollectionApp extends App { ...@@ -196,14 +200,14 @@ class CardCollectionApp extends App {
); );
} }
Widget build() { Widget build(BuildContext context) {
Widget cardCollection = new Container( Widget cardCollection = new Container(
padding: const EdgeDims.symmetric(vertical: 12.0, horizontal: 8.0), padding: const EdgeDims.symmetric(vertical: 12.0, horizontal: 8.0),
decoration: new BoxDecoration(backgroundColor: Theme.of(this).primarySwatch[50]), decoration: new BoxDecoration(backgroundColor: Theme.of(context).primarySwatch[50]),
child: new ScrollableMixedWidgetList( child: new ScrollableMixedWidgetList(
builder: buildCard, builder: buildCard,
token: _cardModels.length, token: _cardModels.length,
layoutState: _layoutState onInvalidatorAvailable: (InvalidatorCallback callback) { _invalidator = callback; }
) )
); );
......
...@@ -315,10 +315,15 @@ abstract class State<T extends StatefulComponent> { ...@@ -315,10 +315,15 @@ abstract class State<T extends StatefulComponent> {
/// component will not be scheduled for rebuilding, meaning that its rendering /// component will not be scheduled for rebuilding, meaning that its rendering
/// will not be updated. /// will not be updated.
void setState(void fn()) { void setState(void fn()) {
assert(_debugLifecycleState == _StateLifecycle.ready); assert(_debugLifecycleState != _StateLifecycle.defunct);
fn(); fn();
if (_element._builder != null) {
// _element._builder is set after initState(). We verify that we're past
// that before calling markNeedsBuild() so that setState()s triggered
// during initState() during lockState() don't cause any trouble.
_element.markNeedsBuild(); _element.markNeedsBuild();
} }
}
/// Called when this object is removed from the tree. Override this to clean /// Called when this object is removed from the tree. Override this to clean
/// up any resources allocated by this object. /// up any resources allocated by this object.
...@@ -509,8 +514,10 @@ abstract class Element<T extends Widget> implements BuildContext { ...@@ -509,8 +514,10 @@ abstract class Element<T extends Widget> implements BuildContext {
_parent = parent; _parent = parent;
_slot = newSlot; _slot = newSlot;
_depth = _parent != null ? _parent.depth + 1 : 1; _depth = _parent != null ? _parent.depth + 1 : 1;
if (widget.key is GlobalKey) if (widget.key is GlobalKey) {
widget.key._register(this); final GlobalKey key = widget.key;
key._register(this);
}
assert(() { _debugLifecycleState = _ElementLifecycle.mounted; return true; }); assert(() { _debugLifecycleState = _ElementLifecycle.mounted; return true; });
} }
...@@ -576,8 +583,10 @@ abstract class Element<T extends Widget> implements BuildContext { ...@@ -576,8 +583,10 @@ abstract class Element<T extends Widget> implements BuildContext {
assert(_debugLifecycleState == _ElementLifecycle.mounted); assert(_debugLifecycleState == _ElementLifecycle.mounted);
assert(widget != null); assert(widget != null);
assert(depth != null); assert(depth != null);
if (widget.key is GlobalKey) if (widget.key is GlobalKey) {
widget.key._unregister(this); final GlobalKey key = widget.key;
key._unregister(this);
}
assert(() { _debugLifecycleState = _ElementLifecycle.defunct; return true; }); assert(() { _debugLifecycleState = _ElementLifecycle.defunct; return true; });
} }
...@@ -631,6 +640,7 @@ abstract class BuildableElement<T extends Widget> extends Element<T> { ...@@ -631,6 +640,7 @@ abstract class BuildableElement<T extends Widget> extends Element<T> {
/// binding when scheduleBuild() has been called to mark this element dirty, /// binding when scheduleBuild() has been called to mark this element dirty,
/// and by update() when the Widget has changed. /// and by update() when the Widget has changed.
void rebuild() { void rebuild() {
assert(_debugLifecycleState != _ElementLifecycle.initial);
if (!_dirty) if (!_dirty)
return; return;
assert(_debugLifecycleState == _ElementLifecycle.mounted); assert(_debugLifecycleState == _ElementLifecycle.mounted);
...@@ -738,6 +748,9 @@ class StatefulComponentElement extends BuildableElement<StatefulComponent> { ...@@ -738,6 +748,9 @@ class StatefulComponentElement extends BuildableElement<StatefulComponent> {
return false; return false;
}); });
assert(() { _state._debugLifecycleState = _StateLifecycle.ready; return true; }); assert(() { _state._debugLifecycleState = _StateLifecycle.ready; return true; });
assert(_builder == null);
// see State.setState() for why it's important that _builder be set after
// initState() is called.
_builder = _state.build; _builder = _state.build;
} }
...@@ -1184,9 +1197,8 @@ typedef void WidgetsExceptionHandler(String context, dynamic exception, StackTra ...@@ -1184,9 +1197,8 @@ typedef void WidgetsExceptionHandler(String context, dynamic exception, StackTra
/// the exception occurred, and may include additional details such as /// the exception occurred, and may include additional details such as
/// descriptions of the objects involved. The 'exception' argument contains the /// descriptions of the objects involved. The 'exception' argument contains the
/// object that was thrown, and the 'stack' argument contains the stack trace. /// object that was thrown, and the 'stack' argument contains the stack trace.
/// The callback is invoked after the information is printed to the console, and /// If no callback is set, then a default behaviour consisting of dumping the
/// could be used to print additional information, such as from /// context, exception, and stack trace to the console is used instead.
/// [debugDumpApp()].
WidgetsExceptionHandler debugWidgetsExceptionHandler; WidgetsExceptionHandler debugWidgetsExceptionHandler;
void _debugReportException(String context, dynamic exception, StackTrace stack) { void _debugReportException(String context, dynamic exception, StackTrace stack) {
if (debugWidgetsExceptionHandler != null) { if (debugWidgetsExceptionHandler != null) {
......
...@@ -163,14 +163,20 @@ class MixedViewportElement extends RenderObjectElement<MixedViewport> { ...@@ -163,14 +163,20 @@ class MixedViewportElement extends RenderObjectElement<MixedViewport> {
assert(renderObject != null); assert(renderObject != null);
final int startIndex = _firstVisibleChildIndex; final int startIndex = _firstVisibleChildIndex;
int lastIndex = startIndex + _childrenByKey.length - 1; int lastIndex = startIndex + _childrenByKey.length - 1;
for (int index = startIndex; index <= lastIndex; index += 1) { Element nextSibling = null;
for (int index = lastIndex; index > startIndex; index -= 1) {
final Widget newWidget = _buildWidgetAt(index); final Widget newWidget = _buildWidgetAt(index);
final _ChildKey key = new _ChildKey.fromWidget(newWidget); final _ChildKey key = new _ChildKey.fromWidget(newWidget);
final Element oldElement = _childrenByKey[key]; final Element oldElement = _childrenByKey[key];
assert(oldElement != null); assert(oldElement != null);
final Element newElement = updateChild(oldElement, newWidget, renderObject.childAfter(oldElement.renderObject)); final Element newElement = updateChild(oldElement, newWidget, nextSibling);
assert(newElement != null); assert(newElement != null);
_childrenByKey[key] = newElement; _childrenByKey[key] = newElement;
// Verify that it hasn't changed size.
// If this assertion fires, it means you didn't call "invalidate"
// before changing the size of one of your items.
assert(_debugIsSameSize(newElement, index, _lastLayoutConstraints));
nextSibling = newElement;
} }
} }
} }
...@@ -245,6 +251,16 @@ class MixedViewportElement extends RenderObjectElement<MixedViewport> { ...@@ -245,6 +251,16 @@ class MixedViewportElement extends RenderObjectElement<MixedViewport> {
return newElement; return newElement;
} }
// Build the widget at index.
Element _maybeGetElement(int index, BoxConstraints innerConstraints) {
assert(index <= _childOffsets.length - 1);
final Widget newWidget = _maybeBuildWidgetAt(index);
if (newWidget == null)
return null;
final Element newElement = _inflateOrUpdateWidget(newWidget);
return newElement;
}
// Build the widget at index, handling the case where there is no such widget. // Build the widget at index, handling the case where there is no such widget.
// Update the offset for that widget. // Update the offset for that widget.
Element _getElementAtLastKnownOffset(int index, BoxConstraints innerConstraints) { Element _getElementAtLastKnownOffset(int index, BoxConstraints innerConstraints) {
...@@ -257,14 +273,14 @@ class MixedViewportElement extends RenderObjectElement<MixedViewport> { ...@@ -257,14 +273,14 @@ class MixedViewportElement extends RenderObjectElement<MixedViewport> {
final Element newElement = _inflateOrUpdateWidget(newWidget); final Element newElement = _inflateOrUpdateWidget(newWidget);
// Update the offsets based on the newElement's dimensions. // Update the offsets based on the newElement's dimensions.
final double newOffset = _getOffset(newElement, innerConstraints); final double newOffset = _getElementExtent(newElement, innerConstraints);
_childOffsets.add(_childOffsets[index] + newOffset); _childOffsets.add(_childOffsets[index] + newOffset);
return newElement; return newElement;
} }
/// Returns the intrinsic size of the given element in the scroll direction /// Returns the intrinsic size of the given element in the scroll direction
double _getOffset(Element element, BoxConstraints innerConstraints) { double _getElementExtent(Element element, BoxConstraints innerConstraints) {
final RenderBox childRenderObject = element.renderObject; final RenderBox childRenderObject = element.renderObject;
switch (widget.direction) { switch (widget.direction) {
case ScrollDirection.vertical: case ScrollDirection.vertical:
...@@ -277,6 +293,38 @@ class MixedViewportElement extends RenderObjectElement<MixedViewport> { ...@@ -277,6 +293,38 @@ class MixedViewportElement extends RenderObjectElement<MixedViewport> {
} }
} }
BoxConstraints _getInnerConstraints(BoxConstraints constraints) {
switch (widget.direction) {
case ScrollDirection.vertical:
return new BoxConstraints.tightFor(width: constraints.constrainWidth());
case ScrollDirection.horizontal:
return new BoxConstraints.tightFor(height: constraints.constrainHeight());
case ScrollDirection.both:
assert(false); // we don't support ScrollDirection.both, see issue 888
return null;
}
}
/// This compares the offsets we had for an element with its current
/// intrinsic dimensions.
bool _debugIsSameSize(Element element, int index, BoxConstraints constraints) {
BoxConstraints innerConstraints = _getInnerConstraints(constraints);
// We multiple both sides by 32 and then round to avoid floating
// point errors. (You have to round, not truncate, because otherwise
// if the error is on either side of an integer, you'll magnify it
// rather than hiding it.)
// This is an issue because we don't actually record the raw data
// (the intrinsic dimensions), we record the offsets.
// The offsets therefore accumulate floating point errors. The
// errors are far too small to make the slightest diffference, but
// they're big enough to trip the assertion if we don't do this.
// We multiply by 32 so that we notice errors up to 1/32nd of a
// logical pixel. I'm assuming 32x resolution displays aren't going
// to happen. When I'm invariably proved wrong, just bump this up to
// a higher power of two.
return ((_childOffsets[index+1] - _childOffsets[index]) * 32.0).round() == (_getElementExtent(element, innerConstraints) * 32.0).round();
}
/// This is the core lazy-build algorithm. It builds widgets incrementally /// This is the core lazy-build algorithm. It builds widgets incrementally
/// from index 0 until it has built enough widgets to cover itself, and /// from index 0 until it has built enough widgets to cover itself, and
/// discards any widgets that are not displayed. /// discards any widgets that are not displayed.
...@@ -306,16 +354,7 @@ class MixedViewportElement extends RenderObjectElement<MixedViewport> { ...@@ -306,16 +354,7 @@ class MixedViewportElement extends RenderObjectElement<MixedViewport> {
final double endOffset = widget.startOffset + extent; final double endOffset = widget.startOffset + extent;
// Create the constraints that we will use to measure the children. // Create the constraints that we will use to measure the children.
BoxConstraints innerConstraints; final BoxConstraints innerConstraints = _getInnerConstraints(constraints);
switch (widget.direction) {
case ScrollDirection.vertical:
innerConstraints = new BoxConstraints.tightFor(width: constraints.constrainWidth());
break;
case ScrollDirection.horizontal:
innerConstraints = new BoxConstraints.tightFor(height: constraints.constrainHeight());
break;
case ScrollDirection.both: assert(false); // we don't support ScrollDirection.both, see issue 888
}
// Before doing the actual layout, fix the offsets for the widgets whose // Before doing the actual layout, fix the offsets for the widgets whose
// size or type has changed. // size or type has changed.
...@@ -323,7 +362,7 @@ class MixedViewportElement extends RenderObjectElement<MixedViewport> { ...@@ -323,7 +362,7 @@ class MixedViewportElement extends RenderObjectElement<MixedViewport> {
assert(_childOffsets.length > 0); assert(_childOffsets.length > 0);
List<int> invalidIndices = _invalidIndices.toList(); List<int> invalidIndices = _invalidIndices.toList();
invalidIndices.sort(); invalidIndices.sort();
for (int i = 0; i < invalidIndices.length - 1; i += 1) { for (int i = 0; i < invalidIndices.length; i += 1) {
// Determine the indices for this pass. // Determine the indices for this pass.
final int widgetIndex = invalidIndices[i]; final int widgetIndex = invalidIndices[i];
...@@ -346,7 +385,7 @@ class MixedViewportElement extends RenderObjectElement<MixedViewport> { ...@@ -346,7 +385,7 @@ class MixedViewportElement extends RenderObjectElement<MixedViewport> {
final Element newElement = _getElement(widgetIndex, innerConstraints); final Element newElement = _getElement(widgetIndex, innerConstraints);
// Update the offsets based on the newElement's dimensions. // Update the offsets based on the newElement's dimensions.
final double newOffset = _getOffset(newElement, innerConstraints); final double newOffset = _getElementExtent(newElement, innerConstraints);
final double oldOffset = _childOffsets[widgetIndex + 1] - _childOffsets[widgetIndex]; final double oldOffset = _childOffsets[widgetIndex + 1] - _childOffsets[widgetIndex];
final double offsetDelta = newOffset - oldOffset; final double offsetDelta = newOffset - oldOffset;
for (int j = widgetIndex + 1; j <= endIndex; j++) for (int j = widgetIndex + 1; j <= endIndex; j++)
...@@ -465,18 +504,20 @@ class MixedViewportElement extends RenderObjectElement<MixedViewport> { ...@@ -465,18 +504,20 @@ class MixedViewportElement extends RenderObjectElement<MixedViewport> {
// Build all the widgets we still need. // Build all the widgets we still need.
while (_childOffsets[index] < endOffset) { while (_childOffsets[index] < endOffset) {
if (!builtChildren.containsKey(index)) { if (!builtChildren.containsKey(index)) {
Element element = _getElement(index, innerConstraints); Element element = _maybeGetElement(index, innerConstraints);
if (element == null) { if (element == null) {
_didReachLastChild = true; _didReachLastChild = true;
break; break;
} }
if (index == _childOffsets.length-1) { if (index == _childOffsets.length-1) {
// Remember this element's offset. // Remember this element's offset.
final double newOffset = _getOffset(element, innerConstraints); final double newOffset = _getElementExtent(element, innerConstraints);
_childOffsets.add(_childOffsets[index] + newOffset); _childOffsets.add(_childOffsets[index] + newOffset);
} else { } else {
// Verify that it hasn't changed size. // Verify that it hasn't changed size.
assert(_childOffsets[index] - _childOffsets[index-1] == _getOffset(element, innerConstraints)); // If this assertion fires, it means you didn't call "invalidate"
// before changing the size of one of your items.
assert(_debugIsSameSize(element, index, constraints));
} }
// Remember the element for when we place the children. // Remember the element for when we place the children.
final _ChildKey key = new _ChildKey.fromWidget(element.widget); final _ChildKey key = new _ChildKey.fromWidget(element.widget);
......
...@@ -14,6 +14,7 @@ import 'package:sky/src/fn3/basic.dart'; ...@@ -14,6 +14,7 @@ import 'package:sky/src/fn3/basic.dart';
import 'package:sky/src/fn3/framework.dart'; import 'package:sky/src/fn3/framework.dart';
import 'package:sky/src/fn3/gesture_detector.dart'; import 'package:sky/src/fn3/gesture_detector.dart';
import 'package:sky/src/fn3/homogeneous_viewport.dart'; import 'package:sky/src/fn3/homogeneous_viewport.dart';
import 'package:sky/src/fn3/mixed_viewport.dart';
// The gesture velocity properties are pixels/second, config min,max limits are pixels/ms // The gesture velocity properties are pixels/second, config min,max limits are pixels/ms
const double _kMillisecondsPerSecond = 1000.0; const double _kMillisecondsPerSecond = 1000.0;
...@@ -386,7 +387,7 @@ abstract class ScrollableWidgetListState<T extends ScrollableWidgetList> extends ...@@ -386,7 +387,7 @@ abstract class ScrollableWidgetListState<T extends ScrollableWidgetList> extends
} }
void _updateScrollBehavior() { void _updateScrollBehavior() {
// if you don't call this from build() or syncConstructorArguments(), you must call it from setState(). // if you don't call this from build(), you must call it from setState().
double contentExtent = config.itemExtent * itemCount; double contentExtent = config.itemExtent * itemCount;
if (config.padding != null) if (config.padding != null)
contentExtent += _leadingPadding + _trailingPadding; contentExtent += _leadingPadding + _trailingPadding;
...@@ -536,4 +537,83 @@ class PageableListState<T> extends ScrollableListState<T, PageableList<T>> { ...@@ -536,4 +537,83 @@ class PageableListState<T> extends ScrollableListState<T, PageableList<T>> {
} }
} }
// TODO(abarth): ScrollableMixedWidgetList /// A general scrollable list for a large number of children that might not all
/// have the same height. Prefer [ScrollableWidgetList] when all the children
/// have the same height because it can use that property to be more efficient.
/// Prefer [ScrollableViewport] with a single child.
class ScrollableMixedWidgetList extends Scrollable {
ScrollableMixedWidgetList({
Key key,
double initialScrollOffset,
this.builder,
this.token,
this.onInvalidatorAvailable
}) : super(key: key, initialScrollOffset: initialScrollOffset);
final IndexedBuilder builder;
final Object token;
final InvalidatorAvailableCallback onInvalidatorAvailable;
ScrollableMixedWidgetListState createState() => new ScrollableMixedWidgetListState();
}
class ScrollableMixedWidgetListState extends ScrollableState<ScrollableMixedWidgetList> {
void initState(BuildContext context) {
super.initState(context);
scrollBehavior.updateExtents(
contentExtent: double.INFINITY
);
}
ScrollBehavior createScrollBehavior() => new OverscrollBehavior();
OverscrollBehavior get scrollBehavior => super.scrollBehavior;
void _handleSizeChanged(Size newSize) {
setState(() {
scrollBy(scrollBehavior.updateExtents(
containerExtent: newSize.height,
scrollOffset: scrollOffset
));
});
}
bool _contentChanged = false;
void didUpdateConfig(ScrollableMixedWidgetList oldConfig) {
super.didUpdateConfig(oldConfig);
if (config.token != oldConfig.token) {
// When the token changes the scrollable's contents may have changed.
// Remember as much so that after the new contents have been laid out we
// can adjust the scrollOffset so that the last page of content is still
// visible.
_contentChanged = true;
}
}
void _handleExtentsUpdate(double newExtents) {
double newScrollOffset;
setState(() {
newScrollOffset = scrollBehavior.updateExtents(
contentExtent: newExtents ?? double.INFINITY,
scrollOffset: scrollOffset
);
});
if (_contentChanged) {
_contentChanged = false;
scrollTo(newScrollOffset);
}
}
Widget buildContent(BuildContext context) {
return new SizeObserver(
callback: _handleSizeChanged,
child: new MixedViewport(
startOffset: scrollOffset,
builder: config.builder,
token: config.token,
onInvalidatorAvailable: config.onInvalidatorAvailable,
onExtentsUpdate: _handleExtentsUpdate
)
);
}
}
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