Commit 35ac1f71 authored by Adam Barth's avatar Adam Barth

Add dartdoc for base

parent 984a39f4
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
import 'dart:sky' as sky; import 'dart:sky' as sky;
/// Indicates whether we're running with asserts enabled.
final bool inDebugBuild = _initInDebugBuild(); final bool inDebugBuild = _initInDebugBuild();
bool _initInDebugBuild() { bool _initInDebugBuild() {
...@@ -16,16 +17,29 @@ bool _initInDebugBuild() { ...@@ -16,16 +17,29 @@ bool _initInDebugBuild() {
return _inDebug; return _inDebug;
} }
/// Causes each RenderBox to paint a box around its bounds.
bool debugPaintSizeEnabled = false; bool debugPaintSizeEnabled = false;
/// The color to use when painting RenderObject bounds.
const sky.Color debugPaintSizeColor = const sky.Color(0xFF00FFFF); const sky.Color debugPaintSizeColor = const sky.Color(0xFF00FFFF);
/// Causes each RenderBox to paint a line at each of its baselines.
bool debugPaintBaselinesEnabled = false; bool debugPaintBaselinesEnabled = false;
/// The color to use when painting alphabetic baselines.
const sky.Color debugPaintAlphabeticBaselineColor = const sky.Color(0xFF00FF00); const sky.Color debugPaintAlphabeticBaselineColor = const sky.Color(0xFF00FF00);
/// The color ot use when painting ideographic baselines.
const sky.Color debugPaintIdeographicBaselineColor = const sky.Color(0xFFFFD000); const sky.Color debugPaintIdeographicBaselineColor = const sky.Color(0xFFFFD000);
/// Causes each Layer to paint a box around its bounds.
bool debugPaintLayerBordersEnabled = false; bool debugPaintLayerBordersEnabled = false;
/// The color to use when painting Layer borders.
const sky.Color debugPaintLayerBordersColor = const sky.Color(0xFFFF9800); const sky.Color debugPaintLayerBordersColor = const sky.Color(0xFFFF9800);
/// Causes RenderObjects to paint warnings when painting outside their bounds.
bool debugPaintBoundsEnabled = false; bool debugPaintBoundsEnabled = false;
/// Slows down animations by this factor to help in development.
double timeDilation = 1.0; double timeDilation = 1.0;
...@@ -4,12 +4,22 @@ ...@@ -4,12 +4,22 @@
import 'dart:sky' as sky; import 'dart:sky' as sky;
/// The outcome of running an event handler.
enum EventDisposition { enum EventDisposition {
/// The event handler ignored this event.
ignored, ignored,
/// The event handler did not ignore the event but other event handlers should
/// process the event as well.
processed, processed,
/// The event handler did not ignore the event and other event handlers
/// should not process the event.
consumed, consumed,
} }
/// Merges two [EventDisposition] values such that the result indicates the
/// maximum amount of processing indicated by the two inputs.
EventDisposition combineEventDispositions(EventDisposition left, EventDisposition right) { EventDisposition combineEventDispositions(EventDisposition left, EventDisposition right) {
if (left == EventDisposition.consumed || right == EventDisposition.consumed) if (left == EventDisposition.consumed || right == EventDisposition.consumed)
return EventDisposition.consumed; return EventDisposition.consumed;
...@@ -18,22 +28,42 @@ EventDisposition combineEventDispositions(EventDisposition left, EventDispositio ...@@ -18,22 +28,42 @@ EventDisposition combineEventDispositions(EventDisposition left, EventDispositio
return EventDisposition.ignored; return EventDisposition.ignored;
} }
/// An object that can handle events.
abstract class HitTestTarget { abstract class HitTestTarget {
/// Override this function to receive events.
EventDisposition handleEvent(sky.Event event, HitTestEntry entry); EventDisposition handleEvent(sky.Event event, HitTestEntry entry);
} }
/// Data collected during a hit test about a specific [HitTestTarget].
///
/// Subclass this object to pass additional information from the hit test phase
/// to the event propagation phase.
class HitTestEntry { class HitTestEntry {
const HitTestEntry(this.target); const HitTestEntry(this.target);
/// The [HitTestTarget] encountered during the hit test.
final HitTestTarget target; final HitTestTarget target;
} }
/// The result of performing a hit test.
class HitTestResult { class HitTestResult {
HitTestResult({ List<HitTestEntry> path }) HitTestResult({ List<HitTestEntry> path })
: path = path != null ? path : new List<HitTestEntry>(); : path = path != null ? path : new List<HitTestEntry>();
/// The list of [HitTestEntry] objects recorded during the hit test.
///
/// The first entry in the path is the least specific, typically the one at
/// the root of tree being hit tested. Event propagation starts with the most
/// specific (i.e., last) entry and proceeds in reverse order through the
/// path.
final List<HitTestEntry> path; final List<HitTestEntry> path;
void add(HitTestEntry data) { /// Add a [HitTestEntry] to the path.
path.add(data); ///
/// The new entry is added at the end of the path, which means entries should
/// be added in order from last specific to most specific, typically during a
/// downward walk in the tree being hit tested.
void add(HitTestEntry entry) {
path.add(entry);
} }
} }
...@@ -5,8 +5,14 @@ ...@@ -5,8 +5,14 @@
import 'dart:async'; import 'dart:async';
import 'dart:sky' as sky; import 'dart:sky' as sky;
/// A callback for when the image is available.
typedef void ImageListener(sky.Image image); typedef void ImageListener(sky.Image image);
/// A handle to an image resource
///
/// ImageResource represents a handle to a [sky.Image] object. The underlying
/// image object might change over time, either because the image is animating
/// or because the underlying image resource was mutated.
class ImageResource { class ImageResource {
ImageResource(this._futureImage) { ImageResource(this._futureImage) {
_futureImage.then(_handleImageLoaded, onError: _handleImageError); _futureImage.then(_handleImageLoaded, onError: _handleImageError);
...@@ -17,14 +23,22 @@ class ImageResource { ...@@ -17,14 +23,22 @@ class ImageResource {
sky.Image _image; sky.Image _image;
final List<ImageListener> _listeners = new List<ImageListener>(); final List<ImageListener> _listeners = new List<ImageListener>();
/// The first concrete [sky.Image] object represented by this handle.
///
/// Instead of receivingly only the first image, most clients will want to
/// [addListener] to be notified whenever a a concrete image is available.
Future<sky.Image> get first => _futureImage; Future<sky.Image> get first => _futureImage;
/// Adds a listener callback that is called whenever a concrete [sky.Image]
/// object is available. Note: If a concrete image is available currently,
/// this object will call the listener synchronously.
void addListener(ImageListener listener) { void addListener(ImageListener listener) {
_listeners.add(listener); _listeners.add(listener);
if (_resolved) if (_resolved)
listener(_image); listener(_image);
} }
/// Stop listening for new concrete [sky.Image] objects.
void removeListener(ImageListener listener) { void removeListener(ImageListener listener) {
_listeners.remove(listener); _listeners.remove(listener);
} }
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
import 'dart:sky'; import 'dart:sky';
/// Linearly interpolate between two numbers.
num lerpNum(num a, num b, double t) { num lerpNum(num a, num b, double t) {
if (a == null && b == null) if (a == null && b == null)
return null; return null;
...@@ -18,6 +19,10 @@ Color _scaleAlpha(Color a, double factor) { ...@@ -18,6 +19,10 @@ Color _scaleAlpha(Color a, double factor) {
return a.withAlpha((a.alpha * factor).round()); return a.withAlpha((a.alpha * factor).round());
} }
/// Linearly interpolate between two [Color] objects.
///
/// If either color is null, this function linearly interpolates from a
/// transparent instance of othe other color.
Color lerpColor(Color a, Color b, double t) { Color lerpColor(Color a, Color b, double t) {
if (a == null && b == null) if (a == null && b == null)
return null; return null;
...@@ -33,6 +38,9 @@ Color lerpColor(Color a, Color b, double t) { ...@@ -33,6 +38,9 @@ Color lerpColor(Color a, Color b, double t) {
); );
} }
/// Linearly interpolate between two [Offset] objects.
///
/// If either offset is null, this function interpolates from [Offset.zero].
Offset lerpOffset(Offset a, Offset b, double t) { Offset lerpOffset(Offset a, Offset b, double t) {
if (a == null && b == null) if (a == null && b == null)
return null; return null;
...@@ -43,6 +51,9 @@ Offset lerpOffset(Offset a, Offset b, double t) { ...@@ -43,6 +51,9 @@ Offset lerpOffset(Offset a, Offset b, double t) {
return new Offset(lerpNum(a.dx, b.dx, t), lerpNum(a.dy, b.dy, t)); return new Offset(lerpNum(a.dx, b.dx, t), lerpNum(a.dy, b.dy, t));
} }
/// Linearly interpolate between two [Rect] objects.
///
/// If either rect is null, this function interpolates from 0x0x0x0.
Rect lerpRect(Rect a, Rect b, double t) { Rect lerpRect(Rect a, Rect b, double t) {
if (a == null && b == null) if (a == null && b == null)
return null; return null;
......
...@@ -2,45 +2,73 @@ ...@@ -2,45 +2,73 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
/// An abstract node in a tree
///
/// AbstractNode has as notion of depth, attachment, and parent, but does not
/// have a model for children.
class AbstractNode { class AbstractNode {
// AbstractNode represents a node in a tree. // AbstractNode represents a node in a tree.
// The AbstractNode protocol is described in README.md. // The AbstractNode protocol is described in README.md.
int _depth = 0; int _depth = 0;
/// The depth of this node in the tree.
///
/// The depth of nodes in a tree monotonically increases as you traverse down
/// the trees.
int get depth => _depth; int get depth => _depth;
void redepthChild(AbstractNode child) { // internal, do not call
/// Call only from overrides of [redepthChildren]
void redepthChild(AbstractNode child) {
assert(child._attached == _attached); assert(child._attached == _attached);
if (child._depth <= _depth) { if (child._depth <= _depth) {
child._depth = _depth + 1; child._depth = _depth + 1;
child.redepthChildren(); child.redepthChildren();
} }
} }
void redepthChildren() { // internal, do not call
// override this in subclasses with child nodes /// Override this function in subclasses with child nodes to call
// simply call redepthChild(child) for each child /// redepthChild(child) for each child. Do not call directly.
} void redepthChildren() { }
bool _attached = false; bool _attached = false;
/// Whether this node is in a tree whose root is attached to something.
bool get attached => _attached; bool get attached => _attached;
/// Mark this node as attached.
///
/// Typically called only from overrides of [attachChildren] and to mark the
/// root of a tree attached.
void attach() { void attach() {
// override this in subclasses with child nodes
// simply call attach() for each child then call your superclass
_attached = true; _attached = true;
attachChildren(); attachChildren();
} }
attachChildren() { } // workaround for lack of inter-class mixins in Dart
/// Override this function in subclasses with child to call attach() for each
/// child. Do not call directly.
attachChildren() { }
/// Mark this node as detached.
///
/// Typically called only from overrides for [detachChildren] and to mark the
/// root of a tree detached.
void detach() { void detach() {
// override this in subclasses with child nodes
// simply call detach() for each child then call your superclass
_attached = false; _attached = false;
detachChildren(); detachChildren();
} }
detachChildren() { } // workaround for lack of inter-class mixins in Dart
/// Override this function in subclasses with child to call detach() for each
/// child. Do not call directly.
detachChildren() { }
// TODO(ianh): remove attachChildren()/detachChildren() workaround once mixins can use super.
AbstractNode _parent; AbstractNode _parent;
/// The parent of this node in the tree.
AbstractNode get parent => _parent; AbstractNode get parent => _parent;
void adoptChild(AbstractNode child) { // only for use by subclasses
/// Subclasses should call this function when they acquire a new child.
void adoptChild(AbstractNode child) {
assert(child != null); assert(child != null);
assert(child._parent == null); assert(child._parent == null);
child._parent = this; child._parent = this;
...@@ -48,7 +76,9 @@ class AbstractNode { ...@@ -48,7 +76,9 @@ class AbstractNode {
child.attach(); child.attach();
redepthChild(child); redepthChild(child);
} }
void dropChild(AbstractNode child) { // only for use by subclasses
/// Subclasses should call this function when they lose a child.
void dropChild(AbstractNode child) {
assert(child != null); assert(child != null);
assert(child._parent == this); assert(child._parent == this);
assert(child.attached == attached); assert(child.attached == attached);
......
...@@ -4,31 +4,44 @@ ...@@ -4,31 +4,44 @@
import 'dart:sky' as sky; import 'dart:sky' as sky;
typedef void _Route(sky.PointerEvent event); /// A callback that receives a [sky.PointerEvent]
typedef void PointerRoute(sky.PointerEvent event);
/// A routing table for [sky.PointerEvent] events.
class PointerRouter { class PointerRouter {
final Map<int, List<_Route>> _routeMap = new Map<int, List<_Route>>(); final Map<int, List<PointerRoute>> _routeMap = new Map<int, List<PointerRoute>>();
void addRoute(int pointer, _Route route) { /// Adds a route to the routing table
List<_Route> routes = _routeMap.putIfAbsent(pointer, () => new List<_Route>()); ///
/// Whenever this object routes a [sky.PointerEvent] corresponding to
/// pointer, call route.
void addRoute(int pointer, PointerRoute route) {
List<PointerRoute> routes = _routeMap.putIfAbsent(pointer, () => new List<PointerRoute>());
assert(!routes.contains(route)); assert(!routes.contains(route));
routes.add(route); routes.add(route);
} }
void removeRoute(int pointer, _Route route) { /// Removes a route from the routing table
///
/// No longer call route when routing a [sky.PointerEvent] corresponding to
/// pointer. Requires that this route was previously added to the router.
void removeRoute(int pointer, PointerRoute route) {
assert(_routeMap.containsKey(pointer)); assert(_routeMap.containsKey(pointer));
List<_Route> routes = _routeMap[pointer]; List<PointerRoute> routes = _routeMap[pointer];
assert(routes.contains(route)); assert(routes.contains(route));
routes.remove(route); routes.remove(route);
if (routes.isEmpty) if (routes.isEmpty)
_routeMap.remove(pointer); _routeMap.remove(pointer);
} }
/// Call the routes registed for this pointer event.
///
/// Calls the routes in the order in which they were added to the route.
void route(sky.PointerEvent event) { void route(sky.PointerEvent event) {
List<_Route> routes = _routeMap[event.pointer]; List<PointerRoute> routes = _routeMap[event.pointer];
if (routes == null) if (routes == null)
return; return;
for (_Route route in new List<_Route>.from(routes)) for (PointerRoute route in new List<PointerRoute>.from(routes))
route(event); route(event);
} }
} }
...@@ -7,22 +7,33 @@ import 'dart:sky' as sky; ...@@ -7,22 +7,33 @@ import 'dart:sky' as sky;
import 'package:sky/base/debug.dart'; import 'package:sky/base/debug.dart';
typedef void Callback(double timeStamp); /// A callback from the scheduler
///
/// The timeStamp is the number of milliseconds since the beginning of the
/// scheduler's epoch. Use timeStamp to determine how far to advance animation
/// timelines so that all the animations in the system are synchronized to a
/// common time base.
typedef void SchedulerCallback(double timeStamp);
bool _haveScheduledVisualUpdate = false; bool _haveScheduledVisualUpdate = false;
int _nextCallbackId = 1; int _nextCallbackId = 1;
final List<Callback> _persistentCallbacks = new List<Callback>(); final List<SchedulerCallback> _persistentCallbacks = new List<SchedulerCallback>();
Map<int, Callback> _transientCallbacks = new LinkedHashMap<int, Callback>(); Map<int, SchedulerCallback> _transientCallbacks = new LinkedHashMap<int, SchedulerCallback>();
final Set<int> _removedIds = new Set<int>(); final Set<int> _removedIds = new Set<int>();
/// Called by the engine to produce a new frame.
///
/// This function first calls all the callbacks registered by
/// [requestAnimationFrame] and then calls all the callbacks registered by
/// [addPersistentFrameCallback], which typically drive the rendering pipeline.
void beginFrame(double timeStamp) { void beginFrame(double timeStamp) {
timeStamp /= timeDilation; timeStamp /= timeDilation;
_haveScheduledVisualUpdate = false; _haveScheduledVisualUpdate = false;
Map<int, Callback> callbacks = _transientCallbacks; Map<int, SchedulerCallback> callbacks = _transientCallbacks;
_transientCallbacks = new Map<int, Callback>(); _transientCallbacks = new Map<int, SchedulerCallback>();
callbacks.forEach((id, callback) { callbacks.forEach((id, callback) {
if (!_removedIds.contains(id)) if (!_removedIds.contains(id))
...@@ -30,30 +41,45 @@ void beginFrame(double timeStamp) { ...@@ -30,30 +41,45 @@ void beginFrame(double timeStamp) {
}); });
_removedIds.clear(); _removedIds.clear();
for (Callback callback in _persistentCallbacks) for (SchedulerCallback callback in _persistentCallbacks)
callback(timeStamp); callback(timeStamp);
} }
/// Registers [beginFrame] callback with the engine.
void init() { void init() {
sky.view.setFrameCallback(beginFrame); sky.view.setFrameCallback(beginFrame);
} }
void addPersistentFrameCallback(Callback callback) { /// Call callback every frame.
void addPersistentFrameCallback(SchedulerCallback callback) {
_persistentCallbacks.add(callback); _persistentCallbacks.add(callback);
} }
int requestAnimationFrame(Callback callback) { /// Schedule a callback for the next frame.
///
/// The callback will be run prior to flushing the main rendering pipeline.
/// Typically, requestAnimationFrame is used to throttle writes into the
/// rendering pipeline until the system is ready to accept a new frame. For
/// example, if you wanted to tick through an animation, you should use
/// requestAnimation frame to determine when to tick the animation. The callback
/// is passed a timeStamp that you can use to determine how far along the
/// timeline to advance your animation.
///
/// Returns an id that can be used to unschedule this callback.
int requestAnimationFrame(SchedulerCallback callback) {
int id = _nextCallbackId++; int id = _nextCallbackId++;
_transientCallbacks[id] = callback; _transientCallbacks[id] = callback;
ensureVisualUpdate(); ensureVisualUpdate();
return id; return id;
} }
/// Cancel the callback identified by id.
void cancelAnimationFrame(int id) { void cancelAnimationFrame(int id) {
_transientCallbacks.remove(id); _transientCallbacks.remove(id);
_removedIds.add(id); _removedIds.add(id);
} }
/// Ensure that a frame will be produced after this function is called.
void ensureVisualUpdate() { void ensureVisualUpdate() {
if (_haveScheduledVisualUpdate) if (_haveScheduledVisualUpdate)
return; return;
......
...@@ -4,21 +4,21 @@ ...@@ -4,21 +4,21 @@
/// Includes and re-exports all Sky rendering classes. /// Includes and re-exports all Sky rendering classes.
export 'rendering/auto_layout.dart'; export 'package:sky/rendering/auto_layout.dart';
export 'rendering/block.dart'; export 'package:sky/rendering/block.dart';
export 'rendering/box.dart'; export 'package:sky/rendering/box.dart';
export 'rendering/flex.dart'; export 'package:sky/rendering/flex.dart';
export 'rendering/grid.dart'; export 'package:sky/rendering/grid.dart';
export 'rendering/image.dart'; export 'package:sky/rendering/image.dart';
export 'rendering/layer.dart'; export 'package:sky/rendering/layer.dart';
export 'rendering/object.dart'; export 'package:sky/rendering/object.dart';
export 'rendering/paragraph.dart'; export 'package:sky/rendering/paragraph.dart';
export 'rendering/proxy_box.dart'; export 'package:sky/rendering/proxy_box.dart';
export 'rendering/shifted_box.dart'; export 'package:sky/rendering/shifted_box.dart';
export 'rendering/sky_binding.dart'; export 'package:sky/rendering/sky_binding.dart';
export 'rendering/stack.dart'; export 'package:sky/rendering/stack.dart';
export 'rendering/toggleable.dart'; export 'package:sky/rendering/toggleable.dart';
export 'rendering/view.dart'; export 'package:sky/rendering/view.dart';
export 'rendering/viewport.dart'; export 'package:sky/rendering/viewport.dart';
export 'package:vector_math/vector_math.dart' show Matrix4; export 'package:vector_math/vector_math.dart' show Matrix4;
...@@ -4,49 +4,49 @@ ...@@ -4,49 +4,49 @@
/// Includes and re-exports all Sky widgets classes. /// Includes and re-exports all Sky widgets classes.
export 'widgets/animated_component.dart'; export 'package:sky/widgets/animated_component.dart';
export 'widgets/animated_container.dart'; export 'package:sky/widgets/animated_container.dart';
export 'widgets/basic.dart'; export 'package:sky/widgets/basic.dart';
export 'widgets/button_base.dart'; export 'package:sky/widgets/button_base.dart';
export 'widgets/card.dart'; export 'package:sky/widgets/card.dart';
export 'widgets/checkbox.dart'; export 'package:sky/widgets/checkbox.dart';
export 'widgets/date_picker.dart'; export 'package:sky/widgets/date_picker.dart';
export 'widgets/default_text_style.dart'; export 'package:sky/widgets/default_text_style.dart';
export 'widgets/dialog.dart'; export 'package:sky/widgets/dialog.dart';
export 'widgets/dismissable.dart'; export 'package:sky/widgets/dismissable.dart';
export 'widgets/drag_target.dart'; export 'package:sky/widgets/drag_target.dart';
export 'widgets/drawer.dart'; export 'package:sky/widgets/drawer.dart';
export 'widgets/drawer_divider.dart'; export 'package:sky/widgets/drawer_divider.dart';
export 'widgets/drawer_header.dart'; export 'package:sky/widgets/drawer_header.dart';
export 'widgets/drawer_item.dart'; export 'package:sky/widgets/drawer_item.dart';
export 'widgets/flat_button.dart'; export 'package:sky/widgets/flat_button.dart';
export 'widgets/floating_action_button.dart'; export 'package:sky/widgets/floating_action_button.dart';
export 'widgets/focus.dart'; export 'package:sky/widgets/focus.dart';
export 'widgets/framework.dart'; export 'package:sky/widgets/framework.dart';
export 'widgets/gesture_detector.dart'; export 'package:sky/widgets/gesture_detector.dart';
export 'widgets/icon.dart'; export 'package:sky/widgets/icon.dart';
export 'widgets/icon_button.dart'; export 'package:sky/widgets/icon_button.dart';
export 'widgets/ink_well.dart'; export 'package:sky/widgets/ink_well.dart';
export 'widgets/material.dart'; export 'package:sky/widgets/material.dart';
export 'widgets/material_button.dart'; export 'package:sky/widgets/material_button.dart';
export 'widgets/mimic.dart'; export 'package:sky/widgets/mimic.dart';
export 'widgets/mimic_overlay.dart'; export 'package:sky/widgets/mimic_overlay.dart';
export 'widgets/mixed_viewport.dart'; export 'package:sky/widgets/mixed_viewport.dart';
export 'widgets/modal_overlay.dart'; export 'package:sky/widgets/modal_overlay.dart';
export 'widgets/navigator.dart'; export 'package:sky/widgets/navigator.dart';
export 'widgets/popup_menu.dart'; export 'package:sky/widgets/popup_menu.dart';
export 'widgets/popup_menu_item.dart'; export 'package:sky/widgets/popup_menu_item.dart';
export 'widgets/progress_indicator.dart'; export 'package:sky/widgets/progress_indicator.dart';
export 'widgets/radio.dart'; export 'package:sky/widgets/radio.dart';
export 'widgets/raised_button.dart'; export 'package:sky/widgets/raised_button.dart';
export 'widgets/scaffold.dart'; export 'package:sky/widgets/scaffold.dart';
export 'widgets/scrollable.dart'; export 'package:sky/widgets/scrollable.dart';
export 'widgets/snack_bar.dart'; export 'package:sky/widgets/snack_bar.dart';
export 'widgets/switch.dart'; export 'package:sky/widgets/switch.dart';
export 'widgets/tabs.dart'; export 'package:sky/widgets/tabs.dart';
export 'widgets/theme.dart'; export 'package:sky/widgets/theme.dart';
export 'widgets/title.dart'; export 'package:sky/widgets/title.dart';
export 'widgets/tool_bar.dart'; export 'package:sky/widgets/tool_bar.dart';
export 'widgets/transitions.dart'; export 'package:sky/widgets/transitions.dart';
export 'package:vector_math/vector_math.dart' show Matrix4; export 'package:vector_math/vector_math.dart' show Matrix4;
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