Skip to content

Commit 6a1a2f5

Browse files
authored
Use a separate BlocklyComponent for each module (#224)
* In editor: Renamed field currentModule to module. Renamed field currentProject to project. Renamed method getCurrentModuleType to getModuleType. Updated places where getCurrentModuleType was called. * Changed editor.getEditorForBlocklyWorkspace to return null if the workspace is not known. Updated callers to explicitly call editor.getCurrentEditor only if appropriate. * Set App's blocklyComponent in a callback named onBlocklyComponentCreated rather than via ref=. * Changed toolbox update event to include the workspace id in the event details. * Create generatorContext earlier, before blockly workspaces are setup. * Modified App to fetch the contents of all the modules in the current project from storage and put them in modulePathToContentText object. App passes modulePathToContentText to editor during loadModuleBlocks. When a module is saved, App updates modulePathToContentText. Modified editor to use modulePathToContentText instead of fetching module content from storage. Renamed saveBlocks to saveModule. * In App.tsx: Replaced blocksEditor and blocklyComponent with modulePaths, modulePathToBocklyComponent and modulePathToEditor. modulePaths controls how BlocklyComponents are created. Changed code that used blocksEditor and blocklyComponent. Added activeEditor function that is called when currentModule changes. Added setupBlocklyComponent, a callback that is a called when a new BlocklyComponent is created. In BlocklyComponent.tsx: Renamed onWorkspaceRecreated to onWorkspaceCreated Added modulePath to BlocklyComponentProps. Pass modulePath to onBlocklyComponentCreated and onWorkspaceCreated callbacks. Added fields for parentDiv and savedScrollX/Y. Only call Blockly.svgResize if the workspace is visible and is the main workspace. Added setActive function and include it in BlocklyComponentType. In setActive, handle either the BlocklyComponenent becoming inactive or active by hiding or showing the blockly workspace. Update scroll position if necessary when going from inactive to active. In editor.ts: Make many fields readonly since an editor is no longer reused for different modules. Check whether the blockly workspace has been abandoned in many places. * Fixed indentation. * Create GeneratorContext once, in the ExtendedPythonGenerator constructor. Change mrcWorkspaceToCode to take a Module instead of GeneratorContext. Change mrcWorkspaceToCode to call context.setModule and this.init(workspace). * Added optional parameter returnCurrentIfNotFound to getEditorForBlocklyWorkspace.
1 parent 681a6ee commit 6a1a2f5

13 files changed

+598
-441
lines changed

src/App.tsx

Lines changed: 163 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ import ToolboxSettingsModal from './reactComponents/ToolboxSettings';
3333
import * as Tabs from './reactComponents/Tabs';
3434
import { TabType } from './types/TabType';
3535

36-
import { createGeneratorContext, GeneratorContext } from './editor/generator_context';
3736
import * as editor from './editor/editor';
3837
import { extendedPythonGenerator } from './editor/extended_python_generator';
3938

@@ -161,6 +160,7 @@ const AppContent: React.FC<AppContentProps> = ({ project, setProject }): React.J
161160
const [messageApi, contextHolder] = Antd.message.useMessage();
162161
const [generatedCode, setGeneratedCode] = React.useState<string>('');
163162
const [toolboxSettingsModalIsOpen, setToolboxSettingsModalIsOpen] = React.useState(false);
163+
const [modulePathToContentText, setModulePathToContentText] = React.useState<{[modulePath: string]: string}>({});
164164
const [tabItems, setTabItems] = React.useState<Tabs.TabItem[]>([]);
165165
const [activeTab, setActiveTab] = React.useState('');
166166
const [shownPythonToolboxCategories, setShownPythonToolboxCategories] = React.useState<Set<string>>(new Set());
@@ -171,9 +171,10 @@ const AppContent: React.FC<AppContentProps> = ({ project, setProject }): React.J
171171
const [languageInitialized, setLanguageInitialized] = React.useState(false);
172172
const [themeInitialized, setThemeInitialized] = React.useState(false);
173173

174-
const blocksEditor = React.useRef<editor.Editor | null>(null);
175-
const generatorContext = React.useRef<GeneratorContext | null>(null);
176-
const blocklyComponent = React.useRef<BlocklyComponentType | null>(null);
174+
/** modulePaths controls how BlocklyComponents are created. */
175+
const modulePaths = React.useRef<string[]>([]);
176+
const modulePathToBlocklyComponent = React.useRef<{[modulePath: string]: BlocklyComponentType}>({});
177+
const modulePathToEditor = React.useRef<{[modulePath: string]: editor.Editor}>({});
177178

178179
/** Initialize language from UserSettings when app first starts. */
179180
React.useEffect(() => {
@@ -207,7 +208,7 @@ const AppContent: React.FC<AppContentProps> = ({ project, setProject }): React.J
207208
// Save current blocks before language change
208209
if (currentModule && areBlocksModified()) {
209210
try {
210-
await saveBlocks();
211+
await saveModule();
211212
} catch (e) {
212213
console.error('Failed to save blocks before language change:', e);
213214
}
@@ -222,9 +223,10 @@ const AppContent: React.FC<AppContentProps> = ({ project, setProject }): React.J
222223
}
223224
}
224225

225-
// Update toolbox after language change
226-
if (blocksEditor.current) {
227-
blocksEditor.current.updateToolbox(shownPythonToolboxCategories);
226+
// Update toolbox in all editors after language change.
227+
for (const modulePath in modulePathToEditor.current) {
228+
const editor = modulePathToEditor.current[modulePath];
229+
editor.updateToolbox(shownPythonToolboxCategories);
228230
}
229231
};
230232

@@ -298,19 +300,32 @@ const AppContent: React.FC<AppContentProps> = ({ project, setProject }): React.J
298300
return;
299301
}
300302

303+
// Check whether this blockly workspace is for the current module.
304+
if (!currentModule ||
305+
!(currentModule.modulePath in modulePathToBlocklyComponent.current)) {
306+
return;
307+
}
308+
const blocklyComponent = modulePathToBlocklyComponent.current[currentModule.modulePath];
309+
if (event.workspaceId != blocklyComponent.getBlocklyWorkspace().id) {
310+
return;
311+
}
312+
301313
setTriggerPythonRegeneration(Date.now());
302314
};
303315

304316
/** Saves blocks to storage with success/error messaging. */
305-
const saveBlocks = async (): Promise<boolean> => {
317+
const saveModule = async (): Promise<boolean> => {
306318
return new Promise(async (resolve, reject) => {
307-
if (!blocksEditor.current) {
319+
if (!currentModule ||
320+
!(currentModule.modulePath in modulePathToEditor.current)) {
308321
reject(new Error('Blocks editor not initialized'));
309322
return;
310323
}
324+
const editor = modulePathToEditor.current[currentModule.modulePath];
311325

312326
try {
313-
await blocksEditor.current.saveBlocks();
327+
const moduleContentText = await editor.saveModule();
328+
modulePathToContentText[currentModule.modulePath] = moduleContentText;
314329
messageApi.open({
315330
type: 'success',
316331
content: SAVE_SUCCESS_MESSAGE,
@@ -339,13 +354,18 @@ const AppContent: React.FC<AppContentProps> = ({ project, setProject }): React.J
339354

340355
/** Checks if blocks have been modified. */
341356
const areBlocksModified = (): boolean => {
342-
return blocksEditor.current ? blocksEditor.current.isModified() : false;
357+
if (currentModule &&
358+
currentModule.modulePath in modulePathToEditor.current) {
359+
const editor = modulePathToEditor.current[currentModule.modulePath];
360+
return editor.isModified();
361+
}
362+
return false;
343363
};
344364

345365
/** Changes current module with automatic saving if modified. */
346366
const changeModule = async (module: storageModule.Module | null): Promise<void> => {
347367
if (currentModule && areBlocksModified()) {
348-
await saveBlocks();
368+
await saveModule();
349369
}
350370
setCurrentModule(module);
351371
};
@@ -391,11 +411,13 @@ const AppContent: React.FC<AppContentProps> = ({ project, setProject }): React.J
391411
};
392412

393413
/** Handles toolbox update requests from blocks */
394-
const handleToolboxUpdateRequest = React.useCallback(() => {
395-
if (blocksEditor.current && currentModule) {
396-
blocksEditor.current.updateToolbox(shownPythonToolboxCategories);
414+
const handleToolboxUpdateRequest = React.useCallback((e: Event) => {
415+
const workspaceId = (e as CustomEvent).detail.workspaceId;
416+
const correspondingEditor = editor.Editor.getEditorForBlocklyWorkspaceId(workspaceId);
417+
if (correspondingEditor) {
418+
correspondingEditor.updateToolbox(shownPythonToolboxCategories);
397419
}
398-
}, [currentModule, shownPythonToolboxCategories, i18n.language]);
420+
}, [shownPythonToolboxCategories, i18n.language]);
399421

400422
// Add event listener for toolbox updates
401423
React.useEffect(() => {
@@ -417,85 +439,153 @@ const AppContent: React.FC<AppContentProps> = ({ project, setProject }): React.J
417439

418440
// Update generator context and load module blocks when current module changes
419441
React.useEffect(() => {
420-
if (generatorContext.current) {
421-
generatorContext.current.setModule(currentModule);
422-
}
423-
if (blocksEditor.current) {
424-
blocksEditor.current.loadModuleBlocks(currentModule, project);
442+
if (currentModule) {
443+
if (modulePaths.current.includes(currentModule.modulePath)) {
444+
activateEditor();
445+
} else {
446+
// Add the module path to modulePaths to create a new BlocklyComponent.
447+
modulePaths.current.push(currentModule.modulePath);
448+
}
425449
}
426450
}, [currentModule]);
427451

428-
const setupWorkspace = (newWorkspace: Blockly.WorkspaceSvg) => {
429-
if (!blocklyComponent.current || !storage) {
452+
const activateEditor = () => {
453+
if (!project || !currentModule) {
454+
return;
455+
}
456+
for (const modulePath in modulePathToBlocklyComponent.current) {
457+
const blocklyComponent = modulePathToBlocklyComponent.current[modulePath];
458+
const active = (modulePath === currentModule.modulePath);
459+
const workspaceIsVisible = blocklyComponent.getBlocklyWorkspace()!.isVisible();
460+
if (active != workspaceIsVisible) {
461+
blocklyComponent.setActive(active);
462+
}
463+
}
464+
if (currentModule.modulePath in modulePathToEditor.current) {
465+
const editor = modulePathToEditor.current[currentModule.modulePath];
466+
editor.makeCurrent(project, modulePathToContentText);
467+
}
468+
};
469+
470+
const setupBlocklyComponent = (modulePath: string, newBlocklyComponent: BlocklyComponentType) => {
471+
modulePathToBlocklyComponent.current[modulePath] = newBlocklyComponent;
472+
if (currentModule) {
473+
newBlocklyComponent.setActive(modulePath === currentModule.modulePath);
474+
}
475+
};
476+
477+
const setupWorkspace = (modulePath: string, newWorkspace: Blockly.WorkspaceSvg) => {
478+
if (!project || !storage) {
479+
return;
480+
}
481+
const module = storageProject.findModuleByModulePath(project, modulePath);
482+
if (!module) {
483+
console.error("setupWorkspace called for unknown module path " + modulePath);
430484
return;
431485
}
432-
// Recreate workspace when Blockly component is ready
486+
433487
ChangeFramework.setup(newWorkspace);
434488
newWorkspace.addChangeListener(mutatorOpenListener);
435489
newWorkspace.addChangeListener(handleBlocksChanged);
436490

437491
registerToolboxButton(newWorkspace, messageApi);
438492

439-
generatorContext.current = createGeneratorContext();
440-
441-
if (currentModule) {
442-
generatorContext.current.setModule(currentModule);
493+
const oldEditor = modulePathToEditor.current[modulePath];
494+
if (oldEditor) {
495+
oldEditor.abandon();
443496
}
444497

445-
if (blocksEditor.current) {
446-
blocksEditor.current.abandon();
447-
}
448-
blocksEditor.current = new editor.Editor(newWorkspace, generatorContext.current, storage);
449-
blocksEditor.current.makeCurrent();
498+
const newEditor = new editor.Editor(
499+
newWorkspace, module, project, storage, modulePathToContentText);
500+
modulePathToEditor.current[modulePath] = newEditor;
501+
newEditor.loadModuleBlocks();
502+
newEditor.updateToolbox(shownPythonToolboxCategories);
450503

451-
// Set the current module in the editor after creating it
452-
if (currentModule) {
453-
blocksEditor.current.loadModuleBlocks(currentModule, project);
504+
if (currentModule && currentModule.modulePath === modulePath) {
505+
activateEditor();
454506
}
455-
456-
blocksEditor.current.updateToolbox(shownPythonToolboxCategories);
457507
};
458508

459-
// Initialize Blockly workspace and editor when component and storage are ready
509+
// Generate code when module or regeneration trigger changes
460510
React.useEffect(() => {
461-
if (!blocklyComponent.current || !storage) {
462-
return;
463-
}
464-
465-
const blocklyWorkspace = blocklyComponent.current.getBlocklyWorkspace();
466-
if (blocklyWorkspace) {
467-
setupWorkspace(blocklyWorkspace);
511+
let generatedCode = '';
512+
if (currentModule) {
513+
if (currentModule.modulePath in modulePathToBlocklyComponent.current) {
514+
const blocklyComponent = modulePathToBlocklyComponent.current[currentModule.modulePath];
515+
generatedCode = extendedPythonGenerator.mrcWorkspaceToCode(
516+
blocklyComponent.getBlocklyWorkspace(), currentModule);
517+
}
468518
}
469-
}, [blocklyComponent, storage]);
519+
setGeneratedCode(generatedCode);
520+
}, [currentModule, project, triggerPythonRegeneration]);
470521

471-
// Generate code when module or regeneration trigger changes
522+
// Update toolbox when categories change
472523
React.useEffect(() => {
473-
if (currentModule && blocklyComponent.current && generatorContext.current) {
474-
const blocklyWorkspace = blocklyComponent.current.getBlocklyWorkspace();
475-
setGeneratedCode(extendedPythonGenerator.mrcWorkspaceToCode(
476-
blocklyWorkspace,
477-
generatorContext.current
478-
));
479-
} else {
480-
setGeneratedCode('');
524+
if (currentModule) {
525+
if (currentModule.modulePath in modulePathToEditor.current) {
526+
const editor = modulePathToEditor.current[currentModule.modulePath];
527+
editor.updateToolbox(shownPythonToolboxCategories);
528+
}
481529
}
482-
}, [currentModule, project, triggerPythonRegeneration, blocklyComponent]);
530+
}, [shownPythonToolboxCategories]);
483531

484-
// Update toolbox when module or categories change
532+
// Fetch modules when project changes.
485533
React.useEffect(() => {
486-
if (blocksEditor.current) {
487-
blocksEditor.current.updateToolbox(shownPythonToolboxCategories);
534+
if (project && storage) {
535+
const fetchModules = async () => {
536+
const promises: {[modulePath: string]: Promise<string>} = {}; // value is promise of module content.
537+
promises[project.robot.modulePath] = storage.fetchFileContentText(project.robot.modulePath);
538+
project.mechanisms.forEach(mechanism => {
539+
promises[mechanism.modulePath] = storage.fetchFileContentText(mechanism.modulePath);
540+
});
541+
project.opModes.forEach(opmode => {
542+
promises[opmode.modulePath] = storage.fetchFileContentText(opmode.modulePath);
543+
});
544+
const updatedModulePathToContentText: {[modulePath: string]: string} = {}; // value is module content text
545+
await Promise.all(
546+
Object.entries(promises).map(async ([modulePath, promise]) => {
547+
updatedModulePathToContentText[modulePath] = await promise;
548+
})
549+
);
550+
const oldModulePathToContentText = modulePathToContentText;
551+
setModulePathToContentText(updatedModulePathToContentText);
552+
553+
// Remove any deleted modules from modulePaths, modulePathToBlocklyComponent, and
554+
// modulePathToEditor. Update currentModule if the current module was deleted.
555+
for (const modulePath in oldModulePathToContentText) {
556+
if (modulePath in updatedModulePathToContentText) {
557+
continue;
558+
}
559+
if (currentModule && currentModule.modulePath === modulePath) {
560+
setCurrentModule(project.robot);
561+
setActiveTab(project.robot.modulePath);
562+
}
563+
const indexToRemove: number = modulePaths.current.indexOf(modulePath);
564+
if (indexToRemove !== -1) {
565+
modulePaths.current.splice(indexToRemove, 1);
566+
}
567+
if (modulePath in modulePathToBlocklyComponent.current) {
568+
delete modulePathToBlocklyComponent.current[modulePath];
569+
}
570+
if (modulePath in modulePathToEditor.current) {
571+
const editor = modulePathToEditor.current[modulePath];
572+
editor.abandon();
573+
delete modulePathToEditor.current[modulePath];
574+
}
575+
}
576+
};
577+
fetchModules();
488578
}
489-
}, [currentModule, shownPythonToolboxCategories]);
579+
}, [project]);
490580

491-
// Update tab items when project changes
581+
// Update tab items when fetching modules is done.
492582
React.useEffect(() => {
493583
if (project) {
494584
const tabs = createTabItemsFromProject(project);
495585
setTabItems(tabs);
496586
setActiveTab(project.robot.modulePath);
497587
}
498-
}, [project]);
588+
}, [modulePathToContentText]);
499589

500590
const { Sider, Content } = Antd.Layout;
501591

@@ -546,11 +636,15 @@ const AppContent: React.FC<AppContentProps> = ({ project, setProject }): React.J
546636
/>
547637
<Antd.Layout>
548638
<Content>
549-
<BlocklyComponent
550-
theme={theme}
551-
onWorkspaceRecreated={setupWorkspace}
552-
ref={blocklyComponent}
553-
/>
639+
{modulePaths.current.map((modulePath) => (
640+
<BlocklyComponent
641+
key={modulePath}
642+
modulePath={modulePath}
643+
onBlocklyComponentCreated={setupBlocklyComponent}
644+
theme={theme}
645+
onWorkspaceCreated={setupWorkspace}
646+
/>
647+
))}
554648
</Content>
555649
<Sider
556650
collapsible

src/blocks/mrc_call_python_function.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -531,7 +531,7 @@ const CALL_PYTHON_FUNCTION = {
531531
getComponents: function(this: CallPythonFunctionBlock): storageModuleContent.Component[] {
532532
// Get the list of components whose type matches this.mrcComponentClassName.
533533
const components: storageModuleContent.Component[] = [];
534-
const editor = Editor.getEditorForBlocklyWorkspace(this.workspace);
534+
const editor = Editor.getEditorForBlocklyWorkspace(this.workspace, true /* returnCurrentIfNotFound */);
535535
if (editor) {
536536
let componentsToConsider: storageModuleContent.Component[] = [];
537537
if (this.mrcMechanismId) {
@@ -550,7 +550,7 @@ const CALL_PYTHON_FUNCTION = {
550550
break;
551551
}
552552
}
553-
} else if (editor.getCurrentModuleType() === storageModule.ModuleType.MECHANISM) {
553+
} else if (editor.getModuleType() === storageModule.ModuleType.MECHANISM) {
554554
// Only consider components (regular and private) in the current workspace.
555555
componentsToConsider = editor.getAllComponentsFromWorkspace();
556556
} else {
@@ -567,7 +567,7 @@ const CALL_PYTHON_FUNCTION = {
567567
},
568568
mrcOnLoad: function(this: CallPythonFunctionBlock): void {
569569
// mrcOnLoad is called for each CallPythonFunctionBlock when the blocks are loaded in the blockly workspace.
570-
const editor = Editor.getEditorForBlocklyWorkspace(this.workspace);
570+
const editor = Editor.getEditorForBlocklyWorkspace(this.workspace, true /* returnCurrentIfNotFound */);
571571
if (!editor) {
572572
return;
573573
}
@@ -654,7 +654,7 @@ const CALL_PYTHON_FUNCTION = {
654654
// If the robot method has changed, update the block if possible or put a
655655
// visible warning on it.
656656
if (this.mrcFunctionKind === FunctionKind.INSTANCE_ROBOT) {
657-
if (editor.getCurrentModuleType() === storageModule.ModuleType.MECHANISM) {
657+
if (editor.getModuleType() === storageModule.ModuleType.MECHANISM) {
658658
warnings.push('This block is not allowed to be used inside a mechanism.');
659659
} else {
660660
let foundRobotMethod = false;
@@ -700,7 +700,7 @@ const CALL_PYTHON_FUNCTION = {
700700
// If the method has changed, update the block if possible or put a
701701
// visible warning on it.
702702
if (this.mrcFunctionKind === FunctionKind.INSTANCE_MECHANISM) {
703-
if (editor.getCurrentModuleType() === storageModule.ModuleType.MECHANISM) {
703+
if (editor.getModuleType() === storageModule.ModuleType.MECHANISM) {
704704
warnings.push('This block is not allowed to be used inside a mechanism.');
705705
} else {
706706
let foundMechanism = false;

0 commit comments

Comments
 (0)