import { clipboard } from "electron"; import React from "react"; import { computed, action, observable, runInAction, toJS, makeObservable } from "mobx"; import { validators } from "eez-studio-shared/validation"; import { validators as validatorsRenderer } from "eez-studio-shared/validation-renderer"; import { showGenericDialog } from "eez-studio-ui/generic-dialog"; import { ClassInfo, registerClass, IEezObject, EezObject, PropertyType, MessageType, IMessage, PropertyInfo, getProperty, PropertyProps, isPropertyDisabled } from "project-editor/core/object"; import { getChildOfObject, Message, propertyNotSetMessage, createObject, isEezObjectArray, getAncestorOfType, findPropertyByNameInObject } from "project-editor/store"; import { isDashboardProject, hasFlowSupport, isLVGLProject } from "project-editor/project/project-type-traits"; import { Project, findVariableDeep, getAssetFullName } from "project-editor/project/project"; import type { IDataContext, IVariable } from "project-editor/flow/flow-interfaces"; import { getProjectStore } from "project-editor/store"; import { evalConstantExpression } from "project-editor/flow//expression"; import { variableTypeProperty, migrateType, VariableTypeFieldComponent, isObjectType, isIntegerVariable, isEnumVariable, getEnumValues, isValueTypeOf, ValueType, getObjectVariableTypeFromType, SYSTEM_STRUCTURES, isValidType, getSystemEnums, isEnumType, getEnumTypeNameFromType } from "project-editor/features/variable/value-type"; import { FLOW_ITERATOR_INDEXES_VARIABLE, FLOW_ITERATOR_INDEX_VARIABLE } from "project-editor/features/variable/defs"; import { ProjectEditor } from "project-editor/project-editor-interface"; import { generalGroup } from "project-editor/ui-components/PropertyGrid/groups"; import { RenderVariableStatusPropertyUI } from "project-editor/features/variable/global-variable-status"; import { VARIABLE_ICON } from "project-editor/ui-components/icons"; import type { ProjectEditorFeature } from "project-editor/store/features"; import type { UserProperty } from "project-editor/flow/user-property"; import type { Flow } from "project-editor/flow/flow"; import { observer } from "mobx-react"; import { ProjectContext } from "project-editor/project/context"; import { Build, getName, NamingConvention } from "project-editor/build/helper"; import { IconAction } from "eez-studio-ui/action"; import { CodeEditor } from "eez-studio-ui/code-editor"; import { Icon } from "eez-studio-ui/icon"; //////////////////////////////////////////////////////////////////////////////// function isGlobalVariable(object: IEezObject) { return !ProjectEditor.getFlow(object); } export function uniqueForVariableAndUserProperty( object: Variable | UserProperty, parent: IEezObject, propertyInfo?: PropertyInfo ) { const flow = getAncestorOfType( parent, ProjectEditor.FlowClass.classInfo ) as Flow; if (!flow) { return validators.unique(object, parent); } const oldIdentifier = object && propertyInfo ? getProperty(object, propertyInfo.name) : undefined; return (object: any, ruleName: string) => { const newIdentifer = object[ruleName]; if (oldIdentifier != undefined && newIdentifer == oldIdentifier) { return null; } const userPropertyOrLocalVariable = flow.userPropertiesAndLocalVariables.find( userPropertyOrLocalVariable => userPropertyOrLocalVariable != object && userPropertyOrLocalVariable.name === newIdentifer ); if (!userPropertyOrLocalVariable) { return null; } if (userPropertyOrLocalVariable instanceof Variable) { return "Local variable with this name already exists"; } else { return "User property with this name already exists"; } }; } //////////////////////////////////////////////////////////////////////////////// export const NativeVariableImplementationInfoPropertyUI = observer( class NativeVariableImplementationInfoPropertyUI extends React.Component { static contextType = ProjectContext; declare context: React.ContextType; codeEditorRef = React.createRef(); componentDidMount() { this.codeEditorRef.current?.resize(); } componentDidUpdate() { this.codeEditorRef.current?.resize(); } get projectStore() { return ProjectEditor.getProjectStore(this.variable); } get implementationLanguage() { return this.projectStore.uiStateStore.implementationLanguage; } set implementationLanguage(value: string) { runInAction(() => { this.projectStore.uiStateStore.implementationLanguage = value; }); } get hasFlowSupport() { return this.projectStore.projectTypeTraits.hasFlowSupport; } get variable() { return this.props.objects[0] as Variable; } get code() { const variable = this.variable; const variableName = getName( "", variable.name, NamingConvention.UnderscoreLowerCase ); let nativeType; if (variable.type == "integer") { nativeType = "int32_t "; } else if (variable.type == "float") { nativeType = "float "; } else if (variable.type == "double") { nativeType = "double "; } else if (variable.type == "boolean") { nativeType = "bool "; } else if (variable.type == "string") { nativeType = "const char *"; } else if (isEnumType(variable.type)) { const enumType = getEnumTypeNameFromType(variable.type); nativeType = `${enumType} `; } else { } const build = new Build(); build.startBuild(); if (variable.type == "string") { if (this.implementationLanguage == "C") { build.line(`#include `); } else { build.line(`#include `); } } build.line(`#include "vars.h"`); build.line(""); build.line( `${ variable.type == "string" ? this.implementationLanguage == "C" ? "char " : "std::string " : nativeType }${variableName}${ variable.type == "string" && this.implementationLanguage == "C" ? "[100] = { 0 }" : "" };` ); build.line(""); build.line( `${ this.implementationLanguage == "C++" ? `extern "C" ` : "" }${nativeType}${"get_var_" + variableName}() {` ); build.indent(); build.line( `return ${variableName}${ variable.type == "string" && this.implementationLanguage == "C++" ? ".c_str()" : "" };` ); build.unindent(); build.line(`}`); build.line(""); build.line( `${ this.implementationLanguage == "C++" ? `extern "C" ` : "" }void ${"set_var_" + variableName}(${nativeType}value) {` ); build.indent(); if ( variable.type == "string" && this.implementationLanguage == "C" ) { build.line( `strncpy(${variableName}, value, sizeof(${variableName}) / sizeof(char));` ); build.line( `${variableName}[sizeof(${variableName}) / sizeof(char) - 1] = 0;` ); } else { build.line(`${variableName} = value;`); } build.unindent(); build.line(`}`); return build.result; } render() { const code = this.code; return (
TIP
{this.hasFlowSupport ? "For native variable " : "For variable "} you must provide implementation of get and set functions. Below is a basic implementation code for such functions. You can copy and paste it into some source file in your project.
{ clipboard.writeText(code); }} />
{}} readOnly={true} className="form-control" minLines={2} maxLines={50} />
); } } ); //////////////////////////////////////////////////////////////////////////////// export class Variable extends EezObject { id: number | undefined; name: string; description?: string; type: ValueType; defaultValue: string; defaultValueList: string; usedIn?: string[]; persistent: boolean; native: boolean; constructor() { super(); makeObservable(this, { fullName: computed }); } override makeEditable() { super.makeEditable(); makeObservable(this, { id: observable, name: observable, description: observable, type: observable, defaultValue: observable, defaultValueList: observable, usedIn: observable, persistent: observable, native: observable }); } static classInfo: ClassInfo = { properties: [ { name: "id", type: PropertyType.Number, isOptional: true, unique: true, propertyGridGroup: generalGroup, disabled: (variable: Variable) => !isGlobalVariable(variable) || isLVGLProject(variable) }, { name: "name", type: PropertyType.String, uniqueIdentifier: uniqueForVariableAndUserProperty }, { name: "description", type: PropertyType.MultilineText }, variableTypeProperty, { name: "defaultValue", type: PropertyType.MultilineText, expressionType: "any", expressionIsConstant: true, flowProperty: "input", monospaceFont: true, disabled: object => { const project = ProjectEditor.getProject(object); return ( project.projectTypeTraits.isLVGL && !project.projectTypeTraits.hasFlowSupport ); } }, { name: "native", type: PropertyType.Boolean, disabled: (variable: Variable) => !isGlobalVariable(variable) || !hasFlowSupport(variable) || !ProjectEditor.getProject( variable ).projectTypeTraits.isVariableTypeSupportedAsNative( variable.type ), checkboxStyleSwitch: true }, { name: "nativeImplementationInfo", type: PropertyType.Any, computed: true, propertyGridRowComponent: NativeVariableImplementationInfoPropertyUI, skipSearch: true, hideInPropertyGrid: (variable: Variable) => { const projectStore = getProjectStore(variable); return !( isLVGLProject(variable) && isGlobalVariable(variable) && (!projectStore.projectTypeTraits.hasFlowSupport || variable.native) ); } }, { name: "defaultValueList", type: PropertyType.MultilineText, disabled: object => isLVGLProject(object) || hasFlowSupport(object), monospaceFont: true }, { name: "usedIn", type: PropertyType.ConfigurationReference, referencedObjectCollectionPath: "settings/build/configurations", disabled: object => isDashboardProject(object) || isLVGLProject(object) }, { name: "persistent", type: PropertyType.Boolean, disabled: (variable: Variable) => isLVGLProject(variable) || !isGlobalVariable(variable) || variable.native || (isObjectType(variable.type) && !getObjectVariableTypeFromType( ProjectEditor.getProjectStore(variable), variable.type )?.editConstructorParams) || variable.type == "object:TCPSocket", checkboxStyleSwitch: true }, { name: "persistedValue", type: PropertyType.Any, computed: true, propertyGridRowComponent: RenderVariableStatusPropertyUI, skipSearch: true, hideInPropertyGrid: (variable: Variable) => isLVGLProject(variable) || !variable.persistent || !isGlobalVariable(variable) || variable.type == "object:TCPSocket" } ], icon: VARIABLE_ICON, listLabel: (variable: Variable) => { return ( <> {!isPropertyDisabled( variable, findPropertyByNameInObject(variable, "native")! ) && variable.native && ( NATIVE )} {!isPropertyDisabled( variable, findPropertyByNameInObject(variable, "persistent")! ) && variable.persistent && ( PERSISTENT )} {variable.name} {variable.type} ); }, propertiesPanelLabel: (variable: Variable) => { return `${ ProjectEditor.getFlow(variable) ? "Local" : "Global" } variable: ${variable.name}`; }, beforeLoadHook: (object: Variable, objectJS: any) => { migrateType(objectJS); }, newItem: async (parent: IEezObject) => { const project = ProjectEditor.getProject(parent); function isNativeFlagEnabled(values: any) { return ( isGlobalVariable(parent) && hasFlowSupport(parent) && ProjectEditor.getProject( parent ).projectTypeTraits.isVariableTypeSupportedAsNative( values.type ) ); } const result = await showGenericDialog({ dialogDefinition: { title: ProjectEditor.getFlow(parent) ? "New Local Variable" : "New Global Variable", fields: [ { name: "name", type: "string", validators: [ validators.required, validatorsRenderer.identifierValidator, uniqueForVariableAndUserProperty( {} as any, parent ) ] }, { name: "type", type: VariableTypeFieldComponent, validators: [validators.required] }, { name: "defaultValue", type: "string", validators: [validators.required], visible: () => !project.projectTypeTraits.isLVGL || project.projectTypeTraits.hasFlowSupport }, { name: "native", type: "boolean", visible: isNativeFlagEnabled } ] }, values: {}, dialogContext: ProjectEditor.getProject(parent) }); let persistent = !ProjectEditor.getFlow(parent) && isObjectType(result.values.type) && result.values.type != "object:TCPSocket"; const variableProperties: Partial = { name: result.values.name, type: result.values.type, defaultValue: result.values.defaultValue, persistent, native: isNativeFlagEnabled(result.values) ? result.values.native : false }; const variable = createObject( project._store, variableProperties, Variable ); return variable; }, check: (variable: Variable, messages: IMessage[]) => { const projectStore = getProjectStore(variable); if (isGlobalVariable(variable)) { ProjectEditor.checkAssetId( projectStore, "variables/globalVariables", variable, messages ); } if (!variable.type) { messages.push(propertyNotSetMessage(variable, "type")); } // if ( // projectStore.projectTypeTraits.hasFlowSupport && // (variable.type === "any" || variable.type === "array:any") // ) { // messages.push( // new Message(MessageType.WARNING, `Any type used`, variable) // ); // } if (!isValidType(projectStore.project, variable.type)) { messages.push( new Message(MessageType.ERROR, `Invalid type`, variable) ); } if (!variable.defaultValue) { messages.push(propertyNotSetMessage(variable, "defaultValue")); } else { if (projectStore.projectTypeTraits.hasFlowSupport) { try { const { value, valueType } = evalConstantExpression( projectStore.project, variable.defaultValue ); const error = isValueTypeOf( projectStore.project, value, valueType, variable.type ); if (error) { messages.push( new Message( MessageType.ERROR, error, getChildOfObject(variable, "defaultValue") ) ); } } catch (err) { messages.push( new Message( MessageType.ERROR, err.toString(), getChildOfObject(variable, "defaultValue") ) ); } } } }, addObjectHook: (variable: Variable, parent: IEezObject) => { if ( ProjectEditor.getProject(parent).projectTypeTraits.isDashboard ) { variable.native = false; } } }; get fullName() { return getAssetFullName(this); } } registerClass("Variable", Variable); //////////////////////////////////////////////////////////////////////////////// export class DataContext implements IDataContext { project: Project; parentDataContext: DataContext | undefined; defaultValueOverrides: any; localVariables: Map | undefined = undefined; runtimeValues: Map; constructor( project: Project, parentDataContext?: DataContext, defaultValueOverrides?: any, localVariables?: Map ) { makeObservable(this, { runtimeValues: observable, clear: action }); this.project = project; this.parentDataContext = parentDataContext; this.defaultValueOverrides = defaultValueOverrides; this.runtimeValues = new Map(); this.localVariables = localVariables; if (this.localVariables) { this.localVariables.forEach(variable => { if (this.project._store.runtime) { this.runtimeValues.set(variable.fullName, undefined); } else { try { const { value } = evalConstantExpression( project, variable.defaultValue ); this.runtimeValues.set(variable.fullName, value); } catch (err) { if (project._store.runtime) { throw err; } this.runtimeValues.set(variable.fullName, undefined); } } }); } if (!this.parentDataContext) { this.initGlobalVariables(); } } initGlobalVariables() { if (this.project.variables) { this.project.allGlobalVariables.forEach(variable => { try { const { value } = evalConstantExpression( this.project, variable.defaultValue ); this.runtimeValues.set(variable.fullName, value); } catch (err) { if (this.project._store.runtime) { throw err; } if (!this.project.projectTypeTraits.hasFlowSupport) { const value = this.project._assets.maps[ "name" ].allAssets.get(variable.defaultValue); if (value) { this.runtimeValues.set(variable.fullName, value); } else { this.runtimeValues.set( variable.fullName, variable.defaultValue ); } } else { this.runtimeValues.set(variable.fullName, null); } } }); } } createWithDefaultValueOverrides(defaultValueOverrides: any): IDataContext { return new DataContext(this.project, this, defaultValueOverrides); } createWithLocalVariables(variablesArray: IVariable[]) { const localVariables = new Map(); variablesArray.forEach(variable => { localVariables.set(variable.fullName, variable); }); return new DataContext(this.project, this, undefined, localVariables); } getDefaultValue(variableName: string): any { if (this.defaultValueOverrides) { const value = this.defaultValueOverrides[variableName]; if (value != undefined) { return value; } } if (this.parentDataContext) { return this.parentDataContext.getDefaultValue(variableName); } return undefined; } getRuntimeValue(variable: IVariable | undefined): { hasValue: boolean; value: any; } { if (variable) { if (this.defaultValueOverrides) { const value = this.defaultValueOverrides[variable.fullName]; if (value != undefined) { return { hasValue: true, value }; } } if (this.runtimeValues.has(variable.fullName)) { return { hasValue: this.runtimeValues.has(variable.fullName), value: this.runtimeValues.get(variable.fullName) }; } if (this.parentDataContext) { return this.parentDataContext.getRuntimeValue(variable); } } return { hasValue: false, value: undefined }; } setRuntimeValue(variableName: string, value: any) { if (this.localVariables && this.localVariables.has(variableName)) { this.runtimeValues.set(variableName, value); } else { if (this.parentDataContext) { this.parentDataContext.setRuntimeValue(variableName, value); } else { const variable = findVariableDeep(this.project, variableName); if (variable) { this.runtimeValues.set(variableName, value); } else { throw `variable "${variableName}" not found`; } } } } clear() { this.runtimeValues.clear(); this.initGlobalVariables(); } set(variableName: string, value: any) { // const oldValue = this.get(variableName); // if (oldValue) { // const variable = this.findVariable(variableName); // if (variable) { // const objectVariableType = getObjectVariableTypeFromType( // this.project._store, // variable.type // ); // if (objectVariableType) { // objectVariableType.destroyValue(oldValue, value); // } // } // } runInAction(() => this.setRuntimeValue(variableName, value)); } findVariableDefaultValue(variableName: string): any { if (this.defaultValueOverrides) { const defaultValue = this.defaultValueOverrides[variableName]; if (defaultValue != undefined) { return defaultValue; } } if (this.parentDataContext) { return this.parentDataContext.findVariableDefaultValue( variableName ); } return undefined; } findVariable(variableName: string): IVariable | undefined { let variable: IVariable | undefined; // find local variable if (this.localVariables && this.localVariables.has(variableName)) { variable = this.localVariables.get(variableName); } if (!variable) { if (this.parentDataContext) { return this.parentDataContext.findVariable(variableName); } } if (!variable) { // find global variable variable = findVariableDeep(this.project, variableName); } return variable; } has(variableName: string) { if ( variableName == FLOW_ITERATOR_INDEX_VARIABLE || variableName == FLOW_ITERATOR_INDEXES_VARIABLE ) { return this.get(variableName) !== undefined; } return !!this.findVariable(variableName); } get(variableName: string): any { if ( variableName == FLOW_ITERATOR_INDEX_VARIABLE || variableName == FLOW_ITERATOR_INDEXES_VARIABLE ) { return this.getDefaultValue(variableName); } if (!variableName) { return undefined; } if (variableName === undefined) { return undefined; } let value: any = undefined; const variable = this.findVariable(variableName); const { hasValue, value: value_ } = this.getRuntimeValue(variable); if (variable) { if (hasValue) { value = value_; } else { let defaultValue = this.findVariableDefaultValue(variableName); if (defaultValue == undefined) { defaultValue = variable.defaultValue; } if (defaultValue !== undefined) { if (isIntegerVariable(variable)) { value = parseInt(defaultValue); if (isNaN(value)) { console.error( "Invalid integer default value", variable ); } } else if (isEnumVariable(variable)) { // TODO this is invalid check value = defaultValue; if (getEnumValues(variable).indexOf(value) == -1) { console.error( "Invalid enum default value", variable ); } } else if (variable.type == "float") { value = parseFloat(defaultValue); if (isNaN(value)) { value = defaultValue; console.error( "Invalid float default value", variable ); } } else if (variable.type == "boolean") { defaultValue = defaultValue .toString() .trim() .toLowerCase(); if (defaultValue == "1" || defaultValue == "true") { value = true; } else if ( defaultValue == "0" || defaultValue == "false" ) { value = false; } else { value = undefined; } } else if (variable.type == "array") { try { value = typeof defaultValue === "string" ? JSON.parse(defaultValue) : defaultValue; } catch (err) { value = []; console.error( "Invalid array default value", variable, err ); } } else { value = defaultValue; } } else { value = undefined; } } } return value; } getBool(variableName: string): boolean { let value = this.get(variableName); if (typeof value === "boolean") { return value; } if (typeof value === "number" && Number.isInteger(value)) { return value ? true : false; } return false; } getEnumValue(variableName: string): number { let value = this.get(variableName); if (typeof value === "boolean") { return value ? 1 : 0; } else if (typeof value === "number" && Number.isInteger(value)) { return value; } else if (typeof value === "string") { let variable = this.findVariable(variableName); if (variable && isEnumVariable(variable)) { // TODO this is invalid check value = getEnumValues(variable).indexOf(value); if (value == -1) { console.error("Invalid enum value", variable); return 0; } else { return value; } } } return 0; } getValueList(variableName: string): string[] { let variable = this.findVariable(variableName); if (variable) { try { return JSON.parse(variable.defaultValueList); } catch (err) { console.error("Invalid value list", variable, err); return []; } } return []; } get debugInfo(): any { const runtimeValues: any = {}; for (const [name, value] of this.runtimeValues) { try { const valueJS = toJS(value); JSON.stringify(valueJS); runtimeValues[name] = valueJS; } catch (err) {} } return { runtimeValues }; } set debugInfo(debugInfo: any) { runInAction(() => { for (const name in debugInfo.runtimeValues) { this.runtimeValues.set(name, debugInfo.runtimeValues[name]); } }); } } //////////////////////////////////////////////////////////////////////////////// export interface IStructureField { name: string; type: ValueType; } export class StructureField extends EezObject implements IStructureField { name: string; type: ValueType; static classInfo: ClassInfo = { properties: [ { name: "name", type: PropertyType.String, uniqueIdentifier: true }, variableTypeProperty ], listLabel: (field: StructureField, collapsed: boolean) => collapsed ? ( {field.name}: {field.type} ) : ( "" ), check: (structureField: StructureField, messages: IMessage[]) => { if (!structureField.name) { messages.push(propertyNotSetMessage(structureField, "name")); } if (!structureField.type) { messages.push(propertyNotSetMessage(structureField, "type")); } // if ( // structureField.type === "any" || // structureField.type === "array:any" // ) { // messages.push( // new Message( // MessageType.WARNING, // `Any type used`, // getChildOfObject(structureField, "type") // ) // ); // } const projectStore = getProjectStore(structureField); if (!isValidType(projectStore.project, structureField.type)) { messages.push( new Message( MessageType.ERROR, `Invalid type`, structureField ) ); } }, beforeLoadHook: (object: Variable, objectJS: any) => { migrateType(objectJS); }, defaultValue: {}, newItem: async (parent: IEezObject) => { const result = await showGenericDialog({ dialogDefinition: { title: "New Structure Field", fields: [ { name: "name", type: "string", validators: [ validators.required, validatorsRenderer.identifierValidator, validators.unique({}, parent) ] }, { name: "type", type: VariableTypeFieldComponent, validators: [validators.required] } ] }, values: {}, dialogContext: ProjectEditor.getProject(parent) }); const structureFieldProperties: Partial = { name: result.values.name, type: result.values.type }; const project = ProjectEditor.getProject(parent); const structureField = createObject( project._store, structureFieldProperties, StructureField ); return structureField; } }; override makeEditable() { super.makeEditable(); makeObservable(this, { name: observable, type: observable }); } } registerClass("StructureField", StructureField); //////////////////////////////////////////////////////////////////////////////// export interface IStructure { name: string; fields: IStructureField[]; get fieldsMap(): Map; } export class Structure extends EezObject implements IStructure { name: string; fields: StructureField[]; static classInfo: ClassInfo = { properties: [ { name: "name", type: PropertyType.String, uniqueIdentifier: true }, { name: "fields", type: PropertyType.Array, typeClass: StructureField } ], propertiesPanelLabel: (structure: Structure) => { return `Structure: ${structure.name}`; }, newItem: async (parent: IEezObject) => { const result = await showGenericDialog({ dialogDefinition: { title: "New Structure", fields: [ { name: "name", type: "string", validators: [ validators.required, validatorsRenderer.identifierValidator, validators.unique({}, parent) ] } ] }, values: {} }); const structureProperties: Partial = { name: result.values.name }; const project = ProjectEditor.getProject(parent); const structure = createObject( project._store, structureProperties, Structure ); return structure; }, icon: ( ) }; constructor() { super(); makeObservable(this, { fieldsMap: computed({ keepAlive: true }) }); } override makeEditable() { super.makeEditable(); makeObservable(this, { name: observable, fields: observable }); } get fieldsMap() { const fieldsMap = new Map(); this.fields.forEach(field => fieldsMap.set(field.name, field)); return fieldsMap; } } registerClass("Structure", Structure); //////////////////////////////////////////////////////////////////////////////// export interface IEnumMember { name: string; value: number; } export class EnumMember extends EezObject implements IEnumMember { name: string; value: number; static classInfo: ClassInfo = { properties: [ { name: "name", type: PropertyType.String, uniqueIdentifier: true }, { name: "value", type: PropertyType.Number } ], listLabel: (member: EnumMember, collapsed: boolean) => collapsed ? `${member.name}: ${member.value}` : "", check: (enumMember: EnumMember, messages: IMessage[]) => { if (!enumMember.name) { messages.push(propertyNotSetMessage(enumMember, "name")); } if (enumMember.value == undefined) { messages.push(propertyNotSetMessage(enumMember, "value")); } }, defaultValue: {}, newItem: async (parent: IEezObject) => { const result = await showGenericDialog({ dialogDefinition: { title: "New Enum Member", fields: [ { name: "name", type: "string", validators: [ validators.required, validatorsRenderer.identifierValidator, validators.unique({}, parent) ] } ] }, values: {} }); const enumMemberProperties: Partial = { name: result.values.name, value: isEezObjectArray(parent) && parent.length > 0 ? (parent[parent.length - 1] as EnumMember).value + 1 : 0 }; const project = ProjectEditor.getProject(parent); const enumMember = createObject( project._store, enumMemberProperties, EnumMember ); return enumMember; } }; override makeEditable() { super.makeEditable(); makeObservable(this, { name: observable, value: observable }); } } registerClass("EnumMember", EnumMember); //////////////////////////////////////////////////////////////////////////////// export interface IEnum { name: string; members: IEnumMember[]; get membersMap(): Map; } export class Enum extends EezObject implements IEnum { name: string; members: EnumMember[]; static classInfo: ClassInfo = { properties: [ { name: "name", type: PropertyType.String, uniqueIdentifier: true }, { name: "members", type: PropertyType.Array, typeClass: EnumMember } ], icon: ( ), propertiesPanelLabel: (enumObject: Enum) => { return `Enum: ${enumObject.name}`; }, newItem: async (parent: IEezObject) => { const result = await showGenericDialog({ dialogDefinition: { title: "New Enum", fields: [ { name: "name", type: "string", validators: [ validators.required, validatorsRenderer.identifierValidator, validators.unique({}, parent) ] } ] }, values: {} }); const enumProperties: Partial = { name: result.values.name }; const project = ProjectEditor.getProject(parent); const enumObject = createObject( project._store, enumProperties, Enum ); return enumObject; } }; constructor() { super(); makeObservable(this, { membersMap: computed }); } override makeEditable() { super.makeEditable(); makeObservable(this, { name: observable, members: observable }); } get membersMap() { const map = new Map(); for (const member of this.members) { map.set(member.name, member); } return map; } } registerClass("Enum", Enum); export class ProjectVariables extends EezObject { globalVariables: Variable[]; structures: Structure[]; enums: Enum[]; static classInfo: ClassInfo = { label: () => "Variables", properties: [ { name: "globalVariables", type: PropertyType.Array, typeClass: Variable, hideInPropertyGrid: true }, { name: "structures", type: PropertyType.Array, typeClass: Structure, hideInPropertyGrid: true }, { name: "enums", type: PropertyType.Array, typeClass: Enum, hideInPropertyGrid: true } ], icon: VARIABLE_ICON, defaultValue: { globalVariables: [], structures: [], enums: [] } }; constructor() { super(); makeObservable(this, { enumsMap: computed({ keepAlive: true }), structsMap: computed({ keepAlive: true }) }); } override makeEditable() { super.makeEditable(); makeObservable(this, { globalVariables: observable, structures: observable, enums: observable }); } get enumsMap() { const map = new Map(); for (const enumDef of this.enums) { map.set(enumDef.name, enumDef); } const projectStore = ProjectEditor.getProjectStore(this); const systemEnums = getSystemEnums(projectStore); for (const enumDef of systemEnums) { map.set(enumDef.name, enumDef); } return map; } get structsMap() { const map = new Map(); for (const structure of this.structures) { map.set(structure.name, structure); } for (const structure of SYSTEM_STRUCTURES) { map.set(structure.name, structure); } return map; } } registerClass("ProjectVariables", ProjectVariables); //////////////////////////////////////////////////////////////////////////////// const feature: ProjectEditorFeature = { name: "eezstudio-project-feature-variables", version: "0.1.0", description: "Variables, Structures and Enums", author: "EEZ", authorLogo: "../eez-studio-ui/_images/eez_logo.png", displayName: "Variables", mandatory: true, key: "variables", type: PropertyType.Object, typeClass: ProjectVariables, icon: VARIABLE_ICON, create: () => { return { globalVariables: [], structures: [], enums: [] }; }, check: (projectStore, object: EezObject[], messages: IMessage[]) => { if (object.length > 32000) { messages.push( new Message( MessageType.ERROR, "Max. 32000 global variables are supported", object ) ); } } }; export default feature;