From b14e1a0c9f6f44046129a798e6dff7cffd0f1a31 Mon Sep 17 00:00:00 2001 From: Alan Smith Date: Mon, 5 May 2025 17:44:49 -0400 Subject: [PATCH 01/12] Get rid of holdover --- src/toolbox/methods_category.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/toolbox/methods_category.ts b/src/toolbox/methods_category.ts index 30097f3a..d41ced35 100644 --- a/src/toolbox/methods_category.ts +++ b/src/toolbox/methods_category.ts @@ -87,10 +87,6 @@ export class MethodsCategory { // Add a block that lets the user define a new method. contents.push( - { - kind: 'block', - type: 'mrc_mechanism_container_holder', - }, { kind: 'label', text: 'Custom Methods', From fbd2c0c067be1eafe1fd58a3973b8c18f455c5dc Mon Sep 17 00:00:00 2001 From: Alan Smith Date: Tue, 6 May 2025 20:26:18 -0400 Subject: [PATCH 02/12] Events in progress --- src/blocks/mrc_class_method_def.ts | 2 +- src/blocks/mrc_event.ts | 112 +++++++++++++++++++ src/blocks/mrc_mechanism_component_holder.ts | 9 +- src/blocks/setup_custom_blocks.ts | 4 +- src/themes/styles.ts | 7 ++ src/toolbox/event_category.ts | 15 +++ src/toolbox/toolbox.ts | 2 + 7 files changed, 146 insertions(+), 5 deletions(-) create mode 100644 src/blocks/mrc_event.ts create mode 100644 src/toolbox/event_category.ts diff --git a/src/blocks/mrc_class_method_def.ts b/src/blocks/mrc_class_method_def.ts index b4696dfc..feefb62f 100644 --- a/src/blocks/mrc_class_method_def.ts +++ b/src/blocks/mrc_class_method_def.ts @@ -30,7 +30,7 @@ import { renameMethodCallers, mutateMethodCallers } from './mrc_call_python_func export const BLOCK_NAME = 'mrc_class_method_def'; -const MUTATOR_BLOCK_NAME = 'methods_mutatorarg'; +export const MUTATOR_BLOCK_NAME = 'methods_mutatorarg'; const PARAM_CONTAINER_BLOCK_NAME = 'method_param_container'; export type Parameter = { diff --git a/src/blocks/mrc_event.ts b/src/blocks/mrc_event.ts new file mode 100644 index 00000000..d9bdc89b --- /dev/null +++ b/src/blocks/mrc_event.ts @@ -0,0 +1,112 @@ +/** + * @license + * Copyright 2025 Porpoiseful LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Creates an event that can be fired + * @author alan@porpoiseful.com (Alan Smith) + */ +import * as Blockly from 'blockly'; +import { Order } from 'blockly/python'; + +import { MRC_STYLE_EVENTS } from '../themes/styles' +import { ExtendedPythonGenerator } from '../editor/extended_python_generator'; +import { MUTATOR_BLOCK_NAME } from './mrc_class_method_def' + +export const BLOCK_NAME = 'mrc_event'; +export const OUTPUT_NAME = 'mrc_event'; + +export type Parameter = { + name: string, + type?: string, +}; + +type EventExtraState = { + params?: Parameter[], +} + +type EventBlock = Blockly.Block & EventMixin & Blockly.BlockSvg; + +interface EventMixin extends EventMixinType { + mrcParams: Parameter[], +} +type EventMixinType = typeof EVENT; + +const EVENT = { + /** + * Block initialization. + */ + init: function (this: EventBlock): void { + this.setStyle(MRC_STYLE_EVENTS); + this.appendDummyInput() + .appendField(new Blockly.FieldTextInput('my_event'), 'NAME'); + this.setPreviousStatement(true, OUTPUT_NAME); + this.setNextStatement(true, OUTPUT_NAME); + this.setMutator(new Blockly.icons.MutatorIcon([MUTATOR_BLOCK_NAME], this)); + }, + + /** + * Returns the state of this block as a JSON serializable object. + */ + saveExtraState: function (this: EventBlock): EventExtraState { + const extraState: EventExtraState = { + }; + extraState.params = []; + if (this.mrcParams) { + this.mrcParams.forEach((arg) => { + extraState.params!.push({ + 'name': arg.name, + 'type': arg.type, + }); + }); + } + return extraState; + }, + /** + * Applies the given state to this block. + */ + loadExtraState: function (this: EventBlock, extraState: EventExtraState): void { + this.mrcParams = []; + + if (extraState.params) { + extraState.params.forEach((arg) => { + this.mrcParams.push({ + 'name': arg.name, + 'type': arg.type, + }); + }); + } + this.mrcParams = extraState.params ? extraState.params : []; + this.updateBlock_(); + }, + /** + * Update the block to reflect the newly loaded extra state. + */ + updateBlock_: function (this: EventBlock): void { + } +} + +export const setup = function () { + Blockly.Blocks[BLOCK_NAME] = EVENT; +} + +export const pythonFromBlock = function ( + block: EventBlock, + generator: ExtendedPythonGenerator, +) { + //TODO (Alan): What should this do here?? + return ''; +} diff --git a/src/blocks/mrc_mechanism_component_holder.ts b/src/blocks/mrc_mechanism_component_holder.ts index b72b1810..372fb439 100644 --- a/src/blocks/mrc_mechanism_component_holder.ts +++ b/src/blocks/mrc_mechanism_component_holder.ts @@ -28,8 +28,9 @@ import { ExtendedPythonGenerator } from '../editor/extended_python_generator'; import { OUTPUT_NAME as MECHANISM_OUTPUT } from './mrc_mechanism'; import { BLOCK_NAME as MRC_MECHANISM_NAME } from './mrc_mechanism'; import { BLOCK_NAME as MRC_COMPONENT_NAME } from './mrc_component'; - import { OUTPUT_NAME as COMPONENT_OUTPUT } from './mrc_component'; +import { BLOCK_NAME as MRC_EVENT_NAME } from './mrc_event'; +import { OUTPUT_NAME as EVENT_OUTPUT } from './mrc_event'; export const BLOCK_NAME = 'mrc_mechanism_component_holder'; @@ -69,12 +70,14 @@ const MECHANISM_COMPONENT_HOLDER = { this.setInputsInline(false); this.appendStatementInput('MECHANISMS').setCheck(MECHANISM_OUTPUT).appendField('Mechanisms'); this.appendStatementInput('COMPONENTS').setCheck(COMPONENT_OUTPUT).appendField('Components'); + this.appendStatementInput('EVENTS').setCheck(EVENT_OUTPUT).appendField('Events'); + 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); }, saveExtraState: function (this: MechanismComponentHolderBlock): MechanismComponentHolderExtraState { const extraState: MechanismComponentHolderExtraState = { @@ -140,7 +143,7 @@ function pythonFromBlockInRobot(block: MechanismComponentHolderBlock, generator: if (body != '') { code += body; } - + generator.addClassMethodDefinition('define_hardware', code); } diff --git a/src/blocks/setup_custom_blocks.ts b/src/blocks/setup_custom_blocks.ts index 645e888c..c34716a0 100644 --- a/src/blocks/setup_custom_blocks.ts +++ b/src/blocks/setup_custom_blocks.ts @@ -13,6 +13,7 @@ import * as Component from './mrc_component'; import * as MechanismContainerHolder from './mrc_mechanism_component_holder'; import * as Port from './mrc_port'; import * as OpModeDetails from './mrc_opmode_details'; +import * as Event from './mrc_event'; const customBlocks = [ CallPythonFunction, @@ -28,7 +29,8 @@ const customBlocks = [ Component, MechanismContainerHolder, Port, - OpModeDetails + OpModeDetails, + Event ]; export const setup = function(forBlock: any) { diff --git a/src/themes/styles.ts b/src/themes/styles.ts index ef86d0cc..428b6775 100644 --- a/src/themes/styles.ts +++ b/src/themes/styles.ts @@ -30,6 +30,7 @@ export const MRC_STYLE_CLASS_BLOCKS = 'mrc_style_class_blocks'; export const MRC_CATEGORY_STYLE_METHODS = 'mrc_category_style_methods'; export const MRC_STYLE_MECHANISMS = 'mrc_style_mechanisms'; export const MRC_STYLE_COMPONENTS = 'mrc_style_components'; +export const MRC_STYLE_EVENTS = 'mrc_style_events'; export const MRC_STYLE_PORTS = 'mrc_style_ports'; export const add_mrc_styles = function (theme: Blockly.Theme): Blockly.Theme { @@ -39,6 +40,12 @@ export const add_mrc_styles = function (theme: Blockly.Theme): Blockly.Theme { colourTertiary: "#664984", hat: "" }); + theme.setBlockStyle(MRC_STYLE_EVENTS, { + colourPrimary: "#805ba5", + colourSecondary: "#e6deed", + colourTertiary: "#664984", + hat: "" + }); theme.setBlockStyle(MRC_STYLE_ENUM, { colourPrimary: "#5ba5a5", colourSecondary: "#deeded", diff --git a/src/toolbox/event_category.ts b/src/toolbox/event_category.ts new file mode 100644 index 00000000..3f4b198f --- /dev/null +++ b/src/toolbox/event_category.ts @@ -0,0 +1,15 @@ +export const category = +{ + kind: 'category', + name: 'Event', + contents: [ + { + kind: 'label', + text: 'New Event', + }, + { + kind: 'block', + type: 'mrc_event', + } + ] +} \ No newline at end of file diff --git a/src/toolbox/toolbox.ts b/src/toolbox/toolbox.ts index 023c6a34..a4d9f217 100644 --- a/src/toolbox/toolbox.ts +++ b/src/toolbox/toolbox.ts @@ -31,6 +31,7 @@ import {category as methodsCategory} from './methods_category'; import {category as componentSampleCategory} from './component_samples_category'; import {category as hardwareCategory} from './hardware_category'; import {category as robotCategory} from './robot_category'; +import {category as eventCategory} from './event_category'; export function getToolboxJSON( opt_includeExportedBlocksFromProject: toolboxItems.ContentsType[], @@ -83,6 +84,7 @@ export function getToolboxJSON( }, methodsCategory, hardwareCategory, + eventCategory, //componentSampleCategory, ]); From c59cff78abd66398fccf5e6a11992f383850b854 Mon Sep 17 00:00:00 2001 From: Alan Smith Date: Sat, 14 Jun 2025 12:54:11 -0400 Subject: [PATCH 03/12] Saw the right way to do this based off field from app inventor --- src/fields/FieldNonEditableText.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/fields/FieldNonEditableText.ts b/src/fields/FieldNonEditableText.ts index df4e9679..f8816564 100644 --- a/src/fields/FieldNonEditableText.ts +++ b/src/fields/FieldNonEditableText.ts @@ -24,7 +24,7 @@ import * as Blockly from 'blockly/core'; class FieldNonEditableText extends Blockly.FieldTextInput { constructor(value: string) { super(value); - this.CURSOR = ''; + this.EDITABLE = false; // This field is not editable } protected override showEditor_() { From ee42fa1a6b77b666df5601bf5e82b1cca9233067 Mon Sep 17 00:00:00 2001 From: Alan Smith Date: Mon, 16 Jun 2025 12:34:58 -0400 Subject: [PATCH 04/12] First pass at handling parameters --- src/blocks/mrc_class_method_def.ts | 227 +++++++++---------- src/blocks/mrc_get_parameter.ts | 64 ++++++ src/blocks/setup_custom_blocks.ts | 4 +- src/fields/field_flydown.ts | 339 +++++++++++++++++++++++++++++ 4 files changed, 520 insertions(+), 114 deletions(-) create mode 100644 src/blocks/mrc_get_parameter.ts create mode 100644 src/fields/field_flydown.ts diff --git a/src/blocks/mrc_class_method_def.ts b/src/blocks/mrc_class_method_def.ts index feefb62f..2068b6c6 100644 --- a/src/blocks/mrc_class_method_def.ts +++ b/src/blocks/mrc_class_method_def.ts @@ -22,6 +22,7 @@ import * as Blockly from 'blockly'; import { MRC_STYLE_CLASS_BLOCKS } from '../themes/styles'; import { createFieldNonEditableText } from '../fields/FieldNonEditableText' +import { createFieldFlydown } from '../fields/field_flydown'; import * as ChangeFramework from './utils/change_framework' import { getLegalName } from './utils/python'; import { Order } from 'blockly/python'; @@ -31,7 +32,7 @@ import { renameMethodCallers, mutateMethodCallers } from './mrc_call_python_func export const BLOCK_NAME = 'mrc_class_method_def'; export const MUTATOR_BLOCK_NAME = 'methods_mutatorarg'; -const PARAM_CONTAINER_BLOCK_NAME = 'method_param_container'; +const PARAM_CONTAINER_BLOCK_NAME = 'method_param_container'; export type Parameter = { name: string, @@ -87,8 +88,7 @@ const CLASS_METHOD_DEF = { */ init: function (this: ClassMethodDefBlock): void { this.appendDummyInput("TITLE") - .appendField('', 'NAME') - .appendField('', 'PARAMS'); + .appendField('', 'NAME'); this.setOutput(false); this.setStyle(MRC_STYLE_CLASS_BLOCKS); this.appendStatementInput('STACK').appendField(''); @@ -147,11 +147,11 @@ const CLASS_METHOD_DEF = { updateBlock_: function (this: ClassMethodDefBlock): void { const name = this.getFieldValue('NAME'); const input = this.getInput('TITLE'); - if (!input){ + if (!input) { return; } input.removeField('NAME'); - + if (this.mrcCanChangeSignature) { const nameField = new Blockly.FieldTextInput(name); input.insertFieldAt(0, nameField, 'NAME'); @@ -161,7 +161,7 @@ const CLASS_METHOD_DEF = { else { input.insertFieldAt(0, createFieldNonEditableText(name), 'NAME'); //Case because a current bug in blockly where it won't allow passing null to Blockly.Block.setMutator makes it necessary. - (this as Blockly.BlockSvg).setMutator( null ); + (this as Blockly.BlockSvg).setMutator(null); } this.mrcUpdateParams(); }, @@ -171,9 +171,9 @@ const CLASS_METHOD_DEF = { let paramBlock = containerBlock.getInputTargetBlock('STACK'); while (paramBlock && !paramBlock.isInsertionMarker()) { - const param : Parameter = { - name : paramBlock.getFieldValue('NAME'), - type : '' + const param: Parameter = { + name: paramBlock.getFieldValue('NAME'), + type: '' } this.mrcParameters.push(param); paramBlock = @@ -201,40 +201,41 @@ const CLASS_METHOD_DEF = { } return topBlock; }, - mrcUpdateParams: function (this : ClassMethodDefBlock) { - let paramString = ''; - if (this.mrcParameters.length > 0){ - this.mrcParameters.forEach((param) => { - if (paramString != '') { - paramString += ', '; - } - paramString += param.name; - }); - paramString = Blockly.Msg['PROCEDURES_BEFORE_PARAMS'] + ' ' + paramString; - } - // The params field is deterministic based on the mutation, - // no need to fire a change event. - Blockly.Events.disable(); - try { - this.setFieldValue(paramString, 'PARAMS'); - } finally { - Blockly.Events.enable(); + mrcUpdateParams: function (this: ClassMethodDefBlock) { + if (this.mrcParameters.length > 0) { + let input = this.getInput('TITLE'); + if (input) { + this.removeParameterFields(input); + this.mrcParameters.forEach((param) => { + const paramName = 'PARAM_' + param.name; + input.appendField(createFieldFlydown(param.name, false), paramName); + }); + } } }, + removeParameterFields: function (input: Blockly.Input) { + const fieldsToRemove = input.fieldRow + .filter(field => field.name?.startsWith('PARAM_')) + .map(field => field.name!); + + fieldsToRemove.forEach(fieldName => { + input.removeField(fieldName); + }); + }, mrcNameFieldValidator(this: ClassMethodDefBlock, nameField: Blockly.FieldTextInput, name: string): string { - // When the user changes the method name on the block, clear the mrcPythonMethodName field. - this.mrcPythonMethodName = ''; - - // Strip leading and trailing whitespace. - name = name.trim(); - - const legalName = findLegalMethodName(name, this); - const oldName = nameField.getValue(); - if (oldName && oldName !== name && oldName !== legalName) { - // Rename any callers. - renameMethodCallers(this.workspace, oldName, legalName); - } - return legalName; + // When the user changes the method name on the block, clear the mrcPythonMethodName field. + this.mrcPythonMethodName = ''; + + // Strip leading and trailing whitespace. + name = name.trim(); + + const legalName = findLegalMethodName(name, this); + const oldName = nameField.getValue(); + if (oldName && oldName !== name && oldName !== legalName) { + // Rename any callers. + renameMethodCallers(this.workspace, oldName, legalName); + } + return legalName; }, }; @@ -248,21 +249,21 @@ const CLASS_METHOD_DEF = { * @returns Non-colliding name. */ function findLegalMethodName(name: string, block: ClassMethodDefBlock): string { - if (block.isInFlyout) { - // Flyouts can have multiple methods called 'my_method'. - return name; - } - name = name || 'unnamed'; - while (isMethodNameUsed(name, block.workspace, block)) { - // Collision with another method. - const r = name.match(/^(.*?)(\d+)$/); - if (!r) { - name += '2'; - } else { - name = r[1] + (parseInt(r[2]) + 1); + if (block.isInFlyout) { + // Flyouts can have multiple methods called 'my_method'. + return name; } - } - return name; + name = name || 'unnamed'; + while (isMethodNameUsed(name, block.workspace, block)) { + // Collision with another method. + const r = name.match(/^(.*?)(\d+)$/); + if (!r) { + name += '2'; + } else { + name = r[1] + (parseInt(r[2]) + 1); + } + } + return name; } /** @@ -276,25 +277,25 @@ function findLegalMethodName(name: string, block: ClassMethodDefBlock): string { */ function isMethodNameUsed( name: string, workspace: Blockly.Workspace, opt_exclude?: Blockly.Block): boolean { - const nameLowerCase = name.toLowerCase(); - for (const block of workspace.getBlocksByType('mrc_class_method_def')) { - if (block === opt_exclude) { - continue; - } - if (nameLowerCase === block.getFieldValue('NAME').toLowerCase()) { - return true; - } - const classMethodDefBlock = block as ClassMethodDefBlock; - if (classMethodDefBlock.mrcPythonMethodName && - nameLowerCase === classMethodDefBlock.mrcPythonMethodName.toLowerCase()) { - return true; + const nameLowerCase = name.toLowerCase(); + for (const block of workspace.getBlocksByType('mrc_class_method_def')) { + if (block === opt_exclude) { + continue; + } + if (nameLowerCase === block.getFieldValue('NAME').toLowerCase()) { + return true; + } + const classMethodDefBlock = block as ClassMethodDefBlock; + if (classMethodDefBlock.mrcPythonMethodName && + nameLowerCase === classMethodDefBlock.mrcPythonMethodName.toLowerCase()) { + return true; + } } - } - return false; + return false; } const METHOD_PARAM_CONTAINER = { - init: function (this : Blockly.Block) { + init: function (this: Blockly.Block) { this.appendDummyInput("TITLE").appendField('Parameters'); this.appendStatementInput('STACK'); this.setStyle(MRC_STYLE_CLASS_BLOCKS); @@ -303,12 +304,12 @@ const METHOD_PARAM_CONTAINER = { }; type MethodMutatorArgBlock = Blockly.Block & MethodMutatorArgMixin & Blockly.BlockSvg; -interface MethodMutatorArgMixin extends MethodMutatorArgMixinType{ +interface MethodMutatorArgMixin extends MethodMutatorArgMixinType { } type MethodMutatorArgMixinType = typeof METHODS_MUTATORARG; -function setName(block: Blockly.BlockSvg){ +function setName(block: Blockly.BlockSvg) { const parentBlock = ChangeFramework.getParentOfType(block, PARAM_CONTAINER_BLOCK_NAME); if (parentBlock) { const variableBlocks = parentBlock!.getDescendants(true) @@ -318,14 +319,14 @@ function setName(block: Blockly.BlockSvg){ otherNames.push(variableBlock.getFieldValue('NAME')); } }); - const currentName = block.getFieldValue('NAME'); + const currentName = block.getFieldValue('NAME'); block.setFieldValue(getLegalName(currentName, otherNames), 'NAME'); updateMutatorFlyout(block.workspace); } } const METHODS_MUTATORARG = { - init: function (this : MethodMutatorArgBlock) { + init: function (this: MethodMutatorArgBlock) { this.appendDummyInput() .appendField(new Blockly.FieldTextInput(Blockly.Procedures.DEFAULT_ARG), 'NAME'); this.setPreviousStatement(true); @@ -335,17 +336,17 @@ const METHODS_MUTATORARG = { ChangeFramework.registerCallback(MUTATOR_BLOCK_NAME, [Blockly.Events.BLOCK_MOVE, Blockly.Events.BLOCK_CHANGE], this.onBlockChanged); }, onBlockChanged: function (block: Blockly.BlockSvg, blockEvent: Blockly.Events.BlockBase) { - if (blockEvent.type == Blockly.Events.BLOCK_MOVE){ + if (blockEvent.type == Blockly.Events.BLOCK_MOVE) { let blockMoveEvent = blockEvent as Blockly.Events.BlockMove; if (blockMoveEvent.reason?.includes('connect')) { - setName(block); + setName(block); } } - else{ - if(blockEvent.type == Blockly.Events.BLOCK_CHANGE){ + else { + if (blockEvent.type == Blockly.Events.BLOCK_CHANGE) { setName(block); } - } + } }, } @@ -357,24 +358,24 @@ const METHODS_MUTATORARG = { * is what is being updated. */ function updateMutatorFlyout(workspace: Blockly.WorkspaceSvg) { - const usedNames = []; - const blocks = workspace.getBlocksByType(MUTATOR_BLOCK_NAME, false); - for (let i = 0, block; (block = blocks[i]); i++) { - usedNames.push(block.getFieldValue('NAME')); - } - const argValue = Blockly.Variables.generateUniqueNameFromOptions( - Blockly.Procedures.DEFAULT_ARG, - usedNames, - ); - const jsonBlock = { - kind: 'block', - type: MUTATOR_BLOCK_NAME, - fields: { - NAME: argValue, - }, - }; - - workspace.updateToolbox({contents:[jsonBlock]}); + const usedNames = []; + const blocks = workspace.getBlocksByType(MUTATOR_BLOCK_NAME, false); + for (let i = 0, block; (block = blocks[i]); i++) { + usedNames.push(block.getFieldValue('NAME')); + } + const argValue = Blockly.Variables.generateUniqueNameFromOptions( + Blockly.Procedures.DEFAULT_ARG, + usedNames, + ); + const jsonBlock = { + kind: 'block', + type: MUTATOR_BLOCK_NAME, + fields: { + NAME: argValue, + }, + }; + + workspace.updateToolbox({ contents: [jsonBlock] }); } @@ -386,37 +387,37 @@ function updateMutatorFlyout(workspace: Blockly.WorkspaceSvg) { * @internal */ export function mutatorOpenListener(e: Blockly.Events.Abstract) { - if (e.type != Blockly.Events.BUBBLE_OPEN){ - return; - } + if (e.type != Blockly.Events.BUBBLE_OPEN) { + return; + } const bubbleEvent = e as Blockly.Events.BubbleOpen; if ( - !(bubbleEvent.bubbleType === 'mutator' && bubbleEvent.isOpen) || - !bubbleEvent.blockId + !(bubbleEvent.bubbleType === 'mutator' && bubbleEvent.isOpen) || + !bubbleEvent.blockId ) { - return; + return; } const workspaceId = bubbleEvent.workspaceId; const block = Blockly.common - .getWorkspaceById(workspaceId)! - .getBlockById(bubbleEvent.blockId) as Blockly.BlockSvg; + .getWorkspaceById(workspaceId)! + .getBlockById(bubbleEvent.blockId) as Blockly.BlockSvg; if (block.type !== BLOCK_NAME) { - return; + return; } const workspace = ( - block.getIcon(Blockly.icons.MutatorIcon.TYPE) as Blockly.icons.MutatorIcon + block.getIcon(Blockly.icons.MutatorIcon.TYPE) as Blockly.icons.MutatorIcon ).getWorkspace()!; updateMutatorFlyout(workspace); ChangeFramework.setup(workspace); - } +} -export const setup = function() { - Blockly.Blocks[BLOCK_NAME] = CLASS_METHOD_DEF; - Blockly.Blocks[MUTATOR_BLOCK_NAME] = METHODS_MUTATORARG; - Blockly.Blocks[PARAM_CONTAINER_BLOCK_NAME] = METHOD_PARAM_CONTAINER; +export const setup = function () { + Blockly.Blocks[BLOCK_NAME] = CLASS_METHOD_DEF; + Blockly.Blocks[MUTATOR_BLOCK_NAME] = METHODS_MUTATORARG; + Blockly.Blocks[PARAM_CONTAINER_BLOCK_NAME] = METHOD_PARAM_CONTAINER; }; export const pythonFromBlock = function ( @@ -457,11 +458,11 @@ export const pythonFromBlock = function ( // After executing the function body, revisit this block for the return. xfix2 = xfix1; } - if(block.mrcPythonMethodName == '__init__'){ + if (block.mrcPythonMethodName == '__init__') { let class_specific = generator.getClassSpecificForInit(); branch = generator.INDENT + 'super().__init__(' + class_specific + ')\n' + generator.defineClassVariables() + branch; - } + } if (returnValue) { returnValue = generator.INDENT + 'return ' + returnValue + '\n'; } else if (!branch) { diff --git a/src/blocks/mrc_get_parameter.ts b/src/blocks/mrc_get_parameter.ts new file mode 100644 index 00000000..94fe07a3 --- /dev/null +++ b/src/blocks/mrc_get_parameter.ts @@ -0,0 +1,64 @@ +/** + * @license + * Copyright 2025 Porpoiseful LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Create a component with a name of a certain type + * @author alan@porpoiseful.com (Alan Smith) + */ +import * as Blockly from 'blockly'; +import { Order } from 'blockly/python'; + +import { MRC_STYLE_VARIABLES } from '../themes/styles' +import { createFieldNonEditableText } from '../fields/FieldNonEditableText'; +import { ExtendedPythonGenerator } from '../editor/extended_python_generator'; + +export const BLOCK_NAME = 'mrc_get_parameter'; + + +type GetParameterBlock = Blockly.Block & GetParameterMixin; +interface GetParameterMixin extends GetParameterMixinType { +} +type GetParameterMixinType = typeof GET_PARAMETER_BLOCK; + +const GET_PARAMETER_BLOCK = { + /** + * Block initialization. + */ + init: function (this: GetParameterBlock): void { + this.setStyle(MRC_STYLE_VARIABLES); + this.appendDummyInput() + .appendField('parameter') + .appendField(createFieldNonEditableText('parameter'), 'PARAMETER_NAME'); + + //this.setOutput(true, OUTPUT_NAME); + this.setOutput(true); + }, +} + +export const setup = function () { + Blockly.Blocks[BLOCK_NAME] = GET_PARAMETER_BLOCK; +} + +export const pythonFromBlock = function ( + block: GetParameterBlock, + generator: ExtendedPythonGenerator, +) { + //TODO (Alan) : Specify the type here as well + let code = block.getFieldValue('PARAMETER_NAME'); + + return [code, Order.ATOMIC]; +} diff --git a/src/blocks/setup_custom_blocks.ts b/src/blocks/setup_custom_blocks.ts index c34716a0..be5c4583 100644 --- a/src/blocks/setup_custom_blocks.ts +++ b/src/blocks/setup_custom_blocks.ts @@ -14,6 +14,7 @@ import * as MechanismContainerHolder from './mrc_mechanism_component_holder'; import * as Port from './mrc_port'; import * as OpModeDetails from './mrc_opmode_details'; import * as Event from './mrc_event'; +import * as GetParameter from './mrc_get_parameter'; const customBlocks = [ CallPythonFunction, @@ -30,7 +31,8 @@ const customBlocks = [ MechanismContainerHolder, Port, OpModeDetails, - Event + Event, + GetParameter ]; export const setup = function(forBlock: any) { diff --git a/src/fields/field_flydown.ts b/src/fields/field_flydown.ts new file mode 100644 index 00000000..35c8e6c2 --- /dev/null +++ b/src/fields/field_flydown.ts @@ -0,0 +1,339 @@ +/** + * @license + * Copyright 2025 Porpoiseful LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Create a field that has a flydown workspace + * @author alan@porpoiseful.com (Alan Smith) + * + * Heavily inspired by + * https://github.com/mit-cml/blockly-plugins/blob/main/block-lexical-variables/src/fields/field_flydown.js + */ +import * as Blockly from 'blockly'; + +enum FlydownLocation { + DISPLAY_BELOW = 'displayBelow', + DISPLAY_ABOVE = 'displayAbove', + DISPLAY_LEFT = 'displayLeft', + DISPLAY_RIGHT = 'displayRight', +} +class CustomFlyout extends Blockly.VerticalFlyout { + protected x : number = 0; + protected y : number = 0; + + public setPosition(x: number, y: number) : void { + this.x = x; + this.y = y; + this.position(); + } + /** Move the flyout to the edge of the workspace. */ + override position() { + if (!this.isVisible() || !this.targetWorkspace!.isVisible()) { + return; + } + const metricsManager = this.targetWorkspace!.getMetricsManager(); + const targetWorkspaceViewMetrics = metricsManager.getViewMetrics(); + this.height_ = 60; + + const edgeWidth = this.width_ - this.CORNER_RADIUS; + const edgeHeight = this.height_ - 2 * this.CORNER_RADIUS; + + this.mySetBackgroundPath(edgeWidth, edgeHeight); + + const x = this.getX(); + const y = this.getY(); + + this.positionAt_(this.width_, this.height_, x, y); + } + + /** + * Create and set the path for the visible boundaries of the flyout. + * + * @param width The width of the flyout, not including the rounded corners. + * @param height The height of the flyout, not including rounded corners. + */ + private mySetBackgroundPath(width: number, height: number) { + const atRight = false; + const totalWidth = width + this.CORNER_RADIUS; + + // Decide whether to start on the left or right. + const path: Array = [ + 'M ' + (atRight ? totalWidth : 0) + ',0', + ]; + // Top. + path.push('h', atRight ? -width : width); + // Rounded corner. + path.push( + 'a', + this.CORNER_RADIUS, + this.CORNER_RADIUS, + 0, + 0, + atRight ? 0 : 1, + atRight ? -this.CORNER_RADIUS : this.CORNER_RADIUS, + this.CORNER_RADIUS, + ); + // Side closest to workspace. + path.push('v', Math.max(0, height)); + // Rounded corner. + path.push( + 'a', + this.CORNER_RADIUS, + this.CORNER_RADIUS, + 0, + 0, + atRight ? 0 : 1, + atRight ? this.CORNER_RADIUS : -this.CORNER_RADIUS, + this.CORNER_RADIUS, + ); + // Bottom. + path.push('h', atRight ? width : -width); + path.push('z'); + this.svgBackground_!.setAttribute('d', path.join(' ')); + } + + /** + * Calculates the x coordinate for the flyout position. + * + * @returns X coordinate. + */ + getX(): number { + return this.x; + } + + /** + * Calculates the y coordinate for the flyout position. + * + * @returns Y coordinate. + */ + getY(): number { + return this.y; + } +} + +export class FieldFlydown extends Blockly.FieldTextInput { + /** + * Milliseconds to wait before showing flydown after mouseover event on flydown + * field. + * @type {number} + * @const + */ + static TIME_OUT = 500; + + + private displayLocation_: FlydownLocation; + protected fieldCSSClassName: string = 'blocklyFlydownField'; + + private flydown_: CustomFlyout | null = null; + private boundMouseOverHandler_: (e: Event) => void; + private boundMouseOutHandler_: (e: Event) => void; + private showTimeout_: number | null = null; + private hideTimeout_: number | null = null; + + constructor(value: string, isEditable: boolean, displayLocation: FlydownLocation = FlydownLocation.DISPLAY_BELOW) { + super(value); + this.EDITABLE = isEditable; + this.displayLocation_ = displayLocation; + + // Bind the handlers + this.boundMouseOverHandler_ = this.onMouseOver_.bind(this); + this.boundMouseOutHandler_ = this.onMouseOut_.bind(this); + } + + protected override showEditor_() { + if (!this.EDITABLE) { + return; + } + super.showEditor_(); + } + override init(): void { + super.init(); + // Blockly.utils.dom.addClass(this.fieldGroup_, this.fieldCSSClassName); + } + + public override initView() { + super.initView(); + + // Add event listeners instead of using mouseOverWrapper_ + if (this.getClickTarget_()) { + this.getClickTarget_()!.addEventListener('mouseover', this.boundMouseOverHandler_); + this.getClickTarget_()!.addEventListener('mouseout', this.boundMouseOutHandler_); + } + } + + private onMouseOver_(e: Event) { + // Clear any pending hide timeout + if (this.hideTimeout_) { + clearTimeout(this.hideTimeout_); + this.hideTimeout_ = null; + } + + // Add small delay to prevent flickering + this.showTimeout_ = window.setTimeout(() => { + this.showFlydown_(); + }, 250); // 250ms delay + + // This event has been handled. No need to bubble up to the document. + e.stopPropagation(); + } + + private onMouseOut_(e: Event) { + // Clear any pending show timeout + if (this.showTimeout_) { + clearTimeout(this.showTimeout_); + this.showTimeout_ = null; + } + + // Add delay before hiding to allow moving to flydown + this.hideTimeout_ = window.setTimeout(() => { + this.hideFlydown_(); + }, FieldFlydown.TIME_OUT); + } + private createFlydown_(mainWorkspace: Blockly.WorkspaceSvg) { + if (this.flydown_) return; + + try { + // Use the workspace's flyout as a template + const existingFlyout = mainWorkspace.getFlyout?.(); + + if (existingFlyout) { + // Clone the existing flyout's options + this.flydown_ = new CustomFlyout(existingFlyout.getWorkspace().options); + } else { + // Fallback to creating a new flyout if no existing one is found + this.flydown_ = new CustomFlyout(mainWorkspace.options); + } + Blockly.utils.dom.insertAfter( + this.flydown_.createDom("svg")!, + mainWorkspace.getParentSvg(), + ); + + + this.flydown_.init(mainWorkspace); + + const fieldElement = this.getClickTarget_(); + if (!fieldElement) return; + + const fieldRect = fieldElement.getBoundingClientRect(); + const workspaceRect = mainWorkspace.getParentSvg().getBoundingClientRect(); + + const x = fieldRect.right - workspaceRect.left; + const y = fieldRect.top - workspaceRect.top; + + // Set flydown position + this.flydown_.setPosition(x, y); + // Create the flydown without explicit DOM manipulation + this.flydown_.setVisible(true); + + } catch (error) { + console.error('Failed to create flydown:', error); + } + } + + private showFlydown_() { + if (this.flydown_ && this.flydown_.isVisible()) { + return; // Already showing + } + + const workspace = this.getSourceBlock()?.workspace; + if (!workspace) return; + + const mainWorkspace = workspace.isFlyout ? + Blockly.getMainWorkspace() as Blockly.WorkspaceSvg : workspace as Blockly.WorkspaceSvg; + + // Create flydown if it doesn't exist + if (!this.flydown_) { + this.createFlydown_(mainWorkspace); + } + if (this.flydown_) { + // Show the flydown with your blocks + this.flydown_.show(this.getBlocksForFlydown_()); + + // Add hover listeners to flydown to keep it open + this.addFlydownHoverListeners_(); + } + } + private getBlocksForFlydown_() { + const name = this.getText(); + return{ + contents: [ + { + kind: 'block', + type: 'mrc_get_parameter', + fields: { + PARAMETER_NAME: name, + }, + }, + ] + }; + } + + + private addFlydownHoverListeners_() { + if (!this.flydown_) return; + + const flydownSvg = this.flydown_.getWorkspace()?.getParentSvg(); + if (flydownSvg) { + flydownSvg.addEventListener('mouseenter', () => { + if (this.hideTimeout_) { + clearTimeout(this.hideTimeout_); + this.hideTimeout_ = null; + } + }); + + flydownSvg.addEventListener('mouseleave', () => { + this.hideTimeout_ = window.setTimeout(() => { + this.hideFlydown_(); + }, FieldFlydown.TIME_OUT); + }); + } + } + + private hideFlydown_() { + if (this.flydown_) { + this.flydown_.hide(); + } + } + + + override dispose() { + // Clear timeouts + if (this.showTimeout_) { + clearTimeout(this.showTimeout_); + } + if (this.hideTimeout_) { + clearTimeout(this.hideTimeout_); + } + + // Remove event listeners + const clickTarget = this.getClickTarget_(); + if (clickTarget) { + clickTarget.removeEventListener('mouseenter', this.boundMouseOverHandler_); + clickTarget.removeEventListener('mouseleave', this.boundMouseOutHandler_); + } + + if (this.flydown_) { + this.flydown_.dispose(); + this.flydown_ = null; + } + super.dispose(); + } + +} + +export function createFieldFlydown(label: string, isEditable: boolean): Blockly.Field { + return new FieldFlydown(label, isEditable); +} \ No newline at end of file From a2e24922be57859ed0d957efcbd87a2f2cd60ac7 Mon Sep 17 00:00:00 2001 From: Alan Smith Date: Wed, 18 Jun 2025 08:18:37 -0400 Subject: [PATCH 05/12] set the position correctly --- src/fields/field_flydown.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/fields/field_flydown.ts b/src/fields/field_flydown.ts index 35c8e6c2..b6871522 100644 --- a/src/fields/field_flydown.ts +++ b/src/fields/field_flydown.ts @@ -259,6 +259,16 @@ export class FieldFlydown extends Blockly.FieldTextInput { this.createFlydown_(mainWorkspace); } if (this.flydown_) { + const fieldElement = this.getClickTarget_(); + + const fieldRect = fieldElement!.getBoundingClientRect(); + const workspaceRect = mainWorkspace.getParentSvg().getBoundingClientRect(); + + const x = fieldRect.right - workspaceRect.left; + const y = fieldRect.top - workspaceRect.top; + + // Set flydown position + this.flydown_.setPosition(x, y); // Show the flydown with your blocks this.flydown_.show(this.getBlocksForFlydown_()); From 7f15fdb7e15d1706aaf2aa459896bc3cc572b7be Mon Sep 17 00:00:00 2001 From: Alan Smith Date: Thu, 19 Jun 2025 08:53:54 -0400 Subject: [PATCH 06/12] ran format code --- src/fields/field_flydown.ts | 182 ++++++++++++++++++------------------ 1 file changed, 90 insertions(+), 92 deletions(-) diff --git a/src/fields/field_flydown.ts b/src/fields/field_flydown.ts index b6871522..9e3bbefc 100644 --- a/src/fields/field_flydown.ts +++ b/src/fields/field_flydown.ts @@ -31,97 +31,94 @@ enum FlydownLocation { DISPLAY_RIGHT = 'displayRight', } class CustomFlyout extends Blockly.VerticalFlyout { - protected x : number = 0; - protected y : number = 0; + protected x: number = 0; + protected y: number = 0; - public setPosition(x: number, y: number) : void { + public setPosition(x: number, y: number): void { this.x = x; this.y = y; this.position(); } - /** Move the flyout to the edge of the workspace. */ - override position() { - if (!this.isVisible() || !this.targetWorkspace!.isVisible()) { - return; + + override position() { + if (!this.isVisible() || !this.targetWorkspace!.isVisible()) { + return; + } + + const edgeWidth = this.width_ - this.CORNER_RADIUS; + const edgeHeight = this.height_ - 2 * this.CORNER_RADIUS; + + this.mySetBackgroundPath(edgeWidth, edgeHeight); + + const x = this.getX(); + const y = this.getY(); + + this.positionAt_(this.width_, this.height_, x, y); + } + + /** + * Create and set the path for the visible boundaries of the flyout. + * + * @param width The width of the flyout, not including the rounded corners. + * @param height The height of the flyout, not including rounded corners. + */ + private mySetBackgroundPath(width: number, height: number) { + const atRight = false; + const totalWidth = width + this.CORNER_RADIUS; + + // Decide whether to start on the left or right. + const path: Array = [ + 'M ' + (atRight ? totalWidth : 0) + ',0', + ]; + // Top. + path.push('h', atRight ? -width : width); + // Rounded corner. + path.push( + 'a', + this.CORNER_RADIUS, + this.CORNER_RADIUS, + 0, + 0, + atRight ? 0 : 1, + atRight ? -this.CORNER_RADIUS : this.CORNER_RADIUS, + this.CORNER_RADIUS, + ); + // Side closest to workspace. + path.push('v', Math.max(0, height)); + // Rounded corner. + path.push( + 'a', + this.CORNER_RADIUS, + this.CORNER_RADIUS, + 0, + 0, + atRight ? 0 : 1, + atRight ? this.CORNER_RADIUS : -this.CORNER_RADIUS, + this.CORNER_RADIUS, + ); + // Bottom. + path.push('h', atRight ? width : -width); + path.push('z'); + this.svgBackground_!.setAttribute('d', path.join(' ')); + } + + /** + * Calculates the x coordinate for the flyout position. + * + * @returns X coordinate. + */ + getX(): number { + return this.x; + } + + /** + * Calculates the y coordinate for the flyout position. + * + * @returns Y coordinate. + */ + getY(): number { + return this.y; } - const metricsManager = this.targetWorkspace!.getMetricsManager(); - const targetWorkspaceViewMetrics = metricsManager.getViewMetrics(); - this.height_ = 60; - - const edgeWidth = this.width_ - this.CORNER_RADIUS; - const edgeHeight = this.height_ - 2 * this.CORNER_RADIUS; - - this.mySetBackgroundPath(edgeWidth, edgeHeight); - - const x = this.getX(); - const y = this.getY(); - - this.positionAt_(this.width_, this.height_, x, y); - } - - /** - * Create and set the path for the visible boundaries of the flyout. - * - * @param width The width of the flyout, not including the rounded corners. - * @param height The height of the flyout, not including rounded corners. - */ - private mySetBackgroundPath(width: number, height: number) { - const atRight = false; - const totalWidth = width + this.CORNER_RADIUS; - - // Decide whether to start on the left or right. - const path: Array = [ - 'M ' + (atRight ? totalWidth : 0) + ',0', - ]; - // Top. - path.push('h', atRight ? -width : width); - // Rounded corner. - path.push( - 'a', - this.CORNER_RADIUS, - this.CORNER_RADIUS, - 0, - 0, - atRight ? 0 : 1, - atRight ? -this.CORNER_RADIUS : this.CORNER_RADIUS, - this.CORNER_RADIUS, - ); - // Side closest to workspace. - path.push('v', Math.max(0, height)); - // Rounded corner. - path.push( - 'a', - this.CORNER_RADIUS, - this.CORNER_RADIUS, - 0, - 0, - atRight ? 0 : 1, - atRight ? this.CORNER_RADIUS : -this.CORNER_RADIUS, - this.CORNER_RADIUS, - ); - // Bottom. - path.push('h', atRight ? width : -width); - path.push('z'); - this.svgBackground_!.setAttribute('d', path.join(' ')); - } - - /** - * Calculates the x coordinate for the flyout position. - * - * @returns X coordinate. - */ - getX(): number { - return this.x; - } - - /** - * Calculates the y coordinate for the flyout position. - * - * @returns Y coordinate. - */ - getY(): number { - return this.y; - } } export class FieldFlydown extends Blockly.FieldTextInput { @@ -220,19 +217,20 @@ export class FieldFlydown extends Blockly.FieldTextInput { this.flydown_.createDom("svg")!, mainWorkspace.getParentSvg(), ); - + this.flydown_.init(mainWorkspace); const fieldElement = this.getClickTarget_(); if (!fieldElement) return; - + const fieldRect = fieldElement.getBoundingClientRect(); const workspaceRect = mainWorkspace.getParentSvg().getBoundingClientRect(); - + const x = fieldRect.right - workspaceRect.left; const y = fieldRect.top - workspaceRect.top; - + + // Set flydown position this.flydown_.setPosition(x, y); // Create the flydown without explicit DOM manipulation @@ -263,10 +261,10 @@ export class FieldFlydown extends Blockly.FieldTextInput { const fieldRect = fieldElement!.getBoundingClientRect(); const workspaceRect = mainWorkspace.getParentSvg().getBoundingClientRect(); - + const x = fieldRect.right - workspaceRect.left; const y = fieldRect.top - workspaceRect.top; - + // Set flydown position this.flydown_.setPosition(x, y); // Show the flydown with your blocks @@ -278,7 +276,7 @@ export class FieldFlydown extends Blockly.FieldTextInput { } private getBlocksForFlydown_() { const name = this.getText(); - return{ + return { contents: [ { kind: 'block', From 5869ad61dec91c7d789a2b692490fd9e126e51c0 Mon Sep 17 00:00:00 2001 From: Alan Smith Date: Thu, 19 Jun 2025 09:13:22 -0400 Subject: [PATCH 07/12] Makes height dependent on what is in flydown --- src/fields/field_flydown.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/fields/field_flydown.ts b/src/fields/field_flydown.ts index 9e3bbefc..dbd446a5 100644 --- a/src/fields/field_flydown.ts +++ b/src/fields/field_flydown.ts @@ -45,6 +45,8 @@ class CustomFlyout extends Blockly.VerticalFlyout { return; } + this.height_ = this.getHeight(); + const edgeWidth = this.width_ - this.CORNER_RADIUS; const edgeHeight = this.height_ - 2 * this.CORNER_RADIUS; @@ -55,6 +57,21 @@ class CustomFlyout extends Blockly.VerticalFlyout { this.positionAt_(this.width_, this.height_, x, y); } + protected getHeight(): number { + let flydownWorkspace = this.getWorkspace(); + if (!flydownWorkspace) { + return 0; + } + const blocks = flydownWorkspace.getAllBlocks(); + let height = 0; + blocks.forEach((block : Blockly.BlockSvg) => { + const blockHeight = block.getHeightWidth().height; + height += blockHeight + this.GAP_Y; // Add some padding between blocks + }); + + return height; + } + /** * Create and set the path for the visible boundaries of the flyout. From b25089fe6f407cf78549aca2b3ea01093f8ded6d Mon Sep 17 00:00:00 2001 From: Alan Smith Date: Thu, 19 Jun 2025 11:06:31 -0400 Subject: [PATCH 08/12] change to override --- src/fields/field_flydown.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/fields/field_flydown.ts b/src/fields/field_flydown.ts index dbd446a5..47e19757 100644 --- a/src/fields/field_flydown.ts +++ b/src/fields/field_flydown.ts @@ -57,7 +57,7 @@ class CustomFlyout extends Blockly.VerticalFlyout { this.positionAt_(this.width_, this.height_, x, y); } - protected getHeight(): number { + override getHeight(): number { let flydownWorkspace = this.getWorkspace(); if (!flydownWorkspace) { return 0; From 62d00c4f1a334fac123a620473d9b9adbbaaab62 Mon Sep 17 00:00:00 2001 From: Alan Smith Date: Thu, 19 Jun 2025 11:08:04 -0400 Subject: [PATCH 09/12] Add placeholder for type --- src/blocks/mrc_get_parameter.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/blocks/mrc_get_parameter.ts b/src/blocks/mrc_get_parameter.ts index 94fe07a3..bc783d2b 100644 --- a/src/blocks/mrc_get_parameter.ts +++ b/src/blocks/mrc_get_parameter.ts @@ -35,6 +35,7 @@ interface GetParameterMixin extends GetParameterMixinType { type GetParameterMixinType = typeof GET_PARAMETER_BLOCK; const GET_PARAMETER_BLOCK = { + parameterType : '', // Later this will be set to the type of the parameter, e.g. 'string', 'number', etc. /** * Block initialization. */ @@ -44,9 +45,13 @@ const GET_PARAMETER_BLOCK = { .appendField('parameter') .appendField(createFieldNonEditableText('parameter'), 'PARAMETER_NAME'); - //this.setOutput(true, OUTPUT_NAME); - this.setOutput(true); + this.setOutput(true, this.parameterType); }, + setNameAndType: function (this: GetParameterBlock, name: string, type: string): void { + this.setFieldValue(name, 'PARAMETER_NAME'); + this.parameterType = type; + this.setOutput(true, type); + } } export const setup = function () { From 41b6b3012effe8f53c3bbc29c86fd0082d338ff6 Mon Sep 17 00:00:00 2001 From: Alan Smith Date: Thu, 19 Jun 2025 12:00:00 -0400 Subject: [PATCH 10/12] Renaming a parameter now updates it in method --- src/blocks/mrc_class_method_def.ts | 29 +++++++++++-- src/blocks/utils/find_connected_blocks.ts | 50 +++++++++++++++++++++++ 2 files changed, 76 insertions(+), 3 deletions(-) create mode 100644 src/blocks/utils/find_connected_blocks.ts diff --git a/src/blocks/mrc_class_method_def.ts b/src/blocks/mrc_class_method_def.ts index 2068b6c6..e67d5d4a 100644 --- a/src/blocks/mrc_class_method_def.ts +++ b/src/blocks/mrc_class_method_def.ts @@ -28,6 +28,9 @@ import { getLegalName } from './utils/python'; import { Order } from 'blockly/python'; import { ExtendedPythonGenerator } from '../editor/extended_python_generator'; import { renameMethodCallers, mutateMethodCallers } from './mrc_call_python_function' +import { findConnectedBlocksOfType } from './utils/find_connected_blocks'; +import { BLOCK_NAME as MRC_GET_PARAMETER_BLOCK_NAME } from './mrc_get_parameter'; + export const BLOCK_NAME = 'mrc_class_method_def'; @@ -170,11 +173,16 @@ const CLASS_METHOD_DEF = { this.mrcParameters = []; let paramBlock = containerBlock.getInputTargetBlock('STACK'); - while (paramBlock && !paramBlock.isInsertionMarker()) { + while (paramBlock) { const param: Parameter = { name: paramBlock.getFieldValue('NAME'), type: '' } + if (paramBlock.originalName) { + // This is a mutator arg block, so we can get the original name. + this.mrcRenameParameter(paramBlock.originalName, param.name); + paramBlock.originalName = param.name; + } this.mrcParameters.push(param); paramBlock = paramBlock.nextConnection && paramBlock.nextConnection.targetBlock(); @@ -194,13 +202,25 @@ const CLASS_METHOD_DEF = { for (let i = 0; i < this.mrcParameters.length; i++) { let itemBlock = workspace.newBlock(MUTATOR_BLOCK_NAME); (itemBlock as Blockly.BlockSvg).initSvg(); - itemBlock.setFieldValue(this.mrcParameters[i].name, 'NAME') + itemBlock.setFieldValue(this.mrcParameters[i].name, 'NAME'); + (itemBlock as MethodMutatorArgBlock).originalName = this.mrcParameters[i].name; connection!.connect(itemBlock.previousConnection!); connection = itemBlock.nextConnection; } return topBlock; }, + mrcRenameParameter: function (this: ClassMethodDefBlock, oldName: string, newName: string) { + let nextBlock = this.getInputTargetBlock('STACK'); + + if(nextBlock){ + findConnectedBlocksOfType(nextBlock, MRC_GET_PARAMETER_BLOCK_NAME).forEach((block) => { + if (block.getFieldValue('PARAMETER_NAME') === oldName) { + block.setFieldValue(newName, 'PARAMETER_NAME'); + } + }); + } + }, mrcUpdateParams: function (this: ClassMethodDefBlock) { if (this.mrcParameters.length > 0) { let input = this.getInput('TITLE'); @@ -305,8 +325,9 @@ const METHOD_PARAM_CONTAINER = { type MethodMutatorArgBlock = Blockly.Block & MethodMutatorArgMixin & Blockly.BlockSvg; interface MethodMutatorArgMixin extends MethodMutatorArgMixinType { - + originalName: string, } + type MethodMutatorArgMixinType = typeof METHODS_MUTATORARG; function setName(block: Blockly.BlockSvg) { @@ -332,6 +353,7 @@ const METHODS_MUTATORARG = { this.setPreviousStatement(true); this.setNextStatement(true); this.setStyle(MRC_STYLE_CLASS_BLOCKS); + this.originalName = ''; this.contextMenu = false; ChangeFramework.registerCallback(MUTATOR_BLOCK_NAME, [Blockly.Events.BLOCK_MOVE, Blockly.Events.BLOCK_CHANGE], this.onBlockChanged); }, @@ -350,6 +372,7 @@ const METHODS_MUTATORARG = { }, } + /** * Updates the procedure mutator's flyout so that the arg block is not a * duplicate of another arg. diff --git a/src/blocks/utils/find_connected_blocks.ts b/src/blocks/utils/find_connected_blocks.ts new file mode 100644 index 00000000..b1299463 --- /dev/null +++ b/src/blocks/utils/find_connected_blocks.ts @@ -0,0 +1,50 @@ +/** + * @license + * Copyright 2025 Porpoiseful LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Allow registering for specific callbacks + * @author alan@porpoiseful.com (Alan Smith) + */ +import * as Blockly from 'blockly'; + +export function findConnectedBlocksOfType(block: Blockly.Block, targetType: string): Blockly.Block[] { + const foundBlocks: Blockly.Block[] = []; + const visited = new Set(); // Prevent infinite loops + + function searchRecursive(currentBlock: Blockly.Block): void { + if (visited.has(currentBlock.id)) return; + visited.add(currentBlock.id); + + // Check if current block matches target type + if (currentBlock.type === targetType) { + foundBlocks.push(currentBlock); + } + + // Search through all inputs + currentBlock.inputList.forEach(input => { + if (input.connection && input.connection.isConnected()) { + const connectedBlock = input.connection.targetBlock(); + if (connectedBlock) { + searchRecursive(connectedBlock); + } + } + }); + } + + searchRecursive(block); + return foundBlocks; +} \ No newline at end of file From b0119dc60d10ead3ac7e9a89f50c694875cebce7 Mon Sep 17 00:00:00 2001 From: Alan Smith Date: Thu, 19 Jun 2025 13:05:56 -0400 Subject: [PATCH 11/12] Made so you can only drag parameters into a method with that named parameter --- src/blocks/mrc_get_parameter.ts | 44 +++++++++++++++++++++++++++------ 1 file changed, 36 insertions(+), 8 deletions(-) diff --git a/src/blocks/mrc_get_parameter.ts b/src/blocks/mrc_get_parameter.ts index bc783d2b..a8d02b6a 100644 --- a/src/blocks/mrc_get_parameter.ts +++ b/src/blocks/mrc_get_parameter.ts @@ -25,17 +25,21 @@ import { Order } from 'blockly/python'; import { MRC_STYLE_VARIABLES } from '../themes/styles' import { createFieldNonEditableText } from '../fields/FieldNonEditableText'; import { ExtendedPythonGenerator } from '../editor/extended_python_generator'; +import { BLOCK_NAME as MRC_CLASS_METHOD_DEF, ClassMethodDefBlock} from './mrc_class_method_def'; +import * as ChangeFramework from './utils/change_framework' +import { truncate } from 'node:fs'; export const BLOCK_NAME = 'mrc_get_parameter'; +export const OUTPUT_NAME = 'mrc_get_parameter_output'; -type GetParameterBlock = Blockly.Block & GetParameterMixin; +type GetParameterBlock = Blockly.Block & Blockly.BlockSvg & GetParameterMixin; interface GetParameterMixin extends GetParameterMixinType { } type GetParameterMixinType = typeof GET_PARAMETER_BLOCK; const GET_PARAMETER_BLOCK = { - parameterType : '', // Later this will be set to the type of the parameter, e.g. 'string', 'number', etc. + parameterType: '', // Later this will be set to the type of the parameter, e.g. 'string', 'number', etc. /** * Block initialization. */ @@ -45,13 +49,37 @@ const GET_PARAMETER_BLOCK = { .appendField('parameter') .appendField(createFieldNonEditableText('parameter'), 'PARAMETER_NAME'); - this.setOutput(true, this.parameterType); + this.setOutput(true, [OUTPUT_NAME, this.parameterType]); + ChangeFramework.registerCallback(BLOCK_NAME, [Blockly.Events.BLOCK_MOVE], this.onBlockChanged); }, setNameAndType: function (this: GetParameterBlock, name: string, type: string): void { this.setFieldValue(name, 'PARAMETER_NAME'); this.parameterType = type; - this.setOutput(true, type); - } + this.setOutput(true, [OUTPUT_NAME, type]); + }, + + onBlockChanged(block: Blockly.BlockSvg, blockEvent: Blockly.Events.BlockBase): void { + let blockBlock = block as Blockly.Block; + + if (blockEvent.type === Blockly.Events.BLOCK_MOVE) { + let parent = ChangeFramework.getParentOfType(block, MRC_CLASS_METHOD_DEF); + + if (parent) { + // It is a class method definition, so we see if this variable is in it. + let classMethodDefBlock = parent as ClassMethodDefBlock; + for(const parameter of classMethodDefBlock.mrcParameters) { + if (parameter.name === blockBlock.getFieldValue('PARAMETER_NAME')) { + // If it is, we allow it to stay. + blockBlock.setWarningText(null); + return; + } + } + } + // If we end up here it shouldn't be allowed + block.unplug(true); + blockBlock.setWarningText("Parameters can only go in their method's block.") + } + }, } export const setup = function () { @@ -62,8 +90,8 @@ export const pythonFromBlock = function ( block: GetParameterBlock, generator: ExtendedPythonGenerator, ) { - //TODO (Alan) : Specify the type here as well - let code = block.getFieldValue('PARAMETER_NAME'); + //TODO (Alan) : Specify the type here as well + let code = block.getFieldValue('PARAMETER_NAME'); - return [code, Order.ATOMIC]; + return [code, Order.ATOMIC]; } From 8158591be0b125a96644516f79b2c97939755eee Mon Sep 17 00:00:00 2001 From: Alan Smith Date: Thu, 19 Jun 2025 13:43:42 -0400 Subject: [PATCH 12/12] Tell Copilot to make this file follow google coding standards --- src/blocks/mrc_get_parameter.ts | 62 +++++++++++++++++---------------- 1 file changed, 32 insertions(+), 30 deletions(-) diff --git a/src/blocks/mrc_get_parameter.ts b/src/blocks/mrc_get_parameter.ts index a8d02b6a..a3784ae7 100644 --- a/src/blocks/mrc_get_parameter.ts +++ b/src/blocks/mrc_get_parameter.ts @@ -16,58 +16,60 @@ */ /** - * @fileoverview Create a component with a name of a certain type + * @fileoverview This is a block that allows your code to use a parameter + * that is passed to a method. * @author alan@porpoiseful.com (Alan Smith) */ import * as Blockly from 'blockly'; -import { Order } from 'blockly/python'; +import {Order} from 'blockly/python'; + +import {ExtendedPythonGenerator} from '../editor/extended_python_generator'; +import {createFieldNonEditableText} from '../fields/FieldNonEditableText'; +import {MRC_STYLE_VARIABLES} from '../themes/styles'; +import {BLOCK_NAME as MRC_CLASS_METHOD_DEF, ClassMethodDefBlock} from './mrc_class_method_def'; +import * as ChangeFramework from './utils/change_framework'; -import { MRC_STYLE_VARIABLES } from '../themes/styles' -import { createFieldNonEditableText } from '../fields/FieldNonEditableText'; -import { ExtendedPythonGenerator } from '../editor/extended_python_generator'; -import { BLOCK_NAME as MRC_CLASS_METHOD_DEF, ClassMethodDefBlock} from './mrc_class_method_def'; -import * as ChangeFramework from './utils/change_framework' -import { truncate } from 'node:fs'; export const BLOCK_NAME = 'mrc_get_parameter'; export const OUTPUT_NAME = 'mrc_get_parameter_output'; type GetParameterBlock = Blockly.Block & Blockly.BlockSvg & GetParameterMixin; -interface GetParameterMixin extends GetParameterMixinType { -} + +interface GetParameterMixin extends GetParameterMixinType {} + type GetParameterMixinType = typeof GET_PARAMETER_BLOCK; const GET_PARAMETER_BLOCK = { parameterType: '', // Later this will be set to the type of the parameter, e.g. 'string', 'number', etc. /** - * Block initialization. - */ - init: function (this: GetParameterBlock): void { + * Block initialization. + */ + init: function(this: GetParameterBlock): void { this.setStyle(MRC_STYLE_VARIABLES); this.appendDummyInput() - .appendField('parameter') - .appendField(createFieldNonEditableText('parameter'), 'PARAMETER_NAME'); + .appendField('parameter') + .appendField(createFieldNonEditableText('parameter'), 'PARAMETER_NAME'); this.setOutput(true, [OUTPUT_NAME, this.parameterType]); ChangeFramework.registerCallback(BLOCK_NAME, [Blockly.Events.BLOCK_MOVE], this.onBlockChanged); }, - setNameAndType: function (this: GetParameterBlock, name: string, type: string): void { + setNameAndType: function(this: GetParameterBlock, name: string, type: string): void { this.setFieldValue(name, 'PARAMETER_NAME'); this.parameterType = type; this.setOutput(true, [OUTPUT_NAME, type]); }, onBlockChanged(block: Blockly.BlockSvg, blockEvent: Blockly.Events.BlockBase): void { - let blockBlock = block as Blockly.Block; + const blockBlock = block as Blockly.Block; if (blockEvent.type === Blockly.Events.BLOCK_MOVE) { - let parent = ChangeFramework.getParentOfType(block, MRC_CLASS_METHOD_DEF); + const parent = ChangeFramework.getParentOfType(block, MRC_CLASS_METHOD_DEF); if (parent) { // It is a class method definition, so we see if this variable is in it. - let classMethodDefBlock = parent as ClassMethodDefBlock; - for(const parameter of classMethodDefBlock.mrcParameters) { + const classMethodDefBlock = parent as ClassMethodDefBlock; + for (const parameter of classMethodDefBlock.mrcParameters) { if (parameter.name === blockBlock.getFieldValue('PARAMETER_NAME')) { // If it is, we allow it to stay. blockBlock.setWarningText(null); @@ -77,21 +79,21 @@ const GET_PARAMETER_BLOCK = { } // If we end up here it shouldn't be allowed block.unplug(true); - blockBlock.setWarningText("Parameters can only go in their method's block.") + blockBlock.setWarningText('Parameters can only go in their method\'s block.'); } }, -} +}; -export const setup = function () { +export const setup = function() { Blockly.Blocks[BLOCK_NAME] = GET_PARAMETER_BLOCK; -} +}; -export const pythonFromBlock = function ( - block: GetParameterBlock, - generator: ExtendedPythonGenerator, +export const pythonFromBlock = function( + block: GetParameterBlock, + _generator: ExtendedPythonGenerator, ) { - //TODO (Alan) : Specify the type here as well - let code = block.getFieldValue('PARAMETER_NAME'); + // TODO (Alan) : Specify the type here as well + const code = block.getFieldValue('PARAMETER_NAME'); return [code, Order.ATOMIC]; -} +};