/** * @license * Visual Blocks Editor * * Copyright 2012 Google Inc. * https://blockly.googlecode.com/ * * 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'); Blockly.Blocks['procedures_defnoreturn'] = { // Define a procedure with no return value. init: function() { this.setHelpUrl(Blockly.Msg.PROCEDURES_DEFNORETURN_HELPURL); this.setColour(290); var name = Blockly.Procedures.findLegalName( Blockly.Msg.PROCEDURES_DEFNORETURN_PROCEDURE, this); this.appendDummyInput() .appendField(Blockly.Msg.PROCEDURES_DEFNORETURN_TITLE) .appendField(new Blockly.FieldTextInput(name, Blockly.Procedures.rename), 'NAME') .appendField('', 'PARAMS'); this.appendStatementInput('STACK') .appendField(Blockly.Msg.PROCEDURES_DEFNORETURN_DO); this.setMutator(new Blockly.Mutator(['procedures_mutatorarg'])); this.setTooltip(Blockly.Msg.PROCEDURES_DEFNORETURN_TOOLTIP); this.arguments_ = []; }, updateParams_: function() { // Check for duplicated arguments. var badArg = false; var hash = {}; for (var x = 0; x < this.arguments_.length; x++) { if (hash['arg_' + this.arguments_[x].toLowerCase()]) { badArg = true; break; } hash['arg_' + this.arguments_[x].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(', '); } this.setFieldValue(paramString, 'PARAMS'); }, mutationToDom: function() { var container = document.createElement('mutation'); for (var x = 0; x < this.arguments_.length; x++) { var parameter = document.createElement('arg'); parameter.setAttribute('name', this.arguments_[x]); container.appendChild(parameter); } return container; }, domToMutation: function(xmlElement) { this.arguments_ = []; for (var x = 0, childNode; childNode = xmlElement.childNodes[x]; x++) { if (childNode.nodeName.toLowerCase() == 'arg') { this.arguments_.push(childNode.getAttribute('name')); } } this.updateParams_(); }, decompose: function(workspace) { var containerBlock = Blockly.Block.obtain(workspace, 'procedures_mutatorcontainer'); containerBlock.initSvg(); var connection = containerBlock.getInput('STACK').connection; for (var x = 0; x < this.arguments_.length; x++) { var paramBlock = Blockly.Block.obtain(workspace, 'procedures_mutatorarg'); paramBlock.initSvg(); paramBlock.setFieldValue(this.arguments_[x], 'NAME'); // Store the old location. paramBlock.oldLocation = x; connection.connect(paramBlock.previousConnection); connection = paramBlock.nextConnection; } // Initialize procedure's callers with blank IDs. Blockly.Procedures.mutateCallers(this.getFieldValue('NAME'), this.workspace, this.arguments_, null); return containerBlock; }, compose: function(containerBlock) { 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.getFieldValue('NAME'), this.workspace, this.arguments_, this.paramIds_); }, dispose: function() { // Dispose of any callers. var name = this.getFieldValue('NAME'); Blockly.Procedures.disposeCallers(name, this.workspace); // Call parent's destructor. Blockly.Block.prototype.dispose.apply(this, arguments); }, getProcedureDef: function() { // Return the name of the defined procedure, // a list of all its arguments, // and that it DOES NOT have a return value. return [this.getFieldValue('NAME'), this.arguments_, false]; }, getVars: function() { return this.arguments_; }, renameVar: function(oldName, newName) { var change = false; for (var x = 0; x < this.arguments_.length; x++) { if (Blockly.Names.equals(oldName, this.arguments_[x])) { this.arguments_[x] = 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 x = 0, block; block = blocks[x]; x++) { if (block.type == 'procedures_mutatorarg' && Blockly.Names.equals(oldName, block.getFieldValue('NAME'))) { block.setFieldValue(newName, 'NAME'); } } } } }, 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 x = 0; x < this.arguments_.length; x++) { var xmlArg = goog.dom.createDom('arg'); xmlArg.setAttribute('name', this.arguments_[x]); 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. for (var x = 0; x < this.arguments_.length; x++) { var option = {enabled: true}; var name = this.arguments_[x]; 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'] = { // Define a procedure with a return value. init: function() { this.setHelpUrl(Blockly.Msg.PROCEDURES_DEFRETURN_HELPURL); this.setColour(290); var name = Blockly.Procedures.findLegalName( Blockly.Msg.PROCEDURES_DEFRETURN_PROCEDURE, this); this.appendDummyInput() .appendField(Blockly.Msg.PROCEDURES_DEFRETURN_TITLE) .appendField(new Blockly.FieldTextInput(name, Blockly.Procedures.rename), 'NAME') .appendField('', 'PARAMS'); this.appendStatementInput('STACK') .appendField(Blockly.Msg.PROCEDURES_DEFRETURN_DO); this.appendValueInput('RETURN') .setAlign(Blockly.ALIGN_RIGHT) .appendField(Blockly.Msg.PROCEDURES_DEFRETURN_RETURN); this.setMutator(new Blockly.Mutator(['procedures_mutatorarg'])); this.setTooltip(Blockly.Msg.PROCEDURES_DEFRETURN_TOOLTIP); this.arguments_ = []; }, 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, dispose: Blockly.Blocks['procedures_defnoreturn'].dispose, getProcedureDef: function() { // Return the name of the defined procedure, // a list of all its arguments, // and that it DOES have a return value. 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'] = { // Procedure container (for mutator dialog). init: function() { this.setColour(290); this.appendDummyInput() .appendField(Blockly.Msg.PROCEDURES_MUTATORCONTAINER_TITLE); this.appendStatementInput('STACK'); this.setTooltip(''); this.contextMenu = false; } }; Blockly.Blocks['procedures_mutatorarg'] = { // Procedure argument (for mutator dialog). init: function() { this.setColour(290); this.appendDummyInput() .appendField(Blockly.Msg.PROCEDURES_MUTATORARG_TITLE) .appendField(new Blockly.FieldTextInput('x', this.validator), 'NAME'); this.setPreviousStatement(true); this.setNextStatement(true); this.setTooltip(''); this.contextMenu = false; }, validator: function(newVar) { // Merge runs of whitespace. Strip leading and trailing whitespace. // Beyond this, all names are legal. newVar = newVar.replace(/[\s\xa0]+/g, ' ').replace(/^ | $/g, ''); return newVar || null; } }; Blockly.Blocks['procedures_callnoreturn'] = { // Call a procedure with no return value. init: function() { this.setHelpUrl(Blockly.Msg.PROCEDURES_CALLNORETURN_HELPURL); this.setColour(290); this.appendDummyInput() .appendField(Blockly.Msg.PROCEDURES_CALLNORETURN_CALL) .appendField('', 'NAME'); this.setPreviousStatement(true); this.setNextStatement(true); this.setTooltip(Blockly.Msg.PROCEDURES_CALLNORETURN_TOOLTIP); this.arguments_ = []; this.quarkConnections_ = null; this.quarkArguments_ = null; }, getProcedureCall: function() { return this.getFieldValue('NAME'); }, renameProcedure: function(oldName, newName) { if (Blockly.Names.equals(oldName, this.getFieldValue('NAME'))) { this.setFieldValue(newName, 'NAME'); this.setTooltip( (this.outputConnection ? Blockly.Msg.PROCEDURES_CALLRETURN_TOOLTIP : Blockly.Msg.PROCEDURES_CALLNORETURN_TOOLTIP) .replace('%1', newName)); } }, setProcedureParameters: function(paramNames, paramIds) { // Data structures for parameters on each call block: // this.arguments = ['x', 'y'] // Existing param names. // paramNames = ['x', 'y', 'z'] // New param names. // paramIds = ['piua', 'f8b_', 'oi.o'] // IDs of params (consistent for each parameter through the life of a // mutator, regardless of param renaming). // this.quarkConnections_ {piua: null, f8b_: Blockly.Connection} // Look-up of paramIds to connections plugged into the call block. // this.quarkArguments_ = ['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. if (!paramIds) { // Reset the quarks (a mutator is about to open). this.quarkConnections_ = {}; this.quarkArguments_ = null; return; } if (paramIds.length != paramNames.length) { throw 'Error: paramNames and paramIds must be the same length.'; } if (!this.quarkArguments_) { // 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.quarkArguments_ = paramIds; } else { this.quarkArguments_ = []; } } // Switch off rendering while the block is rebuilt. var savedRendered = this.rendered; this.rendered = false; // Update the quarkConnections_ with existing connections. for (var x = this.arguments_.length - 1; x >= 0; x--) { var input = this.getInput('ARG' + x); if (input) { var connection = input.connection.targetConnection; this.quarkConnections_[this.quarkArguments_[x]] = connection; // Disconnect all argument blocks and remove all inputs. this.removeInput('ARG' + x); } } // Rebuild the block's arguments. this.arguments_ = [].concat(paramNames); this.quarkArguments_ = paramIds; for (var x = 0; x < this.arguments_.length; x++) { var input = this.appendValueInput('ARG' + x) .setAlign(Blockly.ALIGN_RIGHT) .appendField(this.arguments_[x]); if (this.quarkArguments_) { // Reconnect any child blocks. var quarkName = this.quarkArguments_[x]; if (quarkName in this.quarkConnections_) { var connection = this.quarkConnections_[quarkName]; if (!connection || connection.targetConnection || connection.sourceBlock_.workspace != this.workspace) { // Block no longer exists or has been attached elsewhere. delete this.quarkConnections_[quarkName]; } else { input.connection.connect(connection); } } } } // Restore rendering and show the changes. this.rendered = savedRendered; if (this.rendered) { this.render(); } }, mutationToDom: function() { // Save the name and arguments (none of which are editable). var container = document.createElement('mutation'); container.setAttribute('name', this.getFieldValue('NAME')); for (var x = 0; x < this.arguments_.length; x++) { var parameter = document.createElement('arg'); parameter.setAttribute('name', this.arguments_[x]); container.appendChild(parameter); } return container; }, domToMutation: function(xmlElement) { // Restore the name and parameters. var name = xmlElement.getAttribute('name'); this.setFieldValue(name, 'NAME'); this.setTooltip( (this.outputConnection ? Blockly.Msg.PROCEDURES_CALLRETURN_TOOLTIP : Blockly.Msg.PROCEDURES_CALLNORETURN_TOOLTIP).replace('%1', name)); var def = Blockly.Procedures.getDefinition(name, this.workspace); if (def && def.mutator.isVisible()) { // Initialize caller with the mutator's IDs. this.setProcedureParameters(def.arguments_, def.paramIds_); } else { this.arguments_ = []; for (var x = 0, childNode; childNode = xmlElement.childNodes[x]; x++) { if (childNode.nodeName.toLowerCase() == 'arg') { this.arguments_.push(childNode.getAttribute('name')); } } // For the second argument (paramIds) use the arguments list as a dummy // list. this.setProcedureParameters(this.arguments_, this.arguments_); } }, renameVar: function(oldName, newName) { for (var x = 0; x < this.arguments_.length; x++) { if (Blockly.Names.equals(oldName, this.arguments_[x])) { this.arguments_[x] = newName; this.getInput('ARG' + x).fieldRow[0].setText(newName); } } }, customContextMenu: function(options) { // Add option to find caller. var option = {enabled: true}; option.text = Blockly.Msg.PROCEDURES_HIGHLIGHT_DEF; var name = this.getFieldValue('NAME'); var workspace = this.workspace; option.callback = function() { var def = Blockly.Procedures.getDefinition(name, workspace); def && def.select(); }; options.push(option); } }; Blockly.Blocks['procedures_callreturn'] = { // Call a procedure with a return value. init: function() { this.setHelpUrl(Blockly.Msg.PROCEDURES_CALLRETURN_HELPURL); this.setColour(290); this.appendDummyInput() .appendField(Blockly.Msg.PROCEDURES_CALLRETURN_CALL) .appendField('', 'NAME'); this.setOutput(true); this.setTooltip(Blockly.Msg.PROCEDURES_CALLRETURN_TOOLTIP); this.arguments_ = []; this.quarkConnections_ = null; this.quarkArguments_ = null; }, getProcedureCall: Blockly.Blocks['procedures_callnoreturn'].getProcedureCall, renameProcedure: Blockly.Blocks['procedures_callnoreturn'].renameProcedure, setProcedureParameters: Blockly.Blocks['procedures_callnoreturn'].setProcedureParameters, mutationToDom: Blockly.Blocks['procedures_callnoreturn'].mutationToDom, domToMutation: Blockly.Blocks['procedures_callnoreturn'].domToMutation, renameVar: Blockly.Blocks['procedures_callnoreturn'].renameVar, customContextMenu: Blockly.Blocks['procedures_callnoreturn'].customContextMenu }; Blockly.Blocks['procedures_ifreturn'] = { // Conditionally return value from a procedure. init: function() { this.setHelpUrl('http://c2.com/cgi/wiki?GuardClause'); this.setColour(290); 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.setTooltip(Blockly.Msg.PROCEDURES_IFRETURN_TOOLTIP); this.hasReturnValue_ = true; }, mutationToDom: function() { // Save whether this block has a return value. var container = document.createElement('mutation'); container.setAttribute('value', Number(this.hasReturnValue_)); return container; }, domToMutation: function(xmlElement) { // Restore whether this block has a return value. var value = xmlElement.getAttribute('value'); this.hasReturnValue_ = (value == 1); if (!this.hasReturnValue_) { this.removeInput('VALUE'); this.appendDummyInput('VALUE') .appendField(Blockly.Msg.PROCEDURES_DEFRETURN_RETURN); } }, onchange: function() { if (!this.workspace) { // Block has been deleted. return; } var legal = false; // Is the block nested in a procedure? var block = this; do { if (block.type == 'procedures_defnoreturn' || block.type == 'procedures_defreturn') { 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); } } };