diff --git a/src/blocks/mrc_class_method_def.ts b/src/blocks/mrc_class_method_def.ts index 006946dd..b63dbc9a 100644 --- a/src/blocks/mrc_class_method_def.ts +++ b/src/blocks/mrc_class_method_def.ts @@ -35,6 +35,7 @@ import * as toolboxItems from '../toolbox/items'; import { getClassData } from './utils/python'; import { FunctionData } from './utils/python_json_types'; import { findConnectedBlocksOfType } from './utils/find_connected_blocks'; +import { NONCOPYABLE_BLOCK } from './noncopyable_block'; import { BLOCK_NAME as MRC_GET_PARAMETER_BLOCK_NAME } from './mrc_get_parameter'; import * as paramContainer from './mrc_param_container' @@ -122,6 +123,7 @@ const CLASS_METHOD_DEF = { this.setNextStatement(false); this.updateBlock_(); }, + ...NONCOPYABLE_BLOCK, /** * Returns the state of this block as a JSON serializable object. */ diff --git a/src/blocks/mrc_component.ts b/src/blocks/mrc_component.ts index e0a2a937..e9a0f75b 100644 --- a/src/blocks/mrc_component.ts +++ b/src/blocks/mrc_component.ts @@ -1,7 +1,7 @@ /** * @license * Copyright 2025 Porpoiseful LLC - * + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -31,6 +31,7 @@ import { getAllowedTypesForSetCheck, getClassData, getSubclassNames } from './ut import * as toolboxItems from '../toolbox/items'; import * as storageModule from '../storage/module'; import * as storageModuleContent from '../storage/module_content'; +import { NONCOPYABLE_BLOCK } from './noncopyable_block'; import { BLOCK_NAME as MRC_MECHANISM_COMPONENT_HOLDER, MechanismComponentHolderBlock, @@ -100,6 +101,7 @@ const COMPONENT = { this.setPreviousStatement(true, OUTPUT_NAME); this.setNextStatement(true, OUTPUT_NAME); }, + ...NONCOPYABLE_BLOCK, /** * Returns the state of this block as a JSON serializable object. @@ -192,7 +194,7 @@ const COMPONENT = { // Collect the ports for this component block. for (let i = 0; i < this.mrcArgs.length; i++) { const argName = this.getArgName(i); - ports[argName] = this.mrcArgs[i].name; + ports[argName] = this.mrcArgs[i].name; } }, /** @@ -247,6 +249,13 @@ const COMPONENT = { this.mrcComponentId = oldIdToNewId[this.mrcComponentId]; } }, + upgrade_005_to_006: function(this: ComponentBlock) { + for (let i = 0; i < this.mrcArgs.length; i++) { + if (this.mrcArgs[i].type === 'Port') { + this.mrcArgs[i].type = this.mrcArgs[i].name; + } + } + }, }; export const setup = function () { @@ -318,11 +327,21 @@ function createComponentBlock( if (constructorData.expectedPortType) { extraState.params!.push({ name: constructorData.expectedPortType, - type: 'Port', + type: constructorData.expectedPortType, }); - if ( moduleType == storageModule.ModuleType.ROBOT ) { + if (moduleType == storageModule.ModuleType.ROBOT ) { inputs['ARG0'] = createPort(constructorData.expectedPortType); } } return new toolboxItems.Block(BLOCK_NAME, extraState, fields, Object.keys(inputs).length ? inputs : null); } + +/** + * Upgrades the ComponentBlocks in the given workspace from version 005 to 006. + * This function should only be called when upgrading old projects. + */ +export function upgrade_005_to_006(workspace: Blockly.Workspace): void { + workspace.getBlocksByType(BLOCK_NAME).forEach(block => { + (block as ComponentBlock).upgrade_005_to_006(); + }); +} diff --git a/src/blocks/mrc_event.ts b/src/blocks/mrc_event.ts index db5f2942..070d7761 100644 --- a/src/blocks/mrc_event.ts +++ b/src/blocks/mrc_event.ts @@ -26,6 +26,7 @@ import { createFieldNonEditableText } from '../fields/FieldNonEditableText'; import { Parameter } from './mrc_class_method_def'; import { Editor } from '../editor/editor'; import { ExtendedPythonGenerator } from '../editor/extended_python_generator'; +import { NONCOPYABLE_BLOCK } from './noncopyable_block'; import * as paramContainer from './mrc_param_container' import { BLOCK_NAME as MRC_MECHANISM_COMPONENT_HOLDER, @@ -80,6 +81,7 @@ const EVENT = { this.setNextStatement(true, OUTPUT_NAME); this.updateBlock_(); }, + ...NONCOPYABLE_BLOCK, /** * Returns the state of this block as a JSON serializable object. diff --git a/src/blocks/mrc_mechanism.ts b/src/blocks/mrc_mechanism.ts index 7a754f68..fe42fe38 100644 --- a/src/blocks/mrc_mechanism.ts +++ b/src/blocks/mrc_mechanism.ts @@ -31,6 +31,7 @@ import * as toolboxItems from '../toolbox/items'; import * as storageModule from '../storage/module'; import * as storageModuleContent from '../storage/module_content'; import * as storageNames from '../storage/names'; +import { NONCOPYABLE_BLOCK } from './noncopyable_block'; import { BLOCK_NAME as MRC_MECHANISM_COMPONENT_HOLDER, MechanismComponentHolderBlock, @@ -48,6 +49,8 @@ export const FIELD_TYPE = 'TYPE'; type Parameter = { name: string, type: string, + componentId?: string, + componentPortsIndex?: number, // The zero-based number when iterating through component.ports. }; type MechanismExtraState = { @@ -94,6 +97,7 @@ const MECHANISM = { this.setPreviousStatement(true, OUTPUT_NAME); this.setNextStatement(true, OUTPUT_NAME); }, + ...NONCOPYABLE_BLOCK, /** * Returns the state of this block as a JSON serializable object. @@ -105,10 +109,7 @@ const MECHANISM = { }; extraState.parameters = []; this.mrcParameters.forEach((arg) => { - extraState.parameters!.push({ - name: arg.name, - type: arg.type, - }); + extraState.parameters!.push({...arg}); }); if (this.mrcImportModule) { extraState.importModule = this.mrcImportModule; @@ -125,10 +126,7 @@ const MECHANISM = { this.mrcParameters = []; if (extraState.parameters) { extraState.parameters.forEach((arg) => { - this.mrcParameters.push({ - name: arg.name, - type: arg.type, - }); + this.mrcParameters.push({...arg}); }); } this.updateBlock_(); @@ -289,11 +287,15 @@ const MECHANISM = { } this.mrcParameters = []; components.forEach(component => { + let componentPortsIndex = 0; for (const port in component.ports) { this.mrcParameters.push({ name: port, type: component.ports[port], + componentId: component.componentId, + componentPortsIndex, }); + componentPortsIndex++; } }); this.updateBlock_(); @@ -363,13 +365,17 @@ export function createMechanismBlock( const inputs: {[key: string]: any} = {}; let i = 0; components.forEach(component => { + let componentPortsIndex = 0; for (const port in component.ports) { const parameterType = component.ports[port]; extraState.parameters?.push({ name: port, type: parameterType, + componentId: component.componentId, + componentPortsIndex, }); inputs['ARG' + i] = createPort(parameterType); + componentPortsIndex++; i++; } }); diff --git a/src/blocks/mrc_mechanism_component_holder.ts b/src/blocks/mrc_mechanism_component_holder.ts index 9c7c70b7..e6e25a9b 100644 --- a/src/blocks/mrc_mechanism_component_holder.ts +++ b/src/blocks/mrc_mechanism_component_holder.ts @@ -27,6 +27,7 @@ import { Editor } from '../editor/editor'; import { ExtendedPythonGenerator } from '../editor/extended_python_generator'; import * as storageModule from '../storage/module'; import * as storageModuleContent from '../storage/module_content'; +import { NONCOPYABLE_BLOCK } from './noncopyable_block'; import { BLOCK_NAME as MRC_MECHANISM_NAME } from './mrc_mechanism'; import { OUTPUT_NAME as MECHANISM_OUTPUT } from './mrc_mechanism'; import { MechanismBlock } from './mrc_mechanism'; @@ -79,6 +80,7 @@ const MECHANISM_COMPONENT_HOLDER = { this.mrcEventBlockIds = ''; this.mrcToolboxUpdateTimeout = null; }, + ...NONCOPYABLE_BLOCK, saveExtraState: function (this: MechanismComponentHolderBlock): MechanismComponentHolderExtraState { const extraState: MechanismComponentHolderExtraState = { }; diff --git a/src/blocks/mrc_opmode_details.ts b/src/blocks/mrc_opmode_details.ts index 792e35bd..ab08ff83 100644 --- a/src/blocks/mrc_opmode_details.ts +++ b/src/blocks/mrc_opmode_details.ts @@ -27,6 +27,7 @@ import { Editor } from '../editor/editor'; import { ExtendedPythonGenerator, OpModeDetails } from '../editor/extended_python_generator'; import { createFieldDropdown } from '../fields/FieldDropdown'; import { MRC_STYLE_CLASS_BLOCKS } from '../themes/styles'; +import { NONCOPYABLE_BLOCK } from './noncopyable_block'; export const BLOCK_NAME = 'mrc_opmode_details'; @@ -65,6 +66,7 @@ const OPMODE_DETAILS = { this.getField('NAME')?.setTooltip(Blockly.Msg.OPMODE_NAME_TOOLTIP); this.getField('GROUP')?.setTooltip(Blockly.Msg.OPMODE_GROUP_TOOLTIP); }, + ...NONCOPYABLE_BLOCK, checkOpMode(this: OpmodeDetailsBlock, editor: Editor): void { if (editor.isStepsInWorkspace() || editor.getMethodNamesAlreadyOverriddenInWorkspace().includes(PERIODIC_METHOD_NAME)) { diff --git a/src/blocks/mrc_port.ts b/src/blocks/mrc_port.ts index b32ad248..7f074abe 100644 --- a/src/blocks/mrc_port.ts +++ b/src/blocks/mrc_port.ts @@ -1,7 +1,7 @@ /** * @license * Copyright 2025 Porpoiseful LLC - * + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -26,6 +26,7 @@ import { MRC_STYLE_PORTS } from '../themes/styles' import { createFieldNonEditableText } from '../fields/FieldNonEditableText'; import { ExtendedPythonGenerator } from '../editor/extended_python_generator'; import { createFieldNumberDropdown } from '../fields/field_number_dropdown'; +import { getOutputCheck } from './utils/python'; export const BLOCK_NAME = 'mrc_port'; export const OUTPUT_NAME = 'mrc_port'; @@ -119,6 +120,7 @@ const PORT = { } this.mrcPortType = state.portType; this.mrcPortCount = iField; + this.setOutput(true, getOutputCheck(this.mrcPortType)); }, } diff --git a/src/blocks/mrc_steps.ts b/src/blocks/mrc_steps.ts index 27f97027..676e15d5 100644 --- a/src/blocks/mrc_steps.ts +++ b/src/blocks/mrc_steps.ts @@ -16,7 +16,7 @@ */ /** - * @fileoverview Blocks for class method definition + * @fileoverview Block for defining steps. * @author alan@porpoiseful.com (Alan Smith) */ import * as Blockly from 'blockly'; @@ -25,6 +25,7 @@ import { Order } from 'blockly/python'; import { MRC_STYLE_STEPS } from '../themes/styles'; import { ExtendedPythonGenerator } from '../editor/extended_python_generator'; import { createStepFieldFlydown } from '../fields/field_flydown'; +import { NONCOPYABLE_BLOCK } from './noncopyable_block'; import { renameSteps as updateJumpToStepBlocks } from './mrc_jump_to_step'; import * as stepContainer from './mrc_step_container' import { createBooleanShadowValue } from './utils/value'; @@ -63,6 +64,7 @@ const STEPS = { this.setStyle(MRC_STYLE_STEPS); this.setMutator(stepContainer.getMutatorIcon(this)); }, + ...NONCOPYABLE_BLOCK, saveExtraState: function (this: StepsBlock): StepsExtraState { return { stepNames: this.mrcStepNames, diff --git a/src/blocks/noncopyable_block.ts b/src/blocks/noncopyable_block.ts new file mode 100644 index 00000000..2db45640 --- /dev/null +++ b/src/blocks/noncopyable_block.ts @@ -0,0 +1,36 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @author lizlooney@google.com (Liz Looney) + */ + +import * as Blockly from 'blockly'; + +export const NONCOPYABLE_BLOCK = { + isCopyable: function(this: Blockly.BlockSvg): boolean { + // We don't allow copying, but we return true here so that toCopyData will + // be called. The default in blockly is that a block is only copyable if it + // is both deletable and movable. + return true; + }, + toCopyData: function(this: Blockly.BlockSvg, _addNextBlocks = false): Blockly.clipboard.BlockCopyData | null { + // We don't allow copying, but we return null here so the previous contents + // of the clipboard is cleared. + return null; + }, +} diff --git a/src/editor/editor.ts b/src/editor/editor.ts index a05dc508..8262b867 100644 --- a/src/editor/editor.ts +++ b/src/editor/editor.ts @@ -301,19 +301,6 @@ export class Editor { } } - public isModified(): boolean { - /* - // This code is helpful for debugging issues where the editor says - // 'Blocks have been modified!'. - if (this.getModuleContentText() !== this.moduleContentText) { - console.log('isModified will return true'); - console.log('this.getModuleContentText() is ' + this.getModuleContentText()); - console.log('this.moduleContentText is ' + this.moduleContentText); - } - */ - return this.getModuleContentText() !== this.moduleContentText; - } - public getBlocklyWorkspace(): Blockly.WorkspaceSvg { return this.blocklyWorkspace; } @@ -470,12 +457,14 @@ export class Editor { public async saveModule(): Promise { const moduleContentText = this.getModuleContentText(); - try { - await this.storage.saveFile(this.modulePath, moduleContentText); - this.moduleContentText = moduleContentText; - await storageProject.saveProjectInfo(this.storage, this.projectName); - } catch (e) { - throw e; + if (moduleContentText !== this.moduleContentText) { + try { + await this.storage.saveFile(this.modulePath, moduleContentText); + this.moduleContentText = moduleContentText; + await storageProject.saveProjectInfo(this.storage, this.projectName); + } catch (e) { + throw e; + } } return moduleContentText; } diff --git a/src/reactComponents/TabContent.tsx b/src/reactComponents/TabContent.tsx index d7feb593..103bca5b 100644 --- a/src/reactComponents/TabContent.tsx +++ b/src/reactComponents/TabContent.tsx @@ -74,7 +74,7 @@ export const TabContent = React.forwardRef(({ setAlertErrorMessage, isActive, }, ref) => { - const [blocklyComponent, setBlocklyComponent] = React.useState(null); + const blocklyComponent = React.useRef(null); const [editorInstance, setEditorInstance] = React.useState(null); const [generatedCode, setGeneratedCode] = React.useState(''); const [triggerPythonRegeneration, setTriggerPythonRegeneration] = React.useState(0); @@ -116,7 +116,8 @@ export const TabContent = React.forwardRef(({ } // Check if this event is for our workspace - if (blocklyComponent && event.workspaceId === blocklyComponent.getBlocklyWorkspace().id) { + if (blocklyComponent.current && + event.workspaceId === blocklyComponent.current.getBlocklyWorkspace().id) { setTriggerPythonRegeneration(Date.now()); // Also notify parent } @@ -124,7 +125,7 @@ export const TabContent = React.forwardRef(({ /** Called when BlocklyComponent is created. */ const setupBlocklyComponent = React.useCallback((_modulePath: string, newBlocklyComponent: BlocklyComponentType) => { - setBlocklyComponent(newBlocklyComponent); + blocklyComponent.current = newBlocklyComponent; newBlocklyComponent.setActive(isActive); }, [isActive]); @@ -156,8 +157,8 @@ export const TabContent = React.forwardRef(({ /** Update active state when isActive changes. */ React.useEffect(() => { - if (blocklyComponent) { - blocklyComponent.setActive(isActive); + if (blocklyComponent.current) { + blocklyComponent.current.setActive(isActive); } if (editorInstance && isActive) { editorInstance.makeCurrent(project, modulePathToContentText); @@ -166,9 +167,9 @@ export const TabContent = React.forwardRef(({ /** Generate code when regeneration is triggered. */ React.useEffect(() => { - if (blocklyComponent && module) { + if (blocklyComponent.current && module) { const code = extendedPythonGenerator.mrcWorkspaceToCode( - blocklyComponent.getBlocklyWorkspace(), + blocklyComponent.current.getBlocklyWorkspace(), module ); setGeneratedCode(code); diff --git a/src/storage/upgrade_project.ts b/src/storage/upgrade_project.ts index 47d4acd2..51d7acc8 100644 --- a/src/storage/upgrade_project.ts +++ b/src/storage/upgrade_project.ts @@ -29,10 +29,11 @@ import * as storageNames from './names'; import * as storageProject from './project'; import { upgrade_001_to_002 } from '../blocks/mrc_mechanism_component_holder'; import { upgrade_002_to_003, upgrade_004_to_005 } from '../blocks/mrc_class_method_def'; +import { upgrade_005_to_006 } from '../blocks/mrc_component'; import * as workspaces from '../blocks/utils/workspaces'; export const NO_VERSION = '0.0.0'; -export const CURRENT_VERSION = '0.0.5'; +export const CURRENT_VERSION = '0.0.6'; export async function upgradeProjectIfNecessary( storage: commonStorage.Storage, projectName: string): Promise { @@ -66,6 +67,11 @@ export async function upgradeProjectIfNecessary( // @ts-ignore case '0.0.4': upgradeFrom_004_to_005(storage, projectName, projectInfo); + + // Intentional fallthrough after case '0.0.5' + // @ts-ignore + case '0.0.5': + upgradeFrom_005_to_006(storage, projectName, projectInfo); } await storageProject.saveProjectInfo(storage, projectName); } @@ -212,3 +218,15 @@ async function upgradeFrom_004_to_005( anyModuleType, upgrade_004_to_005); projectInfo.version = '0.0.5'; } + +async function upgradeFrom_005_to_006( + storage: commonStorage.Storage, + projectName: string, + projectInfo: storageProject.ProjectInfo): Promise { + // mrc_component blocks parameter types need to be fixed. + await upgradeBlocksFiles( + storage, projectName, + noModuleTypes, noPreupgrade, + anyModuleType, upgrade_005_to_006); + projectInfo.version = '0.0.6'; +}