diff --git a/src/blocks/mrc_class_method_def.ts b/src/blocks/mrc_class_method_def.ts index 66826404..65660135 100644 --- a/src/blocks/mrc_class_method_def.ts +++ b/src/blocks/mrc_class_method_def.ts @@ -40,314 +40,314 @@ export const BLOCK_NAME = 'mrc_class_method_def'; export const FIELD_METHOD_NAME = 'NAME'; export const RETURN_VALUE = 'RETURN'; -type Parameter = { - name: string, - type?: string, -}; +export interface Parameter { + name: string; + type?: string; +} export type ClassMethodDefBlock = Blockly.Block & ClassMethodDefMixin & Blockly.BlockSvg; interface ClassMethodDefMixin extends ClassMethodDefMixinType { - mrcMethodId: string, - mrcCanChangeSignature: boolean, - mrcCanBeCalledWithinClass: boolean, - mrcCanBeCalledOutsideClass: boolean, - mrcReturnType: string, - mrcParameters: Parameter[], - mrcPythonMethodName: string, - mrcFuncName: string | null, - mrcUpdateReturnInput(): void, + mrcMethodId: string, + mrcCanChangeSignature: boolean, + mrcCanBeCalledWithinClass: boolean, + mrcCanBeCalledOutsideClass: boolean, + mrcReturnType: string, + mrcParameters: Parameter[], + mrcPythonMethodName: string, + mrcFuncName: string | null, + mrcUpdateReturnInput(): void, } type ClassMethodDefMixinType = typeof CLASS_METHOD_DEF; /** Extra state for serialising call_python_* blocks. */ type ClassMethodDefExtraState = { - /** - * The id that identifies this method definition. - */ - methodId?: string, - /** - * Can change name and parameters and return type - */ - canChangeSignature: boolean, - /** - * Can be called from within the class. - */ - canBeCalledWithinClass: boolean, - /** - * Can be called from outside the class. - */ - canBeCalledOutsideClass: boolean, - /** - * The return type of the function. - * Use 'None' for no return value. - * Use '' for an untyped return value. - */ - returnType: string, - /** - * The arguments of the method. - */ - params: Parameter[], - - /** - * Specified if the python method name is different than the name given in - * the NAME field. - */ - pythonMethodName?: string, + /** + * The id that identifies this method definition. + */ + methodId?: string, + /** + * Can change name and parameters and return type + */ + canChangeSignature: boolean, + /** + * Can be called from within the class. + */ + canBeCalledWithinClass: boolean, + /** + * Can be called from outside the class. + */ + canBeCalledOutsideClass: boolean, + /** + * The return type of the function. + * Use 'None' for no return value. + * Use '' for an untyped return value. + */ + returnType: string, + /** + * The arguments of the method. + */ + params: Parameter[], + + /** + * Specified if the python method name is different than the name given in + * the NAME field. + */ + pythonMethodName?: string, }; const CLASS_METHOD_DEF = { - /** - * Block initialization. - */ - init: function (this: ClassMethodDefBlock): void { - this.appendDummyInput("TITLE") - .appendField('', FIELD_METHOD_NAME); - this.setOutput(false); - this.setStyle(MRC_STYLE_FUNCTIONS); - this.appendStatementInput('STACK').appendField(''); - this.mrcParameters = []; - this.setPreviousStatement(false); - this.setNextStatement(false); - this.updateBlock_(); - }, - /** - * Returns the state of this block as a JSON serializable object. - */ - saveExtraState: function ( - this: ClassMethodDefBlock): ClassMethodDefExtraState { - const extraState: ClassMethodDefExtraState = { - methodId: this.mrcMethodId, - canChangeSignature: this.mrcCanChangeSignature, - canBeCalledWithinClass: this.mrcCanBeCalledWithinClass, - canBeCalledOutsideClass: this.mrcCanBeCalledOutsideClass, - returnType: this.mrcReturnType, - params: [], - }; - this.mrcParameters.forEach((param) => { - extraState.params.push({ - 'name': param.name, - 'type': param.type, - }); - }); - if (this.mrcPythonMethodName) { - extraState.pythonMethodName = this.mrcPythonMethodName; + /** + * Block initialization. + */ + init: function (this: ClassMethodDefBlock): void { + this.appendDummyInput("TITLE") + .appendField('', FIELD_METHOD_NAME); + this.setOutput(false); + this.setStyle(MRC_STYLE_FUNCTIONS); + this.appendStatementInput('STACK').appendField(''); + this.mrcParameters = []; + this.setPreviousStatement(false); + this.setNextStatement(false); + this.updateBlock_(); + }, + /** + * Returns the state of this block as a JSON serializable object. + */ + saveExtraState: function ( + this: ClassMethodDefBlock): ClassMethodDefExtraState { + const extraState: ClassMethodDefExtraState = { + methodId: this.mrcMethodId, + canChangeSignature: this.mrcCanChangeSignature, + canBeCalledWithinClass: this.mrcCanBeCalledWithinClass, + canBeCalledOutsideClass: this.mrcCanBeCalledOutsideClass, + returnType: this.mrcReturnType, + params: [], + }; + this.mrcParameters.forEach((param) => { + extraState.params.push({ + name: param.name, + type: param.type, + }); + }); + if (this.mrcPythonMethodName) { + extraState.pythonMethodName = this.mrcPythonMethodName; + } + return extraState; + }, + /** + * Applies the given state to this block. + */ + loadExtraState: function (this: ClassMethodDefBlock, extraState: ClassMethodDefExtraState): void { + this.mrcMethodId = extraState.methodId ? extraState.methodId : this.id; + this.mrcCanChangeSignature = extraState.canChangeSignature; + this.mrcCanBeCalledWithinClass = extraState.canBeCalledWithinClass; + this.mrcCanBeCalledOutsideClass = extraState.canBeCalledOutsideClass; + this.mrcPythonMethodName = extraState.pythonMethodName ? extraState.pythonMethodName : ''; + this.mrcFuncName = null; // Set during python code generation. + this.mrcReturnType = extraState.returnType; + this.mrcParameters = []; + extraState.params.forEach((param) => { + this.mrcParameters.push({ + name: param.name, + type: param.type, + }); + }); + this.updateBlock_(); + }, + /** + * Update the block to reflect the newly loaded extra state. + */ + updateBlock_: function (this: ClassMethodDefBlock): void { + const name = this.getFieldValue(FIELD_METHOD_NAME); + const input = this.getInput('TITLE'); + if (!input) { + return; + } + input.removeField(FIELD_METHOD_NAME); + + if (this.mrcCanChangeSignature) { + const nameField = new Blockly.FieldTextInput(name); + input.insertFieldAt(0, nameField, FIELD_METHOD_NAME); + this.setMutator(new Blockly.icons.MutatorIcon([MUTATOR_BLOCK_NAME], this)); + nameField.setValidator(this.mrcNameFieldValidator.bind(this, nameField)); + } else { + input.insertFieldAt(0, createFieldNonEditableText(name), FIELD_METHOD_NAME); + // Case because a current bug in blockly where it won't allow passing null to Blockly.Block.setMutator makes it necessary. + (this as Blockly.BlockSvg).setMutator(null); + } + this.mrcUpdateParams(); + this.mrcUpdateReturnInput(); + }, + compose: function (this: ClassMethodDefBlock, containerBlock: any) { + // Parameter list. + this.mrcParameters = []; + + let paramBlock = containerBlock.getInputTargetBlock('STACK'); + while (paramBlock) { + const param: Parameter = { + name: paramBlock.getFieldValue('NAME'), + type: '' + }; + if (paramBlock.originalName) { + // This is a mutator arg block, so we can get the original name. + this.mrcRenameParameter(paramBlock.originalName, param.name); + paramBlock.originalName = param.name; + } + this.mrcParameters.push(param); + paramBlock = paramBlock.nextConnection && paramBlock.nextConnection.targetBlock(); + } + this.mrcUpdateParams(); + if (this.mrcCanBeCalledWithinClass) { + const methodForWithin = this.getMethodForWithin(); + if (methodForWithin) { + mutateMethodCallers(this.workspace, this.mrcMethodId, methodForWithin); + } + } + }, + decompose: function (this: ClassMethodDefBlock, workspace: Blockly.Workspace) { + // This is a special sub-block that only gets created in the mutator UI. + // It acts as our "top block" + const topBlock = workspace.newBlock(PARAM_CONTAINER_BLOCK_NAME); + (topBlock as Blockly.BlockSvg).initSvg(); + + // Then we add one sub-block for each item in the list. + let connection = topBlock!.getInput('STACK')!.connection; + + for (let i = 0; i < this.mrcParameters.length; i++) { + const itemBlock = workspace.newBlock(MUTATOR_BLOCK_NAME); + (itemBlock as Blockly.BlockSvg).initSvg(); + itemBlock.setFieldValue(this.mrcParameters[i].name, 'NAME'); + (itemBlock as MethodMutatorArgBlock).originalName = this.mrcParameters[i].name; + + connection!.connect(itemBlock.previousConnection!); + connection = itemBlock.nextConnection; + } + return topBlock; + }, + mrcRenameParameter: function (this: ClassMethodDefBlock, oldName: string, newName: string) { + const nextBlock = this.getInputTargetBlock('STACK'); + if (nextBlock) { + findConnectedBlocksOfType(nextBlock, MRC_GET_PARAMETER_BLOCK_NAME).forEach((block) => { + if (block.getFieldValue('PARAMETER_NAME') === oldName) { + block.setFieldValue(newName, 'PARAMETER_NAME'); } - - return extraState; - }, - /** - * Applies the given state to this block. - */ - loadExtraState: function ( - this: ClassMethodDefBlock, - extraState: ClassMethodDefExtraState - ): void { - this.mrcMethodId = extraState.methodId ? extraState.methodId : this.id; - this.mrcCanChangeSignature = extraState.canChangeSignature; - this.mrcCanBeCalledWithinClass = extraState.canBeCalledWithinClass; - this.mrcCanBeCalledOutsideClass = extraState.canBeCalledOutsideClass; - this.mrcPythonMethodName = extraState.pythonMethodName ? extraState.pythonMethodName : ''; - this.mrcFuncName = null; // Set during python code generation. - this.mrcReturnType = extraState.returnType; - this.mrcParameters = []; - extraState.params.forEach((param) => { - this.mrcParameters.push({ - 'name': param.name, - 'type': param.type, - }); + }); + } + }, + mrcUpdateParams: function (this: ClassMethodDefBlock) { + if (this.mrcParameters.length > 0) { + const input = this.getInput('TITLE'); + if (input) { + this.removeParameterFields(input); + this.mrcParameters.forEach((param) => { + const paramName = 'PARAM_' + param.name; + input.appendField(createFieldFlydown(param.name, false), paramName); }); - this.updateBlock_(); - }, - /** - * Update the block to reflect the newly loaded extra state. - */ - updateBlock_: function (this: ClassMethodDefBlock): void { - const name = this.getFieldValue(FIELD_METHOD_NAME); - const input = this.getInput('TITLE'); - if (!input) { - return; - } - input.removeField(FIELD_METHOD_NAME); - - if (this.mrcCanChangeSignature) { - const nameField = new Blockly.FieldTextInput(name); - input.insertFieldAt(0, nameField, FIELD_METHOD_NAME); - this.setMutator(new Blockly.icons.MutatorIcon([MUTATOR_BLOCK_NAME], this)); - nameField.setValidator(this.mrcNameFieldValidator.bind(this, nameField)); - } - else { - input.insertFieldAt(0, createFieldNonEditableText(name), FIELD_METHOD_NAME); - //Case because a current bug in blockly where it won't allow passing null to Blockly.Block.setMutator makes it necessary. - (this as Blockly.BlockSvg).setMutator(null); - } - this.mrcUpdateParams(); - this.mrcUpdateReturnInput(); - }, - compose: function (this: ClassMethodDefBlock, containerBlock: any) { - // Parameter list. - this.mrcParameters = []; - - let paramBlock = containerBlock.getInputTargetBlock('STACK'); - while (paramBlock) { - const param: Parameter = { - name: paramBlock.getFieldValue('NAME'), - type: '' - } - if (paramBlock.originalName) { - // This is a mutator arg block, so we can get the original name. - this.mrcRenameParameter(paramBlock.originalName, param.name); - paramBlock.originalName = param.name; - } - this.mrcParameters.push(param); - paramBlock = - paramBlock.nextConnection && paramBlock.nextConnection.targetBlock(); - } - this.mrcUpdateParams(); - if (this.mrcCanBeCalledWithinClass) { - const methodForWithin = this.getMethodForWithin(); - if (methodForWithin) { - mutateMethodCallers(this.workspace, this.mrcMethodId, methodForWithin); - } - } - }, - decompose: function (this: ClassMethodDefBlock, workspace: Blockly.Workspace) { - // This is a special sub-block that only gets created in the mutator UI. - // It acts as our "top block" - const topBlock = workspace.newBlock(PARAM_CONTAINER_BLOCK_NAME); - (topBlock as Blockly.BlockSvg).initSvg(); - - // Then we add one sub-block for each item in the list. - let connection = topBlock!.getInput('STACK')!.connection; - - for (let i = 0; i < this.mrcParameters.length; i++) { - let itemBlock = workspace.newBlock(MUTATOR_BLOCK_NAME); - (itemBlock as Blockly.BlockSvg).initSvg(); - itemBlock.setFieldValue(this.mrcParameters[i].name, 'NAME'); - (itemBlock as MethodMutatorArgBlock).originalName = this.mrcParameters[i].name; - - connection!.connect(itemBlock.previousConnection!); - connection = itemBlock.nextConnection; - } - return topBlock; - }, - mrcRenameParameter: function (this: ClassMethodDefBlock, oldName: string, newName: string) { - let nextBlock = this.getInputTargetBlock('STACK'); - - if (nextBlock){ - findConnectedBlocksOfType(nextBlock, MRC_GET_PARAMETER_BLOCK_NAME).forEach((block) => { - if (block.getFieldValue('PARAMETER_NAME') === oldName) { - block.setFieldValue(newName, 'PARAMETER_NAME'); - } - }); - } - }, - mrcUpdateParams: function (this: ClassMethodDefBlock) { - if (this.mrcParameters.length > 0) { - let input = this.getInput('TITLE'); - if (input) { - this.removeParameterFields(input); - this.mrcParameters.forEach((param) => { - const paramName = 'PARAM_' + param.name; - input.appendField(createFieldFlydown(param.name, false), paramName); - }); - } - } - }, - mrcUpdateReturnInput: function (this: ClassMethodDefBlock) { - // Remove existing return input if it exists - if (this.getInput(RETURN_VALUE)) { - this.removeInput(RETURN_VALUE); - } + } + } + }, + mrcUpdateReturnInput: function (this: ClassMethodDefBlock) { + // Remove existing return input if it exists + if (this.getInput(RETURN_VALUE)) { + this.removeInput(RETURN_VALUE); + } - // Add return input if return type is not 'None' - if (this.mrcReturnType && this.mrcReturnType !== 'None') { - this.appendValueInput(RETURN_VALUE) - .setAlign(Blockly.inputs.Align.RIGHT) - .appendField(Blockly.Msg.PROCEDURES_DEFRETURN_RETURN); - // Move the return input to be after the statement input - this.moveInputBefore('STACK', RETURN_VALUE); - } - }, - removeParameterFields: function (input: Blockly.Input) { - const fieldsToRemove = input.fieldRow - .filter(field => field.name?.startsWith('PARAM_')) - .map(field => field.name!); - - fieldsToRemove.forEach(fieldName => { - input.removeField(fieldName); - }); - }, - mrcNameFieldValidator(this: ClassMethodDefBlock, nameField: Blockly.FieldTextInput, name: string): string { - // When the user changes the method name on the block, clear the mrcPythonMethodName field. - this.mrcPythonMethodName = ''; - - // Strip leading and trailing whitespace. - name = name.trim(); - - const legalName = findLegalMethodName(name, this); - const oldName = nameField.getValue(); - if (oldName && oldName !== name && oldName !== legalName) { - // Rename any callers. - renameMethodCallers(this.workspace, this.mrcMethodId, legalName); - } - return legalName; - }, - getMethod: function (this: ClassMethodDefBlock): storageModuleContent.Method | null { - const method: storageModuleContent.Method = { - methodId: this.mrcMethodId, - visibleName: this.getFieldValue(FIELD_METHOD_NAME), - pythonName: this.mrcFuncName ? this.mrcFuncName : '', - returnType: this.mrcReturnType, - args: [{ - name: 'self', - type: '', - }], - }; - if (!method.pythonName) { - method.pythonName = method.visibleName; - } - this.mrcParameters.forEach(param => { - method.args.push({ - name: param.name, - type: param.type ? param.type : '', - }); - }); - return method; - }, - getMethodForWithin: function (this: ClassMethodDefBlock): storageModuleContent.Method | null { - if (this.mrcCanBeCalledWithinClass) { - return this.getMethod(); - } - return null; - }, - getMethodForOutside: function (this: ClassMethodDefBlock): storageModuleContent.Method | null { - if (this.mrcCanBeCalledOutsideClass) { - return this.getMethod(); - } - return null; - }, - canChangeSignature: function (this: ClassMethodDefBlock): boolean { - return this.mrcCanChangeSignature; - }, - getMethodName: function (this: ClassMethodDefBlock): string { - return this.getFieldValue(FIELD_METHOD_NAME); - }, - /** - * mrcChangeIds is called when a module is copied so that the copy has different ids than the original. - */ - mrcChangeIds: function (this: ClassMethodDefBlock, oldIdToNewId: { [oldId: string]: string }): void { - if (this.mrcMethodId in oldIdToNewId) { - this.mrcMethodId = oldIdToNewId[this.mrcMethodId]; - } - }, - upgrade_002_to_003: function(this: ClassMethodDefBlock) { - if (this.getFieldValue('NAME') === 'init') { - // Remove robot parameter from init method - const methodBlock = this as ClassMethodDefBlock; - let filteredParams: Parameter[] = methodBlock.mrcParameters.filter(param => param.name !== 'robot'); - methodBlock.mrcParameters = filteredParams; - } - }, + // Add return input if return type is not 'None' + if (this.mrcReturnType && this.mrcReturnType !== 'None') { + this.appendValueInput(RETURN_VALUE) + .setAlign(Blockly.inputs.Align.RIGHT) + .appendField(Blockly.Msg.PROCEDURES_DEFRETURN_RETURN); + // Move the return input to be after the statement input + this.moveInputBefore('STACK', RETURN_VALUE); + } + }, + removeParameterFields: function (input: Blockly.Input) { + const fieldsToRemove = input.fieldRow + .filter(field => field.name?.startsWith('PARAM_')) + .map(field => field.name!); + + fieldsToRemove.forEach(fieldName => { + input.removeField(fieldName); + }); + }, + mrcNameFieldValidator(this: ClassMethodDefBlock, nameField: Blockly.FieldTextInput, name: string): string { + // When the user changes the method name on the block, clear the mrcPythonMethodName field. + this.mrcPythonMethodName = ''; + + // Strip leading and trailing whitespace. + name = name.trim(); + + const legalName = findLegalMethodName(name, this); + const oldName = nameField.getValue(); + if (oldName && oldName !== name && oldName !== legalName) { + // Rename any callers. + renameMethodCallers(this.workspace, this.mrcMethodId, legalName); + } + return legalName; + }, + getMethod: function (this: ClassMethodDefBlock): storageModuleContent.Method | null { + const method: storageModuleContent.Method = { + methodId: this.mrcMethodId, + visibleName: this.getFieldValue(FIELD_METHOD_NAME), + pythonName: this.mrcFuncName ? this.mrcFuncName : '', + returnType: this.mrcReturnType, + args: [{ + name: 'self', + type: '', + }], + }; + if (!method.pythonName) { + method.pythonName = method.visibleName; + } + this.mrcParameters.forEach(param => { + method.args.push({ + name: param.name, + type: param.type ? param.type : '', + }); + }); + return method; + }, + getMethodForWithin: function (this: ClassMethodDefBlock): storageModuleContent.Method | null { + if (this.mrcCanBeCalledWithinClass) { + return this.getMethod(); + } + return null; + }, + getMethodForOutside: function (this: ClassMethodDefBlock): storageModuleContent.Method | null { + if (this.mrcCanBeCalledOutsideClass) { + return this.getMethod(); + } + return null; + }, + canChangeSignature: function (this: ClassMethodDefBlock): boolean { + return this.mrcCanChangeSignature; + }, + getMethodName: function (this: ClassMethodDefBlock): string { + return this.getFieldValue(FIELD_METHOD_NAME); + }, + /** + * mrcChangeIds is called when a module is copied so that the copy has different ids than the original. + */ + mrcChangeIds: function (this: ClassMethodDefBlock, oldIdToNewId: { [oldId: string]: string }): void { + if (this.mrcMethodId in oldIdToNewId) { + this.mrcMethodId = oldIdToNewId[this.mrcMethodId]; + } + }, + mrcGetParameterNames: function(this: ClassMethodDefBlock): string[] { + const parameterNames: string[] = []; + this.mrcParameters.forEach(parameter => { + parameterNames.push(parameter.name); + }); + return parameterNames; + }, + upgrade_002_to_003: function(this: ClassMethodDefBlock) { + if (this.getFieldValue('NAME') === 'init') { + // Remove robot parameter from init method + const methodBlock = this as ClassMethodDefBlock; + const filteredParams: Parameter[] = methodBlock.mrcParameters.filter(param => param.name !== 'robot'); + methodBlock.mrcParameters = filteredParams; + } + }, }; /** @@ -360,21 +360,21 @@ const CLASS_METHOD_DEF = { * @returns Non-colliding name. */ function findLegalMethodName(name: string, block: ClassMethodDefBlock): string { - if (block.isInFlyout) { - // Flyouts can have multiple methods called 'my_method'. - return name; - } - name = name || 'unnamed'; - while (isMethodNameUsed(name, block.workspace, block)) { - // Collision with another method. - const r = name.match(/^(.*?)(\d+)$/); - if (!r) { - name += '2'; - } else { - name = r[1] + (parseInt(r[2]) + 1); - } - } + if (block.isInFlyout) { + // Flyouts can have multiple methods called 'my_method'. return name; + } + name = name || 'unnamed'; + while (isMethodNameUsed(name, block.workspace, block)) { + // Collision with another method. + const r = name.match(/^(.*?)(\d+)$/); + if (!r) { + name += '2'; + } else { + name = r[1] + (parseInt(r[2]) + 1); + } + } + return name; } /** @@ -387,122 +387,118 @@ function findLegalMethodName(name: string, block: ClassMethodDefBlock): string { * @returns True if the name is used, otherwise return false. */ function isMethodNameUsed( - name: string, workspace: Blockly.Workspace, opt_exclude?: Blockly.Block): boolean { - const nameLowerCase = name.toLowerCase(); - for (const block of workspace.getBlocksByType(BLOCK_NAME)) { - if (block === opt_exclude) { - continue; - } - if (nameLowerCase === block.getFieldValue(FIELD_METHOD_NAME).toLowerCase()) { - return true; - } - const classMethodDefBlock = block as ClassMethodDefBlock; - if (classMethodDefBlock.mrcPythonMethodName && - nameLowerCase === classMethodDefBlock.mrcPythonMethodName.toLowerCase()) { - return true; - } + name: string, workspace: Blockly.Workspace, opt_exclude?: Blockly.Block): boolean { + const nameLowerCase = name.toLowerCase(); + for (const block of workspace.getBlocksByType(BLOCK_NAME)) { + if (block === opt_exclude) { + continue; + } + if (nameLowerCase === block.getFieldValue(FIELD_METHOD_NAME).toLowerCase()) { + return true; } - return false; + const classMethodDefBlock = block as ClassMethodDefBlock; + if (classMethodDefBlock.mrcPythonMethodName && + nameLowerCase === classMethodDefBlock.mrcPythonMethodName.toLowerCase()) { + return true; + } + } + return false; } export const setup = function () { - Blockly.Blocks[BLOCK_NAME] = CLASS_METHOD_DEF; + Blockly.Blocks[BLOCK_NAME] = CLASS_METHOD_DEF; }; export const pythonFromBlock = function ( block: ClassMethodDefBlock, generator: ExtendedPythonGenerator, ) { - const blocklyName = block.mrcPythonMethodName ? block.mrcPythonMethodName : block.getFieldValue(FIELD_METHOD_NAME); + const blocklyName = block.mrcPythonMethodName ? block.mrcPythonMethodName : block.getFieldValue(FIELD_METHOD_NAME); - // Call generator.getProcedureName so our function name is not a reserved word such as "while". - const funcName = generator.getProcedureName(blocklyName); + // Call generator.getProcedureName so our function name is not a reserved word such as "while". + const funcName = generator.getProcedureName(blocklyName); - let xfix1 = ''; - if (generator.STATEMENT_PREFIX) { - xfix1 += generator.injectId(generator.STATEMENT_PREFIX, block); - } - if (generator.STATEMENT_SUFFIX) { - xfix1 += generator.injectId(generator.STATEMENT_SUFFIX, block); - } - if (xfix1) { - xfix1 = generator.prefixLines(xfix1, generator.INDENT); - } - let loopTrap = ''; - if (generator.INFINITE_LOOP_TRAP) { - loopTrap = generator.prefixLines( - generator.injectId(generator.INFINITE_LOOP_TRAP, block), - generator.INDENT, - ); - } - let branch = ''; - if (block.getInput('STACK')) { - branch = generator.statementToCode(block, 'STACK'); - } - let returnValue = ''; - if (block.getInput('RETURN')) { - returnValue = generator.valueToCode(block, 'RETURN', Order.NONE) || ''; - } - let xfix2 = ''; - if (branch && returnValue) { - // After executing the function body, revisit this block for the return. - xfix2 = xfix1; - } - if (block.mrcPythonMethodName === '__init__') { - let class_specific = generator.getClassSpecificForInit(); - branch = generator.INDENT + 'super().__init__(' + class_specific + ')\n' + - generator.generateInitStatements() + branch; - } - else if (generator.inBaseClassMethod(blocklyName)){ - // Special case for methods inherited from the based class: generate the - // call to the method in the base class. - branch = generator.INDENT + 'super().' + blocklyName + '()\n' + branch; - } - if (returnValue) { - returnValue = generator.INDENT + 'return ' + returnValue + '\n'; - } else if (!branch) { - branch = generator.PASS; - } + let xfix1 = ''; + if (generator.STATEMENT_PREFIX) { + xfix1 += generator.injectId(generator.STATEMENT_PREFIX, block); + } + if (generator.STATEMENT_SUFFIX) { + xfix1 += generator.injectId(generator.STATEMENT_SUFFIX, block); + } + if (xfix1) { + xfix1 = generator.prefixLines(xfix1, generator.INDENT); + } + let loopTrap = ''; + if (generator.INFINITE_LOOP_TRAP) { + loopTrap = generator.prefixLines( + generator.injectId(generator.INFINITE_LOOP_TRAP, block), + generator.INDENT, + ); + } + let branch = ''; + if (block.getInput('STACK')) { + branch = generator.statementToCode(block, 'STACK'); + } + let returnValue = ''; + if (block.getInput('RETURN')) { + returnValue = generator.valueToCode(block, 'RETURN', Order.NONE) || ''; + } + let xfix2 = ''; + if (branch && returnValue) { + // After executing the function body, revisit this block for the return. + xfix2 = xfix1; + } + if (block.mrcPythonMethodName === '__init__') { + const classSpecific = generator.getClassSpecificForInit(); + branch = generator.INDENT + 'super().__init__(' + classSpecific + ')\n' + + generator.generateInitStatements() + branch; + } + else if (generator.inBaseClassMethod(blocklyName)) { + // Special case for methods inherited from the based class: generate the + // call to the method in the base class. + branch = generator.INDENT + 'super().' + blocklyName + '()\n' + branch; + } + if (returnValue) { + returnValue = generator.INDENT + 'return ' + returnValue + '\n'; + } else if (!branch) { + branch = generator.PASS; + } - let params = block.mrcParameters; - let paramString = "self"; - if (generator.getModuleType() === storageModule.ModuleType.MECHANISM && block.mrcPythonMethodName === '__init__') { - const ports: string[] = generator.getComponentPortParameters(); - if (ports.length) { - paramString += ', ' + ports.join(', '); - } + const params = block.mrcParameters; + let paramString = "self"; + if (generator.getModuleType() === storageModule.ModuleType.MECHANISM && block.mrcPythonMethodName === '__init__') { + const ports: string[] = generator.getComponentPortParameters(); + if (ports.length) { + paramString += ', ' + ports.join(', '); } + } - if (generator.getModuleType() === storageModule.ModuleType.OPMODE && block.mrcPythonMethodName === '__init__') { - paramString += ', robot'; - } + if (generator.getModuleType() === storageModule.ModuleType.OPMODE && block.mrcPythonMethodName === '__init__') { + paramString += ', robot'; + } - if (params.length != 0) { - block.mrcParameters.forEach((param) => { - paramString += ', ' + param.name; - }); - } + if (params.length != 0) { + block.mrcParameters.forEach(param => { + paramString += ', ' + param.name; + }); + } + + let code = 'def ' + funcName + '(' + paramString + '):\n'; - let code = 'def ' + - funcName + - '(' + - paramString + - '):\n'; - - code += - xfix1 + - loopTrap + - branch + - xfix2 + - returnValue; - code = generator.scrub_(block, code); - generator.addClassMethodDefinition(funcName, code); - - // Save the name of the function we just generated so we can use it to create the storageModuleContent.Method. - // in the getMethod function. - block.mrcFuncName = funcName; - - return ''; + code += + xfix1 + + loopTrap + + branch + + xfix2 + + returnValue; + code = generator.scrub_(block, code); + generator.addClassMethodDefinition(funcName, code); + + // Save the name of the function we just generated so we can use it to create the storageModuleContent.Method. + // in the getMethod function. + block.mrcFuncName = funcName; + + return ''; } // Functions used for creating blocks for the toolbox. @@ -521,20 +517,20 @@ export function createCustomMethodBlock(): toolboxItems.Block { } export function createCustomMethodBlockWithReturn(): toolboxItems.Block { - const extraState: ClassMethodDefExtraState = { - canChangeSignature: true, - canBeCalledWithinClass: true, - canBeCalledOutsideClass: true, - returnType: 'Any', - params: [], - }; - const fields: {[key: string]: any} = {}; - fields[FIELD_METHOD_NAME] = 'my_method_with_return'; - const inputs: {[key: string]: any} = {}; - inputs[RETURN_VALUE] = { - 'type': 'input_value', - }; - return new toolboxItems.Block(BLOCK_NAME, extraState, fields, inputs); + const extraState: ClassMethodDefExtraState = { + canChangeSignature: true, + canBeCalledWithinClass: true, + canBeCalledOutsideClass: true, + returnType: 'Any', + params: [], + }; + const fields: {[key: string]: any} = {}; + fields[FIELD_METHOD_NAME] = 'my_method_with_return'; + const inputs: {[key: string]: any} = {}; + inputs[RETURN_VALUE] = { + type: 'input_value', + }; + return new toolboxItems.Block(BLOCK_NAME, extraState, fields, inputs); } export function getBaseClassBlocks( @@ -568,8 +564,8 @@ function createClassMethodDefBlock( }; for (let i = 1; i < functionData.args.length; i++) { extraState.params.push({ - 'name': functionData.args[i].name, - 'type': functionData.args[i].type, + name: functionData.args[i].name, + type: functionData.args[i].type, }); } const fields: {[key: string]: any} = {}; diff --git a/src/blocks/mrc_event.ts b/src/blocks/mrc_event.ts index 8b94318a..d64964b7 100644 --- a/src/blocks/mrc_event.ts +++ b/src/blocks/mrc_event.ts @@ -22,6 +22,7 @@ import * as Blockly from 'blockly'; import { MRC_STYLE_EVENTS } from '../themes/styles' +import { Parameter } from './mrc_class_method_def'; import { ExtendedPythonGenerator } from '../editor/extended_python_generator'; import { MUTATOR_BLOCK_NAME, PARAM_CONTAINER_BLOCK_NAME, MethodMutatorArgBlock } from './mrc_param_container' import { BLOCK_NAME as MRC_MECHANISM_COMPONENT_HOLDER } from './mrc_mechanism_component_holder'; @@ -36,11 +37,6 @@ const INPUT_TITLE = 'TITLE'; const FIELD_EVENT_NAME = 'NAME'; const FIELD_PARAM_PREFIX = 'PARAM_'; -type Parameter = { - name: string, - type?: string, -}; - const WARNING_ID_NOT_IN_HOLDER = 'not in holder'; type EventExtraState = { @@ -214,7 +210,7 @@ const EVENT = { this.checkParentIsHolder(); }, /** - * mrcOnLoad is called when an EventBlock is moved. + * mrcOnMove is called when an EventBlock is moved. */ mrcOnMove: function(this: EventBlock): void { this.checkParentIsHolder(); diff --git a/src/blocks/mrc_event_handler.ts b/src/blocks/mrc_event_handler.ts index 03f0c01a..fdac4aad 100644 --- a/src/blocks/mrc_event_handler.ts +++ b/src/blocks/mrc_event_handler.ts @@ -24,6 +24,7 @@ import * as Blockly from 'blockly'; import {Order} from 'blockly/python'; import type { MessageInstance } from 'antd/es/message/interface'; +import { Parameter } from './mrc_class_method_def'; import { Editor } from '../editor/editor'; import { ExtendedPythonGenerator } from '../editor/extended_python_generator'; import { createFieldFlydown } from '../fields/field_flydown'; @@ -47,11 +48,6 @@ export enum SenderType { COMPONENT = 'component' } -export interface Parameter { - name: string; - type?: string; -} - const SENDER_VALUE_ROBOT = 'robot'; const WARNING_ID_EVENT_CHANGED = 'event changed'; @@ -311,6 +307,13 @@ const EVENT_HANDLER = { this.mrcMechanismId = oldIdToNewId[this.mrcMechanismId]; } }, + mrcGetParameterNames: function(this: EventHandlerBlock): string[] { + const parameterNames: string[] = []; + this.mrcParameters.forEach(parameter => { + parameterNames.push(parameter.name); + }); + return parameterNames; + }, }; export function setup(): void { diff --git a/src/blocks/mrc_get_parameter.ts b/src/blocks/mrc_get_parameter.ts index 32b30b58..09dd3a7e 100644 --- a/src/blocks/mrc_get_parameter.ts +++ b/src/blocks/mrc_get_parameter.ts @@ -27,70 +27,94 @@ import {ExtendedPythonGenerator} from '../editor/extended_python_generator'; import {createFieldNonEditableText} from '../fields/FieldNonEditableText'; import {MRC_STYLE_VARIABLES} from '../themes/styles'; import {BLOCK_NAME as MRC_CLASS_METHOD_DEF, ClassMethodDefBlock} from './mrc_class_method_def'; -import {BLOCK_NAME as MRC_EVENT_HANDLER } from './mrc_event_handler'; -import * as ChangeFramework from './utils/change_framework'; +import {BLOCK_NAME as MRC_EVENT_HANDLER, EventHandlerBlock } from './mrc_event_handler'; export const BLOCK_NAME = 'mrc_get_parameter'; export const OUTPUT_NAME = 'mrc_get_parameter_output'; +const FIELD_PARAMETER_NAME = 'PARAMETER_NAME'; + +const WARNING_ID_NOT_IN_METHOD = 'not in method'; + type GetParameterBlock = Blockly.Block & Blockly.BlockSvg & GetParameterMixin; -interface GetParameterMixin extends GetParameterMixinType {} +interface GetParameterMixin extends GetParameterMixinType { + // TODO(lizlooney): currently mrcParameterType is never set to anything other than '' because + // setNameAndType is never called. If we add code that uses setNameAndType, we will probably + // need to save and load extra state that includes the parameter type. + /** + * mrcParameterType is initialized in init to ''. It can be set by calling setNameAndType. + */ + mrcParameterType: string, + + /** + * mrcHasWarning is set to true if we set the warning text on the block. It is checked to avoid + * adding a warning if there already is one. Otherwise, if we get two move events (one for drag + * and one for snap), and we call setWarningText for both events, we get a detached warning + * balloon. See https://github.com/wpilibsuite/systemcore-blocks-interface/issues/248. + */ + mrcHasWarning: boolean, +} type GetParameterMixinType = typeof GET_PARAMETER_BLOCK; const GET_PARAMETER_BLOCK = { - parameterType: '', // Later this will be set to the type of the parameter, e.g. 'string', 'number', etc. /** * Block initialization. */ init: function(this: GetParameterBlock): void { + this.mrcParameterType = ''; + this.mrcHasWarning = false; + this.setStyle(MRC_STYLE_VARIABLES); this.appendDummyInput() .appendField(Blockly.Msg.PARAMETER) - .appendField(createFieldNonEditableText('parameter'), 'PARAMETER_NAME'); - - this.setOutput(true, this.parameterType); - ChangeFramework.registerCallback(BLOCK_NAME, [Blockly.Events.BLOCK_MOVE], this.onBlockChanged); + .appendField(createFieldNonEditableText(''), FIELD_PARAMETER_NAME); + this.setOutput(true, this.mrcParameterType); }, setNameAndType: function(this: GetParameterBlock, name: string, type: string): void { - this.setFieldValue(name, 'PARAMETER_NAME'); - this.parameterType = type; + this.setFieldValue(name, FIELD_PARAMETER_NAME); + this.mrcParameterType = type; this.setOutput(true, [OUTPUT_NAME, type]); }, + /** + * mrcOnMove is called when an EventBlock is moved. + */ + mrcOnMove: function(this: GetParameterBlock): void { + this.checkBlockPlacement(); + }, + mrcOnAncestorMove: function(this: GetParameterBlock): void { + this.checkBlockPlacement(); + }, + checkBlockPlacement: function(this: GetParameterBlock): void { + const legalParameterNames: string[] = []; - onBlockChanged(block: Blockly.BlockSvg, blockEvent: Blockly.Events.BlockBase): void { - const blockBlock = block as Blockly.Block; - - if (blockEvent.type === Blockly.Events.BLOCK_MOVE) { - let parent = blockBlock.getRootBlock(); - if ( parent.type === MRC_CLASS_METHOD_DEF) { - // It is a class method definition, so we see if this variable is in it. - const classMethodDefBlock = parent as ClassMethodDefBlock; - for (const parameter of classMethodDefBlock.mrcParameters) { - if (parameter.name === blockBlock.getFieldValue('PARAMETER_NAME')) { - // If it is, we allow it to stay. - blockBlock.setWarningText(null); - return; - } - } - } - else if (parent.type === MRC_EVENT_HANDLER) { - const classMethodDefBlock = parent as ClassMethodDefBlock; - for (const parameter of classMethodDefBlock.mrcParameters) { - if (parameter.name === blockBlock.getFieldValue('PARAMETER_NAME')) { - // If it is, we allow it to stay. - blockBlock.setWarningText(null); - return; - } - } + const rootBlock: Blockly.Block = this.getRootBlock(); + if (rootBlock.type === MRC_CLASS_METHOD_DEF) { + // This block is within a class method definition. + const classMethodDefBlock = rootBlock as ClassMethodDefBlock; + // Add the method's parameter names to legalParameterNames. + legalParameterNames.push(...classMethodDefBlock.mrcGetParameterNames()); + } else if (rootBlock.type === MRC_EVENT_HANDLER) { + // This block is within an event handler. + const eventHandlerBlock = rootBlock as EventHandlerBlock; + // Add the method's parameter names to legalParameterNames. + legalParameterNames.push(...eventHandlerBlock.mrcGetParameterNames()); + } + + if (legalParameterNames.includes(this.getFieldValue(FIELD_PARAMETER_NAME))) { + // If this blocks's parameter name is in legalParameterNames, it's good. + this.setWarningText(null, WARNING_ID_NOT_IN_METHOD); + this.mrcHasWarning = false; + } else { + // Otherwise, add a warning to this block. + if (!this.mrcHasWarning) { + this.setWarningText(Blockly.Msg.PARAMETERS_CAN_ONLY_GO_IN_THEIR_METHODS_BLOCK, WARNING_ID_NOT_IN_METHOD); + this.getIcon(Blockly.icons.IconType.WARNING)!.setBubbleVisible(true); + this.mrcHasWarning = true; } - // If we end up here it shouldn't be allowed - block.unplug(true); - blockBlock.setWarningText(Blockly.Msg.PARAMETERS_CAN_ONLY_GO_IN_THEIR_METHODS_BLOCK); - blockBlock.getIcon(Blockly.icons.IconType.WARNING)!.setBubbleVisible(true); } }, }; @@ -104,7 +128,7 @@ export const pythonFromBlock = function( _generator: ExtendedPythonGenerator, ) { // TODO (Alan) : Specify the type here as well - const code = block.getFieldValue('PARAMETER_NAME'); + const code = block.getFieldValue(FIELD_PARAMETER_NAME); return [code, Order.ATOMIC]; }; diff --git a/src/editor/editor.ts b/src/editor/editor.ts index 19f24743..6e446c6b 100644 --- a/src/editor/editor.ts +++ b/src/editor/editor.ts @@ -40,6 +40,7 @@ const EMPTY_TOOLBOX: Blockly.utils.toolbox.ToolboxInfo = { const MRC_ON_LOAD = 'mrcOnLoad'; const MRC_ON_MOVE = 'mrcOnMove'; +const MRC_ON_ANCESTOR_MOVE = 'mrcOnAncestorMove'; const MRC_VALIDATE = 'mrcValidate'; export class Editor { @@ -129,9 +130,16 @@ export class Editor { if (!block) { return; } + // Call MRC_ON_MOVE for the block that was moved. if (MRC_ON_MOVE in block && typeof block[MRC_ON_MOVE] === 'function') { block[MRC_ON_MOVE](); } + // Call MRC_ON_ANCESTOR_MOVE for all descendents of the block that was moved. + block.getDescendants(false).forEach(descendant => { + if (MRC_ON_ANCESTOR_MOVE in descendant && typeof descendant[MRC_ON_ANCESTOR_MOVE] === 'function') { + descendant[MRC_ON_ANCESTOR_MOVE](); + } + }); } }