diff options
Diffstat (limited to 'src/blockly/blocks/procedures.js')
-rw-r--r-- | src/blockly/blocks/procedures.js | 875 |
1 files changed, 875 insertions, 0 deletions
diff --git a/src/blockly/blocks/procedures.js b/src/blockly/blocks/procedures.js new file mode 100644 index 0000000..19a5f6e --- /dev/null +++ b/src/blockly/blocks/procedures.js @@ -0,0 +1,875 @@ +/** + * @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 Procedure blocks for Blockly. + * @author fraser@google.com (Neil Fraser) + */ +'use strict'; + +goog.provide('Blockly.Blocks.procedures'); + +goog.require('Blockly.Blocks'); + + +/** + * Common HSV hue for all blocks in this category. + */ +Blockly.Blocks.procedures.HUE = 290; + +Blockly.Blocks['procedures_defnoreturn'] = { + /** + * Block for defining a procedure with no return value. + * @this Blockly.Block + */ + init: function() { + var nameField = new Blockly.FieldTextInput( + Blockly.Msg.PROCEDURES_DEFNORETURN_PROCEDURE, + Blockly.Procedures.rename); + nameField.setSpellcheck(false); + this.appendDummyInput() + .appendField(Blockly.Msg.PROCEDURES_DEFNORETURN_TITLE) + .appendField(nameField, 'NAME') + .appendField('', 'PARAMS'); + this.setMutator(new Blockly.Mutator(['procedures_mutatorarg'])); + if (Blockly.Msg.PROCEDURES_DEFNORETURN_COMMENT) { + this.setCommentText(Blockly.Msg.PROCEDURES_DEFNORETURN_COMMENT); + } + this.setColour(Blockly.Blocks.procedures.HUE); + this.setTooltip(Blockly.Msg.PROCEDURES_DEFNORETURN_TOOLTIP); + this.setHelpUrl(Blockly.Msg.PROCEDURES_DEFNORETURN_HELPURL); + this.arguments_ = []; + this.setStatements_(true); + this.statementConnection_ = null; + }, + /** + * Add or remove the statement block from this function definition. + * @param {boolean} hasStatements True if a statement block is needed. + * @this Blockly.Block + */ + setStatements_: function(hasStatements) { + if (this.hasStatements_ === hasStatements) { + return; + } + if (hasStatements) { + this.appendStatementInput('STACK') + .appendField(Blockly.Msg.PROCEDURES_DEFNORETURN_DO); + if (this.getInput('RETURN')) { + this.moveInputBefore('STACK', 'RETURN'); + } + } else { + this.removeInput('STACK', true); + } + this.hasStatements_ = hasStatements; + }, + /** + * Update the display of parameters for this procedure definition block. + * Display a warning if there are duplicately named parameters. + * @private + * @this Blockly.Block + */ + updateParams_: function() { + // Check for duplicated arguments. + var badArg = false; + var hash = {}; + for (var i = 0; i < this.arguments_.length; i++) { + if (hash['arg_' + this.arguments_[i].toLowerCase()]) { + badArg = true; + break; + } + hash['arg_' + this.arguments_[i].toLowerCase()] = true; + } + if (badArg) { + this.setWarningText(Blockly.Msg.PROCEDURES_DEF_DUPLICATE_WARNING); + } else { + this.setWarningText(null); + } + // Merge the arguments into a human-readable list. + var paramString = ''; + if (this.arguments_.length) { + paramString = Blockly.Msg.PROCEDURES_BEFORE_PARAMS + + ' ' + this.arguments_.join(', '); + } + // The params field is deterministic based on the mutation, + // no need to fire a change event. + Blockly.Events.disable(); + try { + this.setFieldValue(paramString, 'PARAMS'); + } finally { + Blockly.Events.enable(); + } + }, + /** + * Create XML to represent the argument inputs. + * @param {=boolean} opt_paramIds If true include the IDs of the parameter + * quarks. Used by Blockly.Procedures.mutateCallers for reconnection. + * @return {!Element} XML storage element. + * @this Blockly.Block + */ + mutationToDom: function(opt_paramIds) { + var container = document.createElement('mutation'); + if (opt_paramIds) { + container.setAttribute('name', this.getFieldValue('NAME')); + } + for (var i = 0; i < this.arguments_.length; i++) { + var parameter = document.createElement('arg'); + parameter.setAttribute('name', this.arguments_[i]); + if (opt_paramIds && this.paramIds_) { + parameter.setAttribute('paramId', this.paramIds_[i]); + } + container.appendChild(parameter); + } + + // Save whether the statement input is visible. + if (!this.hasStatements_) { + container.setAttribute('statements', 'false'); + } + return container; + }, + /** + * Parse XML to restore the argument inputs. + * @param {!Element} xmlElement XML storage element. + * @this Blockly.Block + */ + domToMutation: function(xmlElement) { + this.arguments_ = []; + for (var i = 0, childNode; childNode = xmlElement.childNodes[i]; i++) { + if (childNode.nodeName.toLowerCase() == 'arg') { + this.arguments_.push(childNode.getAttribute('name')); + } + } + this.updateParams_(); + Blockly.Procedures.mutateCallers(this); + + // Show or hide the statement input. + this.setStatements_(xmlElement.getAttribute('statements') !== 'false'); + }, + /** + * Populate the mutator's dialog with this block's components. + * @param {!Blockly.Workspace} workspace Mutator's workspace. + * @return {!Blockly.Block} Root block in mutator. + * @this Blockly.Block + */ + decompose: function(workspace) { + var containerBlock = workspace.newBlock('procedures_mutatorcontainer'); + containerBlock.initSvg(); + + // Check/uncheck the allow statement box. + if (this.getInput('RETURN')) { + containerBlock.setFieldValue(this.hasStatements_ ? 'TRUE' : 'FALSE', + 'STATEMENTS'); + } else { + containerBlock.getInput('STATEMENT_INPUT').setVisible(false); + } + + // Parameter list. + var connection = containerBlock.getInput('STACK').connection; + for (var i = 0; i < this.arguments_.length; i++) { + var paramBlock = workspace.newBlock('procedures_mutatorarg'); + paramBlock.initSvg(); + paramBlock.setFieldValue(this.arguments_[i], 'NAME'); + // Store the old location. + paramBlock.oldLocation = i; + connection.connect(paramBlock.previousConnection); + connection = paramBlock.nextConnection; + } + // Initialize procedure's callers with blank IDs. + Blockly.Procedures.mutateCallers(this); + return containerBlock; + }, + /** + * Reconfigure this block based on the mutator dialog's components. + * @param {!Blockly.Block} containerBlock Root block in mutator. + * @this Blockly.Block + */ + compose: function(containerBlock) { + // Parameter list. + this.arguments_ = []; + this.paramIds_ = []; + var paramBlock = containerBlock.getInputTargetBlock('STACK'); + while (paramBlock) { + this.arguments_.push(paramBlock.getFieldValue('NAME')); + this.paramIds_.push(paramBlock.id); + paramBlock = paramBlock.nextConnection && + paramBlock.nextConnection.targetBlock(); + } + this.updateParams_(); + Blockly.Procedures.mutateCallers(this); + + // Show/hide the statement input. + var hasStatements = containerBlock.getFieldValue('STATEMENTS'); + if (hasStatements !== null) { + hasStatements = hasStatements == 'TRUE'; + if (this.hasStatements_ != hasStatements) { + if (hasStatements) { + this.setStatements_(true); + // Restore the stack, if one was saved. + Blockly.Mutator.reconnect(this.statementConnection_, this, 'STACK'); + this.statementConnection_ = null; + } else { + // Save the stack, then disconnect it. + var stackConnection = this.getInput('STACK').connection; + this.statementConnection_ = stackConnection.targetConnection; + if (this.statementConnection_) { + var stackBlock = stackConnection.targetBlock(); + stackBlock.unplug(); + stackBlock.bumpNeighbours_(); + } + this.setStatements_(false); + } + } + } + }, + /** + * Return the signature of this procedure definition. + * @return {!Array} Tuple containing three elements: + * - the name of the defined procedure, + * - a list of all its arguments, + * - that it DOES NOT have a return value. + * @this Blockly.Block + */ + getProcedureDef: function() { + return [this.getFieldValue('NAME'), this.arguments_, false]; + }, + /** + * Return all variables referenced by this block. + * @return {!Array.<string>} List of variable names. + * @this Blockly.Block + */ + getVars: function() { + return this.arguments_; + }, + /** + * Notification that a variable is renaming. + * If the name matches one of this block's variables, rename it. + * @param {string} oldName Previous name of variable. + * @param {string} newName Renamed variable. + * @this Blockly.Block + */ + renameVar: function(oldName, newName) { + var change = false; + for (var i = 0; i < this.arguments_.length; i++) { + if (Blockly.Names.equals(oldName, this.arguments_[i])) { + this.arguments_[i] = newName; + change = true; + } + } + if (change) { + this.updateParams_(); + // Update the mutator's variables if the mutator is open. + if (this.mutator.isVisible()) { + var blocks = this.mutator.workspace_.getAllBlocks(); + for (var i = 0, block; block = blocks[i]; i++) { + if (block.type == 'procedures_mutatorarg' && + Blockly.Names.equals(oldName, block.getFieldValue('NAME'))) { + block.setFieldValue(newName, 'NAME'); + } + } + } + } + }, + /** + * Add custom menu options to this block's context menu. + * @param {!Array} options List of menu options to add to. + * @this Blockly.Block + */ + customContextMenu: function(options) { + // Add option to create caller. + var option = {enabled: true}; + var name = this.getFieldValue('NAME'); + option.text = Blockly.Msg.PROCEDURES_CREATE_DO.replace('%1', name); + var xmlMutation = goog.dom.createDom('mutation'); + xmlMutation.setAttribute('name', name); + for (var i = 0; i < this.arguments_.length; i++) { + var xmlArg = goog.dom.createDom('arg'); + xmlArg.setAttribute('name', this.arguments_[i]); + xmlMutation.appendChild(xmlArg); + } + var xmlBlock = goog.dom.createDom('block', null, xmlMutation); + xmlBlock.setAttribute('type', this.callType_); + option.callback = Blockly.ContextMenu.callbackFactory(this, xmlBlock); + options.push(option); + + // Add options to create getters for each parameter. + if (!this.isCollapsed()) { + for (var i = 0; i < this.arguments_.length; i++) { + var option = {enabled: true}; + var name = this.arguments_[i]; + option.text = Blockly.Msg.VARIABLES_SET_CREATE_GET.replace('%1', name); + var xmlField = goog.dom.createDom('field', null, name); + xmlField.setAttribute('name', 'VAR'); + var xmlBlock = goog.dom.createDom('block', null, xmlField); + xmlBlock.setAttribute('type', 'variables_get'); + option.callback = Blockly.ContextMenu.callbackFactory(this, xmlBlock); + options.push(option); + } + } + }, + callType_: 'procedures_callnoreturn' +}; + +Blockly.Blocks['procedures_defreturn'] = { + /** + * Block for defining a procedure with a return value. + * @this Blockly.Block + */ + init: function() { + var nameField = new Blockly.FieldTextInput( + Blockly.Msg.PROCEDURES_DEFRETURN_PROCEDURE, + Blockly.Procedures.rename); + nameField.setSpellcheck(false); + this.appendDummyInput() + .appendField(Blockly.Msg.PROCEDURES_DEFRETURN_TITLE) + .appendField(nameField, 'NAME') + .appendField('', 'PARAMS'); + this.appendValueInput('RETURN') + .setAlign(Blockly.ALIGN_RIGHT) + .appendField(Blockly.Msg.PROCEDURES_DEFRETURN_RETURN); + this.setMutator(new Blockly.Mutator(['procedures_mutatorarg'])); + if (Blockly.Msg.PROCEDURES_DEFRETURN_COMMENT) { + this.setCommentText(Blockly.Msg.PROCEDURES_DEFRETURN_COMMENT); + } + this.setColour(Blockly.Blocks.procedures.HUE); + this.setTooltip(Blockly.Msg.PROCEDURES_DEFRETURN_TOOLTIP); + this.setHelpUrl(Blockly.Msg.PROCEDURES_DEFRETURN_HELPURL); + this.arguments_ = []; + this.setStatements_(true); + this.statementConnection_ = null; + }, + setStatements_: Blockly.Blocks['procedures_defnoreturn'].setStatements_, + updateParams_: Blockly.Blocks['procedures_defnoreturn'].updateParams_, + mutationToDom: Blockly.Blocks['procedures_defnoreturn'].mutationToDom, + domToMutation: Blockly.Blocks['procedures_defnoreturn'].domToMutation, + decompose: Blockly.Blocks['procedures_defnoreturn'].decompose, + compose: Blockly.Blocks['procedures_defnoreturn'].compose, + /** + * Return the signature of this procedure definition. + * @return {!Array} Tuple containing three elements: + * - the name of the defined procedure, + * - a list of all its arguments, + * - that it DOES have a return value. + * @this Blockly.Block + */ + getProcedureDef: function() { + return [this.getFieldValue('NAME'), this.arguments_, true]; + }, + getVars: Blockly.Blocks['procedures_defnoreturn'].getVars, + renameVar: Blockly.Blocks['procedures_defnoreturn'].renameVar, + customContextMenu: Blockly.Blocks['procedures_defnoreturn'].customContextMenu, + callType_: 'procedures_callreturn' +}; + +Blockly.Blocks['procedures_mutatorcontainer'] = { + /** + * Mutator block for procedure container. + * @this Blockly.Block + */ + init: function() { + this.appendDummyInput() + .appendField(Blockly.Msg.PROCEDURES_MUTATORCONTAINER_TITLE); + this.appendStatementInput('STACK'); + this.appendDummyInput('STATEMENT_INPUT') + .appendField(Blockly.Msg.PROCEDURES_ALLOW_STATEMENTS) + .appendField(new Blockly.FieldCheckbox('TRUE'), 'STATEMENTS'); + this.setColour(Blockly.Blocks.procedures.HUE); + this.setTooltip(Blockly.Msg.PROCEDURES_MUTATORCONTAINER_TOOLTIP); + this.contextMenu = false; + } +}; + +Blockly.Blocks['procedures_mutatorarg'] = { + /** + * Mutator block for procedure argument. + * @this Blockly.Block + */ + init: function() { + var field = new Blockly.FieldTextInput('x', this.validator_); + this.appendDummyInput() + .appendField(Blockly.Msg.PROCEDURES_MUTATORARG_TITLE) + .appendField(field, 'NAME'); + this.setPreviousStatement(true); + this.setNextStatement(true); + this.setColour(Blockly.Blocks.procedures.HUE); + this.setTooltip(Blockly.Msg.PROCEDURES_MUTATORARG_TOOLTIP); + this.contextMenu = false; + + // Create the default variable when we drag the block in from the flyout. + // Have to do this after installing the field on the block. + field.onFinishEditing_ = this.createNewVar_; + field.onFinishEditing_('x'); + }, + /** + * Obtain a valid name for the procedure. + * Merge runs of whitespace. Strip leading and trailing whitespace. + * Beyond this, all names are legal. + * @param {string} newVar User-supplied name. + * @return {?string} Valid name, or null if a name was not specified. + * @private + * @this Blockly.Block + */ + validator_: function(newVar) { + newVar = newVar.replace(/[\s\xa0]+/g, ' ').replace(/^ | $/g, ''); + return newVar || null; + }, + /** + * Called when focusing away from the text field. + * Creates a new variable with this name. + * @param {string} newText The new variable name. + * @private + * @this Blockly.FieldTextInput + */ + createNewVar_: function(newText) { + var source = this.sourceBlock_; + if (source && source.workspace && source.workspace.options + && source.workspace.options.parentWorkspace) { + source.workspace.options.parentWorkspace.createVariable(newText); + } + } +}; + +Blockly.Blocks['procedures_callnoreturn'] = { + /** + * Block for calling a procedure with no return value. + * @this Blockly.Block + */ + init: function() { + this.appendDummyInput('TOPROW') + .appendField(this.id, 'NAME'); + this.setPreviousStatement(true); + this.setNextStatement(true); + this.setColour(Blockly.Blocks.procedures.HUE); + // Tooltip is set in renameProcedure. + this.setHelpUrl(Blockly.Msg.PROCEDURES_CALLNORETURN_HELPURL); + this.arguments_ = []; + this.quarkConnections_ = {}; + this.quarkIds_ = null; + }, + /** + * Returns the name of the procedure this block calls. + * @return {string} Procedure name. + * @this Blockly.Block + */ + getProcedureCall: function() { + // The NAME field is guaranteed to exist, null will never be returned. + return /** @type {string} */ (this.getFieldValue('NAME')); + }, + /** + * Notification that a procedure is renaming. + * If the name matches this block's procedure, rename it. + * @param {string} oldName Previous name of procedure. + * @param {string} newName Renamed procedure. + * @this Blockly.Block + */ + renameProcedure: function(oldName, newName) { + if (Blockly.Names.equals(oldName, this.getProcedureCall())) { + this.setFieldValue(newName, 'NAME'); + this.setTooltip( + (this.outputConnection ? Blockly.Msg.PROCEDURES_CALLRETURN_TOOLTIP : + Blockly.Msg.PROCEDURES_CALLNORETURN_TOOLTIP) + .replace('%1', newName)); + } + }, + /** + * Notification that the procedure's parameters have changed. + * @param {!Array.<string>} paramNames New param names, e.g. ['x', 'y', 'z']. + * @param {!Array.<string>} paramIds IDs of params (consistent for each + * parameter through the life of a mutator, regardless of param renaming), + * e.g. ['piua', 'f8b_', 'oi.o']. + * @private + * @this Blockly.Block + */ + setProcedureParameters_: function(paramNames, paramIds) { + // Data structures: + // this.arguments = ['x', 'y'] + // Existing param names. + // this.quarkConnections_ {piua: null, f8b_: Blockly.Connection} + // Look-up of paramIds to connections plugged into the call block. + // this.quarkIds_ = ['piua', 'f8b_'] + // Existing param IDs. + // Note that quarkConnections_ may include IDs that no longer exist, but + // which might reappear if a param is reattached in the mutator. + var defBlock = Blockly.Procedures.getDefinition(this.getProcedureCall(), + this.workspace); + var mutatorOpen = defBlock && defBlock.mutator && + defBlock.mutator.isVisible(); + if (!mutatorOpen) { + this.quarkConnections_ = {}; + this.quarkIds_ = null; + } + if (!paramIds) { + // Reset the quarks (a mutator is about to open). + return; + } + if (goog.array.equals(this.arguments_, paramNames)) { + // No change. + this.quarkIds_ = paramIds; + return; + } + if (paramIds.length != paramNames.length) { + throw 'Error: paramNames and paramIds must be the same length.'; + } + this.setCollapsed(false); + if (!this.quarkIds_) { + // Initialize tracking for this block. + this.quarkConnections_ = {}; + if (paramNames.join('\n') == this.arguments_.join('\n')) { + // No change to the parameters, allow quarkConnections_ to be + // populated with the existing connections. + this.quarkIds_ = paramIds; + } else { + this.quarkIds_ = []; + } + } + // Switch off rendering while the block is rebuilt. + var savedRendered = this.rendered; + this.rendered = false; + // Update the quarkConnections_ with existing connections. + for (var i = 0; i < this.arguments_.length; i++) { + var input = this.getInput('ARG' + i); + if (input) { + var connection = input.connection.targetConnection; + this.quarkConnections_[this.quarkIds_[i]] = connection; + if (mutatorOpen && connection && + paramIds.indexOf(this.quarkIds_[i]) == -1) { + // This connection should no longer be attached to this block. + connection.disconnect(); + connection.getSourceBlock().bumpNeighbours_(); + } + } + } + // Rebuild the block's arguments. + this.arguments_ = [].concat(paramNames); + this.updateShape_(); + this.quarkIds_ = paramIds; + // Reconnect any child blocks. + if (this.quarkIds_) { + for (var i = 0; i < this.arguments_.length; i++) { + var quarkId = this.quarkIds_[i]; + if (quarkId in this.quarkConnections_) { + var connection = this.quarkConnections_[quarkId]; + if (!Blockly.Mutator.reconnect(connection, this, 'ARG' + i)) { + // Block no longer exists or has been attached elsewhere. + delete this.quarkConnections_[quarkId]; + } + } + } + } + // Restore rendering and show the changes. + this.rendered = savedRendered; + if (this.rendered) { + this.render(); + } + }, + /** + * Modify this block to have the correct number of arguments. + * @private + * @this Blockly.Block + */ + updateShape_: function() { + for (var i = 0; i < this.arguments_.length; i++) { + var field = this.getField('ARGNAME' + i); + if (field) { + // Ensure argument name is up to date. + // The argument name field is deterministic based on the mutation, + // no need to fire a change event. + Blockly.Events.disable(); + try { + field.setValue(this.arguments_[i]); + } finally { + Blockly.Events.enable(); + } + } else { + // Add new input. + field = new Blockly.FieldLabel(this.arguments_[i]); + var input = this.appendValueInput('ARG' + i) + .setAlign(Blockly.ALIGN_RIGHT) + .appendField(field, 'ARGNAME' + i); + input.init(); + } + } + // Remove deleted inputs. + while (this.getInput('ARG' + i)) { + this.removeInput('ARG' + i); + i++; + } + // Add 'with:' if there are parameters, remove otherwise. + var topRow = this.getInput('TOPROW'); + if (topRow) { + if (this.arguments_.length) { + if (!this.getField('WITH')) { + topRow.appendField(Blockly.Msg.PROCEDURES_CALL_BEFORE_PARAMS, 'WITH'); + topRow.init(); + } + } else { + if (this.getField('WITH')) { + topRow.removeField('WITH'); + } + } + } + }, + /** + * Create XML to represent the (non-editable) name and arguments. + * @return {!Element} XML storage element. + * @this Blockly.Block + */ + mutationToDom: function() { + var container = document.createElement('mutation'); + container.setAttribute('name', this.getProcedureCall()); + for (var i = 0; i < this.arguments_.length; i++) { + var parameter = document.createElement('arg'); + parameter.setAttribute('name', this.arguments_[i]); + container.appendChild(parameter); + } + return container; + }, + /** + * Parse XML to restore the (non-editable) name and parameters. + * @param {!Element} xmlElement XML storage element. + * @this Blockly.Block + */ + domToMutation: function(xmlElement) { + var name = xmlElement.getAttribute('name'); + this.renameProcedure(this.getProcedureCall(), name); + var args = []; + var paramIds = []; + for (var i = 0, childNode; childNode = xmlElement.childNodes[i]; i++) { + if (childNode.nodeName.toLowerCase() == 'arg') { + args.push(childNode.getAttribute('name')); + paramIds.push(childNode.getAttribute('paramId')); + } + } + this.setProcedureParameters_(args, paramIds); + }, + /** + * Notification that a variable is renaming. + * If the name matches one of this block's variables, rename it. + * @param {string} oldName Previous name of variable. + * @param {string} newName Renamed variable. + * @this Blockly.Block + */ + renameVar: function(oldName, newName) { + for (var i = 0; i < this.arguments_.length; i++) { + if (Blockly.Names.equals(oldName, this.arguments_[i])) { + this.arguments_[i] = newName; + this.getField('ARGNAME' + i).setValue(newName); + } + } + }, + /** + * Procedure calls cannot exist without the corresponding procedure + * definition. Enforce this link whenever an event is fired. + * @this Blockly.Block + */ + onchange: function(event) { + if (!this.workspace || this.workspace.isFlyout) { + // Block is deleted or is in a flyout. + return; + } + if (event.type == Blockly.Events.CREATE && + event.ids.indexOf(this.id) != -1) { + // Look for the case where a procedure call was created (usually through + // paste) and there is no matching definition. In this case, create + // an empty definition block with the correct signature. + var name = this.getProcedureCall(); + var def = Blockly.Procedures.getDefinition(name, this.workspace); + if (def && (def.type != this.defType_ || + JSON.stringify(def.arguments_) != JSON.stringify(this.arguments_))) { + // The signatures don't match. + def = null; + } + if (!def) { + Blockly.Events.setGroup(event.group); + /** + * Create matching definition block. + * <xml> + * <block type="procedures_defreturn" x="10" y="20"> + * <mutation name="test"> + * <arg name="x"></arg> + * </mutation> + * <field name="NAME">test</field> + * </block> + * </xml> + */ + var xml = goog.dom.createDom('xml'); + var block = goog.dom.createDom('block'); + block.setAttribute('type', this.defType_); + var xy = this.getRelativeToSurfaceXY(); + var x = xy.x + Blockly.SNAP_RADIUS * (this.RTL ? -1 : 1); + var y = xy.y + Blockly.SNAP_RADIUS * 2; + block.setAttribute('x', x); + block.setAttribute('y', y); + var mutation = this.mutationToDom(); + block.appendChild(mutation); + var field = goog.dom.createDom('field'); + field.setAttribute('name', 'NAME'); + field.appendChild(document.createTextNode(this.getProcedureCall())); + block.appendChild(field); + xml.appendChild(block); + Blockly.Xml.domToWorkspace(xml, this.workspace); + Blockly.Events.setGroup(false); + } + } else if (event.type == Blockly.Events.DELETE) { + // Look for the case where a procedure definition has been deleted, + // leaving this block (a procedure call) orphaned. In this case, delete + // the orphan. + var name = this.getProcedureCall(); + var def = Blockly.Procedures.getDefinition(name, this.workspace); + if (!def) { + Blockly.Events.setGroup(event.group); + this.dispose(true, false); + Blockly.Events.setGroup(false); + } + } + }, + /** + * Add menu option to find the definition block for this call. + * @param {!Array} options List of menu options to add to. + * @this Blockly.Block + */ + customContextMenu: function(options) { + var option = {enabled: true}; + option.text = Blockly.Msg.PROCEDURES_HIGHLIGHT_DEF; + var name = this.getProcedureCall(); + var workspace = this.workspace; + option.callback = function() { + var def = Blockly.Procedures.getDefinition(name, workspace); + def && def.select(); + }; + options.push(option); + }, + defType_: 'procedures_defnoreturn' +}; + +Blockly.Blocks['procedures_callreturn'] = { + /** + * Block for calling a procedure with a return value. + * @this Blockly.Block + */ + init: function() { + this.appendDummyInput('TOPROW') + .appendField('', 'NAME'); + this.setOutput(true); + this.setColour(Blockly.Blocks.procedures.HUE); + // Tooltip is set in domToMutation. + this.setHelpUrl(Blockly.Msg.PROCEDURES_CALLRETURN_HELPURL); + this.arguments_ = []; + this.quarkConnections_ = {}; + this.quarkIds_ = null; + }, + getProcedureCall: Blockly.Blocks['procedures_callnoreturn'].getProcedureCall, + renameProcedure: Blockly.Blocks['procedures_callnoreturn'].renameProcedure, + setProcedureParameters_: + Blockly.Blocks['procedures_callnoreturn'].setProcedureParameters_, + updateShape_: Blockly.Blocks['procedures_callnoreturn'].updateShape_, + mutationToDom: Blockly.Blocks['procedures_callnoreturn'].mutationToDom, + domToMutation: Blockly.Blocks['procedures_callnoreturn'].domToMutation, + renameVar: Blockly.Blocks['procedures_callnoreturn'].renameVar, + onchange: Blockly.Blocks['procedures_callnoreturn'].onchange, + customContextMenu: + Blockly.Blocks['procedures_callnoreturn'].customContextMenu, + defType_: 'procedures_defreturn' +}; + +Blockly.Blocks['procedures_ifreturn'] = { + /** + * Block for conditionally returning a value from a procedure. + * @this Blockly.Block + */ + init: function() { + this.appendValueInput('CONDITION') + .setCheck('Boolean') + .appendField(Blockly.Msg.CONTROLS_IF_MSG_IF); + this.appendValueInput('VALUE') + .appendField(Blockly.Msg.PROCEDURES_DEFRETURN_RETURN); + this.setInputsInline(true); + this.setPreviousStatement(true); + this.setNextStatement(true); + this.setColour(Blockly.Blocks.procedures.HUE); + this.setTooltip(Blockly.Msg.PROCEDURES_IFRETURN_TOOLTIP); + this.setHelpUrl(Blockly.Msg.PROCEDURES_IFRETURN_HELPURL); + this.hasReturnValue_ = true; + }, + /** + * Create XML to represent whether this block has a return value. + * @return {!Element} XML storage element. + * @this Blockly.Block + */ + mutationToDom: function() { + var container = document.createElement('mutation'); + container.setAttribute('value', Number(this.hasReturnValue_)); + return container; + }, + /** + * Parse XML to restore whether this block has a return value. + * @param {!Element} xmlElement XML storage element. + * @this Blockly.Block + */ + domToMutation: function(xmlElement) { + var value = xmlElement.getAttribute('value'); + this.hasReturnValue_ = (value == 1); + if (!this.hasReturnValue_) { + this.removeInput('VALUE'); + this.appendDummyInput('VALUE') + .appendField(Blockly.Msg.PROCEDURES_DEFRETURN_RETURN); + } + }, + /** + * Called whenever anything on the workspace changes. + * Add warning if this flow block is not nested inside a loop. + * @param {!Blockly.Events.Abstract} e Change event. + * @this Blockly.Block + */ + onchange: function(e) { + var legal = false; + // Is the block nested in a procedure? + var block = this; + do { + if (this.FUNCTION_TYPES.indexOf(block.type) != -1) { + legal = true; + break; + } + block = block.getSurroundParent(); + } while (block); + if (legal) { + // If needed, toggle whether this block has a return value. + if (block.type == 'procedures_defnoreturn' && this.hasReturnValue_) { + this.removeInput('VALUE'); + this.appendDummyInput('VALUE') + .appendField(Blockly.Msg.PROCEDURES_DEFRETURN_RETURN); + this.hasReturnValue_ = false; + } else if (block.type == 'procedures_defreturn' && + !this.hasReturnValue_) { + this.removeInput('VALUE'); + this.appendValueInput('VALUE') + .appendField(Blockly.Msg.PROCEDURES_DEFRETURN_RETURN); + this.hasReturnValue_ = true; + } + this.setWarningText(null); + } else { + this.setWarningText(Blockly.Msg.PROCEDURES_IFRETURN_WARNING); + } + }, + /** + * List of block types that are functions and thus do not need warnings. + * To add a new function type add this to your code: + * Blockly.Blocks['procedures_ifreturn'].FUNCTION_TYPES.push('custom_func'); + */ + FUNCTION_TYPES: ['procedures_defnoreturn', 'procedures_defreturn'] +}; |