2013-10-30 14:46:03 -07:00
|
|
|
/**
|
2014-01-28 03:00:09 -08:00
|
|
|
* @license
|
2013-10-30 14:46:03 -07:00
|
|
|
* Visual Blocks Editor
|
|
|
|
*
|
|
|
|
* Copyright 2012 Google Inc.
|
2014-10-07 13:09:55 -07:00
|
|
|
* https://developers.google.com/blockly/
|
2013-10-30 14:46:03 -07:00
|
|
|
*
|
|
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
* you may not use this file except in compliance with the License.
|
|
|
|
* You may obtain a copy of the License at
|
|
|
|
*
|
|
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
*
|
|
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
* See the License for the specific language governing permissions and
|
|
|
|
* limitations under the License.
|
|
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @fileoverview Object representing a mutator dialog. A mutator allows the
|
|
|
|
* user to change the shape of a block using a nested blocks editor.
|
|
|
|
* @author fraser@google.com (Neil Fraser)
|
|
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
|
|
|
|
goog.provide('Blockly.Mutator');
|
|
|
|
|
|
|
|
goog.require('Blockly.Bubble');
|
|
|
|
goog.require('Blockly.Icon');
|
2014-12-23 11:22:02 -08:00
|
|
|
goog.require('Blockly.WorkspaceSvg');
|
2015-02-27 14:24:20 -08:00
|
|
|
goog.require('goog.Timer');
|
2015-06-17 13:05:24 -07:00
|
|
|
goog.require('goog.dom');
|
2013-10-30 14:46:03 -07:00
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Class for a mutator dialog.
|
|
|
|
* @param {!Array.<string>} quarkNames List of names of sub-blocks for flyout.
|
|
|
|
* @extends {Blockly.Icon}
|
|
|
|
* @constructor
|
|
|
|
*/
|
|
|
|
Blockly.Mutator = function(quarkNames) {
|
|
|
|
Blockly.Mutator.superClass_.constructor.call(this, null);
|
2015-01-01 14:30:37 -08:00
|
|
|
this.quarkNames_ = quarkNames;
|
2013-10-30 14:46:03 -07:00
|
|
|
};
|
|
|
|
goog.inherits(Blockly.Mutator, Blockly.Icon);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Width of workspace.
|
|
|
|
* @private
|
|
|
|
*/
|
|
|
|
Blockly.Mutator.prototype.workspaceWidth_ = 0;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Height of workspace.
|
|
|
|
* @private
|
|
|
|
*/
|
|
|
|
Blockly.Mutator.prototype.workspaceHeight_ = 0;
|
|
|
|
|
2015-12-13 11:06:15 +01:00
|
|
|
/**
|
|
|
|
* Draw the mutator icon.
|
|
|
|
* @param {!Element} group The icon group.
|
|
|
|
* @private
|
|
|
|
*/
|
|
|
|
Blockly.Mutator.prototype.drawIcon_ = function(group) {
|
|
|
|
// Square with rounded corners.
|
|
|
|
Blockly.createSvgElement('rect',
|
|
|
|
{'class': 'blocklyIconShape',
|
|
|
|
'rx': '4', 'ry': '4',
|
|
|
|
'height': '16', 'width': '16'},
|
|
|
|
group);
|
2015-12-16 18:22:42 -08:00
|
|
|
// Gear teeth.
|
2015-12-13 11:06:15 +01:00
|
|
|
Blockly.createSvgElement('path',
|
|
|
|
{'class': 'blocklyIconSymbol',
|
2015-12-16 18:22:42 -08:00
|
|
|
'd': 'm4.203,7.296 0,1.368 -0.92,0.677 -0.11,0.41 0.9,1.559 0.41,0.11 1.043,-0.457 1.187,0.683 0.127,1.134 0.3,0.3 1.8,0 0.3,-0.299 0.127,-1.138 1.185,-0.682 1.046,0.458 0.409,-0.11 0.9,-1.559 -0.11,-0.41 -0.92,-0.677 0,-1.366 0.92,-0.677 0.11,-0.41 -0.9,-1.559 -0.409,-0.109 -1.046,0.458 -1.185,-0.682 -0.127,-1.138 -0.3,-0.299 -1.8,0 -0.3,0.3 -0.126,1.135 -1.187,0.682 -1.043,-0.457 -0.41,0.11 -0.899,1.559 0.108,0.409z'},
|
2015-12-13 11:06:15 +01:00
|
|
|
group);
|
2015-12-16 18:22:42 -08:00
|
|
|
// Axle hole.
|
2015-12-13 11:06:15 +01:00
|
|
|
Blockly.createSvgElement('circle',
|
|
|
|
{'class': 'blocklyIconShape', 'r': '2.7', 'cx': '8', 'cy': '8'},
|
|
|
|
group);
|
|
|
|
};
|
|
|
|
|
2014-01-11 03:00:02 -08:00
|
|
|
/**
|
|
|
|
* Clicking on the icon toggles if the mutator bubble is visible.
|
|
|
|
* Disable if block is uneditable.
|
|
|
|
* @param {!Event} e Mouse click event.
|
|
|
|
* @private
|
|
|
|
* @override
|
|
|
|
*/
|
|
|
|
Blockly.Mutator.prototype.iconClick_ = function(e) {
|
|
|
|
if (this.block_.isEditable()) {
|
|
|
|
Blockly.Icon.prototype.iconClick_.call(this, e);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2013-10-30 14:46:03 -07:00
|
|
|
/**
|
|
|
|
* Create the editor for the mutator's bubble.
|
|
|
|
* @return {!Element} The top-level node of the editor.
|
|
|
|
* @private
|
|
|
|
*/
|
|
|
|
Blockly.Mutator.prototype.createEditor_ = function() {
|
|
|
|
/* Create the editor. Here's the markup that will be generated:
|
|
|
|
<svg>
|
|
|
|
[Workspace]
|
|
|
|
</svg>
|
|
|
|
*/
|
|
|
|
this.svgDialog_ = Blockly.createSvgElement('svg',
|
|
|
|
{'x': Blockly.Bubble.BORDER_WIDTH, 'y': Blockly.Bubble.BORDER_WIDTH},
|
|
|
|
null);
|
2015-04-28 13:51:25 -07:00
|
|
|
// Convert the list of names into a list of XML objects for the flyout.
|
2015-12-04 23:35:39 -08:00
|
|
|
if (this.quarkNames_.length) {
|
|
|
|
var quarkXml = goog.dom.createDom('xml');
|
|
|
|
for (var i = 0, quarkName; quarkName = this.quarkNames_[i]; i++) {
|
|
|
|
quarkXml.appendChild(goog.dom.createDom('block', {'type': quarkName}));
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
var quarkXml = null;
|
2015-04-28 13:51:25 -07:00
|
|
|
}
|
|
|
|
var workspaceOptions = {
|
|
|
|
languageTree: quarkXml,
|
|
|
|
parentWorkspace: this.block_.workspace,
|
2015-04-28 23:43:03 -07:00
|
|
|
pathToMedia: this.block_.workspace.options.pathToMedia,
|
2015-04-28 13:51:25 -07:00
|
|
|
RTL: this.block_.RTL,
|
2016-01-08 13:03:22 -08:00
|
|
|
getMetrics: this.getFlyoutMetrics_.bind(this),
|
|
|
|
setMetrics: null
|
2015-04-28 13:51:25 -07:00
|
|
|
};
|
|
|
|
this.workspace_ = new Blockly.WorkspaceSvg(workspaceOptions);
|
2015-03-07 19:44:58 -06:00
|
|
|
this.svgDialog_.appendChild(
|
|
|
|
this.workspace_.createDom('blocklyMutatorBackground'));
|
2013-10-30 14:46:03 -07:00
|
|
|
return this.svgDialog_;
|
|
|
|
};
|
|
|
|
|
2014-01-11 03:00:02 -08:00
|
|
|
/**
|
|
|
|
* Add or remove the UI indicating if this icon may be clicked or not.
|
|
|
|
*/
|
|
|
|
Blockly.Mutator.prototype.updateEditable = function() {
|
|
|
|
if (this.block_.isEditable()) {
|
|
|
|
// Default behaviour for an icon.
|
|
|
|
Blockly.Icon.prototype.updateEditable.call(this);
|
|
|
|
} else {
|
|
|
|
// Close any mutator bubble. Icon is not clickable.
|
|
|
|
this.setVisible(false);
|
2015-02-06 17:06:16 -08:00
|
|
|
if (this.iconGroup_) {
|
2015-03-25 23:58:58 -07:00
|
|
|
Blockly.addClass_(/** @type {!Element} */ (this.iconGroup_),
|
2015-04-03 16:07:25 -07:00
|
|
|
'blocklyIconGroupReadonly');
|
2015-02-06 17:06:16 -08:00
|
|
|
}
|
2014-01-11 03:00:02 -08:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2013-10-30 14:46:03 -07:00
|
|
|
/**
|
|
|
|
* Callback function triggered when the bubble has resized.
|
|
|
|
* Resize the workspace accordingly.
|
|
|
|
* @private
|
|
|
|
*/
|
|
|
|
Blockly.Mutator.prototype.resizeBubble_ = function() {
|
|
|
|
var doubleBorderWidth = 2 * Blockly.Bubble.BORDER_WIDTH;
|
|
|
|
var workspaceSize = this.workspace_.getCanvas().getBBox();
|
|
|
|
var width;
|
2015-04-28 13:51:25 -07:00
|
|
|
if (this.block_.RTL) {
|
2013-10-30 14:46:03 -07:00
|
|
|
width = -workspaceSize.x;
|
|
|
|
} else {
|
|
|
|
width = workspaceSize.width + workspaceSize.x;
|
|
|
|
}
|
2015-12-04 23:35:39 -08:00
|
|
|
var height = workspaceSize.height + doubleBorderWidth * 3;
|
|
|
|
if (this.workspace_.flyout_) {
|
|
|
|
var flyoutMetrics = this.workspace_.flyout_.getMetrics_();
|
|
|
|
height = Math.max(height, flyoutMetrics.contentHeight + 20);
|
|
|
|
}
|
2013-10-30 14:46:03 -07:00
|
|
|
width += doubleBorderWidth * 3;
|
|
|
|
// Only resize if the size difference is significant. Eliminates shuddering.
|
|
|
|
if (Math.abs(this.workspaceWidth_ - width) > doubleBorderWidth ||
|
|
|
|
Math.abs(this.workspaceHeight_ - height) > doubleBorderWidth) {
|
|
|
|
// Record some layout information for getFlyoutMetrics_.
|
|
|
|
this.workspaceWidth_ = width;
|
|
|
|
this.workspaceHeight_ = height;
|
|
|
|
// Resize the bubble.
|
|
|
|
this.bubble_.setBubbleSize(width + doubleBorderWidth,
|
|
|
|
height + doubleBorderWidth);
|
|
|
|
this.svgDialog_.setAttribute('width', this.workspaceWidth_);
|
|
|
|
this.svgDialog_.setAttribute('height', this.workspaceHeight_);
|
|
|
|
}
|
|
|
|
|
2015-04-28 13:51:25 -07:00
|
|
|
if (this.block_.RTL) {
|
2013-10-30 14:46:03 -07:00
|
|
|
// Scroll the workspace to always left-align.
|
|
|
|
var translation = 'translate(' + this.workspaceWidth_ + ',0)';
|
|
|
|
this.workspace_.getCanvas().setAttribute('transform', translation);
|
|
|
|
}
|
2015-04-28 23:43:03 -07:00
|
|
|
this.workspace_.resize();
|
2013-10-30 14:46:03 -07:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Show or hide the mutator bubble.
|
|
|
|
* @param {boolean} visible True if the bubble should be visible.
|
|
|
|
*/
|
|
|
|
Blockly.Mutator.prototype.setVisible = function(visible) {
|
|
|
|
if (visible == this.isVisible()) {
|
|
|
|
// No change.
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (visible) {
|
|
|
|
// Create the bubble.
|
|
|
|
this.bubble_ = new Blockly.Bubble(this.block_.workspace,
|
2014-12-23 11:22:02 -08:00
|
|
|
this.createEditor_(), this.block_.svgPath_,
|
2013-10-30 14:46:03 -07:00
|
|
|
this.iconX_, this.iconY_, null, null);
|
2015-12-04 23:35:39 -08:00
|
|
|
var tree = this.workspace_.options.languageTree;
|
|
|
|
if (tree) {
|
|
|
|
this.workspace_.flyout_.init(this.workspace_);
|
|
|
|
this.workspace_.flyout_.show(tree.childNodes);
|
|
|
|
}
|
2013-10-30 14:46:03 -07:00
|
|
|
|
|
|
|
this.rootBlock_ = this.block_.decompose(this.workspace_);
|
|
|
|
var blocks = this.rootBlock_.getDescendants();
|
|
|
|
for (var i = 0, child; child = blocks[i]; i++) {
|
|
|
|
child.render();
|
|
|
|
}
|
|
|
|
// The root block should not be dragable or deletable.
|
|
|
|
this.rootBlock_.setMovable(false);
|
|
|
|
this.rootBlock_.setDeletable(false);
|
2015-12-04 23:35:39 -08:00
|
|
|
if (this.workspace_.flyout_) {
|
|
|
|
var margin = this.workspace_.flyout_.CORNER_RADIUS * 2;
|
|
|
|
var x = this.workspace_.flyout_.width_ + margin;
|
|
|
|
} else {
|
|
|
|
var margin = 16;
|
|
|
|
var x = margin;
|
|
|
|
}
|
2015-04-28 13:51:25 -07:00
|
|
|
if (this.block_.RTL) {
|
2013-10-30 14:46:03 -07:00
|
|
|
x = -x;
|
|
|
|
}
|
|
|
|
this.rootBlock_.moveBy(x, margin);
|
|
|
|
// Save the initial connections, then listen for further changes.
|
|
|
|
if (this.block_.saveConnections) {
|
2015-12-07 16:40:45 +01:00
|
|
|
var thisMutator = this;
|
2013-10-30 14:46:03 -07:00
|
|
|
this.block_.saveConnections(this.rootBlock_);
|
2016-02-11 21:40:33 -08:00
|
|
|
this.sourceListener_ = function() {
|
|
|
|
thisMutator.block_.saveConnections(thisMutator.rootBlock_)
|
|
|
|
};
|
|
|
|
this.block_.workspace.addChangeListener(this.sourceListener_);
|
2013-10-30 14:46:03 -07:00
|
|
|
}
|
|
|
|
this.resizeBubble_();
|
|
|
|
// When the mutator's workspace changes, update the source block.
|
2016-02-11 21:40:33 -08:00
|
|
|
this.workspace_.addChangeListener(this.workspaceChanged_.bind(this));
|
2013-10-30 14:46:03 -07:00
|
|
|
this.updateColour();
|
|
|
|
} else {
|
|
|
|
// Dispose of the bubble.
|
|
|
|
this.svgDialog_ = null;
|
|
|
|
this.workspace_.dispose();
|
|
|
|
this.workspace_ = null;
|
|
|
|
this.rootBlock_ = null;
|
|
|
|
this.bubble_.dispose();
|
|
|
|
this.bubble_ = null;
|
|
|
|
this.workspaceWidth_ = 0;
|
|
|
|
this.workspaceHeight_ = 0;
|
|
|
|
if (this.sourceListener_) {
|
2016-02-11 21:40:33 -08:00
|
|
|
this.block_.workspace.removeChangeListener(this.sourceListener_);
|
2013-10-30 14:46:03 -07:00
|
|
|
this.sourceListener_ = null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Update the source block when the mutator's blocks are changed.
|
2014-11-28 21:43:39 -08:00
|
|
|
* Bump down any block that's too high.
|
2013-10-30 14:46:03 -07:00
|
|
|
* Fired whenever a change is made to the mutator's workspace.
|
|
|
|
* @private
|
|
|
|
*/
|
|
|
|
Blockly.Mutator.prototype.workspaceChanged_ = function() {
|
2014-12-23 11:22:02 -08:00
|
|
|
if (Blockly.dragMode_ == 0) {
|
2013-10-30 14:46:03 -07:00
|
|
|
var blocks = this.workspace_.getTopBlocks(false);
|
|
|
|
var MARGIN = 20;
|
|
|
|
for (var b = 0, block; block = blocks[b]; b++) {
|
|
|
|
var blockXY = block.getRelativeToSurfaceXY();
|
|
|
|
var blockHW = block.getHeightWidth();
|
2014-11-28 21:43:39 -08:00
|
|
|
if (blockXY.y + blockHW.height < MARGIN) {
|
2013-10-30 14:46:03 -07:00
|
|
|
// Bump any block that's above the top back inside.
|
|
|
|
block.moveBy(0, MARGIN - blockHW.height - blockXY.y);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// When the mutator's workspace changes, update the source block.
|
|
|
|
if (this.rootBlock_.workspace == this.workspace_) {
|
2016-02-11 21:40:33 -08:00
|
|
|
var oldMutationDom = this.block_.mutationToDom();
|
|
|
|
var oldMutation = oldMutationDom && Blockly.Xml.domToText(oldMutationDom);
|
2013-10-30 14:46:03 -07:00
|
|
|
// Switch off rendering while the source block is rebuilt.
|
|
|
|
var savedRendered = this.block_.rendered;
|
|
|
|
this.block_.rendered = false;
|
|
|
|
// Allow the source block to rebuild itself.
|
|
|
|
this.block_.compose(this.rootBlock_);
|
|
|
|
// Restore rendering and show the changes.
|
|
|
|
this.block_.rendered = savedRendered;
|
2014-12-23 11:22:02 -08:00
|
|
|
// Mutation may have added some elements that need initalizing.
|
|
|
|
this.block_.initSvg();
|
2016-02-11 21:40:33 -08:00
|
|
|
var newMutationDom = this.block_.mutationToDom();
|
|
|
|
var newMutation = newMutationDom && Blockly.Xml.domToText(newMutationDom);
|
|
|
|
if (oldMutation != newMutation) {
|
2016-02-10 21:40:51 -08:00
|
|
|
Blockly.Events.fire(new Blockly.Events.Change(
|
|
|
|
this.block_, 'mutation', null, oldMutation, newMutation));
|
|
|
|
goog.Timer.callOnce(
|
|
|
|
this.block_.bumpNeighbours_, Blockly.BUMP_DELAY, this.block_);
|
|
|
|
}
|
2013-10-30 14:46:03 -07:00
|
|
|
if (this.block_.rendered) {
|
|
|
|
this.block_.render();
|
|
|
|
}
|
|
|
|
this.resizeBubble_();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return an object with all the metrics required to size scrollbars for the
|
|
|
|
* mutator flyout. The following properties are computed:
|
|
|
|
* .viewHeight: Height of the visible rectangle,
|
2015-04-28 23:43:03 -07:00
|
|
|
* .viewWidth: Width of the visible rectangle,
|
2013-10-30 14:46:03 -07:00
|
|
|
* .absoluteTop: Top-edge of view.
|
|
|
|
* .absoluteLeft: Left-edge of view.
|
|
|
|
* @return {!Object} Contains size and position metrics of mutator dialog's
|
|
|
|
* workspace.
|
|
|
|
* @private
|
|
|
|
*/
|
|
|
|
Blockly.Mutator.prototype.getFlyoutMetrics_ = function() {
|
|
|
|
return {
|
|
|
|
viewHeight: this.workspaceHeight_,
|
2015-04-28 23:43:03 -07:00
|
|
|
viewWidth: this.workspaceWidth_,
|
2013-10-30 14:46:03 -07:00
|
|
|
absoluteTop: 0,
|
2015-04-28 23:43:03 -07:00
|
|
|
absoluteLeft: 0
|
2013-10-30 14:46:03 -07:00
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Dispose of this mutator.
|
|
|
|
*/
|
|
|
|
Blockly.Mutator.prototype.dispose = function() {
|
|
|
|
this.block_.mutator = null;
|
|
|
|
Blockly.Icon.prototype.dispose.call(this);
|
|
|
|
};
|