/** * @license * Copyright 2012 Google LLC * SPDX-License-Identifier: Apache-2.0 */ /** * @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'); goog.require('Blockly.Events.BlockChange'); goog.require('Blockly.Field'); goog.require('Blockly.Msg'); goog.require('Blockly.Names'); goog.require('Blockly.utils.xml'); goog.require('Blockly.Workspace'); goog.require('Blockly.Xml'); /** * 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; /** * The default argument for a procedures_mutatorarg block. * @type {string} */ Blockly.Procedures.DEFAULT_ARG = 'x'; /** * Procedure block type. * @typedef {{ * getProcedureCall: function():string, * renameProcedure: function(string,string), * getProcedureDef: function():!Array * }} */ Blockly.Procedures.ProcedureBlock; /** * Find all user-created procedure definitions in a workspace. * @param {!Blockly.Workspace} root Root workspace. * @return {!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 proceduresNoReturn = root.getBlocksByType('procedures_defnoreturn', false) .map(function(block) { return /** @type {!Blockly.Procedures.ProcedureBlock} */ (block).getProcedureDef(); }); var proceduresReturn = root.getBlocksByType('procedures_defreturn', false).map(function(block) { return /** @type {!Blockly.Procedures.ProcedureBlock} */ (block).getProcedureDef(); }); proceduresNoReturn.sort(Blockly.Procedures.procTupleComparator_); proceduresReturn.sort(Blockly.Procedures.procTupleComparator_); return [proceduresNoReturn, proceduresReturn]; }; /** * 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 ta[0].toLowerCase().localeCompare(tb[0].toLowerCase()); }; /** * Ensure two identically-named procedures don't exist. * Take the proposed procedure name, and return a legal name i.e. one that * is not empty and doesn't collide with other procedures. * @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; } name = name || Blockly.Msg['UNNAMED_KEY'] || 'unnamed'; 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(false); // 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 procedureBlock = /** @type {!Blockly.Procedures.ProcedureBlock} */ ( blocks[i]); var procName = procedureBlock.getProcedureDef(); if (Blockly.Names.equals(procName[0], name)) { return true; } } } return false; }; /** * 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.trim(); var legalName = Blockly.Procedures.findLegalName(name, /** @type {!Blockly.Block} */ (this.getSourceBlock())); var oldName = this.getValue(); if (oldName != name && oldName != legalName) { // Rename any callers. var blocks = this.getSourceBlock().workspace.getAllBlocks(false); for (var i = 0; i < blocks.length; i++) { if (blocks[i].renameProcedure) { var procedureBlock = /** @type {!Blockly.Procedures.ProcedureBlock} */ ( blocks[i]); procedureBlock.renameProcedure( /** @type {string} */ (oldName), legalName); } } } return legalName; }; /** * Construct the blocks required by the flyout for the procedure category. * @param {!Blockly.Workspace} workspace The workspace containing procedures. * @return {!Array.} Array of XML block elements. */ Blockly.Procedures.flyoutCategory = function(workspace) { var xmlList = []; if (Blockly.Blocks['procedures_defnoreturn']) { // // do something // var block = Blockly.utils.xml.createElement('block'); block.setAttribute('type', 'procedures_defnoreturn'); block.setAttribute('gap', 16); var nameField = Blockly.utils.xml.createElement('field'); nameField.setAttribute('name', 'NAME'); nameField.appendChild(Blockly.utils.xml.createTextNode( Blockly.Msg['PROCEDURES_DEFNORETURN_PROCEDURE'])); block.appendChild(nameField); xmlList.push(block); } if (Blockly.Blocks['procedures_defreturn']) { // // do something // var block = Blockly.utils.xml.createElement('block'); block.setAttribute('type', 'procedures_defreturn'); block.setAttribute('gap', 16); var nameField = Blockly.utils.xml.createElement('field'); nameField.setAttribute('name', 'NAME'); nameField.appendChild(Blockly.utils.xml.createTextNode( Blockly.Msg['PROCEDURES_DEFRETURN_PROCEDURE'])); block.appendChild(nameField); xmlList.push(block); } if (Blockly.Blocks['procedures_ifreturn']) { // var block = Blockly.utils.xml.createElement('block'); block.setAttribute('type', 'procedures_ifreturn'); block.setAttribute('gap', 16); xmlList.push(block); } if (xmlList.length) { // Add slightly larger gap between system blocks and user calls. xmlList[xmlList.length - 1].setAttribute('gap', 24); } function populateProcedures(procedureList, templateName) { for (var i = 0; i < procedureList.length; i++) { var name = procedureList[i][0]; var args = procedureList[i][1]; // // // // // var block = Blockly.utils.xml.createElement('block'); block.setAttribute('type', templateName); block.setAttribute('gap', 16); var mutation = Blockly.utils.xml.createElement('mutation'); mutation.setAttribute('name', name); block.appendChild(mutation); for (var j = 0; j < args.length; j++) { var arg = Blockly.utils.xml.createElement('arg'); arg.setAttribute('name', args[j]); mutation.appendChild(arg); } xmlList.push(block); } } var tuple = Blockly.Procedures.allProcedures(workspace); populateProcedures(tuple[0], 'procedures_callnoreturn'); populateProcedures(tuple[1], 'procedures_callreturn'); return xmlList; }; /** * Updates the procedure mutator's flyout so that the arg block is not a * duplicate of another arg. * @param {!Blockly.Workspace} workspace The procedure mutator's workspace. This * workspace's flyout is what is being updated. * @private */ Blockly.Procedures.updateMutatorFlyout_ = function(workspace) { var usedNames = []; var blocks = workspace.getBlocksByType('procedures_mutatorarg', false); for (var i = 0, block; (block = blocks[i]); i++) { usedNames.push(block.getFieldValue('NAME')); } var xml = Blockly.utils.xml.createElement('xml'); var argBlock = Blockly.utils.xml.createElement('block'); argBlock.setAttribute('type', 'procedures_mutatorarg'); var nameField = Blockly.utils.xml.createElement('field'); nameField.setAttribute('name', 'NAME'); var argValue = Blockly.Variables.generateUniqueNameFromOptions( Blockly.Procedures.DEFAULT_ARG, usedNames); var fieldContent = Blockly.utils.xml.createTextNode(argValue); nameField.appendChild(fieldContent); argBlock.appendChild(nameField); xml.appendChild(argBlock); workspace.updateToolbox(xml); }; /** * Listens for when a procedure mutator is opened. Then it triggers a flyout * update and adds a mutator change listener to the mutator workspace. * @param {!Blockly.Events.Abstract} e The event that triggered this listener. * @package */ Blockly.Procedures.mutatorOpenListener = function(e) { if (!(e.type == Blockly.Events.BUBBLE_OPEN && e.bubbleType === 'mutator' && e.isOpen)) { return; } var workspaceId = /** @type {string} */ (e.workspaceId); var block = Blockly.Workspace.getById(workspaceId) .getBlockById(e.blockId); var type = block.type; if (type != 'procedures_defnoreturn' && type != 'procedures_defreturn') { return; } var workspace = block.mutator.getWorkspace(); Blockly.Procedures.updateMutatorFlyout_(workspace); workspace.addChangeListener(Blockly.Procedures.mutatorChangeListener_); }; /** * Listens for changes in a procedure mutator and triggers flyout updates when * necessary. * @param {!Blockly.Events.Abstract} e The event that triggered this listener. * @private */ Blockly.Procedures.mutatorChangeListener_ = function(e) { if (e.type != Blockly.Events.BLOCK_CREATE && e.type != Blockly.Events.BLOCK_DELETE && e.type != Blockly.Events.BLOCK_CHANGE) { return; } var workspaceId = /** @type {string} */ (e.workspaceId); var workspace = /** @type {!Blockly.WorkspaceSvg} */ (Blockly.Workspace.getById(workspaceId)); Blockly.Procedures.updateMutatorFlyout_(workspace); }; /** * Find all the callers of a named procedure. * @param {string} name Name of procedure. * @param {!Blockly.Workspace} workspace The workspace to find callers in. * @return {!Array.} Array of caller blocks. */ Blockly.Procedures.getCallers = function(name, workspace) { var callers = []; var blocks = workspace.getAllBlocks(false); // Iterate through every block and check the name. for (var i = 0; i < blocks.length; i++) { if (blocks[i].getProcedureCall) { var procedureBlock = /** @type {!Blockly.Procedures.ProcedureBlock} */ ( blocks[i]); var procName = procedureBlock.getProcedureCall(); // Procedure name may be null if the block is only half-built. if (procName && Blockly.Names.equals(procName, name)) { callers.push(blocks[i]); } } } return callers; }; /** * When a procedure definition changes its parameters, find and edit all its * callers. * @param {!Blockly.Block} defBlock Procedure definition block. */ Blockly.Procedures.mutateCallers = function(defBlock) { var oldRecordUndo = Blockly.Events.recordUndo; var procedureBlock = /** @type {!Blockly.Procedures.ProcedureBlock} */ ( defBlock); var name = procedureBlock.getProcedureDef()[0]; var xmlElement = defBlock.mutationToDom(true); var callers = Blockly.Procedures.getCallers(name, defBlock.workspace); for (var i = 0, caller; (caller = callers[i]); i++) { var oldMutationDom = caller.mutationToDom(); var oldMutation = oldMutationDom && Blockly.Xml.domToText(oldMutationDom); caller.domToMutation(xmlElement); var newMutationDom = caller.mutationToDom(); var newMutation = newMutationDom && Blockly.Xml.domToText(newMutationDom); if (oldMutation != newMutation) { // Fire a mutation on every caller block. But don't record this as an // undo action since it is deterministically tied to the procedure's // definition mutation. Blockly.Events.recordUndo = false; Blockly.Events.fire(new Blockly.Events.BlockChange( caller, 'mutation', null, oldMutation, newMutation)); Blockly.Events.recordUndo = oldRecordUndo; } } }; /** * Find the definition block for the named procedure. * @param {string} name Name of procedure. * @param {!Blockly.Workspace} workspace The workspace to search. * @return {Blockly.Block} The procedure definition block, or null not found. */ Blockly.Procedures.getDefinition = function(name, 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].getProcedureDef) { var procedureBlock = /** @type {!Blockly.Procedures.ProcedureBlock} */ ( blocks[i]); var tuple = procedureBlock.getProcedureDef(); if (tuple && Blockly.Names.equals(tuple[0], name)) { return blocks[i]; } } } return null; };