scratch-blocks/core/procedures.js

577 lines
19 KiB
JavaScript

/**
* @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 Utility functions for handling procedures.
* @author fraser@google.com (Neil Fraser)
*/
'use strict';
/**
* @name Blockly.Procedures
* @namespace
**/
goog.provide('Blockly.Procedures');
goog.require('Blockly.Blocks');
goog.require('Blockly.constants');
goog.require('Blockly.Events.BlockChange');
goog.require('Blockly.Field');
goog.require('Blockly.Names');
goog.require('Blockly.Workspace');
/**
* Constant to separate procedure names from variables and generated functions
* when running generators.
* @deprecated Use Blockly.PROCEDURE_CATEGORY_NAME
*/
Blockly.Procedures.NAME_TYPE = Blockly.PROCEDURE_CATEGORY_NAME;
/**
* Find all user-created procedure definitions in a workspace.
* @param {!Blockly.Workspace} root Root workspace.
* @return {!Array.<!Array.<!Array>>} Pair of arrays, the
* first contains procedures without return variables, the second with.
* Each procedure is defined by a three-element list of name, parameter
* list, and return value boolean.
*/
Blockly.Procedures.allProcedures = function(root) {
var blocks = root.getAllBlocks();
var proceduresReturn = [];
var proceduresNoReturn = [];
for (var i = 0; i < blocks.length; i++) {
if (blocks[i].getProcedureDef) {
var tuple = blocks[i].getProcedureDef();
if (tuple) {
if (tuple[2]) {
proceduresReturn.push(tuple);
} else {
proceduresNoReturn.push(tuple);
}
}
}
}
proceduresNoReturn.sort(Blockly.Procedures.procTupleComparator_);
proceduresReturn.sort(Blockly.Procedures.procTupleComparator_);
return [proceduresNoReturn, proceduresReturn];
};
/**
* Find all user-created procedure definition mutations in a workspace.
* @param {!Blockly.Workspace} root Root workspace.
* @return {!Array.<Element>} Array of mutation xml elements.
* @package
*/
Blockly.Procedures.allProcedureMutations = function(root) {
var blocks = root.getAllBlocks();
var mutations = [];
for (var i = 0; i < blocks.length; i++) {
if (blocks[i].type == Blockly.PROCEDURES_PROTOTYPE_BLOCK_TYPE) {
var mutation = blocks[i].mutationToDom(/* opt_generateShadows */ true);
if (mutation) {
mutations.push(mutation);
}
}
}
return mutations;
};
/**
* Sorts an array of procedure definition mutations alphabetically.
* (Does not mutate the given array.)
* @param {!Array.<Element>} mutations Array of mutation xml elements.
* @return {!Array.<Element>} Sorted array of mutation xml elements.
* @private
*/
Blockly.Procedures.sortProcedureMutations_ = function(mutations) {
var newMutations = mutations.slice();
newMutations.sort(function(a, b) {
var procCodeA = a.getAttribute('proccode');
var procCodeB = b.getAttribute('proccode');
return Blockly.scratchBlocksUtils.compareStrings(procCodeA, procCodeB);
});
return newMutations;
};
/**
* Comparison function for case-insensitive sorting of the first element of
* a tuple.
* @param {!Array} ta First tuple.
* @param {!Array} tb Second tuple.
* @return {number} -1, 0, or 1 to signify greater than, equality, or less than.
* @private
*/
Blockly.Procedures.procTupleComparator_ = function(ta, tb) {
return Blockly.scratchBlocksUtils.compareStrings(ta[0], tb[0]);
};
/**
* Ensure two identically-named procedures don't exist.
* @param {string} name Proposed procedure name.
* @param {!Blockly.Block} block Block to disambiguate.
* @return {string} Non-colliding name.
*/
Blockly.Procedures.findLegalName = function(name, block) {
if (block.isInFlyout) {
// Flyouts can have multiple procedures called 'do something'.
return name;
}
while (!Blockly.Procedures.isLegalName_(name, block.workspace, block)) {
// Collision with another procedure.
var r = name.match(/^(.*?)(\d+)$/);
if (!r) {
name += '2';
} else {
name = r[1] + (parseInt(r[2], 10) + 1);
}
}
return name;
};
/**
* Does this procedure have a legal name? Illegal names include names of
* procedures already defined.
* @param {string} name The questionable name.
* @param {!Blockly.Workspace} workspace The workspace to scan for collisions.
* @param {Blockly.Block=} opt_exclude Optional block to exclude from
* comparisons (one doesn't want to collide with oneself).
* @return {boolean} True if the name is legal.
* @private
*/
Blockly.Procedures.isLegalName_ = function(name, workspace, opt_exclude) {
return !Blockly.Procedures.isNameUsed(name, workspace, opt_exclude);
};
/**
* Return if the given name is already a procedure name.
* @param {string} name The questionable name.
* @param {!Blockly.Workspace} workspace The workspace to scan for collisions.
* @param {Blockly.Block=} opt_exclude Optional block to exclude from
* comparisons (one doesn't want to collide with oneself).
* @return {boolean} True if the name is used, otherwise return false.
*/
Blockly.Procedures.isNameUsed = function(name, workspace, opt_exclude) {
var blocks = workspace.getAllBlocks();
// Iterate through every block and check the name.
for (var i = 0; i < blocks.length; i++) {
if (blocks[i] == opt_exclude) {
continue;
}
if (blocks[i].getProcedureDef) {
var procName = blocks[i].getProcedureDef();
if (Blockly.Names.equals(procName[0], name)) {
return false;
}
}
}
return true;
};
/**
* Rename a procedure. Called by the editable field.
* @param {string} name The proposed new name.
* @return {string} The accepted name.
* @this {Blockly.Field}
*/
Blockly.Procedures.rename = function(name) {
// Strip leading and trailing whitespace. Beyond this, all names are legal.
name = name.replace(/^[\s\xa0]+|[\s\xa0]+$/g, '');
// Ensure two identically-named procedures don't exist.
var legalName = Blockly.Procedures.findLegalName(name, this.sourceBlock_);
var oldName = this.text_;
if (oldName != name && oldName != legalName) {
// Rename any callers.
var blocks = this.sourceBlock_.workspace.getAllBlocks();
for (var i = 0; i < blocks.length; i++) {
if (blocks[i].renameProcedure) {
blocks[i].renameProcedure(oldName, legalName);
}
}
}
return legalName;
};
/**
* Construct the blocks required by the flyout for the procedure category.
* @param {!Blockly.Workspace} workspace The workspace contianing procedures.
* @return {!Array.<!Element>} Array of XML block elements.
*/
Blockly.Procedures.flyoutCategory = function(workspace) {
var xmlList = [];
Blockly.Procedures.addCreateButton_(workspace, xmlList);
// Create call blocks for each procedure defined in the workspace
var mutations = Blockly.Procedures.allProcedureMutations(workspace);
mutations = Blockly.Procedures.sortProcedureMutations_(mutations);
for (var i = 0; i < mutations.length; i++) {
var mutation = mutations[i];
// <block type="procedures_call">
// <mutation ...></mutation>
// </block>
var block = goog.dom.createDom('block');
block.setAttribute('type', 'procedures_call');
block.setAttribute('gap', 16);
block.appendChild(mutation);
xmlList.push(block);
}
return xmlList;
};
/**
* Create the "Make a Block..." button.
* @param {!Blockly.Workspace} workspace The workspace contianing procedures.
* @param {!Array.<!Element>} xmlList Array of XML block elements to add to.
* @private
*/
Blockly.Procedures.addCreateButton_ = function(workspace, xmlList) {
var button = goog.dom.createDom('button');
var msg = Blockly.Msg.NEW_PROCEDURE;
var callbackKey = 'CREATE_PROCEDURE';
var callback = function() {
Blockly.Procedures.createProcedureDefCallback_(workspace);
};
button.setAttribute('text', msg);
button.setAttribute('callbackKey', callbackKey);
workspace.registerButtonCallback(callbackKey, callback);
xmlList.push(button);
};
/**
* Find all callers of a named procedure.
* @param {string} name Name of procedure (procCode in scratch-blocks).
* @param {!Blockly.Workspace} ws The workspace to find callers in.
* @param {!Blockly.Block} definitionRoot The root of the stack where the
* procedure is defined.
* @param {boolean} allowRecursive True if the search should include recursive
* procedure calls. False if the search should ignore the stack starting
* with definitionRoot.
* @return {!Array.<!Blockly.Block>} Array of caller blocks.
* @package
*/
Blockly.Procedures.getCallers = function(name, ws, definitionRoot,
allowRecursive) {
var allBlocks = [];
var topBlocks = ws.getTopBlocks();
// Start by deciding which stacks to investigate.
for (var i = 0; i < topBlocks.length; i++) {
var block = topBlocks[i];
if (block.id == definitionRoot.id && !allowRecursive) {
continue;
}
allBlocks.push.apply(allBlocks, block.getDescendants(false));
}
var callers = [];
for (var i = 0; i < allBlocks.length; i++) {
var block = allBlocks[i];
if (block.type == Blockly.PROCEDURES_CALL_BLOCK_TYPE ) {
var procCode = block.getProcCode();
if (procCode && procCode == name) {
callers.push(block);
}
}
}
return callers;
};
/**
* Find and edit all callers with a procCode using a new mutation.
* @param {string} name Name of procedure (procCode in scratch-blocks).
* @param {!Blockly.Workspace} ws The workspace to find callers in.
* @param {!Element} mutation New mutation for the callers.
* @package
*/
Blockly.Procedures.mutateCallersAndPrototype = function(name, ws, mutation) {
var defineBlock = Blockly.Procedures.getDefineBlock(name, ws);
var prototypeBlock = Blockly.Procedures.getPrototypeBlock(name, ws);
if (defineBlock && prototypeBlock) {
var callers = Blockly.Procedures.getCallers(name,
defineBlock.workspace, defineBlock, true /* allowRecursive */);
callers.push(prototypeBlock);
Blockly.Events.setGroup(true);
for (var i = 0, caller; caller = callers[i]; i++) {
var oldMutationDom = caller.mutationToDom();
var oldMutation = oldMutationDom && Blockly.Xml.domToText(oldMutationDom);
caller.domToMutation(mutation);
var newMutationDom = caller.mutationToDom();
var newMutation = newMutationDom && Blockly.Xml.domToText(newMutationDom);
if (oldMutation != newMutation) {
Blockly.Events.fire(new Blockly.Events.BlockChange(
caller, 'mutation', null, oldMutation, newMutation));
}
}
Blockly.Events.setGroup(false);
} else {
alert('No define block on workspace'); // TODO decide what to do about this.
}
};
/**
* Find the definition block for the named procedure.
* @param {string} procCode The identifier of the procedure.
* @param {!Blockly.Workspace} workspace The workspace to search.
* @return {Blockly.Block} The procedure definition block, or null not found.
* @package
*/
Blockly.Procedures.getDefineBlock = function(procCode, workspace) {
// Assume that a procedure definition is a top block.
var blocks = workspace.getTopBlocks(false);
for (var i = 0; i < blocks.length; i++) {
if (blocks[i].type == Blockly.PROCEDURES_DEFINITION_BLOCK_TYPE) {
var prototypeBlock = blocks[i].getInput('custom_block').connection.targetBlock();
if (prototypeBlock.getProcCode && prototypeBlock.getProcCode() == procCode) {
return blocks[i];
}
}
}
return null;
};
/**
* Find the prototype block for the named procedure.
* @param {string} procCode The identifier of the procedure.
* @param {!Blockly.Workspace} workspace The workspace to search.
* @return {Blockly.Block} The procedure prototype block, or null not found.
* @package
*/
Blockly.Procedures.getPrototypeBlock = function(procCode, workspace) {
var defineBlock = Blockly.Procedures.getDefineBlock(procCode, workspace);
if (defineBlock) {
return defineBlock.getInput('custom_block').connection.targetBlock();
}
return null;
};
/**
* Create a mutation for a brand new custom procedure.
* @return {Element} The mutation for a new custom procedure
* @package
*/
Blockly.Procedures.newProcedureMutation = function() {
var mutationText = '<xml>' +
'<mutation' +
' proccode="' + Blockly.Msg['PROCEDURE_DEFAULT_NAME'] + '"' +
' argumentids="[]"' +
' argumentnames="[]"' +
' argumentdefaults="[]"' +
' warp="false">' +
'</mutation>' +
'</xml>';
return Blockly.Xml.textToDom(mutationText).firstChild;
};
/**
* Callback to create a new procedure custom command block.
* @param {!Blockly.Workspace} workspace The workspace to create the new procedure on.
* @private
*/
Blockly.Procedures.createProcedureDefCallback_ = function(workspace) {
Blockly.Procedures.externalProcedureDefCallback(
Blockly.Procedures.newProcedureMutation(),
Blockly.Procedures.createProcedureCallbackFactory_(workspace)
);
};
/**
* Callback factory for adding a new custom procedure from a mutation.
* @param {!Blockly.Workspace} workspace The workspace to create the new procedure on.
* @return {function(?Element)} callback for creating the new custom procedure.
* @private
*/
Blockly.Procedures.createProcedureCallbackFactory_ = function(workspace) {
return function(mutation) {
if (mutation) {
var blockText = '<xml>' +
'<block type="procedures_definition">' +
'<statement name="custom_block">' +
'<shadow type="procedures_prototype">' +
Blockly.Xml.domToText(mutation) +
'</shadow>' +
'</statement>' +
'</block>' +
'</xml>';
var blockDom = Blockly.Xml.textToDom(blockText).firstChild;
Blockly.Events.setGroup(true);
var block = Blockly.Xml.domToBlock(blockDom, workspace);
var scale = workspace.scale; // To convert from pixel units to workspace units
// Position the block so that it is at the top left of the visible workspace,
// padded from the edge by 30 units. Position in the top right if RTL.
var posX = -workspace.scrollX;
if (workspace.RTL) {
posX += workspace.getMetrics().contentWidth - 30;
} else {
posX += 30;
}
block.moveBy(posX / scale, (-workspace.scrollY + 30) / scale);
block.scheduleSnapAndBump();
Blockly.Events.setGroup(false);
}
};
};
/**
* Callback to open the modal for editing custom procedures.
* @param {!Blockly.Block} block The block that was right-clicked.
* @private
*/
Blockly.Procedures.editProcedureCallback_ = function(block) {
// Edit can come from one of three block types (call, define, prototype)
// Normalize by setting the block to the prototype block for the procedure.
if (block.type == Blockly.PROCEDURES_DEFINITION_BLOCK_TYPE) {
var input = block.getInput('custom_block');
if (!input) {
alert('Bad input'); // TODO: Decide what to do about this.
return;
}
var conn = input.connection;
if (!conn) {
alert('Bad connection'); // TODO: Decide what to do about this.
return;
}
var innerBlock = conn.targetBlock();
if (!innerBlock ||
!innerBlock.type == Blockly.PROCEDURES_PROTOTYPE_BLOCK_TYPE) {
alert('Bad inner block'); // TODO: Decide what to do about this.
return;
}
block = innerBlock;
} else if (block.type == Blockly.PROCEDURES_CALL_BLOCK_TYPE) {
// This is a call block, find the prototype corresponding to the procCode.
// Make sure to search the correct workspace, call block can be in flyout.
var workspaceToSearch = block.workspace.isFlyout ?
block.workspace.targetWorkspace : block.workspace;
block = Blockly.Procedures.getPrototypeBlock(
block.getProcCode(), workspaceToSearch);
}
// Block now refers to the procedure prototype block, it is safe to proceed.
Blockly.Procedures.externalProcedureDefCallback(
block.mutationToDom(),
Blockly.Procedures.editProcedureCallbackFactory_(block)
);
};
/**
* Callback factory for editing an existing custom procedure.
* @param {!Blockly.Block} block The procedure prototype block being edited.
* @return {function(?Element)} Callback for editing the custom procedure.
* @private
*/
Blockly.Procedures.editProcedureCallbackFactory_ = function(block) {
return function(mutation) {
if (mutation) {
Blockly.Procedures.mutateCallersAndPrototype(block.getProcCode(),
block.workspace, mutation);
}
};
};
/**
* Callback to create a new procedure custom command block.
* @public
*/
Blockly.Procedures.externalProcedureDefCallback = function(/** mutator, callback */) {
alert('External procedure editor must be override Blockly.Procedures.externalProcedureDefCallback');
};
/**
* Make a context menu option for editing a custom procedure.
* This appears in the context menu for procedure definitions and procedure
* calls.
* @param {!Blockly.BlockSvg} block The block where the right-click originated.
* @return {!Object} A menu option, containing text, enabled, and a callback.
* @package
*/
Blockly.Procedures.makeEditOption = function(block) {
var editOption = {
enabled: true,
text: Blockly.Msg.EDIT_PROCEDURE,
callback: function() {
Blockly.Procedures.editProcedureCallback_(block);
}
};
return editOption;
};
/**
* Callback to show the procedure definition corresponding to a custom command
* block.
* TODO(#1136): Implement.
* @param {!Blockly.Block} block The block that was right-clicked.
* @private
*/
Blockly.Procedures.showProcedureDefCallback_ = function(block) {
alert('TODO(#1136): implement showing procedure definition (procCode was "' +
block.procCode_ + '")');
};
/**
* Make a context menu option for showing the definition for a custom procedure,
* based on a right-click on a custom command block.
* @param {!Blockly.BlockSvg} block The block where the right-click originated.
* @return {!Object} A menu option, containing text, enabled, and a callback.
* @package
*/
Blockly.Procedures.makeShowDefinitionOption = function(block) {
var option = {
enabled: true,
text: Blockly.Msg.SHOW_PROCEDURE_DEFINITION,
callback: function() {
Blockly.Procedures.showProcedureDefCallback_(block);
}
};
return option;
};
/**
* Callback to try to delete a custom block definitions.
* @param {string} procCode The identifier of the procedure to delete.
* @param {!Blockly.Block} definitionRoot The root block of the stack that
* defines the custom procedure.
* @return {boolean} True if the custom procedure was deleted, false otherwise.
* @package
*/
Blockly.Procedures.deleteProcedureDefCallback = function(procCode,
definitionRoot) {
var callers = Blockly.Procedures.getCallers(procCode,
definitionRoot.workspace, definitionRoot, false /* allowRecursive */);
if (callers.length > 0) {
return false;
}
var workspace = definitionRoot.workspace;
// Delete the whole stack.
Blockly.Events.setGroup(true);
definitionRoot.dispose();
Blockly.Events.setGroup(false);
// TODO (#1354) Update this function when '_' is removed
// Refresh toolbox, so caller doesn't appear there anymore
workspace.refreshToolboxSelection_();
return true;
};