From 8af85765717d9bc929c3492dc3be49069933eb39 Mon Sep 17 00:00:00 2001 From: Liz Looney Date: Mon, 28 Jul 2025 22:53:37 -0700 Subject: [PATCH] In common_storage.ts, added function makeUniqueName. Modified makeUploadProjectName to call makeUniqueName. In editor.ts, added methods getCurrentModuleType and getEventHandlerNames. Removed fields methodsCategory and eventsCategory, but still create all custom toolbox categories in the constructor. In mrc_event_handler.ts, renamed function getHasEventHandler to getHasAnyEnabledEventHandlers. Added function getEventHandlerNames. In mrc_event.ts, modified createCustomEventBlock to take the event name as a parameter. In hardware_category.ts, renamed functions that return a category to be named get...Category instead of get...Blocks. Make RobotEventsCategory a custom category with a flyout callback. In event_category, removed field currentModule and method setCurrentModule. Make a unique name for creating a new event. In methods_category, removed field currentModule and method setCurrentModule. Use editor.getCurrentModuleType() instead. --- src/blocks/mrc_event.ts | 4 +- src/blocks/mrc_event_handler.ts | 13 +++-- src/editor/editor.ts | 24 ++++++-- src/editor/extended_python_generator.ts | 2 +- src/storage/common_storage.ts | 41 ++++++++------ src/toolbox/event_category.ts | 33 ++++++----- src/toolbox/hardware_category.ts | 75 +++++++++++++++---------- src/toolbox/methods_category.ts | 17 +++--- 8 files changed, 122 insertions(+), 87 deletions(-) diff --git a/src/blocks/mrc_event.ts b/src/blocks/mrc_event.ts index 4046a64d..4e662322 100644 --- a/src/blocks/mrc_event.ts +++ b/src/blocks/mrc_event.ts @@ -238,11 +238,11 @@ export const pythonFromBlock = function ( // Functions used for creating blocks for the toolbox. -export function createCustomEventBlock(): toolboxItems.Block { +export function createCustomEventBlock(name: string): toolboxItems.Block { const extraState: EventExtraState = { params: [], }; const fields: {[key: string]: any} = {}; - fields[FIELD_EVENT_NAME] = 'my_event'; + fields[FIELD_EVENT_NAME] = name; return new toolboxItems.Block(BLOCK_NAME, extraState, fields, null); } diff --git a/src/blocks/mrc_event_handler.ts b/src/blocks/mrc_event_handler.ts index 920957f1..7b93d4ac 100644 --- a/src/blocks/mrc_event_handler.ts +++ b/src/blocks/mrc_event_handler.ts @@ -226,9 +226,6 @@ export function pythonFromBlock( const blocklyName = `${sender}_${eventName}`; const funcName = generator.getProcedureName(blocklyName); - // TODO(lizlooney): if the user adds multiple event handlers for the same event - // name, we need to make the event handler function names unique. - let xfix1 = ''; if (generator.STATEMENT_PREFIX) { xfix1 += generator.injectId(generator.STATEMENT_PREFIX, block); @@ -323,8 +320,16 @@ function createRobotEventHandlerBlock( // Misc -export function getHasEventHandler(workspace: Blockly.Workspace): boolean { +export function getHasAnyEnabledEventHandlers(workspace: Blockly.Workspace): boolean { return workspace.getBlocksByType(BLOCK_NAME).filter(block => { return block.isEnabled(); }).length > 0; } + +export function getEventHandlerNames(workspace: Blockly.Workspace, names: string[]): void { + // Here we collect the event names of the event handlers in the given + // workspace, regardless of whether the event handler is enabled. + workspace.getBlocksByType(BLOCK_NAME).forEach(block => { + names.push(block.getFieldValue(FIELD_EVENT_NAME)); + }); +} diff --git a/src/editor/editor.ts b/src/editor/editor.ts index 76ff2e75..6645c726 100644 --- a/src/editor/editor.ts +++ b/src/editor/editor.ts @@ -31,6 +31,7 @@ import * as mechanismComponentHolder from '../blocks/mrc_mechanism_component_hol //import { testAllBlocksInToolbox } from '../toolbox/toolbox_tests'; import { MethodsCategory } from '../toolbox/methods_category'; import { EventsCategory } from '../toolbox/event_category'; +import { RobotEventsCategory } from '../toolbox/hardware_category'; import { getToolboxJSON } from '../toolbox/toolbox'; const EMPTY_TOOLBOX: Blockly.utils.toolbox.ToolboxDefinition = { @@ -44,8 +45,6 @@ export class Editor { private blocklyWorkspace: Blockly.WorkspaceSvg; private generatorContext: GeneratorContext; private storage: commonStorage.Storage; - private methodsCategory: MethodsCategory; - private eventsCategory: EventsCategory; private currentModule: commonStorage.Module | null = null; private modulePath: string = ''; private robotPath: string = ''; @@ -59,8 +58,10 @@ export class Editor { this.blocklyWorkspace = blocklyWorkspace; this.generatorContext = generatorContext; this.storage = storage; - this.methodsCategory = new MethodsCategory(blocklyWorkspace); - this.eventsCategory = new EventsCategory(blocklyWorkspace); + // Create the custom toolbox categories so they register their flyout callbacks. + new MethodsCategory(blocklyWorkspace); + new EventsCategory(blocklyWorkspace); + new RobotEventsCategory(blocklyWorkspace); } private onChangeWhileLoading(event: Blockly.Events.Abstract) { @@ -107,8 +108,6 @@ export class Editor { public async loadModuleBlocks(currentModule: commonStorage.Module | null) { this.generatorContext.setModule(currentModule); this.currentModule = currentModule; - this.methodsCategory.setCurrentModule(currentModule); - this.eventsCategory.setCurrentModule(currentModule); if (currentModule) { this.modulePath = currentModule.modulePath; @@ -201,6 +200,13 @@ export class Editor { return this.getModuleContentText() !== this.moduleContentText; } + public getCurrentModuleType(): string { + if (this.currentModule) { + return this.currentModule.moduleType; + } + return commonStorage.MODULE_TYPE_UNKNOWN; + } + private getModuleContentText(): string { if (!this.currentModule) { throw new Error('getModuleContentText: this.currentModule is null.'); @@ -258,6 +264,12 @@ export class Editor { return events; } + public getEventHandlerNamesFromWorkspace(): string[] { + const names: string[] = []; + eventHandler.getEventHandlerNames(this.blocklyWorkspace, names); + return names; + } + public async saveBlocks() { const moduleContentText = this.getModuleContentText(); try { diff --git a/src/editor/extended_python_generator.ts b/src/editor/extended_python_generator.ts index 0be37b49..a9a89249 100644 --- a/src/editor/extended_python_generator.ts +++ b/src/editor/extended_python_generator.ts @@ -134,7 +134,7 @@ export class ExtendedPythonGenerator extends PythonGenerator { this.ports = Object.create(null); this.hasHardware = mechanismContainerHolder.getHardwarePorts(this.workspace, this.ports); - this.hasEventHandler = eventHandler.getHasEventHandler(this.workspace); + this.hasEventHandler = eventHandler.getHasAnyEnabledEventHandlers(this.workspace); const code = super.workspaceToCode(workspace); diff --git a/src/storage/common_storage.ts b/src/storage/common_storage.ts index 536f87a4..557853f7 100644 --- a/src/storage/common_storage.ts +++ b/src/storage/common_storage.ts @@ -629,25 +629,10 @@ export function getClassNameForModule(moduleType: string, moduleName: string) { * Make a unique project name for an uploaded project. */ export function makeUploadProjectName( - uploadFileName: string, existingProjectNames: string[]): string { + uploadFileName: string, existingProjectNames: string[]): string { const preferredName = uploadFileName.substring( 0, uploadFileName.length - UPLOAD_DOWNLOAD_FILE_EXTENSION.length); - let name = preferredName; // No suffix. - let suffix = 0; - while (true) { - let nameClash = false; - for (const existingProjectName of existingProjectNames) { - if (name == existingProjectName) { - nameClash = true; - break; - } - } - if (!nameClash) { - return name; - } - suffix++; - name = preferredName + suffix; - } + return makeUniqueName(preferredName, existingProjectNames); } /** @@ -704,3 +689,25 @@ export function _processUploadedModule( const moduleContentText = moduleContent.getModuleContentText(); return [moduleName, moduleType, moduleContentText]; } + +/** + * Makes a unique name given a list of existing names + */ +export function makeUniqueName(preferredName: string, existingNames: string[]): string { + let name = preferredName; // No suffix. + let suffix = 0; + while (true) { + let nameClash = false; + for (const existingName of existingNames) { + if (name == existingName) { + nameClash = true; + break; + } + } + if (!nameClash) { + return name; + } + suffix++; + name = preferredName + suffix; + } +} diff --git a/src/toolbox/event_category.ts b/src/toolbox/event_category.ts index c2deea18..a68296e9 100644 --- a/src/toolbox/event_category.ts +++ b/src/toolbox/event_category.ts @@ -38,32 +38,31 @@ export const getCategory = () => ({ }); 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 a block that lets the user define a new event. - contents.push( - { - kind: 'label', - text: 'Custom Events', - }, - createCustomEventBlock() - ); - - // Get blocks for firing methods defined in the current workspace. const editor = Editor.getEditorForBlocklyWorkspace(workspace); if (editor) { const eventsFromWorkspace = editor.getEventsFromWorkspace(); + const eventNames: string[] = []; + eventsFromWorkspace.forEach(event => { + eventNames.push(event.name); + }); + + // Add a block that lets the user define a new event. + contents.push( + { + kind: 'label', + text: 'Custom Events', + }, + createCustomEventBlock(commonStorage.makeUniqueName('my_event', eventNames)) + ); + + // Get blocks for firing methods defined in the current workspace. addFireEventBlocks(eventsFromWorkspace, contents); } @@ -73,4 +72,4 @@ export class EventsCategory { return toolboxInfo; } -} \ No newline at end of file +} diff --git a/src/toolbox/hardware_category.ts b/src/toolbox/hardware_category.ts index f87bb3fc..035f0eec 100644 --- a/src/toolbox/hardware_category.ts +++ b/src/toolbox/hardware_category.ts @@ -34,31 +34,31 @@ export function getHardwareCategory(currentModule: commonStorage.Module): toolbo kind: 'category', name: Blockly.Msg['MRC_CATEGORY_HARDWARE'], contents: [ - getRobotMechanismsBlocks(currentModule), - getComponentsBlocks(false), + getRobotMechanismsCategory(currentModule), + getComponentsCategory(false), ] }; } if (currentModule.moduleType === commonStorage.MODULE_TYPE_MECHANISM) { - return getComponentsBlocks(true); + return getComponentsCategory(true); } if (currentModule.moduleType === commonStorage.MODULE_TYPE_OPMODE) { return { kind: 'category', name: Blockly.Msg['MRC_CATEGORY_ROBOT'], contents: [ - getRobotMechanismsBlocks(currentModule), - getRobotComponentsBlocks(), - getRobotMethodsBlocks(), - getRobotEventsBlocks(), + getRobotMechanismsCategory(currentModule), + getRobotComponentsCategory(), + getRobotMethodsCategory(), + getRobotEventsCategory(), ] }; } throw new Error('currentModule.moduleType has unexpected value: ' + currentModule.moduleType) } -function getRobotMechanismsBlocks(currentModule: commonStorage.Module): toolboxItems.Category { - // getRobotMechanismsBlocks is called when the user is editing the robot or an opmode. +function getRobotMechanismsCategory(currentModule: commonStorage.Module): toolboxItems.Category { + // getRobotMechanismsCategory is called when the user is editing the robot or an opmode. // If the user is editing the robot, it allows the user to add a mechanism to // the robot or use an existing mechanism. // If the user is editing an opmode, it allows the user to use a mechanism that @@ -211,8 +211,8 @@ function getRobotMechanismsBlocks(currentModule: commonStorage.Module): toolboxI }; } -function getRobotComponentsBlocks(): toolboxItems.Category { - // getRobotComponentsBlocks is called when the user is editing an opmode. +function getRobotComponentsCategory(): toolboxItems.Category { + // getRobotComponentsCategory is called when the user is editing an opmode. // It allows the user to use a component that was previously added to the Robot. const contents: toolboxItems.ContentsType[] = []; @@ -242,8 +242,8 @@ function getRobotComponentsBlocks(): toolboxItems.Category { }; } -function getRobotMethodsBlocks(): toolboxItems.Category { - // getRobotMethodsBlocks is called when the user is editing an opmode. +function getRobotMethodsCategory(): toolboxItems.Category { + // getRobotMethodsCategory is called when the user is editing an opmode. // It allows the user to use methods there previously defined in the Robot. const contents: toolboxItems.ContentsType[] = []; @@ -266,8 +266,8 @@ function getRobotMethodsBlocks(): toolboxItems.Category { }; } -function getComponentsBlocks(hideParams : boolean): toolboxItems.Category { - // getComponentsBlocks is called when the user is editing the robot or a +function getComponentsCategory(hideParams : boolean): toolboxItems.Category { + // getComponentsCategory is called when the user is editing the robot or a // mechanism. It allows the user to add a component or use an existing component. const contents: toolboxItems.ContentsType[] = []; @@ -303,26 +303,41 @@ function getComponentsBlocks(hideParams : boolean): toolboxItems.Category { }; } -function getRobotEventsBlocks(): toolboxItems.Category { - // getRobotEventsBlocks is called when the user is editing an opmode. - // It allows the user to create event handlers for events previously defined in the Robot. +const CUSTOM_CATEGORY_ROBOT_EVENTS = 'ROBOT_EVENTS'; - const contents: toolboxItems.ContentsType[] = []; +// The robot events category is shown when the user is editing an opmode. +// It allows the user to create event handlers for events previously defined in the Robot. +const getRobotEventsCategory = () => ({ + kind: 'category', + name: Blockly.Msg['MRC_CATEGORY_EVENTS'], + custom: CUSTOM_CATEGORY_ROBOT_EVENTS, +}); + +export class RobotEventsCategory { + constructor(blocklyWorkspace: Blockly.WorkspaceSvg) { + blocklyWorkspace.registerToolboxCategoryCallback(CUSTOM_CATEGORY_ROBOT_EVENTS, this.robotEventsFlyout.bind(this)); + } + + public robotEventsFlyout(workspace: Blockly.WorkspaceSvg) { + const contents: toolboxItems.ContentsType[] = []; + + // Get the list of events from the robot and add the blocks for handling events. - // Get the list of events from the robot and add the blocks for calling the - // robot functions. - const workspace = Blockly.getMainWorkspace(); - if (workspace) { const editor = Editor.getEditorForBlocklyWorkspace(workspace); if (editor) { const eventsFromRobot = editor.getEventsFromRobot(); - addRobotEventHandlerBlocks(eventsFromRobot, contents); + // Remove events if there is already a corresponding handler in the workspace. + const eventHandlerNames = editor.getEventHandlerNamesFromWorkspace(); + const eventsToShow = eventsFromRobot.filter(event => { + return !eventHandlerNames.includes(event.name); + }); + addRobotEventHandlerBlocks(eventsToShow, contents); } - } - return { - kind: 'category', - name: Blockly.Msg['MRC_CATEGORY_EVENTS'], - contents, - }; + const toolboxInfo = { + contents: contents, + }; + + return toolboxInfo; + } } diff --git a/src/toolbox/methods_category.ts b/src/toolbox/methods_category.ts index a7708920..02a54fdc 100644 --- a/src/toolbox/methods_category.ts +++ b/src/toolbox/methods_category.ts @@ -40,7 +40,6 @@ export const getCategory = () => ({ }); export class MethodsCategory { - private currentModule: commonStorage.Module | null = null; private robotClassBlocks = getBaseClassBlocks(CLASS_NAME_ROBOT_BASE); private mechanismClassBlocks = getBaseClassBlocks(CLASS_NAME_MECHANISM); private opmodeClassBlocks = getBaseClassBlocks(CLASS_NAME_OPMODE); @@ -49,10 +48,6 @@ export class MethodsCategory { blocklyWorkspace.registerToolboxCategoryCallback(CUSTOM_CATEGORY_METHODS, this.methodsFlyout.bind(this)); } - public setCurrentModule(currentModule: commonStorage.Module | null) { - this.currentModule = currentModule; - } - public methodsFlyout(workspace: Blockly.WorkspaceSvg) { const contents: toolboxItems.ContentsType[] = []; @@ -65,8 +60,8 @@ export class MethodsCategory { // Collect the method names that are already overridden in the blockly workspace. const methodNamesAlreadyOverridden = editor.getMethodNamesAlreadyOverriddenInWorkspace(); - if (this.currentModule) { - if (this.currentModule.moduleType == commonStorage.MODULE_TYPE_ROBOT) { + switch (editor.getCurrentModuleType()) { + case commonStorage.MODULE_TYPE_ROBOT: // TODO(lizlooney): We need a way to mark a method in python as not overridable. // For example, in RobotBase, register_event_handler, unregister_event_handler, // and fire_event should not be overridden in a user's robot. @@ -79,17 +74,19 @@ export class MethodsCategory { this.addClassBlocksForCurrentModule( 'More Robot Methods', this.robotClassBlocks, methodNamesNotOverrideable, methodNamesAlreadyOverridden, contents); - } else if (this.currentModule.moduleType == commonStorage.MODULE_TYPE_MECHANISM) { + break; + case commonStorage.MODULE_TYPE_MECHANISM: // Add the methods for a Mechanism. this.addClassBlocksForCurrentModule( 'More Mechanism Methods', this.mechanismClassBlocks, [], methodNamesAlreadyOverridden, contents); - } else if (this.currentModule.moduleType == commonStorage.MODULE_TYPE_OPMODE) { + break; + case commonStorage.MODULE_TYPE_OPMODE: // Add the methods for an OpMode. this.addClassBlocksForCurrentModule( 'More OpMode Methods', this.opmodeClassBlocks, [], methodNamesAlreadyOverridden, contents); - } + break; } }