diff --git a/src/blocks/mrc_call_python_function.ts b/src/blocks/mrc_call_python_function.ts index 9944a601..bf8073bf 100644 --- a/src/blocks/mrc_call_python_function.ts +++ b/src/blocks/mrc_call_python_function.ts @@ -23,25 +23,24 @@ import * as Blockly from 'blockly'; import { Order } from 'blockly/python'; -import { ClassMethodDefExtraState } from './mrc_class_method_def' import { getClassData, getAllowedTypesForSetCheck, getOutputCheck } from './utils/python'; import { ArgData, FunctionData, findSuperFunctionData } from './utils/python_json_types'; -import * as Value from './utils/value'; -import * as Variable from './utils/variable'; +import * as value from './utils/value'; +import * as variable from './utils/variable'; import { Editor } from '../editor/editor'; import { ExtendedPythonGenerator } from '../editor/extended_python_generator'; import { createFieldDropdown } from '../fields/FieldDropdown'; import { createFieldNonEditableText } from '../fields/FieldNonEditableText'; import { MRC_STYLE_FUNCTIONS } from '../themes/styles' -import * as ToolboxItems from '../toolbox/items'; -import * as CommonStorage from '../storage/common_storage'; +import * as toolboxItems from '../toolbox/items'; +import * as commonStorage from '../storage/common_storage'; // A block to call a python function. export const BLOCK_NAME = 'mrc_call_python_function'; -export enum FunctionKind { +enum FunctionKind { BUILT_IN = 'built-in', MODULE = 'module', STATIC = 'static', @@ -53,13 +52,14 @@ export enum FunctionKind { EVENT = 'event', } -export const RETURN_TYPE_NONE = 'None'; +const RETURN_TYPE_NONE = 'None'; const FIELD_MODULE_OR_CLASS_NAME = 'MODULE_OR_CLASS'; const FIELD_FUNCTION_NAME = 'FUNC'; +const FIELD_EVENT_NAME = 'EVENT'; const FIELD_COMPONENT_NAME = 'COMPONENT_NAME'; -export type FunctionArg = { +type FunctionArg = { name: string, type: string, }; @@ -70,17 +70,13 @@ export type CallPythonFunctionBlock = Blockly.Block & CallPythonFunctionMixin & interface CallPythonFunctionMixin extends CallPythonFunctionMixinType { mrcFunctionKind: FunctionKind, mrcReturnType: string, - mrcArgs: FunctionArg[], + mrcArgs: FunctionArg[], // Include the self arg only if there is a socket for it. mrcTooltip: string, mrcImportModule: string, mrcActualFunctionName: string, - mrcClassMethodDefBlockId: string, + mrcOtherBlockId: string, mrcComponentClassName: string, - mrcComponents: CommonStorage.Component[], - mrcComponentName: string, - mrcComponentBlockId: string, - renameMethod(this: CallPythonFunctionBlock, newName: string): void; - mutateMethod(this: CallPythonFunctionBlock, defBlockExtraState: ClassMethodDefExtraState): void; + mrcOriginalComponentName: string, } type CallPythonFunctionMixinType = typeof CALL_PYTHON_FUNCTION; @@ -116,9 +112,12 @@ type CallPythonFunctionExtraState = { */ actualFunctionName?: string, /** - * The id of the mrc_class_method_def type that defines the method. Specified only if the function kind is INSTANCE_ROBOT. + * The id of the block that defines the method, component, or event. + * For INSTANCE_ROBOT or INSTANCE_WITHIN, this is the id of an mrc_class_method_def block. + * For INSTANCE_COMPONENT, this is the id of an mrc_component block. + * For EVENT, this is the id of an mrc_event block. */ - classMethodDefBlockId?: string, + otherBlockId?: string, /** * The component name. Specified only if the function kind is INSTANCE_COMPONENT. */ @@ -127,10 +126,6 @@ type CallPythonFunctionExtraState = { * The component class name. Specified only if the function kind is INSTANCE_COMPONENT. */ componentClassName?: string, - /** - * The id of the mrc_component type that defines the component. Specified only if the function kind is INSTANCE_COMPONENT. - */ - componentBlockId?: string, }; const CALL_PYTHON_FUNCTION = { @@ -176,8 +171,8 @@ const CALL_PYTHON_FUNCTION = { break; } case FunctionKind.EVENT: { - const functionName = this.getFieldValue(FIELD_FUNCTION_NAME); - tooltip = 'Fires the event ' + functionName + '.'; + const eventName = this.getFieldValue(FIELD_EVENT_NAME); + tooltip = 'Fires the event ' + eventName + '.'; break; } case FunctionKind.INSTANCE_COMPONENT: { @@ -215,8 +210,8 @@ const CALL_PYTHON_FUNCTION = { if (this.mrcArgs){ this.mrcArgs.forEach((arg) => { extraState.args.push({ - 'name': arg.name, - 'type': arg.type, + name: arg.name, + type: arg.type, }); }); } @@ -229,18 +224,25 @@ const CALL_PYTHON_FUNCTION = { if (this.mrcActualFunctionName) { extraState.actualFunctionName = this.mrcActualFunctionName; } - if (this.mrcClassMethodDefBlockId) { - extraState.classMethodDefBlockId = this.mrcClassMethodDefBlockId; + if (this.mrcOtherBlockId) { + extraState.otherBlockId = this.mrcOtherBlockId; } if (this.mrcComponentClassName) { extraState.componentClassName = this.mrcComponentClassName; } if (this.getField(FIELD_COMPONENT_NAME)) { extraState.componentName = this.getFieldValue(FIELD_COMPONENT_NAME); - for (const component of this.mrcComponents) { - if (component.name == extraState.componentName) { - extraState.componentBlockId = component.blockId; - break; + // The component name field is a drop down where the user can choose between different + // components of the same type. For example, they can easily switch from a motor component + // name "left_motor" to a motor component named "right_motor". + if (extraState.componentName !== this.mrcOriginalComponentName) { + // The user has chosen a different component name. We need to get the block id of the + // mrc_component block that defines the component that the user has chosen. + for (const component of this.getComponentsFromRobot()) { + if (component.name == extraState.componentName) { + extraState.otherBlockId = component.blockId; + break; + } } } } @@ -258,8 +260,8 @@ const CALL_PYTHON_FUNCTION = { this.mrcArgs = []; extraState.args.forEach((arg) => { this.mrcArgs.push({ - 'name': arg.name, - 'type': arg.type, + name: arg.name, + type: arg.type, }); }); this.mrcTooltip = extraState.tooltip ? extraState.tooltip : ''; @@ -267,25 +269,23 @@ const CALL_PYTHON_FUNCTION = { ? extraState.importModule : ''; this.mrcActualFunctionName = extraState.actualFunctionName ? extraState.actualFunctionName : ''; - this.mrcClassMethodDefBlockId = extraState.classMethodDefBlockId - ? extraState.classMethodDefBlockId : ''; + this.mrcOtherBlockId = extraState.otherBlockId + ? extraState.otherBlockId : ''; + // TODO(lizlooney: Remove this if statement after Alan and I have updated our projects. + if (!this.mrcOtherBlockId) { + const oldExtraState = extraState as any; + if (oldExtraState.classMethodDefBlockId) { + this.mrcOtherBlockId = oldExtraState.classMethodDefBlockId; + } else if (oldExtraState.componentBlockId) { + this.mrcOtherBlockId = oldExtraState.componentBlockId; + } else if (oldExtraState.eventBlockId) { + this.mrcOtherBlockId = oldExtraState.eventBlockId; + } + } this.mrcComponentClassName = extraState.componentClassName ? extraState.componentClassName : ''; - // Get the list of components whose type matches this.mrcComponentClassName so we can - // create a dropdown that has the appropriate component name choices. - this.mrcComponents = []; - const editor = Editor.getEditorForBlocklyWorkspace(this.workspace); - if (editor) { - editor.getComponentsFromRobot().forEach(component => { - if (component.className === this.mrcComponentClassName) { - this.mrcComponents.push(component); - } - }); - } - this.mrcComponentName = extraState.componentName + this.mrcOriginalComponentName = extraState.componentName ? extraState.componentName : ''; - this.mrcComponentBlockId = extraState.componentBlockId - ? extraState.componentBlockId : ''; this.updateBlock_(); }, /** @@ -357,15 +357,15 @@ const CALL_PYTHON_FUNCTION = { if (!input) { this.appendDummyInput('TITLE') .appendField('fire') - .appendField(createFieldNonEditableText(''), FIELD_FUNCTION_NAME); + .appendField(createFieldNonEditableText(''), FIELD_EVENT_NAME); } break; } case FunctionKind.INSTANCE_COMPONENT: { const componentNameChoices : string[] = []; - this.mrcComponents.forEach(component => componentNameChoices.push(component.name)); - if (!componentNameChoices.includes(this.mrcComponentName)) { - componentNameChoices.push(this.mrcComponentName); + this.getComponentsFromRobot().forEach(component => componentNameChoices.push(component.name)); + if (!componentNameChoices.includes(this.mrcOriginalComponentName)) { + componentNameChoices.push(this.mrcOriginalComponentName); } this.appendDummyInput('TITLE') .appendField('call') @@ -415,23 +415,54 @@ const CALL_PYTHON_FUNCTION = { this.removeInput('ARG' + i); } }, - renameMethod: function(this: CallPythonFunctionBlock, newName: string): void { - this.setFieldValue(newName, FIELD_FUNCTION_NAME); + renameMethodCaller: function(this: CallPythonFunctionBlock, newName: string): void { + if (this.mrcFunctionKind == FunctionKind.EVENT) { + this.setFieldValue(newName, FIELD_EVENT_NAME); + } else if (this.mrcFunctionKind == FunctionKind.INSTANCE_WITHIN) { + // TODO(lizlooney): What about this.mrcActualFunctionName? Does it need to be updated? + this.setFieldValue(newName, FIELD_FUNCTION_NAME); + } }, - mutateMethod: function( + mutateMethodCaller: function( this: CallPythonFunctionBlock, - defBlockExtraState: ClassMethodDefExtraState + methodOrEvent: commonStorage.Method | commonStorage.Event ): void { - this.mrcReturnType = defBlockExtraState.returnType; - this.mrcArgs = []; - defBlockExtraState.params.forEach((param) => { - this.mrcArgs.push({ - 'name': param.name, - 'type': param.type ?? '', + if (this.mrcFunctionKind == FunctionKind.EVENT) { + const event = methodOrEvent as commonStorage.Event; + this.mrcArgs = []; + event.args.forEach((arg) => { + this.mrcArgs.push({ + name: arg.name, + type: arg.type, + }); }); - }); + } else if (this.mrcFunctionKind == FunctionKind.INSTANCE_WITHIN) { + const method = methodOrEvent as commonStorage.Method; + this.mrcReturnType = method.returnType; + this.mrcArgs = []; + // We don't include the arg for the self argument because we don't want a socket for it. + for (let i = 1; i < method.args.length; i++) { + this.mrcArgs.push({ + name: method.args[i].name, + type: method.args[i].type, + }); + } + } this.updateBlock_(); }, + getComponentsFromRobot: function(this: CallPythonFunctionBlock): commonStorage.Component[] { + // Get the list of components whose type matches this.mrcComponentClassName. + const components: commonStorage.Component[] = []; + const editor = Editor.getEditorForBlocklyWorkspace(this.workspace); + if (editor) { + editor.getComponentsFromRobot().forEach(component => { + if (component.className === this.mrcComponentClassName) { + components.push(component); + } + }); + } + return components; + }, onLoad: function(this: CallPythonFunctionBlock): void { const warnings: string[] = []; @@ -442,8 +473,9 @@ const CALL_PYTHON_FUNCTION = { // visible warning on it. if (this.mrcFunctionKind === FunctionKind.INSTANCE_COMPONENT) { let foundComponent = false; - for (const component of this.mrcComponents) { - if (component.blockId === this.mrcComponentBlockId) { + const components = this.getComponentsFromRobot(); + for (const component of components) { + if (component.blockId === this.mrcOtherBlockId) { foundComponent = true; // If the component name has changed, we can handle that. @@ -460,7 +492,7 @@ const CALL_PYTHON_FUNCTION = { } if (indexOfComponentName != -1) { const componentNameChoices : string[] = []; - this.mrcComponents.forEach(component => componentNameChoices.push(component.name)); + components.forEach(component => componentNameChoices.push(component.name)); titleInput.removeField(FIELD_COMPONENT_NAME); titleInput.insertFieldAt(indexOfComponentName, createFieldDropdown(componentNameChoices), FIELD_COMPONENT_NAME); @@ -491,7 +523,7 @@ const CALL_PYTHON_FUNCTION = { if (editor) { const robotMethods = editor.getMethodsFromRobot(); for (const robotMethod of robotMethods) { - if (robotMethod.blockId === this.mrcClassMethodDefBlockId) { + if (robotMethod.blockId === this.mrcOtherBlockId) { foundRobotMethod = true; // If the function name has changed, we can fix this block. @@ -604,9 +636,10 @@ export const pythonFromBlock = function( break; } case FunctionKind.EVENT: { - const blocklyName = block.getFieldValue(FIELD_FUNCTION_NAME); - const functionName = generator.getProcedureName(blocklyName); - code = 'if self.events.get("' + functionName + '", None):\n' + generator.INDENT + 'self.events["' + functionName + '"]'; + const eventName = block.getFieldValue(FIELD_EVENT_NAME); + code = + 'if self.events.get("' + eventName + '", None):\n' + + generator.INDENT + 'self.events["' + eventName + '"]'; break; } case FunctionKind.INSTANCE_COMPONENT: { @@ -616,11 +649,11 @@ export const pythonFromBlock = function( : block.getFieldValue(FIELD_FUNCTION_NAME); // Generate the correct code depending on the module type. switch (generator.getModuleType()) { - case CommonStorage.MODULE_TYPE_ROBOT: - case CommonStorage.MODULE_TYPE_MECHANISM: + case commonStorage.MODULE_TYPE_ROBOT: + case commonStorage.MODULE_TYPE_MECHANISM: code = 'self.'; break; - case CommonStorage.MODULE_TYPE_OPMODE: + case commonStorage.MODULE_TYPE_OPMODE: default: code = 'self.robot.'; break; @@ -652,45 +685,42 @@ function generateCodeForArguments( startIndex: number) { let code = ''; if (block.mrcArgs.length - startIndex === 1) { + // If there is only one input, put it on the same line as the function name. code += generator.valueToCode(block, 'ARG' + startIndex, Order.NONE) || 'None'; } else { - let delimiter = '\n' + generator.INDENT + generator.INDENT; + let delimiter = ''; for (let i = startIndex; i < block.mrcArgs.length; i++) { - code += delimiter; + code += delimiter + '\n' + generator.INDENT + generator.INDENT; code += generator.valueToCode(block, 'ARG' + i, Order.NONE) || 'None'; - delimiter = ',\n' + generator.INDENT + generator.INDENT; + delimiter = ','; } } return code; } -function getMethodCallers(workspace: Blockly.Workspace, name: string): Blockly.Block[] { +function getMethodCallers(workspace: Blockly.Workspace, otherBlockId: 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(FIELD_FUNCTION_NAME) === name - ); + return (block as CallPythonFunctionBlock).mrcOtherBlockId === otherBlockId; }); } -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 renameMethodCallers(workspace: Blockly.Workspace, otherBlockId: string, newName: string): void { + for (const block of getMethodCallers(workspace, otherBlockId)) { + (block as CallPythonFunctionBlock).renameMethodCaller(newName); } } export function mutateMethodCallers( - workspace: Blockly.Workspace, methodName: string, defBlockExtraState: ClassMethodDefExtraState) { + workspace: Blockly.Workspace, otherBlockId: string, methodOrEvent: commonStorage.Method | commonStorage.Event) { const oldRecordUndo = Blockly.Events.getRecordUndo(); - for (const block of getMethodCallers(workspace, methodName)) { + for (const block of getMethodCallers(workspace, otherBlockId)) { const callBlock = block as CallPythonFunctionBlock; // Get the extra state before changing the call block. const oldExtraState = callBlock.saveExtraState(); // Apply the changes. - callBlock.mutateMethod(defBlockExtraState); + callBlock.mutateMethodCaller(methodOrEvent); // Get the extra state after changing the call block. const newExtraState = callBlock.saveExtraState(); @@ -715,14 +745,14 @@ export function mutateMethodCallers( export function addBuiltInFunctionBlocks( functions: FunctionData[], - contents: ToolboxItems.ContentsType[]) { + contents: toolboxItems.ContentsType[]) { functions.forEach(functionData => { contents.push(createBuiltInMethodBlock(functionData)); }); } function createBuiltInMethodBlock( - functionData: FunctionData): ToolboxItems.Block { + functionData: FunctionData): toolboxItems.Block { const extraState: CallPythonFunctionExtraState = { functionKind: FunctionKind.BUILT_IN, returnType: functionData.returnType, @@ -744,14 +774,14 @@ function processArgs( for (let i = 0; i < args.length; i++) { let argName = args[i].name; if (i === 0 && argName === 'self' && declaringClassName) { - argName = Variable.getSelfArgName(declaringClassName); + argName = variable.getSelfArgName(declaringClassName); } extraState.args.push({ - 'name': argName, - 'type': args[i].type, + name: argName, + type: args[i].type, }); // Check if we should plug a variable getter block into the argument input socket. - const input = Value.valueForFunctionArgInput(args[i].type, args[i].defaultValue); + const input = value.valueForFunctionArgInput(args[i].type, args[i].defaultValue); if (input) { inputs['ARG' + i] = input; } @@ -761,12 +791,12 @@ function processArgs( function createBlock( extraState: CallPythonFunctionExtraState, fields: {[key: string]: any}, - inputs: {[key: string]: any}): ToolboxItems.Block { - let block = new ToolboxItems.Block(BLOCK_NAME, extraState, fields, Object.keys(inputs).length ? inputs : null); - if (extraState.returnType && extraState.returnType != 'None') { - const varName = Variable.varNameForType(extraState.returnType); + inputs: {[key: string]: any}): toolboxItems.Block { + let block = new toolboxItems.Block(BLOCK_NAME, extraState, fields, Object.keys(inputs).length ? inputs : null); + if (extraState.returnType && extraState.returnType != RETURN_TYPE_NONE) { + const varName = variable.varNameForType(extraState.returnType); if (varName) { - block = Variable.createVariableSetterBlock(varName, block); + block = variable.createVariableSetterBlock(varName, block); } } return block; @@ -775,7 +805,7 @@ function createBlock( export function addModuleFunctionBlocks( moduleName: string, functions: FunctionData[], - contents: ToolboxItems.ContentsType[]) { + contents: toolboxItems.ContentsType[]) { functions.forEach(functionData => { const block = createModuleFunctionOrStaticMethodBlock( FunctionKind.MODULE, moduleName, moduleName, functionData); @@ -786,7 +816,7 @@ export function addModuleFunctionBlocks( export function addStaticMethodBlocks( importModule: string, functions: FunctionData[], - contents: ToolboxItems.ContentsType[]) { + contents: toolboxItems.ContentsType[]) { functions.forEach(functionData => { if (functionData.declaringClassName) { const block = createModuleFunctionOrStaticMethodBlock( @@ -800,7 +830,7 @@ function createModuleFunctionOrStaticMethodBlock( functionKind: FunctionKind, importModule: string, moduleOrClassName: string, - functionData: FunctionData): ToolboxItems.Block { + functionData: FunctionData): toolboxItems.Block { const extraState: CallPythonFunctionExtraState = { functionKind: functionKind, returnType: functionData.returnType, @@ -819,7 +849,7 @@ function createModuleFunctionOrStaticMethodBlock( export function addConstructorBlocks( importModule: string, functions: FunctionData[], - contents: ToolboxItems.ContentsType[]) { + contents: toolboxItems.ContentsType[]) { functions.forEach(functionData => { contents.push(createConstructorBlock(importModule, functionData)); }); @@ -827,7 +857,7 @@ export function addConstructorBlocks( function createConstructorBlock( importModule: string, - functionData: FunctionData): ToolboxItems.Block { + functionData: FunctionData): toolboxItems.Block { const extraState: CallPythonFunctionExtraState = { functionKind: FunctionKind.CONSTRUCTOR, returnType: functionData.returnType, @@ -844,14 +874,14 @@ function createConstructorBlock( export function addInstanceMethodBlocks( functions: FunctionData[], - contents: ToolboxItems.ContentsType[]) { + contents: toolboxItems.ContentsType[]) { functions.forEach(functionData => { contents.push(createInstanceMethodBlock(functionData)); }); } function createInstanceMethodBlock( - functionData: FunctionData): ToolboxItems.Block { + functionData: FunctionData): toolboxItems.Block { const extraState: CallPythonFunctionExtraState = { functionKind: FunctionKind.INSTANCE, returnType: functionData.returnType, @@ -862,32 +892,34 @@ function createInstanceMethodBlock( fields[FIELD_MODULE_OR_CLASS_NAME] = functionData.declaringClassName; fields[FIELD_FUNCTION_NAME] = functionData.functionName; const inputs: {[key: string]: any} = {}; + // We include the arg for the self argument for INSTANCE methods because we want a socket that + // the user can plug the object into. processArgs(functionData.args, extraState, inputs, functionData.declaringClassName); return createBlock(extraState, fields, inputs); } export function addInstanceWithinBlocks( - methods: CommonStorage.Method[], - contents: ToolboxItems.ContentsType[]) { + methods: commonStorage.Method[], + contents: toolboxItems.ContentsType[]) { methods.forEach(method => { contents.push(createInstanceWithinBlock(method)); }); } -function createInstanceWithinBlock(method: CommonStorage.Method): ToolboxItems.Block { +function createInstanceWithinBlock(method: commonStorage.Method): toolboxItems.Block { const extraState: CallPythonFunctionExtraState = { functionKind: FunctionKind.INSTANCE_WITHIN, returnType: method.returnType, actualFunctionName: method.pythonName, args: [], - classMethodDefBlockId: method.blockId, + otherBlockId: method.blockId, }; const fields: {[key: string]: any} = {}; fields[FIELD_FUNCTION_NAME] = method.visibleName; const inputs: {[key: string]: any} = {}; - // Convert method.args from CommonStorage.MethodArg[] to ArgData[]. + // Convert method.args from commonStorage.MethodArg[] to ArgData[]. const args: ArgData[] = []; - // We don't include the arg for the self argument. + // We don't include the arg for the self argument because we don't need a socket for it. for (let i = 1; i < method.args.length; i++) { args.push({ name: method.args[i].name, @@ -900,8 +932,8 @@ function createInstanceWithinBlock(method: CommonStorage.Method): ToolboxItems.B } export function getInstanceComponentBlocks( - component: CommonStorage.Component): ToolboxItems.ContentsType[] { - const contents: ToolboxItems.ContentsType[] = []; + component: commonStorage.Component): toolboxItems.ContentsType[] { + const contents: toolboxItems.ContentsType[] = []; const classData = getClassData(component.className); if (!classData) { @@ -928,7 +960,7 @@ export function getInstanceComponentBlocks( } function createInstanceComponentBlock( - component: CommonStorage.Component, functionData: FunctionData): ToolboxItems.Block { + component: commonStorage.Component, functionData: FunctionData): toolboxItems.Block { const extraState: CallPythonFunctionExtraState = { functionKind: FunctionKind.INSTANCE_COMPONENT, returnType: functionData.returnType, @@ -937,7 +969,7 @@ function createInstanceComponentBlock( importModule: '', componentClassName: component.className, componentName: component.name, - componentBlockId: component.blockId, + otherBlockId: component.blockId, }; const fields: {[key: string]: any} = {}; fields[FIELD_COMPONENT_NAME] = component.name; @@ -945,34 +977,34 @@ function createInstanceComponentBlock( const inputs: {[key: string]: any} = {}; // For INSTANCE_COMPONENT functions, the 0 argument is 'self', but // self is represented by the FIELD_COMPONENT_NAME field. - // We don't include the arg for self. + // We don't include the arg for the self argument because we don't need a socket for it. const argsWithoutSelf = functionData.args.slice(1); processArgs(argsWithoutSelf, extraState, inputs); return createBlock(extraState, fields, inputs); } export function addInstanceRobotBlocks( - methods: CommonStorage.Method[], - contents: ToolboxItems.ContentsType[]) { + methods: commonStorage.Method[], + contents: toolboxItems.ContentsType[]) { methods.forEach(method => { contents.push(createInstanceRobotBlock(method)); }); } -function createInstanceRobotBlock(method: CommonStorage.Method): ToolboxItems.Block { +function createInstanceRobotBlock(method: commonStorage.Method): toolboxItems.Block { const extraState: CallPythonFunctionExtraState = { functionKind: FunctionKind.INSTANCE_ROBOT, returnType: method.returnType, actualFunctionName: method.pythonName, args: [], - classMethodDefBlockId: method.blockId, + otherBlockId: method.blockId, }; const fields: {[key: string]: any} = {}; fields[FIELD_FUNCTION_NAME] = method.visibleName; const inputs: {[key: string]: any} = {}; - // Convert method.args from CommonStorage.MethodArg[] to ArgData[]. + // Convert method.args from commonStorage.MethodArg[] to ArgData[]. const args: ArgData[] = []; - // We don't include the arg for the self argument. + // We don't include the arg for the self argument because we don't need a socket for it. for (let i = 1; i < method.args.length; i++) { args.push({ name: method.args[i].name, @@ -983,3 +1015,34 @@ function createInstanceRobotBlock(method: CommonStorage.Method): ToolboxItems.Bl processArgs(args, extraState, inputs); return createBlock(extraState, fields, inputs); } + +export function addFireEventBlocks( + events: commonStorage.Event[], + contents: toolboxItems.ContentsType[]) { + events.forEach(event => { + contents.push(createFireEventBlock(event)); + }); +} + +function createFireEventBlock(event: commonStorage.Event): toolboxItems.Block { + const extraState: CallPythonFunctionExtraState = { + functionKind: FunctionKind.EVENT, + returnType: RETURN_TYPE_NONE, + args: [], + otherBlockId: event.blockId, + }; + const fields: {[key: string]: any} = {}; + fields[FIELD_EVENT_NAME] = event.name; + const inputs: {[key: string]: any} = {}; + // Convert event.args from commonStorage.MethodArg[] to ArgData[]. + const args: ArgData[] = []; + event.args.forEach(methodArg => { + args.push({ + name: methodArg.name, + type: methodArg.type, + defaultValue: '', + }); + }); + processArgs(args, extraState, inputs); + return createBlock(extraState, fields, inputs); +} diff --git a/src/blocks/mrc_class_method_def.ts b/src/blocks/mrc_class_method_def.ts index 14de03f5..979feb90 100644 --- a/src/blocks/mrc_class_method_def.ts +++ b/src/blocks/mrc_class_method_def.ts @@ -38,7 +38,7 @@ export const BLOCK_NAME = 'mrc_class_method_def'; const FIELD_METHOD_NAME = 'NAME'; -export type Parameter = { +type Parameter = { name: string, type?: string, }; @@ -51,12 +51,12 @@ interface ClassMethodDefMixin extends ClassMethodDefMixinType { mrcReturnType: string, mrcParameters: Parameter[], mrcPythonMethodName: string, - mrcMethod: commonStorage.Method | null, + mrcFuncName: string | null, } type ClassMethodDefMixinType = typeof CLASS_METHOD_DEF; /** Extra state for serialising call_python_* blocks. */ -export type ClassMethodDefExtraState = { +type ClassMethodDefExtraState = { /** * Can change name and parameters and return type */ @@ -137,10 +137,9 @@ const CLASS_METHOD_DEF = { 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 = []; - this.mrcMethod = null; - extraState.params.forEach((param) => { this.mrcParameters.push({ 'name': param.name, @@ -148,7 +147,6 @@ const CLASS_METHOD_DEF = { }); }); this.updateBlock_(); - mutateMethodCallers(this.workspace, this.getFieldValue(FIELD_METHOD_NAME), this.saveExtraState()); }, /** * Update the block to reflect the newly loaded extra state. @@ -194,7 +192,12 @@ const CLASS_METHOD_DEF = { paramBlock.nextConnection && paramBlock.nextConnection.targetBlock(); } this.mrcUpdateParams(); - mutateMethodCallers(this.workspace, this.getFieldValue(FIELD_METHOD_NAME), this.saveExtraState()); + if (this.mrcCanBeCalledWithinClass) { + const methodForWithin = this.getMethodForWithin(); + if (methodForWithin) { + mutateMethodCallers(this.workspace, this.id, methodForWithin); + } + } }, decompose: function (this: ClassMethodDefBlock, workspace: Blockly.Workspace) { // This is a special sub-block that only gets created in the mutator UI. @@ -259,15 +262,43 @@ const CLASS_METHOD_DEF = { const oldName = nameField.getValue(); if (oldName && oldName !== name && oldName !== legalName) { // Rename any callers. - renameMethodCallers(this.workspace, oldName, legalName); + renameMethodCallers(this.workspace, this.id, legalName); } return legalName; }, + getMethod: function (this: ClassMethodDefBlock): commonStorage.Method | null { + const method: commonStorage.Method = { + blockId: this.id, + 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): commonStorage.Method | null { - return this.mrcCanBeCalledWithinClass ? this.mrcMethod : null; + if (this.mrcCanBeCalledWithinClass) { + return this.getMethod(); + } + return null; }, getMethodForOutside: function (this: ClassMethodDefBlock): commonStorage.Method | null { - return this.mrcCanBeCalledOutsideClass ? this.mrcMethod : null; + if (this.mrcCanBeCalledOutsideClass) { + return this.getMethod(); + } + return null; }, canChangeSignature: function (this: ClassMethodDefBlock): boolean { return this.mrcCanChangeSignature; @@ -342,6 +373,7 @@ export const pythonFromBlock = function ( ) { 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); let xfix1 = ''; @@ -380,7 +412,8 @@ export const pythonFromBlock = function ( generator.defineClassVariables() + branch; } else if (generator.inBaseClassMethod(blocklyName)){ - // Special case for update, to also call the update method of the base class + // 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) { @@ -416,25 +449,9 @@ export const pythonFromBlock = function ( code = generator.scrub_(block, code); generator.addClassMethodDefinition(funcName, code); - if (block.mrcCanBeCalledWithinClass || block.mrcCanBeCalledOutsideClass) { - // Update the mrcMethod. - block.mrcMethod = { - blockId: block.id, - visibleName: block.getFieldValue(FIELD_METHOD_NAME), - pythonName: funcName, - returnType: block.mrcReturnType, - args: [{ - name: 'self', - type: '', - }], - }; - block.mrcParameters.forEach(param => { - block.mrcMethod!.args.push({ - name: param.name, - type: param.type ? param.type : '', - }); - }); - } + // Save the name of the function we just generated so we can use it to create the commonStorage.Method. + // in the getMethod function. + block.mrcFuncName = funcName; return ''; } diff --git a/src/blocks/mrc_component.ts b/src/blocks/mrc_component.ts index 5812ccea..0c19c032 100644 --- a/src/blocks/mrc_component.ts +++ b/src/blocks/mrc_component.ts @@ -26,8 +26,8 @@ import { MRC_STYLE_COMPONENTS } from '../themes/styles' import { createFieldNonEditableText } from '../fields/FieldNonEditableText'; import { ExtendedPythonGenerator } from '../editor/extended_python_generator'; import { getAllowedTypesForSetCheck, getClassData, getModuleData, getSubclassNames } from './utils/python'; -import * as ToolboxItems from '../toolbox/items'; -import * as CommonStorage from '../storage/common_storage'; +import * as toolboxItems from '../toolbox/items'; +import * as commonStorage from '../storage/common_storage'; import { createPortShadow } from './mrc_port'; import { createNumberShadowValue } from './utils/value'; import { ClassData, FunctionData } from './utils/python_json_types'; @@ -52,7 +52,7 @@ type ComponentExtraState = { params?: ConstructorArg[], } -type ComponentBlock = Blockly.Block & ComponentMixin; +export type ComponentBlock = Blockly.Block & ComponentMixin; interface ComponentMixin extends ComponentMixinType { mrcArgs: ConstructorArg[], hideParams: boolean, @@ -136,7 +136,16 @@ const COMPONENT = { } } } - } + }, + getComponent: function (this: ComponentBlock): commonStorage.Component | null { + const componentName = this.getFieldValue(FIELD_NAME); + const componentType = this.getFieldValue(FIELD_TYPE); + return { + blockId: this.id, + name: componentName, + className: componentType, + }; + }, } export const setup = function () { @@ -177,8 +186,8 @@ export const pythonFromBlock = function ( return code; } -export function getAllPossibleComponents(hideParams: boolean): ToolboxItems.ContentsType[] { - const contents: ToolboxItems.ContentsType[] = []; +export function getAllPossibleComponents(hideParams: boolean): toolboxItems.ContentsType[] { + const contents: toolboxItems.ContentsType[] = []; // Iterate through all the components subclasses and add definition blocks. const componentTypes = getSubclassNames('component.Component'); @@ -190,7 +199,7 @@ export function getAllPossibleComponents(hideParams: boolean): ToolboxItems.Cont const componentName = ( 'my_' + - CommonStorage.pascalCaseToSnakeCase( + commonStorage.pascalCaseToSnakeCase( componentType.substring(componentType.lastIndexOf('.') + 1))); classData.staticMethods.forEach(staticFunctionData => { @@ -204,7 +213,7 @@ export function getAllPossibleComponents(hideParams: boolean): ToolboxItems.Cont } function createComponentBlock( - componentName: string, classData: ClassData, staticFunctionData: FunctionData, hideParams: boolean): ToolboxItems.Block { + componentName: string, classData: ClassData, staticFunctionData: FunctionData, hideParams: boolean): toolboxItems.Block { const extraState: ComponentExtraState = { importModule: classData.moduleName, staticFunctionName: staticFunctionData.functionName, @@ -232,7 +241,7 @@ function createComponentBlock( } } } - return new ToolboxItems.Block(BLOCK_NAME, extraState, fields, Object.keys(inputs).length ? inputs : null); + return new toolboxItems.Block(BLOCK_NAME, extraState, fields, Object.keys(inputs).length ? inputs : null); } function getPortTypeForArgument(argName: string): string | null { diff --git a/src/blocks/mrc_event.ts b/src/blocks/mrc_event.ts index c16c78c3..423b4582 100644 --- a/src/blocks/mrc_event.ts +++ b/src/blocks/mrc_event.ts @@ -20,7 +20,6 @@ * @author alan@porpoiseful.com (Alan Smith) */ import * as Blockly from 'blockly'; -import { Order } from 'blockly/python'; import { MRC_STYLE_EVENTS } from '../themes/styles' import { ExtendedPythonGenerator } from '../editor/extended_python_generator'; @@ -28,13 +27,15 @@ import { MUTATOR_BLOCK_NAME, PARAM_CONTAINER_BLOCK_NAME, MethodMutatorArgBlock } import * as ChangeFramework from './utils/change_framework'; import { BLOCK_NAME as MRC_MECHANISM_COMPONENT_HOLDER } from './mrc_mechanism_component_holder'; import * as toolboxItems from '../toolbox/items'; +import * as commonStorage from '../storage/common_storage'; +import { renameMethodCallers, mutateMethodCallers } from './mrc_call_python_function' export const BLOCK_NAME = 'mrc_event'; export const OUTPUT_NAME = 'mrc_event'; const FIELD_EVENT_NAME = 'NAME'; -export type Parameter = { +type Parameter = { name: string, type?: string, }; @@ -112,7 +113,7 @@ const EVENT = { const nameField = new Blockly.FieldTextInput(name); input.insertFieldAt(0, nameField, FIELD_EVENT_NAME); this.setMutator(new Blockly.icons.MutatorIcon([MUTATOR_BLOCK_NAME], this)); - // nameField.setValidator(this.mrcNameFieldValidator.bind(this, nameField)); + nameField.setValidator(this.mrcNameFieldValidator.bind(this, nameField)); this.mrcUpdateParams(); }, @@ -135,7 +136,7 @@ const EVENT = { paramBlock.nextConnection && paramBlock.nextConnection.targetBlock(); } this.mrcUpdateParams(); - //mutateMethodCallers(this.workspace, this.getFieldValue(FIELD_EVENT_NAME), this.saveExtraState()); + mutateMethodCallers(this.workspace, this.id, this.getEvent()); }, decompose: function (this: EventBlock, workspace: Blockly.Workspace) { // This is a special sub-block that only gets created in the mutator UI. @@ -180,6 +181,18 @@ const EVENT = { input.removeField(fieldName); }); }, + mrcNameFieldValidator(this: EventBlock, nameField: Blockly.FieldTextInput, name: string): string { + // Strip leading and trailing whitespace. + name = name.trim(); + + const legalName = name; + const oldName = nameField.getValue(); + if (oldName && oldName !== name && oldName !== legalName) { + // Rename any callers. + renameMethodCallers(this.workspace, this.id, legalName); + } + return legalName; + }, onBlockChanged(block: Blockly.BlockSvg, blockEvent: Blockly.Events.BlockBase): void { const blockBlock = block as Blockly.Block; @@ -193,9 +206,23 @@ const EVENT = { } // If we end up here it shouldn't be allowed block.unplug(true); - blockBlock.setWarningText('Events can only go in the events block'); + blockBlock.setWarningText('Events can only go in the events section of the robot or mechanism'); } }, + getEvent: function (this: EventBlock): commonStorage.Event { + const event: commonStorage.Event = { + blockId: this.id, + name: this.getFieldValue(FIELD_EVENT_NAME), + args: [], + }; + this.mrcParameters.forEach(param => { + event.args.push({ + name: param.name, + type: param.type ? param.type : '', + }); + }); + return event; + }, } export const setup = function () { @@ -204,9 +231,8 @@ export const setup = function () { export const pythonFromBlock = function ( block: EventBlock, - generator: ExtendedPythonGenerator, -) { - //TODO (Alan): What should this do here?? + generator: ExtendedPythonGenerator) { + // TODO (Alan): What should this do here?? return ''; } diff --git a/src/blocks/mrc_mechanism_component_holder.ts b/src/blocks/mrc_mechanism_component_holder.ts index 7e3cf584..310fd49c 100644 --- a/src/blocks/mrc_mechanism_component_holder.ts +++ b/src/blocks/mrc_mechanism_component_holder.ts @@ -29,10 +29,11 @@ import * as commonStorage from '../storage/common_storage'; import { OUTPUT_NAME as MECHANISM_OUTPUT } from './mrc_mechanism'; import { BLOCK_NAME as MRC_MECHANISM_NAME } from './mrc_mechanism'; import { BLOCK_NAME as MRC_COMPONENT_NAME } from './mrc_component'; -import * as Component from './mrc_component'; import { OUTPUT_NAME as COMPONENT_OUTPUT } from './mrc_component'; +import { ComponentBlock } from './mrc_component'; import { BLOCK_NAME as MRC_EVENT_NAME } from './mrc_event'; import { OUTPUT_NAME as EVENT_OUTPUT } from './mrc_event'; +import { EventBlock } from './mrc_event'; export const BLOCK_NAME = 'mrc_mechanism_component_holder'; @@ -139,15 +140,9 @@ const MECHANISM_COMPONENT_HOLDER = { let componentBlock = componentsInput.connection.targetBlock(); while (componentBlock) { if (componentBlock.type === MRC_COMPONENT_NAME) { - const componentName = componentBlock.getFieldValue(Component.FIELD_NAME); - const componentType = componentBlock.getFieldValue(Component.FIELD_TYPE); - - if (componentName && componentType) { - components.push({ - blockId: componentBlock.id, - name: componentName, - className: componentType, - }); + const component = (componentBlock as ComponentBlock).getComponent(); + if (component) { + components.push(component); } } // Move to the next block in the chain @@ -157,6 +152,28 @@ const MECHANISM_COMPONENT_HOLDER = { return components; }, + getEvents: function (this: MechanismComponentHolderBlock): commonStorage.Event[] { + const events: commonStorage.Event[] = [] + + // Get event blocks from the EVENTS input + const eventsInput = this.getInput('EVENTS'); + if (eventsInput && eventsInput.connection) { + // Walk through all connected event blocks. + let eventBlock = eventsInput.connection.targetBlock(); + while (eventBlock) { + if (eventBlock.type === MRC_EVENT_NAME) { + const event = (eventBlock as EventBlock).getEvent(); + if (event) { + events.push(event); + } + } + // Move to the next block in the chain + eventBlock = eventBlock.getNextBlock(); + } + } + + return events; + }, } let toolboxUpdateTimeout: NodeJS.Timeout | null = null; diff --git a/src/editor/editor.ts b/src/editor/editor.ts index b581e912..b790dab9 100644 --- a/src/editor/editor.ts +++ b/src/editor/editor.ts @@ -225,9 +225,7 @@ export class Editor { holderBlocks.forEach(holderBlock => { const componentsFromHolder: commonStorage.Component[] = (holderBlock as mechanismComponentHolder.MechanismComponentHolderBlock).getComponents(); - componentsFromHolder.forEach(component => { - components.push(component); - }); + components.push(...componentsFromHolder); }); } return components; @@ -280,6 +278,21 @@ export class Editor { return methodNamesAlreadyOverridden; } + public getEventsFromWorkspace(): commonStorage.Event[] { + const events: commonStorage.Event[] = []; + if (this.currentModule?.moduleType === commonStorage.MODULE_TYPE_ROBOT || + this.currentModule?.moduleType === commonStorage.MODULE_TYPE_MECHANISM) { + // Get the holder block and ask it for the components. + const holderBlocks = this.blocklyWorkspace.getBlocksByType(mechanismComponentHolder.BLOCK_NAME); + holderBlocks.forEach(holderBlock => { + const eventsFromHolder: commonStorage.Event[] = + (holderBlock as mechanismComponentHolder.MechanismComponentHolderBlock).getEvents(); + events.push(...eventsFromHolder); + }); + } + return events; + } + public async saveBlocks() { const moduleContent = this.getModuleContent(); try { diff --git a/src/editor/extended_python_generator.ts b/src/editor/extended_python_generator.ts index 1bb16c3e..207bd553 100644 --- a/src/editor/extended_python_generator.ts +++ b/src/editor/extended_python_generator.ts @@ -249,6 +249,7 @@ export class ExtendedPythonGenerator extends PythonGenerator { * @returns list of method names */ getBaseClassMethods() : string[] { + // TODO(lizlooney): the names of base class methods should not be hard coded. let classParent = this.context?.getClassParent(); if (classParent == 'OpMode'){ return ['start', 'loop', 'stop']; diff --git a/src/storage/common_storage.ts b/src/storage/common_storage.ts index 8650fa5c..a6729d1d 100644 --- a/src/storage/common_storage.ts +++ b/src/storage/common_storage.ts @@ -72,6 +72,12 @@ export type Component = { className: string, } +export type Event = { + blockId: string, // ID of the mrc_event block that defines the event. + name: string, + args: MethodArg[], +}; + export const MODULE_TYPE_UNKNOWN = 'unknown'; export const MODULE_TYPE_ROBOT = 'robot'; export const MODULE_TYPE_MECHANISM = 'mechanism'; diff --git a/src/toolbox/event_category.ts b/src/toolbox/event_category.ts index f388e17e..c2deea18 100644 --- a/src/toolbox/event_category.ts +++ b/src/toolbox/event_category.ts @@ -23,10 +23,10 @@ import * as Blockly from 'blockly/core'; import * as toolboxItems from './items'; import * as commonStorage from '../storage/common_storage'; -import { EventBlock } from '../blocks/mrc_event'; import { MRC_CATEGORY_STYLE_METHODS } from '../themes/styles'; -import { RETURN_TYPE_NONE, FunctionKind } from '../blocks/mrc_call_python_function'; import { createCustomEventBlock } from '../blocks/mrc_event'; +import { addFireEventBlocks } from '../blocks/mrc_call_python_function'; +import { Editor } from '../editor/editor'; const CUSTOM_CATEGORY_EVENTS = 'EVENTS'; @@ -57,38 +57,15 @@ export class EventsCategory { kind: 'label', text: 'Custom Events', }, - createCustomEventBlock()); + createCustomEventBlock() + ); - // For each mrc_class_method_def block in the blockly workspace, check if it - // can be called from within the class, and if so, add a - // mrc_call_python_function block. - workspace.getBlocksByType('mrc_event', false).forEach((block) => { - const eventBlock = block as EventBlock; - const callPythonFunctionBlock: toolboxItems.Block = { - kind: 'block', - type: 'mrc_call_python_function', - extraState: { - functionKind: FunctionKind.EVENT, - returnType: RETURN_TYPE_NONE, - args: [], - importModule: '', - }, - fields: { - FUNC: block.getFieldValue('NAME'), - }, - }; - // Add the parameters from the event block to the callPythonFunctionBlock. - eventBlock.mrcParameters.forEach((param) => { - if (callPythonFunctionBlock.extraState) { - callPythonFunctionBlock.extraState.args.push( - { - name: param.name, - type: param.type ?? '', - }); - } - }); - contents.push(callPythonFunctionBlock); - }); + // Get blocks for firing methods defined in the current workspace. + const editor = Editor.getEditorForBlocklyWorkspace(workspace); + if (editor) { + const eventsFromWorkspace = editor.getEventsFromWorkspace(); + addFireEventBlocks(eventsFromWorkspace, contents); + } const toolboxInfo = { contents: contents, diff --git a/src/toolbox/methods_category.ts b/src/toolbox/methods_category.ts index d39c4487..8da4fbcb 100644 --- a/src/toolbox/methods_category.ts +++ b/src/toolbox/methods_category.ts @@ -23,8 +23,8 @@ import * as Blockly from 'blockly/core'; import * as toolboxItems from './items'; import * as commonStorage from '../storage/common_storage'; -import { MRC_CATEGORY_STYLE_METHODS } from '../themes/styles' -import { addInstanceWithinBlocks } from '../blocks/mrc_call_python_function' +import { MRC_CATEGORY_STYLE_METHODS } from '../themes/styles'; +import { addInstanceWithinBlocks } from '../blocks/mrc_call_python_function'; import { createCustomMethodBlock, getBaseClassBlocks } from '../blocks/mrc_class_method_def'; import { Editor } from '../editor/editor';