From ef4af935c3443ad52a38598bc5246e00d4a7a276 Mon Sep 17 00:00:00 2001 From: Liz Looney Date: Sun, 16 Nov 2025 21:47:04 -0800 Subject: [PATCH] In editor.ts: Added code in onChangeAfterLoading to handle VIEWPORT_CHANGE and BLOCK_CREATE events. For VIEWPORT_CHANGE, set the paste location to the center of the blockly workspace. For BLOCK_CREATE, call mrcOnCreate on the newly created block (if the block has that method). In makeCurrent, set the paste workspace and location. In mrc_call_python_function.ts, mrc_component.ts, mrc_event.ts, mrc_event_handler.ts, mrc_mechanism.ts: Added mrcOnCreate so blocks that are created or pasted are checked. In mrc_call_python_function.ts, mrc_event_handler.ts, mrc_jump_to_step.ts, mrc_mechanism.ts: Check whether getIcon(Blockly.icons.IconType.WARNING) returns null. In mrc_call_python_function.ts: Added code to checkFunction to check blocks that call an instance method defined in the same module. --- src/blocks/mrc_call_python_function.ts | 51 +++++++++++++++++++++++++- src/blocks/mrc_component.ts | 6 +++ src/blocks/mrc_event.ts | 6 +++ src/blocks/mrc_event_handler.ts | 13 ++++++- src/blocks/mrc_jump_to_step.ts | 5 ++- src/blocks/mrc_mechanism.ts | 12 +++++- src/blocks/tokens.ts | 1 + src/editor/editor.ts | 46 +++++++++++++++++++++++ src/i18n/locales/en/translation.json | 1 + src/i18n/locales/es/translation.json | 1 + src/i18n/locales/he/translation.json | 1 + 11 files changed, 137 insertions(+), 6 deletions(-) diff --git a/src/blocks/mrc_call_python_function.ts b/src/blocks/mrc_call_python_function.ts index 09380044..32e8686f 100644 --- a/src/blocks/mrc_call_python_function.ts +++ b/src/blocks/mrc_call_python_function.ts @@ -597,13 +597,57 @@ const CALL_PYTHON_FUNCTION = { mrcOnLoad: function(this: CallPythonFunctionBlock, editor: Editor): void { this.checkFunction(editor); }, + /** + * mrcOnCreate is called for each CallPythonFunctionBlock when it is created. + */ + mrcOnCreate: function(this: CallPythonFunctionBlock, editor: Editor): void { + this.checkFunction(editor); + }, /** * checkFunction checks the block, updates it, and/or adds a warning balloon if necessary. - * It is called from mrcOnModuleCurrent and mrcOnLoad above. + * It is called from mrcOnModuleCurrent, mrcOnLoad, and mrcOnCreate above. */ checkFunction: function(this: CallPythonFunctionBlock, editor: Editor): void { const warnings: string[] = []; + // If this block is calling a method defined in this module, check whether the method still + // exists and whether it has been changed. + // If the method doesn't exist, put a visible warning on this block. + // If the robot method has changed, update the block if possible or put a + // visible warning on it. + if (this.mrcFunctionKind === FunctionKind.INSTANCE_WITHIN) { + let foundMethod = false; + const methodsFromWorkspace = editor.getMethodsForWithinFromWorkspace(); + for (const method of methodsFromWorkspace) { + if (method.methodId === this.mrcMethodId) { + foundMethod = true; + if (this.mrcActualFunctionName !== method.pythonName) { + this.mrcActualFunctionName = method.pythonName; + } + if (this.getFieldValue(FIELD_FUNCTION_NAME) !== method.visibleName) { + this.setFieldValue(method.visibleName, FIELD_FUNCTION_NAME); + } + + this.mrcReturnType = method.returnType; + this.mrcArgs = []; + // We don't include the arg for the self argument because we don't need a socket for it. + for (let i = 1; i < method.args.length; i++) { + this.mrcArgs.push({ + name: method.args[i].name, + type: method.args[i].type, + }); + } + this.updateBlock_(); + + // Since we found the method, we can break out of the loop. + break; + } + } + if (!foundMethod) { + warnings.push(Blockly.Msg.WARNING_CALL_INSTANCE_WITHIN_METHOD_MISSING_METHOD); + } + } + // If this block is calling a component method, check whether the component // still exists and whether it has been changed. // If the component doesn't exist, put a visible warning on this block. @@ -823,7 +867,10 @@ const CALL_PYTHON_FUNCTION = { // Add a warnings to the block. const warningText = warnings.join('\n\n'); this.setWarningText(warningText, WARNING_ID_FUNCTION_CHANGED); - this.getIcon(Blockly.icons.IconType.WARNING)!.setBubbleVisible(true); + const icon = this.getIcon(Blockly.icons.IconType.WARNING); + if (icon) { + icon.setBubbleVisible(true); + } this.bringToFront(); } else { // Clear the existing warning on the block. diff --git a/src/blocks/mrc_component.ts b/src/blocks/mrc_component.ts index 4a1bb1a5..e0a2a937 100644 --- a/src/blocks/mrc_component.ts +++ b/src/blocks/mrc_component.ts @@ -201,6 +201,12 @@ const COMPONENT = { mrcOnLoad: function(this: ComponentBlock, _editor: Editor): void { this.checkBlockIsInHolder(); }, + /** + * mrcOnCreate is called for each ComponentBlock when it is created. + */ + mrcOnCreate: function(this: ComponentBlock, _editor: Editor): void { + this.checkBlockIsInHolder(); + }, /** * mrcOnMove is called when a ComponentBlock is moved. */ diff --git a/src/blocks/mrc_event.ts b/src/blocks/mrc_event.ts index 70a2e9e0..db5f2942 100644 --- a/src/blocks/mrc_event.ts +++ b/src/blocks/mrc_event.ts @@ -200,6 +200,12 @@ const EVENT = { mrcOnLoad: function(this: EventBlock, _editor: Editor): void { this.checkBlockIsInHolder(); }, + /** + * mrcOnCreate is called for each EventBlock when it is created. + */ + mrcOnCreate: function(this: EventBlock, _editor: Editor): void { + this.checkBlockIsInHolder(); + }, /** * mrcOnMove is called when an EventBlock is moved. */ diff --git a/src/blocks/mrc_event_handler.ts b/src/blocks/mrc_event_handler.ts index eaae03df..c3cee1cb 100644 --- a/src/blocks/mrc_event_handler.ts +++ b/src/blocks/mrc_event_handler.ts @@ -180,9 +180,15 @@ const EVENT_HANDLER = { mrcOnLoad: function(this: EventHandlerBlock, editor: Editor): void { this.checkEvent(editor); }, + /** + * mrcOnCreate is called for each EventHandlerBlock when it is created. + */ + mrcOnCreate: function(this: EventHandlerBlock, editor: Editor): void { + this.checkEvent(editor); + }, /** * checkEvent checks the block, updates it, and/or adds a warning balloon if necessary. - * It is called from mrcOnModuleCurrent and mrcOnLoad above. + * It is called from mrcOnModuleCurrent, mrcOnLoad, and mrcCreate above. */ checkEvent: function(this: EventHandlerBlock, editor: Editor): void { const warnings: string[] = []; @@ -282,7 +288,10 @@ const EVENT_HANDLER = { // Add a warnings to the block. const warningText = warnings.join('\n\n'); this.setWarningText(warningText, WARNING_ID_EVENT_CHANGED); - this.getIcon(Blockly.icons.IconType.WARNING)!.setBubbleVisible(true); + const icon = this.getIcon(Blockly.icons.IconType.WARNING); + if (icon) { + icon.setBubbleVisible(true); + } this.bringToFront(); } else { // Clear the existing warning on the block. diff --git a/src/blocks/mrc_jump_to_step.ts b/src/blocks/mrc_jump_to_step.ts index 10bf77a4..fbd470dd 100644 --- a/src/blocks/mrc_jump_to_step.ts +++ b/src/blocks/mrc_jump_to_step.ts @@ -87,7 +87,10 @@ const JUMP_TO_STEP_BLOCK = { // Otherwise, add a warning to this block. if (!this.mrcHasWarning) { this.setWarningText(Blockly.Msg.JUMP_CAN_ONLY_GO_IN_THEIR_STEPS_BLOCK, WARNING_ID_NOT_IN_STEP); - this.getIcon(Blockly.icons.IconType.WARNING)!.setBubbleVisible(true); + const icon = this.getIcon(Blockly.icons.IconType.WARNING); + if (icon) { + icon.setBubbleVisible(true); + } this.mrcHasWarning = true; } } diff --git a/src/blocks/mrc_mechanism.ts b/src/blocks/mrc_mechanism.ts index 439b46f9..7a754f68 100644 --- a/src/blocks/mrc_mechanism.ts +++ b/src/blocks/mrc_mechanism.ts @@ -204,6 +204,13 @@ const MECHANISM = { this.checkBlockIsInHolder(); this.checkMechanism(editor); }, + /** + * mrcOnCreate is called for each MechanismBlock when it is created. + */ + mrcOnCreate: function(this: MechanismBlock, editor: Editor): void { + this.checkBlockIsInHolder(); + this.checkMechanism(editor); + }, /** * mrcOnMove is called when a MechanismBlock is moved. */ @@ -299,7 +306,10 @@ const MECHANISM = { // Add a warnings to the block. const warningText = warnings.join('\n\n'); this.setWarningText(warningText, WARNING_ID_MECHANISM_CHANGED); - this.getIcon(Blockly.icons.IconType.WARNING)!.setBubbleVisible(true); + const icon = this.getIcon(Blockly.icons.IconType.WARNING); + if (icon) { + icon.setBubbleVisible(true); + } this.bringToFront(); } else { // Clear the existing warning on the block. diff --git a/src/blocks/tokens.ts b/src/blocks/tokens.ts index 8ce0b0c2..11ec3b43 100644 --- a/src/blocks/tokens.ts +++ b/src/blocks/tokens.ts @@ -84,6 +84,7 @@ export function customTokens(t: (key: string) => string): typeof Blockly.Msg { SET_INSTANCE_VARIABLE_TOOLTIP: t('BLOCKLY.TOOLTIP.SET_INSTANCE_VARIABLE'), VAR_KIND_MUST_BE_MODULE_CLASS_OR_INSTANCE: t('BLOCKLY.ERROR.VAR_KIND_MUST_BE_MODULE_CLASS_OR_INSTANCE'), MECHANISM_NOT_FOUND_WARNING: t('BLOCKLY.WARNING.MECHANISM_NOT_FOUND'), + WARNING_CALL_INSTANCE_WITHIN_METHOD_MISSING_METHOD: t('BLOCKLY.WARNING.CALL_INSTANCE_WITHIN_METHOD_MISSING_METHOD'), WARNING_CALL_COMPONENT_INSTANCE_METHOD_PRIVATE_COMPONENT: t('BLOCKLY.WARNING.CALL_COMPONENT_INSTANCE_METHOD_PRIVATE_COMPONENT'), WARNING_CALL_COMPONENT_INSTANCE_METHOD_MISSING_COMPONENT: t('BLOCKLY.WARNING.CALL_COMPONENT_INSTANCE_METHOD_MISSING_COMPONENT'), WARNING_CALL_MECHANISM_COMPONENT_INSTANCE_METHOD_MISSING_MECHANISM: t('BLOCKLY.WARNING.CALL_MECHANISM_COMPONENT_INSTANCE_METHOD_MISSING_MECHANISM'), diff --git a/src/editor/editor.ts b/src/editor/editor.ts index b34f3876..a05dc508 100644 --- a/src/editor/editor.ts +++ b/src/editor/editor.ts @@ -42,6 +42,7 @@ const EMPTY_TOOLBOX: Blockly.utils.toolbox.ToolboxInfo = { }; const MRC_ON_LOAD = 'mrcOnLoad'; +const MRC_ON_CREATE = 'mrcOnCreate'; const MRC_ON_MOVE = 'mrcOnMove'; const MRC_ON_DESCENDANT_DISCONNECT = 'mrcOnDescendantDisconnect'; const MRC_ON_ANCESTOR_MOVE = 'mrcOnAncestorMove'; @@ -134,6 +135,24 @@ export class Editor { return; } + if (event.type === Blockly.Events.VIEWPORT_CHANGE) { + this.setPasteLocation(); + } + + if (event.type === Blockly.Events.BLOCK_CREATE) { + const blockCreateEvent = event as Blockly.Events.BlockCreate; + if (blockCreateEvent.ids) { + blockCreateEvent.ids.forEach(id => { + const block = this.blocklyWorkspace.getBlockById(id); + if (block) { + if (MRC_ON_CREATE in block && typeof block[MRC_ON_CREATE] === 'function') { + block[MRC_ON_CREATE](this); + } + } + }); + } + } + if (event.type === Blockly.Events.BLOCK_MOVE) { const blockMoveEvent = event as Blockly.Events.BlockMove; const reason: string[] = blockMoveEvent.reason ?? []; @@ -165,6 +184,7 @@ export class Editor { } }); } + if (event.type === Blockly.Events.BUBBLE_OPEN) { const bubbleOpenEvent = event as Blockly.Events.BubbleOpen; if (bubbleOpenEvent.bubbleType === 'mutator' && bubbleOpenEvent.isOpen) { @@ -199,6 +219,16 @@ export class Editor { block[MRC_ON_MODULE_CURRENT](this); } }); + + if (Blockly.clipboard.getLastCopiedWorkspace()) { + Blockly.clipboard.setLastCopiedWorkspace(this.blocklyWorkspace); + + setTimeout(() => { + this.blocklyWorkspace.markFocused(); + Blockly.getFocusManager().focusNode(this.blocklyWorkspace); + this.setPasteLocation(); + }); + } } public abandon(): void { @@ -618,4 +648,20 @@ export class Editor { public static getCurrentEditor(): Editor | null { return Editor.currentEditor; } + + private setPasteLocation(): void { + const copyData = Blockly.clipboard.getLastCopiedData(); + if (copyData && copyData.paster === 'block') { + const blockCopyData = copyData as Blockly.clipboard.BlockCopyData; + if (blockCopyData.blockState) { + const metrics = this.blocklyWorkspace.getMetrics(); + const center = new Blockly.utils.Coordinate( + metrics.viewLeft + metrics.viewWidth / 2, + metrics.viewTop + metrics.viewHeight / 2); + blockCopyData.blockState.x = center.x; + blockCopyData.blockState.y = center.y; + Blockly.clipboard.setLastCopiedLocation(center); + } + } + } } diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index 8d525102..84bc9450 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -206,6 +206,7 @@ "PAGINATION_TOTAL": "{{start}}-{{end}} of {{total}} items" }, "WARNING":{ + "CALL_INSTANCE_WITHIN_METHOD_MISSING_METHOD": "This block calls a method that does not exist in this module.", "CALL_COMPONENT_INSTANCE_METHOD_PRIVATE_COMPONENT": "This blocks calls a method on a private component in the {{mechanismClassName}} mechanism.", "CALL_COMPONENT_INSTANCE_METHOD_MISSING_COMPONENT": "This block calls a method on a component that no longer exists.", "CALL_MECHANISM_COMPONENT_INSTANCE_METHOD_MISSING_MECHANISM": "This block calls a method on a component that belongs to a mechanism that no longer exists.", diff --git a/src/i18n/locales/es/translation.json b/src/i18n/locales/es/translation.json index 79d004d4..210bf2d3 100644 --- a/src/i18n/locales/es/translation.json +++ b/src/i18n/locales/es/translation.json @@ -207,6 +207,7 @@ "PAGINATION_TOTAL": "{{start}}-{{end}} de {{total}} elementos" }, "WARNING":{ + "CALL_INSTANCE_WITHIN_METHOD_MISSING_METHOD": "Este bloque llama a un método que no existe en este módulo.", "CALL_COMPONENT_INSTANCE_METHOD_PRIVATE_COMPONENT": "Este bloque llama a un método en un componente privado en el mecanismo {{mechanismClassName}}.", "CALL_COMPONENT_INSTANCE_METHOD_MISSING_COMPONENT": "Este bloque llama a un método en un componente que ya no existe.", "CALL_MECHANISM_COMPONENT_INSTANCE_METHOD_MISSING_MECHANISM": "Este bloque llama a un método de un componente que pertenece a un mecanismo que ya no existe.", diff --git a/src/i18n/locales/he/translation.json b/src/i18n/locales/he/translation.json index 4203f602..58540f45 100644 --- a/src/i18n/locales/he/translation.json +++ b/src/i18n/locales/he/translation.json @@ -206,6 +206,7 @@ "PAGINATION_TOTAL": "{{start}}-{{end}} מתוך {{total}} פריטים" }, "WARNING": { + "CALL_INSTANCE_WITHIN_METHOD_MISSING_METHOD": "בלוק זה קורא למתודה שאינה קיימת במודול זה.", "CALL_COMPONENT_INSTANCE_METHOD_PRIVATE_COMPONENT": "בלוק זה קורא למתודה על רכיב פרטי במנגנון {{mechanismClassName}}.", "CALL_COMPONENT_INSTANCE_METHOD_MISSING_COMPONENT": "בלוק זה קורא למתודה על רכיב שכבר לא קיים.", "CALL_MECHANISM_COMPONENT_INSTANCE_METHOD_MISSING_MECHANISM": "בלוק זה קורא למתודה על רכיב ששייך למנגנון שכבר לא קיים.",