diff --git a/src/App.tsx b/src/App.tsx index 5019038b..c3cc316e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -33,7 +33,6 @@ import ToolboxSettingsModal from './reactComponents/ToolboxSettings'; import * as Tabs from './reactComponents/Tabs'; import { TabType } from './types/TabType'; -import { createGeneratorContext, GeneratorContext } from './editor/generator_context'; import * as editor from './editor/editor'; import { extendedPythonGenerator } from './editor/extended_python_generator'; @@ -161,6 +160,7 @@ const AppContent: React.FC = ({ project, setProject }): React.J const [messageApi, contextHolder] = Antd.message.useMessage(); const [generatedCode, setGeneratedCode] = React.useState(''); const [toolboxSettingsModalIsOpen, setToolboxSettingsModalIsOpen] = React.useState(false); + const [modulePathToContentText, setModulePathToContentText] = React.useState<{[modulePath: string]: string}>({}); const [tabItems, setTabItems] = React.useState([]); const [activeTab, setActiveTab] = React.useState(''); const [shownPythonToolboxCategories, setShownPythonToolboxCategories] = React.useState>(new Set()); @@ -171,9 +171,10 @@ const AppContent: React.FC = ({ project, setProject }): React.J const [languageInitialized, setLanguageInitialized] = React.useState(false); const [themeInitialized, setThemeInitialized] = React.useState(false); - const blocksEditor = React.useRef(null); - const generatorContext = React.useRef(null); - const blocklyComponent = React.useRef(null); + /** modulePaths controls how BlocklyComponents are created. */ + const modulePaths = React.useRef([]); + const modulePathToBlocklyComponent = React.useRef<{[modulePath: string]: BlocklyComponentType}>({}); + const modulePathToEditor = React.useRef<{[modulePath: string]: editor.Editor}>({}); /** Initialize language from UserSettings when app first starts. */ React.useEffect(() => { @@ -207,7 +208,7 @@ const AppContent: React.FC = ({ project, setProject }): React.J // Save current blocks before language change if (currentModule && areBlocksModified()) { try { - await saveBlocks(); + await saveModule(); } catch (e) { console.error('Failed to save blocks before language change:', e); } @@ -222,9 +223,10 @@ const AppContent: React.FC = ({ project, setProject }): React.J } } - // Update toolbox after language change - if (blocksEditor.current) { - blocksEditor.current.updateToolbox(shownPythonToolboxCategories); + // Update toolbox in all editors after language change. + for (const modulePath in modulePathToEditor.current) { + const editor = modulePathToEditor.current[modulePath]; + editor.updateToolbox(shownPythonToolboxCategories); } }; @@ -298,19 +300,32 @@ const AppContent: React.FC = ({ project, setProject }): React.J return; } + // Check whether this blockly workspace is for the current module. + if (!currentModule || + !(currentModule.modulePath in modulePathToBlocklyComponent.current)) { + return; + } + const blocklyComponent = modulePathToBlocklyComponent.current[currentModule.modulePath]; + if (event.workspaceId != blocklyComponent.getBlocklyWorkspace().id) { + return; + } + setTriggerPythonRegeneration(Date.now()); }; /** Saves blocks to storage with success/error messaging. */ - const saveBlocks = async (): Promise => { + const saveModule = async (): Promise => { return new Promise(async (resolve, reject) => { - if (!blocksEditor.current) { + if (!currentModule || + !(currentModule.modulePath in modulePathToEditor.current)) { reject(new Error('Blocks editor not initialized')); return; } + const editor = modulePathToEditor.current[currentModule.modulePath]; try { - await blocksEditor.current.saveBlocks(); + const moduleContentText = await editor.saveModule(); + modulePathToContentText[currentModule.modulePath] = moduleContentText; messageApi.open({ type: 'success', content: SAVE_SUCCESS_MESSAGE, @@ -339,13 +354,18 @@ const AppContent: React.FC = ({ project, setProject }): React.J /** Checks if blocks have been modified. */ const areBlocksModified = (): boolean => { - return blocksEditor.current ? blocksEditor.current.isModified() : false; + if (currentModule && + currentModule.modulePath in modulePathToEditor.current) { + const editor = modulePathToEditor.current[currentModule.modulePath]; + return editor.isModified(); + } + return false; }; /** Changes current module with automatic saving if modified. */ const changeModule = async (module: storageModule.Module | null): Promise => { if (currentModule && areBlocksModified()) { - await saveBlocks(); + await saveModule(); } setCurrentModule(module); }; @@ -391,11 +411,13 @@ const AppContent: React.FC = ({ project, setProject }): React.J }; /** Handles toolbox update requests from blocks */ - const handleToolboxUpdateRequest = React.useCallback(() => { - if (blocksEditor.current && currentModule) { - blocksEditor.current.updateToolbox(shownPythonToolboxCategories); + const handleToolboxUpdateRequest = React.useCallback((e: Event) => { + const workspaceId = (e as CustomEvent).detail.workspaceId; + const correspondingEditor = editor.Editor.getEditorForBlocklyWorkspaceId(workspaceId); + if (correspondingEditor) { + correspondingEditor.updateToolbox(shownPythonToolboxCategories); } - }, [currentModule, shownPythonToolboxCategories, i18n.language]); + }, [shownPythonToolboxCategories, i18n.language]); // Add event listener for toolbox updates React.useEffect(() => { @@ -417,85 +439,153 @@ const AppContent: React.FC = ({ project, setProject }): React.J // Update generator context and load module blocks when current module changes React.useEffect(() => { - if (generatorContext.current) { - generatorContext.current.setModule(currentModule); - } - if (blocksEditor.current) { - blocksEditor.current.loadModuleBlocks(currentModule, project); + if (currentModule) { + if (modulePaths.current.includes(currentModule.modulePath)) { + activateEditor(); + } else { + // Add the module path to modulePaths to create a new BlocklyComponent. + modulePaths.current.push(currentModule.modulePath); + } } }, [currentModule]); - const setupWorkspace = (newWorkspace: Blockly.WorkspaceSvg) => { - if (!blocklyComponent.current || !storage) { + const activateEditor = () => { + if (!project || !currentModule) { + return; + } + for (const modulePath in modulePathToBlocklyComponent.current) { + const blocklyComponent = modulePathToBlocklyComponent.current[modulePath]; + const active = (modulePath === currentModule.modulePath); + const workspaceIsVisible = blocklyComponent.getBlocklyWorkspace()!.isVisible(); + if (active != workspaceIsVisible) { + blocklyComponent.setActive(active); + } + } + if (currentModule.modulePath in modulePathToEditor.current) { + const editor = modulePathToEditor.current[currentModule.modulePath]; + editor.makeCurrent(project, modulePathToContentText); + } + }; + + const setupBlocklyComponent = (modulePath: string, newBlocklyComponent: BlocklyComponentType) => { + modulePathToBlocklyComponent.current[modulePath] = newBlocklyComponent; + if (currentModule) { + newBlocklyComponent.setActive(modulePath === currentModule.modulePath); + } + }; + + const setupWorkspace = (modulePath: string, newWorkspace: Blockly.WorkspaceSvg) => { + if (!project || !storage) { + return; + } + const module = storageProject.findModuleByModulePath(project, modulePath); + if (!module) { + console.error("setupWorkspace called for unknown module path " + modulePath); return; } - // Recreate workspace when Blockly component is ready + ChangeFramework.setup(newWorkspace); newWorkspace.addChangeListener(mutatorOpenListener); newWorkspace.addChangeListener(handleBlocksChanged); registerToolboxButton(newWorkspace, messageApi); - generatorContext.current = createGeneratorContext(); - - if (currentModule) { - generatorContext.current.setModule(currentModule); + const oldEditor = modulePathToEditor.current[modulePath]; + if (oldEditor) { + oldEditor.abandon(); } - if (blocksEditor.current) { - blocksEditor.current.abandon(); - } - blocksEditor.current = new editor.Editor(newWorkspace, generatorContext.current, storage); - blocksEditor.current.makeCurrent(); + const newEditor = new editor.Editor( + newWorkspace, module, project, storage, modulePathToContentText); + modulePathToEditor.current[modulePath] = newEditor; + newEditor.loadModuleBlocks(); + newEditor.updateToolbox(shownPythonToolboxCategories); - // Set the current module in the editor after creating it - if (currentModule) { - blocksEditor.current.loadModuleBlocks(currentModule, project); + if (currentModule && currentModule.modulePath === modulePath) { + activateEditor(); } - - blocksEditor.current.updateToolbox(shownPythonToolboxCategories); }; - // Initialize Blockly workspace and editor when component and storage are ready + // Generate code when module or regeneration trigger changes React.useEffect(() => { - if (!blocklyComponent.current || !storage) { - return; - } - - const blocklyWorkspace = blocklyComponent.current.getBlocklyWorkspace(); - if (blocklyWorkspace) { - setupWorkspace(blocklyWorkspace); + let generatedCode = ''; + if (currentModule) { + if (currentModule.modulePath in modulePathToBlocklyComponent.current) { + const blocklyComponent = modulePathToBlocklyComponent.current[currentModule.modulePath]; + generatedCode = extendedPythonGenerator.mrcWorkspaceToCode( + blocklyComponent.getBlocklyWorkspace(), currentModule); + } } - }, [blocklyComponent, storage]); + setGeneratedCode(generatedCode); + }, [currentModule, project, triggerPythonRegeneration]); - // Generate code when module or regeneration trigger changes + // Update toolbox when categories change React.useEffect(() => { - if (currentModule && blocklyComponent.current && generatorContext.current) { - const blocklyWorkspace = blocklyComponent.current.getBlocklyWorkspace(); - setGeneratedCode(extendedPythonGenerator.mrcWorkspaceToCode( - blocklyWorkspace, - generatorContext.current - )); - } else { - setGeneratedCode(''); + if (currentModule) { + if (currentModule.modulePath in modulePathToEditor.current) { + const editor = modulePathToEditor.current[currentModule.modulePath]; + editor.updateToolbox(shownPythonToolboxCategories); + } } - }, [currentModule, project, triggerPythonRegeneration, blocklyComponent]); + }, [shownPythonToolboxCategories]); - // Update toolbox when module or categories change + // Fetch modules when project changes. React.useEffect(() => { - if (blocksEditor.current) { - blocksEditor.current.updateToolbox(shownPythonToolboxCategories); + if (project && storage) { + const fetchModules = async () => { + const promises: {[modulePath: string]: Promise} = {}; // value is promise of module content. + promises[project.robot.modulePath] = storage.fetchFileContentText(project.robot.modulePath); + project.mechanisms.forEach(mechanism => { + promises[mechanism.modulePath] = storage.fetchFileContentText(mechanism.modulePath); + }); + project.opModes.forEach(opmode => { + promises[opmode.modulePath] = storage.fetchFileContentText(opmode.modulePath); + }); + const updatedModulePathToContentText: {[modulePath: string]: string} = {}; // value is module content text + await Promise.all( + Object.entries(promises).map(async ([modulePath, promise]) => { + updatedModulePathToContentText[modulePath] = await promise; + }) + ); + const oldModulePathToContentText = modulePathToContentText; + setModulePathToContentText(updatedModulePathToContentText); + + // Remove any deleted modules from modulePaths, modulePathToBlocklyComponent, and + // modulePathToEditor. Update currentModule if the current module was deleted. + for (const modulePath in oldModulePathToContentText) { + if (modulePath in updatedModulePathToContentText) { + continue; + } + if (currentModule && currentModule.modulePath === modulePath) { + setCurrentModule(project.robot); + setActiveTab(project.robot.modulePath); + } + const indexToRemove: number = modulePaths.current.indexOf(modulePath); + if (indexToRemove !== -1) { + modulePaths.current.splice(indexToRemove, 1); + } + if (modulePath in modulePathToBlocklyComponent.current) { + delete modulePathToBlocklyComponent.current[modulePath]; + } + if (modulePath in modulePathToEditor.current) { + const editor = modulePathToEditor.current[modulePath]; + editor.abandon(); + delete modulePathToEditor.current[modulePath]; + } + } + }; + fetchModules(); } - }, [currentModule, shownPythonToolboxCategories]); + }, [project]); - // Update tab items when project changes + // Update tab items when fetching modules is done. React.useEffect(() => { if (project) { const tabs = createTabItemsFromProject(project); setTabItems(tabs); setActiveTab(project.robot.modulePath); } - }, [project]); + }, [modulePathToContentText]); const { Sider, Content } = Antd.Layout; @@ -546,11 +636,15 @@ const AppContent: React.FC = ({ project, setProject }): React.J /> - + {modulePaths.current.map((modulePath) => ( + + ))} { const event = new CustomEvent(TOOLBOX_UPDATE_EVENT, { - detail: { timestamp: Date.now() } + detail: { + timestamp: Date.now(), + workspaceId: block.workspace.id, + } }); window.dispatchEvent(event); toolboxUpdateTimeout = null; diff --git a/src/editor/editor.ts b/src/editor/editor.ts index 900d1d3d..7dc1d0e1 100644 --- a/src/editor/editor.ts +++ b/src/editor/editor.ts @@ -22,7 +22,6 @@ import * as Blockly from 'blockly/core'; import { extendedPythonGenerator } from './extended_python_generator'; -import { GeneratorContext } from './generator_context'; import * as commonStorage from '../storage/common_storage'; import * as storageModule from '../storage/module'; import * as storageModuleContent from '../storage/module_content'; @@ -43,27 +42,43 @@ export class Editor { private static workspaceIdToEditor: { [workspaceId: string]: Editor } = {}; private static currentEditor: Editor | null = null; - private blocklyWorkspace: Blockly.WorkspaceSvg; - private generatorContext: GeneratorContext; - private storage: commonStorage.Storage; - private currentModule: storageModule.Module | null = null; - private currentProject: storageProject.Project | null = null; - private modulePath: string = ''; - private robotPath: string = ''; - private moduleContentText: string = ''; + private readonly blocklyWorkspace: Blockly.WorkspaceSvg; + private readonly module: storageModule.Module; + private readonly projectName: string; + private readonly storage: commonStorage.Storage; + private readonly modulePath: string; + private readonly robotPath: string; + private moduleContentText: string; + private modulePathToModuleContent: {[modulePath: string]: storageModuleContent.ModuleContent} = {}; private robotContent: storageModuleContent.ModuleContent | null = null; + private mechanisms: storageModule.Mechanism[] = []; private mechanismClassNameToModuleContent: {[mechanismClassName: string]: storageModuleContent.ModuleContent} = {}; private bindedOnChange: any = null; + private shownPythonToolboxCategories: Set | null = null; private toolbox: Blockly.utils.toolbox.ToolboxDefinition = EMPTY_TOOLBOX; - constructor(blocklyWorkspace: Blockly.WorkspaceSvg, generatorContext: GeneratorContext, storage: commonStorage.Storage) { - Editor.workspaceIdToEditor[blocklyWorkspace.id] = this; + constructor( + blocklyWorkspace: Blockly.WorkspaceSvg, + module: storageModule.Module, + project: storageProject.Project, + storage: commonStorage.Storage, + modulePathToContentText: {[modulePath: string]: string}) { this.blocklyWorkspace = blocklyWorkspace; - this.generatorContext = generatorContext; + this.module = module; + this.projectName = project.projectName; this.storage = storage; + this.modulePath = module.modulePath; + this.robotPath = project.robot.modulePath; + this.moduleContentText = modulePathToContentText[module.modulePath]; + this.parseModules(project, modulePathToContentText); + Editor.workspaceIdToEditor[blocklyWorkspace.id] = this; } private onChangeWhileLoading(event: Blockly.Events.Abstract) { + if (!this.blocklyWorkspace.rendered) { + // This editor has been abandoned. + return; + } if (event.type === Blockly.Events.FINISHED_LOADING) { // Remove the while-loading listener. this.blocklyWorkspace.removeChangeListener(this.bindedOnChange); @@ -96,14 +111,24 @@ export class Editor { // UI events are things like scrolling, zooming, etc. return; } + if (!this.blocklyWorkspace.rendered) { + // This editor has been abandoned. + return; + } if (this.blocklyWorkspace.isDragging()) { return; } // TODO(lizlooney): do we need to do anything here? } - public makeCurrent(): void { + public makeCurrent( + project: storageProject.Project, + modulePathToContentText: {[modulePath: string]: string}): void { Editor.currentEditor = this; + + // Parse modules since they might have changed. + this.parseModules(project, modulePathToContentText); + this.updateToolboxImpl(); } public abandon(): void { @@ -115,99 +140,59 @@ export class Editor { } } - public async loadModuleBlocks( - currentModule: storageModule.Module | null, - currentProject: storageProject.Project | null) { - this.generatorContext.setModule(currentModule); - this.currentModule = currentModule; - this.currentProject = currentProject; - - if (this.currentModule && this.currentProject) { - this.modulePath = this.currentModule.modulePath; - this.robotPath = this.currentProject.robot.modulePath; - } else { - this.modulePath = ''; - this.robotPath = ''; - } - this.moduleContentText = ''; - this.robotContent = null; - this.mechanismClassNameToModuleContent = {} - this.clearBlocklyWorkspace(); - - if (this.currentModule && this.currentProject) { - // Fetch the content for the current module, the robot, and the mechanisms. - const promises: { [modulePath: string]: Promise } = {}; // value is promise of module content. - promises[this.modulePath] = this.storage.fetchFileContentText(this.modulePath); - if (this.robotPath !== this.modulePath) { - // Also fetch the robot module content. It contains components, etc, that can be used in OpModes. - promises[this.robotPath] = this.storage.fetchFileContentText(this.robotPath) - } - for (const mechanism of this.currentProject.mechanisms) { - // Fetch the module content text for the mechanism. - if (mechanism.modulePath !== this.modulePath) { - promises[mechanism.modulePath] = this.storage.fetchFileContentText(mechanism.modulePath); - } - } - - const modulePathToContentText: { [modulePath: string]: string } = {}; // value is module content - await Promise.all( - Object.entries(promises).map(async ([modulePath, promise]) => { - modulePathToContentText[modulePath] = await promise; - }) - ); - this.moduleContentText = modulePathToContentText[this.modulePath]; - this.robotContent = storageModuleContent.parseModuleContentText( - (this.robotPath === this.modulePath) - ? this.moduleContentText - : modulePathToContentText[this.robotPath]); - for (const mechanism of this.currentProject.mechanisms) { - this.mechanismClassNameToModuleContent[mechanism.className] = - storageModuleContent.parseModuleContentText( - (mechanism.modulePath === this.modulePath) - ? this.moduleContentText - : modulePathToContentText[mechanism.modulePath]); - } - this.loadBlocksIntoBlocklyWorkspace(); + private parseModules( + project: storageProject.Project, + modulePathToContentText: {[modulePath: string]: string}): void { + // Parse the modules. + this.modulePathToModuleContent = {} + for (const modulePath in modulePathToContentText) { + const moduleContentText = modulePathToContentText[modulePath]; + this.modulePathToModuleContent[modulePath] = storageModuleContent.parseModuleContentText( + moduleContentText); } - } - private clearBlocklyWorkspace() { - if (this.bindedOnChange) { - this.blocklyWorkspace.removeChangeListener(this.bindedOnChange); - this.bindedOnChange = null; - } - this.blocklyWorkspace.hideChaff(); - this.blocklyWorkspace.clear(); - this.blocklyWorkspace.scroll(0, 0); - this.setToolbox(EMPTY_TOOLBOX); - } + this.robotContent = this.modulePathToModuleContent[this.robotPath]; - private setToolbox(toolbox: Blockly.utils.toolbox.ToolboxDefinition) { - if (toolbox != this.toolbox) { - this.toolbox = toolbox; - this.blocklyWorkspace.updateToolbox(toolbox); - // testAllBlocksInToolbox(toolbox); + this.mechanisms = project.mechanisms; + this.mechanismClassNameToModuleContent = {}; + for (const mechanism of this.mechanisms) { + const moduleContent = this.modulePathToModuleContent[mechanism.modulePath]; + if (!moduleContent) { + console.error(this.modulePath + " editor.parseModules - modulePathToModuleContent['" + + mechanism.modulePath + "'] is undefined"); + continue; + } + this.mechanismClassNameToModuleContent[mechanism.className] = moduleContent; } } - private loadBlocksIntoBlocklyWorkspace() { + public loadModuleBlocks() { + if (!this.blocklyWorkspace.rendered) { + // This editor has been abandoned. + return; + } // Add the while-loading listener. this.bindedOnChange = this.onChangeWhileLoading.bind(this); this.blocklyWorkspace.addChangeListener(this.bindedOnChange); - const moduleContent = storageModuleContent.parseModuleContentText(this.moduleContentText); + const moduleContent = this.modulePathToModuleContent[this.modulePath]; Blockly.serialization.workspaces.load(moduleContent.getBlocks(), this.blocklyWorkspace); } public updateToolbox(shownPythonToolboxCategories: Set): void { - if (this.currentModule) { - if (!this.robotContent) { - // The Robot content hasn't been fetched yet. Try again in a bit. - setTimeout(() => { - this.updateToolbox(shownPythonToolboxCategories) - }, 50); - return; - } - this.setToolbox(getToolboxJSON(shownPythonToolboxCategories, this)); + this.shownPythonToolboxCategories = shownPythonToolboxCategories; + this.updateToolboxImpl(); + } + + private updateToolboxImpl(): void { + if (!this.blocklyWorkspace.rendered) { + // This editor has been abandoned. + return; + } + const toolbox = getToolboxJSON(this.shownPythonToolboxCategories, this); + if (toolbox != this.toolbox) { + this.toolbox = toolbox; + this.blocklyWorkspace.updateToolbox(toolbox); + // testAllBlocksInToolbox(toolbox); } } @@ -228,21 +213,18 @@ export class Editor { return this.blocklyWorkspace; } - public getCurrentModuleType(): storageModule.ModuleType | null { - if (this.currentModule) { - return this.currentModule.moduleType; - } - return null; + public getModuleType(): storageModule.ModuleType { + return this.module.moduleType; } private getModuleContentText(): string { - if (!this.currentModule) { - throw new Error('getModuleContentText: this.currentModule is null.'); + if (!this.blocklyWorkspace.rendered) { + // This editor has been abandoned. + throw new Error('getModuleContentText: this.blocklyWorkspace has been disposed.'); } // Generate python because some parts of components, events, and methods are affected. - extendedPythonGenerator.init(this.blocklyWorkspace); - extendedPythonGenerator.mrcWorkspaceToCode(this.blocklyWorkspace, this.generatorContext); + extendedPythonGenerator.mrcWorkspaceToCode(this.blocklyWorkspace, this.module); const blocks = Blockly.serialization.workspaces.save(this.blocklyWorkspace); const mechanisms: storageModuleContent.MechanismInRobot[] = this.getMechanismsFromWorkspace(); @@ -250,61 +232,89 @@ export class Editor { const privateComponents: storageModuleContent.Component[] = this.getPrivateComponentsFromWorkspace(); const events: storageModuleContent.Event[] = this.getEventsFromWorkspace(); const methods: storageModuleContent.Method[] = ( - this.currentModule?.moduleType === storageModule.ModuleType.ROBOT || - this.currentModule?.moduleType === storageModule.ModuleType.MECHANISM) + this.module.moduleType === storageModule.ModuleType.ROBOT || + this.module.moduleType === storageModule.ModuleType.MECHANISM) ? this.getMethodsForOutsideFromWorkspace() : []; return storageModuleContent.makeModuleContentText( - this.currentModule, blocks, mechanisms, components, privateComponents, events, methods); + this.module, blocks, mechanisms, components, privateComponents, events, methods); } private getMechanismsFromWorkspace(): storageModuleContent.MechanismInRobot[] { + if (!this.blocklyWorkspace.rendered) { + // This editor has been abandoned. + throw new Error('this.blocklyWorkspace has been disposed.'); + } const mechanisms: storageModuleContent.MechanismInRobot[] = []; - if (this.currentModule?.moduleType === storageModule.ModuleType.ROBOT) { + if (this.module.moduleType === storageModule.ModuleType.ROBOT) { mechanismComponentHolder.getMechanisms(this.blocklyWorkspace, mechanisms); } return mechanisms; } private getComponentsFromWorkspace(): storageModuleContent.Component[] { + if (!this.blocklyWorkspace.rendered) { + // This editor has been abandoned. + throw new Error('this.blocklyWorkspace has been disposed.'); + } const components: storageModuleContent.Component[] = []; - if (this.currentModule?.moduleType === storageModule.ModuleType.ROBOT || - this.currentModule?.moduleType === storageModule.ModuleType.MECHANISM) { + if (this.module.moduleType === storageModule.ModuleType.ROBOT || + this.module.moduleType === storageModule.ModuleType.MECHANISM) { mechanismComponentHolder.getComponents(this.blocklyWorkspace, components); } return components; } private getPrivateComponentsFromWorkspace(): storageModuleContent.Component[] { + if (!this.blocklyWorkspace.rendered) { + // This editor has been abandoned. + throw new Error('this.blocklyWorkspace has been disposed.'); + } const components: storageModuleContent.Component[] = []; - if (this.currentModule?.moduleType === storageModule.ModuleType.MECHANISM) { + if (this.module.moduleType === storageModule.ModuleType.MECHANISM) { mechanismComponentHolder.getPrivateComponents(this.blocklyWorkspace, components); } return components; } public getAllComponentsFromWorkspace(): storageModuleContent.Component[] { + if (!this.blocklyWorkspace.rendered) { + // This editor has been abandoned. + throw new Error('this.blocklyWorkspace has been disposed.'); + } const components: storageModuleContent.Component[] = []; - if (this.currentModule?.moduleType === storageModule.ModuleType.ROBOT || - this.currentModule?.moduleType === storageModule.ModuleType.MECHANISM) { + if (this.module.moduleType === storageModule.ModuleType.ROBOT || + this.module.moduleType === storageModule.ModuleType.MECHANISM) { mechanismComponentHolder.getAllComponents(this.blocklyWorkspace, components); } return components; } public getMethodsForWithinFromWorkspace(): storageModuleContent.Method[] { + if (!this.blocklyWorkspace.rendered) { + // This editor has been abandoned. + throw new Error('this.blocklyWorkspace has been disposed.'); + } const methods: storageModuleContent.Method[] = []; classMethodDef.getMethodsForWithin(this.blocklyWorkspace, methods); return methods; } private getMethodsForOutsideFromWorkspace(): storageModuleContent.Method[] { + if (!this.blocklyWorkspace.rendered) { + // This editor has been abandoned. + throw new Error('this.blocklyWorkspace has been disposed.'); + } const methods: storageModuleContent.Method[] = []; classMethodDef.getMethodsForOutside(this.blocklyWorkspace, methods); return methods; } public getMethodNamesAlreadyOverriddenInWorkspace(): string[] { + if (!this.blocklyWorkspace.rendered) { + // This editor has been abandoned. + throw new Error('this.blocklyWorkspace has been disposed.'); + } const methodNamesAlreadyOverridden: string[] = []; classMethodDef.getMethodNamesAlreadyOverriddenInWorkspace( this.blocklyWorkspace, methodNamesAlreadyOverridden); @@ -312,15 +322,23 @@ export class Editor { } public getEventsFromWorkspace(): storageModuleContent.Event[] { + if (!this.blocklyWorkspace.rendered) { + // This editor has been abandoned. + throw new Error('this.blocklyWorkspace has been disposed.'); + } const events: storageModuleContent.Event[] = []; - if (this.currentModule?.moduleType === storageModule.ModuleType.ROBOT || - this.currentModule?.moduleType === storageModule.ModuleType.MECHANISM) { + if (this.module.moduleType === storageModule.ModuleType.ROBOT || + this.module.moduleType === storageModule.ModuleType.MECHANISM) { mechanismComponentHolder.getEvents(this.blocklyWorkspace, events); } return events; } public getRobotEventHandlersAlreadyInWorkspace(): eventHandler.EventHandlerBlock[] { + if (!this.blocklyWorkspace.rendered) { + // This editor has been abandoned. + throw new Error('this.blocklyWorkspace has been disposed.'); + } const eventHandlerBlocks: eventHandler.EventHandlerBlock[] = []; eventHandler.getRobotEventHandlerBlocks(this.blocklyWorkspace, eventHandlerBlocks); return eventHandlerBlocks; @@ -328,30 +346,33 @@ export class Editor { public getMechanismEventHandlersAlreadyInWorkspace( mechanismInRobot: storageModuleContent.MechanismInRobot): eventHandler.EventHandlerBlock[] { + if (!this.blocklyWorkspace.rendered) { + // This editor has been abandoned. + throw new Error('this.blocklyWorkspace has been disposed.'); + } const eventHandlerBlocks: eventHandler.EventHandlerBlock[] = []; eventHandler.getMechanismEventHandlerBlocks( this.blocklyWorkspace, mechanismInRobot.mechanismId, eventHandlerBlocks); return eventHandlerBlocks; } - public async saveBlocks() { + public async saveModule(): Promise { const moduleContentText = this.getModuleContentText(); try { await this.storage.saveFile(this.modulePath, moduleContentText); this.moduleContentText = moduleContentText; - if (this.currentProject) { - await storageProject.saveProjectInfo(this.storage, this.currentProject.projectName); - } + await storageProject.saveProjectInfo(this.storage, this.projectName); } catch (e) { throw e; } + return moduleContentText; } /** * Returns the mechanisms defined in the robot. */ public getMechanismsFromRobot(): storageModuleContent.MechanismInRobot[] { - if (this.currentModule?.moduleType === storageModule.ModuleType.ROBOT) { + if (this.module.moduleType === storageModule.ModuleType.ROBOT) { return this.getMechanismsFromWorkspace(); } if (this.robotContent) { @@ -364,7 +385,7 @@ export class Editor { * Returns the components defined in the robot. */ public getComponentsFromRobot(): storageModuleContent.Component[] { - if (this.currentModule?.moduleType === storageModule.ModuleType.ROBOT) { + if (this.module.moduleType === storageModule.ModuleType.ROBOT) { return this.getComponentsFromWorkspace(); } if (this.robotContent) { @@ -377,7 +398,7 @@ export class Editor { * Returns the events defined in the robot. */ public getEventsFromRobot(): storageModuleContent.Event[] { - if (this.currentModule?.moduleType === storageModule.ModuleType.ROBOT) { + if (this.module.moduleType === storageModule.ModuleType.ROBOT) { return this.getEventsFromWorkspace(); } if (this.robotContent) { @@ -390,7 +411,7 @@ export class Editor { * Returns the methods defined in the robot. */ public getMethodsFromRobot(): storageModuleContent.Method[] { - if (this.currentModule?.moduleType === storageModule.ModuleType.ROBOT) { + if (this.module.moduleType === storageModule.ModuleType.ROBOT) { return this.getMethodsForWithinFromWorkspace(); } if (this.robotContent) { @@ -403,19 +424,17 @@ export class Editor { * Returns the mechanisms in this project. */ public getMechanisms(): storageModule.Mechanism[] { - return this.currentProject ? this.currentProject.mechanisms : []; + return this.mechanisms; } /** * Returns the Mechanism matching the given MechanismInRobot. */ public getMechanism(mechanismInRobot: storageModuleContent.MechanismInRobot): storageModule.Mechanism | null { - if (this.currentProject) { - for (const mechanism of this.currentProject.mechanisms) { - const fullClassName = storageNames.pascalCaseToSnakeCase(mechanism.className) + '.' + mechanism.className; - if (fullClassName === mechanismInRobot.className) { - return mechanism; - } + for (const mechanism of this.mechanisms) { + const fullClassName = storageNames.pascalCaseToSnakeCase(mechanism.className) + '.' + mechanism.className; + if (fullClassName === mechanismInRobot.className) { + return mechanism; } } return null; @@ -425,7 +444,7 @@ export class Editor { * Returns the components defined in the given mechanism. */ public getComponentsFromMechanism(mechanism: storageModule.Mechanism): storageModuleContent.Component[] { - if (this.currentModule?.modulePath === mechanism.modulePath) { + if (this.module.modulePath === mechanism.modulePath) { return this.getComponentsFromWorkspace(); } if (mechanism.className in this.mechanismClassNameToModuleContent) { @@ -439,7 +458,7 @@ export class Editor { * This is used when creating mechanism blocks that need all components for port parameters. */ public getAllComponentsFromMechanism(mechanism: storageModule.Mechanism): storageModuleContent.Component[] { - if (this.currentModule?.modulePath === mechanism.modulePath) { + if (this.module.modulePath === mechanism.modulePath) { return this.getAllComponentsFromWorkspace(); } if (mechanism.className in this.mechanismClassNameToModuleContent) { @@ -457,7 +476,7 @@ export class Editor { * Returns the events defined in the given mechanism. */ public getEventsFromMechanism(mechanism: storageModule.Mechanism): storageModuleContent.Event[] { - if (this.currentModule?.modulePath === mechanism.modulePath) { + if (this.module.modulePath === mechanism.modulePath) { return this.getEventsFromWorkspace(); } if (mechanism.className in this.mechanismClassNameToModuleContent) { @@ -470,7 +489,7 @@ export class Editor { * Returns the methods defined in the given mechanism. */ public getMethodsFromMechanism(mechanism: storageModule.Mechanism): storageModuleContent.Method[] { - if (this.currentModule?.modulePath === mechanism.modulePath) { + if (this.module.modulePath === mechanism.modulePath) { return this.getMethodsForWithinFromWorkspace(); } if (mechanism.className in this.mechanismClassNameToModuleContent) { @@ -479,21 +498,25 @@ export class Editor { throw new Error('getMethodsFromMechanism: mechanism not found: ' + mechanism.className); } - public static getEditorForBlocklyWorkspace(workspace: Blockly.Workspace): Editor | null { + public static getEditorForBlocklyWorkspace(workspace: Blockly.Workspace, opt_returnCurrentIfNotFound?: boolean): Editor | null { if (workspace.id in Editor.workspaceIdToEditor) { return Editor.workspaceIdToEditor[workspace.id]; } - // If the workspace id was not found, it might be because the workspace is associated with a block mutator's flyout. - // Try this workspaces's root workspace. + // If the workspace id was not found, it might be because the workspace is associated with a + // block mutator's flyout. Try this workspaces's root workspace. const rootWorkspace = workspace.getRootWorkspace(); if (rootWorkspace && rootWorkspace.id in Editor.workspaceIdToEditor) { return Editor.workspaceIdToEditor[rootWorkspace.id]; } - // Otherwise, return the current editor. - return Editor.currentEditor; + return opt_returnCurrentIfNotFound ? Editor.currentEditor : null; + } + + public static getEditorForBlocklyWorkspaceId(workspaceId: string): Editor | null { + const workspace = Blockly.Workspace.getById(workspaceId); + return workspace ? Editor.getEditorForBlocklyWorkspace(workspace) : null; } public static getCurrentEditor(): Editor | null { diff --git a/src/editor/extended_python_generator.ts b/src/editor/extended_python_generator.ts index f22b52fb..d4a207aa 100644 --- a/src/editor/extended_python_generator.ts +++ b/src/editor/extended_python_generator.ts @@ -21,7 +21,7 @@ import * as Blockly from 'blockly/core'; import { PythonGenerator } from 'blockly/python'; -import { GeneratorContext } from './generator_context'; +import { createGeneratorContext, GeneratorContext } from './generator_context'; import * as mechanismContainerHolder from '../blocks/mrc_mechanism_component_holder'; import * as eventHandler from '../blocks/mrc_event_handler'; import { @@ -69,7 +69,7 @@ export class OpModeDetails { export class ExtendedPythonGenerator extends PythonGenerator { private workspace: Blockly.Workspace | null = null; - private context: GeneratorContext | null = null; + private readonly context: GeneratorContext; // Fields related to generating the __init__ for a mechanism. private hasAnyComponents = false; @@ -85,6 +85,7 @@ export class ExtendedPythonGenerator extends PythonGenerator { constructor() { super('Python'); + this.context = createGeneratorContext(); } init(workspace: Blockly.Workspace){ @@ -128,22 +129,24 @@ export class ExtendedPythonGenerator extends PythonGenerator { return "self." + varName; } - mrcWorkspaceToCode(workspace: Blockly.Workspace, context: GeneratorContext): string { + mrcWorkspaceToCode(workspace: Blockly.Workspace, module: storageModule.Module): string { this.workspace = workspace; - this.context = context; + + this.context.setModule(module); + this.init(workspace); this.hasAnyComponents = false; this.componentPorts = Object.create(null); if (this.getModuleType() === storageModule.ModuleType.MECHANISM) { - this.hasAnyComponents = mechanismContainerHolder.hasAnyComponents(this.workspace); - mechanismContainerHolder.getComponentPorts(this.workspace, this.componentPorts); + this.hasAnyComponents = mechanismContainerHolder.hasAnyComponents(workspace); + mechanismContainerHolder.getComponentPorts(workspace, this.componentPorts); } - this.hasAnyEventHandlers = eventHandler.getHasAnyEnabledEventHandlers(this.workspace); + this.hasAnyEventHandlers = eventHandler.getHasAnyEnabledEventHandlers(workspace); const code = super.workspaceToCode(workspace); - this.workspace = workspace; - this.context = null; + this.context.setModule(null); + this.workspace = null; return code; } @@ -192,7 +195,7 @@ export class ExtendedPythonGenerator extends PythonGenerator { } finish(code: string): string { - if (this.context && this.workspace) { + if (this.workspace) { const className = this.context.getClassName(); const baseClassName = this.context.getBaseClassName(); const decorations = this.details?.decorations(className); diff --git a/src/reactComponents/BlocklyComponent.tsx b/src/reactComponents/BlocklyComponent.tsx index db3f2f81..5920cda8 100644 --- a/src/reactComponents/BlocklyComponent.tsx +++ b/src/reactComponents/BlocklyComponent.tsx @@ -35,12 +35,15 @@ import { useTranslation } from 'react-i18next'; /** Interface for methods exposed by the BlocklyComponent. */ export interface BlocklyComponentType { getBlocklyWorkspace: () => Blockly.WorkspaceSvg; + setActive: (active: boolean) => void; } /** Interface for props passed to the BlocklyComponent. */ export interface BlocklyComponentProps { + modulePath: string; + onBlocklyComponentCreated: (modulePath: string, blocklyComponent: BlocklyComponentType) => void; theme: string; - onWorkspaceRecreated: (workspace: Blockly.WorkspaceSvg) => void; + onWorkspaceCreated: (modulePath: string, workspace: Blockly.WorkspaceSvg) => void; } /** Grid spacing for the Blockly workspace. */ @@ -80,213 +83,252 @@ const WORKSPACE_STYLE: React.CSSProperties = { * React component that renders a Blockly workspace with proper initialization, * cleanup, and resize handling. */ -const BlocklyComponent = React.forwardRef( - (props, ref): React.JSX.Element => { - const blocklyDiv = React.useRef(null); - const workspaceRef = React.useRef(null); - - const { t, i18n } = useTranslation(); - - - const getBlocklyTheme = (): Blockly.Theme => { - const blocklyTheme = 'mrc_theme_' + props.theme.replace(/-/g, '_'); - // Find the theme by key - const themeObj = themes.find(t => t.name === blocklyTheme); - if (!themeObj) { - throw new Error(`Theme not found: ${blocklyTheme}`); - } - - // Return the corresponding Blockly theme - return themeObj; - }; +export default function BlocklyComponent(props: BlocklyComponentProps): React.JSX.Element { + const blocklyDiv = React.useRef(null); + const workspaceRef = React.useRef(null); + const parentDiv = React.useRef(null); + const savedScrollX = React.useRef(0); + const savedScrollY = React.useRef(0); + + const { t, i18n } = useTranslation(); + + + const getBlocklyTheme = (): Blockly.Theme => { + const blocklyTheme = 'mrc_theme_' + props.theme.replace(/-/g, '_'); + // Find the theme by key + const themeObj = themes.find(t => t.name === blocklyTheme); + if (!themeObj) { + throw new Error(`Theme not found: ${blocklyTheme}`); + } - /** Creates the Blockly workspace configuration object. */ - const createWorkspaceConfig = (): Blockly.BlocklyOptions => ({ - theme: getBlocklyTheme(), - horizontalLayout: false, // Forces vertical layout for the workspace - // Start with an empty (but not null) toolbox. It will be replaced later. - toolbox: { - kind: 'categoryToolbox', - contents: [], - }, - grid: { - spacing: GRID_SPACING, - length: GRID_LENGTH, - colour: GRID_COLOR, - snap: true, - }, - zoom: { - controls: true, - wheel: true, - startScale: DEFAULT_ZOOM_START_SCALE, - maxScale: MAX_ZOOM_SCALE, - minScale: MIN_ZOOM_SCALE, - scaleSpeed: ZOOM_SCALE_SPEED, - }, - scrollbars: true, - trashcan: false, - move: { - scrollbars: true, - drag: true, - wheel: true, - }, - oneBasedIndex: false, - plugins: { - ...HardwareConnectionsPluginInfo, - }, - }); - - /** Updates the Blockly locale when the language changes. */ - const updateBlocklyLocale = (): void => { - if (!workspaceRef.current) { - return; - } + // Return the corresponding Blockly theme + return themeObj; + }; + + /** Creates the Blockly workspace configuration object. */ + const createWorkspaceConfig = (): Blockly.BlocklyOptions => ({ + theme: getBlocklyTheme(), + horizontalLayout: false, // Forces vertical layout for the workspace + // Start with an empty (but not null) toolbox. It will be replaced later. + toolbox: { + kind: 'categoryToolbox', + contents: [], + }, + grid: { + spacing: GRID_SPACING, + length: GRID_LENGTH, + colour: GRID_COLOR, + snap: true, + }, + zoom: { + controls: true, + wheel: true, + startScale: DEFAULT_ZOOM_START_SCALE, + maxScale: MAX_ZOOM_SCALE, + minScale: MIN_ZOOM_SCALE, + scaleSpeed: ZOOM_SCALE_SPEED, + }, + scrollbars: true, + trashcan: false, + move: { + scrollbars: true, + drag: true, + wheel: true, + }, + oneBasedIndex: false, + plugins: { + ...HardwareConnectionsPluginInfo, + }, + }); + + /** Updates the Blockly locale when the language changes. */ + const updateBlocklyLocale = (): void => { + if (!workspaceRef.current) { + return; + } - const newIsRtl = i18n.dir() === 'rtl'; - const currentIsRtl = workspaceRef.current.RTL; + const newIsRtl = i18n.dir() === 'rtl'; + const currentIsRtl = workspaceRef.current.RTL; + + // If RTL direction changed, we need to recreate the workspace + if (newIsRtl !== currentIsRtl) { + cleanupWorkspace(); + initializeWorkspace(); + if (props.onWorkspaceCreated) { + props.onWorkspaceCreated(props.modulePath, workspaceRef.current!); + } + return; + } - // If RTL direction changed, we need to recreate the workspace - if (newIsRtl !== currentIsRtl) { - cleanupWorkspace(); - initializeWorkspace(); - if (props.onWorkspaceRecreated) { - props.onWorkspaceRecreated(workspaceRef.current!); - } - return; - } + // Set new locale + switch (i18n.language) { + case 'es': + Blockly.setLocale(Es as any); + break; + case 'en': + Blockly.setLocale(En as any); + break; + case 'he': + Blockly.setLocale(He as any); + break; + default: + Blockly.setLocale(En as any); + break; + } + // Apply custom tokens + Blockly.setLocale(customTokens(t)); - // Set new locale - switch (i18n.language) { - case 'es': - Blockly.setLocale(Es as any); - break; - case 'en': - Blockly.setLocale(En as any); - break; - case 'he': - Blockly.setLocale(He as any); - break; - default: - Blockly.setLocale(En as any); - break; - } - // Apply custom tokens - Blockly.setLocale(customTokens(t)); + // Force complete toolbox rebuild by calling onWorkspaceCreated AFTER locale is set + if (props.onWorkspaceCreated) { + props.onWorkspaceCreated(props.modulePath, workspaceRef.current); + } + }; - // Force complete toolbox rebuild by calling onWorkspaceRecreated AFTER locale is set - if (props.onWorkspaceRecreated) { - props.onWorkspaceRecreated(workspaceRef.current); - } - }; + /** Initializes the Blockly workspace. */ + const initializeWorkspace = (): void => { + if (!blocklyDiv.current) { + return; + } - /** Initializes the Blockly workspace. */ - const initializeWorkspace = (): void => { - if (!blocklyDiv.current) { - return; - } + // Set Blockly locale + switch (i18n.language) { + case 'es': + Blockly.setLocale(Es as any); + break; + case 'en': + Blockly.setLocale(En as any); + break; + case 'he': + Blockly.setLocale(He as any); + break; + default: + Blockly.setLocale(En as any); + break; + } + Blockly.setLocale(customTokens(t)); + + // Create workspace + const workspaceConfig = createWorkspaceConfig(); + workspaceConfig.rtl = i18n.dir() === 'rtl'; + const workspace = Blockly.inject(blocklyDiv.current, workspaceConfig); + workspaceRef.current = workspace; + parentDiv.current = blocklyDiv.current.parentNode as HTMLDivElement; + }; + + /** Cleans up the Blockly workspace on unmount. */ + const cleanupWorkspace = (): void => { + if (workspaceRef.current) { + workspaceRef.current.dispose(); + workspaceRef.current = null; + } + }; + + /** Handles workspace resize events. */ + const handleWorkspaceResize = (): void => { + if (workspaceRef.current) { + if (workspaceRef.current.isVisible() && + Blockly.getMainWorkspace().id === workspaceRef.current.id) { + Blockly.svgResize(workspaceRef.current); + } + } + }; - // Set Blockly locale - switch (i18n.language) { - case 'es': - Blockly.setLocale(Es as any); - break; - case 'en': - Blockly.setLocale(En as any); - break; - case 'he': - Blockly.setLocale(He as any); - break; - default: - Blockly.setLocale(En as any); - break; - } - Blockly.setLocale(customTokens(t)); - - // Create workspace - const workspaceConfig = createWorkspaceConfig(); - workspaceConfig.rtl = i18n.dir() === 'rtl'; - const workspace = Blockly.inject(blocklyDiv.current, workspaceConfig); - workspaceRef.current = workspace; - }; + /** Sets up resize observer for the workspace container. */ + const setupResizeObserver = (): (() => void) | undefined => { + const div = blocklyDiv.current; + if (!div) { + return undefined; + } - /** Cleans up the Blockly workspace on unmount. */ - const cleanupWorkspace = (): void => { - if (workspaceRef.current) { - workspaceRef.current.dispose(); - workspaceRef.current = null; - } - }; + const resizeObserver = new ResizeObserver(handleWorkspaceResize); + resizeObserver.observe(div); - /** Handles workspace resize events. */ - const handleWorkspaceResize = (): void => { - if (workspaceRef.current) { - Blockly.svgResize(workspaceRef.current); - } - }; + return () => { + resizeObserver.unobserve(div); + }; + }; - /** Sets up resize observer for the workspace container. */ - const setupResizeObserver = (): (() => void) | undefined => { - const div = blocklyDiv.current; - if (!div) { - return undefined; + /** Gets the current Blockly workspace instance. */ + const getBlocklyWorkspace = (): Blockly.WorkspaceSvg => { + if (!workspaceRef.current) { + throw new Error('Blockly workspace not initialized'); + } + return workspaceRef.current; + }; + + const setActive = (active: boolean): void => { + if (workspaceRef.current) { + if (!active) { + // Save the scroll position before making this workspace invisible. + if (isScrollPositionValid(workspaceRef.current)) { + savedScrollX.current = workspaceRef.current.scrollX; + savedScrollY.current = workspaceRef.current.scrollY; + } else { + savedScrollX.current = 0; + savedScrollY.current = 0; } + } + workspaceRef.current.setVisible(active); + } + if (parentDiv.current) { + parentDiv.current.hidden = !active; + } + if (workspaceRef.current) { + if (active) { + workspaceRef.current.markFocused(); - const resizeObserver = new ResizeObserver(handleWorkspaceResize); - resizeObserver.observe(div); - - return () => { - resizeObserver.unobserve(div); - }; - }; - - /** Gets the current Blockly workspace instance. */ - const getBlocklyWorkspace = (): Blockly.WorkspaceSvg => { - if (!workspaceRef.current) { - throw new Error('Blockly workspace not initialized'); + const needScroll = !isScrollPositionValid(workspaceRef.current); + if (Blockly.getMainWorkspace().id === workspaceRef.current.id) { + Blockly.svgResize(workspaceRef.current); + if (needScroll) { + workspaceRef.current.scroll(savedScrollX.current, savedScrollY.current); + } } - return workspaceRef.current; + } + } + }; + + const isScrollPositionValid = (workspace: Blockly.WorkspaceSvg): boolean => { + return !( + Math.round(workspace.getMetrics().svgWidth) === 0 && + Math.round(workspace.getMetrics().svgHeight) === 0 && + Math.round(workspace.scrollX) === -10 && + Math.round(workspace.scrollY) === -10); + }; + + // Initialize Blockly workspace + React.useEffect(() => { + if (props.onBlocklyComponentCreated) { + const blocklyComponent: BlocklyComponentType = { + getBlocklyWorkspace, + setActive, }; - - // Initialize Blockly workspace - React.useEffect(() => { - initializeWorkspace(); - return cleanupWorkspace; - }, []); - - // Update theme when theme prop changes - React.useEffect(() => { - if (workspaceRef.current) { - const newTheme = getBlocklyTheme(); - workspaceRef.current.setTheme(newTheme); - } - }, [props.theme]); - - React.useEffect(() => { - updateBlocklyLocale(); - }, [i18n.language]); - - // Handle workspace resize - React.useEffect(() => { - return setupResizeObserver(); - }, []); - - // Expose methods through ref - React.useImperativeHandle( - ref, - (): BlocklyComponentType => ({ - getBlocklyWorkspace, - }), - [] - ); - - return ( -
-
-
- ); + props.onBlocklyComponentCreated(props.modulePath, blocklyComponent); } -); - -BlocklyComponent.displayName = 'BlocklyComponent'; - -export default BlocklyComponent; + initializeWorkspace(); + return cleanupWorkspace; + }, []); + + // Update theme when theme prop changes + React.useEffect(() => { + if (workspaceRef.current) { + const newTheme = getBlocklyTheme(); + workspaceRef.current.setTheme(newTheme); + } + }, [props.theme]); + + React.useEffect(() => { + updateBlocklyLocale(); + }, [i18n.language]); + + // Handle workspace resize + React.useEffect(() => { + return setupResizeObserver(); + }, []); + + return ( +
+
+
+ ); +} diff --git a/src/storage/create_python_files.ts b/src/storage/create_python_files.ts index 300a00be..8f591aa5 100644 --- a/src/storage/create_python_files.ts +++ b/src/storage/create_python_files.ts @@ -29,7 +29,6 @@ import { parseModuleContentText } from './module_content'; import { Project } from './project'; import { pascalCaseToSnakeCase } from './names'; import JSZip from 'jszip'; -import { GeneratorContext } from '../editor/generator_context'; /** Result of Python code generation for a single module */ interface ModulePythonResult { @@ -66,18 +65,10 @@ async function generatePythonForModule(module: Module, storage: Storage): Promis // Parse and load the JSON into the workspace const blocks = moduleContent.getBlocks(); - Blockly.serialization.workspaces.load(blocks, workspace); - // Create and set up generator context like the editor does - const generatorContext = new GeneratorContext(); - generatorContext.setModule(module); - - // Initialize the generator - extendedPythonGenerator.init(workspace); - - // Generate Python code using the same method as the editor - const pythonCode = extendedPythonGenerator.mrcWorkspaceToCode(workspace, generatorContext); + // Generate Python code. + const pythonCode = extendedPythonGenerator.mrcWorkspaceToCode(workspace, module); // Clean up the workspace workspace.dispose(); diff --git a/src/toolbox/hardware_category.ts b/src/toolbox/hardware_category.ts index d226b64b..ad02e725 100644 --- a/src/toolbox/hardware_category.ts +++ b/src/toolbox/hardware_category.ts @@ -33,7 +33,7 @@ import { import { Editor } from '../editor/editor'; export function getHardwareCategory(editor: Editor): toolboxItems.Category { - const moduleType = editor.getCurrentModuleType(); + const moduleType = editor.getModuleType(); switch (moduleType) { case storageModule.ModuleType.ROBOT: return new toolboxItems.Category( @@ -69,7 +69,7 @@ function getRobotMechanismsCategory(editor: Editor): toolboxItems.Category { const contents: toolboxItems.ContentsType[] = []; // Include the "+ Mechanism" category if the user it editing the robot and there are any mechanism modules. - if (editor.getCurrentModuleType() === storageModule.ModuleType.ROBOT) { + if (editor.getModuleType() === storageModule.ModuleType.ROBOT) { const mechanisms = editor.getMechanisms(); if (mechanisms.length) { const mechanismBlocks: toolboxItems.Block[] = []; diff --git a/src/toolbox/methods_category.ts b/src/toolbox/methods_category.ts index 0c030d25..fe978289 100644 --- a/src/toolbox/methods_category.ts +++ b/src/toolbox/methods_category.ts @@ -68,7 +68,7 @@ class MethodsCategory { // Collect the method names that are already overridden in the blockly workspace. const methodNamesAlreadyOverridden = editor.getMethodNamesAlreadyOverriddenInWorkspace(); - switch (editor.getCurrentModuleType()) { + switch (editor.getModuleType()) { case storageModule.ModuleType.ROBOT: // TODO(lizlooney): We need a way to mark a method in python as not overridable. // For example, in RobotBase, define_hardware, register_event_handler, diff --git a/src/toolbox/toolbox.ts b/src/toolbox/toolbox.ts index fbedc431..998d6bfa 100644 --- a/src/toolbox/toolbox.ts +++ b/src/toolbox/toolbox.ts @@ -15,7 +15,7 @@ export function getToolboxJSON( contents: [] }; - switch (editor.getCurrentModuleType()) { + switch (editor.getModuleType()) { case storageModule.ModuleType.ROBOT: case storageModule.ModuleType.MECHANISM: toolbox.contents.push(getHardwareCategory(editor));