mirror of
https://github.com/google/blockly.git
synced 2026-01-10 10:27:08 +01:00
518 lines
19 KiB
JavaScript
518 lines
19 KiB
JavaScript
/**
|
|
* Visual Blocks Editor
|
|
*
|
|
* Copyright 2012 Google Inc.
|
|
* http://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);
|
|
}
|
|
}
|
|
};
|