diff --git a/src/App.tsx b/src/App.tsx index 4b774a08..eeb14e9e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -45,7 +45,6 @@ import * as clientSideStorage from './storage/client_side_storage'; import * as CustomBlocks from './blocks/setup_custom_blocks'; import { initialize as initializePythonBlocks } from './blocks/utils/python'; -import * as ChangeFramework from './blocks/utils/change_framework' import { registerToolboxButton } from './blocks/mrc_event_handler' import { mutatorOpenListener } from './blocks/mrc_param_container' import { TOOLBOX_UPDATE_EVENT } from './blocks/mrc_mechanism_component_holder'; @@ -487,7 +486,6 @@ const AppContent: React.FC = ({ project, setProject }): React.J return; } - ChangeFramework.setup(newWorkspace); newWorkspace.addChangeListener(mutatorOpenListener); newWorkspace.addChangeListener(handleBlocksChanged); diff --git a/src/blocks/mrc_call_python_function.ts b/src/blocks/mrc_call_python_function.ts index 8aa53fbe..24a53679 100644 --- a/src/blocks/mrc_call_python_function.ts +++ b/src/blocks/mrc_call_python_function.ts @@ -340,7 +340,7 @@ const CALL_PYTHON_FUNCTION = { this.mrcMechanismId = extraState.mechanismId ? extraState.mechanismId : ''; this.mrcComponentClassName = extraState.componentClassName ? extraState.componentClassName : ''; this.mrcMechanismClassName = extraState.mechanismClassName ? extraState.mechanismClassName : ''; - // Initialize mrcMapComponentNameToId here. It will be filled during mrcValidate. + // Initialize mrcMapComponentNameToId here. It will be filled during checkFunction. this.mrcMapComponentNameToId = {}; this.updateBlock_(); }, @@ -426,7 +426,7 @@ const CALL_PYTHON_FUNCTION = { .appendField('.'); } // Here we create a text field for the component name. - // Later, in mrcValidate, we will replace it with a dropdown. + // Later, in checkFunction, we will replace it with a dropdown. titleInput .appendField(createFieldNonEditableText(''), FIELD_COMPONENT_NAME) .appendField('.') @@ -586,18 +586,25 @@ const CALL_PYTHON_FUNCTION = { } return components; }, + + /** + * mrcOnModuleCurrent is called for each CallPythonFunctionBlock when the module becomes the current module. + */ + mrcOnModuleCurrent: function(this: CallPythonFunctionBlock): void { + this.checkFunction(); + }, /** * mrcOnLoad is called for each CallPythonFunctionBlock when the blocks are loaded in the blockly * workspace. */ mrcOnLoad: function(this: CallPythonFunctionBlock): void { - this.mrcValidate(); + this.checkFunction(); }, /** - * mrcValidate checks the block, updates it, and/or adds a warning balloon if necessary. - * It is called from mrcOnLoad above and from Editor.makeCurrent. + * checkFunction checks the block, updates it, and/or adds a warning balloon if necessary. + * It is called from mrcOnModuleCurrent and mrcOnLoad above. */ - mrcValidate: function(this: CallPythonFunctionBlock): void { + checkFunction: function(this: CallPythonFunctionBlock): void { const editor = Editor.getEditorForBlocklyWorkspace(this.workspace, true /* returnCurrentIfNotFound */); if (!editor) { return; diff --git a/src/blocks/mrc_component.ts b/src/blocks/mrc_component.ts index 633cf88f..689c5324 100644 --- a/src/blocks/mrc_component.ts +++ b/src/blocks/mrc_component.ts @@ -30,6 +30,10 @@ import { getAllowedTypesForSetCheck, getClassData, getSubclassNames } from './ut import * as toolboxItems from '../toolbox/items'; import * as storageModule from '../storage/module'; import * as storageModuleContent from '../storage/module_content'; +import { + BLOCK_NAME as MRC_MECHANISM_COMPONENT_HOLDER, + MechanismComponentHolderBlock, + mrcDescendantsMayHaveChanged } from './mrc_mechanism_component_holder'; import { createPort } from './mrc_port'; import { ClassData, FunctionData } from './utils/python_json_types'; import { renameMethodCallers } from './mrc_call_python_function' @@ -41,6 +45,8 @@ export const OUTPUT_NAME = 'mrc_component'; export const FIELD_NAME = 'NAME'; export const FIELD_TYPE = 'TYPE'; +const WARNING_ID_NOT_IN_HOLDER = 'not in holder'; + type ConstructorArg = { name: string, type: string, @@ -60,6 +66,15 @@ interface ComponentMixin extends ComponentMixinType { mrcArgs: ConstructorArg[], mrcImportModule: string, mrcStaticFunctionName: string, + + /** + * mrcHasNotInHolderWarning is set to true if we set the NOT_IN_HOLDER 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. + */ + mrcHasNotInHolderWarning: boolean, } type ComponentMixinType = typeof COMPONENT; @@ -68,6 +83,7 @@ const COMPONENT = { * Block initialization. */ init: function (this: ComponentBlock): void { + this.mrcHasNotInHolderWarning = false; this.setStyle(MRC_STYLE_COMPONENTS); const nameField = new Blockly.FieldTextInput('') nameField.setValidator(this.mrcNameFieldValidator.bind(this, nameField)); @@ -90,8 +106,8 @@ const COMPONENT = { if (this.mrcArgs){ this.mrcArgs.forEach((arg) => { extraState.params!.push({ - 'name': arg.name, - 'type': arg.type, + name: arg.name, + type: arg.type, }); }); } @@ -115,12 +131,11 @@ const COMPONENT = { if (extraState.params) { extraState.params.forEach((arg) => { this.mrcArgs.push({ - 'name': arg.name, - 'type': arg.type, + name: arg.name, + type: arg.type, }); }); } - this.mrcArgs = extraState.params ? extraState.params : []; this.updateBlock_(); }, /** @@ -167,8 +182,6 @@ const COMPONENT = { getArgName: function (this: ComponentBlock, _: number): string { return this.getFieldValue(FIELD_NAME) + '__' + 'port'; }, - - getComponentPorts: function (this: ComponentBlock, ports: {[argName: string]: string}): void { // Collect the ports for this component block. for (let i = 0; i < this.mrcArgs.length; i++) { @@ -176,6 +189,44 @@ const COMPONENT = { ports[argName] = this.mrcArgs[i].name; } }, + /** + * mrcOnLoad is called for each ComponentBlock when the blocks are loaded in the blockly workspace. + */ + mrcOnLoad: function(this: ComponentBlock): void { + this.checkBlockIsInHolder(); + }, + /** + * mrcOnMove is called when a ComponentBlock is moved. + */ + mrcOnMove: function(this: ComponentBlock, reason: string[]): void { + this.checkBlockIsInHolder(); + if (reason.includes('connect')) { + const rootBlock: Blockly.Block | null = this.getRootBlock(); + if (rootBlock && rootBlock.type === MRC_MECHANISM_COMPONENT_HOLDER) { + (rootBlock as MechanismComponentHolderBlock).setNameOfChildBlock(this); + } + } + mrcDescendantsMayHaveChanged(this.workspace); + }, + checkBlockIsInHolder: function(this: ComponentBlock): void { + const rootBlock: Blockly.Block | null = this.getRootBlock(); + if (rootBlock && rootBlock.type === MRC_MECHANISM_COMPONENT_HOLDER) { + // If the root block is the mechanism_component_holder, the component block is allowed to stay. + // Remove any previous warning. + this.setWarningText(null, WARNING_ID_NOT_IN_HOLDER); + this.mrcHasNotInHolderWarning = false; + } else { + // Otherwise, add a warning to the block. + if (!this.mrcHasNotInHolderWarning) { + this.setWarningText(Blockly.Msg.WARNING_COMPONENT_NOT_IN_HOLDER, WARNING_ID_NOT_IN_HOLDER); + const icon = this.getIcon(Blockly.icons.IconType.WARNING); + if (icon) { + icon.setBubbleVisible(true); + } + this.mrcHasNotInHolderWarning = true; + } + } + }, /** * mrcChangeIds is called when a module is copied so that the copy has different ids than the original. */ @@ -253,8 +304,8 @@ function createComponentBlock( if (constructorData.expectedPortType) { extraState.params!.push({ - 'name': constructorData.expectedPortType, - 'type': 'Port', + name: constructorData.expectedPortType, + type: 'Port', }); if ( moduleType == storageModule.ModuleType.ROBOT ) { inputs['ARG0'] = createPort(constructorData.expectedPortType); diff --git a/src/blocks/mrc_event.ts b/src/blocks/mrc_event.ts index d64964b7..1760120b 100644 --- a/src/blocks/mrc_event.ts +++ b/src/blocks/mrc_event.ts @@ -25,7 +25,10 @@ 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'; +import { + BLOCK_NAME as MRC_MECHANISM_COMPONENT_HOLDER, + MechanismComponentHolderBlock, + mrcDescendantsMayHaveChanged } from './mrc_mechanism_component_holder'; import * as toolboxItems from '../toolbox/items'; import * as storageModuleContent from '../storage/module_content'; import { renameMethodCallers, mutateMethodCallers } from './mrc_call_python_function' @@ -51,12 +54,13 @@ interface EventMixin extends EventMixinType { mrcParameters: Parameter[], /** - * 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. + * mrcHasNotInHolderWarning is set to true if we set the NOT_IN_HOLDER 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, + mrcHasNotInHolderWarning: boolean, } type EventMixinType = typeof EVENT; @@ -65,6 +69,7 @@ const EVENT = { * Block initialization. */ init: function (this: EventBlock): void { + this.mrcHasNotInHolderWarning = false; this.setStyle(MRC_STYLE_EVENTS); this.appendDummyInput(INPUT_TITLE) .appendField(new Blockly.FieldTextInput('my_event'), FIELD_EVENT_NAME); @@ -84,8 +89,8 @@ const EVENT = { if (this.mrcParameters) { this.mrcParameters.forEach((arg) => { extraState.params!.push({ - 'name': arg.name, - 'type': arg.type, + name: arg.name, + type: arg.type, }); }); } @@ -97,17 +102,14 @@ const EVENT = { loadExtraState: function (this: EventBlock, extraState: EventExtraState): void { this.mrcEventId = extraState.eventId ? extraState.eventId : this.id; this.mrcParameters = []; - this.mrcHasWarning = false; - if (extraState.params) { extraState.params.forEach((arg) => { this.mrcParameters.push({ - 'name': arg.name, - 'type': arg.type, + name: arg.name, + type: arg.type, }); }); } - this.mrcParameters = extraState.params ? extraState.params : []; this.updateBlock_(); }, /** @@ -207,28 +209,37 @@ const EVENT = { * mrcOnLoad is called for each EventBlock when the blocks are loaded in the blockly workspace. */ mrcOnLoad: function(this: EventBlock): void { - this.checkParentIsHolder(); + this.checkBlockIsInHolder(); }, /** * mrcOnMove is called when an EventBlock is moved. */ - mrcOnMove: function(this: EventBlock): void { - this.checkParentIsHolder(); + mrcOnMove: function(this: EventBlock, reason: string[]): void { + this.checkBlockIsInHolder(); + if (reason.includes('connect')) { + const rootBlock: Blockly.Block | null = this.getRootBlock(); + if (rootBlock && rootBlock.type === MRC_MECHANISM_COMPONENT_HOLDER) { + (rootBlock as MechanismComponentHolderBlock).setNameOfChildBlock(this); + } + } + mrcDescendantsMayHaveChanged(this.workspace); }, - checkParentIsHolder: function(this: EventBlock): void { - const parentBlock = this.getParent(); - if (parentBlock && parentBlock.type === MRC_MECHANISM_COMPONENT_HOLDER) { - // If the parent block is the mechanism_component_holder, the event block is allowed to stay. + checkBlockIsInHolder: function(this: EventBlock): void { + const rootBlock: Blockly.Block | null = this.getRootBlock(); + if (rootBlock && rootBlock.type === MRC_MECHANISM_COMPONENT_HOLDER) { + // If the root block is the mechanism_component_holder, the event block is allowed to stay. // Remove any previous warning. this.setWarningText(null, WARNING_ID_NOT_IN_HOLDER); - this.mrcHasWarning = false; + this.mrcHasNotInHolderWarning = false; } else { // Otherwise, add a warning to the block. - this.unplug(true); - if (!this.mrcHasWarning) { + if (!this.mrcHasNotInHolderWarning) { this.setWarningText(Blockly.Msg.WARNING_EVENT_NOT_IN_HOLDER, WARNING_ID_NOT_IN_HOLDER); - this.getIcon(Blockly.icons.IconType.WARNING)!.setBubbleVisible(true); - this.mrcHasWarning = true; + const icon = this.getIcon(Blockly.icons.IconType.WARNING); + if (icon) { + icon.setBubbleVisible(true); + } + this.mrcHasNotInHolderWarning = true; } } }, diff --git a/src/blocks/mrc_event_handler.ts b/src/blocks/mrc_event_handler.ts index fdac4aad..3bb0aac2 100644 --- a/src/blocks/mrc_event_handler.ts +++ b/src/blocks/mrc_event_handler.ts @@ -167,18 +167,24 @@ const EVENT_HANDLER = { }); }, + /** + * mrcOnModuleCurrent is called for each EventHandlerBlock when the module becomes the current module. + */ + mrcOnModuleCurrent: function(this: EventHandlerBlock): void { + this.checkEvent(); + }, /** * mrcOnLoad is called for each EventHandlerBlock when the blocks are loaded in the blockly * workspace. */ mrcOnLoad: function(this: EventHandlerBlock): void { - this.mrcValidate(); + this.checkEvent(); }, /** - * mrcValidate checks the block, updates it, and/or adds a warning balloon if necessary. - * It is called from mrcOnLoad above and from Editor.makeCurrent. + * checkEvent checks the block, updates it, and/or adds a warning balloon if necessary. + * It is called from mrcOnModuleCurrent and mrcOnLoad above. */ - mrcValidate: function(this: EventHandlerBlock): void { + checkEvent: function(this: EventHandlerBlock): void { const warnings: string[] = []; const editor = Editor.getEditorForBlocklyWorkspace(this.workspace, true /* returnCurrentIfNotFound */); diff --git a/src/blocks/mrc_get_parameter.ts b/src/blocks/mrc_get_parameter.ts index 09dd3a7e..10f1709f 100644 --- a/src/blocks/mrc_get_parameter.ts +++ b/src/blocks/mrc_get_parameter.ts @@ -82,7 +82,7 @@ const GET_PARAMETER_BLOCK = { /** * mrcOnMove is called when an EventBlock is moved. */ - mrcOnMove: function(this: GetParameterBlock): void { + mrcOnMove: function(this: GetParameterBlock, _reason: string[]): void { this.checkBlockPlacement(); }, mrcOnAncestorMove: function(this: GetParameterBlock): void { @@ -91,7 +91,7 @@ const GET_PARAMETER_BLOCK = { checkBlockPlacement: function(this: GetParameterBlock): void { const legalParameterNames: string[] = []; - const rootBlock: Blockly.Block = this.getRootBlock(); + const rootBlock: Blockly.Block | null = this.getRootBlock(); if (rootBlock.type === MRC_CLASS_METHOD_DEF) { // This block is within a class method definition. const classMethodDefBlock = rootBlock as ClassMethodDefBlock; diff --git a/src/blocks/mrc_mechanism.ts b/src/blocks/mrc_mechanism.ts index 286d6b56..c2dfa34d 100644 --- a/src/blocks/mrc_mechanism.ts +++ b/src/blocks/mrc_mechanism.ts @@ -31,6 +31,10 @@ import * as toolboxItems from '../toolbox/items'; import * as storageModule from '../storage/module'; import * as storageModuleContent from '../storage/module_content'; import * as storageNames from '../storage/names'; +import { + BLOCK_NAME as MRC_MECHANISM_COMPONENT_HOLDER, + MechanismComponentHolderBlock, + mrcDescendantsMayHaveChanged } from './mrc_mechanism_component_holder'; import { renameMethodCallers } from './mrc_call_python_function' import { renameMechanismName as renameMechanismNameInEventHandlers } from './mrc_event_handler' import { createPort } from './mrc_port'; @@ -53,6 +57,7 @@ type MechanismExtraState = { parameters?: Parameter[], } +const WARNING_ID_NOT_IN_HOLDER = 'not in holder'; const WARNING_ID_MECHANISM_CHANGED = 'mechanism changed'; export type MechanismBlock = Blockly.Block & MechanismMixin & Blockly.BlockSvg; @@ -61,6 +66,15 @@ interface MechanismMixin extends MechanismMixinType { mrcMechanismId: string, mrcImportModule: string, mrcParameters: Parameter[], + + /** + * mrcHasNotInHolderWarning is set to true if we set the NOT_IN_HOLDER 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. + */ + mrcHasNotInHolderWarning: boolean, } type MechanismMixinType = typeof MECHANISM; @@ -69,6 +83,7 @@ const MECHANISM = { * Block initialization. */ init: function (this: MechanismBlock): void { + this.mrcHasNotInHolderWarning = false; this.setStyle(MRC_STYLE_MECHANISMS); const nameField = new Blockly.FieldTextInput('') nameField.setValidator(this.mrcNameFieldValidator.bind(this, nameField)); @@ -91,8 +106,8 @@ const MECHANISM = { extraState.parameters = []; this.mrcParameters.forEach((arg) => { extraState.parameters!.push({ - 'name': arg.name, - 'type': arg.type, + name: arg.name, + type: arg.type, }); }); if (this.mrcImportModule) { @@ -175,18 +190,57 @@ const MECHANISM = { }; }, + /** + * mrcOnModuleCurrent is called for each MechanismBlock when the module becomes the current module. + */ + mrcOnModuleCurrent: function(this: MechanismBlock): void { + this.checkMechanism(); + }, /** * mrcOnLoad is called for each MechanismBlock when the blocks are loaded in the blockly * workspace. */ mrcOnLoad: function(this: MechanismBlock): void { - this.mrcValidate(); + this.checkBlockIsInHolder(); + this.checkMechanism(); + }, + /** + * mrcOnMove is called when a MechanismBlock is moved. + */ + mrcOnMove: function(this: MechanismBlock, reason: string[]): void { + this.checkBlockIsInHolder(); + if (reason.includes('connect')) { + const rootBlock: Blockly.Block | null = this.getRootBlock(); + if (rootBlock && rootBlock.type === MRC_MECHANISM_COMPONENT_HOLDER) { + (rootBlock as MechanismComponentHolderBlock).setNameOfChildBlock(this); + } + } + mrcDescendantsMayHaveChanged(this.workspace); + }, + checkBlockIsInHolder: function(this: MechanismBlock): void { + const rootBlock: Blockly.Block | null = this.getRootBlock(); + if (rootBlock && rootBlock.type === MRC_MECHANISM_COMPONENT_HOLDER) { + // If the root block is the mechanism_component_holder, the mechanism block is allowed to stay. + // Remove any previous warning. + this.setWarningText(null, WARNING_ID_NOT_IN_HOLDER); + this.mrcHasNotInHolderWarning = false; + } else { + // Otherwise, add a warning to the block. + if (!this.mrcHasNotInHolderWarning) { + this.setWarningText(Blockly.Msg.WARNING_MECHANISM_NOT_IN_HOLDER, WARNING_ID_NOT_IN_HOLDER); + const icon = this.getIcon(Blockly.icons.IconType.WARNING); + if (icon) { + icon.setBubbleVisible(true); + } + this.mrcHasNotInHolderWarning = true; + } + } }, /** - * mrcValidate checks the block, updates it, and/or adds a warning balloon if necessary. - * It is called from mrcOnLoad above and from Editor.makeCurrent. + * checkMechanism checks the block, updates it, and/or adds a warning balloon if necessary. + * It is called from mrcOnModuleCurrent and mrcOnLoad above. */ - mrcValidate: function(this: MechanismBlock): void { + checkMechanism: function(this: MechanismBlock): void { const warnings: string[] = []; const editor = Editor.getEditorForBlocklyWorkspace(this.workspace, true /* returnCurrentIfNotFound */); diff --git a/src/blocks/mrc_mechanism_component_holder.ts b/src/blocks/mrc_mechanism_component_holder.ts index 12029f96..e4797204 100644 --- a/src/blocks/mrc_mechanism_component_holder.ts +++ b/src/blocks/mrc_mechanism_component_holder.ts @@ -22,7 +22,6 @@ import * as Blockly from 'blockly'; import { MRC_STYLE_MECHANISMS } from '../themes/styles'; -import * as ChangeFramework from './utils/change_framework'; import { getLegalName } from './utils/python'; import { ExtendedPythonGenerator } from '../editor/extended_python_generator'; import * as storageModule from '../storage/module'; @@ -55,23 +54,14 @@ export type MechanismComponentHolderBlock = Blockly.Block & MechanismComponentHo interface MechanismComponentHolderMixin extends MechanismComponentHolderMixinType { mrcHideMechanisms: boolean; mrcHidePrivateComponents: boolean; -} -type MechanismComponentHolderMixinType = typeof MECHANISM_COMPONENT_HOLDER; -function setName(block: Blockly.BlockSvg){ - const parentBlock = ChangeFramework.getParentOfType(block, 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'); - } + mrcMechanismBlockIds: string, + mrcComponentBlockIds: string, + mrcPrivateComponentBlockIds: string, + mrcEventBlockIds: string, + mrcToolboxUpdateTimeout: NodeJS.Timeout | null; } +type MechanismComponentHolderMixinType = typeof MECHANISM_COMPONENT_HOLDER; const MECHANISM_COMPONENT_HOLDER = { /** @@ -81,11 +71,11 @@ const MECHANISM_COMPONENT_HOLDER = { this.setInputsInline(false); this.setOutput(false); this.setStyle(MRC_STYLE_MECHANISMS); - ChangeFramework.registerCallback(MRC_COMPONENT_NAME, [Blockly.Events.BLOCK_MOVE, Blockly.Events.BLOCK_CHANGE], this.onBlockChanged); - ChangeFramework.registerCallback(MRC_MECHANISM_NAME, [Blockly.Events.BLOCK_MOVE, Blockly.Events.BLOCK_CHANGE], this.onBlockChanged); - ChangeFramework.registerCallback(MRC_EVENT_NAME, [Blockly.Events.BLOCK_MOVE, Blockly.Events.BLOCK_CHANGE], this.onBlockChanged); - // TODO: We also need an event handler for when a mechanism, component, or event is deleted or - // disconnected from the holder. + this.mrcMechanismBlockIds = ''; + this.mrcComponentBlockIds = ''; + this.mrcPrivateComponentBlockIds = ''; + this.mrcEventBlockIds = ''; + this.mrcToolboxUpdateTimeout = null; }, saveExtraState: function (this: MechanismComponentHolderBlock): MechanismComponentHolderExtraState { const extraState: MechanismComponentHolderExtraState = { @@ -137,20 +127,125 @@ const MECHANISM_COMPONENT_HOLDER = { .setCheck(EVENT_OUTPUT) .appendField(Blockly.Msg.EVENTS); }, - 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); - updateToolboxAfterDelay(block); + /** + * mrcOnLoad is called for each MechanismComponentHolderBlock when the blocks are loaded in the blockly + * workspace. + */ + mrcOnLoad: function(this: MechanismComponentHolderBlock): void { + this.collectDescendants(false); + }, + /** + * mrcOnDescendantDisconnect is called for each MechanismComponentHolderBlock when any descendant is + * disconnected. + */ + mrcOnDescendantDisconnect: function(this: MechanismComponentHolderBlock): void { + this.collectDescendants(true); + }, + mrcDescendantsMayHaveChanged: function (this: MechanismComponentHolderBlock): void { + this.collectDescendants(true); + }, + collectDescendants: function ( + this: MechanismComponentHolderBlock, updateToolboxIfDescendantsChanged: boolean): void { + let mechanismBlockIds = ''; + let componentBlockIds = ''; + let privateComponentBlockIds = ''; + let eventBlockIds = ''; + + const mechanismsInput = this.getInput(INPUT_MECHANISMS); + if (mechanismsInput && mechanismsInput.connection) { + // Walk through all connected mechanism blocks. + let mechanismBlock = mechanismsInput.connection.targetBlock(); + while (mechanismBlock) { + if (mechanismBlock.type === MRC_MECHANISM_NAME) { + mechanismBlockIds += mechanismBlock.id; + } + // Move to the next block in the stack. + mechanismBlock = mechanismBlock.getNextBlock(); + } + } + const componentsInput = this.getInput(INPUT_COMPONENTS); + if (componentsInput && componentsInput.connection) { + // Walk through all connected component blocks. + let componentBlock = componentsInput.connection.targetBlock(); + while (componentBlock) { + if (componentBlock.type === MRC_COMPONENT_NAME) { + componentBlockIds += componentBlock.id; + } + // Move to the next block in the stack. + componentBlock = componentBlock.getNextBlock(); + } + } + const privateComponentsInput = this.getInput(INPUT_PRIVATE_COMPONENTS); + if (privateComponentsInput && privateComponentsInput.connection) { + // Walk through all connected component blocks. + let componentBlock = privateComponentsInput.connection.targetBlock(); + while (componentBlock) { + if (componentBlock.type === MRC_COMPONENT_NAME) { + privateComponentBlockIds += componentBlock.id; + } + // Move to the next block in the stack. + componentBlock = componentBlock.getNextBlock(); + } + } + const eventsInput = this.getInput(INPUT_EVENTS); + if (eventsInput && eventsInput.connection) { + // Walk through all connected event blocks. + let eventBlock = eventsInput.connection.targetBlock(); + while (eventBlock) { + if (eventBlock.type === MRC_EVENT_NAME) { + eventBlockIds += eventBlock.id; + } + // Move to the next block in the stack. + eventBlock = eventBlock.getNextBlock(); } } - else { - if (blockEvent.type == Blockly.Events.BLOCK_CHANGE) { - setName(block); - updateToolboxAfterDelay(block); + + if (updateToolboxIfDescendantsChanged) { + if (mechanismBlockIds !== this.mrcMechanismBlockIds || + componentBlockIds !== this.mrcComponentBlockIds || + privateComponentBlockIds !== this.mrcPrivateComponentBlockIds || + eventBlockIds !== this.mrcEventBlockIds) { + this.updateToolboxAfterDelay(); } } + + this.mrcMechanismBlockIds = mechanismBlockIds; + this.mrcComponentBlockIds = componentBlockIds; + this.mrcPrivateComponentBlockIds = privateComponentBlockIds; + this.mrcEventBlockIds = eventBlockIds; + }, + updateToolboxAfterDelay: function (this: MechanismComponentHolderBlock): void { + if (this.mrcToolboxUpdateTimeout) { + clearTimeout(this.mrcToolboxUpdateTimeout); + } + this.mrcToolboxUpdateTimeout = setTimeout(() => { + const event = new CustomEvent(TOOLBOX_UPDATE_EVENT, { + detail: { + timestamp: Date.now(), + workspaceId: this.workspace.id, + } + }); + window.dispatchEvent(event); + this.mrcToolboxUpdateTimeout = null; + }, 100); + }, + /** + * setNameOfChildBlock is called from mrc_mechanism, mrc_component, and mrc_event blocks when they + * connect to this mrc_mechanism_component_holder block. + */ + setNameOfChildBlock(this: MechanismComponentHolderBlock, child: Blockly.Block): void { + const otherNames: string[] = [] + const descendants = this.getDescendants(true); + descendants + .filter(descendant => descendant.id !== child.id) + .forEach(descendant => { + otherNames.push(descendant.getFieldValue('NAME')); + }); + const currentName = child.getFieldValue('NAME'); + const legalName = getLegalName(currentName, otherNames); + if (legalName !== currentName) { + child.setFieldValue(legalName, 'NAME'); + } }, getMechanisms: function (this: MechanismComponentHolderBlock): storageModuleContent.MechanismInRobot[] { const mechanisms: storageModuleContent.MechanismInRobot[] = [] @@ -242,24 +337,6 @@ const MECHANISM_COMPONENT_HOLDER = { }, } -let toolboxUpdateTimeout: NodeJS.Timeout | null = null; - -function updateToolboxAfterDelay(block: Blockly.BlockSvg) { - if (toolboxUpdateTimeout) { - clearTimeout(toolboxUpdateTimeout); - } - toolboxUpdateTimeout = setTimeout(() => { - const event = new CustomEvent(TOOLBOX_UPDATE_EVENT, { - detail: { - timestamp: Date.now(), - workspaceId: block.workspace.id, - } - }); - window.dispatchEvent(event); - toolboxUpdateTimeout = null; - }, 100); -} - export const setup = function () { Blockly.Blocks[BLOCK_NAME] = MECHANISM_COMPONENT_HOLDER; } @@ -434,6 +511,13 @@ export function getEvents( }); } +export function mrcDescendantsMayHaveChanged(workspace: Blockly.Workspace): void { + // Get the holder block and call its mrcDescendantsMayHaveChanged method. + workspace.getBlocksByType(BLOCK_NAME).forEach(block => { + (block as MechanismComponentHolderBlock).mrcDescendantsMayHaveChanged(); + }); +} + /** * Hide private components. * This function should only be called when upgrading old projects. diff --git a/src/blocks/tokens.ts b/src/blocks/tokens.ts index 3f995cf7..a70663da 100644 --- a/src/blocks/tokens.ts +++ b/src/blocks/tokens.ts @@ -103,6 +103,8 @@ export function customTokens(t: (key: string) => string): typeof Blockly.Msg { CALL_ROBOT_INSTANCE_METHOD_TOOLTIP: t('BLOCKLY.TOOLTIP.CALL_ROBOT_INSTANCE_METHOD'), CALL_MECHANISM_INSTANCE_METHOD_TOOLTIP: t('BLOCKLY.TOOLTIP.CALL_MECHANISM_INSTANCE_METHOD'), WARNING_EVENT_NOT_IN_HOLDER: t('BLOCKLY.WARNING.EVENT_NOT_IN_HOLDER'), + WARNING_COMPONENT_NOT_IN_HOLDER: t('BLOCKLY.WARNING.COMPONENT_NOT_IN_HOLDER'), + WARNING_MECHANISM_NOT_IN_HOLDER: t('BLOCKLY.WARNING.MECHANISM_NOT_IN_HOLDER'), MRC_CATEGORY_HARDWARE: t('BLOCKLY.CATEGORY.HARDWARE'), MRC_CATEGORY_ROBOT: t('BLOCKLY.CATEGORY.ROBOT'), MRC_CATEGORY_COMPONENTS: t('BLOCKLY.CATEGORY.COMPONENTS'), diff --git a/src/editor/editor.ts b/src/editor/editor.ts index 6e446c6b..6eeca537 100644 --- a/src/editor/editor.ts +++ b/src/editor/editor.ts @@ -40,8 +40,9 @@ const EMPTY_TOOLBOX: Blockly.utils.toolbox.ToolboxInfo = { const MRC_ON_LOAD = 'mrcOnLoad'; const MRC_ON_MOVE = 'mrcOnMove'; +const MRC_ON_DESCENDANT_DISCONNECT = 'mrcOnDescendantDisconnect'; const MRC_ON_ANCESTOR_MOVE = 'mrcOnAncestorMove'; -const MRC_VALIDATE = 'mrcValidate'; +const MRC_ON_MODULE_CURRENT = 'mrcOnModuleCurrent'; export class Editor { private static workspaceIdToEditor: { [workspaceId: string]: Editor } = {}; @@ -126,15 +127,29 @@ export class Editor { if (event.type === Blockly.Events.BLOCK_MOVE) { const blockMoveEvent = event as Blockly.Events.BlockMove; + const reason: string[] = blockMoveEvent.reason ?? []; + if (reason.includes('disconnect') && blockMoveEvent.oldParentId) { + const oldParent = this.blocklyWorkspace.getBlockById(blockMoveEvent.oldParentId!); + if (oldParent) { + const rootBlock = oldParent.getRootBlock(); + if (rootBlock) { + // Call MRC_ON_DESCENDANT_DISCONNECT on the root block of the block that was disconnected. + if (MRC_ON_DESCENDANT_DISCONNECT in rootBlock && typeof rootBlock[MRC_ON_DESCENDANT_DISCONNECT] === 'function') { + rootBlock[MRC_ON_DESCENDANT_DISCONNECT](); + } + } + } + } + const block = this.blocklyWorkspace.getBlockById(blockMoveEvent.blockId!); if (!block) { return; } - // Call MRC_ON_MOVE for the block that was moved. + // Call MRC_ON_MOVE on the block that was moved. if (MRC_ON_MOVE in block && typeof block[MRC_ON_MOVE] === 'function') { - block[MRC_ON_MOVE](); + block[MRC_ON_MOVE](reason); } - // Call MRC_ON_ANCESTOR_MOVE for all descendents of the block that was moved. + // Call MRC_ON_ANCESTOR_MOVE on 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](); @@ -152,10 +167,10 @@ export class Editor { this.parseModules(project, modulePathToContentText); this.updateToolboxImpl(); - // Go through all the blocks in the workspace and call their mrcValidate method. + // Go through all the blocks in the workspace and call their mrcOnModuleCurrent method. this.blocklyWorkspace.getAllBlocks().forEach(block => { - if (MRC_VALIDATE in block && typeof block[MRC_VALIDATE] === 'function') { - block[MRC_VALIDATE](); + if (MRC_ON_MODULE_CURRENT in block && typeof block[MRC_ON_MODULE_CURRENT] === 'function') { + block[MRC_ON_MODULE_CURRENT](); } }); } diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index 86c04aa6..5fe8b45d 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -208,6 +208,8 @@ "CALL_MECHANISM_INSTANCE_METHOD_MISSING_METHOD": "This block calls a method that no longer exists in the mechanism.", "CALL_MECHANISM_INSTANCE_METHOD_MISSING_MECHANISM": "This block calls a method in a mechanism that no longer exists.", "EVENT_NOT_IN_HOLDER": "This block can only go in the events section of the robot or mechanism.", + "COMPONENT_NOT_IN_HOLDER": "This block can only go in the components section of the robot or mechanism.", + "MECHANISM_NOT_IN_HOLDER": "This block can only go in the mechanisms section of the robot.", "MECHANISM_NOT_FOUND": "This block refers to a mechanism that no longer exists." }, "ERROR":{ diff --git a/src/i18n/locales/es/translation.json b/src/i18n/locales/es/translation.json index 00d654d5..07e51be4 100644 --- a/src/i18n/locales/es/translation.json +++ b/src/i18n/locales/es/translation.json @@ -209,6 +209,8 @@ "CALL_MECHANISM_INSTANCE_METHOD_MISSING_METHOD": "Este bloque llama a un método que ya no existe en el mecanismo.", "CALL_MECHANISM_INSTANCE_METHOD_MISSING_MECHANISM": "Este bloque llama a un método en un mecanismo que ya no existe.", "EVENT_NOT_IN_HOLDER": "Este bloque solo puede ir en la sección de eventos del robot o mecanismo.", + "COMPONENT_NOT_IN_HOLDER": "Este bloque solo puede ir en la sección de componentes del robot o mecanismo.", + "MECHANISM_NOT_IN_HOLDER": "Este bloque solo puede ir en la sección de mecanismos del robot.", "MECHANISM_NOT_FOUND": "Este bloque se refiere a un mecanismo que ya no existe." }, "ERROR":{ diff --git a/src/i18n/locales/he/translation.json b/src/i18n/locales/he/translation.json index 47f337e4..cf8f37b7 100644 --- a/src/i18n/locales/he/translation.json +++ b/src/i18n/locales/he/translation.json @@ -208,6 +208,8 @@ "CALL_MECHANISM_INSTANCE_METHOD_MISSING_METHOD": "בלוק זה קורא למתודה שכבר לא קיימת במנגנון.", "CALL_MECHANISM_INSTANCE_METHOD_MISSING_MECHANISM": "בלוק זה קורא למתודה במנגנון שכבר לא קיים.", "EVENT_NOT_IN_HOLDER": "בלוק זה יכול להיכנס רק לאזור האירועים של הרובוט או המנגנון.", + "COMPONENT_NOT_IN_HOLDER": "בלוק זה יכול להיכנס רק לאזור הרכיבים של הרובוט או המנגנון.", + "MECHANISM_NOT_IN_HOLDER": "בלוק זה יכול להיכנס רק לחלק המנגנונים של הרובוט.", "MECHANISM_NOT_FOUND": "בלוק זה מתייחס למנגנון שכבר לא קיים." }, "ERROR": {