@@ -33,7 +33,6 @@ import ToolboxSettingsModal from './reactComponents/ToolboxSettings';
33
33
import * as Tabs from './reactComponents/Tabs' ;
34
34
import { TabType } from './types/TabType' ;
35
35
36
- import { createGeneratorContext , GeneratorContext } from './editor/generator_context' ;
37
36
import * as editor from './editor/editor' ;
38
37
import { extendedPythonGenerator } from './editor/extended_python_generator' ;
39
38
@@ -161,6 +160,7 @@ const AppContent: React.FC<AppContentProps> = ({ project, setProject }): React.J
161
160
const [ messageApi , contextHolder ] = Antd . message . useMessage ( ) ;
162
161
const [ generatedCode , setGeneratedCode ] = React . useState < string > ( '' ) ;
163
162
const [ toolboxSettingsModalIsOpen , setToolboxSettingsModalIsOpen ] = React . useState ( false ) ;
163
+ const [ modulePathToContentText , setModulePathToContentText ] = React . useState < { [ modulePath : string ] : string } > ( { } ) ;
164
164
const [ tabItems , setTabItems ] = React . useState < Tabs . TabItem [ ] > ( [ ] ) ;
165
165
const [ activeTab , setActiveTab ] = React . useState ( '' ) ;
166
166
const [ shownPythonToolboxCategories , setShownPythonToolboxCategories ] = React . useState < Set < string > > ( new Set ( ) ) ;
@@ -171,9 +171,10 @@ const AppContent: React.FC<AppContentProps> = ({ project, setProject }): React.J
171
171
const [ languageInitialized , setLanguageInitialized ] = React . useState ( false ) ;
172
172
const [ themeInitialized , setThemeInitialized ] = React . useState ( false ) ;
173
173
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 } > ( { } ) ;
177
178
178
179
/** Initialize language from UserSettings when app first starts. */
179
180
React . useEffect ( ( ) => {
@@ -207,7 +208,7 @@ const AppContent: React.FC<AppContentProps> = ({ project, setProject }): React.J
207
208
// Save current blocks before language change
208
209
if ( currentModule && areBlocksModified ( ) ) {
209
210
try {
210
- await saveBlocks ( ) ;
211
+ await saveModule ( ) ;
211
212
} catch ( e ) {
212
213
console . error ( 'Failed to save blocks before language change:' , e ) ;
213
214
}
@@ -222,9 +223,10 @@ const AppContent: React.FC<AppContentProps> = ({ project, setProject }): React.J
222
223
}
223
224
}
224
225
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 ) ;
228
230
}
229
231
} ;
230
232
@@ -298,19 +300,32 @@ const AppContent: React.FC<AppContentProps> = ({ project, setProject }): React.J
298
300
return ;
299
301
}
300
302
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
+
301
313
setTriggerPythonRegeneration ( Date . now ( ) ) ;
302
314
} ;
303
315
304
316
/** Saves blocks to storage with success/error messaging. */
305
- const saveBlocks = async ( ) : Promise < boolean > => {
317
+ const saveModule = async ( ) : Promise < boolean > => {
306
318
return new Promise ( async ( resolve , reject ) => {
307
- if ( ! blocksEditor . current ) {
319
+ if ( ! currentModule ||
320
+ ! ( currentModule . modulePath in modulePathToEditor . current ) ) {
308
321
reject ( new Error ( 'Blocks editor not initialized' ) ) ;
309
322
return ;
310
323
}
324
+ const editor = modulePathToEditor . current [ currentModule . modulePath ] ;
311
325
312
326
try {
313
- await blocksEditor . current . saveBlocks ( ) ;
327
+ const moduleContentText = await editor . saveModule ( ) ;
328
+ modulePathToContentText [ currentModule . modulePath ] = moduleContentText ;
314
329
messageApi . open ( {
315
330
type : 'success' ,
316
331
content : SAVE_SUCCESS_MESSAGE ,
@@ -339,13 +354,18 @@ const AppContent: React.FC<AppContentProps> = ({ project, setProject }): React.J
339
354
340
355
/** Checks if blocks have been modified. */
341
356
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 ;
343
363
} ;
344
364
345
365
/** Changes current module with automatic saving if modified. */
346
366
const changeModule = async ( module : storageModule . Module | null ) : Promise < void > => {
347
367
if ( currentModule && areBlocksModified ( ) ) {
348
- await saveBlocks ( ) ;
368
+ await saveModule ( ) ;
349
369
}
350
370
setCurrentModule ( module ) ;
351
371
} ;
@@ -391,11 +411,13 @@ const AppContent: React.FC<AppContentProps> = ({ project, setProject }): React.J
391
411
} ;
392
412
393
413
/** 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 ) ;
397
419
}
398
- } , [ currentModule , shownPythonToolboxCategories , i18n . language ] ) ;
420
+ } , [ shownPythonToolboxCategories , i18n . language ] ) ;
399
421
400
422
// Add event listener for toolbox updates
401
423
React . useEffect ( ( ) => {
@@ -417,85 +439,153 @@ const AppContent: React.FC<AppContentProps> = ({ project, setProject }): React.J
417
439
418
440
// Update generator context and load module blocks when current module changes
419
441
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
+ }
425
449
}
426
450
} , [ currentModule ] ) ;
427
451
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 ) ;
430
484
return ;
431
485
}
432
- // Recreate workspace when Blockly component is ready
486
+
433
487
ChangeFramework . setup ( newWorkspace ) ;
434
488
newWorkspace . addChangeListener ( mutatorOpenListener ) ;
435
489
newWorkspace . addChangeListener ( handleBlocksChanged ) ;
436
490
437
491
registerToolboxButton ( newWorkspace , messageApi ) ;
438
492
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 ( ) ;
443
496
}
444
497
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 ) ;
450
503
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 ( ) ;
454
506
}
455
-
456
- blocksEditor . current . updateToolbox ( shownPythonToolboxCategories ) ;
457
507
} ;
458
508
459
- // Initialize Blockly workspace and editor when component and storage are ready
509
+ // Generate code when module or regeneration trigger changes
460
510
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
+ }
468
518
}
469
- } , [ blocklyComponent , storage ] ) ;
519
+ setGeneratedCode ( generatedCode ) ;
520
+ } , [ currentModule , project , triggerPythonRegeneration ] ) ;
470
521
471
- // Generate code when module or regeneration trigger changes
522
+ // Update toolbox when categories change
472
523
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
+ }
481
529
}
482
- } , [ currentModule , project , triggerPythonRegeneration , blocklyComponent ] ) ;
530
+ } , [ shownPythonToolboxCategories ] ) ;
483
531
484
- // Update toolbox when module or categories change
532
+ // Fetch modules when project changes.
485
533
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 ( ) ;
488
578
}
489
- } , [ currentModule , shownPythonToolboxCategories ] ) ;
579
+ } , [ project ] ) ;
490
580
491
- // Update tab items when project changes
581
+ // Update tab items when fetching modules is done.
492
582
React . useEffect ( ( ) => {
493
583
if ( project ) {
494
584
const tabs = createTabItemsFromProject ( project ) ;
495
585
setTabItems ( tabs ) ;
496
586
setActiveTab ( project . robot . modulePath ) ;
497
587
}
498
- } , [ project ] ) ;
588
+ } , [ modulePathToContentText ] ) ;
499
589
500
590
const { Sider, Content } = Antd . Layout ;
501
591
@@ -546,11 +636,15 @@ const AppContent: React.FC<AppContentProps> = ({ project, setProject }): React.J
546
636
/>
547
637
< Antd . Layout >
548
638
< 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
+ ) ) }
554
648
</ Content >
555
649
< Sider
556
650
collapsible
0 commit comments