diff --git a/src/blocks/mrc_jump_to_step.ts b/src/blocks/mrc_jump_to_step.ts index 96dd4e6b..8e23e305 100644 --- a/src/blocks/mrc_jump_to_step.ts +++ b/src/blocks/mrc_jump_to_step.ts @@ -23,7 +23,6 @@ import * as Blockly from 'blockly'; import { Editor } from '../editor/editor'; import { ExtendedPythonGenerator } from '../editor/extended_python_generator'; -import { createFieldNonEditableText } from '../fields/FieldNonEditableText'; import { MRC_STYLE_VARIABLES } from '../themes/styles'; import { BLOCK_NAME as MRC_STEPS, StepsBlock } from './mrc_steps' @@ -47,12 +46,39 @@ const JUMP_TO_STEP_BLOCK = { * Block initialization. */ init: function (this: JumpToStepBlock): void { + this.mrcHasWarning = false; + + this.setStyle(MRC_STYLE_VARIABLES); + + // Create a custom dropdown that accepts any value and displays it correctly + class CustomStepDropdown extends Blockly.FieldDropdown { + override doClassValidation_(newValue?: string): string | null { + // Always accept the value, even if it's not in the current options + return newValue ?? null; + } + + override getText_(): string { + // Always return the current value, even if not in options + return this.value_ || ''; + } + } + + const blockRef = this; + // Use a function to dynamically generate options when dropdown opens + const dropdown: Blockly.Field = new CustomStepDropdown( + function() { + // This function will be called to regenerate options when dropdown opens + return blockRef.getStepOptions(); + } + ); + + dropdown.setValidator(this.validateStepSelection.bind(this)); + this.appendDummyInput() .appendField(Blockly.Msg.JUMP_TO) - .appendField(createFieldNonEditableText(''), FIELD_STEP_NAME); + .appendField(dropdown, FIELD_STEP_NAME); this.setPreviousStatement(true, null); this.setInputsInline(true); - this.setStyle(MRC_STYLE_VARIABLES); this.setTooltip(() => { const stepName = this.getFieldValue(FIELD_STEP_NAME); let tooltip = Blockly.Msg.JUMP_TO_STEP_TOOLTIP; @@ -60,6 +86,40 @@ const JUMP_TO_STEP_BLOCK = { return tooltip; }); }, + getStepOptions: function(this: JumpToStepBlock): [string, string][] { + const legalStepNames: string[] = []; + + const rootBlock: Blockly.Block | null = this.getRootBlock(); + if (rootBlock && rootBlock.type === MRC_STEPS) { + const stepsBlock = rootBlock as StepsBlock; + legalStepNames.push(...stepsBlock.mrcGetStepNames()); + } + + // Get the field to check its value + const field = this.getField(FIELD_STEP_NAME) as Blockly.FieldDropdown | null; + const currentValue = field?.getValue(); + + // Always include the current field value if it exists and isn't already in the list + if (currentValue && currentValue !== '' && !legalStepNames.includes(currentValue)) { + legalStepNames.unshift(currentValue); + } + + if (legalStepNames.length === 0) { + return [[Blockly.Msg.NO_STEPS, '']]; + } + + return legalStepNames.map(name => [name, name]); + }, + validateStepSelection: function(this: JumpToStepBlock, newValue: string): string { + // Clear any previous warnings + this.setWarningText(null, WARNING_ID_NOT_IN_STEP); + this.mrcHasWarning = false; + + // Options will be regenerated automatically on next dropdown open + // via the function passed to the CustomStepDropdown constructor + + return newValue; + }, /** * mrcOnMove is called when a JumpToStepBlock is moved. */ @@ -80,14 +140,31 @@ const JUMP_TO_STEP_BLOCK = { legalStepNames.push(...stepsBlock.mrcGetStepNames()); } - if (legalStepNames.includes(this.getFieldValue(FIELD_STEP_NAME))) { + const currentStepName = this.getFieldValue(FIELD_STEP_NAME); + + if (legalStepNames.includes(currentStepName)) { // If this blocks's step name is in legalStepNames, it's good. this.setWarningText(null, WARNING_ID_NOT_IN_STEP); this.mrcHasWarning = false; } else { // 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); + // Provide a more specific message depending on the situation + let warningMessage: string; + if (rootBlock.type === MRC_STEPS) { + // We're in a steps block but the step doesn't exist + if (currentStepName && currentStepName !== '') { + const messageTemplate = Blockly.Msg.STEP_DOES_NOT_EXIST_IN_STEPS; + warningMessage = messageTemplate.replace('%1', currentStepName); + } else { + warningMessage = Blockly.Msg.NO_STEP_SELECTED; + } + } else { + // We're not even in a steps block + warningMessage = Blockly.Msg.JUMP_CAN_ONLY_GO_IN_THEIR_STEPS_BLOCK; + } + + this.setWarningText(warningMessage, WARNING_ID_NOT_IN_STEP); const icon = this.getIcon(Blockly.icons.IconType.WARNING); if (icon) { icon.setBubbleVisible(true); @@ -96,6 +173,12 @@ const JUMP_TO_STEP_BLOCK = { } } }, + /** + * Called to recheck step validity. Used when steps are changed. + */ + mrcCheckStep: function(this: JumpToStepBlock): void { + this.checkBlockPlacement(); + }, }; export const setup = function () { diff --git a/src/blocks/mrc_steps.ts b/src/blocks/mrc_steps.ts index 323cf226..1fe06ab4 100644 --- a/src/blocks/mrc_steps.ts +++ b/src/blocks/mrc_steps.ts @@ -26,8 +26,9 @@ import { MRC_STYLE_STEPS } from '../themes/styles'; import { ExtendedPythonGenerator } from '../editor/extended_python_generator'; import { createStepFieldFlydown } from '../fields/field_flydown'; import { NONCOPYABLE_BLOCK } from './noncopyable_block'; -import { renameSteps as updateJumpToStepBlocks } from './mrc_jump_to_step'; +import { renameSteps as updateJumpToStepBlocks, BLOCK_NAME as MRC_JUMP_TO_STEP } from './mrc_jump_to_step'; import * as stepContainer from './mrc_step_container' +import { findConnectedBlocksOfType } from './utils/find_connected_blocks'; import { createBooleanShadowValue } from './utils/value'; import * as toolboxItems from '../toolbox/items'; @@ -152,6 +153,28 @@ const STEPS = { // Update jump blocks for any renamed steps. updateJumpToStepBlocks(this.workspace, mapOldStepNameToNewStepName); } + + // Update all mrc_jump_to_step blocks to recheck validity + this.mrcCheckJumpBlocks(); + }, + /** + * Checks all mrc_jump_to_step blocks within this steps block to revalidate + * that their step names are still valid. + */ + mrcCheckJumpBlocks: function(this: StepsBlock): void { + // Check each statement input for jump blocks + for (let i = 0; i < this.mrcStepNames.length; i++) { + const statementInput = this.getInput(INPUT_STATEMENT_PREFIX + i); + const nextBlock = statementInput?.connection?.targetBlock(); + if (nextBlock) { + const jumpBlocks = findConnectedBlocksOfType(nextBlock, MRC_JUMP_TO_STEP); + jumpBlocks.forEach((block) => { + if ('mrcCheckStep' in block && typeof block.mrcCheckStep === 'function') { + block.mrcCheckStep(); + } + }); + } + } }, /** * mrcOnMutatorOpen is called when the mutator on an StepsBlock is opened. diff --git a/src/blocks/tokens.ts b/src/blocks/tokens.ts index c3212f6b..729bf037 100644 --- a/src/blocks/tokens.ts +++ b/src/blocks/tokens.ts @@ -141,6 +141,9 @@ export function customTokens(t: (key: string) => string): typeof Blockly.Msg { JUMP_TO: t('BLOCKLY.JUMP_TO'), JUMP_TO_STEP_TOOLTIP: t('BLOCKLY.TOOLTIP.JUMP_TO_STEP'), JUMP_CAN_ONLY_GO_IN_THEIR_STEPS_BLOCK: t('BLOCKLY.JUMP_CAN_ONLY_GO_IN_THEIR_STEPS_BLOCK'), + STEP_DOES_NOT_EXIST_IN_STEPS: t('BLOCKLY.STEP_DOES_NOT_EXIST_IN_STEPS'), + NO_STEP_SELECTED: t('BLOCKLY.NO_STEP_SELECTED'), + NO_STEPS: t('BLOCKLY.NO_STEPS'), } }; diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index c3474222..a6125d17 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -132,6 +132,9 @@ "PARAMETERS": "Parameters", "PARAMETERS_CAN_ONLY_GO_IN_THEIR_METHODS_BLOCK": "Parameters can only go in their method's block", "JUMP_CAN_ONLY_GO_IN_THEIR_STEPS_BLOCK": "Jump can only go in their step's block", + "STEP_DOES_NOT_EXIST_IN_STEPS": "Step \"%1\" does not exist in this steps block.", + "NO_STEP_SELECTED": "No step selected.", + "NO_STEPS": "(no steps)", "CLASS_METHOD_DEF_ALREADY_ON_WORKSPACE": "This method is already on the workspace.", "EVENT_HANDLER_ALREADY_ON_WORKSPACE": "This event handler is already on the workspace.", "EVENT_HANDLER_ROBOT_EVENT_NOT_FOUND": "This block is an event handler for an event that no longer exists.", diff --git a/src/i18n/locales/es/translation.json b/src/i18n/locales/es/translation.json index 0a79eb5f..f77646e5 100644 --- a/src/i18n/locales/es/translation.json +++ b/src/i18n/locales/es/translation.json @@ -133,6 +133,9 @@ "PARAMETERS": "Parámetros", "PARAMETERS_CAN_ONLY_GO_IN_THEIR_METHODS_BLOCK": "Los parámetros solo pueden ir en el bloque de su método", "JUMP_CAN_ONLY_GO_IN_THEIR_STEPS_BLOCK": "El salto solo puede ir en el bloque de su paso", + "STEP_DOES_NOT_EXIST_IN_STEPS": "El paso \"%1\" no existe en este bloque de pasos.", + "NO_STEP_SELECTED": "No se ha seleccionado ningún paso.", + "NO_STEPS": "(sin pasos)", "CLASS_METHOD_DEF_ALREADY_ON_WORKSPACE": "Este método ya está en el espacio de trabajo.", "EVENT_HANDLER_ALREADY_ON_WORKSPACE": "Este controlador de eventos ya está en el espacio de trabajo.", "EVENT_HANDLER_ROBOT_EVENT_NOT_FOUND": "Este bloque es un controlador de eventos para un evento que ya no existe.", diff --git a/src/i18n/locales/he/translation.json b/src/i18n/locales/he/translation.json index 5196a209..5ae08c76 100644 --- a/src/i18n/locales/he/translation.json +++ b/src/i18n/locales/he/translation.json @@ -132,6 +132,9 @@ "PARAMETERS": "פרמטרים", "PARAMETERS_CAN_ONLY_GO_IN_THEIR_METHODS_BLOCK": "פרמטרים יכולים ללכת רק בבלוק השיטה שלהם", "JUMP_CAN_ONLY_GO_IN_THEIR_STEPS_BLOCK": "קפיצה יכולה ללכת רק בבלוק הצעד שלה", + "STEP_DOES_NOT_EXIST_IN_STEPS": "הצעד \"%1\" לא קיים בבלוק הצעדים הזה.", + "NO_STEP_SELECTED": "לא נבחר צעד.", + "NO_STEPS": "(אין צעדים)", "CLASS_METHOD_DEF_ALREADY_ON_WORKSPACE": "שיטה זו כבר קיימת בסביבת העבודה.", "EVENT_HANDLER_ALREADY_ON_WORKSPACE": "מטפל אירועים זה כבר נמצא במרחב העבודה.", "EVENT_HANDLER_ROBOT_EVENT_NOT_FOUND": "הבלוק הזה הוא מנהל אירועים לאירוע שכבר לא קיים.",