From 1cec3864231164ee36693993806da1e6c0f8da3c Mon Sep 17 00:00:00 2001 From: Liz Looney Date: Fri, 3 Oct 2025 20:08:49 -0700 Subject: [PATCH 1/4] Rename mrcValidate to mrcOnModuleCurrent because it is called when the module becomes the current module. --- src/blocks/mrc_call_python_function.ts | 19 +++++++++++++------ src/blocks/mrc_event_handler.ts | 14 ++++++++++---- src/blocks/mrc_mechanism.ts | 14 ++++++++++---- src/editor/editor.ts | 8 ++++---- 4 files changed, 37 insertions(+), 18 deletions(-) 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_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_mechanism.ts b/src/blocks/mrc_mechanism.ts index 286d6b56..70cad3b8 100644 --- a/src/blocks/mrc_mechanism.ts +++ b/src/blocks/mrc_mechanism.ts @@ -175,18 +175,24 @@ 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.checkMechanism(); }, /** - * 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/editor/editor.ts b/src/editor/editor.ts index 6e446c6b..7f0b9e3d 100644 --- a/src/editor/editor.ts +++ b/src/editor/editor.ts @@ -41,7 +41,7 @@ const EMPTY_TOOLBOX: Blockly.utils.toolbox.ToolboxInfo = { const MRC_ON_LOAD = 'mrcOnLoad'; const MRC_ON_MOVE = 'mrcOnMove'; const MRC_ON_ANCESTOR_MOVE = 'mrcOnAncestorMove'; -const MRC_VALIDATE = 'mrcValidate'; +const MRC_ON_MODULE_CURRENT = 'mrcOnModuleCurrent'; export class Editor { private static workspaceIdToEditor: { [workspaceId: string]: Editor } = {}; @@ -152,10 +152,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](); } }); } From 395c1dead9535ea3df1653c84a08ebe25f52a154 Mon Sep 17 00:00:00 2001 From: Liz Looney Date: Fri, 3 Oct 2025 20:47:52 -0700 Subject: [PATCH 2/4] Added code to mrc_component.ts and mrc_mechanism.ts to add a warning if the block is not in the holder. Updated code in mrc_event.ts (which already existed) to be similar. --- src/blocks/mrc_component.ts | 60 +++++++++++++++++++++++----- src/blocks/mrc_event.ts | 46 +++++++++++---------- src/blocks/mrc_mechanism.ts | 43 +++++++++++++++++++- src/blocks/tokens.ts | 2 + src/i18n/locales/en/translation.json | 2 + src/i18n/locales/es/translation.json | 2 + src/i18n/locales/he/translation.json | 2 + 7 files changed, 124 insertions(+), 33 deletions(-) diff --git a/src/blocks/mrc_component.ts b/src/blocks/mrc_component.ts index 633cf88f..5c2f7c01 100644 --- a/src/blocks/mrc_component.ts +++ b/src/blocks/mrc_component.ts @@ -30,6 +30,7 @@ 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 } 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 +42,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 +63,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 +80,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 +103,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 +128,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 +179,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 +186,38 @@ 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): void { + this.checkBlockIsInHolder(); + }, + 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. + this.unplug(true); + 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 +295,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..de3d3531 100644 --- a/src/blocks/mrc_event.ts +++ b/src/blocks/mrc_event.ts @@ -51,12 +51,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 +66,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 +86,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 +99,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 +206,31 @@ 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(); + this.checkBlockIsInHolder(); }, - 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_mechanism.ts b/src/blocks/mrc_mechanism.ts index 70cad3b8..94dcf2d6 100644 --- a/src/blocks/mrc_mechanism.ts +++ b/src/blocks/mrc_mechanism.ts @@ -31,6 +31,7 @@ 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 } 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 +54,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 +63,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 +80,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 +103,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) { @@ -186,8 +198,35 @@ const MECHANISM = { * workspace. */ mrcOnLoad: function(this: MechanismBlock): void { + this.checkBlockIsInHolder(); this.checkMechanism(); }, + /** + * mrcOnMove is called when a MechanismBlock is moved. + */ + mrcOnMove: function(this: MechanismBlock): void { + this.checkBlockIsInHolder(); + }, + 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. + this.unplug(true); + 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; + } + } + }, /** * checkMechanism checks the block, updates it, and/or adds a warning balloon if necessary. * It is called from mrcOnModuleCurrent and mrcOnLoad above. 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/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index 0634f76d..0e5142ae 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -180,6 +180,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 c4ecd328..5b24712b 100644 --- a/src/i18n/locales/es/translation.json +++ b/src/i18n/locales/es/translation.json @@ -181,6 +181,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 709d7bcc..f10bfff5 100644 --- a/src/i18n/locales/he/translation.json +++ b/src/i18n/locales/he/translation.json @@ -180,6 +180,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": { From 303cafc2f9c96ba18eea4135e8f890d05320a9f4 Mon Sep 17 00:00:00 2001 From: Liz Looney Date: Fri, 3 Oct 2025 21:09:33 -0700 Subject: [PATCH 3/4] Added reason parameter to mrcOnLoad methods. In mrc_mechanism_component_holder.ts: Removed code that uses change framework. Replaced function setName with method MechanismComponentHolderBlock.setNameOfChildBlock. Replaced function updateToolboxAfterDelay with method MechanismComponentHolderBlock.updateToolboxAfterDelay. Added function mrcDescenantsMayHaveChanged, which calls method MechanismComponentHolderBlock.mrcDescenantsMayHaveChanged. MechanismComponentHolderBlock.mrcDescenantsMayHaveChanged collects the ids of mechanisms, components, private components, and events and if they have changed, calls updateToolboxAfterDelay. Updated mrc_mechanism, mrc_component, and mrc_event to call MechanismComponentHolderBlock.setNameOfChildBlock when they are connected to the holder. Update mrc_mechanism, mrc_component, and mrc_event to call mrcDescendantsMayHaveChanged when they are moved. --- src/App.tsx | 2 - src/blocks/mrc_component.ts | 17 +- src/blocks/mrc_event.ts | 17 +- src/blocks/mrc_get_parameter.ts | 4 +- src/blocks/mrc_mechanism.ts | 17 +- src/blocks/mrc_mechanism_component_holder.ts | 214 ++++++++++++++----- src/editor/editor.ts | 3 +- 7 files changed, 214 insertions(+), 60 deletions(-) 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_component.ts b/src/blocks/mrc_component.ts index 5c2f7c01..8a5aab81 100644 --- a/src/blocks/mrc_component.ts +++ b/src/blocks/mrc_component.ts @@ -30,7 +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 } from './mrc_mechanism_component_holder'; +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' @@ -164,6 +167,9 @@ const COMPONENT = { } return legalName; }, + getComponentId: function (this: ComponentBlock): string { + return this.mrcComponentId; + }, getComponent: function (this: ComponentBlock): storageModuleContent.Component | null { const componentName = this.getFieldValue(FIELD_NAME); const componentType = this.getFieldValue(FIELD_TYPE); @@ -195,8 +201,15 @@ const COMPONENT = { /** * mrcOnMove is called when a ComponentBlock is moved. */ - mrcOnMove: function(this: ComponentBlock): void { + 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(); diff --git a/src/blocks/mrc_event.ts b/src/blocks/mrc_event.ts index de3d3531..6f4ccd43 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' @@ -211,8 +214,15 @@ const EVENT = { /** * mrcOnMove is called when an EventBlock is moved. */ - mrcOnMove: function(this: EventBlock): void { + 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); }, checkBlockIsInHolder: function(this: EventBlock): void { const rootBlock: Blockly.Block | null = this.getRootBlock(); @@ -234,6 +244,9 @@ const EVENT = { } } }, + getEventId: function (this: EventBlock): string { + return this.mrcEventId; + }, getEvent: function (this: EventBlock): storageModuleContent.Event { const event: storageModuleContent.Event = { eventId: this.mrcEventId, 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 94dcf2d6..6a2d9026 100644 --- a/src/blocks/mrc_mechanism.ts +++ b/src/blocks/mrc_mechanism.ts @@ -31,7 +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 } from './mrc_mechanism_component_holder'; +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'; @@ -176,6 +179,9 @@ const MECHANISM = { } return legalName; }, + getMechanismId: function (this: MechanismBlock): string { + return this.mrcMechanismId; + }, getMechanism: function (this: MechanismBlock): storageModuleContent.MechanismInRobot | null { const mechanismName = this.getFieldValue(FIELD_NAME); const mechanismType = this.mrcImportModule + '.' + this.getFieldValue(FIELD_TYPE); @@ -204,8 +210,15 @@ const MECHANISM = { /** * mrcOnMove is called when a MechanismBlock is moved. */ - mrcOnMove: function(this: MechanismBlock): void { + 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(); diff --git a/src/blocks/mrc_mechanism_component_holder.ts b/src/blocks/mrc_mechanism_component_holder.ts index 12029f96..8ad356e9 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'); - } + mrcMechanismIds: string[], + mrcComponentIds: string[], + mrcPrivateComponentIds: string[], + mrcEventIds: 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.mrcMechanismIds = []; + this.mrcComponentIds = []; + this.mrcPrivateComponentIds = []; + this.mrcEventIds = []; + this.mrcToolboxUpdateTimeout = null; }, saveExtraState: function (this: MechanismComponentHolderBlock): MechanismComponentHolderExtraState { const extraState: MechanismComponentHolderExtraState = { @@ -137,19 +127,156 @@ 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); + }, + mrcDescendantsMayHaveChanged: function (this: MechanismComponentHolderBlock): void { + this.collectDescendants(true); + }, + collectDescendants: function ( + this: MechanismComponentHolderBlock, updateToolboxIfDescendantsChanged: boolean): void { + const mechanismIds: string[] = []; + const componentIds: string[] = []; + const privateComponentIds: string[] = []; + const eventIds: string[] = []; + + 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) { + mechanismIds.push((mechanismBlock as MechanismBlock).getMechanismId()); + } + // 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) { + componentIds.push((componentBlock as ComponentBlock).getComponentId()); + } + // 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) { + privateComponentIds.push((componentBlock as ComponentBlock).getComponentId()); + } + // 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) { + eventIds.push((eventBlock as EventBlock).getEventId()); + } + // 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) { + let descendantsChanged = false; + if (mechanismIds.length === this.mrcMechanismIds.length) { + for (let i = 0; i < mechanismIds.length; i++) { + if (mechanismIds[i] !== this.mrcMechanismIds[i]) { + descendantsChanged = true; + break; + } + } + } else { + descendantsChanged = true; + } + if (componentIds.length === this.mrcComponentIds.length) { + for (let i = 0; i < componentIds.length; i++) { + if (componentIds[i] !== this.mrcComponentIds[i]) { + descendantsChanged = true; + break; + } + } + } else { + descendantsChanged = true; + } + if (privateComponentIds.length === this.mrcPrivateComponentIds.length) { + for (let i = 0; i < privateComponentIds.length; i++) { + if (privateComponentIds[i] !== this.mrcPrivateComponentIds[i]) { + descendantsChanged = true; + break; + } + } + } else { + descendantsChanged = true; + } + if (eventIds.length === this.mrcEventIds.length) { + for (let i = 0; i < eventIds.length; i++) { + if (eventIds[i] !== this.mrcEventIds[i]) { + descendantsChanged = true; + break; + } + } + } else { + descendantsChanged = true; } + + if (descendantsChanged) { + this.updateToolboxAfterDelay(); + } + } + + this.mrcMechanismIds = mechanismIds; + this.mrcComponentIds = componentIds; + this.mrcPrivateComponentIds = privateComponentIds; + this.mrcEventIds = eventIds; + }, + 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[] { @@ -242,24 +369,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 +543,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/editor/editor.ts b/src/editor/editor.ts index 7f0b9e3d..ac725de9 100644 --- a/src/editor/editor.ts +++ b/src/editor/editor.ts @@ -132,7 +132,8 @@ export class Editor { } // Call MRC_ON_MOVE for the block that was moved. if (MRC_ON_MOVE in block && typeof block[MRC_ON_MOVE] === 'function') { - block[MRC_ON_MOVE](); + const reason: string[] = blockMoveEvent.reason ?? []; + block[MRC_ON_MOVE](reason); } // Call MRC_ON_ANCESTOR_MOVE for all descendents of the block that was moved. block.getDescendants(false).forEach(descendant => { From ea439ccfa898d961c30aa523e2329247546016d7 Mon Sep 17 00:00:00 2001 From: Liz Looney Date: Sat, 4 Oct 2025 22:47:19 -0700 Subject: [PATCH 4/4] Removed unplug call on mechanism, component, or event blocks if they aren't in the holder. If there are more than one of these in a stack and the user drags the whole stack out of the holder, the unplug will unplug the top one from the stack which is alarming. In mrc_mechanism_component_holder: Changed mrcMechanismIds to mrcMechanismBlockIds, mrcComponentIds to mrcComponentBlockIds, mrcPrivateComponentIds to mrcPrivateComponentBlockIds, and mrcEventIds to mrcEventBlockIds. Made them strings, which makes the comparing much more simple. Added mrcOnDescendantDisconnected, which is called when a mechanism, component, or event block is deleted. In editor, added code to handle move event for disconnect and call mrcOnDescendantDisconnect on the root block that the block was disconnected from. --- src/blocks/mrc_component.ts | 4 - src/blocks/mrc_event.ts | 4 - src/blocks/mrc_mechanism.ts | 4 - src/blocks/mrc_mechanism_component_holder.ts | 94 +++++++------------- src/editor/editor.ts | 20 ++++- 5 files changed, 48 insertions(+), 78 deletions(-) diff --git a/src/blocks/mrc_component.ts b/src/blocks/mrc_component.ts index 8a5aab81..689c5324 100644 --- a/src/blocks/mrc_component.ts +++ b/src/blocks/mrc_component.ts @@ -167,9 +167,6 @@ const COMPONENT = { } return legalName; }, - getComponentId: function (this: ComponentBlock): string { - return this.mrcComponentId; - }, getComponent: function (this: ComponentBlock): storageModuleContent.Component | null { const componentName = this.getFieldValue(FIELD_NAME); const componentType = this.getFieldValue(FIELD_TYPE); @@ -220,7 +217,6 @@ const COMPONENT = { this.mrcHasNotInHolderWarning = false; } else { // Otherwise, add a warning to the block. - this.unplug(true); 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); diff --git a/src/blocks/mrc_event.ts b/src/blocks/mrc_event.ts index 6f4ccd43..1760120b 100644 --- a/src/blocks/mrc_event.ts +++ b/src/blocks/mrc_event.ts @@ -233,7 +233,6 @@ const EVENT = { this.mrcHasNotInHolderWarning = false; } else { // Otherwise, add a warning to the block. - this.unplug(true); if (!this.mrcHasNotInHolderWarning) { this.setWarningText(Blockly.Msg.WARNING_EVENT_NOT_IN_HOLDER, WARNING_ID_NOT_IN_HOLDER); const icon = this.getIcon(Blockly.icons.IconType.WARNING); @@ -244,9 +243,6 @@ const EVENT = { } } }, - getEventId: function (this: EventBlock): string { - return this.mrcEventId; - }, getEvent: function (this: EventBlock): storageModuleContent.Event { const event: storageModuleContent.Event = { eventId: this.mrcEventId, diff --git a/src/blocks/mrc_mechanism.ts b/src/blocks/mrc_mechanism.ts index 6a2d9026..c2dfa34d 100644 --- a/src/blocks/mrc_mechanism.ts +++ b/src/blocks/mrc_mechanism.ts @@ -179,9 +179,6 @@ const MECHANISM = { } return legalName; }, - getMechanismId: function (this: MechanismBlock): string { - return this.mrcMechanismId; - }, getMechanism: function (this: MechanismBlock): storageModuleContent.MechanismInRobot | null { const mechanismName = this.getFieldValue(FIELD_NAME); const mechanismType = this.mrcImportModule + '.' + this.getFieldValue(FIELD_TYPE); @@ -229,7 +226,6 @@ const MECHANISM = { this.mrcHasNotInHolderWarning = false; } else { // Otherwise, add a warning to the block. - this.unplug(true); 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); diff --git a/src/blocks/mrc_mechanism_component_holder.ts b/src/blocks/mrc_mechanism_component_holder.ts index 8ad356e9..e4797204 100644 --- a/src/blocks/mrc_mechanism_component_holder.ts +++ b/src/blocks/mrc_mechanism_component_holder.ts @@ -55,10 +55,10 @@ interface MechanismComponentHolderMixin extends MechanismComponentHolderMixinTyp mrcHideMechanisms: boolean; mrcHidePrivateComponents: boolean; - mrcMechanismIds: string[], - mrcComponentIds: string[], - mrcPrivateComponentIds: string[], - mrcEventIds: string[], + mrcMechanismBlockIds: string, + mrcComponentBlockIds: string, + mrcPrivateComponentBlockIds: string, + mrcEventBlockIds: string, mrcToolboxUpdateTimeout: NodeJS.Timeout | null; } type MechanismComponentHolderMixinType = typeof MECHANISM_COMPONENT_HOLDER; @@ -71,10 +71,10 @@ const MECHANISM_COMPONENT_HOLDER = { this.setInputsInline(false); this.setOutput(false); this.setStyle(MRC_STYLE_MECHANISMS); - this.mrcMechanismIds = []; - this.mrcComponentIds = []; - this.mrcPrivateComponentIds = []; - this.mrcEventIds = []; + this.mrcMechanismBlockIds = ''; + this.mrcComponentBlockIds = ''; + this.mrcPrivateComponentBlockIds = ''; + this.mrcEventBlockIds = ''; this.mrcToolboxUpdateTimeout = null; }, saveExtraState: function (this: MechanismComponentHolderBlock): MechanismComponentHolderExtraState { @@ -134,15 +134,22 @@ const MECHANISM_COMPONENT_HOLDER = { 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 { - const mechanismIds: string[] = []; - const componentIds: string[] = []; - const privateComponentIds: string[] = []; - const eventIds: string[] = []; + let mechanismBlockIds = ''; + let componentBlockIds = ''; + let privateComponentBlockIds = ''; + let eventBlockIds = ''; const mechanismsInput = this.getInput(INPUT_MECHANISMS); if (mechanismsInput && mechanismsInput.connection) { @@ -150,7 +157,7 @@ const MECHANISM_COMPONENT_HOLDER = { let mechanismBlock = mechanismsInput.connection.targetBlock(); while (mechanismBlock) { if (mechanismBlock.type === MRC_MECHANISM_NAME) { - mechanismIds.push((mechanismBlock as MechanismBlock).getMechanismId()); + mechanismBlockIds += mechanismBlock.id; } // Move to the next block in the stack. mechanismBlock = mechanismBlock.getNextBlock(); @@ -162,7 +169,7 @@ const MECHANISM_COMPONENT_HOLDER = { let componentBlock = componentsInput.connection.targetBlock(); while (componentBlock) { if (componentBlock.type === MRC_COMPONENT_NAME) { - componentIds.push((componentBlock as ComponentBlock).getComponentId()); + componentBlockIds += componentBlock.id; } // Move to the next block in the stack. componentBlock = componentBlock.getNextBlock(); @@ -174,7 +181,7 @@ const MECHANISM_COMPONENT_HOLDER = { let componentBlock = privateComponentsInput.connection.targetBlock(); while (componentBlock) { if (componentBlock.type === MRC_COMPONENT_NAME) { - privateComponentIds.push((componentBlock as ComponentBlock).getComponentId()); + privateComponentBlockIds += componentBlock.id; } // Move to the next block in the stack. componentBlock = componentBlock.getNextBlock(); @@ -186,7 +193,7 @@ const MECHANISM_COMPONENT_HOLDER = { let eventBlock = eventsInput.connection.targetBlock(); while (eventBlock) { if (eventBlock.type === MRC_EVENT_NAME) { - eventIds.push((eventBlock as EventBlock).getEventId()); + eventBlockIds += eventBlock.id; } // Move to the next block in the stack. eventBlock = eventBlock.getNextBlock(); @@ -194,57 +201,18 @@ const MECHANISM_COMPONENT_HOLDER = { } if (updateToolboxIfDescendantsChanged) { - let descendantsChanged = false; - if (mechanismIds.length === this.mrcMechanismIds.length) { - for (let i = 0; i < mechanismIds.length; i++) { - if (mechanismIds[i] !== this.mrcMechanismIds[i]) { - descendantsChanged = true; - break; - } - } - } else { - descendantsChanged = true; - } - if (componentIds.length === this.mrcComponentIds.length) { - for (let i = 0; i < componentIds.length; i++) { - if (componentIds[i] !== this.mrcComponentIds[i]) { - descendantsChanged = true; - break; - } - } - } else { - descendantsChanged = true; - } - if (privateComponentIds.length === this.mrcPrivateComponentIds.length) { - for (let i = 0; i < privateComponentIds.length; i++) { - if (privateComponentIds[i] !== this.mrcPrivateComponentIds[i]) { - descendantsChanged = true; - break; - } - } - } else { - descendantsChanged = true; - } - if (eventIds.length === this.mrcEventIds.length) { - for (let i = 0; i < eventIds.length; i++) { - if (eventIds[i] !== this.mrcEventIds[i]) { - descendantsChanged = true; - break; - } - } - } else { - descendantsChanged = true; - } - - if (descendantsChanged) { + if (mechanismBlockIds !== this.mrcMechanismBlockIds || + componentBlockIds !== this.mrcComponentBlockIds || + privateComponentBlockIds !== this.mrcPrivateComponentBlockIds || + eventBlockIds !== this.mrcEventBlockIds) { this.updateToolboxAfterDelay(); } } - this.mrcMechanismIds = mechanismIds; - this.mrcComponentIds = componentIds; - this.mrcPrivateComponentIds = privateComponentIds; - this.mrcEventIds = eventIds; + this.mrcMechanismBlockIds = mechanismBlockIds; + this.mrcComponentBlockIds = componentBlockIds; + this.mrcPrivateComponentBlockIds = privateComponentBlockIds; + this.mrcEventBlockIds = eventBlockIds; }, updateToolboxAfterDelay: function (this: MechanismComponentHolderBlock): void { if (this.mrcToolboxUpdateTimeout) { diff --git a/src/editor/editor.ts b/src/editor/editor.ts index ac725de9..6eeca537 100644 --- a/src/editor/editor.ts +++ b/src/editor/editor.ts @@ -40,6 +40,7 @@ const EMPTY_TOOLBOX: Blockly.utils.toolbox.ToolboxInfo = { const MRC_ON_LOAD = 'mrcOnLoad'; const MRC_ON_MOVE = 'mrcOnMove'; +const MRC_ON_DESCENDANT_DISCONNECT = 'mrcOnDescendantDisconnect'; const MRC_ON_ANCESTOR_MOVE = 'mrcOnAncestorMove'; const MRC_ON_MODULE_CURRENT = 'mrcOnModuleCurrent'; @@ -126,16 +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') { - const reason: string[] = blockMoveEvent.reason ?? []; 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]();