UNPKG

@xiamh/block-shareable-procedures-custom

Version:

A plugin that adds procedure blocks which are backed by explicit data models.

1,219 lines (1,117 loc) 39.1 kB
/** * @license * Copyright 2023 Google LLC * SPDX-License-Identifier: Apache-2.0 */ 'use strict'; import * as Blockly from 'blockly/core'; import {ObservableProcedureModel} from './observable_procedure_model'; import {ObservableParameterModel} from './observable_parameter_model'; import {IProcedureBlock, isProcedureBlock} from './i_procedure_block'; import {ProcedureCreate} from './events_procedure_create'; /* eslint-disable @typescript-eslint/naming-convention */ /** * A dictionary of the block definitions provided by this module. * @type {!Object<string, Object>} */ export const blocks = Blockly.common.createBlockDefinitionsFromJsonArray([ { 'type': 'procedures_defnoreturn', 'message0': '%{BKY_PROCEDURES_DEFNORETURN_TITLE} %1 %2 %3', 'message1': '%{BKY_PROCEDURES_DEFNORETURN_DO} %1', 'args0': [ { 'type': 'field_input', 'name': 'NAME', 'text': '', 'spellcheck': false, }, { 'type': 'field_label', 'name': 'PARAMS', 'text': '', }, { 'type': 'input_dummy', 'name': 'TOP', }, ], 'args1': [ { 'type': 'input_statement', 'name': 'STACK', }, ], 'style': 'procedure_blocks', 'helpUrl': '%{BKY_PROCEDURES_DEFNORETURN_HELPURL}', 'tooltip': '%{BKY_PROCEDURES_DEFNORETURN_TOOLTIP}', 'extensions': [ 'procedure_def_get_def_mixin', 'procedure_def_var_mixin', 'procedure_def_update_shape_mixin', 'procedure_def_context_menu_mixin', 'procedure_def_onchange_mixin', 'procedure_def_validator_helper', 'procedure_defnoreturn_get_caller_block_mixin', 'procedure_defnoreturn_set_comment_helper', 'procedure_def_set_no_return_helper', ], 'mutator': 'procedure_def_mutator', }, { 'type': 'procedures_callnoreturn', 'message0': '%1 %2', 'args0': [ {'type': 'field_label', 'name': 'NAME', 'text': '%{BKY_UNNAMED_KEY}'}, { 'type': 'input_dummy', 'name': 'TOPROW', }, ], 'nextStatement': null, 'previousStatement': null, 'style': 'procedure_blocks', 'helpUrl': '%{BKY_PROCEDURES_CALLNORETURN_HELPURL}', 'extensions': [ 'procedure_caller_get_def_mixin', 'procedure_caller_var_mixin', 'procedure_caller_update_shape_mixin', 'procedure_caller_context_menu_mixin', 'procedure_caller_onchange_mixin', 'procedure_callernoreturn_get_def_block_mixin', ], 'mutator': 'procedure_caller_mutator', }, { 'type': 'procedures_defreturn', 'message0': '%{BKY_PROCEDURES_DEFRETURN_TITLE} %1 %2 %3', 'message1': '%{BKY_PROCEDURES_DEFRETURN_DO} %1', 'message2': '%{BKY_PROCEDURES_DEFRETURN_RETURN} %1', 'args0': [ { 'type': 'field_input', 'name': 'NAME', 'text': '', 'spellcheck': false, }, { 'type': 'field_label', 'name': 'PARAMS', 'text': '', }, { 'type': 'input_dummy', 'name': 'TOP', }, ], 'args1': [ { 'type': 'input_statement', 'name': 'STACK', }, ], 'args2': [ { 'type': 'input_value', 'align': 'right', 'name': 'RETURN', }, ], 'style': 'procedure_blocks', 'helpUrl': '%{BKY_PROCEDURES_DEFRETURN_HELPURL}', 'tooltip': '%{BKY_PROCEDURES_DEFRETURN_TOOLTIP}', 'extensions': [ 'procedure_def_get_def_mixin', 'procedure_def_var_mixin', 'procedure_def_update_shape_mixin', 'procedure_def_context_menu_mixin', 'procedure_def_onchange_mixin', 'procedure_def_validator_helper', 'procedure_defreturn_get_caller_block_mixin', 'procedure_defreturn_set_comment_helper', 'procedure_def_set_return_helper', ], 'mutator': 'procedure_def_mutator', }, { 'type': 'procedures_callreturn', 'message0': '%1 %2', 'args0': [ {'type': 'field_label', 'name': 'NAME', 'text': '%{BKY_UNNAMED_KEY}'}, { 'type': 'input_dummy', 'name': 'TOPROW', }, ], 'output': null, 'style': 'procedure_blocks', 'helpUrl': '%{BKY_PROCEDURES_CALLRETURN_HELPURL}', 'extensions': [ 'procedure_caller_get_def_mixin', 'procedure_caller_var_mixin', 'procedure_caller_update_shape_mixin', 'procedure_caller_context_menu_mixin', 'procedure_caller_onchange_mixin', 'procedure_callerreturn_get_def_block_mixin', ], 'mutator': 'procedure_caller_mutator', }, ]); const procedureDefGetDefMixin = function() { const mixin = { model_: null, /** * Returns the data model for this procedure block. * @returns The data model for this procedure * block. */ getProcedureModel() { return this.model_; }, /** * True if this is a procedure definition block, false otherwise (i.e. * it is a caller). * @returns True because this is a procedure definition block. */ isProcedureDef() { return true; }, /** * Return all variables referenced by this block. * @returns List of variable names. * @this {Blockly.Block} */ getVars: function() { return this.getProcedureModel().getParameters().map( (p) => p.getVariableModel().name); }, /** * Return all variables referenced by this block. * @returns List of variable models. * @this {Blockly.Block} */ getVarModels: function() { return this.getProcedureModel().getParameters().map( (p) => p.getVariableModel()); }, /** * Disposes of the data model for this procedure block when the block is * disposed. */ destroy: function() { if (this.isInsertionMarker()) return; this.workspace.getProcedureMap().delete(this.getProcedureModel().getId()); }, }; mixin.model_ = new ObservableProcedureModel( this.workspace, Blockly.Procedures.findLegalName(this.getFieldValue('NAME'), this)); // Events cannot be fired from instantiation when deserializing or dragging // from the flyout. So make this consistent and never fire from instantiation. Blockly.Events.disable(); this.workspace.getProcedureMap().add(mixin.getProcedureModel()); Blockly.Events.enable(); this.mixin(mixin, true); }; // Using register instead of registerMixin to avoid triggering warnings about // overriding built-ins. Blockly.Extensions.register( 'procedure_def_get_def_mixin', procedureDefGetDefMixin); const procedureDefVarMixin = function() { const mixin = { /** * Notification that a variable is renaming. * If the ID matches one of this block's variables, rename it. * @param oldId ID of variable to rename. * @param newId ID of new variable. May be the same as oldId, but * with an updated name. Guaranteed to be the same type as the old * variable. * @override * @this {Blockly.Block} */ renameVarById: function(oldId, newId) { const oldVar = this.workspace.getVariableById(oldId); const model = this.getProcedureModel(); const index = model.getParameters().findIndex( (p) => p.getVariableModel() === oldVar); if (index === -1) return; // Not found. const newVar = this.workspace.getVariableById(newId); const oldParam = model.getParameter(index); oldParam.setName(newVar.name); }, /** * Notification that a variable is renaming but keeping the same ID. If the * variable is in use on this block, rerender to show the new name. * @param variable The variable being renamed. * @package * @override * @this {Blockly.Block} */ updateVarName: function(variable) { const containsVar = this.getProcedureModel().getParameters().some( (p) => p.getVariableModel() === variable); if (containsVar) { this.doProcedureUpdate(); // Rerender. } }, }; this.mixin(mixin, true); }; // Using register instead of registerMixin to avoid triggering warnings about // overriding built-ins. Blockly.Extensions.register('procedure_def_var_mixin', procedureDefVarMixin); const procedureDefUpdateShapeMixin = { /** * Updates the block to reflect the state of the procedure model. */ doProcedureUpdate: function() { this.setFieldValue(this.getProcedureModel().getName(), 'NAME'); this.setEnabled(this.getProcedureModel().getEnabled()); this.updateParameters_(); this.updateMutator_(); }, /** * Updates the parameters field to reflect the parameters in the procedure * model. */ updateParameters_: function() { const params = this.getProcedureModel().getParameters().map((p) => p.getName()); const paramString = params.length ? `${Blockly.Msg['PROCEDURES_BEFORE_PARAMS']} ${params.join(', ')}` : ''; // The field is deterministic based on other events, no need to fire. Blockly.Events.disable(); try { this.setFieldValue(paramString, 'PARAMS'); } finally { Blockly.Events.enable(); } }, /** * Updates the parameter blocks in the mutator (if it is open) to reflect * the state of the procedure model. */ updateMutator_: function() { if (!this.mutator?.isVisible()) return; const mutatorWorkspace = this.mutator.getWorkspace(); for (const p of this.getProcedureModel().getParameters()) { const block = mutatorWorkspace.getBlockById(p.getId()); if (!block) continue; // Should not happen. if (block.getFieldValue('NAME') !== p.getName()) { block.setFieldValue(p.getName(), 'NAME'); } } }, /** * Add or remove the statement block from this function definition. * @param 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'); } // Restore the stack, if one was saved. Blockly.Mutator.reconnect(this.statementConnection_, this, 'STACK'); this.statementConnection_ = null; } else { // Save the stack, then disconnect it. const stackConnection = this.getInput('STACK').connection; this.statementConnection_ = stackConnection.targetConnection; if (this.statementConnection_) { const stackBlock = stackConnection.targetBlock(); stackBlock.unplug(); stackBlock.bumpNeighbours(); } this.removeInput('STACK', true); } this.hasStatements_ = hasStatements; }, }; Blockly.Extensions.registerMixin( 'procedure_def_update_shape_mixin', procedureDefUpdateShapeMixin); const procedureDefValidatorHelper = function() { const nameField = this.getField('NAME'); nameField.setValue(Blockly.Procedures.findLegalName('', this)); nameField.setValidator(Blockly.Procedures.rename); }; Blockly.Extensions.register( 'procedure_def_validator_helper', procedureDefValidatorHelper); const procedureDefMutator = { hasStatements_: true, /** * Create XML to represent the argument inputs. * Backwards compatible serialization implementation. * @returns XML storage element. * @this {Blockly.Block} */ mutationToDom: function() { const container = Blockly.utils.xml.createElement('mutation'); const params = this.getProcedureModel().getParameters(); for (let i = 0; i < params.length; i++) { const parameter = Blockly.utils.xml.createElement('arg'); const varModel = params[i].getVariableModel(); parameter.setAttribute('name', varModel.name); parameter.setAttribute('varid', varModel.getId()); 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. * Backwards compatible serialization implementation. * @param xmlElement XML storage element. * @this {Blockly.Block} */ domToMutation: function(xmlElement) { for (let i = 0; i < xmlElement.childNodes.length; i++) { const node = xmlElement.childNodes[i]; if (node.nodeName.toLowerCase() !== 'arg') continue; const varId = node.getAttribute('varid'); this.getProcedureModel().insertParameter( new ObservableParameterModel( this.workspace, node.getAttribute('name'), undefined, varId), i); } this.setStatements_(xmlElement.getAttribute('statements') !== 'false'); }, /** * Returns the state of this block as a JSON serializable object. * @returns The state of this block, eg the parameters and statements. */ saveExtraState: function() { const state = Object.create(null); state['procedureId'] = this.getProcedureModel().getId(); const params = this.getProcedureModel().getParameters(); if (!params.length && this.hasStatements_) return state; if (params.length) { state['params'] = params.map((p) => { return { 'name': p.getName(), 'id': p.getVariableModel().getId(), // Ideally this would be id, and the other would be varId, // but backwards compatibility :/ 'paramId': p.getId(), }; }); } if (!this.hasStatements_) { state['hasStatements'] = false; } return state; }, /** * Applies the given state to this block. * @param state The state to apply to this block, eg the parameters and * statements. */ loadExtraState: function(state) { const map = this.workspace.getProcedureMap(); const procedureId = state['procedureId']; if (procedureId && procedureId != this.model_.getId() && map.has(procedureId) && (this.isInsertionMarker() || this.noBlockHasClaimedModel_(procedureId))) { if (map.has(this.model_.getId())) { map.delete(this.model_.getId()); } this.model_ = map.get(procedureId); } if (state['params'] && !this.getProcedureModel().getParameters().length) { for (let i = 0; i < state['params'].length; i++) { const {name, id, paramId} = state['params'][i]; this.getProcedureModel().insertParameter( new ObservableParameterModel(this.workspace, name, paramId, id), i); } } this.doProcedureUpdate(); this.setStatements_(state['hasStatements'] === false ? false : true); }, /** * Returns true if there is no definition block currently associated with the * given procedure ID. False otherwise. * @param procedureId The ID of the procedure to check for a claiming * block. * @returns True if there is no definition block currently associated * with the given procedure ID. False otherwise. */ noBlockHasClaimedModel_(procedureId) { const model = this.workspace.getProcedureMap().get(procedureId); return this.workspace.getAllBlocks(false).every( (b) => !isProcedureBlock(b) || !b.isProcedureDef() || b.getProcedureModel() !== model); }, /** * Populate the mutator's dialog with this block's components. * @param workspace Blockly.Mutator's workspace. * @returns Root block in mutator. * @this {Blockly.Block} */ decompose: function(workspace) { const containerBlockDef = { 'type': 'procedures_mutatorcontainer', 'inputs': { 'STACK': {}, }, }; let connDef = containerBlockDef['inputs']['STACK']; for (const param of this.getProcedureModel().getParameters()) { connDef['block'] = { 'type': 'procedures_mutatorarg', 'id': param.getId(), 'fields': { 'NAME': param.getName(), }, 'next': {}, }; connDef = connDef['block']['next']; } const containerBlock = Blockly.serialization.blocks.append( containerBlockDef as unknown as Blockly.serialization.blocks.State, workspace, {recordUndo: false}); if (this.type === 'procedures_defreturn') { containerBlock.setFieldValue(this.hasStatements_, 'STATEMENTS'); } else { containerBlock.removeInput('STATEMENT_INPUT'); } return containerBlock; }, /** * Reconfigure this block based on the mutator dialog's components. * @param containerBlock Root block in mutator. * @this {Blockly.Block} */ compose: function(containerBlock) { // Note that only one of these four things can actually occur for any given // composition, because the user can only drag blocks around so quickly. // So we can use that when making assumptions inside the definitions of // these sub procedures. this.deleteParamsFromModel_(containerBlock); this.renameParamsInModel_(containerBlock); this.addParamsToModel_(containerBlock); const hasStatements = containerBlock.getFieldValue('STATEMENTS'); if (hasStatements !== null) { this.setStatements_(hasStatements === 'TRUE'); } }, /** * Deletes any parameters from the procedure model that do not have associated * parameter blocks in the mutator. * @param containerBlock Root block in the mutator. */ deleteParamsFromModel_: function(containerBlock) { const ids = new Set(containerBlock.getDescendants().map((b) => b.id)); const model = this.getProcedureModel(); const count = model.getParameters().length; for (let i = count - 1; i >= 0; i--) { if (!ids.has(model.getParameter(i).getId())) { model.deleteParameter(i); } } }, /** * Renames any parameters in the procedure model whose associated parameter * blocks have been renamed. * @param containerBlock Root block in the mutator. */ renameParamsInModel_: function(containerBlock) { const model = this.getProcedureModel(); let i = 0; let paramBlock = containerBlock.getInputTargetBlock('STACK'); while (paramBlock && !paramBlock.isInsertionMarker()) { const param = model.getParameter(i); if (param && param.getId() === paramBlock.id && param.getName() !== paramBlock.getFieldValue('NAME')) { param.setName(paramBlock.getFieldValue('NAME')); } paramBlock = paramBlock.nextConnection && paramBlock.nextConnection.targetBlock(); i++; } }, /** * Adds new parameters to the procedure model for any new procedure parameter * blocks. * @param containerBlock Root block in the mutator. */ addParamsToModel_: function(containerBlock) { const model = this.getProcedureModel(); let i = 0; let paramBlock = containerBlock.getInputTargetBlock('STACK'); while (paramBlock && !paramBlock.isInsertionMarker()) { if (!model.getParameter(i) || model.getParameter(i).getId() !== paramBlock.id) { model.insertParameter( new ObservableParameterModel( this.workspace, paramBlock.getFieldValue('NAME'), paramBlock.id), i); } paramBlock = paramBlock.nextConnection && paramBlock.nextConnection.targetBlock(); i++; } }, }; Blockly.Extensions.registerMutator( 'procedure_def_mutator', procedureDefMutator, undefined, ['procedures_mutatorarg']); const procedureDefContextMenuMixin = { /** * Add custom menu options to this block's context menu. * @param options List of menu options to add to. * @this {Blockly.Block} */ customContextMenu: function( options: Array<Blockly.ContextMenuRegistry.ContextMenuOption| Blockly.ContextMenuRegistry.LegacyContextMenuOption>) { if (this.isInFlyout) { return; } const xmlMutation = Blockly.utils.xml.createElement('mutation'); xmlMutation.setAttribute('name', this.getFieldValue('NAME')); const params = this.getProcedureModel().getParameters(); for (const param of params) { const xmlArg = Blockly.utils.xml.createElement('arg'); xmlArg.setAttribute('name', param.getName()); xmlMutation.appendChild(xmlArg); } const xmlBlock = Blockly.utils.xml.createElement('block'); xmlBlock.setAttribute('type', this.callType_); xmlBlock.appendChild(xmlMutation); // Add option to create caller. options.push({ enabled: true, text: Blockly.Msg['PROCEDURES_CREATE_DO'] .replace('%1', this.getFieldValue('NAME')), callback: Blockly.ContextMenu.callbackFactory(this, xmlBlock) as () => void, }); // Add options to create getters for each parameter. if (this.isCollapsed()) return; for (const param of params) { const argVar = param.getVariableModel(); const argXmlField = Blockly.Variables.generateVariableFieldDom(argVar); const argXmlBlock = Blockly.utils.xml.createElement('block'); argXmlBlock.setAttribute('type', 'variables_get'); argXmlBlock.appendChild(argXmlField); options.push({ enabled: true, text: Blockly.Msg['VARIABLES_SET_CREATE_GET'].replace('%1', argVar.name), callback: Blockly.ContextMenu.callbackFactory(this, argXmlBlock) as () => void, }); } }, }; Blockly.Extensions.registerMixin( 'procedure_def_context_menu_mixin', procedureDefContextMenuMixin); const procedureDefOnChangeMixin = { onchange: function(e) { if (e.type === Blockly.Events.BLOCK_CREATE && e.blockId === this.id) { Blockly.Events.fire( new ProcedureCreate(this.workspace, this.getProcedureModel())); } if (e.type === Blockly.Events.BLOCK_CHANGE && e.blockId === this.id && e.element === 'disabled') { this.getProcedureModel().setEnabled(!e.newValue); } }, }; Blockly.Extensions.registerMixin( 'procedure_def_onchange_mixin', procedureDefOnChangeMixin); const procedureDefNoReturnSetCommentHelper = function() { if ((this.workspace.options.comments || (this.workspace.options.parentWorkspace && this.workspace.options.parentWorkspace.options.comments)) && Blockly.Msg['PROCEDURES_DEFNORETURN_COMMENT']) { this.setCommentText(Blockly.Msg['PROCEDURES_DEFNORETURN_COMMENT']); } }; Blockly.Extensions.register( 'procedure_defnoreturn_set_comment_helper', procedureDefNoReturnSetCommentHelper); const procedureDefReturnSetCommentHelper = function() { if ((this.workspace.options.comments || (this.workspace.options.parentWorkspace && this.workspace.options.parentWorkspace.options.comments)) && Blockly.Msg['PROCEDURES_DEFRETURN_COMMENT']) { this.setCommentText(Blockly.Msg['PROCEDURES_DEFRETURN_COMMENT']); } }; Blockly.Extensions.register( 'procedure_defreturn_set_comment_helper', procedureDefReturnSetCommentHelper); const procedureDefNoReturnGetCallerBlockMixin = { callType_: 'procedures_callnoreturn', }; Blockly.Extensions.registerMixin( 'procedure_defnoreturn_get_caller_block_mixin', procedureDefNoReturnGetCallerBlockMixin); const procedureDefReturnGetCallerBlockMixin = { callType_: 'procedures_callreturn', }; Blockly.Extensions.registerMixin( 'procedure_defreturn_get_caller_block_mixin', procedureDefReturnGetCallerBlockMixin); const procedureDefSetNoReturnHelper = function() { this.getProcedureModel().setReturnTypes(null); }; Blockly.Extensions.register( 'procedure_def_set_no_return_helper', procedureDefSetNoReturnHelper); const procedureDefSetReturnHelper = function() { this.getProcedureModel().setReturnTypes([]); }; Blockly.Extensions.register( 'procedure_def_set_return_helper', procedureDefSetReturnHelper); const procedureCallerGetDefMixin = function() { const mixin = { model_: null, prevParams_: [], argsMap_: new Map(), /** * Returns the procedure model associated with this block. * @returns The procedure model associated with this block. */ getProcedureModel() { return this.model_; }, /** * Returns the procedure model tha was found. * @param name The name of the procedure model to find. * @param params The param names of the procedure model * to find. * @returns The procedure model that was found. */ findProcedureModel_(name, params = []) { const workspace = this.getTargetWorkspace_(); const model = workspace.getProcedureMap().getProcedures().find( (proc) => proc.getName() === name); if (!model) return null; const returnTypes = model.getReturnTypes(); const hasMatchingReturn = this.hasReturn_ ? returnTypes : !returnTypes; if (!hasMatchingReturn) return null; const hasMatchingParams = model.getParameters().every((p, i) => p.getName() === params[i]); if (!hasMatchingParams) return null; return model; }, /** * Returns the main workspace (i.e. not the flyout workspace) associated * with this block. * @returns The main workspace (i.e. not the flyout workspace) associated * with this block. */ getTargetWorkspace_() { return this.workspace.isFlyout ? this.workspace.targetWorkspace : this.workspace; }, /** * True if this is a procedure definition block, false otherwise (i.e. * it is a caller). * @returns False because this is not a procedure definition block. */ isProcedureDef() { return false; }, /** * Return all variables referenced by this block. * @returns List of variable names. * @this {Blockly.Block} */ getVars: function() { return this.getProcedureModel().getParameters().map( (p) => p.getVariableModel().name); }, /** * Return all variables referenced by this block. * @returns List of variable models. * @this {Blockly.Block} */ getVarModels: function() { return this.getProcedureModel().getParameters().map( (p) => p.getVariableModel()); }, }; this.mixin(mixin, true); }; // Using register instead of registerMixin to avoid triggering warnings about // overriding built-ins. Blockly.Extensions.register( 'procedure_caller_get_def_mixin', procedureCallerGetDefMixin); const procedureCallerVarMixin = function() { const mixin = { /** * Notification that a variable is renaming but keeping the same ID. If the * variable is in use on this block, rerender to show the new name. * @param variable The variable being renamed. * @package * @override * @this {Blockly.Block} */ updateVarName: function(variable) { const containsVar = this.getProcedureModel().getParameters().some( (p) => p.getVariableModel() === variable); if (containsVar) { this.doProcedureUpdate(); // Rerender. } }, }; this.mixin(mixin, true); }; // Using register instead of registerMixin to avoid triggering warnings about // overriding built-ins. Blockly.Extensions.register( 'procedure_caller_var_mixin', procedureCallerVarMixin); const procedureCallerMutator = { previousEnabledState_: true, paramsFromSerializedState_: [], /** * Create XML to represent the (non-editable) name and arguments. * Backwards compatible serialization implementation. * @returns XML storage element. * @this {Blockly.Block} */ mutationToDom: function() { const container = Blockly.utils.xml.createElement('mutation'); const model = this.getProcedureModel(); if (!model) return container; container.setAttribute('name', model.getName()); for (const param of model.getParameters()) { const arg = Blockly.utils.xml.createElement('arg'); arg.setAttribute('name', param.getName()); container.appendChild(arg); } return container; }, /** * Parse XML to restore the (non-editable) name and parameters. * Backwards compatible serialization implementation. * @param xmlElement XML storage element. * @this {Blockly.Block} */ domToMutation: function(xmlElement) { const name = xmlElement.getAttribute('name'); const params = []; for (const n of xmlElement.childNodes) { if (n.nodeName.toLowerCase() === 'arg') { params.push(n.getAttribute('name')); } } this.deserialize_(name, params); }, /** * Returns the state of this block as a JSON serializable object. * @returns The state of * this block, ie the params and procedure name. */ saveExtraState: function() { const state = Object.create(null); const model = this.getProcedureModel(); if (!model) return state; state['name'] = model.getName(); if (model.getParameters().length) { state['params'] = model.getParameters().map((p) => p.getName()); } return state; }, /** * Applies the given state to this block. * @param state The state to apply to this block, ie the params and * procedure name. */ loadExtraState: function(state) { this.deserialize_(state['name'], state['params'] || []); }, /** * Applies the given name and params from the serialized state to the block. * @param name The name to apply to the block. * @param params The parameters to apply to the block. */ deserialize_: function(name, params) { this.setFieldValue(name, 'NAME'); if (!this.model_) this.model_ = this.findProcedureModel_(name, params); if (this.getProcedureModel()) { this.initBlockWithProcedureModel_(); } else { // Create inputs based on the mutation so that children can be connected. this.createArgInputs_(params); } this.paramsFromSerializedState_ = params; }, }; Blockly.Extensions.registerMutator( 'procedure_caller_mutator', procedureCallerMutator); const procedureCallerUpdateShapeMixin = { /** * Renders the block for the first time based on the procedure model. */ initBlockWithProcedureModel_() { this.prevParams_ = [...this.getProcedureModel().getParameters()]; this.doProcedureUpdate(); }, /** * Updates the shape of this block to reflect the state of the data model. */ doProcedureUpdate: function() { if (!this.getProcedureModel()) return; const id = this.getProcedureModel().getId(); if (!this.getTargetWorkspace_().getProcedureMap().has(id)) { this.dispose(); return; } this.updateName_(); this.updateEnabled_(); this.updateParameters_(); }, /** * Updates the name field of this block to match the state of the data model. */ updateName_: function() { const name = this.getProcedureModel().getName(); this.setFieldValue(name, 'NAME'); const baseMsg = this.outputConnection ? Blockly.Msg['PROCEDURES_CALLRETURN_TOOLTIP'] : Blockly.Msg['PROCEDURES_CALLNORETURN_TOOLTIP']; this.setTooltip(baseMsg.replace('%1', name)); }, /** * Updates the enabled state of this block to match the state of the data * model. */ updateEnabled_: function() { if (!this.getProcedureModel().getEnabled()) { this.previousEnabledState_ = this.isEnabled(); this.setEnabled(false); } else { this.setEnabled(this.previousEnabledState_); } }, /** * Updates the parameter fields/inputs of this block to match the state of the * data model. */ updateParameters_: function() { this.updateArgsMap_(); this.deleteAllArgInputs_(); this.addParametersLabel__(); this.createArgInputs_(); this.reattachBlocks_(); this.prevParams_ = [...this.getProcedureModel().getParameters()]; }, /** * Saves a map of parameter IDs to target blocks attached to the inputs * of this caller block. */ updateArgsMap_: function() { for (const [i, p] of this.prevParams_.entries()) { const target = this.getInputTargetBlock(`ARG${i}`); if (target) this.argsMap_.set(p.getId(), target); } }, /** * Deletes all the parameter inputs on this block. */ deleteAllArgInputs_: function() { let i = 0; while (this.getInput(`ARG${i}`)) { this.removeInput(`ARG${i}`); i++; } }, /** * Adds or removes the parameter label to match the state of the data model. */ addParametersLabel__: function() { const topRow = this.getInput('TOPROW'); if (this.getProcedureModel().getParameters().length) { if (!this.getField('WITH')) { topRow.appendField( Blockly.Msg['PROCEDURES_CALL_BEFORE_PARAMS'], 'WITH'); topRow.init(); } } else if (this.getField('WITH')) { topRow.removeField('WITH'); } }, /** * Creates all of the parameter inputs to match the state of the data model. * @param params The params to add to the block, or null to * use the params defined in the procedure model. */ createArgInputs_: function(params = null) { if (!params) { params = this.getProcedureModel().getParameters().map((p) => p.getName()); } for (const [i, p] of params.entries()) { this.appendValueInput(`ARG${i}`) .appendField(new Blockly.FieldLabel(p), `ARGNAME${i}`) .setAlign(Blockly.Input.Align.RIGHT); } }, /** * Reattaches blocks to this blocks' inputs based on the data saved in the * argsMap_. */ reattachBlocks_: function() { const params = this.getProcedureModel().getParameters(); for (const [i, p] of params.entries()) { if (!this.argsMap_.has(p.getId())) continue; this.getInput(`ARG${i}`).connection.connect( this.argsMap_.get(p.getId()).outputConnection); } }, /** * Notification that a procedure is renaming. * If the name matches this block's procedure, rename it. * @param oldName Previous name of procedure. * @param newName Renamed procedure. * @this {Blockly.Block} */ renameProcedure: function(oldName, newName) { if (Blockly.Names.equals(oldName, this.getFieldValue('NAME'))) { this.setFieldValue(newName, 'NAME'); const baseMsg = this.outputConnection ? Blockly.Msg['PROCEDURES_CALLRETURN_TOOLTIP'] : Blockly.Msg['PROCEDURES_CALLNORETURN_TOOLTIP']; this.setTooltip(baseMsg.replace('%1', newName)); } }, }; Blockly.Extensions.registerMixin( 'procedure_caller_update_shape_mixin', procedureCallerUpdateShapeMixin); const procedureCallerOnChangeMixin = { /** * Procedure calls cannot exist without the corresponding procedure * definition. Enforce this link whenever an event is fired. * @param event Change event. * @this {Blockly.Block} */ onchange: function(event) { if (this.disposed || this.workspace.isFlyout) return; // Blockly.Events not generated by user. Skip handling. if (!event.recordUndo) return; if (event.type !== Blockly.Events.BLOCK_CREATE) return; if (event.blockId !== this.id && event.ids.indexOf(this.id) === -1) return; // We already found our model, which means we don't need to create a block. if (this.getProcedureModel()) return; // 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. const name = this.getFieldValue('NAME'); let def = Blockly.Procedures.getDefinition(name, this.workspace); if (!this.defMatches_(def)) def = null; if (!def) { // We have no def nor procedure model. Blockly.Events.setGroup(event.group); this.model_ = this.createDef_( this.getFieldValue('NAME'), this.paramsFromSerializedState_); Blockly.Events.setGroup(false); } if (!this.getProcedureModel()) { // We have a def, but no reference to its model. this.model_ = this.findProcedureModel_( this.getFieldValue('NAME'), this.paramsFromSerializedState_); } this.initBlockWithProcedureModel_(); }, /** * Returns true if the given def block matches the definition of this caller * block. * @param defBlock The definition block to check against. * @returns Whether the def block matches or not. */ defMatches_(defBlock) { return defBlock && defBlock.type === this.defType_ && JSON.stringify(defBlock.getVars()) === JSON.stringify(this.paramsFromSerializedState_); }, /** * Creates a procedure definition block with the given name and params, * and returns the procedure model associated with it. * @param name The name of the procedure to create. * @param params The names of the parameters to create. * @returns The procedure model associated with the new * procedure definition block. */ createDef_(name, params = []) { const xy = this.getRelativeToSurfaceXY(); const newName = Blockly.Procedures.findLegalName(name, this); this.renameProcedure(name, newName); const blockDef = { 'type': this.defType_, 'x': xy.x + Blockly.config.snapRadius * (this.RTL ? -1 : 1), 'y': xy.y + Blockly.config.snapRadius * 2, 'extraState': { 'params': params.map((p) => ({'name': p})), }, 'fields': {'NAME': newName}, }; const block = Blockly.serialization.blocks .append(blockDef, this.getTargetWorkspace_(), {recordUndo: true}); return (block as unknown as IProcedureBlock).getProcedureModel(); }, }; Blockly.Extensions.registerMixin( 'procedure_caller_onchange_mixin', procedureCallerOnChangeMixin); const procedureCallerContextMenuMixin = { /** * Add menu option to find the definition block for this call. * @param options List of menu options to add to. * @this {Blockly.Block} */ customContextMenu: function(options) { if (!this.workspace.isMovable()) { // If we center on the block and the workspace isn't movable we could // lose blocks at the edges of the workspace. return; } const name = this.getFieldValue('NAME'); const workspace = this.workspace; const callback = function() { const def = Blockly.Procedures.getDefinition(name, workspace); if (def && def instanceof Blockly.BlockSvg) { workspace.centerOnBlock(def.id); def.select(); } }; options.push({ enabled: true, text: Blockly.Msg['PROCEDURES_HIGHLIGHT_DEF'], callback: callback, }); }, }; Blockly.Extensions.registerMixin( 'procedure_caller_context_menu_mixin', procedureCallerContextMenuMixin); const procedureCallerNoReturnGetDefBlockMixin = { hasReturn_: false, defType_: 'procedures_defnoreturn', }; Blockly.Extensions.registerMixin( 'procedure_callernoreturn_get_def_block_mixin', procedureCallerNoReturnGetDefBlockMixin); const procedureCallerReturnGetDefBlockMixin = { hasReturn_: true, defType_: 'procedures_defreturn', }; Blockly.Extensions.registerMixin( 'procedure_callerreturn_get_def_block_mixin', procedureCallerReturnGetDefBlockMixin);