diff --git a/src/App.tsx b/src/App.tsx index 2939e53d..971d69a2 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -38,9 +38,10 @@ import * as commonStorage from './storage/common_storage'; import * as clientSideStorage from './storage/client_side_storage'; import * as CustomBlocks from './blocks/setup_custom_blocks'; -import {initialize as initializePythonBlocks} from './blocks/utils/python'; -import * as ChangeFramework from './blocks/utils/change_framework'; -import {mutatorOpenListener} from './blocks/mrc_class_method_def'; + +import { initialize as initializePythonBlocks } from './blocks/utils/python'; +import * as ChangeFramework from './blocks/utils/change_framework' +import { mutatorOpenListener } from './blocks/mrc_param_container' /** Storage key for shown toolbox categories. */ const SHOWN_TOOLBOX_CATEGORIES_KEY = 'shownPythonToolboxCategories'; diff --git a/src/blocks/mrc_call_python_function.ts b/src/blocks/mrc_call_python_function.ts index d28977fa..f6fd2c9d 100644 --- a/src/blocks/mrc_call_python_function.ts +++ b/src/blocks/mrc_call_python_function.ts @@ -40,16 +40,17 @@ import * as toolboxItems from '../toolbox/items'; export const BLOCK_NAME = 'mrc_call_python_function'; -enum FunctionKind { +export enum FunctionKind { MODULE = 'module', STATIC = 'static', CONSTRUCTOR = 'constructor', INSTANCE = 'instance', INSTANCE_WITHIN = 'instance_within', - INSTANCE_COMPONENT = 'instance_component' + INSTANCE_COMPONENT = 'instance_component', + EVENT = 'event', } -const RETURN_TYPE_NONE = 'None'; +export const RETURN_TYPE_NONE = 'None'; const FIELD_MODULE_OR_CLASS_NAME = 'MODULE_OR_CLASS'; const FIELD_FUNCTION_NAME = 'FUNC'; @@ -318,6 +319,11 @@ const CALL_PYTHON_FUNCTION = { tooltip = 'Calls the instance method ' + functionName + '.'; break; } + case FunctionKind.EVENT: { + const functionName = this.getFieldValue(FIELD_FUNCTION_NAME); + tooltip = 'Fires the event ' + functionName + '.'; + break; + } case FunctionKind.INSTANCE_COMPONENT: { const className = this.mrcComponentClassName; const functionName = this.getFieldValue(FIELD_FUNCTION_NAME); @@ -459,6 +465,15 @@ const CALL_PYTHON_FUNCTION = { } break; } + case FunctionKind.EVENT: { + const input = this.getInput('TITLE'); + if (!input) { + this.appendDummyInput('TITLE') + .appendField('fire') + .appendField(createFieldNonEditableText(''), FIELD_FUNCTION_NAME); + } + break; + } case FunctionKind.INSTANCE_COMPONENT: { const componentNames = Editor.getComponentNames(this.workspace, this.mrcComponentClassName); const componentName = this.getComponentName(); @@ -586,6 +601,12 @@ export const pythonFromBlock = function( code = 'self.' + functionName; break; } + case FunctionKind.EVENT: { + const blocklyName = block.getFieldValue(FIELD_FUNCTION_NAME); + const functionName = generator.getProcedureName(blocklyName); + code = 'if self.events.get("' + functionName + '", None):\n' + generator.INDENT + 'self.events["' + functionName + '"]'; + break; + } case FunctionKind.INSTANCE_COMPONENT: { const componentName = callPythonFunctionBlock.getComponentName(); const functionName = callPythonFunctionBlock.mrcActualFunctionName diff --git a/src/blocks/mrc_class_method_def.ts b/src/blocks/mrc_class_method_def.ts index e67d5d4a..eadacb12 100644 --- a/src/blocks/mrc_class_method_def.ts +++ b/src/blocks/mrc_class_method_def.ts @@ -23,20 +23,15 @@ 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'; 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'; - +import { MUTATOR_BLOCK_NAME, PARAM_CONTAINER_BLOCK_NAME, MethodMutatorArgBlock } from './mrc_param_container' export const BLOCK_NAME = 'mrc_class_method_def'; -export const MUTATOR_BLOCK_NAME = 'methods_mutatorarg'; -const PARAM_CONTAINER_BLOCK_NAME = 'method_param_container'; - export type Parameter = { name: string, type?: string, @@ -314,133 +309,8 @@ function isMethodNameUsed( return false; } -const METHOD_PARAM_CONTAINER = { - init: function (this: Blockly.Block) { - this.appendDummyInput("TITLE").appendField('Parameters'); - this.appendStatementInput('STACK'); - this.setStyle(MRC_STYLE_CLASS_BLOCKS); - this.contextMenu = false; - }, -}; - -type MethodMutatorArgBlock = Blockly.Block & MethodMutatorArgMixin & Blockly.BlockSvg; -interface MethodMutatorArgMixin extends MethodMutatorArgMixinType { - originalName: string, -} - -type MethodMutatorArgMixinType = typeof METHODS_MUTATORARG; - -function setName(block: Blockly.BlockSvg) { - const parentBlock = ChangeFramework.getParentOfType(block, PARAM_CONTAINER_BLOCK_NAME); - if (parentBlock) { - const variableBlocks = parentBlock!.getDescendants(true) - const otherNames: string[] = [] - variableBlocks?.forEach(function (variableBlock) { - if (variableBlock != block) { - otherNames.push(variableBlock.getFieldValue('NAME')); - } - }); - const currentName = block.getFieldValue('NAME'); - block.setFieldValue(getLegalName(currentName, otherNames), 'NAME'); - updateMutatorFlyout(block.workspace); - } -} - -const METHODS_MUTATORARG = { - init: function (this: MethodMutatorArgBlock) { - this.appendDummyInput() - .appendField(new Blockly.FieldTextInput(Blockly.Procedures.DEFAULT_ARG), 'NAME'); - 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); - }, - onBlockChanged: function (block: Blockly.BlockSvg, blockEvent: Blockly.Events.BlockBase) { - if (blockEvent.type == Blockly.Events.BLOCK_MOVE) { - let blockMoveEvent = blockEvent as Blockly.Events.BlockMove; - if (blockMoveEvent.reason?.includes('connect')) { - setName(block); - } - } - else { - if (blockEvent.type == Blockly.Events.BLOCK_CHANGE) { - setName(block); - } - } - }, -} - - -/** - * Updates the procedure mutator's flyout so that the arg block is not a - * duplicate of another arg. - * - * @param workspace The procedure mutator's workspace. This workspace's flyout - * 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] }); -} - - -/** - * Listens for when a procedure mutator is opened. Then it triggers a flyout - * update and adds a mutator change listener to the mutator workspace. - * - * @param e The event that triggered this listener. - * @internal - */ -export function mutatorOpenListener(e: Blockly.Events.Abstract) { - if (e.type != Blockly.Events.BUBBLE_OPEN) { - return; - } - const bubbleEvent = e as Blockly.Events.BubbleOpen; - if ( - !(bubbleEvent.bubbleType === 'mutator' && bubbleEvent.isOpen) || - !bubbleEvent.blockId - ) { - return; - } - const workspaceId = bubbleEvent.workspaceId; - const block = Blockly.common - .getWorkspaceById(workspaceId)! - .getBlockById(bubbleEvent.blockId) as Blockly.BlockSvg; - - if (block.type !== BLOCK_NAME) { - return; - } - const workspace = ( - 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 pythonFromBlock = function ( @@ -486,6 +356,10 @@ export const pythonFromBlock = function ( branch = generator.INDENT + 'super().__init__(' + class_specific + ')\n' + generator.defineClassVariables() + branch; } + else if (funcName == 'update'){ + // Special case for update, to also call the update method of the base class + branch = generator.INDENT + 'self.update()\n' + branch; + } if (returnValue) { returnValue = generator.INDENT + 'return ' + returnValue + '\n'; } else if (!branch) { diff --git a/src/blocks/mrc_event.ts b/src/blocks/mrc_event.ts index d9bdc89b..59dca05d 100644 --- a/src/blocks/mrc_event.ts +++ b/src/blocks/mrc_event.ts @@ -24,7 +24,9 @@ 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' +import { MUTATOR_BLOCK_NAME, PARAM_CONTAINER_BLOCK_NAME, MethodMutatorArgBlock } from './mrc_param_container' +import * as ChangeFramework from './utils/change_framework'; +import { BLOCK_NAME as MRC_MECHANISM_COMPONENT_HOLDER } from './mrc_mechanism_component_holder'; export const BLOCK_NAME = 'mrc_event'; export const OUTPUT_NAME = 'mrc_event'; @@ -38,10 +40,10 @@ type EventExtraState = { params?: Parameter[], } -type EventBlock = Blockly.Block & EventMixin & Blockly.BlockSvg; +export type EventBlock = Blockly.Block & EventMixin & Blockly.BlockSvg; interface EventMixin extends EventMixinType { - mrcParams: Parameter[], + mrcParameters: Parameter[], } type EventMixinType = typeof EVENT; @@ -51,11 +53,12 @@ const EVENT = { */ init: function (this: EventBlock): void { this.setStyle(MRC_STYLE_EVENTS); - this.appendDummyInput() + this.appendDummyInput("TITLE") .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)); + ChangeFramework.registerCallback(BLOCK_NAME, [Blockly.Events.BLOCK_MOVE], this.onBlockChanged); }, /** @@ -65,8 +68,8 @@ const EVENT = { const extraState: EventExtraState = { }; extraState.params = []; - if (this.mrcParams) { - this.mrcParams.forEach((arg) => { + if (this.mrcParameters) { + this.mrcParameters.forEach((arg) => { extraState.params!.push({ 'name': arg.name, 'type': arg.type, @@ -79,24 +82,117 @@ const EVENT = { * Applies the given state to this block. */ loadExtraState: function (this: EventBlock, extraState: EventExtraState): void { - this.mrcParams = []; + this.mrcParameters = []; if (extraState.params) { extraState.params.forEach((arg) => { - this.mrcParams.push({ + this.mrcParameters.push({ 'name': arg.name, 'type': arg.type, }); }); } - this.mrcParams = extraState.params ? extraState.params : []; + this.mrcParameters = extraState.params ? extraState.params : []; this.updateBlock_(); }, /** * Update the block to reflect the newly loaded extra state. */ updateBlock_: function (this: EventBlock): void { - } + const name = this.getFieldValue('NAME'); + const input = this.getInput('TITLE'); + if (!input) { + return; + } + input.removeField('NAME'); + + const nameField = new Blockly.FieldTextInput(name); + input.insertFieldAt(0, nameField, 'NAME'); + this.setMutator(new Blockly.icons.MutatorIcon([MUTATOR_BLOCK_NAME], this)); + // nameField.setValidator(this.mrcNameFieldValidator.bind(this, nameField)); + + this.mrcUpdateParams(); + }, + compose: function (this: EventBlock, containerBlock: any) { + // Parameter list. + this.mrcParameters = []; + + let paramBlock = containerBlock.getInputTargetBlock('STACK'); + 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. + paramBlock.originalName = param.name; + } + this.mrcParameters.push(param); + paramBlock = + paramBlock.nextConnection && paramBlock.nextConnection.targetBlock(); + } + this.mrcUpdateParams(); + //mutateMethodCallers(this.workspace, this.getFieldValue('NAME'), this.saveExtraState()); + }, + decompose: function (this: EventBlock, workspace: Blockly.Workspace) { + // This is a special sub-block that only gets created in the mutator UI. + // It acts as our "top block" + const topBlock = workspace.newBlock(PARAM_CONTAINER_BLOCK_NAME); + (topBlock as Blockly.BlockSvg).initSvg(); + + // Then we add one sub-block for each item in the list. + let connection = topBlock!.getInput('STACK')!.connection; + + 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 as MethodMutatorArgBlock).originalName = this.mrcParameters[i].name; + + connection!.connect(itemBlock.previousConnection!); + connection = itemBlock.nextConnection; + } + return topBlock; + }, + mrcUpdateParams: function (this: EventBlock) { + if (this.mrcParameters.length > 0) { + let input = this.getInput('TITLE'); + if (input) { + this.removeParameterFields(input); + this.mrcParameters.forEach((param) => { + const paramName = 'PARAM_' + param.name; + const field = new Blockly.FieldTextInput(param.name); + field.EDITABLE = false; + input.appendField(field, 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); + }); + }, + onBlockChanged(block: Blockly.BlockSvg, blockEvent: Blockly.Events.BlockBase): void { + const blockBlock = block as Blockly.Block; + + if (blockEvent.type === Blockly.Events.BLOCK_MOVE) { + const parent = ChangeFramework.getParentOfType(block, MRC_MECHANISM_COMPONENT_HOLDER); + + if (parent) { + // 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('Events can only go in the events block'); + } + }, } export const setup = function () { diff --git a/src/blocks/mrc_mechanism_component_holder.ts b/src/blocks/mrc_mechanism_component_holder.ts index 372fb439..39dd7541 100644 --- a/src/blocks/mrc_mechanism_component_holder.ts +++ b/src/blocks/mrc_mechanism_component_holder.ts @@ -158,7 +158,6 @@ function pythonFromBlockInMechanism(block: MechanismComponentHolderBlock, genera if (components != '') { code += components; } - generator.addClassMethodDefinition('define_hardware', code); } diff --git a/src/blocks/mrc_param_container.ts b/src/blocks/mrc_param_container.ts new file mode 100644 index 00000000..a53cae97 --- /dev/null +++ b/src/blocks/mrc_param_container.ts @@ -0,0 +1,156 @@ +/** + * @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 Mutator for method and event parameters. + * @author alan@porpoiseful.com (Alan Smith) + */ +import * as Blockly from 'blockly'; +import * as ChangeFramework from './utils/change_framework' +import { MRC_STYLE_CLASS_BLOCKS } from '../themes/styles'; +import { BLOCK_NAME as MRC_CLASS_METHOD_DEF } from './mrc_class_method_def'; +import { BLOCK_NAME as MRC_EVENT } from './mrc_event'; + +export const MUTATOR_BLOCK_NAME = 'methods_mutatorarg'; +export const PARAM_CONTAINER_BLOCK_NAME = 'method_param_container'; +import { getLegalName } from './utils/python'; + +export const setup = function () { + Blockly.Blocks[MUTATOR_BLOCK_NAME] = METHODS_MUTATORARG; + Blockly.Blocks[PARAM_CONTAINER_BLOCK_NAME] = METHOD_PARAM_CONTAINER; +}; + +const METHOD_PARAM_CONTAINER = { + init: function (this: Blockly.Block) { + this.appendDummyInput("TITLE").appendField('Parameters'); + this.appendStatementInput('STACK'); + this.setStyle(MRC_STYLE_CLASS_BLOCKS); + this.contextMenu = false; + }, +}; + +export type MethodMutatorArgBlock = Blockly.Block & MethodMutatorArgMixin & Blockly.BlockSvg; +interface MethodMutatorArgMixin extends MethodMutatorArgMixinType { + originalName: string, +} + +type MethodMutatorArgMixinType = typeof METHODS_MUTATORARG; + +function setName(block: Blockly.BlockSvg) { + const parentBlock = ChangeFramework.getParentOfType(block, PARAM_CONTAINER_BLOCK_NAME); + if (parentBlock) { + const variableBlocks = parentBlock!.getDescendants(true) + const otherNames: string[] = [] + variableBlocks?.forEach(function (variableBlock) { + if (variableBlock != block) { + otherNames.push(variableBlock.getFieldValue('NAME')); + } + }); + const currentName = block.getFieldValue('NAME'); + block.setFieldValue(getLegalName(currentName, otherNames), 'NAME'); + updateMutatorFlyout(block.workspace); + } +} + +const METHODS_MUTATORARG = { + init: function (this: MethodMutatorArgBlock) { + this.appendDummyInput() + .appendField(new Blockly.FieldTextInput(Blockly.Procedures.DEFAULT_ARG), 'NAME'); + 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); + }, + onBlockChanged: function (block: Blockly.BlockSvg, blockEvent: Blockly.Events.BlockBase) { + if (blockEvent.type == Blockly.Events.BLOCK_MOVE) { + let blockMoveEvent = blockEvent as Blockly.Events.BlockMove; + if (blockMoveEvent.reason?.includes('connect')) { + setName(block); + } + } + else { + if (blockEvent.type == Blockly.Events.BLOCK_CHANGE) { + setName(block); + } + } + }, +} + + +/** + * Updates the procedure mutator's flyout so that the arg block is not a + * duplicate of another arg. + * + * @param workspace The procedure mutator's workspace. This workspace's flyout + * 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] }); +} + +/** + * Listens for when a procedure mutator is opened. Then it triggers a flyout + * update and adds a mutator change listener to the mutator workspace. + * + * @param e The event that triggered this listener. + * @internal + */ +export function mutatorOpenListener(e: Blockly.Events.Abstract) { + if (e.type != Blockly.Events.BUBBLE_OPEN) { + return; + } + const bubbleEvent = e as Blockly.Events.BubbleOpen; + if ( + !(bubbleEvent.bubbleType === 'mutator' && bubbleEvent.isOpen) || + !bubbleEvent.blockId + ) { + return; + } + const workspaceId = bubbleEvent.workspaceId; + const block = Blockly.common + .getWorkspaceById(workspaceId)! + .getBlockById(bubbleEvent.blockId) as Blockly.BlockSvg; + + if ((block.type !== MRC_EVENT) && (block.type !== MRC_CLASS_METHOD_DEF)) { + return; + } + const workspace = ( + block.getIcon(Blockly.icons.MutatorIcon.TYPE) as Blockly.icons.MutatorIcon + ).getWorkspace()!; + + updateMutatorFlyout(workspace); + ChangeFramework.setup(workspace); +} \ No newline at end of file diff --git a/src/blocks/setup_custom_blocks.ts b/src/blocks/setup_custom_blocks.ts index be5c4583..896c6040 100644 --- a/src/blocks/setup_custom_blocks.ts +++ b/src/blocks/setup_custom_blocks.ts @@ -15,6 +15,7 @@ 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'; +import * as ParameterMutator from './mrc_param_container' const customBlocks = [ CallPythonFunction, @@ -32,12 +33,16 @@ const customBlocks = [ Port, OpModeDetails, Event, - GetParameter + GetParameter, + ParameterMutator ]; export const setup = function(forBlock: any) { customBlocks.forEach(block => { block.setup(); - forBlock[block.BLOCK_NAME] = block.pythonFromBlock; + const maybeBlock = block as { pythonFromBlock?: any; BLOCK_NAME?: string }; + if(maybeBlock.pythonFromBlock && maybeBlock.BLOCK_NAME) { + forBlock[maybeBlock.BLOCK_NAME] = maybeBlock.pythonFromBlock; + } }); }; diff --git a/src/blocks/utils/connection_checker.ts b/src/blocks/utils/connection_checker.ts index cda8a768..06bbb692 100644 --- a/src/blocks/utils/connection_checker.ts +++ b/src/blocks/utils/connection_checker.ts @@ -22,6 +22,7 @@ import * as Blockly from 'blockly/core'; import { OUTPUT_NAME as MECHANISM_OUTPUT } from '../mrc_mechanism'; import { OUTPUT_NAME as COMPONENT_OUTPUT } from '../mrc_component'; +import { OUTPUT_NAME as EVENT_OUTPUT } from '../mrc_event'; export class MethodConnectionChecker extends Blockly.ConnectionChecker { /** @@ -50,7 +51,11 @@ export class MethodConnectionChecker extends Blockly.ConnectionChecker { (checkArrayTwo && (checkArrayTwo.indexOf(MECHANISM_OUTPUT) != -1))){ return false; } - + // if either one has mrc_event, they must match + if((checkArrayOne && (checkArrayOne.indexOf(EVENT_OUTPUT) != -1)) || + (checkArrayTwo && (checkArrayTwo.indexOf(EVENT_OUTPUT) != -1))){ + return false; + } return true; } diff --git a/src/editor/editor.ts b/src/editor/editor.ts index 1af14842..ba957898 100644 --- a/src/editor/editor.ts +++ b/src/editor/editor.ts @@ -30,6 +30,7 @@ import * as toolboxRobot from '../toolbox/toolbox_robot'; //import { testAllBlocksInToolbox } from '../toolbox/toolbox_tests'; import { MethodsCategory} from '../toolbox/methods_category'; +import { EventsCategory} from '../toolbox/event_category'; const EMPTY_TOOLBOX: Blockly.utils.toolbox.ToolboxDefinition = { @@ -44,6 +45,7 @@ export class Editor { private generatorContext: GeneratorContext; private storage: commonStorage.Storage; private methodsCategory: MethodsCategory; + private eventsCategory: EventsCategory; private currentModule: commonStorage.Module | null = null; private modulePath: string = ''; private projectPath: string = ''; @@ -58,6 +60,7 @@ export class Editor { this.generatorContext = generatorContext; this.storage = storage; this.methodsCategory = new MethodsCategory(blocklyWorkspace); + this.eventsCategory = new EventsCategory(blocklyWorkspace); } private onChangeWhileLoading(event: Blockly.Events.Abstract) { @@ -120,6 +123,7 @@ export class Editor { this.generatorContext.setModule(currentModule); this.currentModule = currentModule; this.methodsCategory.setCurrentModule(currentModule); + this.eventsCategory.setCurrentModule(currentModule); if (currentModule) { this.modulePath = currentModule.modulePath; this.projectPath = commonStorage.makeProjectPath(currentModule.projectName); diff --git a/src/editor/extended_python_generator.ts b/src/editor/extended_python_generator.ts index 887aaf9e..e86a862c 100644 --- a/src/editor/extended_python_generator.ts +++ b/src/editor/extended_python_generator.ts @@ -88,7 +88,7 @@ export class ExtendedPythonGenerator extends PythonGenerator { if (this.context?.getHasHardware()) { variableDefinitions += this.INDENT + "self.define_hardware("; variableDefinitions += this.getListOfPorts(true); - variableDefinitions += ')'; + variableDefinitions += ')\n'; } return variableDefinitions; diff --git a/src/toolbox/event_category.ts b/src/toolbox/event_category.ts index 3f4b198f..4fd9f30e 100644 --- a/src/toolbox/event_category.ts +++ b/src/toolbox/event_category.ts @@ -1,15 +1,114 @@ -export const category = -{ - kind: 'category', - name: 'Event', - contents: [ - { - kind: 'label', - text: 'New Event', - }, - { - kind: 'block', - type: 'mrc_event', - } - ] +/** + * @license + * Copyright 2025 Google 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. + */ + +/** + * @author lizlooney@google.com (Liz Looney) + */ + +import * as Blockly from 'blockly/core'; + +import * as toolboxItems from './items'; +import * as commonStorage from '../storage/common_storage'; +import { EventBlock } from '../blocks/mrc_event'; +import { MRC_CATEGORY_STYLE_METHODS } from '../themes/styles'; +import { RETURN_TYPE_NONE, FunctionKind } from '../blocks/mrc_call_python_function'; + +const CUSTOM_CATEGORY_EVENTS = 'EVENTS'; + +export const category = { + kind: 'category', + categorystyle: MRC_CATEGORY_STYLE_METHODS, + name: 'Events', + custom: CUSTOM_CATEGORY_EVENTS, +}; + +export class EventsCategory { + private currentModule: commonStorage.Module | null = null; + + constructor(blocklyWorkspace: Blockly.WorkspaceSvg) { + blocklyWorkspace.registerToolboxCategoryCallback(CUSTOM_CATEGORY_EVENTS, this.eventsFlyout.bind(this)); + } + + public setCurrentModule(currentModule: commonStorage.Module | null) { + this.currentModule = currentModule; + } + + public eventsFlyout(workspace: Blockly.WorkspaceSvg) { + const contents: toolboxItems.ContentsType[] = []; + + // Add blocks for defining any methods that can be defined in the current + // module. For example, if the current module is an OpMode, add blocks to + // define the methods declared in the OpMode class. + if (this.currentModule) { + // Collect the method names for mrc_class_method_def blocks that are + // already in the blockly workspace. + const eventNamesAlreadyUsed: string[] = []; + workspace.getBlocksByType('mrc_event', false).forEach((block) => { + eventNamesAlreadyUsed.push(block.getFieldValue('NAME')); + }); + } + + // Add a block that lets the user define a new method. + contents.push( + { + kind: 'label', + text: 'Custom Events', + }, + { + kind: 'block', + type: 'mrc_event', + fields: {NAME: "my_event"}, + }); + + // For each mrc_class_method_def block in the blockly workspace, check if it + // can be called from within the class, and if so, add a + // mrc_call_python_function block. + workspace.getBlocksByType('mrc_event', false).forEach((block) => { + const eventBlock = block as EventBlock; + const callPythonFunctionBlock: toolboxItems.Block = { + kind: 'block', + type: 'mrc_call_python_function', + extraState: { + functionKind: FunctionKind.EVENT, + returnType: RETURN_TYPE_NONE, + args: [], + importModule: '', + }, + fields: { + FUNC: block.getFieldValue('NAME'), + }, + }; + // Add the parameters from the event block to the callPythonFunctionBlock. + eventBlock.mrcParameters.forEach((param) => { + if (callPythonFunctionBlock.extraState) { + callPythonFunctionBlock.extraState.args.push( + { + name: param.name, + type: param.type ?? '', + }); + } + }); + contents.push(callPythonFunctionBlock); + }); + + const toolboxInfo = { + contents: contents, + }; + + return toolboxInfo; + } } \ No newline at end of file diff --git a/src/toolbox/toolbox_mechanism.ts b/src/toolbox/toolbox_mechanism.ts index 3868559d..6f4487f0 100644 --- a/src/toolbox/toolbox_mechanism.ts +++ b/src/toolbox/toolbox_mechanism.ts @@ -1,5 +1,6 @@ import * as common from './toolbox_common' import { category as hardwareComponentCategory } from './hardware_component_category'; +import { category as eventCategory } from './event_category'; export function getToolboxJSON( shownPythonToolboxCategories: Set | null) { @@ -8,6 +9,7 @@ export function getToolboxJSON( kind: 'categoryToolbox', contents: [ hardwareComponentCategory, + eventCategory, { kind: 'sep' }, ...common.getToolboxItems(shownPythonToolboxCategories) ]