import { MenuItem } from "@electron/remote"; import React from "react"; import { observable, computed, makeObservable, toJS, runInAction } from "mobx"; import { observer } from "mobx-react"; import { range } from "lodash"; import * as FlexLayout from "flexlayout-react"; import { Button } from "eez-studio-ui/button"; import { IEezObject, registerClass, PropertyType, makeDerivedClassInfo, isAncestor, getParent, PropertyProps, MessageType, getId, IMessage } from "project-editor/core/object"; import { Message, objectToJS, propertyNotFoundMessage, propertyNotSetMessage, createObject, getChildOfObject, getAncestorOfType, updateObject, getObjectPathAsString } from "project-editor/store"; import { getProjectStore, IContextMenuContext } from "project-editor/store"; import { checkObjectReference, getProject, ProjectType, findPage, findVariable, Project } from "project-editor/project/project"; import type { IFlowContext, IDataContext } from "project-editor/flow/flow-interfaces"; import { ComponentsContainerEnclosure, ComponentEnclosure, ComponentCanvas } from "project-editor/flow/editor/render"; import { FLOW_ITERATOR_INDEXES_VARIABLE, FLOW_ITERATOR_INDEX_VARIABLE } from "project-editor/features/variable/defs"; import { getEnumTypeNameFromVariable, isEnumVariable } from "project-editor/features/variable/value-type"; import * as draw from "project-editor/flow/editor/eez-gui-draw"; import { Widget, makeDataPropertyInfo, ComponentInput, ComponentOutput, makeStylePropertyInfo } from "project-editor/flow/component"; import { EndActionComponent, InputActionComponent, OutputActionComponent, StartActionComponent } from "project-editor/flow/components/actions"; import { Assets, DataBuffer } from "project-editor/build/assets"; import { buildWidget } from "project-editor/build/widgets"; import { WIDGET_TYPE_CONTAINER, WIDGET_TYPE_LIST, WIDGET_TYPE_GRID, WIDGET_TYPE_SELECT, WIDGET_TYPE_USER_WIDGET } from "project-editor/flow/components/component-types"; import { ProjectEditor } from "project-editor/project-editor-interface"; import { generalGroup, layoutGroup, specificGroup } from "project-editor/ui-components/PropertyGrid/groups"; import { getBooleanValue, evalProperty, getStringValue } from "project-editor/flow/helper"; import { USER_WIDGET_ICON } from "project-editor/ui-components/icons"; import { getComponentName } from "project-editor/flow/components/components-registry"; import type { Page } from "project-editor/features/page/page"; import { visitObjects } from "project-editor/core/search"; import { isArray } from "eez-studio-shared/util"; import { FlexLayoutContainer } from "eez-studio-ui/FlexLayout"; import { showDialog } from "eez-studio-ui/dialog"; import { EditorFlowContext } from "project-editor/flow/editor/context"; import type { PageTabState } from "project-editor/features/page/PageEditor"; import { ProjectContext } from "project-editor/project/context"; import { userPropertyValuesProperty, getAdditionalFlowPropertiesForUserProperties, UserPropertyValues } from "project-editor/flow/user-property"; const LIST_TYPE_VERTICAL = 1; const LIST_TYPE_HORIZONTAL = 2; const GRID_FLOW_ROW = 1; const GRID_FLOW_COLUMN = 2; //////////////////////////////////////////////////////////////////////////////// const ContainerWidgetEditLayout = observer( class ContainerWidgetEditLayout extends React.Component { editLayout = () => { const containerWidget = this.props.objects[0] as ContainerWidget; const projectStore = getProjectStore(containerWidget); const page = getAncestorOfType( containerWidget, ProjectEditor.PageClass.classInfo ); if (!page) { return; } const editor = projectStore.editorsStore.getEditorByObject(page!); if (!editor) { return; } const flowContext = new EditorFlowContext(); flowContext.set(editor!.state as PageTabState, { disableUpdateComponentGeometry: true }); let disposed = false; const onSave = () => { updateObject(containerWidget, { dockingLayout: model.toJson() }); projectStore.runtimeSettings.writeDockingManagerContainerLayout( getObjectPathAsString(containerWidget), undefined ); onDispose(); }; const onDispose = () => { if (!disposed) { disposed = true; if (modalDialog) { modalDialog.close(); } } }; const model = containerWidget.getDockingLayoutModel(flowContext); const [modalDialog] = showDialog(
, { jsPanel: { id: "container-widget-edit-layout", title: "Edit Layout", width: page.width, height: page.height, onclosed: onDispose } } ); }; render() { if (this.props.objects.length > 1) { return null; } return (
); } } ); const DockingLayoutTabComponentEnclosure = observer( class DockingLayoutTabComponentEnclosure extends React.Component<{ flowContext: IFlowContext; widget: Widget; }> { width: number = 0; height: number = 0; el: HTMLElement | null = null; requestAnimationFrameId: any; constructor(props: any) { super(props); makeObservable(this, { width: observable, height: observable }); } updateSize = () => { if (!this.el) { let dataFlowObjectId = getId(this.props.widget); const container = document.getElementById( this.props.flowContext.viewState.containerId ); const el = container?.querySelector( `[data-eez-flow-object-id='${dataFlowObjectId}']` ); if (el) { this.el = el.parentElement; } } if (this.el) { const rect = this.el.getBoundingClientRect(); if (rect.width != this.width || rect.height != this.height) { runInAction(() => { this.width = rect.width; this.height = rect.height; }); } } this.requestAnimationFrameId = requestAnimationFrame( this.updateSize ); }; componentDidMount() { this.updateSize(); } componentWillUnmount() { cancelAnimationFrame(this.requestAnimationFrameId); } render() { return ( ); } } ); export class ContainerWidget extends Widget { name?: string; widgets: Widget[]; overlay?: string; shadow?: boolean; layout: "static" | "horizontal" | "vertical" | "docking-manager"; dockingLayout: any; static classInfo = makeDerivedClassInfo(Widget.classInfo, { enabledInComponentPalette: (projectType: ProjectType) => projectType !== ProjectType.LVGL, componentPaletteGroupName: "!1Containers", flowComponentId: WIDGET_TYPE_CONTAINER, label: (widget: ContainerWidget) => { let name = getComponentName(widget.type); if (widget.name) { return widget.name; } return name; }, properties: [ { name: "name", type: PropertyType.String, propertyGridGroup: generalGroup }, { name: "widgets", type: PropertyType.Array, typeClass: Widget, hideInPropertyGrid: true }, makeDataPropertyInfo("data", { hideInPropertyGrid: true, hideInDocumentation: "all" }), makeDataPropertyInfo("overlay", { disabled: (containerWidget: ContainerWidget) => { const project = ProjectEditor.getProject(containerWidget); return ( project.projectTypeTraits.hasFlowSupport || project.projectTypeTraits.isDashboard ); } }), { name: "shadow", type: PropertyType.Boolean, propertyGridGroup: specificGroup, disabled: (containerWidget: ContainerWidget) => { return !containerWidget.overlay; } }, { name: "layout", type: PropertyType.Enum, enumItems: (object: IEezObject) => getProject(object).projectTypeTraits.isDashboard ? [ { id: "static" }, { id: "horizontal" }, { id: "vertical" }, { id: "docking-manager", label: "Docking Manager" } ] : [ { id: "static" }, { id: "horizontal" }, { id: "vertical" } ], propertyGridGroup: layoutGroup }, { name: "dockingLayout", type: PropertyType.Any, propertyGridGroup: layoutGroup, hideInPropertyGrid: true, hideInDocumentation: "all" }, { name: "editLayout", type: PropertyType.Any, propertyGridGroup: layoutGroup, computed: true, propertyGridRowComponent: ContainerWidgetEditLayout, skipSearch: true, hideInPropertyGrid: (widget: ContainerWidget) => { return widget.layout != "docking-manager"; } }, makeStylePropertyInfo("style", "Default style", { hideInDocumentation: "none" }) ], defaultValue: { type: "Container", style: { useStyle: "default" }, widgets: [], layout: "static", left: 0, top: 0, width: 64, height: 32 }, beforeLoadHook: ( widget: ContainerWidget, jsWidget: Partial, project: Project ) => { if (jsWidget.layout == undefined) { jsWidget.layout = "static"; } else if (jsWidget.layout == "docking-manager") { if (!project.projectTypeTraits.isDashboard) { jsWidget.layout = "static"; } } }, icon: ( ), check: (object: ContainerWidget, messages: IMessage[]) => { checkObjectReference(object, "overlay", messages); } }); override makeEditable() { super.makeEditable(); makeObservable(this, { name: observable, widgets: observable, overlay: observable, shadow: observable, layout: observable, dockingLayout: observable }); } getDockingLayoutModel(flowContext: IFlowContext) { let savedDockingLayout; if (flowContext.flowState) { savedDockingLayout = flowContext.projectStore.runtimeSettings.readDockingManagerContainerLayout( getObjectPathAsString(this) ); } const dockingLayout = savedDockingLayout || toJS(this.dockingLayout) || { global: { borderEnableAutoHide: true, splitterSize: 4, splitterExtra: 4, legacyOverflowMenu: false, tabEnableRename: false }, borders: [ /* { type: "border", location: "top", children: [] }, { type: "border", location: "left", children: [] }, { type: "border", location: "right", children: [] }, { type: "border", location: "bottom", children: [] } */ ], layout: { type: "row", children: this.widgets.map((widget, i) => ({ type: "tabset", children: [ { type: "tab", enableClose: false, name: `Child ${i}`, id: `child${i}`, component: `child${i}`, icon: undefined } ] })) } }; const model = FlexLayout.Model.fromJson(dockingLayout); let tabSetNode: FlexLayout.TabSetNode; let tabSetNodeTabIndex = -1; let numTabNodes = 0; model.visitNodes((node, level) => { if (node instanceof FlexLayout.TabSetNode) { if (!tabSetNode) { tabSetNode = node; } } else if (node instanceof FlexLayout.TabNode) { numTabNodes++; const i = parseInt(node.getId().slice("child".length)); if (i > tabSetNodeTabIndex) { tabSetNode = node.getParent() as FlexLayout.TabSetNode; tabSetNodeTabIndex = i; } } }); if (numTabNodes > this.widgets.length) { for (let i = this.widgets.length; i < numTabNodes; i++) { model.doAction(FlexLayout.Actions.deleteTab(`child${i}`)); } } else if (numTabNodes < this.widgets.length) { for (let i = numTabNodes; i < this.widgets.length; i++) { const tab = model.doAction( FlexLayout.Actions.addNode( { type: "tab", enableClose: false, name: `Child ${i}`, id: `child${i}`, component: `child${i}`, icon: undefined }, tabSetNode!.getId(), FlexLayout.DockLocation.BOTTOM, -1, false ) ); tabSetNode = tab!.getParent() as FlexLayout.TabSetNode; } } for (let i = 0; i < this.widgets.length; i++) { const widget = this.widgets[i]; const title = getStringValue( flowContext, widget, "tabTitle", widget.tabTitle ? `{${widget.tabTitle}}` : `Child ${i + 1}` ); model.doAction(FlexLayout.Actions.renameTab(`child${i}`, title)); } return model; } dockingLayoutFactory = (flowContext: IFlowContext) => { return (node: FlexLayout.TabNode) => { var component = node.getComponent(); if (component) { const i = parseInt(component?.slice("child".length)); const widget = this.widgets[i]; return ( ); } return null; }; }; // getClassName(flowContext: IFlowContext) { // return classNames(super.getClassName(flowContext), { // "eez-flow-editor-capture-pointers": this.layout == "docking-manager" // }); // } styleHook(style: React.CSSProperties, flowContext: IFlowContext) { super.styleHook(style, flowContext); if (this.overlay) { if (this.shadow) { style.boxShadow = "1px 1px 8px 1px rgba(0,0,0,0.5)"; } style.opacity = this.style.opacityProperty / 255; } } override render( flowContext: IFlowContext, containerWidth: number, containerHeight: number ): React.ReactNode { let children; if (flowContext.flowState && this.layout == "horizontal") { let offset = 0; children = this.widgets.map((widget, i) => { let left = offset; let top = 0; let width = widget.width; let height = widget.height; if ( !getBooleanValue( flowContext, widget, "visible", !widget.visible ) ) { return null; } offset += width; if ( flowContext.projectStore.runtime && flowContext.projectStore.runtime.isRTL ) { left = containerWidth - (left + width); } return ( ); }); } else if (flowContext.flowState && this.layout == "vertical") { let offset = 0; children = this.widgets.map((widget, i) => { let left = 0; let top = offset; let width = widget.width; let height = widget.height; if ( !getBooleanValue( flowContext, widget, "visible", !widget.visible ) ) { return null; } offset += height; if ( flowContext.projectStore.runtime && flowContext.projectStore.runtime.isRTL ) { left = containerWidth - (left + width); } return ( ); }); } else if (this.layout == "docking-manager") { children = [ { if (flowContext.flowState) { flowContext.projectStore.runtimeSettings.writeDockingManagerContainerLayout( getObjectPathAsString(this), model.toJson() ); } }} /> ]; } else { children = ( ); } const fragment = ( <> {flowContext.projectStore.projectTypeTraits .isDashboard ? null : ( { draw.drawBackground( ctx, 0, 0, containerWidth, containerHeight, this.style, true ); }} /> )} {children} {super.render(flowContext, containerWidth, containerHeight)} ); const onClick = this.onClick(flowContext); if (onClick) { return (
{fragment}
); } else { return fragment; } } buildFlowWidgetSpecific(assets: Assets, dataBuffer: DataBuffer) { // widgets dataBuffer.writeArray(this.widgets, widget => buildWidget(widget, assets, dataBuffer) ); let overlay = assets.getWidgetDataItemIndex(this, "overlay"); // flags let flags = 0; const SHADOW_FLAG = 1 << 0; if (overlay && this.shadow) { flags |= SHADOW_FLAG; } dataBuffer.writeUint16(flags); // overlay dataBuffer.writeInt16(overlay); // layout const CONTAINER_WIDGET_LAYOUT_STATIC = 0; const CONTAINER_WIDGET_LAYOUT_HORIZONTAL = 1; const CONTAINER_WIDGET_LAYOUT_VERTICAL = 2; let layout = CONTAINER_WIDGET_LAYOUT_STATIC; if (this.layout === "horizontal") { layout = CONTAINER_WIDGET_LAYOUT_HORIZONTAL; } else if (this.layout === "vertical") { layout = CONTAINER_WIDGET_LAYOUT_VERTICAL; } dataBuffer.writeUint16(layout); // reserved1 dataBuffer.writeUint16(0); } } //////////////////////////////////////////////////////////////////////////////// export class ListWidget extends Widget { itemWidget?: Widget; listType?: string; gap?: number; static classInfo = makeDerivedClassInfo(Widget.classInfo, { enabledInComponentPalette: (projectType: ProjectType) => projectType !== ProjectType.LVGL, componentPaletteGroupName: "!1Containers", flowComponentId: WIDGET_TYPE_LIST, properties: [ { name: "itemWidget", type: PropertyType.Object, typeClass: Widget, hideInPropertyGrid: true, isOptional: true }, { name: "listType", type: PropertyType.Enum, propertyGridGroup: specificGroup, enumItems: [ { id: "vertical" }, { id: "horizontal" } ] }, { name: "gap", type: PropertyType.Number, propertyGridGroup: specificGroup }, makeStylePropertyInfo("style", "Default style"), makeDataPropertyInfo("data") ], defaultValue: { itemWidget: { type: "Container", widgets: [], left: 0, top: 0, width: 64, height: 32 }, left: 0, top: 0, width: 64, height: 32, listType: "vertical", gap: 0 }, icon: ( ), check: (object: ListWidget, messages: IMessage[]) => { if (!object.data) { messages.push(propertyNotSetMessage(object, "data")); } if (!object.itemWidget) { messages.push( new Message( MessageType.ERROR, "List item widget is missing", object ) ); } } }); override makeEditable() { super.makeEditable(); makeObservable(this, { itemWidget: observable, listType: observable, gap: observable }); } override render(flowContext: IFlowContext, width: number, height: number) { const itemWidget = this.itemWidget; if (!itemWidget) { return null; } let dataValue; if (this.data) { if (flowContext.projectStore.projectTypeTraits.hasFlowSupport) { try { dataValue = evalProperty(flowContext, this, "data"); } catch (err) { //console.error(err); } } else { dataValue = flowContext.dataContext.get(this.data); } } if (!isArray(dataValue)) { dataValue = [{}]; } const iterators = flowContext.dataContext.get(FLOW_ITERATOR_INDEXES_VARIABLE) || []; return ( <> {range(flowContext.flowState ? dataValue.length : 1).map(i => ( ))} {super.render(flowContext, width, height)} ); } buildFlowWidgetSpecific(assets: Assets, dataBuffer: DataBuffer) { // itemWidget const itemWidget = this.itemWidget; if (itemWidget) { dataBuffer.writeObjectOffset(() => buildWidget(itemWidget, assets, dataBuffer) ); } else { dataBuffer.writeUint32(0); } // listType dataBuffer.writeUint8( this.listType === "vertical" ? LIST_TYPE_VERTICAL : LIST_TYPE_HORIZONTAL ); // gap dataBuffer.writeUint8(this.gap || 0); } } const ListWidgetItem = observer( class ListWidgetItem extends React.Component<{ flowContext: IFlowContext; listWidget: ListWidget; itemWidget: Widget; i: number; gap: number; iterators: any; }> { render() { const { flowContext, listWidget, itemWidget, i, gap, iterators } = this.props; let xListItem = 0; let yListItem = 0; if (listWidget.listType === "horizontal") { xListItem += i * (itemWidget.width + gap); } else { yListItem += i * (itemWidget.height + gap); } const overridenFlowContext = flowContext.overrideDataContext({ [FLOW_ITERATOR_INDEX_VARIABLE]: i, [FLOW_ITERATOR_INDEXES_VARIABLE]: [i, ...iterators] }); return ( ); } } ); //////////////////////////////////////////////////////////////////////////////// export class GridWidget extends Widget { itemWidget?: Widget; gridFlow?: string; static classInfo = makeDerivedClassInfo(Widget.classInfo, { enabledInComponentPalette: (projectType: ProjectType) => projectType !== ProjectType.LVGL, componentPaletteGroupName: "!1Containers", flowComponentId: WIDGET_TYPE_GRID, properties: [ { name: "itemWidget", type: PropertyType.Object, typeClass: Widget, hideInPropertyGrid: true, isOptional: true }, { name: "gridFlow", type: PropertyType.Enum, propertyGridGroup: specificGroup, enumItems: [ { id: "row" }, { id: "column" } ] }, makeStylePropertyInfo("style", "Default style"), makeDataPropertyInfo("data") ], defaultValue: { itemWidget: { type: "Container", widgets: [], left: 0, top: 0, width: 32, height: 32, gridFlow: "row" }, left: 0, top: 0, width: 64, height: 64 }, icon: ( ), check: (object: GridWidget, messages: IMessage[]) => { if (!object.data) { messages.push(propertyNotSetMessage(object, "data")); } if (!object.itemWidget) { messages.push( new Message( MessageType.ERROR, "Grid item widget is missing", object ) ); } } }); override makeEditable() { super.makeEditable(); makeObservable(this, { itemWidget: observable, gridFlow: observable }); } override render(flowContext: IFlowContext, width: number, height: number) { const itemWidget = this.itemWidget; if (!itemWidget) { return null; } let dataValue; if (this.data) { if (flowContext.projectStore.projectTypeTraits.hasFlowSupport) { try { dataValue = evalProperty(flowContext, this, "data"); } catch (err) { //console.error(err); } } else { dataValue = flowContext.dataContext.get(this.data); } } if (dataValue == undefined && flowContext.flowState) { return null; } if (!isArray(dataValue)) { dataValue = [{}]; } const iterators = flowContext.dataContext.get(FLOW_ITERATOR_INDEXES_VARIABLE) || []; return ( <> {range(flowContext.flowState ? dataValue.length : 1).map(i => ( ))} {super.render(flowContext, width, height)} ); } buildFlowWidgetSpecific(assets: Assets, dataBuffer: DataBuffer) { // itemWidget const itemWidget = this.itemWidget; if (itemWidget) { dataBuffer.writeObjectOffset(() => buildWidget(itemWidget, assets, dataBuffer) ); } else { dataBuffer.writeUint32(0); } // gridFlow dataBuffer.writeUint8( this.gridFlow === "column" ? GRID_FLOW_COLUMN : GRID_FLOW_ROW ); } } const GridWidgetItem = observer( class GridWidgetItem extends React.Component<{ flowContext: IFlowContext; gridWidget: GridWidget; itemWidget: Widget; i: number; width: number; height: number; iterators: any; }> { render() { const { flowContext, gridWidget, itemWidget, i, width, height, iterators } = this.props; const rows = Math.floor(height / itemWidget.height); const cols = Math.floor(width / itemWidget.width); let row; let col; if (gridWidget.gridFlow === "column") { row = i % rows; col = Math.floor(i / rows); } else { col = i % cols; row = Math.floor(i / cols); } let xListItem = col * itemWidget.width; let yListItem = row * itemWidget.height; const overridenFlowContext = flowContext.overrideDataContext({ [FLOW_ITERATOR_INDEX_VARIABLE]: i, [FLOW_ITERATOR_INDEXES_VARIABLE]: [i, ...iterators] }); return ( ); } } ); //////////////////////////////////////////////////////////////////////////////// export function htmlEncode(value: string) { const el = document.createElement("div"); el.innerText = value; return el.innerHTML; } export class SelectWidget extends Widget { widgets: Widget[]; _lastSelectedIndexInSelectWidget: number | undefined; static classInfo = makeDerivedClassInfo(Widget.classInfo, { enabledInComponentPalette: (projectType: ProjectType) => projectType !== ProjectType.LVGL, componentPaletteGroupName: "!1Containers", flowComponentId: WIDGET_TYPE_SELECT, properties: [ { name: "widgets", type: PropertyType.Array, typeClass: Widget, hideInPropertyGrid: true, childLabel: (childObject: IEezObject, childLabel: string) => { let label; if (getParent(childObject)) { let selectWidgetProperties = getParent( getParent(childObject) ) as SelectWidget; label = selectWidgetProperties.getChildLabel( childObject as Widget ); } if (!label) { label = (getParent(childObject) as IEezObject[]) .indexOf(childObject) .toString(); } return `${label} ➔ ${childLabel}`; }, interceptAddObject: (widgets: Widget[], object: Widget) => { object.left = 0; object.top = 0; object.width = (getParent(widgets) as SelectWidget).width; object.height = (getParent(widgets) as SelectWidget).height; return object; } }, makeStylePropertyInfo("style", "Default style"), makeDataPropertyInfo("data", {}, "boolean") ], defaultValue: { widgets: [], left: 0, top: 0, width: 64, height: 32 }, icon: ( ), check: (object: SelectWidget, messages: IMessage[]) => { if (!object.data) { messages.push(propertyNotSetMessage(object, "data")); } else { let variable = findVariable(getProject(object), object.data); if (variable) { if (variable.type != "integer") { let enumItems: string[] = []; if (isEnumVariable(variable)) { const project = getProject(variable); const enumName = getEnumTypeNameFromVariable(variable); enumItems = enumName ? project.variables.enumsMap .get(enumName) ?.members.map(member => member.name) ?? [] : []; } else if (variable.type == "boolean") { enumItems = ["0", "1"]; } if (enumItems.length > object.widgets.length) { messages.push( new Message( MessageType.ERROR, "Some select children are missing", object ) ); } else if (enumItems.length < object.widgets.length) { messages.push( new Message( MessageType.ERROR, "Too many select children defined", object ) ); } } } } object.widgets.forEach(childObject => { if (childObject.width != object.width) { messages.push( new Message( MessageType.WARNING, "Child of Select widget has different width", childObject ) ); } if (childObject.height != object.height) { messages.push( new Message( MessageType.WARNING, "Child of Select widget has different height", childObject ) ); } }); } }); override makeEditable() { super.makeEditable(); makeObservable(this, { widgets: observable }); } getChildLabel(childObject: Widget) { if (this.widgets) { let index = this.widgets.indexOf(childObject); if (index != -1) { if (this.data) { let variable = findVariable(getProject(this), this.data); if (variable) { if (isEnumVariable(variable)) { let enumItems: string[]; const project = getProject(this); const enumName = getEnumTypeNameFromVariable(variable); enumItems = enumName ? project.variables.enumsMap .get(enumName) ?.members.map(member => member.name) ?? [] : []; if (index < enumItems.length) { let enumItemLabel = htmlEncode( enumItems[index] ); return enumItemLabel; } } else if (variable.type == "boolean") { if (index == 0) { return "0"; } else if (index == 1) { return "1"; } } } } } } return undefined; } getSelectedIndex(flowContext: IFlowContext) { let index: number; if (flowContext.flowState) { try { index = evalProperty(flowContext, this, "data"); if (typeof index === "number") { // pass } else if (typeof index === "boolean") { index = index ? 1 : 0; } else { index = -1; } return index; } catch (err) { //console.error(err); return -1; } } else { const selectedObjects = flowContext.viewState.selectedObjects; for (let i = 0; i < this.widgets.length; ++i) { if ( selectedObjects.find(selectedObject => isAncestor(selectedObject.object, this.widgets[i]) ) ) { this._lastSelectedIndexInSelectWidget = i; return i; } } if ( this._lastSelectedIndexInSelectWidget !== undefined && this._lastSelectedIndexInSelectWidget < this.widgets.length ) { return this._lastSelectedIndexInSelectWidget; } try { index = evalProperty(flowContext, this, "data"); if (typeof index === "number") { // pass } else if (typeof index === "boolean") { index = index ? 1 : 0; } else { index = 0; } return index; } catch (err) { //console.error(err); } if (this.widgets.length > 0) { this._lastSelectedIndexInSelectWidget = 0; return 0; } return -1; } } override render(flowContext: IFlowContext, width: number, height: number) { const index = this.getSelectedIndex(flowContext); let selectedWidget = index >= 0 && index < this.widgets.length ? this.widgets[index] : null; const fragment = ( <> {flowContext.projectStore.projectTypeTraits .isDashboard ? null : ( { draw.drawBackground( ctx, 0, 0, width, height, this.style, true ); }} /> )} {super.render(flowContext, width, height)} ); const onClick = this.onClick(flowContext); if (onClick) { return (
{fragment}
); } else { return fragment; } } buildFlowWidgetSpecific(assets: Assets, dataBuffer: DataBuffer) { // widgets dataBuffer.writeArray(this.widgets, widget => buildWidget(widget, assets, dataBuffer) ); } } //////////////////////////////////////////////////////////////////////////////// const UserWidgetPropertyGridUI = observer( class UserWidgetPropertyGridUI extends React.Component { showUserWidgetPage = () => { (this.props.objects[0] as UserWidgetWidget).open(); }; fitSize = () => { (this.props.objects[0] as UserWidgetWidget).fitSize(); }; render() { if (this.props.objects.length > 1) { return null; } return (
); } } ); export class UserWidgetWidget extends Widget { userWidgetPageName: string; userPropertyValues: UserPropertyValues; context?: string; static classInfo = makeDerivedClassInfo(Widget.classInfo, { enabledInComponentPalette: (projectType: ProjectType) => projectType !== ProjectType.LVGL, componentPaletteGroupName: "!1Containers", flowComponentId: WIDGET_TYPE_USER_WIDGET, properties: [ makeDataPropertyInfo("context"), { name: "userWidgetPageName", displayName: "User widget", type: PropertyType.ObjectReference, propertyGridGroup: specificGroup, referencedObjectCollectionPath: "userWidgets" }, userPropertyValuesProperty, { name: "customUI", type: PropertyType.Any, propertyGridGroup: specificGroup, computed: true, propertyGridRowComponent: UserWidgetPropertyGridUI, skipSearch: true, hideInPropertyGrid: (widget: UserWidgetWidget) => { if (!widget.userWidgetPageName) { return true; } const project = getProject(widget); const userWidgetPage = findPage( project, widget.userWidgetPageName ); if (!userWidgetPage) { return true; } return false; } } ], getAdditionalFlowProperties: getAdditionalFlowPropertiesForUserProperties, beforeLoadHook: ( widget: UserWidgetWidget, jsWidget: Partial ) => { if ((jsWidget as any).layout != undefined) { jsWidget.userWidgetPageName = (jsWidget as any).layout; delete (jsWidget as any).layout; } }, label: (widget: UserWidgetWidget) => { let name = getComponentName(widget.type); if (widget.userWidgetPageName) { return `${name}: ${widget.userWidgetPageName}`; } return name; }, defaultValue: { left: 0, top: 0, width: 64, height: 32 }, icon: USER_WIDGET_ICON, check: (object: UserWidgetWidget, messages: IMessage[]) => { if (!object.data && !object.userWidgetPageName) { messages.push( new Message( MessageType.ERROR, "Either user widget page or data must be set", object ) ); } else { if (object.data && object.userWidgetPageName) { messages.push( new Message( MessageType.ERROR, "Both user widget page and data set, only user widget page is used", object ) ); } if (object.userWidgetPageName) { let userWidgetPage = findPage( getProject(object), object.userWidgetPageName ); if (!userWidgetPage) { messages.push( propertyNotFoundMessage( object, "userWidgetPageName" ) ); } else { if (!userWidgetPage.isUsedAsUserWidget) { messages.push( new Message( MessageType.ERROR, `Page "${userWidgetPage.name}" is not an user widget page`, object ) ); } if (object.isCycleDetected) { messages.push( new Message( MessageType.ERROR, `Cycle detected in user widget page`, getChildOfObject( object, "userWidgetPageName" ) ) ); } } } } checkObjectReference(object, "context", messages); }, open: (object: UserWidgetWidget) => { object.open(); }, extendContextMenu: ( thisObject: Widget, context: IContextMenuContext, objects: IEezObject[], menuItems: Electron.MenuItem[], editable: boolean ): void => { UserWidgetWidget.classInfo.parentClassInfo!.extendContextMenu!( thisObject, context, objects, menuItems, editable ); if (objects.length === 1) { const object = objects[0]; if (object instanceof UserWidgetWidget) { menuItems.push( new MenuItem({ label: "Replace with Container", click: () => { const widget = object.replaceWithContainer(); if (widget) { context.selectObject(widget); } } }) ); } } } }); constructor() { super(); makeObservable(this, { userWidgetPage: computed, isCycleDetected: computed }); } override makeEditable() { super.makeEditable(); makeObservable(this, { userWidgetPageName: observable, userPropertyValues: observable, context: observable }); } get userWidgetPage() { return this.getUserWidgetPage(getProjectStore(this).dataContext); } getUserWidgetPage(dataContext: IDataContext) { let userWidgetPage; const project = getProject(this); if (this.data) { const userWidgetPageName = dataContext.get(this.data); if (userWidgetPageName) { userWidgetPage = findPage(project, userWidgetPageName); } } if (!userWidgetPage) { userWidgetPage = findPage(project, this.userWidgetPageName); } return userWidgetPage; } get isCycleDetected() { const visited = new Set(); function testForCycle(page: Page): boolean { if (visited.has(page)) { return false; } visited.add(page); for (const widget of visitObjects(page)) { if (widget instanceof ProjectEditor.UserWidgetWidgetClass) { if (widget.userWidgetPageName) { const userWidgetPage = findPage( project, widget.userWidgetPageName ); if (userWidgetPage) { if (userWidgetPage === origPage) { return true; } if (testForCycle(userWidgetPage)) { return true; } } } } } return false; } if (!this.userWidgetPageName) { return false; } const project = getProject(this); const userWidgetPage = findPage(project, this.userWidgetPageName); if (!userWidgetPage) { return false; } const origPage = getAncestorOfType( this, ProjectEditor.PageClass.classInfo ) as Page; return testForCycle(userWidgetPage); } getInputs() { const page = findPage(getProject(this), this.userWidgetPageName); if (!page) { return super.getInputs(); } const startComponents: ComponentInput[] = page.components .filter(component => component instanceof StartActionComponent) .map(() => ({ name: "@seqin", type: "null", isSequenceInput: true, isOptionalInput: false })); const inputComponents: ComponentInput[] = page.components .filter(component => component instanceof InputActionComponent) .sort((a, b) => a.top - b.top) .map((inputActionComponent: InputActionComponent) => ({ name: inputActionComponent.objID, displayName: inputActionComponent.name, type: inputActionComponent.inputType, isSequenceInput: false, isOptionalInput: false })); return [...startComponents, ...inputComponents, ...super.getInputs()]; } getOutputs() { const page = findPage(getProject(this), this.userWidgetPageName); if (!page) { return super.getOutputs(); } const endComponents: ComponentOutput[] = page.components .filter(component => component instanceof EndActionComponent) .map(() => ({ name: "@seqout", type: "any", isSequenceOutput: true, isOptionalOutput: true })); const outputComponents: ComponentOutput[] = page.components .filter(component => component instanceof OutputActionComponent) .sort((a, b) => a.top - b.top) .map((outputActionComponent: OutputActionComponent) => ({ name: outputActionComponent.objID, displayName: outputActionComponent.name, type: outputActionComponent.outputType, isSequenceOutput: false, isOptionalOutput: false })); return [...endComponents, ...outputComponents, ...super.getOutputs()]; } override render( flowContext: IFlowContext, width: number, height: number ): React.ReactNode { let element; const userWidgetPage = this.getUserWidgetPage(flowContext.dataContext); if (userWidgetPage && !this.isCycleDetected) { let flowStateExists = true; if (flowContext.flowState) { flowStateExists = !!flowContext.flowState.getFlowStateByComponent(this); } if (flowStateExists) { element = ( ); } } return ( <> {flowContext.projectStore.projectTypeTraits .isDashboard ? null : ( { draw.drawBackground( ctx, 0, 0, width, height, this.style, true ); }} /> )} {element} {super.render(flowContext, width, height)} ); } open() { if (this.userWidgetPage) { getProjectStore(this).navigationStore.showObjects( [this.userWidgetPage], true, false, false ); } } fitSize() { if (this.userWidgetPage) { updateObject(this, { width: this.userWidgetPage.rect.width, height: this.userWidgetPage.rect.height }); } } replaceWithContainer() { if (this.userWidgetPage) { var containerWidgetJsObject: Partial = Object.assign({}, ContainerWidget.classInfo.defaultValue); containerWidgetJsObject.widgets = this.userWidgetPage.components.map(widget => objectToJS(widget) ); containerWidgetJsObject.left = this.left; containerWidgetJsObject.top = this.top; containerWidgetJsObject.width = this.width; containerWidgetJsObject.height = this.height; const projectStore = getProjectStore(this); return projectStore.replaceObject( getParent(this), this, createObject( projectStore, containerWidgetJsObject, ContainerWidget ) ); } return undefined; } buildFlowWidgetSpecific(assets: Assets, dataBuffer: DataBuffer) { // layout let userWidgetPage: number = 0; if (this.userWidgetPageName) { userWidgetPage = assets.getPageIndex(this, "userWidgetPageName"); } dataBuffer.writeInt16(userWidgetPage); // context dataBuffer.writeInt16(assets.getWidgetDataItemIndex(this, "context")); // component index dataBuffer.writeUint16(assets.getComponentIndex(this)); } buildFlowComponentSpecific(assets: Assets, dataBuffer: DataBuffer) { const userWidgetPage = this.userWidgetPage; if (userWidgetPage) { // flowIndex const flowIndex = assets.flows.indexOf(userWidgetPage); dataBuffer.writeInt16(flowIndex); // inputsStartIndex if (userWidgetPage.inputComponents.length > 0) { dataBuffer.writeUint8( this.buildInputs.findIndex( input => input.name == userWidgetPage.inputComponents[0].objID ) ); } else { dataBuffer.writeUint8(1); } // outputsStartIndex if (userWidgetPage.outputComponents.length > 0) { dataBuffer.writeUint8( this.buildOutputs.findIndex( output => output.name == userWidgetPage.outputComponents[0].objID ) ); } else { dataBuffer.writeUint8(0); } } else { // flowIndex dataBuffer.writeInt16(-1); // inputsStartIndex dataBuffer.writeUint8(0); // outputsStartIndex dataBuffer.writeUint8(0); } } } registerClass("ContainerWidget", ContainerWidget); registerClass("ListWidget", ListWidget); registerClass("GridWidget", GridWidget); registerClass("SelectWidget", SelectWidget); registerClass("UserWidgetWidget", UserWidgetWidget);