import React from "react"; import { observable, action, runInAction, autorun, makeObservable } from "mobx"; import { observer } from "mobx-react"; import classNames from "classnames"; import { dialog, getCurrentWindow } from "@electron/remote"; import { guid } from "eez-studio-shared/guid"; import { humanize } from "eez-studio-shared/string"; import { filterNumber } from "eez-studio-shared/validation"; import { confirm } from "eez-studio-ui/dialog-electron"; import { Icon } from "eez-studio-ui/icon"; import { PropertyType, PropertyProps, getObjectPropertyDisplayName, isPropertyOptional, EnumItem } from "project-editor/core/object"; import { info } from "project-editor/core/util"; import { ConfigurationReferencesPropertyValue } from "project-editor/ui-components/ConfigurationReferencesPropertyValue"; import { ProjectContext } from "project-editor/project/context"; import { evalConstantExpression, parseIdentifier } from "project-editor/flow/expression"; import { EXPR_MARK_END, EXPR_MARK_START } from "project-editor/flow/expression/ExpressionBuilder"; import { getEnumItems, getFormText, getObjectPropertyValue, getPropertyValue, getPropertyValueAsString } from "./utils"; import { CodeEditorProperty } from "./CodeEditorProperty"; import { ThemedColorInput } from "./ThemedColorInput"; import { ObjectReferenceInput } from "./ObjectReferenceInput"; import { ProjectEditor } from "project-editor/project-editor-interface"; import { Checkbox } from "./Checkbox"; import { ImageProperty } from "./ImageProperty"; import { General } from "project-editor/project/project"; import { UniqueValueInput } from "./UniqueValueInput"; //////////////////////////////////////////////////////////////////////////////// export const Property = observer( class Property extends React.Component { static contextType = ProjectContext; declare context: React.ContextType; textarea: HTMLTextAreaElement | undefined; input: HTMLInputElement; select: HTMLSelectElement; _value: any = undefined; static propertyComponents: Property[] = []; static resizeTextAreaInterval: any; static resizePropertyComponentIndex = 0; changeDocumentDisposer: any; disposeEventHandlers: (() => void) | undefined; constructor(props: PropertyProps) { super(props); makeObservable(this, { _value: observable, componentDidUpdate: action, changeValue: action.bound }); } static resizeTextAreas() { if ( Property.resizePropertyComponentIndex >= Property.propertyComponents.length ) { Property.resizePropertyComponentIndex = 0; } const textarea = Property.propertyComponents[ Property.resizePropertyComponentIndex ].textarea; if (textarea) { if (textarea.style.height != textarea.scrollHeight + "px") { textarea.style.height = "0"; textarea.style.height = textarea.scrollHeight + "px"; } } Property.resizePropertyComponentIndex++; } static registerProperty(property: Property) { if (property.textarea) { if (Property.propertyComponents.indexOf(property) == -1) { if (Property.propertyComponents.length == 0) { Property.resizeTextAreaInterval = setInterval( Property.resizeTextAreas ); } Property.propertyComponents.push(property); } } else { Property.unregisterProperty(property); } } static unregisterProperty(property: Property) { const i = Property.propertyComponents.indexOf(property); if (i != -1) { Property.propertyComponents.splice(i, 1); if (Property.propertyComponents.length == 0) { clearInterval(Property.resizeTextAreaInterval); } } } updateChangeDocumentObserver() { if (this.changeDocumentDisposer) { this.changeDocumentDisposer(); } this.changeDocumentDisposer = autorun(() => { if (this.context.project) { const getPropertyValueResult = getPropertyValue( this.props.objects, this.props.propertyInfo ); runInAction(() => { this._value = getPropertyValueResult ? getPropertyValueResult.value : undefined; }); } }); } onFocus = (event: JQuery.FocusEvent) => this.context.undoManager.setCombineCommands(true); onBlur = (event: JQuery.BlurEvent) => this.context.undoManager.setCombineCommands(false); onInputKeyDown = (event: JQuery.KeyDownEvent) => { if (event.target.readOnly && event.key == "Delete") { event.preventDefault(); event.stopPropagation(); return; } if (event.key == "Escape") { if (this.context.undoManager.commands.length > 0) { this.context.undoManager.undo(); } } }; addEventHandlers() { if (this.disposeEventHandlers) { this.disposeEventHandlers(); } const el = this.props.propertyInfo.type != PropertyType.Boolean ? this.input || this.textarea || this.select : undefined; if (el) { $(el).on("focus", this.onFocus); $(el).on("blur", this.onBlur); $(el).on("keydown", this.onInputKeyDown); this.disposeEventHandlers = () => { $(el).off("focus", this.onFocus); $(el).off("blur", this.onBlur); $(el).off("keydown", this.onInputKeyDown); }; } } componentDidMount() { this.updateChangeDocumentObserver(); this.addEventHandlers(); Property.registerProperty(this); } componentDidUpdate(prevProps: PropertyProps) { if ( !arrayCompareShallow(prevProps.objects, this.props.objects) || prevProps.propertyInfo != this.props.propertyInfo ) { this.updateChangeDocumentObserver(); } this.addEventHandlers(); Property.registerProperty(this); } componentWillUnmount() { this.changeDocumentDisposer(); if (this.disposeEventHandlers) { this.disposeEventHandlers(); } Property.unregisterProperty(this); } onSelect = (event: React.MouseEvent) => { if (this.props.propertyInfo.onSelect) { let params; if ( this.props.propertyInfo.type == PropertyType.String || this.props.propertyInfo.type == PropertyType.NumberArrayAsString || this.props.propertyInfo.type == PropertyType.MultilineText || this.props.propertyInfo.type == PropertyType.ObjectReference ) { const input = $(event.target) .parent() .parent() .find("input,textarea")[0] as HTMLInputElement; if (input) { params = { textInputSelection: { start: input.selectionStart, end: input.selectionEnd, direction: input.selectionDirection } }; } } this.props.propertyInfo .onSelect( this.props.objects[0], this.props.propertyInfo, params ) .then(propertyValues => { this.props.updateObject(propertyValues); }) .catch(error => console.error(error)); } }; changeValue(newValue: any) { if (this.props.readOnly) { return; } runInAction(() => { this._value = newValue; }); if (this.props.propertyInfo.type === PropertyType.Number) { if ( newValue.trim() === "" && isPropertyOptional( this.props.objects[0], this.props.propertyInfo ) ) { newValue = undefined; } else { newValue = filterNumber(newValue); if (isNaN(newValue)) { return; } } } this.props.updateObject({ [this.props.propertyInfo.name]: newValue }); } onChange = (event: any) => { const target = event.target; if ( this.props.propertyInfo.type === PropertyType.ConfigurationReference ) { if (target.value === "all") { this.changeValue(undefined); } else { this.changeValue([]); } } else if (this.props.propertyInfo.type === PropertyType.Enum) { const id = target.value.toString(); const enumItem = getEnumItems( this.props.objects, this.props.propertyInfo )!.find(enumItem => enumItem.id.toString() === id); this.changeValue(enumItem && enumItem!.id); } else { this.changeValue( target.type === "checkbox" ? target.checked : target.value ); } }; onSelectionChange = ( event: React.SyntheticEvent< HTMLTextAreaElement | HTMLInputElement, Event > ) => { const start = event.currentTarget.selectionStart; const end = event.currentTarget.selectionEnd; if (!(typeof start == "number") || !(typeof end == "number")) { return; } const value = event.currentTarget.value; let expressionStart: number | undefined; for (let i = start; i >= 0; i--) { if ( value[i] == EXPR_MARK_START[0] && value[i + 1] == EXPR_MARK_START[1] ) { expressionStart = i; break; } } if (expressionStart === undefined) { return; } let expressionEnd: number | undefined; for (let i = end; i < value.length; i++) { if ( value[i] == EXPR_MARK_END[1] && value[i - 1] == EXPR_MARK_END[0] ) { expressionEnd = i + 1; break; } } if (expressionEnd === undefined) { return; } const identifier = value.substring( expressionStart + 2, expressionEnd - 2 ); if (identifier.length == 0) { return; } let isIdentifier = false; try { isIdentifier = parseIdentifier(identifier); } catch (err) { return; } if (!isIdentifier) { return; } event.currentTarget.setSelectionRange( expressionStart, expressionEnd, event.currentTarget.selectionDirection ?? undefined ); }; onGenerateGuid = () => { this.changeValue(guid()); }; onKeyDown = (event: React.KeyboardEvent) => { if (event.keyCode === 13) { if (this.props.propertyInfo.type === PropertyType.Number) { try { const newValue = evalConstantExpression( this.context.project, this._value ); if ( typeof newValue.value == "number" && !isNaN(newValue.value) && newValue.value.toString() !== this._value.toString() ) { this.props.updateObject({ [this.props.propertyInfo.name]: newValue.value }); } } catch (err) { console.error(err); } } } }; get displayValue() { const { propertyInfo } = this.props; if (!propertyInfo.displayValue) { return this._value || ""; } let displayValue = propertyInfo.displayValue(this.props.objects[0]); for (let i = 1; i < this.props.objects.length; i++) { if ( displayValue != propertyInfo.displayValue(this.props.objects[i]) ) { return ""; } } return displayValue; } render() { const { propertyInfo, readOnly } = this.props; // if (readOnly && propertyInfo.type != PropertyType.CSS) { // const getPropertyValueAsStringResult = getPropertyValueAsString( // this.props.objects, // propertyInfo // ); // let value = // (getPropertyValueAsStringResult !== undefined // ? getPropertyValueAsStringResult.value // : undefined) || ""; // return ( // // ); // } let isOnSelectAvailable; if (propertyInfo.onSelect) { if (propertyInfo.isOnSelectAvailable) { isOnSelectAvailable = propertyInfo.isOnSelectAvailable( this.props.objects[0] ); } else { isOnSelectAvailable = true; } } else { isOnSelectAvailable = false; } if (propertyInfo.propertyGridRowComponent) { return ( ); } else if (propertyInfo.propertyGridFullRowComponent) { return ( ); } else if (propertyInfo.propertyGridColumnComponent) { return ( ); } else if ( propertyInfo.type === PropertyType.String && (propertyInfo.unique || propertyInfo.uniqueIdentifier) ) { return ( ); } else if (propertyInfo.type === PropertyType.MultilineText) { if (!readOnly && isOnSelectAvailable) { const formText = getFormText(this.props); return (