diff --git a/src/blocks/mrc_jump_to_step.ts b/src/blocks/mrc_jump_to_step.ts index 83f42aa6..4bf409e1 100644 --- a/src/blocks/mrc_jump_to_step.ts +++ b/src/blocks/mrc_jump_to_step.ts @@ -47,16 +47,21 @@ const JUMP_TO_STEP_BLOCK = { */ init: function (this: JumpToStepBlock): void { this.appendDummyInput() - .appendField('Jump to') + .appendField(Blockly.Msg.JUMP_TO) .appendField(createFieldNonEditableText(''), FIELD_STEP_NAME); this.setPreviousStatement(true, null); this.setInputsInline(true); this.setStyle(MRC_STYLE_VARIABLES); - this.setTooltip('Jump to the specified step.'); + this.setTooltip(() => { + const stepName = this.getFieldValue(FIELD_STEP_NAME); + let tooltip = Blockly.Msg.JUMP_TO_STEP_TOOLTIP; + tooltip = tooltip.replace('{{stepName}}', stepName); + return tooltip; + }); }, /** - * mrcOnMove is called when an EventBlock is moved. - */ + * mrcOnMove is called when a JumpToStepBlock is moved. + */ mrcOnMove: function (this: JumpToStepBlock, _reason: string[]): void { this.checkBlockPlacement(); }, @@ -68,14 +73,14 @@ const JUMP_TO_STEP_BLOCK = { const rootBlock: Blockly.Block | null = this.getRootBlock(); if (rootBlock.type === MRC_STEPS) { - // This block is within a class method definition. + // This block is within a steps block. const stepsBlock = rootBlock as StepsBlock; - // Add the method's parameter names to legalStepNames. + // Add the step names to legalStepNames. legalStepNames.push(...stepsBlock.mrcGetStepNames()); } if (legalStepNames.includes(this.getFieldValue(FIELD_STEP_NAME))) { - // If this blocks's parameter name is in legalParameterNames, it's good. + // If this blocks's step name is in legalStepNames, it's good. this.setWarningText(null, WARNING_ID_NOT_IN_STEP); this.mrcHasWarning = false; } else { @@ -103,3 +108,13 @@ export const pythonFromBlock = function ( return code; }; + +export function renameSteps(workspace: Blockly.Workspace, mapOldStepNameToNewStepName: {[newStepName: string]: string}): void { + workspace.getBlocksByType(BLOCK_NAME, false).forEach((jumpBlock) => { + const stepName = jumpBlock.getFieldValue(FIELD_STEP_NAME); + if (stepName in mapOldStepNameToNewStepName) { + const newStepName = mapOldStepNameToNewStepName[stepName]; + jumpBlock.setFieldValue(newStepName, FIELD_STEP_NAME); + } + }); +} diff --git a/src/blocks/mrc_step_container.ts b/src/blocks/mrc_step_container.ts index c2a96ccf..c8d74f0b 100644 --- a/src/blocks/mrc_step_container.ts +++ b/src/blocks/mrc_step_container.ts @@ -66,6 +66,9 @@ const FIELD_NAME = 'NAME'; export type StepItemBlock = StepItemMixin & Blockly.BlockSvg; interface StepItemMixin extends StepItemMixinType { originalName: string, + conditionShadowState?: any; + conditionTargetConnection?: Blockly.Connection | null; + statementTargetConnection?: Blockly.Connection | null; } type StepItemMixinType = typeof STEP_ITEM; @@ -174,7 +177,7 @@ function onChange(mutatorWorkspace: Blockly.Workspace, event: Blockly.Events.Abs } /** - * Called for mrc_event and mrc_class_method_def blocks when their mutator opesn. + * Called for mrc_steps blocks when their mutator opesn. * Triggers a flyout update and adds an event listener to the mutator workspace. * * @param block The block whose mutator is open. @@ -193,9 +196,9 @@ export function getMutatorIcon(block: Blockly.BlockSvg): Blockly.icons.MutatorIc return new Blockly.icons.MutatorIcon([STEP_ITEM_BLOCK_NAME], block); } -export function createMutatorBlocks(workspace: Blockly.Workspace, stepNames: string[]): Blockly.BlockSvg { +export function createMutatorBlocks(workspace: Blockly.Workspace, stepNames: string[]): StepContainerBlock { // First create the container block. - const containerBlock = workspace.newBlock(STEP_CONTAINER_BLOCK_NAME) as Blockly.BlockSvg; + const containerBlock = workspace.newBlock(STEP_CONTAINER_BLOCK_NAME) as StepContainerBlock; containerBlock.initSvg(); // Then add one step item block for each step. diff --git a/src/blocks/mrc_steps.ts b/src/blocks/mrc_steps.ts index 71aa3cc7..873b67b1 100644 --- a/src/blocks/mrc_steps.ts +++ b/src/blocks/mrc_steps.ts @@ -25,20 +25,20 @@ import { Order } from 'blockly/python'; import { MRC_STYLE_STEPS } from '../themes/styles'; import { ExtendedPythonGenerator } from '../editor/extended_python_generator'; import { createStepFieldFlydown } from '../fields/field_flydown'; -import { BLOCK_NAME as MRC_JUMP_TO_STEP } from './mrc_jump_to_step'; +import { renameSteps as updateJumpToStepBlocks } from './mrc_jump_to_step'; import * as stepContainer from './mrc_step_container' -import * as value from './utils/value'; +import { createBooleanShadowValue } from './utils/value'; import * as toolboxItems from '../toolbox/items'; export const BLOCK_NAME = 'mrc_steps'; const INPUT_CONDITION_PREFIX = 'CONDITION_'; -const INPUT_STEP_PREFIX = 'STEP_'; +const INPUT_STATEMENT_PREFIX = 'STATEMENT_'; /** Extra state for serialising mrc_steps blocks. */ type StepsExtraState = { /** - * The steps + * The step names. */ stepNames: string[], }; @@ -60,7 +60,6 @@ const STEPS = { this.setInputsInline(false); this.setStyle(MRC_STYLE_STEPS); this.setMutator(stepContainer.getMutatorIcon(this)); - this.updateShape_(); }, saveExtraState: function (this: StepsBlock): StepsExtraState { return { @@ -71,65 +70,88 @@ const STEPS = { this.mrcStepNames = state.stepNames; this.updateShape_(); }, - compose: function (this: StepsBlock, containerBlock: Blockly.Block) { - if (containerBlock.type !== stepContainer.STEP_CONTAINER_BLOCK_NAME) { - throw new Error('compose: containerBlock.type should be ' + stepContainer.STEP_CONTAINER_BLOCK_NAME); + /** + * Populate the mutator's dialog with this block's components. + */ + decompose: function (this: StepsBlock, workspace: Blockly.Workspace): stepContainer.StepContainerBlock { + const stepNames: string[] = []; + this.mrcStepNames.forEach(step => { + stepNames.push(step); + }); + return stepContainer.createMutatorBlocks(workspace, stepNames); + }, + /** + * Store condition and statement connections on the StepItemBlocks + */ + saveConnections: function (this: StepsBlock, containerBlock: stepContainer.StepContainerBlock) { + const stepItemBlocks: stepContainer.StepItemBlock[] = containerBlock.getStepItemBlocks(); + for (let i = 0; i < stepItemBlocks.length; i++) { + const stepItemBlock = stepItemBlocks[i]; + const conditionInput = this.getInput(INPUT_CONDITION_PREFIX + i); + stepItemBlock.conditionShadowState = + conditionInput && conditionInput.connection!.getShadowState(true); + stepItemBlock.conditionTargetConnection = + conditionInput && conditionInput.connection!.targetConnection; + const statementInput = this.getInput(INPUT_STATEMENT_PREFIX + i); + stepItemBlock.statementTargetConnection = + statementInput && statementInput.connection!.targetConnection; } - const stepContainerBlock = containerBlock as stepContainer.StepContainerBlock; - const stepItemBlocks: stepContainer.StepItemBlock[] = stepContainerBlock.getStepItemBlocks(); - + }, + /** + * Reconfigure this block based on the mutator dialog's components. + */ + compose: function (this: StepsBlock, containerBlock: stepContainer.StepContainerBlock) { + const mapOldStepNameToNewStepName: {[newStepName: string]: string} = {}; + const conditionShadowStates: Array = []; + const conditionTargetConnections: Array = []; + const statementTargetConnections: Array = []; + + const stepItemBlocks: stepContainer.StepItemBlock[] = containerBlock.getStepItemBlocks(); + + // Iterate through the step item blocks to: + // - Update this.mrcStepNames + // - Keep track of steps that were renamed + // - Collect the condition and statement connections that were saved on the StepItemBlocks. this.mrcStepNames = []; stepItemBlocks.forEach((stepItemBlock) => { - this.mrcStepNames.push(stepItemBlock.getName()); - }); - - // Update jump blocks for any renamed steps - const workspace = this.workspace; - const jumpBlocks = workspace.getBlocksByType(MRC_JUMP_TO_STEP, false); - stepItemBlocks.forEach((stepItemBlock) => { - const oldName = stepItemBlock.getOriginalName(); - const newName = stepItemBlock.getName(); - if (oldName && oldName !== newName) { - jumpBlocks.forEach((jumpBlock) => { - if (jumpBlock.getFieldValue('STEP_NAME') === oldName) { - jumpBlock.setFieldValue(newName, 'STEP_NAME'); - } - }); + const oldStepName = stepItemBlock.getOriginalName(); + const newStepName = stepItemBlock.getName(); + stepItemBlock.setOriginalName(newStepName); + this.mrcStepNames.push(newStepName); + if (oldStepName !== newStepName) { + mapOldStepNameToNewStepName[oldStepName] = newStepName; } + conditionShadowStates.push(stepItemBlock.conditionShadowState); + conditionTargetConnections.push(stepItemBlock.conditionTargetConnection as Blockly.Connection | null); + statementTargetConnections.push(stepItemBlock.statementTargetConnection as Blockly.Connection | null); }); this.updateShape_(); - // Add a shadow True block to each empty condition input. - for (var i = 0; i < this.mrcStepNames.length; i++) { + // Reconnect blocks. + for (let i = 0; i < this.mrcStepNames.length; i++) { + // Reconnect the condition. + conditionTargetConnections[i]?.reconnect(this, INPUT_CONDITION_PREFIX + i); + // Add the boolean shadow block to the condition input. This must be done after the condition + // has been reconnected. If it is done before the condition is reconnected, the shadow will + // become disconnected. + const conditionShadowState = conditionShadowStates[i] || createBooleanShadowValue(true).shadow; const conditionInput = this.getInput(INPUT_CONDITION_PREFIX + i); - if (conditionInput && !conditionInput.connection?.targetConnection) { - const shadowBlock = this.workspace.newBlock('logic_boolean') as Blockly.BlockSvg; - shadowBlock.setShadow(true); - shadowBlock.setFieldValue('TRUE', 'BOOL'); - if (this.workspace.rendered) { - shadowBlock.initSvg(); - shadowBlock.render(); - } - conditionInput.connection?.connect(shadowBlock.outputConnection!); - } + conditionInput?.connection?.setShadowState(conditionShadowState as any); + // Reconnect the statement. + statementTargetConnections[i]?.reconnect(this, INPUT_STATEMENT_PREFIX + i); + } + + if (Object.keys(mapOldStepNameToNewStepName).length) { + // Update jump blocks for any renamed steps. + updateJumpToStepBlocks(this.workspace, mapOldStepNameToNewStepName); } - }, - decompose: function (this: StepsBlock, workspace: Blockly.Workspace) { - const stepNames: string[] = []; - this.mrcStepNames.forEach(step => { - stepNames.push(step); - }); - return stepContainer.createMutatorBlocks(workspace, stepNames); }, /** - * mrcOnMutatorOpen is called when the mutator on an EventBlock is opened. + * mrcOnMutatorOpen is called when the mutator on an StepsBlock is opened. */ mrcOnMutatorOpen: function (this: StepsBlock): void { stepContainer.onMutatorOpen(this); - }, - mrcOnChange: function (this: StepsBlock): void { - }, mrcUpdateStepName: function (this: StepsBlock, step: number, newName: string): string { const oldName = this.mrcStepNames[step]; @@ -152,99 +174,31 @@ const STEPS = { } this.mrcStepNames[step] = currentName; - // Update all mrc_jump_to_step blocks that reference the old name if (oldName !== currentName) { - const workspace = this.workspace; - const jumpBlocks = workspace.getBlocksByType(MRC_JUMP_TO_STEP, false); - jumpBlocks.forEach((jumpBlock) => { - if (jumpBlock.getFieldValue('STEP_NAME') === oldName) { - jumpBlock.setFieldValue(currentName, 'STEP_NAME'); - } - }); + // Update all mrc_jump_to_step blocks that reference the old name + const mapOldStepNameToNewStepName: {[newStepName: string]: string} = {}; + mapOldStepNameToNewStepName[oldName] = currentName; + updateJumpToStepBlocks(this.workspace, mapOldStepNameToNewStepName); } return currentName; }, updateShape_: function (this: StepsBlock): void { - // Build a map of step names to their current input indices - const currentStepMap: { [stepName: string]: number } = {}; - let i = 0; - while (this.getInput(INPUT_CONDITION_PREFIX + i)) { - const conditionInput = this.getInput(INPUT_CONDITION_PREFIX + i); - const field = conditionInput?.fieldRow[0]; - if (field) { - currentStepMap[field.getValue()] = i; - } - i++; - } - - // For each new step position, find where it currently is (if it exists) - for (let j = 0; j < this.mrcStepNames.length; j++) { - const stepName = this.mrcStepNames[j]; - const currentIndex = currentStepMap[stepName]; - - if (currentIndex !== undefined && currentIndex !== j) { - // Step exists but is at wrong position - move it - const conditionConnection = this.getInput(INPUT_CONDITION_PREFIX + currentIndex)?.connection?.targetConnection; - const stepConnection = this.getInput(INPUT_STEP_PREFIX + currentIndex)?.connection?.targetConnection; - - // Temporarily disconnect - if (conditionConnection) { - conditionConnection.disconnect(); - } - if (stepConnection) { - stepConnection.disconnect(); - } - - // Remove old inputs - this.removeInput(INPUT_CONDITION_PREFIX + currentIndex, false); - this.removeInput(INPUT_STEP_PREFIX + currentIndex, false); - - // Create new inputs at correct position - const fieldFlydown = createStepFieldFlydown(stepName, true); - fieldFlydown.setValidator(this.mrcUpdateStepName.bind(this, j)); - - this.appendValueInput(INPUT_CONDITION_PREFIX + j) - .appendField(fieldFlydown) - .setCheck('Boolean') - .appendField(Blockly.Msg.REPEAT_UNTIL); - this.appendStatementInput(INPUT_STEP_PREFIX + j); - - // Reconnect - if (conditionConnection) { - this.getInput(INPUT_CONDITION_PREFIX + j)?.connection?.connect(conditionConnection); - } - if (stepConnection) { - this.getInput(INPUT_STEP_PREFIX + j)?.connection?.connect(stepConnection); - } - - delete currentStepMap[stepName]; - } else if (currentIndex !== undefined) { - // Step is at correct position - just update the field - const conditionInput = this.getInput(INPUT_CONDITION_PREFIX + j); - const field = conditionInput?.fieldRow[0]; - if (field && field.getValue() !== stepName) { - field.setValue(stepName); - } - delete currentStepMap[stepName]; - } else { - // Step doesn't exist - create it - const fieldFlydown = createStepFieldFlydown(stepName, true); - fieldFlydown.setValidator(this.mrcUpdateStepName.bind(this, j)); - - this.appendValueInput(INPUT_CONDITION_PREFIX + j) - .appendField(fieldFlydown) - .setCheck('Boolean') - .appendField(Blockly.Msg.REPEAT_UNTIL); - this.appendStatementInput(INPUT_STEP_PREFIX + j); - } + // Remove all inputs. + for (let i = 0; this.getInput(INPUT_CONDITION_PREFIX + i); i++) { + this.removeInput(INPUT_CONDITION_PREFIX + i); + this.removeInput(INPUT_STATEMENT_PREFIX + i); } - - // Remove any leftover inputs (steps that were deleted) - for (const stepName in currentStepMap) { - const index = currentStepMap[stepName]; - this.removeInput(INPUT_CONDITION_PREFIX + index, false); - this.removeInput(INPUT_STEP_PREFIX + index, false); + // Add inputs for each step. + for (let i = 0; i < this.mrcStepNames.length; i++) { + const stepName = this.mrcStepNames[i]; + const fieldFlydown = createStepFieldFlydown(stepName, true); + fieldFlydown.setValidator(this.mrcUpdateStepName.bind(this, i)); + this.appendValueInput(INPUT_CONDITION_PREFIX + i) + .appendField(fieldFlydown) + .setCheck('Boolean') + .appendField(Blockly.Msg.REPEAT_UNTIL); + this.appendStatementInput(INPUT_STATEMENT_PREFIX + i); } }, mrcGetStepNames: function (this: StepsBlock): string[] { @@ -276,11 +230,11 @@ export const pythonFromBlock = function ( code += generator.INDENT + 'match self._current_step:\n'; block.mrcStepNames.forEach((stepName, index) => { code += generator.INDENT.repeat(2) + `case "${stepName}":\n`; - let stepCode = generator.statementToCode(block, INPUT_STEP_PREFIX + index); + const stepCode = generator.statementToCode(block, INPUT_STATEMENT_PREFIX + index); if (stepCode !== '') { code += generator.prefixLines(stepCode, generator.INDENT.repeat(2)); } - let conditionCode = generator.valueToCode(block, INPUT_CONDITION_PREFIX + index, Order.NONE) || 'False'; + const conditionCode = generator.valueToCode(block, INPUT_CONDITION_PREFIX + index, Order.NONE) || 'False'; code += generator.INDENT.repeat(3) + 'if ' + conditionCode + ':\n'; if (index === block.mrcStepNames.length - 1) { code += generator.INDENT.repeat(4) + 'self._current_step = None\n'; @@ -300,6 +254,6 @@ export function createStepsBlock(): toolboxItems.Block { }; const fields: {[key: string]: any} = {}; const inputs: {[key: string]: any} = {}; - inputs[INPUT_CONDITION_PREFIX + 0] = value.createBooleanShadowValue(true); + inputs[INPUT_CONDITION_PREFIX + 0] = createBooleanShadowValue(true); return new toolboxItems.Block(BLOCK_NAME, extraState, fields, inputs); } diff --git a/src/blocks/tokens.ts b/src/blocks/tokens.ts index 0be5fc91..bcc7109f 100644 --- a/src/blocks/tokens.ts +++ b/src/blocks/tokens.ts @@ -131,6 +131,8 @@ export function customTokens(t: (key: string) => string): typeof Blockly.Msg { COMMENT_DEFAULT_TEXT: t('BLOCKLY.COMMENT_DEFAULT_TEXT'), STEPS: t('BLOCKLY.STEPS'), REPEAT_UNTIL: t('BLOCKLY.REPEAT_UNTIL'), + 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'), } }; diff --git a/src/fields/field_flydown.ts b/src/fields/field_flydown.ts index 5b2dcac8..f178d7ae 100644 --- a/src/fields/field_flydown.ts +++ b/src/fields/field_flydown.ts @@ -373,7 +373,7 @@ export class FieldFlydown extends Blockly.FieldTextInput { } } -export function createParameterBlock(paramName: string): Blockly.utils.toolbox.FlyoutDefinition { +function createParameterBlock(paramName: string): Blockly.utils.toolbox.FlyoutDefinition { return { contents: [ { @@ -387,7 +387,7 @@ export function createParameterBlock(paramName: string): Blockly.utils.toolbox.F }; } -export function createAdvanceToBlock(stepName: string): Blockly.utils.toolbox.FlyoutDefinition { +function createJumpToStepBlock(stepName: string): Blockly.utils.toolbox.FlyoutDefinition { return { contents: [ { @@ -413,6 +413,6 @@ export function createParameterFieldFlydown(paramName: string, isEditable: boole return new FieldFlydown(paramName, isEditable, createParameterBlock); } -export function createStepFieldFlydown(paramName: string, isEditable: boolean): Blockly.Field { - return new FieldFlydown(paramName, isEditable, createAdvanceToBlock); +export function createStepFieldFlydown(stepName: string, isEditable: boolean): Blockly.Field { + return new FieldFlydown(stepName, isEditable, createJumpToStepBlock); } \ No newline at end of file diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index 6f4e4f74..668e3647 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -150,6 +150,7 @@ "TO": "to", "STEPS": "steps", "REPEAT_UNTIL": "repeat until", + "JUMP_TO": "jump to", "CUSTOM_EVENTS_LABEL": "Custom Events", "CUSTOM_METHODS_LABEL": "Custom Methods", "MORE_ROBOT_METHODS_LABEL": "More Robot Methods", @@ -165,6 +166,7 @@ "OPMODE_GROUP": "An optional group to group OpModes on Driver Station", "COMPONENTS": "These components are visible in this mechanism, the robot, and all opmodes.", "PRIVATE_COMPONENTS": "These components will not be visible in the robot or opmodes. They are only accessible within this mechanism.", + "JUMP_TO_STEP": "Jump to the step named {{stepName}}.", "CALL_BUILTIN_FUNCTION": "Calls the builtin function {{functionName}}.", "CALL_MODULE_FUNCTION": "Calls the module function {{moduleName}}.{{functionName}}.", "CALL_STATIC_METHOD": "Calls the static method {{className}}.{{functionName}}.", diff --git a/src/i18n/locales/es/translation.json b/src/i18n/locales/es/translation.json index 3f568ae5..907bfa43 100644 --- a/src/i18n/locales/es/translation.json +++ b/src/i18n/locales/es/translation.json @@ -151,6 +151,7 @@ "TO": "a", "STEPS": "pasos", "REPEAT_UNTIL": "repetir hasta", + "JUMP_TO": "saltar a", "CUSTOM_EVENTS_LABEL": "Eventos Personalizados", "CUSTOM_METHODS_LABEL": "Métodos Personalizados", "MORE_ROBOT_METHODS_LABEL": "Más Métodos del Robot", @@ -166,6 +167,7 @@ "OPMODE_GROUP": "Un grupo opcional para agrupar OpModes en la Estación del Conductor", "COMPONENTS": "Estos componentes son visibles en este mecanismo, el robot y todos los opmodes.", "PRIVATE_COMPONENTS": "Estos componentes no serán visibles en el robot o en los opmodes. Solo son accesibles dentro de este mecanismo.", + "JUMP_TO_STEP": "Saltar al paso llamado {{stepName}}.", "CALL_BUILTIN_FUNCTION": "Llama a la función incorporada {{functionName}}.", "CALL_MODULE_FUNCTION": "Llama a la función del módulo {{moduleName}}.{{functionName}}.", "CALL_STATIC_METHOD": "Llama al método estático {{className}}.{{functionName}}.", diff --git a/src/i18n/locales/he/translation.json b/src/i18n/locales/he/translation.json index 061c0fd7..56f6e55d 100644 --- a/src/i18n/locales/he/translation.json +++ b/src/i18n/locales/he/translation.json @@ -148,6 +148,9 @@ "GET": "קבל", "SET": "הגדר", "TO": "ל", + "STEPS": "צעדים", + "REPEAT_UNTIL": "לחזור על כך עד", + "JUMP_TO": "לקפוץ אל", "CUSTOM_EVENTS_LABEL": "אירועים מותאמים אישית", "CUSTOM_METHODS_LABEL": "מתודות מותאמות אישית", "MORE_ROBOT_METHODS_LABEL": "מתודות נוספות לרובוט", @@ -163,6 +166,7 @@ "OPMODE_GROUP": "קבוצה אופציונלית לארגון אופמודים באפליקציית ה־Driver Station", "COMPONENTS": "הרכיבים האלה גלויים במנגנון הזה, ברובוט ובכל האופמודים.", "PRIVATE_COMPONENTS": "הרכיבים האלה לא יהיו גלויים ברובוט או באופמודים. הם נגישים רק בתוך המנגנון הזה.", + "JUMP_TO_STEP": "קפוץ לשלב בשם {{stepName}}.", "CALL_BUILTIN_FUNCTION": "קורא לפונקציה מובנית בשם {{functionName}}.", "CALL_MODULE_FUNCTION": "קורא לפונקציה {{functionName}} במודול {{moduleName}}.", "CALL_STATIC_METHOD": "קורא למתודה סטטית {{className}}.{{functionName}}.",