import React from "react"; import { observable, makeObservable, computed, runInAction, action } from "mobx"; import classNames from "classnames"; import QRC from "../qrcodegen"; import { to16bitsColor } from "eez-studio-shared/color"; import { registerClass, PropertyType, makeDerivedClassInfo, getId, IEezObject, IMessage, PropertyProps, MessageType } from "project-editor/core/object"; import { Message, ProjectStore, getProjectStore, propertyNotFoundMessage, propertyNotSetMessage, propertySetButNotUsedMessage } from "project-editor/store"; import { Project, ProjectType, checkObjectReference, findBitmap, getProject } from "project-editor/project/project"; import type { IDataContext, IFlowContext } from "project-editor/flow/flow-interfaces"; import { FLOW_ITERATOR_INDEXES_VARIABLE } from "project-editor/features/variable/defs"; import { CHECKBOX_CHANGE_EVENT_STRUCT_NAME, RADIO_CHANGE_EVENT_STRUCT_NAME, DROP_DOWN_LIST_CHANGE_EVENT_STRUCT_NAME, SLIDER_CHANGE_EVENT_STRUCT_NAME, SWITCH_CHANGE_EVENT_STRUCT_NAME, TEXT_INPUT_CHANGE_EVENT_STRUCT_NAME, makeCheckboxActionParamsValue, makeRadioActionParamsValue, makeDropDownListActionParamsValue, makeSliderActionParamsValue, makeTextInputActionParamsValue as makeTextInputChangeEventValue } from "project-editor/features/variable/value-type"; import { Widget, makeDataPropertyInfo, ComponentOutput, makeExpressionProperty, makeStylePropertyInfo, migrateStyleProperty, makeAssignableExpressionProperty } from "project-editor/flow/component"; import type { FlowState } from "project-editor/flow/runtime/runtime"; import { generalGroup, specificGroup } from "project-editor/ui-components/PropertyGrid/groups"; import { evalProperty, getAnyValue, getBooleanValue, getTextValue } from "project-editor/flow/helper"; import { Loader } from "eez-studio-ui/loader"; import { Style } from "project-editor/features/style/style"; import { ProjectEditor } from "project-editor/project-editor-interface"; import { getComponentName } from "../../components-registry"; import { observer } from "mobx-react"; import { Button } from "eez-studio-ui/button"; import { Bitmap } from "project-editor/features/bitmap/bitmap"; import { SWITCH_WIDGET_ICON } from "project-editor/ui-components/icons"; import type * as FileTypeModule from "instrument/connection/file-type"; import { WasmRuntime } from "project-editor/flow/runtime/wasm-runtime"; import { isArray } from "eez-studio-shared/util"; //////////////////////////////////////////////////////////////////////////////// export class TextDashboardWidget extends Widget { name: string; static classInfo = makeDerivedClassInfo(Widget.classInfo, { enabledInComponentPalette: (projectType: ProjectType) => projectType === ProjectType.DASHBOARD, label: (widget: TextDashboardWidget) => { let name = getComponentName(widget.type); if (widget.name) { return `${name}: ${widget.name}`; } if (widget.data) { return `${name}: ${widget.data}`; } return name; }, properties: [ { name: "name", type: PropertyType.String, propertyGridGroup: generalGroup }, makeDataPropertyInfo("data", { displayName: "Text" }), makeStylePropertyInfo("style", "Default style") ], beforeLoadHook: (widget: Widget, jsObject: any, project: Project) => { jsObject.type = "TextDashboardWidget"; if (jsObject.text) { if (!jsObject.data) { jsObject.data = `"${jsObject.text}"`; } delete jsObject.text; } }, defaultValue: { left: 0, top: 0, width: 64, height: 32 }, icon: ( ), check: (widget: TextDashboardWidget, messages: IMessage[]) => { const project = ProjectEditor.getProject(widget); if (!project.projectTypeTraits.hasFlowSupport) { if (!widget.data) { messages.push(propertyNotSetMessage(widget, "text")); } } else { if (!widget.data) { messages.push(propertyNotSetMessage(widget, "data")); } } } }); override makeEditable() { super.makeEditable(); makeObservable(this, { name: observable }); } getClassName(flowContext: IFlowContext) { return classNames("eez-widget", this.type); } styleHook(style: React.CSSProperties, flowContext: IFlowContext) { super.styleHook(style, flowContext); if (this.style.alignHorizontalProperty == "left") { style.textAlign = "left"; } else if (this.style.alignHorizontalProperty == "center") { style.textAlign = "center"; } else if (this.style.alignHorizontalProperty == "right") { style.textAlign = "right"; } } override render(flowContext: IFlowContext, width: number, height: number) { const result = getTextValue(flowContext, this, "data", this.name, ""); let text: string; let node: React.ReactNode | null; if (typeof result == "object") { text = result.text; node = result.node; } else { text = result; node = null; } const style: React.CSSProperties = {}; this.styleHook(style, flowContext); return ( <> {node || text} {super.render(flowContext, width, height)} ); } } registerClass("TextDashboardWidget", TextDashboardWidget); //////////////////////////////////////////////////////////////////////////////// export class RectangleDashboardWidget extends Widget { static classInfo = makeDerivedClassInfo(Widget.classInfo, { enabledInComponentPalette: (projectType: ProjectType) => projectType === ProjectType.DASHBOARD, properties: [makeStylePropertyInfo("style", "Default style")], beforeLoadHook: (widget: Widget, jsObject: any, project: Project) => { jsObject.type = "RectangleDashboardWidget"; }, defaultValue: { left: 0, top: 0, width: 64, height: 32 }, icon: ( ), check: (object: RectangleDashboardWidget, messages: IMessage[]) => { if (object.data) { messages.push(propertySetButNotUsedMessage(object, "data")); } } }); override makeEditable() { super.makeEditable(); makeObservable(this, {}); } override render(flowContext: IFlowContext, width: number, height: number) { return <>{super.render(flowContext, width, height)}; } } registerClass("RectangleDashboardWidget", RectangleDashboardWidget); //////////////////////////////////////////////////////////////////////////////// const TextInputWidgetInput = observer( class TextInputWidgetInput extends React.Component<{ value: string; flowContext: IFlowContext; textInputWidget: TextInputWidget; readOnly: boolean; placeholder: string; password: boolean; iterators: number[]; }> { inputElement = React.createRef(); latestFlowValue: any; inputValue: any; constructor(props: any) { super(props); makeObservable(this, { inputValue: observable }); } handleKeyDown = (event: React.KeyboardEvent) => { if (event.key === "Enter") { const flowState = this.props.flowContext.flowState as FlowState; if (flowState && flowState.runtime) { flowState.runtime.executeWidgetAction( this.props.flowContext, this.props.textInputWidget, "ON_CHANGE", makeTextInputChangeEventValue( this.props.flowContext, this.props.value ), `struct:${TEXT_INPUT_CHANGE_EVENT_STRUCT_NAME}` ); } } }; onChange = (event: React.ChangeEvent) => { const { flowContext, textInputWidget, iterators } = this.props; runInAction(() => { this.inputValue = event.target.value; }); const flowState = flowContext.flowState as FlowState; if (flowState) { const value = this.inputValue; if (this.props.textInputWidget.data) { assignProperty( flowState, textInputWidget, "data", value, iterators ); } if (flowState.runtime) { flowState.runtime.executeWidgetAction( flowContext, textInputWidget, "ON_INPUT", makeTextInputChangeEventValue(flowContext, value), `struct:${TEXT_INPUT_CHANGE_EVENT_STRUCT_NAME}` ); } } }; onBlur = () => { const { flowContext, textInputWidget, value } = this.props; const flowState = flowContext.flowState as FlowState; if (flowState && flowState.runtime) { flowState.runtime.executeWidgetAction( flowContext, textInputWidget, "ON_CHANGE", makeTextInputChangeEventValue(flowContext, value), `struct:${TEXT_INPUT_CHANGE_EVENT_STRUCT_NAME}` ); } }; render() { const { value, readOnly, placeholder, password } = this.props; if (value != this.latestFlowValue) { this.latestFlowValue = value; setTimeout( action(() => { this.inputValue = undefined; }) ); } return ( <> ); } } ); export class TextInputWidget extends Widget { static classInfo = makeDerivedClassInfo(Widget.classInfo, { enabledInComponentPalette: (projectType: ProjectType) => projectType === ProjectType.DASHBOARD, componentPaletteGroupName: "!1Input", properties: [ makeDataPropertyInfo("data", { displayName: "Value" }), makeDataPropertyInfo("readOnly"), makeDataPropertyInfo("placehoder"), { name: "password", type: PropertyType.Boolean, propertyGridGroup: specificGroup }, makeStylePropertyInfo("style", "Default style") ], defaultValue: { left: 0, top: 0, width: 160, height: 32, title: "" }, componentDefaultValue: (projectStore: ProjectStore) => { return projectStore.projectTypeTraits.isFirmwareModule || projectStore.projectTypeTraits.isApplet || projectStore.projectTypeTraits.isResource ? { style: { useStyle: "default" } } : projectStore.projectTypeTraits.isFirmware ? { style: { useStyle: "text_input" } } : {}; }, icon: ( ), widgetEvents: { ON_INPUT: { code: 1, paramExpressionType: `struct:${TEXT_INPUT_CHANGE_EVENT_STRUCT_NAME}`, oldName: "action" }, ON_CHANGE: { code: 2, paramExpressionType: `struct:${TEXT_INPUT_CHANGE_EVENT_STRUCT_NAME}`, oldName: "onChange" } } }); readOnly: string; placehoder: string; password: boolean; override makeEditable() { super.makeEditable(); makeObservable(this, { placehoder: observable, password: observable }); } getValue(flowContext: IFlowContext) { if (flowContext.projectStore.projectTypeTraits.hasFlowSupport) { if (this.data) { try { return evalProperty(flowContext, this, "data"); } catch (err) { //console.error(err); } } return ""; } if (this.data) { return flowContext.dataContext.get(this.data) ?? ""; } return ""; } getReadOnly(flowContext: IFlowContext) { if (flowContext.projectStore.projectTypeTraits.hasFlowSupport) { if (this.readOnly) { try { return evalProperty(flowContext, this, "readOnly"); } catch (err) { //console.error(err); } } } return false; } getPlaceholder(flowContext: IFlowContext) { if (flowContext.projectStore.projectTypeTraits.hasFlowSupport) { if (this.placehoder) { try { return evalProperty(flowContext, this, "placehoder"); } catch (err) { //console.error(err); } } return ""; } return ""; } getPassword(flowContext: IFlowContext) { if (flowContext.projectStore.projectTypeTraits.hasFlowSupport) { if (this.password) { try { return evalProperty(flowContext, this, "password"); } catch (err) { //console.error(err); } } return ""; } return ""; } override render( flowContext: IFlowContext, width: number, height: number ): React.ReactNode { let value = this.getValue(flowContext) ?? ""; let readOnly = this.getReadOnly(flowContext) ?? false; let placeholder = this.getPlaceholder(flowContext) ?? ""; const iterators = flowContext.dataContext.get(FLOW_ITERATOR_INDEXES_VARIABLE) || []; return ( <> {super.render(flowContext, width, height)} ); } } registerClass("TextInputWidget", TextInputWidget); //////////////////////////////////////////////////////////////////////////////// class NumberInputDashboardExecutionState { focus?: () => void; } const NumberInputDashboardWidgetElement = observer( class NumberInputDashboardWidgetElement extends React.Component<{ className: string; component: NumberInputDashboardWidget; flowContext: IFlowContext; width: number; height: number; disableDefaultTabHandling: boolean; iterators: number[]; }> { inputElement = React.createRef(); latestFlowValue: any; inputValue: any; constructor(props: any) { super(props); makeObservable(this, { inputValue: observable }); } componentDidMount() { if (this.props.flowContext.flowState && this.inputElement.current) { this.inputElement.current.focus(); } let executionState = this.props.flowContext.flowState?.getComponentExecutionState( this.props.component ); if (executionState) { executionState.focus = () => { this.inputElement.current?.focus(); }; } } render() { const { flowContext, component } = this.props; let value = evalProperty(flowContext, component, "value") ?? 0; let min = evalProperty(flowContext, component, "min") ?? 0; let max = evalProperty(flowContext, component, "max") ?? 100; let step = evalProperty(flowContext, component, "step") ?? 1; if (value != this.latestFlowValue) { this.latestFlowValue = value; setTimeout( action(() => { this.inputValue = undefined; }) ); } return ( { runInAction(() => { this.inputValue = event.target.value; }); const flowState = flowContext.flowState as FlowState; if (flowState) { let value = parseFloat(this.inputValue); if (isNaN(value) || value < min || value > max) { return; } if (component.value) { assignProperty( flowState, component, "value", value, this.props.iterators ); } if (flowState.runtime) { flowState.runtime.executeWidgetAction( flowContext, component, "ON_CHANGE", makeSliderActionParamsValue( flowContext, value ), `struct:${SLIDER_CHANGE_EVENT_STRUCT_NAME}` ); } } }} > ); } } ); export class NumberInputDashboardWidget extends Widget { static classInfo = makeDerivedClassInfo(Widget.classInfo, { enabledInComponentPalette: (projectType: ProjectType) => projectType === ProjectType.DASHBOARD, componentPaletteGroupName: "!1Input", properties: [ makeDataPropertyInfo("data", { hideInPropertyGrid: true, hideInDocumentation: "all" }), makeExpressionProperty( { name: "value", type: PropertyType.MultilineText, propertyGridGroup: specificGroup }, "double" ), makeExpressionProperty( { name: "min", type: PropertyType.MultilineText, propertyGridGroup: specificGroup }, "double" ), makeExpressionProperty( { name: "max", type: PropertyType.MultilineText, propertyGridGroup: specificGroup }, "double" ), makeExpressionProperty( { name: "step", type: PropertyType.MultilineText, propertyGridGroup: specificGroup }, "double" ), { name: "disableDefaultTabHandling", type: PropertyType.Boolean, checkboxStyleSwitch: true, propertyGridGroup: specificGroup }, makeStylePropertyInfo("style", "Default style") ], beforeLoadHook: (widget: Widget, jsObject: any, project: Project) => { if (jsObject.step == undefined) { jsObject.step = "1"; } }, defaultValue: { left: 0, top: 0, width: 180, height: 32, min: "0", max: "100", step: "1" }, icon: ( ), widgetEvents: { ON_CHANGE: { code: 1, paramExpressionType: `struct:${SLIDER_CHANGE_EVENT_STRUCT_NAME}`, oldName: "action" } }, execute: (context: IDashboardComponentContext) => { Widget.classInfo.execute!(context); let executionState = context.getComponentExecutionState(); if (!executionState) { context.setComponentExecutionState( new NumberInputDashboardExecutionState() ); } } }); value: string; min: string; max: string; step: string; disableDefaultTabHandling: boolean; override makeEditable() { super.makeEditable(); makeObservable(this, { value: observable, min: observable, max: observable, step: observable, disableDefaultTabHandling: observable }); } getOutputs(): ComponentOutput[] { return [...super.getOutputs()]; } getClassName(flowContext: IFlowContext) { return classNames("eez-widget", this.type); } override render(flowContext: IFlowContext, width: number, height: number) { const style: React.CSSProperties = {}; this.styleHook(style, flowContext); const iterators = flowContext.dataContext.get(FLOW_ITERATOR_INDEXES_VARIABLE) || []; return ( <> {super.render(flowContext, width, height)} ); } } registerClass("NumberInputDashboardWidget", NumberInputDashboardWidget); //////////////////////////////////////////////////////////////////////////////// export class CheckboxWidget extends Widget { static classInfo = makeDerivedClassInfo(Widget.classInfo, { enabledInComponentPalette: (projectType: ProjectType) => projectType === ProjectType.DASHBOARD, componentPaletteGroupName: "!1Input", properties: [ makeDataPropertyInfo("data", { displayName: "Value" }), makeExpressionProperty( { name: "label", type: PropertyType.MultilineText, propertyGridGroup: specificGroup }, "string" ), makeDataPropertyInfo("enabled"), makeStylePropertyInfo("style", "Default style") ], defaultValue: { left: 0, top: 0, width: 120, height: 20 }, icon: ( ), widgetEvents: { ON_CHANGE: { code: 1, paramExpressionType: `struct:${CHECKBOX_CHANGE_EVENT_STRUCT_NAME}`, oldName: "action" } } }); label: string; enabled?: string; override makeEditable() { super.makeEditable(); makeObservable(this, { label: observable, enabled: observable }); } getOutputs(): ComponentOutput[] { return [...super.getOutputs()]; } getChecked(flowContext: IFlowContext) { if (flowContext.projectStore.projectTypeTraits.hasFlowSupport) { if (this.data) { try { return !!evalProperty(flowContext, this, "data"); } catch (err) { //console.error(err); } } return false; } if (this.data) { return !!flowContext.dataContext.get(this.data); } return false; } getClassName(flowContext: IFlowContext) { return classNames("eez-widget", this.type); } override render( flowContext: IFlowContext, width: number, height: number ): React.ReactNode { let checked = this.getChecked(flowContext); const iterators = flowContext.dataContext.get(FLOW_ITERATOR_INDEXES_VARIABLE) || []; let index = iterators.length > 0 ? iterators[0] : 0; let id = "c-" + guid(); if (index > 0) { id = id + "-" + index; } const style: React.CSSProperties = {}; this.styleHook(style, flowContext); const label = getTextValue( flowContext, this, "label", undefined, this.label ); let isEnabled = getBooleanValue( flowContext, this, "enabled", flowContext.flowState ? !this.enabled : true ); return ( <>
{ const flowState = flowContext.flowState as FlowState; if (flowState) { const value = event.target.checked; if (this.data) { assignProperty( flowState, this, "data", value, iterators ); } if (flowState.runtime) { flowState.runtime.executeWidgetAction( flowContext, this, "ON_CHANGE", makeCheckboxActionParamsValue( flowContext, value ), `struct:${CHECKBOX_CHANGE_EVENT_STRUCT_NAME}` ); } } }} id={id} disabled={!isEnabled} >
{super.render(flowContext, width, height)} ); } } registerClass("CheckboxWidget", CheckboxWidget); //////////////////////////////////////////////////////////////////////////////// export class RadioWidget extends Widget { static classInfo = makeDerivedClassInfo(Widget.classInfo, { enabledInComponentPalette: (projectType: ProjectType) => projectType === ProjectType.DASHBOARD, componentPaletteGroupName: "!1Input", properties: [ makeDataPropertyInfo("data", { hideInPropertyGrid: true }), makeExpressionProperty( { name: "label", type: PropertyType.MultilineText, propertyGridGroup: specificGroup }, "string" ), makeAssignableExpressionProperty( { name: "variable", displayName: "Group variable", type: PropertyType.MultilineText, propertyGridGroup: specificGroup }, "any" ), makeExpressionProperty( { name: "value", type: PropertyType.MultilineText, propertyGridGroup: specificGroup }, "any" ), makeDataPropertyInfo("visible"), makeDataPropertyInfo("enabled"), makeStylePropertyInfo("style", "Default style") ], defaultValue: { left: 0, top: 0, width: 120, height: 20 }, icon: ( ), widgetEvents: { ON_CHANGE: { code: 1, paramExpressionType: `struct:${RADIO_CHANGE_EVENT_STRUCT_NAME}`, oldName: "action" } } }); label: string; variable: string; value: string; enabled?: string; override makeEditable() { super.makeEditable(); makeObservable(this, { label: observable, variable: observable, value: observable, enabled: observable }); } getOutputs(): ComponentOutput[] { return [...super.getOutputs()]; } getClassName(flowContext: IFlowContext) { return classNames("eez-widget", this.type); } override render( flowContext: IFlowContext, width: number, height: number ): React.ReactNode { const label = getTextValue( flowContext, this, "label", undefined, this.label ); let variable = getAnyValue(flowContext, this, "variable", true); let value = getAnyValue(flowContext, this, "value", false); let isEnabled = getBooleanValue( flowContext, this, "enabled", flowContext.flowState ? !this.enabled : true ); const iterators = flowContext.dataContext.get(FLOW_ITERATOR_INDEXES_VARIABLE) || []; let index = iterators.length > 0 ? iterators[0] : 0; let id = "CheckboxWidgetInput-" + getId(this); if (index > 0) { id = id + "-" + index; } const style: React.CSSProperties = {}; this.styleHook(style, flowContext); return ( <>
{ const flowState = flowContext.flowState as FlowState; if (flowState) { assignProperty( flowState, this, "variable", value, iterators ); if (flowState.runtime) { flowState.runtime.executeWidgetAction( flowContext, this, "ON_CHANGE", makeRadioActionParamsValue( flowContext, event.target.checked ), `struct:${RADIO_CHANGE_EVENT_STRUCT_NAME}` ); } } }} id={id} disabled={!isEnabled} >
{super.render(flowContext, width, height)} ); } } registerClass("RadioWidget", RadioWidget); //////////////////////////////////////////////////////////////////////////////// export class SwitchDashboardWidget extends Widget { static classInfo = makeDerivedClassInfo(Widget.classInfo, { enabledInComponentPalette: (projectType: ProjectType) => projectType === ProjectType.DASHBOARD, componentPaletteGroupName: "!1Input", properties: [ makeDataPropertyInfo("data", { displayName: "Value" }), makeDataPropertyInfo("enabled"), makeStylePropertyInfo("style", "Default style") ], defaultValue: { left: 0, top: 0, width: 64, height: 32 }, icon: SWITCH_WIDGET_ICON, widgetEvents: { ON_CHANGE: { code: 1, paramExpressionType: `struct:${SWITCH_CHANGE_EVENT_STRUCT_NAME}`, oldName: "action" } } }); enabled?: string; override makeEditable() { super.makeEditable(); makeObservable(this, { enabled: observable }); } getOutputs(): ComponentOutput[] { return [...super.getOutputs()]; } getChecked(flowContext: IFlowContext) { if (flowContext.projectStore.projectTypeTraits.hasFlowSupport) { if (this.data) { try { return !!evalProperty(flowContext, this, "data"); } catch (err) { //console.error(err); } } return false; } if (this.data) { return !!flowContext.dataContext.get(this.data); } return false; } getClassName(flowContext: IFlowContext) { return classNames("eez-widget", this.type); } override render( flowContext: IFlowContext, width: number, height: number ): React.ReactNode { let checked = this.getChecked(flowContext); const style: React.CSSProperties = {}; this.styleHook(style, flowContext); let isEnabled = getBooleanValue( flowContext, this, "enabled", flowContext.flowState ? !this.enabled : true ); const iterators = flowContext.dataContext.get(FLOW_ITERATOR_INDEXES_VARIABLE) || []; return ( <>
{ const flowState = flowContext.flowState as FlowState; if (flowState) { const value = event.target.checked; if (this.data) { assignProperty( flowState, this, "data", value, iterators ); } if (flowState.runtime) { flowState.runtime.executeWidgetAction( flowContext, this, "ON_CHANGE", makeCheckboxActionParamsValue( flowContext, value ), `struct:${CHECKBOX_CHANGE_EVENT_STRUCT_NAME}` ); } } }} disabled={!isEnabled} >
{super.render(flowContext, width, height)} ); } } registerClass("SwitchDashboardWidget", SwitchDashboardWidget); //////////////////////////////////////////////////////////////////////////////// export class DropDownListDashboardWidget extends Widget { options: string; enabled?: string; static classInfo = makeDerivedClassInfo(Widget.classInfo, { enabledInComponentPalette: (projectType: ProjectType) => projectType === ProjectType.DASHBOARD, componentPaletteGroupName: "!1Input", componentPaletteLabel: "Dropdown", properties: [ makeDataPropertyInfo("data", {}, "integer"), makeDataPropertyInfo("options"), makeDataPropertyInfo("enabled"), makeStylePropertyInfo("style", "Default style") ], beforeLoadHook: ( widget: DropDownListDashboardWidget, jsWidget: Partial ) => { jsWidget.type = "DropDownListDashboardWidget"; }, defaultValue: { left: 0, top: 0, width: 120, height: 32 }, icon: ( ), widgetEvents: { ON_CHANGE: { code: 1, paramExpressionType: `struct:${DROP_DOWN_LIST_CHANGE_EVENT_STRUCT_NAME}`, oldName: "action" } } }); override makeEditable() { super.makeEditable(); makeObservable(this, { options: observable, enabled: observable }); } override render(flowContext: IFlowContext, width: number, height: number) { let options: string[] = evalProperty(flowContext, this, "options"); if (options == undefined || !isArray(options)) { options = []; } let selectedIndex: number = evalProperty(flowContext, this, "data"); let isEnabled = getBooleanValue( flowContext, this, "enabled", flowContext.flowState ? !this.enabled : true ); const iterators = flowContext.dataContext.get(FLOW_ITERATOR_INDEXES_VARIABLE) || []; return ( <> {super.render(flowContext, width, height)} ); } } registerClass("DropDownListDashboardWidget", DropDownListDashboardWidget); //////////////////////////////////////////////////////////////////////////////// export class ProgressDashboardWidget extends Widget { static classInfo = makeDerivedClassInfo(Widget.classInfo, { enabledInComponentPalette: (projectType: ProjectType) => projectType === ProjectType.DASHBOARD, componentPaletteGroupName: "!1Visualiser", properties: [ makeDataPropertyInfo("data", {}, "integer"), makeDataPropertyInfo("min"), makeDataPropertyInfo("max"), { name: "orientation", type: PropertyType.Enum, propertyGridGroup: specificGroup, enumItems: [ { id: "horizontal" }, { id: "vertical" } ] }, makeStylePropertyInfo("style", "Default style") ], beforeLoadHook: ( progressWidget: ProgressDashboardWidget, jsProgressWidget: Partial, project: Project ) => { jsProgressWidget.type = "ProgressDashboardWidget"; if (project.projectTypeTraits.hasFlowSupport) { if (jsProgressWidget.min == undefined) { jsProgressWidget.min = "0"; } if (jsProgressWidget.max == undefined) { jsProgressWidget.max = "100"; } } if (jsProgressWidget.orientation == undefined) { jsProgressWidget.orientation = jsProgressWidget.width! > jsProgressWidget.height! ? "horizontal" : "vertical"; } }, defaultValue: { left: 0, top: 0, width: 128, height: 20 }, icon: ( ) }); min: string; max: string; orientation: string; override makeEditable() { super.makeEditable(); makeObservable(this, { min: observable, max: observable, orientation: observable }); } getPercent(flowContext: IFlowContext) { if (flowContext.projectStore.projectTypeTraits.hasFlowSupport) { if (flowContext.flowState) { try { const min = evalProperty(flowContext, this, "min"); const max = evalProperty(flowContext, this, "max"); let value = evalProperty(flowContext, this, "data"); value = ((value - min) * 100) / (max - min); if (value != null && value != undefined) { return value; } } catch (err) { //console.error(err); } return 0; } return 25; } if (this.data) { const result = flowContext.dataContext.get(this.data); if (result != undefined) { return result; } } return 25; } override render(flowContext: IFlowContext, width: number, height: number) { const percent = this.getPercent(flowContext); let isHorizontal = this.orientation == "horizontal"; return ( <>
{super.render(flowContext, width, height)} ); } } registerClass("ProgressDashboardWidget", ProgressDashboardWidget); //////////////////////////////////////////////////////////////////////////////// export class SpinnerWidget extends Widget { static classInfo = makeDerivedClassInfo(Widget.classInfo, { enabledInComponentPalette: (projectType: ProjectType) => projectType === ProjectType.DASHBOARD, componentPaletteGroupName: "!1Visualiser", properties: [makeStylePropertyInfo("style", "Default style")], defaultValue: { left: 0, top: 0, width: 40, height: 40 }, icon: ( ) }); override makeEditable() { super.makeEditable(); makeObservable(this, {}); } override render( flowContext: IFlowContext, width: number, height: number ): React.ReactNode { return ; } } registerClass("SpinnerWidget", SpinnerWidget); //////////////////////////////////////////////////////////////////////////////// export class QRCodeDashboardWidget extends Widget { errorCorrection: any; constructor() { super(); makeObservable(this, { errorCorrectionValue: computed }); } override makeEditable() { super.makeEditable(); makeObservable(this, { errorCorrection: observable }); } static classInfo = makeDerivedClassInfo(Widget.classInfo, { enabledInComponentPalette: (projectType: ProjectType) => projectType === ProjectType.DASHBOARD, componentPaletteGroupName: "!1Visualiser", properties: [ makeDataPropertyInfo("data", { displayName: "Text" }), { name: "errorCorrection", type: PropertyType.Enum, enumItems: [ { id: "low" }, { id: "medium" }, { id: "quartile" }, { id: "high" } ], propertyGridGroup: specificGroup }, makeStylePropertyInfo("style", "Default style") ], beforeLoadHook: ( widget: QRCodeDashboardWidget, jsWidget: Partial ) => { jsWidget.type = "QRCodeDashboardWidget"; }, defaultValue: { left: 0, top: 0, width: 128, height: 128, errorCorrection: "medium", style: { useStyle: "default", color: "white", backgroundColor: "black" } }, icon: ( ) }); getText(flowContext: IFlowContext) { if (!this.data) { return undefined; } if (flowContext.projectStore.projectTypeTraits.hasFlowSupport) { return evalProperty(flowContext, this, "data"); } return this.data; } get errorCorrectionValue() { if (this.errorCorrection == "low") return QRC.Ecc.LOW; if (this.errorCorrection == "medium") return QRC.Ecc.MEDIUM; if (this.errorCorrection == "quartile") return QRC.Ecc.QUARTILE; return QRC.Ecc.HIGH; } styleHook(style: React.CSSProperties, flowContext: IFlowContext) { super.styleHook(style, flowContext); style.backgroundColor = to16bitsColor( this.style.backgroundColorProperty ); } static toSvgString( qr: any, border: number, lightColor: string, darkColor: string ) { let parts: Array = []; for (let y = 0; y < qr.size; y++) { for (let x = 0; x < qr.size; x++) { if (qr.getModule(x, y)) parts.push(`M${x + border},${y + border}h1v1h-1z`); } } return ( ); } override render(flowContext: IFlowContext, width: number, height: number) { const text = this.getText(flowContext) || ""; const qr0 = QRC.encodeText(text, this.errorCorrectionValue); const svg = QRCodeDashboardWidget.toSvgString( qr0, 1, to16bitsColor(this.style.backgroundColorProperty), to16bitsColor(this.style.colorProperty) ); return ( <> {svg} {super.render(flowContext, width, height)} ); } } registerClass("QRCodeDashboardWidget", QRCodeDashboardWidget); //////////////////////////////////////////////////////////////////////////////// export class ButtonDashboardWidget extends Widget { enabled?: string; disabledStyle: Style; static classInfo = makeDerivedClassInfo(Widget.classInfo, { enabledInComponentPalette: (projectType: ProjectType) => projectType === ProjectType.DASHBOARD, properties: [ makeDataPropertyInfo("data", { displayName: "Label" }), makeDataPropertyInfo("enabled"), makeStylePropertyInfo("style", "Default style"), makeStylePropertyInfo("disabledStyle") ], beforeLoadHook: ( widget: IEezObject, jsObject: any, project: Project ) => { jsObject.type = "ButtonDashboardWidget"; if (jsObject.text) { if (!jsObject.data) { jsObject.data = `"${jsObject.text}"`; } delete jsObject.text; } migrateStyleProperty(jsObject, "disabledStyle"); }, defaultValue: { left: 0, top: 0, width: 80, height: 40, data: `"Button"`, eventHandlers: [ { eventName: "CLICKED", handlerType: "flow" } ] }, icon: ( ), check: (widget: ButtonDashboardWidget, messages: IMessage[]) => { const project = ProjectEditor.getProject(widget); if (!project.projectTypeTraits.hasFlowSupport) { if (!widget.data && !widget.isInputProperty("data")) { messages.push(propertyNotSetMessage(widget, "text")); } checkObjectReference(widget, "enabled", messages, true); } else { if (!widget.data) { messages.push(propertyNotSetMessage(widget, "text")); } } } }); override makeEditable() { super.makeEditable(); makeObservable(this, { enabled: observable, disabledStyle: observable }); } get styles() { return [this.style, this.disabledStyle]; } getClassName(flowContext: IFlowContext) { return classNames("eez-widget", this.type); } override render(flowContext: IFlowContext, width: number, height: number) { const result = getTextValue(flowContext, this, "data", undefined, ""); let text: string; let node: React.ReactNode | null; if (typeof result == "object") { text = result.text; node = result.node; } else { text = result; node = null; } let buttonEnabled = getBooleanValue( flowContext, this, "enabled", flowContext.flowState ? !this.enabled : true ); let buttonStyle = buttonEnabled ? this.style : this.disabledStyle; const style: React.CSSProperties = {}; this.styleHook(style, flowContext); return ( <> {super.render(flowContext, width, height)} ); } } registerClass("ButtonDashboardWidget", ButtonDashboardWidget); //////////////////////////////////////////////////////////////////////////////// const BitmapWidgetPropertyGridUI = observer( class BitmapWidgetPropertyGridUI extends React.Component { get bitmapWidget() { return this.props.objects[0] as BitmapDashboardWidget; } resizeToFitBitmap = () => { getProjectStore(this.props.objects[0]).updateObject( this.props.objects[0], { width: this.bitmapWidget.bitmapObject!.imageElement!.width, height: this.bitmapWidget.bitmapObject!.imageElement!.height } ); }; render() { if (this.props.readOnly) { return null; } if (this.props.objects.length > 1) { return null; } const bitmapObject = this.bitmapWidget.bitmapObject; if (!bitmapObject) { return null; } const imageElement = bitmapObject.imageElement; if (!imageElement) { return null; } const widget = this.props.objects[0] as Widget; if ( widget.width == imageElement.width && widget.height == imageElement.height ) { return null; } return ( ); } } ); //////////////////////////////////////////////////////////////////////////////// export class BitmapDashboardWidget extends Widget { bitmap?: string; constructor() { super(); makeObservable(this, { bitmapObject: computed }); } override makeEditable() { super.makeEditable(); makeObservable(this, { bitmap: observable }); } get label() { return this.bitmap ? `${this.type}: ${this.bitmap}` : this.type; } static classInfo = makeDerivedClassInfo(Widget.classInfo, { enabledInComponentPalette: (projectType: ProjectType) => projectType === ProjectType.DASHBOARD, properties: [ makeDataPropertyInfo("data", {}, "any"), { name: "bitmap", type: PropertyType.ObjectReference, referencedObjectCollectionPath: "bitmaps", propertyGridGroup: specificGroup }, { name: "customUI", type: PropertyType.Any, propertyGridGroup: specificGroup, computed: true, propertyGridRowComponent: BitmapWidgetPropertyGridUI }, makeStylePropertyInfo("style", "Default style") ], beforeLoadHook: (widget: Widget, jsObject: any, project: Project) => { jsObject.type = "BitmapDashboardWidget"; }, defaultValue: { left: 0, top: 0, width: 64, height: 32 }, icon: ( ), check: (object: BitmapDashboardWidget, messages: IMessage[]) => { if (!object.data && !object.bitmap) { messages.push( new Message( MessageType.ERROR, "Either bitmap or data must be set", object ) ); } else { if (object.data && object.bitmap) { messages.push( new Message( MessageType.ERROR, "Both bitmap and data set, only bitmap is used", object ) ); } if (object.bitmap) { let bitmap = findBitmap(getProject(object), object.bitmap); if (!bitmap) { messages.push( propertyNotFoundMessage(object, "bitmap") ); } } } } }); get bitmapObject() { return this.getBitmapObject(getProjectStore(this).dataContext); } getBitmapObject(dataContext: IDataContext) { return this.bitmap ? findBitmap(getProject(this), this.bitmap) : this.data ? findBitmap(getProject(this), dataContext.get(this.data) as string) : undefined; } getBitmap(flowContext: IFlowContext) { if (this.bitmap) { return findBitmap(getProject(this), this.bitmap); } if (this.data) { let data; if (flowContext.flowState) { data = evalProperty(flowContext, this, "data"); } else { data = flowContext.dataContext.get(this.data); } if (typeof data === "string") { if (data.startsWith("data:image/png;base64,")) { return data; } const bitmap = findBitmap(getProject(this), data as string); if (bitmap) { return bitmap; } return undefined; } if (data instanceof Uint8Array) { const { detectFileType } = require("instrument/connection/file-type") as typeof FileTypeModule; const fileType = detectFileType(data); return URL.createObjectURL( new Blob([data], { type: fileType.mime } /* (1) */) ); } if (typeof data == "number") { const runtime = flowContext.flowState?.runtime; if (runtime instanceof WasmRuntime) { const bitmap = findBitmap( getProject(this), runtime.assetsMap.bitmaps[data - 1] ); if (bitmap) { return bitmap; } return undefined; } } return data; } return undefined; } override render(flowContext: IFlowContext, width: number, height: number) { const bitmap = this.getBitmap(flowContext); return ( <> {bitmap ? ( bitmap instanceof Bitmap ? ( ) : ( ) ) : null} {super.render(flowContext, width, height)} ); } } registerClass("BitmapDashboardWidget", BitmapDashboardWidget); //////////////////////////////////////////////////////////////////////////////// const SliderDashboardWidgetElement = observer( class SliderDashboardWidgetElement extends React.Component<{ component: SliderDashboardWidget; flowContext: IFlowContext; width: number; height: number; }> { constructor(props: any) { super(props); makeObservable(this, { currentValue: observable }); } onChangeTimer: any; currentValue: any; render() { const { flowContext, component } = this.props; let value = this.currentValue != undefined ? this.currentValue : evalProperty(flowContext, component, "value") ?? 25; let min = evalProperty(flowContext, component, "min") ?? 0; let max = evalProperty(flowContext, component, "max") ?? 100; let viewMin = evalProperty(flowContext, component, "viewMin") ?? min; let viewMax = evalProperty(flowContext, component, "viewMax") ?? max; let step = evalProperty(flowContext, component, "step") ?? 1; if (min < viewMin) { viewMin = min; } if (max > viewMax) { viewMax = max; } let isEnabled = getBooleanValue( flowContext, component, "enabled", flowContext.flowState ? !component.enabled : true ); const iterators = flowContext.dataContext.get(FLOW_ITERATOR_INDEXES_VARIABLE) || []; return ( { let value = parseFloat(event.target.value); if (value < min) { value = min; } if (value > max) { value = max; } runInAction(() => { this.currentValue = value; }); if (this.onChangeTimer) { return; } this.onChangeTimer = setTimeout(() => { const flowState = flowContext.flowState as FlowState; if (flowState) { if (component.value) { assignProperty( flowState, component, "value", this.currentValue, iterators ); runInAction(() => { this.currentValue = undefined; }); } if (flowState.runtime) { flowState.runtime.executeWidgetAction( flowContext, component, "ON_CHANGE", makeSliderActionParamsValue( flowContext, value ), `struct:${SLIDER_CHANGE_EVENT_STRUCT_NAME}` ); } } this.onChangeTimer = undefined; }, 20); }} disabled={!isEnabled} > ); } } ); export class SliderDashboardWidget extends Widget { static classInfo = makeDerivedClassInfo(Widget.classInfo, { enabledInComponentPalette: (projectType: ProjectType) => projectType === ProjectType.DASHBOARD, componentPaletteGroupName: "!1Input", properties: [ makeDataPropertyInfo("data", { hideInPropertyGrid: true }), makeExpressionProperty( { name: "value", type: PropertyType.MultilineText, propertyGridGroup: specificGroup }, "double" ), makeExpressionProperty( { name: "min", type: PropertyType.MultilineText, propertyGridGroup: specificGroup }, "double" ), makeExpressionProperty( { name: "max", type: PropertyType.MultilineText, propertyGridGroup: specificGroup }, "double" ), makeExpressionProperty( { name: "step", type: PropertyType.MultilineText, propertyGridGroup: specificGroup }, "double" ), makeExpressionProperty( { name: "viewMin", type: PropertyType.MultilineText, propertyGridGroup: specificGroup }, "double" ), makeExpressionProperty( { name: "viewMax", type: PropertyType.MultilineText, propertyGridGroup: specificGroup }, "double" ), makeDataPropertyInfo("enabled"), makeStylePropertyInfo("style", "Default style") ], beforeLoadHook: (widget: Widget, jsObject: any, project: Project) => { if (jsObject.step == undefined) { jsObject.step = "1"; } if (jsObject.viewMin == undefined) { jsObject.viewMin = jsObject.min; } if (jsObject.viewMax == undefined) { jsObject.viewMax = jsObject.max; } }, defaultValue: { left: 0, top: 0, width: 180, height: 20, min: "0", max: "100", step: "1", viewMin: "0", viewMax: "100" }, icon: ( ), widgetEvents: { ON_CHANGE: { code: 1, paramExpressionType: `struct:${SLIDER_CHANGE_EVENT_STRUCT_NAME}`, oldName: "action" } } }); value: string; min: string; max: string; viewMin: string; viewMax: string; enabled?: string; override makeEditable() { super.makeEditable(); makeObservable(this, { value: observable, min: observable, max: observable, viewMin: observable, viewMax: observable, enabled: observable }); } getOutputs(): ComponentOutput[] { return [...super.getOutputs()]; } getClassName(flowContext: IFlowContext) { return classNames("eez-widget", this.type); } override render(flowContext: IFlowContext, width: number, height: number) { return ( <> {super.render(flowContext, width, height)} ); } } registerClass("SliderDashboardWidget", SliderDashboardWidget); //////////////////////////////////////////////////////////////////////////////// import "project-editor/flow/components/widgets/dashboard/eez-chart"; import "project-editor/flow/components/widgets/dashboard/markdown"; import "project-editor/flow/components/widgets/dashboard/plotly"; import "project-editor/flow/components/widgets/dashboard/tabulator"; import "project-editor/flow/components/widgets/dashboard/terminal"; import "project-editor/flow/components/widgets/dashboard/instrument-terminal"; import "project-editor/flow/components/widgets/dashboard/embedded-dashboard"; import { assignProperty } from "project-editor/flow/runtime/worker-dashboard-component-context"; import { guid } from "eez-studio-shared/guid"; import { IDashboardComponentContext } from "eez-studio-types";