Commit 3122c8a9 authored by Neil Fraser's avatar Neil Fraser

De-singleton Blockly. Part 1.

parent 6dc68379
......@@ -622,7 +622,7 @@ Blockly.Block.prototype.getFieldValue = function(name) {
* @deprecated December 2013
*/
Blockly.Block.prototype.getTitleValue = function(name) {
console.log('Deprecated call to getTitleValue, use getFieldValue instead.');
console.warn('Deprecated call to getTitleValue, use getFieldValue instead.');
return this.getFieldValue(name);
};
......@@ -644,7 +644,7 @@ Blockly.Block.prototype.setFieldValue = function(newValue, name) {
* @deprecated December 2013
*/
Blockly.Block.prototype.setTitleValue = function(newValue, name) {
console.log('Deprecated call to setTitleValue, use setFieldValue instead.');
console.warn('Deprecated call to setTitleValue, use setFieldValue instead.');
this.setFieldValue(newValue, name);
};
......
......@@ -484,7 +484,7 @@ Blockly.BlockSvg.prototype.showHelp_ = function() {
* @private
*/
Blockly.BlockSvg.prototype.showContextMenu_ = function(e) {
if (Blockly.readOnly || !this.contextMenu) {
if (this.workspace.options.readOnly || !this.contextMenu) {
return;
}
// Save the current block in a variable for use in closures.
......@@ -505,7 +505,8 @@ Blockly.BlockSvg.prototype.showContextMenu_ = function(e) {
}
options.push(duplicateOption);
if (this.isEditable() && !this.collapsed_ && Blockly.comments) {
if (this.isEditable() && !this.collapsed_ &&
this.workspace.options.comments) {
// Option to add/remove a comment.
var commentOption = {enabled: true};
if (this.comment) {
......@@ -539,7 +540,7 @@ Blockly.BlockSvg.prototype.showContextMenu_ = function(e) {
}
}
if (Blockly.collapse) {
if (this.workspace.options.collapse) {
// Option to collapse/expand block.
if (this.collapsed_) {
var expandOption = {enabled: true};
......@@ -558,7 +559,7 @@ Blockly.BlockSvg.prototype.showContextMenu_ = function(e) {
}
}
if (Blockly.disable) {
if (this.workspace.options.disable) {
// Option to disable/enable block.
var disableOption = {
text: this.disabled ?
......
......@@ -54,12 +54,6 @@ goog.require('goog.color');
goog.require('goog.userAgent');
/**
* Path to Blockly's media directory. Can be relative, absolute, or remote.
* Used for loading sounds and sprites. Defaults to demo server.
*/
Blockly.pathToMedia = 'https://blockly-demo.appspot.com/static/media/';
/**
* Required name space for SVG elements.
* @const
......@@ -383,7 +377,6 @@ Blockly.onKeyDown_ = function(e) {
// When focused on an HTML text input widget, don't trap any keys.
return;
}
// TODO: Add keyboard support for cursoring around the context menu.
if (e.keyCode == 27) {
// Pressing esc closes the context menu.
Blockly.hideChaff();
......@@ -494,17 +487,18 @@ Blockly.copy_ = function(block) {
* @private
*/
Blockly.showContextMenu_ = function(e) {
if (Blockly.readOnly) {
var workspace = Blockly.mainWorkspace;
if (workspace.options.readOnly) {
return;
}
var options = [];
// Add a little animation to collapsing and expanding.
var COLLAPSE_DELAY = 10;
if (Blockly.collapse) {
if (workspace.options.collapse) {
var hasCollapsedBlocks = false;
var hasExpandedBlocks = false;
var topBlocks = Blockly.mainWorkspace.getTopBlocks(false);
var topBlocks = workspace.getTopBlocks(false);
for (var i = 0; i < topBlocks.length; i++) {
var block = topBlocks[i];
while (block) {
......@@ -702,19 +696,20 @@ Blockly.playAudio = function(name, opt_volume) {
* @private
*/
Blockly.getMainWorkspaceMetrics_ = function() {
var mainWorkspace = Blockly.mainWorkspace;
var svgSize = Blockly.svgSize();
if (Blockly.mainWorkspace.toolbox_) {
svgSize.width -= Blockly.mainWorkspace.toolbox_.width;
if (mainWorkspace.toolbox_) {
svgSize.width -= mainWorkspace.toolbox_.width;
}
var viewWidth = svgSize.width - Blockly.Scrollbar.scrollbarThickness;
var viewHeight = svgSize.height - Blockly.Scrollbar.scrollbarThickness;
try {
var blockBox = Blockly.mainWorkspace.getCanvas().getBBox();
var blockBox = mainWorkspace.getCanvas().getBBox();
} catch (e) {
// Firefox has trouble with hidden elements (Bug 528969).
return null;
}
if (Blockly.mainWorkspace.scrollbar) {
if (mainWorkspace.scrollbar) {
// Add a border around the content that is at least half a screenful wide.
// Ensure border is wide enough that blocks can scroll over entire screen.
var MARGIN = 5;
......@@ -736,16 +731,16 @@ Blockly.getMainWorkspaceMetrics_ = function() {
var bottomEdge = topEdge + blockBox.height;
}
var absoluteLeft = 0;
if (!Blockly.RTL && Blockly.mainWorkspace.toolbox_) {
absoluteLeft = Blockly.mainWorkspace.toolbox_.width;
if (!Blockly.RTL && mainWorkspace.toolbox_) {
absoluteLeft = mainWorkspace.toolbox_.width;
}
var metrics = {
viewHeight: svgSize.height,
viewWidth: svgSize.width,
contentHeight: bottomEdge - topEdge,
contentWidth: rightEdge - leftEdge,
viewTop: -Blockly.mainWorkspace.scrollY,
viewLeft: -Blockly.mainWorkspace.scrollX,
viewTop: -mainWorkspace.scrollY,
viewLeft: -mainWorkspace.scrollX,
contentTop: topEdge,
contentLeft: leftEdge,
absoluteTop: 0,
......@@ -761,23 +756,24 @@ Blockly.getMainWorkspaceMetrics_ = function() {
* @private
*/
Blockly.setMainWorkspaceMetrics_ = function(xyRatio) {
if (!Blockly.mainWorkspace.scrollbar) {
var mainWorkspace = Blockly.mainWorkspace;
if (!mainWorkspace.scrollbar) {
throw 'Attempt to set main workspace scroll without scrollbars.';
}
var metrics = Blockly.getMainWorkspaceMetrics_();
if (goog.isNumber(xyRatio.x)) {
Blockly.mainWorkspace.scrollX = -metrics.contentWidth * xyRatio.x -
mainWorkspace.scrollX = -metrics.contentWidth * xyRatio.x -
metrics.contentLeft;
}
if (goog.isNumber(xyRatio.y)) {
Blockly.mainWorkspace.scrollY = -metrics.contentHeight * xyRatio.y -
mainWorkspace.scrollY = -metrics.contentHeight * xyRatio.y -
metrics.contentTop;
}
var x = Blockly.mainWorkspace.scrollX + metrics.absoluteLeft;
var y = Blockly.mainWorkspace.scrollY + metrics.absoluteTop;
Blockly.mainWorkspace.translate(x, y);
Blockly.mainWorkspacePattern_.setAttribute('x', x);
Blockly.mainWorkspacePattern_.setAttribute('y', y);
var x = mainWorkspace.scrollX + metrics.absoluteLeft;
var y = mainWorkspace.scrollY + metrics.absoluteTop;
mainWorkspace.translate(x, y);
mainWorkspace.gridPattern_.setAttribute('x', x);
mainWorkspace.gridPattern_.setAttribute('y', y);
};
/**
......
......@@ -66,15 +66,18 @@ Blockly.Css.mediaPath_ = '';
* a) It loads synchronously and doesn't force a redraw later.
* b) It speeds up loading by not blocking on a separate HTTP transfer.
* c) The CSS content may be made dynamic depending on init options.
* @param {boolean} hasCss If false, don't inject CSS
* (providing CSS becomes the document's responsibility).
* @param {string} pathToMedia Path from page to the Blockly media directory.
*/
Blockly.Css.inject = function() {
Blockly.Css.inject = function(hasCss, pathToMedia) {
// Placeholder for cursor rule. Must be first rule (index 0).
var text = '.blocklyDraggable {}\n';
if (Blockly.hasCss) {
if (hasCss) {
text += Blockly.Css.CONTENT.join('\n');
}
// Strip off any trailing slash (either Unix or Windows).
Blockly.Css.mediaPath_ = Blockly.pathToMedia.replace(/[\\\/]$/, '');
Blockly.Css.mediaPath_ = pathToMedia.replace(/[\\\/]$/, '');
text = text.replace(/<<<PATH>>>/g, Blockly.Css.mediaPath_);
Blockly.Css.styleSheet_ = goog.cssom.addCssText(text).sheet;
Blockly.Css.setCursor(Blockly.Css.Cursor.OPEN);
......
......@@ -34,31 +34,32 @@ goog.require('goog.userAgent');
/**
* Initialize the SVG document with various handlers.
* Inject a Blockly editor into the specified container DIV.
* @param {!Element} container Containing element.
* @param {Object} opt_options Optional dictionary of options.
* @return {!Blockly.Workspace} Newly created main workspace.
*/
Blockly.inject = function(container, opt_options) {
// Verify that the container is in document.
if (!goog.dom.contains(document, container)) {
throw 'Error: container is not in current document.';
}
if (opt_options) {
Blockly.parseOptions_(opt_options);
}
var options = Blockly.parseOptions_(opt_options || {});
var workspace;
var startUi = function() {
Blockly.createDom_(container);
Blockly.init_();
workspace = Blockly.createDom_(container, options);
Blockly.init_(workspace);
};
if (Blockly.enableRealtime) {
if (options.enableRealtime) {
var realtimeElement = document.getElementById('realtime');
if (realtimeElement) {
realtimeElement.style.display = 'block';
}
Blockly.Realtime.startRealtime(startUi, container, Blockly.realtimeOptions);
Blockly.Realtime.startRealtime(startUi, container, options.realtimeOptions);
} else {
startUi();
}
return workspace;
};
/**
......@@ -87,7 +88,9 @@ Blockly.parseToolboxTree_ = function(tree) {
/**
* Configure Blockly to behave according to a set of options.
* @param {!Object} options Dictionary of options.
* @param {!Object} options Dictionary of options. Specification:
* https://developers.google.com/blockly/installation/overview#configuration
* @return {!Object} Dictionary of normalized options.
* @private
*/
Blockly.parseOptions_ = function(options) {
......@@ -147,38 +150,44 @@ Blockly.parseOptions_ = function(options) {
grid['length'] = parseFloat(grid['length']);
}
grid['snap'] = !!grid['snap'];
var enableRealtime = !!options['realtime'];
var realtimeOptions = enableRealtime ? options['realtimeOptions'] : undefined;
Blockly.RTL = !!options['rtl'];
Blockly.collapse = hasCollapse;
Blockly.comments = hasComments;
Blockly.disable = hasDisable;
Blockly.readOnly = readOnly;
Blockly.maxBlocks = options['maxBlocks'] || Infinity;
var pathToMedia = 'https://blockly-demo.appspot.com/static/media/';
if (options['media']) {
Blockly.pathToMedia = options['media'];
pathToMedia = options['media'];
} else if (options['path']) {
// 'path' is a deprecated option which has been replaced by 'media'.
Blockly.pathToMedia = options['path'] + 'media/';
pathToMedia = options['path'] + 'media/';
}
Blockly.hasCategories = hasCategories;
Blockly.hasScrollbars = hasScrollbars;
Blockly.hasTrashcan = hasTrashcan;
Blockly.hasSounds = hasSounds;
Blockly.hasCss = hasCss;
Blockly.languageTree = tree;
Blockly.gridOptions = grid;
Blockly.enableRealtime = enableRealtime;
Blockly.realtimeOptions = realtimeOptions;
var enableRealtime = !!options['realtime'];
var realtimeOptions = enableRealtime ? options['realtimeOptions'] : undefined;
return {
RTL: !!options['rtl'],
collapse: hasCollapse,
comments: hasComments,
disable: hasDisable,
readOnly: readOnly,
maxBlocks: options['maxBlocks'] || Infinity,
pathToMedia: pathToMedia,
hasCategories: hasCategories,
hasScrollbars: hasScrollbars,
hasTrashcan: hasTrashcan,
hasSounds: hasSounds,
hasCss: hasCss,
languageTree: tree,
gridOptions: grid,
enableRealtime: enableRealtime,
realtimeOptions: realtimeOptions
};
};
/**
* Create the SVG image.
* @param {!Element} container Containing element.
* @param {Object} options Dictionary of options.
* @return {!Blockly.Workspace} Newly created main workspace.
* @private
*/
Blockly.createDom_ = function(container) {
Blockly.createDom_ = function(container, options) {
// Sadly browsers (Chrome vs Firefox) are currently inconsistent in laying
// out content in RTL mode. Therefore Blockly forces the use of LTR,
// then manually positions content in RTL as needed.
......@@ -187,7 +196,7 @@ Blockly.createDom_ = function(container) {
goog.ui.Component.setDefaultRightToLeft(Blockly.RTL);
// Load CSS.
Blockly.Css.inject();
Blockly.Css.inject(options.hasCss, options.pathToMedia);
// Build the SVG DOM.
/*
......@@ -213,7 +222,7 @@ Blockly.createDom_ = function(container) {
</defs>
*/
var defs = Blockly.createSvgElement('defs', {}, svg);
var filter, feSpecularLighting, feMerge, pattern;
var filter, feSpecularLighting, feMerge;
/*
<filter id="blocklyEmboss">
<feGaussianBlur in="SourceAlpha" stdDeviation="1" result="blur"/>
......@@ -258,13 +267,13 @@ Blockly.createDom_ = function(container) {
<path d="M 0 0 L 10 10 M 10 0 L 0 10" stroke="#cc0" />
</pattern>
*/
pattern = Blockly.createSvgElement('pattern',
var disabledPattern = Blockly.createSvgElement('pattern',
{'id': 'blocklyDisabledPattern', 'patternUnits': 'userSpaceOnUse',
'width': 10, 'height': 10}, defs);
Blockly.createSvgElement('rect',
{'width': 10, 'height': 10, 'fill': '#aaa'}, pattern);
{'width': 10, 'height': 10, 'fill': '#aaa'}, disabledPattern);
Blockly.createSvgElement('path',
{'d': 'M 0 0 L 10 10 M 10 0 L 0 10', 'stroke': '#cc0'}, pattern);
{'d': 'M 0 0 L 10 10 M 10 0 L 0 10', 'stroke': '#cc0'}, disabledPattern);
/*
<pattern id="blocklyGridPattern" patternUnits="userSpaceOnUse"
width="10" height="10">
......@@ -272,52 +281,55 @@ Blockly.createDom_ = function(container) {
<rect width="1" height="1" stroke="#888" />
</pattern>
*/
pattern = Blockly.createSvgElement('pattern',
var gridPattern = Blockly.createSvgElement('pattern',
{'id': 'blocklyGridPattern',
'patternUnits': 'userSpaceOnUse',
'width': Blockly.gridOptions['spacing'],
'height': Blockly.gridOptions['spacing']}, defs);
if (Blockly.gridOptions['length'] > 0) {
var half = Math.floor(Blockly.gridOptions['spacing'] / 2) + .5;
var start = half - Blockly.gridOptions['length'] / 2;
var end = half + Blockly.gridOptions['length'] / 2;
'width': options.gridOptions['spacing'],
'height': options.gridOptions['spacing']}, defs);
if (options.gridOptions['length'] > 0 && options.gridOptions['spacing'] > 0) {
var half = Math.floor(options.gridOptions['spacing'] / 2) + .5;
var start = half - options.gridOptions['length'] / 2;
var end = half + options.gridOptions['length'] / 2;
Blockly.createSvgElement('line',
{'x1': start,
'y1': half,
'x2': end,
'y2': half,
'stroke': Blockly.gridOptions['colour']},
pattern);
if (Blockly.gridOptions['length'] > 1) {
'stroke': options.gridOptions['colour']},
gridPattern);
if (options.gridOptions['length'] > 1) {
Blockly.createSvgElement('line',
{'x1': half,
'y1': start,
'x2': half,
'y2': end,
'stroke': Blockly.gridOptions['colour']},
pattern);
'stroke': options.gridOptions['colour']},
gridPattern);
}
Blockly.mainWorkspacePattern_ = pattern;
}
Blockly.mainWorkspace = new Blockly.WorkspaceSvg(
var mainWorkspace = new Blockly.WorkspaceSvg(
Blockly.getMainWorkspaceMetrics_,
Blockly.setMainWorkspaceMetrics_);
svg.appendChild(Blockly.mainWorkspace.createDom('blocklyMainBackground'));
Blockly.mainWorkspace.maxBlocks = Blockly.maxBlocks;
mainWorkspace.options = options;
goog.mixin(Blockly, options); // TODO: Delete this (#singletonHunt).
Blockly.mainWorkspace = mainWorkspace; // TODO: Delete this (#singletonHunt).
svg.appendChild(mainWorkspace.createDom('blocklyMainBackground'));
mainWorkspace.maxBlocks = options.maxBlocks;
mainWorkspace.gridPattern_ = gridPattern;
if (!Blockly.readOnly) {
if (!options.readOnly) {
// Determine if there needs to be a category tree, or a simple list of
// blocks. This cannot be changed later, since the UI is very different.
if (Blockly.hasCategories) {
Blockly.mainWorkspace.toolbox_ = new Blockly.Toolbox(svg, container);
} else if (Blockly.languageTree) {
Blockly.mainWorkspace.addFlyout();
if (options.hasCategories) {
mainWorkspace.toolbox_ = new Blockly.Toolbox(svg, container);
} else if (options.languageTree) {
mainWorkspace.addFlyout();
}
if (!Blockly.hasScrollbars) {
if (!options.hasScrollbars) {
var workspaceChanged = function() {
if (Blockly.dragMode_ == 0) {
var metrics = Blockly.mainWorkspace.getMetrics();
var metrics = mainWorkspace.getMetrics();
var edgeLeft = metrics.viewLeft + metrics.absoluteLeft;
var edgeTop = metrics.viewTop + metrics.absoluteTop;
if (metrics.contentTop < edgeTop ||
......@@ -329,7 +341,7 @@ Blockly.createDom_ = function(container) {
metrics.viewWidth : metrics.viewWidth + edgeLeft)) {
// One or more blocks may be out of bounds. Bump them back in.
var MARGIN = 25;
var blocks = Blockly.mainWorkspace.getTopBlocks(false);
var blocks = mainWorkspace.getTopBlocks(false);
for (var b = 0, block; block = blocks[b]; b++) {
var blockXY = block.getRelativeToSurfaceXY();
var blockHW = block.getHeightWidth();
......@@ -374,13 +386,16 @@ Blockly.createDom_ = function(container) {
Blockly.WidgetDiv.DIV = goog.dom.createDom('div', 'blocklyWidgetDiv');
Blockly.WidgetDiv.DIV.style.direction = Blockly.RTL ? 'rtl' : 'ltr';
document.body.appendChild(Blockly.WidgetDiv.DIV);
return mainWorkspace;
};
/**
* Initialize Blockly with various handlers.
* @param {!Blockly.Workspace} mainWorkspace Newly created main workspace.
* @private
*/
Blockly.init_ = function() {
Blockly.init_ = function(mainWorkspace) {
var options = mainWorkspace.options;
// Bind events for scrolling the workspace.
// Most of these events should be bound to the SVG's surface.
// However, 'mouseup' has to be on the whole document so that a block dragged
......@@ -415,34 +430,32 @@ Blockly.init_ = function() {
Blockly.documentEventsBound_ = true;
}
if (Blockly.languageTree) {
if (Blockly.mainWorkspace.toolbox_) {
Blockly.mainWorkspace.toolbox_.init(Blockly.mainWorkspace);
} else if (Blockly.mainWorkspace.flyout_) {
if (options.languageTree) {
if (mainWorkspace.toolbox_) {
mainWorkspace.toolbox_.init(mainWorkspace);
} else if (mainWorkspace.flyout_) {
// Build a fixed flyout with the root blocks.
Blockly.mainWorkspace.flyout_.init(Blockly.mainWorkspace);
Blockly.mainWorkspace.flyout_.show(Blockly.languageTree.childNodes);
mainWorkspace.flyout_.init(mainWorkspace);
mainWorkspace.flyout_.show(options.languageTree.childNodes);
// Translate the workspace sideways to avoid the fixed flyout.
Blockly.mainWorkspace.scrollX = Blockly.mainWorkspace.flyout_.width_;
if (Blockly.RTL) {
Blockly.mainWorkspace.scrollX *= -1;
mainWorkspace.scrollX = mainWorkspace.flyout_.width_;
if (options.RTL) {
mainWorkspace.scrollX *= -1;
}
var translation = 'translate(' + Blockly.mainWorkspace.scrollX + ', 0)';
Blockly.mainWorkspace.getCanvas().setAttribute('transform', translation);
Blockly.mainWorkspace.getBubbleCanvas().setAttribute('transform',
translation);
var translation = 'translate(' + mainWorkspace.scrollX + ', 0)';
mainWorkspace.getCanvas().setAttribute('transform', translation);
mainWorkspace.getBubbleCanvas().setAttribute('transform', translation);
}
}
if (Blockly.hasScrollbars) {
Blockly.mainWorkspace.scrollbar =
new Blockly.ScrollbarPair(Blockly.mainWorkspace);
Blockly.mainWorkspace.scrollbar.resize();
if (options.hasScrollbars) {
mainWorkspace.scrollbar = new Blockly.ScrollbarPair(mainWorkspace);
mainWorkspace.scrollbar.resize();
}
Blockly.mainWorkspace.addTrashcan();
mainWorkspace.addTrashcan();
// Load the sounds.
if (Blockly.hasSounds) {
if (options.hasSounds) {
Blockly.loadAudio_(
[Blockly.pathToMedia + 'click.mp3',
Blockly.pathToMedia + 'click.wav',
......@@ -472,30 +485,8 @@ Blockly.init_ = function() {
* Modify the block tree on the existing toolbox.
* @param {Node|string} tree DOM tree of blocks, or text representation of same.
*/
Blockly.updateToolbox = function(tree) {
tree = Blockly.parseToolboxTree_(tree);
if (!tree) {
if (Blockly.languageTree) {
throw 'Can\'t nullify an existing toolbox.';
}
// No change (null to null).
return;
}
if (!Blockly.languageTree) {
throw 'Existing toolbox is null. Can\'t create new toolbox.';
}
var hasCategories = !!tree.getElementsByTagName('category').length;
if (hasCategories) {
if (!Blockly.hasCategories) {
throw 'Existing toolbox has no categories. Can\'t change mode.';
}
Blockly.languageTree = tree;
Blockly.mainWorkspace.toolbox_.populate_();
} else {
if (Blockly.hasCategories) {
throw 'Existing toolbox has categories. Can\'t change mode.';
}
Blockly.languageTree = tree;
Blockly.mainWorkspace.flyout_.show(Blockly.languageTree.childNodes);
}
Blockly.updateToolbox = function(tree, workspace) {
console.warn('Deprecated call to Blockly.updateToolbox, ' +
'use workspace.updateToolbox instead.');
Blockly.getMainWorkspace().updateToolbox(tree);
};
......@@ -102,7 +102,7 @@ Blockly.Input.prototype.appendField = function(field, opt_name) {
* @deprecated December 2013
*/
Blockly.Input.prototype.appendTitle = function(field, opt_name) {
console.log('Deprecated call to appendTitle, use appendField instead.');
console.warn('Deprecated call to appendTitle, use appendField instead.');
return this.appendField(field, opt_name);
};
......
......@@ -186,3 +186,35 @@ Blockly.Workspace.prototype.remainingCapacity = function() {
Blockly.Workspace.prototype.fireChangeEvent = function() {
// NOP.
};
/**
* Modify the block tree on the existing toolbox.
* @param {Node|string} tree DOM tree of blocks, or text representation of same.
*/
Blockly.Workspace.prototype.updateToolbox = function(tree) {
tree = Blockly.parseToolboxTree_(tree);
if (!tree) {
if (Blockly.languageTree) {
throw 'Can\'t nullify an existing toolbox.';
}
// No change (null to null).
return;
}
if (!Blockly.languageTree) {
throw 'Existing toolbox is null. Can\'t create new toolbox.';
}
var hasCategories = !!tree.getElementsByTagName('category').length;
if (hasCategories) {
if (!this.toolbox_) {
throw 'Existing toolbox has no categories. Can\'t change mode.';
}
Blockly.languageTree = tree;
this.toolbox_.populate_();
} else {
if (!this.flyout_) {
throw 'Existing toolbox has categories. Can\'t change mode.';
}
Blockly.languageTree = tree;
this.flyout_.show(Blockly.languageTree.childNodes);
}
};
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