/** * @license * Visual Blocks Editor * * Copyright 2012 Google Inc. * 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'); goog.require('Blockly.WorkspaceSvg'); goog.require('goog.Timer'); goog.require('goog.dom'); /** * 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); 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); // 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; var workspaceOptions = { languageTree: quarkXml, parentWorkspace: this.block_.workspace, pathToMedia: this.block_.workspace.options.pathToMedia, RTL: this.block_.RTL, getMetrics: function() {return mutator.getFlyoutMetrics_();}, setMetrics: null, svg: this.svgDialog_ }; this.workspace_ = new Blockly.WorkspaceSvg(workspaceOptions); 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_), '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; 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_); } if (this.block_.RTL) { // Scroll the workspace to always left-align. var translation = 'translate(' + this.workspaceWidth_ + ',0)'; this.workspace_.getCanvas().setAttribute('transform', translation); } 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, 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; 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() { 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; // 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, * .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_, viewWidth: this.workspaceWidth_, absoluteTop: 0, absoluteLeft: 0 }; }; /** * Dispose of this mutator. */ Blockly.Mutator.prototype.dispose = function() { this.block_.mutator = null; Blockly.Icon.prototype.dispose.call(this); };