diff --git a/src/App.tsx b/src/App.tsx index eeb14e9e..133367b6 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -46,7 +46,6 @@ import * as CustomBlocks from './blocks/setup_custom_blocks'; import { initialize as initializePythonBlocks } from './blocks/utils/python'; import { registerToolboxButton } from './blocks/mrc_event_handler' -import { mutatorOpenListener } from './blocks/mrc_param_container' import { TOOLBOX_UPDATE_EVENT } from './blocks/mrc_mechanism_component_holder'; import { antdThemeFromString } from './reactComponents/ThemeModal'; import { useTranslation } from 'react-i18next'; @@ -486,7 +485,6 @@ const AppContent: React.FC = ({ project, setProject }): React.J return; } - newWorkspace.addChangeListener(mutatorOpenListener); newWorkspace.addChangeListener(handleBlocksChanged); registerToolboxButton(newWorkspace, messageApi); diff --git a/src/blocks/mrc_class_method_def.ts b/src/blocks/mrc_class_method_def.ts index 65660135..0c6af826 100644 --- a/src/blocks/mrc_class_method_def.ts +++ b/src/blocks/mrc_class_method_def.ts @@ -33,12 +33,15 @@ import { getClassData } from './utils/python'; import { FunctionData } from './utils/python_json_types'; import { findConnectedBlocksOfType } from './utils/find_connected_blocks'; import { BLOCK_NAME as MRC_GET_PARAMETER_BLOCK_NAME } from './mrc_get_parameter'; -import { MUTATOR_BLOCK_NAME, PARAM_CONTAINER_BLOCK_NAME, MethodMutatorArgBlock } from './mrc_param_container' +import * as paramContainer from './mrc_param_container' export const BLOCK_NAME = 'mrc_class_method_def'; +const INPUT_TITLE = 'TITLE'; export const FIELD_METHOD_NAME = 'NAME'; -export const RETURN_VALUE = 'RETURN'; +const FIELD_PARAM_PREFIX = 'PARAM_'; +const INPUT_STACK = 'STACK'; +export const INPUT_RETURN = 'RETURN'; export interface Parameter { name: string; @@ -100,12 +103,12 @@ const CLASS_METHOD_DEF = { * Block initialization. */ init: function (this: ClassMethodDefBlock): void { - this.appendDummyInput("TITLE") + this.mrcParameters = []; + this.appendDummyInput(INPUT_TITLE) .appendField('', FIELD_METHOD_NAME); this.setOutput(false); this.setStyle(MRC_STYLE_FUNCTIONS); - this.appendStatementInput('STACK').appendField(''); - this.mrcParameters = []; + this.appendStatementInput(INPUT_STACK).appendField(''); this.setPreviousStatement(false); this.setNextStatement(false); this.updateBlock_(); @@ -159,7 +162,7 @@ const CLASS_METHOD_DEF = { */ updateBlock_: function (this: ClassMethodDefBlock): void { const name = this.getFieldValue(FIELD_METHOD_NAME); - const input = this.getInput('TITLE'); + const input = this.getInput(INPUT_TITLE); if (!input) { return; } @@ -168,7 +171,7 @@ const CLASS_METHOD_DEF = { 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)); + this.setMutator(paramContainer.getMutatorIcon(this)); nameField.setValidator(this.mrcNameFieldValidator.bind(this, nameField)); } else { input.insertFieldAt(0, createFieldNonEditableText(name), FIELD_METHOD_NAME); @@ -178,24 +181,26 @@ const CLASS_METHOD_DEF = { this.mrcUpdateParams(); this.mrcUpdateReturnInput(); }, - compose: function (this: ClassMethodDefBlock, containerBlock: any) { - // Parameter list. + compose: function (this: ClassMethodDefBlock, containerBlock: Blockly.Block) { + if (containerBlock.type !== paramContainer.PARAM_CONTAINER_BLOCK_NAME) { + throw new Error('compose: containerBlock.type should be ' + paramContainer.PARAM_CONTAINER_BLOCK_NAME); + } + const paramContainerBlock = containerBlock as paramContainer.ParamContainerBlock; + const paramItemBlocks: paramContainer.ParamItemBlock[] = paramContainerBlock.getParamItemBlocks(); + this.mrcParameters = []; - let paramBlock = containerBlock.getInputTargetBlock('STACK'); - while (paramBlock) { + paramItemBlocks.forEach(paramItemBlock => { + const itemName = paramItemBlock.getName(); const param: Parameter = { - name: paramBlock.getFieldValue('NAME'), + name: itemName, 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.mrcRenameParameter(paramItemBlock.getOriginalName(), itemName); + paramItemBlock.setOriginalName(itemName); this.mrcParameters.push(param); - paramBlock = paramBlock.nextConnection && paramBlock.nextConnection.targetBlock(); - } + }); + this.mrcUpdateParams(); if (this.mrcCanBeCalledWithinClass) { const methodForWithin = this.getMethodForWithin(); @@ -205,29 +210,24 @@ const CLASS_METHOD_DEF = { } }, 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; + const parameterNames: string[] = []; + this.mrcParameters.forEach(parameter => { + parameterNames.push(parameter.name); + }); + return paramContainer.createMutatorBlocks(workspace, parameterNames); + }, + /** + * mrcOnMutatorOpen is called when the mutator on a ClassMethodDefBlock is opened. + */ + mrcOnMutatorOpen: function(this: ClassMethodDefBlock): void { + paramContainer.onMutatorOpen(this); }, mrcRenameParameter: function (this: ClassMethodDefBlock, oldName: string, newName: string) { - const nextBlock = this.getInputTargetBlock('STACK'); + const nextBlock = this.getInputTargetBlock(INPUT_STACK); if (nextBlock) { findConnectedBlocksOfType(nextBlock, MRC_GET_PARAMETER_BLOCK_NAME).forEach((block) => { + // TODO(lizlooney): add methods getParameterName and setParameterName to GetParameterBlock + // in mrc_get_parameter.ts and call them here. if (block.getFieldValue('PARAMETER_NAME') === oldName) { block.setFieldValue(newName, 'PARAMETER_NAME'); } @@ -236,11 +236,11 @@ const CLASS_METHOD_DEF = { }, mrcUpdateParams: function (this: ClassMethodDefBlock) { if (this.mrcParameters.length > 0) { - const input = this.getInput('TITLE'); + const input = this.getInput(INPUT_TITLE); if (input) { this.removeParameterFields(input); this.mrcParameters.forEach((param) => { - const paramName = 'PARAM_' + param.name; + const paramName = FIELD_PARAM_PREFIX + param.name; input.appendField(createFieldFlydown(param.name, false), paramName); }); } @@ -248,22 +248,22 @@ const CLASS_METHOD_DEF = { }, mrcUpdateReturnInput: function (this: ClassMethodDefBlock) { // Remove existing return input if it exists - if (this.getInput(RETURN_VALUE)) { - this.removeInput(RETURN_VALUE); + if (this.getInput(INPUT_RETURN)) { + this.removeInput(INPUT_RETURN); } // Add return input if return type is not 'None' if (this.mrcReturnType && this.mrcReturnType !== 'None') { - this.appendValueInput(RETURN_VALUE) + this.appendValueInput(INPUT_RETURN) .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); + this.moveInputBefore(INPUT_STACK, INPUT_RETURN); } }, removeParameterFields: function (input: Blockly.Input) { const fieldsToRemove = input.fieldRow - .filter(field => field.name?.startsWith('PARAM_')) + .filter(field => field.name?.startsWith(FIELD_PARAM_PREFIX)) .map(field => field.name!); fieldsToRemove.forEach(fieldName => { @@ -436,12 +436,12 @@ export const pythonFromBlock = function ( ); } let branch = ''; - if (block.getInput('STACK')) { - branch = generator.statementToCode(block, 'STACK'); + if (block.getInput(INPUT_STACK)) { + branch = generator.statementToCode(block, INPUT_STACK); } let returnValue = ''; - if (block.getInput('RETURN')) { - returnValue = generator.valueToCode(block, 'RETURN', Order.NONE) || ''; + if (block.getInput(INPUT_RETURN)) { + returnValue = generator.valueToCode(block, INPUT_RETURN, Order.NONE) || ''; } let xfix2 = ''; if (branch && returnValue) { @@ -465,7 +465,7 @@ export const pythonFromBlock = function ( } const params = block.mrcParameters; - let paramString = "self"; + let paramString = 'self'; if (generator.getModuleType() === storageModule.ModuleType.MECHANISM && block.mrcPythonMethodName === '__init__') { const ports: string[] = generator.getComponentPortParameters(); if (ports.length) { @@ -527,7 +527,7 @@ export function createCustomMethodBlockWithReturn(): toolboxItems.Block { const fields: {[key: string]: any} = {}; fields[FIELD_METHOD_NAME] = 'my_method_with_return'; const inputs: {[key: string]: any} = {}; - inputs[RETURN_VALUE] = { + inputs[INPUT_RETURN] = { type: 'input_value', }; return new toolboxItems.Block(BLOCK_NAME, extraState, fields, inputs); diff --git a/src/blocks/mrc_event.ts b/src/blocks/mrc_event.ts index 1760120b..0fc07f94 100644 --- a/src/blocks/mrc_event.ts +++ b/src/blocks/mrc_event.ts @@ -24,7 +24,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 * as paramContainer from './mrc_param_container' import { BLOCK_NAME as MRC_MECHANISM_COMPONENT_HOLDER, MechanismComponentHolderBlock, @@ -70,12 +70,13 @@ const EVENT = { */ init: function (this: EventBlock): void { this.mrcHasNotInHolderWarning = false; + this.mrcParameters = []; this.setStyle(MRC_STYLE_EVENTS); this.appendDummyInput(INPUT_TITLE) - .appendField(new Blockly.FieldTextInput('my_event'), FIELD_EVENT_NAME); + .appendField(new Blockly.FieldTextInput(''), FIELD_EVENT_NAME); this.setPreviousStatement(true, OUTPUT_NAME); this.setNextStatement(true, OUTPUT_NAME); - this.setMutator(new Blockly.icons.MutatorIcon([MUTATOR_BLOCK_NAME], this)); + this.updateBlock_(); }, /** @@ -125,50 +126,37 @@ 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)); + this.setMutator(paramContainer.getMutatorIcon(this)); nameField.setValidator(this.mrcNameFieldValidator.bind(this, nameField)); this.mrcUpdateParams(); }, - compose: function (this: EventBlock, containerBlock: any) { - // Parameter list. + compose: function (this: EventBlock, containerBlock: Blockly.Block) { + if (containerBlock.type !== paramContainer.PARAM_CONTAINER_BLOCK_NAME) { + throw new Error('compose: containerBlock.type should be ' + paramContainer.PARAM_CONTAINER_BLOCK_NAME); + } this.mrcParameters = []; - let paramBlock = containerBlock.getInputTargetBlock('STACK'); - while (paramBlock) { + const paramContainerBlock = containerBlock as paramContainer.ParamContainerBlock; + paramContainerBlock.getParamItemBlocks().forEach(paramItemBlock => { + const itemName = paramItemBlock.getName(); const param: Parameter = { - name: paramBlock.getFieldValue('NAME'), + name: itemName, type: '' - } - if (paramBlock.originalName) { - // This is a mutator arg block, so we can get the original name. - paramBlock.originalName = param.name; - } + }; + paramItemBlock.setOriginalName(itemName); this.mrcParameters.push(param); - paramBlock = paramBlock.nextConnection && paramBlock.nextConnection.targetBlock(); - } + }); + this.mrcUpdateParams(); mutateMethodCallers(this.workspace, this.mrcEventId, this.getEvent()); }, decompose: function (this: EventBlock, 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; + const parameterNames: string[] = []; + this.mrcParameters.forEach(parameter => { + parameterNames.push(parameter.name); + }); + return paramContainer.createMutatorBlocks(workspace, parameterNames); }, mrcUpdateParams: function (this: EventBlock) { if (this.mrcParameters.length > 0) { @@ -224,6 +212,12 @@ const EVENT = { } mrcDescendantsMayHaveChanged(this.workspace); }, + /** + * mrcOnMutatorOpen is called when the mutator on an EventBlock is opened. + */ + mrcOnMutatorOpen: function(this: EventBlock): void { + paramContainer.onMutatorOpen(this); + }, checkBlockIsInHolder: function(this: EventBlock): void { const rootBlock: Blockly.Block | null = this.getRootBlock(); if (rootBlock && rootBlock.type === MRC_MECHANISM_COMPONENT_HOLDER) { diff --git a/src/blocks/mrc_param_container.ts b/src/blocks/mrc_param_container.ts index a53cae97..ed3de194 100644 --- a/src/blocks/mrc_param_container.ts +++ b/src/blocks/mrc_param_container.ts @@ -20,137 +20,174 @@ * @author alan@porpoiseful.com (Alan Smith) */ import * as Blockly from 'blockly'; -import * as ChangeFramework from './utils/change_framework' import { MRC_STYLE_CLASS_BLOCKS } from '../themes/styles'; -import { BLOCK_NAME as MRC_CLASS_METHOD_DEF } from './mrc_class_method_def'; -import { BLOCK_NAME as MRC_EVENT } from './mrc_event'; - -export const MUTATOR_BLOCK_NAME = 'methods_mutatorarg'; -export const PARAM_CONTAINER_BLOCK_NAME = 'method_param_container'; import { getLegalName } from './utils/python'; +export const PARAM_CONTAINER_BLOCK_NAME = 'mrc_param_container'; +const PARAM_ITEM_BLOCK_NAME = 'mrc_param_item'; + export const setup = function () { - Blockly.Blocks[MUTATOR_BLOCK_NAME] = METHODS_MUTATORARG; - Blockly.Blocks[PARAM_CONTAINER_BLOCK_NAME] = METHOD_PARAM_CONTAINER; + Blockly.Blocks[PARAM_CONTAINER_BLOCK_NAME] = PARAM_CONTAINER; + Blockly.Blocks[PARAM_ITEM_BLOCK_NAME] = PARAM_ITEM; }; -const METHOD_PARAM_CONTAINER = { - init: function (this: Blockly.Block) { - this.appendDummyInput("TITLE").appendField('Parameters'); - this.appendStatementInput('STACK'); - this.setStyle(MRC_STYLE_CLASS_BLOCKS); - this.contextMenu = false; - }, -}; +// The parameter container block. -export type MethodMutatorArgBlock = Blockly.Block & MethodMutatorArgMixin & Blockly.BlockSvg; -interface MethodMutatorArgMixin extends MethodMutatorArgMixinType { - originalName: string, -} +const INPUT_STACK = 'STACK'; + +export type ParamContainerBlock = ParamContainerMixin & Blockly.BlockSvg; +interface ParamContainerMixin extends ParamContainerMixinType {} +type ParamContainerMixinType = typeof PARAM_CONTAINER; -type MethodMutatorArgMixinType = typeof METHODS_MUTATORARG; - -function setName(block: Blockly.BlockSvg) { - const parentBlock = ChangeFramework.getParentOfType(block, PARAM_CONTAINER_BLOCK_NAME); - if (parentBlock) { - const variableBlocks = parentBlock!.getDescendants(true) - const otherNames: string[] = [] - variableBlocks?.forEach(function (variableBlock) { - if (variableBlock != block) { - otherNames.push(variableBlock.getFieldValue('NAME')); - } - }); - const currentName = block.getFieldValue('NAME'); - block.setFieldValue(getLegalName(currentName, otherNames), 'NAME'); - updateMutatorFlyout(block.workspace); +const PARAM_CONTAINER = { + init: function (this: ParamContainerBlock) { + this.appendDummyInput().appendField(Blockly.Msg.PARAMETERS); + this.appendStatementInput(INPUT_STACK); + this.setStyle(MRC_STYLE_CLASS_BLOCKS); + this.contextMenu = false; + }, + getParamItemBlocks: function (this: ParamContainerBlock): ParamItemBlock[] { + const paramItemBlocks: ParamItemBlock[] = []; + let block = this.getInputTargetBlock(INPUT_STACK); + while (block && !block.isInsertionMarker()) { + if (block.type !== PARAM_ITEM_BLOCK_NAME) { + throw new Error('getItemNames: block.type should be ' + PARAM_ITEM_BLOCK_NAME); + } + paramItemBlocks.push(block as ParamItemBlock); + block = block.nextConnection && block.nextConnection.targetBlock(); } + return paramItemBlocks; + }, +}; + +// The parameter item block. + +const FIELD_NAME = 'NAME'; + +export type ParamItemBlock = ParamItemMixin & Blockly.BlockSvg; +interface ParamItemMixin extends ParamItemMixinType { + originalName: string, } -const METHODS_MUTATORARG = { - init: function (this: MethodMutatorArgBlock) { - this.appendDummyInput() - .appendField(new Blockly.FieldTextInput(Blockly.Procedures.DEFAULT_ARG), 'NAME'); - this.setPreviousStatement(true); - this.setNextStatement(true); - this.setStyle(MRC_STYLE_CLASS_BLOCKS); - this.originalName = ''; - this.contextMenu = false; - ChangeFramework.registerCallback(MUTATOR_BLOCK_NAME, [Blockly.Events.BLOCK_MOVE, Blockly.Events.BLOCK_CHANGE], this.onBlockChanged); - }, - onBlockChanged: function (block: Blockly.BlockSvg, blockEvent: Blockly.Events.BlockBase) { - if (blockEvent.type == Blockly.Events.BLOCK_MOVE) { - let blockMoveEvent = blockEvent as Blockly.Events.BlockMove; - if (blockMoveEvent.reason?.includes('connect')) { - setName(block); - } - } - else { - if (blockEvent.type == Blockly.Events.BLOCK_CHANGE) { - setName(block); - } +type ParamItemMixinType = typeof PARAM_ITEM; + +const PARAM_ITEM = { + init: function (this: ParamItemBlock) { + this.appendDummyInput() + .appendField(new Blockly.FieldTextInput(''), FIELD_NAME); + this.setPreviousStatement(true); + this.setNextStatement(true); + this.setStyle(MRC_STYLE_CLASS_BLOCKS); + this.originalName = ''; + this.contextMenu = false; + }, + makeNameLegal: function (this: ParamItemBlock): void { + const rootBlock: Blockly.Block | null = this.getRootBlock(); + if (rootBlock) { + const otherNames: string[] = [] + rootBlock!.getDescendants(true)?.forEach(itemBlock => { + if (itemBlock != this) { + otherNames.push(itemBlock.getFieldValue(FIELD_NAME)); } - }, + }); + const currentName = this.getFieldValue(FIELD_NAME); + this.setFieldValue(getLegalName(currentName, otherNames), FIELD_NAME); + updateMutatorFlyout(this.workspace); + } + }, + getName: function (this: ParamItemBlock): string { + return this.getFieldValue(FIELD_NAME); + }, + getOriginalName: function (this: ParamItemBlock): string { + return this.originalName; + }, + setOriginalName: function (this: ParamItemBlock, originalName: string): void { + this.originalName = originalName; + }, } - /** - * Updates the procedure mutator's flyout so that the arg block is not a - * duplicate of another arg. + * Updates the mutator's flyout so that it contains a single param item block + * whose name is not a duplicate of an existing param item. * - * @param workspace The procedure mutator's workspace. This workspace's flyout - * is what is being updated. + * @param workspace The mutator's workspace. This workspace's flyout is what is being updated. */ function updateMutatorFlyout(workspace: Blockly.WorkspaceSvg) { - const usedNames = []; - const blocks = workspace.getBlocksByType(MUTATOR_BLOCK_NAME, false); - for (let i = 0, block; (block = blocks[i]); i++) { - usedNames.push(block.getFieldValue('NAME')); - } - const argValue = Blockly.Variables.generateUniqueNameFromOptions( - Blockly.Procedures.DEFAULT_ARG, - usedNames, - ); - const jsonBlock = { - kind: 'block', - type: MUTATOR_BLOCK_NAME, - fields: { - NAME: argValue, - }, - }; - - workspace.updateToolbox({ contents: [jsonBlock] }); + const usedNames: string[] = []; + workspace.getBlocksByType(PARAM_ITEM_BLOCK_NAME, false).forEach(block => { + usedNames.push(block.getFieldValue(FIELD_NAME)); + }); + const uniqueName = Blockly.Variables.generateUniqueNameFromOptions( + Blockly.Procedures.DEFAULT_ARG, usedNames); + const jsonBlock = { + kind: 'block', + type: PARAM_ITEM_BLOCK_NAME, + fields: { + NAME: uniqueName, + }, + }; + + workspace.updateToolbox({ contents: [jsonBlock] }); } /** - * Listens for when a procedure mutator is opened. Then it triggers a flyout - * update and adds a mutator change listener to the mutator workspace. - * - * @param e The event that triggered this listener. - * @internal + * The blockly event listener function for the mutator's workspace. */ -export function mutatorOpenListener(e: Blockly.Events.Abstract) { - if (e.type != Blockly.Events.BUBBLE_OPEN) { - return; +function onChange(mutatorWorkspace: Blockly.Workspace, event: Blockly.Events.Abstract) { + if (event.type === Blockly.Events.BLOCK_MOVE) { + const blockMoveEvent = event as Blockly.Events.BlockMove; + const reason: string[] = blockMoveEvent.reason ?? []; + if (reason.includes('connect') && blockMoveEvent.blockId) { + const block = mutatorWorkspace.getBlockById(blockMoveEvent.blockId); + if (block && block.type === PARAM_ITEM_BLOCK_NAME) { + (block as ParamItemBlock).makeNameLegal(); + } } - const bubbleEvent = e as Blockly.Events.BubbleOpen; - if ( - !(bubbleEvent.bubbleType === 'mutator' && bubbleEvent.isOpen) || - !bubbleEvent.blockId - ) { - return; + } else if (event.type === Blockly.Events.BLOCK_CHANGE) { + const blockChangeEvent = event as Blockly.Events.BlockChange; + if (blockChangeEvent.blockId) { + const block = mutatorWorkspace.getBlockById(blockChangeEvent.blockId); + if (block && block.type === PARAM_ITEM_BLOCK_NAME) { + (block as ParamItemBlock).makeNameLegal(); + } } - const workspaceId = bubbleEvent.workspaceId; - const block = Blockly.common - .getWorkspaceById(workspaceId)! - .getBlockById(bubbleEvent.blockId) as Blockly.BlockSvg; + } +} - if ((block.type !== MRC_EVENT) && (block.type !== MRC_CLASS_METHOD_DEF)) { - return; - } - const workspace = ( - block.getIcon(Blockly.icons.MutatorIcon.TYPE) as Blockly.icons.MutatorIcon - ).getWorkspace()!; +/** + * Called for mrc_event and mrc_class_method_def blocks when their mutator opesn. + * Triggers a flyout update and adds an event listener to the mutator workspace. + * + * @param block The block whose mutator is open. + */ +export function onMutatorOpen(block: Blockly.BlockSvg) { + const mutatorIcon = block.getIcon(Blockly.icons.MutatorIcon.TYPE) as Blockly.icons.MutatorIcon; + const mutatorWorkspace = mutatorIcon.getWorkspace()!; + updateMutatorFlyout(mutatorWorkspace); + mutatorWorkspace.addChangeListener(event => onChange(mutatorWorkspace, event)); +} + +/** + * Returns the MutatorIcon for the given block. + */ +export function getMutatorIcon(block: Blockly.BlockSvg): Blockly.icons.MutatorIcon { + return new Blockly.icons.MutatorIcon([PARAM_ITEM_BLOCK_NAME], block); +} - updateMutatorFlyout(workspace); - ChangeFramework.setup(workspace); -} \ No newline at end of file +export function createMutatorBlocks(workspace: Blockly.Workspace, parameterNames: string[]): Blockly.BlockSvg { + // First create the container block. + const containerBlock = workspace.newBlock(PARAM_CONTAINER_BLOCK_NAME) as Blockly.BlockSvg; + containerBlock.initSvg(); + + // Then add one param item block for each parameter. + let connection = containerBlock!.getInput(INPUT_STACK)!.connection; + for (const parameterName of parameterNames) { + const itemBlock = workspace.newBlock(PARAM_ITEM_BLOCK_NAME) as ParamItemBlock; + itemBlock.initSvg(); + itemBlock.setFieldValue(parameterName, FIELD_NAME); + itemBlock.originalName = parameterName; + connection!.connect(itemBlock.previousConnection!); + connection = itemBlock.nextConnection; + } + return containerBlock; +} diff --git a/src/blocks/setup_custom_blocks.ts b/src/blocks/setup_custom_blocks.ts index 352182f3..7b990663 100644 --- a/src/blocks/setup_custom_blocks.ts +++ b/src/blocks/setup_custom_blocks.ts @@ -15,7 +15,7 @@ import * as MiscComment from './mrc_misc_comment'; import * as MiscEvaluateButIgnoreResult from './mrc_misc_evaluate_but_ignore_result'; import * as None from './mrc_none'; import * as OpModeDetails from './mrc_opmode_details'; -import * as ParameterMutator from './mrc_param_container' +import * as ParamContainer from './mrc_param_container' import * as Port from './mrc_port'; import * as SetPythonVariable from './mrc_set_python_variable'; @@ -36,7 +36,7 @@ const customBlocks = [ MiscEvaluateButIgnoreResult, None, OpModeDetails, - ParameterMutator, + ParamContainer, Port, SetPythonVariable, ]; diff --git a/src/blocks/tokens.ts b/src/blocks/tokens.ts index a70663da..3656664a 100644 --- a/src/blocks/tokens.ts +++ b/src/blocks/tokens.ts @@ -33,6 +33,7 @@ export function customTokens(t: (key: string) => string): typeof Blockly.Msg { WITH: t('BLOCKLY.WITH'), WHEN: t('BLOCKLY.WHEN'), PARAMETER: t('BLOCKLY.PARAMETER'), + PARAMETERS: t('BLOCKLY.PARAMETERS'), PARAMETERS_CAN_ONLY_GO_IN_THEIR_METHODS_BLOCK: t('BLOCKLY.PARAMETERS_CAN_ONLY_GO_IN_THEIR_METHODS_BLOCK'), EVENT_HANDLER_ALREADY_ON_WORKSPACE: diff --git a/src/blocks/utils/change_framework.ts b/src/blocks/utils/change_framework.ts deleted file mode 100644 index d8138199..00000000 --- a/src/blocks/utils/change_framework.ts +++ /dev/null @@ -1,68 +0,0 @@ -/** - * @license - * Copyright 2025 Porpoiseful LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * @fileoverview Allow registering for specific callbacks - * @author alan@porpoiseful.com (Alan Smith) - */ -import * as Blockly from 'blockly'; - -// The purpose of this is to allow blocks to register for changes only of their type and the events they care about -// warning - only one change listener per block type is allowed. a second one will overwrite it. - -type CallbackFunctionBlockType = (block: Blockly.BlockSvg, blockEvent : Blockly.Events.BlockBase) => void; - -let registeredCallbacks = new Map; - -let blockEvents = [Blockly.Events.BLOCK_CHANGE, Blockly.Events.BLOCK_CREATE, Blockly.Events.BLOCK_DELETE, Blockly.Events.BLOCK_MOVE]; - -export function registerCallback(blockType : string, blockEvents : string[], func : CallbackFunctionBlockType){ - registeredCallbacks.set(blockType, [blockEvents, func]); -} - -export function getParentOfType(block : Blockly.Block | null, type : string) : Blockly.Block | null{ - let parentBlock = block?.getParent(); - while(parentBlock){ - if (parentBlock.type == type){ - return parentBlock; - } - parentBlock = parentBlock.getParent(); - } - return null -} - -function changeListener(e: Blockly.Events.Abstract){ - if (blockEvents.includes(e.type as any)){ - let eventBlockBase = (e as Blockly.Events.BlockBase); - let workspace = Blockly.Workspace.getById(eventBlockBase.workspaceId!) - let block = (workspace?.getBlockById(eventBlockBase.blockId!)! as Blockly.BlockSvg) - if (!block){ - return; - } - let callbackInfo = registeredCallbacks.get(block.type); - if (!callbackInfo){ - return; - } - if (callbackInfo[0].includes(e.type)){ - callbackInfo[1](block, eventBlockBase); - } - } -} - -export const setup = function (workspace: Blockly.Workspace) { - workspace.addChangeListener(changeListener); -} diff --git a/src/editor/editor.ts b/src/editor/editor.ts index 6eeca537..50d7727c 100644 --- a/src/editor/editor.ts +++ b/src/editor/editor.ts @@ -43,6 +43,7 @@ const MRC_ON_MOVE = 'mrcOnMove'; const MRC_ON_DESCENDANT_DISCONNECT = 'mrcOnDescendantDisconnect'; const MRC_ON_ANCESTOR_MOVE = 'mrcOnAncestorMove'; const MRC_ON_MODULE_CURRENT = 'mrcOnModuleCurrent'; +const MRC_ON_MUTATOR_OPEN = 'mrcOnMutatorOpen'; export class Editor { private static workspaceIdToEditor: { [workspaceId: string]: Editor } = {}; @@ -113,10 +114,6 @@ export class Editor { } private onChangeAfterLoading(event: Blockly.Events.Abstract) { - if (event.isUiEvent) { - // UI events are things like scrolling, zooming, etc. - return; - } if (!this.blocklyWorkspace.rendered) { // This editor has been abandoned. return; @@ -156,6 +153,19 @@ export class Editor { } }); } + if (event.type === Blockly.Events.BUBBLE_OPEN) { + const bubbleOpenEvent = event as Blockly.Events.BubbleOpen; + if (bubbleOpenEvent.bubbleType === 'mutator' && bubbleOpenEvent.isOpen) { + const block = this.blocklyWorkspace.getBlockById(bubbleOpenEvent.blockId!); + if (!block) { + return; + } + // Call MRC_ON_MUTATOR_OPEN on the block. + if (MRC_ON_MUTATOR_OPEN in block && typeof block[MRC_ON_MUTATOR_OPEN] === 'function') { + block[MRC_ON_MUTATOR_OPEN](); + } + } + } } public makeCurrent( diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index 5fe8b45d..57ba9688 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -122,6 +122,7 @@ "WITH": "with", "WHEN": "when", "PARAMETER": "parameter", + "PARAMETERS": "Parameters", "PARAMETERS_CAN_ONLY_GO_IN_THEIR_METHODS_BLOCK": "Parameters can only go in their method's block", "EVENT_HANDLER_ALREADY_ON_WORKSPACE": "This event handler is already on the workspace.", "EVENT_HANDLER_ROBOT_EVENT_NOT_FOUND": "This block is an event handler for an event that no longer exists.", diff --git a/src/i18n/locales/es/translation.json b/src/i18n/locales/es/translation.json index 07e51be4..c3739b1c 100644 --- a/src/i18n/locales/es/translation.json +++ b/src/i18n/locales/es/translation.json @@ -123,6 +123,7 @@ "WITH": "con", "WHEN": "cuando", "PARAMETER": "parámetro", + "PARAMETERS": "Parámetros", "PARAMETERS_CAN_ONLY_GO_IN_THEIR_METHODS_BLOCK": "Los parámetros solo pueden ir en el bloque de su método", "EVENT_HANDLER_ALREADY_ON_WORKSPACE": "Este controlador de eventos ya está en el área de trabajo.", "EVENT_HANDLER_ROBOT_EVENT_NOT_FOUND": "Este bloque es un controlador de eventos para un evento que ya no existe.", diff --git a/src/i18n/locales/he/translation.json b/src/i18n/locales/he/translation.json index cf8f37b7..7b9d5bfc 100644 --- a/src/i18n/locales/he/translation.json +++ b/src/i18n/locales/he/translation.json @@ -122,6 +122,7 @@ "WITH": "עם", "WHEN": "כאשר", "PARAMETER": "פרמטר", + "PARAMETERS": "פרמטרים", "PARAMETERS_CAN_ONLY_GO_IN_THEIR_METHODS_BLOCK": "פרמטרים יכולים להופיע רק בתוך הבלוק של המתודה שלהם.", "EVENT_HANDLER_ALREADY_ON_WORKSPACE": "מנהל האירועים הזה כבר נמצא בסביבת העבודה.", "EVENT_HANDLER_ROBOT_EVENT_NOT_FOUND": "הבלוק הזה הוא מנהל אירועים לאירוע שכבר לא קיים.",