diff --git a/src/blocks/mrc_call_python_function.ts b/src/blocks/mrc_call_python_function.ts index 273664c0..185c4f9a 100644 --- a/src/blocks/mrc_call_python_function.ts +++ b/src/blocks/mrc_call_python_function.ts @@ -28,6 +28,7 @@ import { createFieldNonEditableText } from '../fields/FieldNonEditableText'; import { getAllowedTypesForSetCheck, getOutputCheck } from './utils/python'; import { ExtendedPythonGenerator } from '../editor/extended_python_generator'; import { MRC_STYLE_FUNCTIONS } from '../themes/styles' +import { ClassMethodDefExtraState } from './mrc_class_method_def' // A block to call a python function. @@ -57,7 +58,8 @@ interface CallPythonFunctionMixin extends CallPythonFunctionMixinType { mrcImportModule: string, mrcActualFunctionName: string, mrcExportedFunction: boolean, - maybeRenameProcedure(this: CallPythonFunctionBlock, oldName: string, legalName: string): void; + renameMethod(this: CallPythonFunctionBlock, oldName: string, newName: string): void; + mutateMethod(this: CallPythonFunctionBlock, defBlockExtraState: ClassMethodDefExtraState): void; } type CallPythonFunctionMixinType = typeof CALL_PYTHON_FUNCTION; @@ -219,59 +221,95 @@ const CALL_PYTHON_FUNCTION = { this.setNextStatement(true, null); this.setOutput(false); } - // Add the dummy input. - switch (this.mrcFunctionKind) { - case FunctionKind.MODULE: - this.appendDummyInput() - .appendField('call') - .appendField(createFieldNonEditableText(''), pythonUtils.FIELD_MODULE_OR_CLASS_NAME) - .appendField('.') - .appendField(createFieldNonEditableText(''), pythonUtils.FIELD_FUNCTION_NAME); - break; - case FunctionKind.STATIC: - this.appendDummyInput() - .appendField('call') - .appendField(createFieldNonEditableText(''), pythonUtils.FIELD_MODULE_OR_CLASS_NAME) - .appendField('.') - .appendField(createFieldNonEditableText(''), pythonUtils.FIELD_FUNCTION_NAME); - break; - case FunctionKind.CONSTRUCTOR: - this.appendDummyInput() - .appendField('create') - .appendField(createFieldNonEditableText(''), pythonUtils.FIELD_MODULE_OR_CLASS_NAME); - break; - case FunctionKind.INSTANCE: - this.appendDummyInput() - .appendField('call') - .appendField(createFieldNonEditableText(''), pythonUtils.FIELD_MODULE_OR_CLASS_NAME) - .appendField('.') - .appendField(createFieldNonEditableText(''), pythonUtils.FIELD_FUNCTION_NAME); - break; - case FunctionKind.INSTANCE_WITHIN: - this.appendDummyInput() - .appendField('call') - .appendField(createFieldNonEditableText(''), pythonUtils.FIELD_FUNCTION_NAME); - break; - default: - throw new Error('mrcFunctionKind has unexpected value: ' + mrcFunctionKind) + + if (!this.getInput('TITLE')) { + // Add the dummy input. + switch (this.mrcFunctionKind) { + case FunctionKind.MODULE: + this.appendDummyInput('TITLE') + .appendField('call') + .appendField(createFieldNonEditableText(''), pythonUtils.FIELD_MODULE_OR_CLASS_NAME) + .appendField('.') + .appendField(createFieldNonEditableText(''), pythonUtils.FIELD_FUNCTION_NAME); + break; + case FunctionKind.STATIC: + this.appendDummyInput('TITLE') + .appendField('call') + .appendField(createFieldNonEditableText(''), pythonUtils.FIELD_MODULE_OR_CLASS_NAME) + .appendField('.') + .appendField(createFieldNonEditableText(''), pythonUtils.FIELD_FUNCTION_NAME); + break; + case FunctionKind.CONSTRUCTOR: + this.appendDummyInput('TITLE') + .appendField('create') + .appendField(createFieldNonEditableText(''), pythonUtils.FIELD_MODULE_OR_CLASS_NAME); + break; + case FunctionKind.INSTANCE: + this.appendDummyInput('TITLE') + .appendField('call') + .appendField(createFieldNonEditableText(''), pythonUtils.FIELD_MODULE_OR_CLASS_NAME) + .appendField('.') + .appendField(createFieldNonEditableText(''), pythonUtils.FIELD_FUNCTION_NAME); + break; + case FunctionKind.INSTANCE_WITHIN: { + const input = this.getInput('TITLE'); + if (!input) { + this.appendDummyInput('TITLE') + .appendField('call') + .appendField(createFieldNonEditableText(''), pythonUtils.FIELD_FUNCTION_NAME); + } + break; + } + default: + throw new Error('mrcFunctionKind has unexpected value: ' + mrcFunctionKind) + } } - // Add input sockets for the arguments. + + // Update input sockets for the arguments. for (let i = 0; i < this.mrcArgs.length; i++) { - const input = this.appendValueInput('ARG' + i) - .setAlign(Blockly.inputs.Align.RIGHT) - .appendField(this.mrcArgs[i].name); + const argName = this.mrcArgs[i].name; + let argInput = this.getInput('ARG' + i); + const argField = this.getField('ARGNAME' + i); + if (argInput && argField) { + // Ensure argument name is up to date. No need to fire a change event. + Blockly.Events.disable(); + try { + argField.setValue(argName); + } finally { + Blockly.Events.enable(); + } + } else { + // Add new input. + argInput = this.appendValueInput('ARG' + i) + .setAlign(Blockly.inputs.Align.RIGHT) + .appendField(argName, 'ARGNAME' + i); + } if (this.mrcArgs[i].type) { - input.setCheck(getAllowedTypesForSetCheck(this.mrcArgs[i].type)); + argInput.setCheck(getAllowedTypesForSetCheck(this.mrcArgs[i].type)); } } - }, - maybeRenameProcedure: function(this: CallPythonFunctionBlock, oldName: string, newName: string): void { - if (this.mrcFunctionKind === FunctionKind.INSTANCE_WITHIN) { - if (this.getFieldValue(pythonUtils.FIELD_FUNCTION_NAME) == oldName) { - this.setFieldValue(newName, pythonUtils.FIELD_FUNCTION_NAME); - } + // Remove deleted inputs. + for (let i = this.mrcArgs.length; this.getInput('ARG' + i); i++) { + this.removeInput('ARG' + i); } - } + }, + renameMethod: function(this: CallPythonFunctionBlock, newName: string): void { + this.setFieldValue(newName, pythonUtils.FIELD_FUNCTION_NAME); + }, + mutateMethod: function( + this: CallPythonFunctionBlock, + defBlockExtraState: ClassMethodDefExtraState + ): void { + this.mrcReturnType = defBlockExtraState.returnType; + this.mrcArgs = []; + defBlockExtraState.params.forEach((param) => { + this.mrcArgs.push({ + 'name': param.name, + 'type': param.type, + }); + }); + this.updateBlock_(); + }, }; export const setup = function() { @@ -356,3 +394,50 @@ function generateCodeForArguments( } return code; } + +function getMethodCallers(workspace: Blockly.Workspace, name: string): Blockly.Block[] { + return workspace.getBlocksByType('mrc_call_python_function').filter((block) => { + const callBlock = block as CallPythonFunctionBlock; + return ( + callBlock.mrcFunctionKind === FunctionKind.INSTANCE_WITHIN && + callBlock.getFieldValue(pythonUtils.FIELD_FUNCTION_NAME) === name + ); + }); +} + +export function renameMethodCallers(workspace: Blockly.Workspace, oldName: string, newName: string): void { + for (const block of getMethodCallers(workspace, oldName)) { + (block as CallPythonFunctionBlock).renameMethod(newName); + } +} + +export function mutateMethodCallers( + workspace: Blockly.Workspace, methodName: string, defBlockExtraState: ClassMethodDefExtraState) { + const oldRecordUndo = Blockly.Events.getRecordUndo(); + + for (const block of getMethodCallers(workspace, methodName)) { + const callBlock = block as CallPythonFunctionBlock; + // Get the extra state before changing the call block. + const oldExtraState = callBlock.saveExtraState(); + + // Apply the changes. + callBlock.mutateMethod(defBlockExtraState); + + // Get the extra state after changing the call block. + const newExtraState = callBlock.saveExtraState(); + if (oldExtraState !== newExtraState) { + // Fire a change event, but don't record it as an undoable action. + Blockly.Events.setRecordUndo(false); + Blockly.Events.fire( + new (Blockly.Events.get(Blockly.Events.BLOCK_CHANGE))( + callBlock, + 'mutation', + null, + oldExtraState, + newExtraState, + ), + ); + Blockly.Events.setRecordUndo(oldRecordUndo); + } + } +} diff --git a/src/blocks/mrc_class_method_def.ts b/src/blocks/mrc_class_method_def.ts index 8f5a4d42..22c71943 100644 --- a/src/blocks/mrc_class_method_def.ts +++ b/src/blocks/mrc_class_method_def.ts @@ -26,7 +26,7 @@ import * as ChangeFramework from './utils/change_framework' import { getLegalName } from './utils/python'; import { Order } from 'blockly/python'; import { ExtendedPythonGenerator } from '../editor/extended_python_generator'; -import { CallPythonFunctionBlock } from './mrc_call_python_function' +import { renameMethodCallers, mutateMethodCallers } from './mrc_call_python_function' export const BLOCK_NAME = 'mrc_class_method_def'; @@ -146,6 +146,7 @@ const CLASS_METHOD_DEF = { }); }); this.updateBlock_(); + mutateMethodCallers(this.workspace, this.getFieldValue('NAME'), this.saveExtraState()); }, /** * Update the block to reflect the newly loaded extra state. @@ -186,7 +187,7 @@ const CLASS_METHOD_DEF = { paramBlock.nextConnection && paramBlock.nextConnection.targetBlock(); } this.mrcUpdateParams(); - //Blockly.Procedures.mutateCallers(this); + mutateMethodCallers(this.workspace, this.getFieldValue('NAME'), this.saveExtraState()); }, decompose: function (this: ClassMethodDefBlock, workspace: Blockly.Workspace) { // This is a special sub-block that only gets created in the mutator UI. @@ -238,9 +239,7 @@ const CLASS_METHOD_DEF = { const oldName = nameField.getValue(); if (oldName !== name && oldName !== legalName) { // Rename any callers. - for (const block of this.workspace.getBlocksByType('mrc_call_python_function')) { - (block as CallPythonFunctionBlock).maybeRenameProcedure(oldName, legalName); - } + renameMethodCallers(this.workspace, oldName, legalName); } return legalName; },