scratch-blocks/core/mutator.js

304 lines
10 KiB
JavaScript
Raw Normal View History

/**
* @license
* Visual Blocks Editor
*
* Copyright 2012 Google Inc.
2014-10-07 13:09:55 -07:00
* https://developers.google.com/blockly/
*
* 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');
goog.require('goog.dom');
goog.require('goog.Timer');
/**
* 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;
};
goog.inherits(Blockly.Mutator, Blockly.Icon);
/**
* Icon in base64 format.
* @private
*/
Blockly.Mutator.prototype.png_ = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABEAAAARCAYAAAA7bUf6AAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAANyAAADcgBffIlqAAAAAd0SU1FB98DGhULOIajyb8AAAHMSURBVDjLnZS9SiRBFIXP/CQ9iIHgPoGBTo8vIAaivoKaKJr6DLuxYqKYKIqRgSCMrblmIxqsICgOmAriziIiRXWjYPdnUDvT2+PMsOyBoop7qk71vedWS5KAkrWsGUMjSYjpgSQhNoZGFLEKeGoKGMNttUpULkOhAFL3USiA70MQEBnDDeDJWtaqVaJeB7uNICAKQ1ZkDI1yufOm+XnY2YHl5c6874MxPClJiDulkMvBxYWrw/095POdU0sS4hxALqcWtreloSGpVJLGxtL49bX0+Ci9vUkzM2kcXGFbypUKxHHLBXZ3YW4ONjfh4yN1aGIiPQOQEenrg6MjR+zvZz99Y8PFT09hYCArktdfsFY6PHTr83NlUKu5+eREennJchmR/n5pYcGtJyezG6em3Dw7Kw0OZrlMOr6f1gTg4ACWlmBvz9WoifHxbDpf3Flfd+54njQ9ncYvL6WHB+n9XVpcbHOnW59IUKu5m+p11zftfLHo+qRorZ6Hh/Xt7k5fsLUl1evS1dWfG9swMiJZq9+KIlaD4P/eztkZNgz5LsAzhpvjY6JK5d9e8eioE3h95SdQbDrkhSErxvArjkl6/U/imMQYnsKQH02BT7vbZZfVOiWhAAAAAElFTkSuQmCC';
/**
* Width of workspace.
* @private
*/
Blockly.Mutator.prototype.workspaceWidth_ = 0;
/**
* Height of workspace.
* @private
*/
Blockly.Mutator.prototype.workspaceHeight_ = 0;
/**
* 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);
}
};
/**
* 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>
<rect class="blocklyMutatorBackground" />
[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.
var quarkXml = goog.dom.createDom('xml');
for (var i = 0, quarkName; quarkName = this.quarkNames_[i]; i++) {
quarkXml.appendChild(goog.dom.createDom('block', {'type': quarkName}));
}
var mutator = this;
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,
getMetrics: function() {return mutator.getFlyoutMetrics_();},
setMetrics: null,
svg: this.svgDialog_
};
this.workspace_ = new Blockly.WorkspaceSvg(workspaceOptions);
2015-03-07 19:44:58 -06:00
this.svgDialog_.appendChild(
this.workspace_.createDom('blocklyMutatorBackground'));
return this.svgDialog_;
};
/**
* 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);
if (this.iconGroup_) {
Blockly.addClass_(/** @type {!Element} */ (this.iconGroup_),
2015-04-03 16:07:25 -07:00
'blocklyIconGroupReadonly');
}
}
};
/**
* 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 flyoutMetrics = this.workspace_.flyout_.getMetrics_();
var width;
2015-04-28 13:51:25 -07:00
if (this.block_.RTL) {
width = -workspaceSize.x;
} else {
width = workspaceSize.width + workspaceSize.x;
}
var height = Math.max(workspaceSize.height + doubleBorderWidth * 3,
flyoutMetrics.contentHeight + 20);
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) {
// 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();
};
/**
* 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_,
this.iconX_, this.iconY_, null, null);
var thisObj = this;
this.workspace_.flyout_.init(this.workspace_);
this.workspace_.flyout_.show(
this.workspace_.options.languageTree.childNodes);
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);
var margin = this.workspace_.flyout_.CORNER_RADIUS * 2;
var x = this.workspace_.flyout_.width_ + margin;
2015-04-28 13:51:25 -07:00
if (this.block_.RTL) {
x = -x;
}
this.rootBlock_.moveBy(x, margin);
// Save the initial connections, then listen for further changes.
if (this.block_.saveConnections) {
this.block_.saveConnections(this.rootBlock_);
this.sourceListener_ = Blockly.bindEvent_(
this.block_.workspace.getCanvas(),
'blocklyWorkspaceChange', this.block_,
function() {thisObj.block_.saveConnections(thisObj.rootBlock_)});
}
this.resizeBubble_();
// When the mutator's workspace changes, update the source block.
Blockly.bindEvent_(this.workspace_.getCanvas(), 'blocklyWorkspaceChange',
this.block_, function() {thisObj.workspaceChanged_();});
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_) {
Blockly.unbindEvent_(this.sourceListener_);
this.sourceListener_ = null;
}
}
};
/**
* Update the source block when the mutator's blocks are changed.
* Bump down any block that's too high.
* 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) {
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();
if (blockXY.y + blockHW.height < MARGIN) {
// 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_) {
// 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();
if (this.block_.rendered) {
this.block_.render();
}
this.resizeBubble_();
// The source block may have changed, notify its workspace.
this.block_.workspace.fireChangeEvent();
goog.Timer.callOnce(
this.block_.bumpNeighbours_, Blockly.BUMP_DELAY, this.block_);
}
};
/**
* 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,
* .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_,
absoluteTop: 0,
2015-04-28 23:43:03 -07:00
absoluteLeft: 0
};
};
/**
* Dispose of this mutator.
*/
Blockly.Mutator.prototype.dispose = function() {
this.block_.mutator = null;
Blockly.Icon.prototype.dispose.call(this);
};