From 05377dae743be1dabc76fb9008f6074659ea44c9 Mon Sep 17 00:00:00 2001 From: Martin Vladic Date: Sun, 8 Dec 2024 13:06:06 +0100 Subject: [PATCH] Implemented: Adding widgets and actions using context menu #659 --- .../_stylesheets/project-editor.less | 29 ++ packages/eez-studio-ui/dialog.tsx | 1 - packages/home/settings.tsx | 22 + packages/home/tabs-store.tsx | 8 + packages/main/menu.ts | 13 + packages/main/settings.ts | 33 +- packages/project-editor/core/objectAdapter.ts | 35 +- .../features/changes/flow-viewer.tsx | 11 + .../features/page/PagesNavigation.tsx | 7 +- .../features/style/StylesTreeNavigation.tsx | 2 +- .../project-editor/features/style/style.tsx | 1 - .../flow/editor/ComponentsPalette.tsx | 266 ++++++++-- .../project-editor/flow/editor/editor.tsx | 3 +- .../flow/editor/flow-document.tsx | 454 +++++++++++++----- .../flow/editor/mouse-handler.tsx | 12 + .../project-editor/flow/flow-interfaces.ts | 10 + .../flow/runtime-viewer/flow-document.tsx | 11 + .../lvgl/LVGLStylesTreeNavigation.tsx | 2 +- .../project-editor/lvgl/widgets/Container.tsx | 4 +- .../project-editor/lvgl/widgets/Panel.tsx | 4 +- .../project-editor/project-editor-create.tsx | 4 +- .../project-editor-interface.tsx | 2 + packages/project-editor/project/project.tsx | 28 ++ .../project/ui/SettingsNavigation.tsx | 24 +- packages/project-editor/store/editor.ts | 2 +- packages/project-editor/store/helper.ts | 35 +- .../project-editor/store/layout-models.tsx | 25 +- .../ui-components/ListNavigation.tsx | 7 +- .../ui-components/PropertyGrid/index.tsx | 7 +- 29 files changed, 872 insertions(+), 190 deletions(-) diff --git a/packages/eez-studio-ui/_stylesheets/project-editor.less b/packages/eez-studio-ui/_stylesheets/project-editor.less index cb5213b71..207bf791a 100644 --- a/packages/eez-studio-ui/_stylesheets/project-editor.less +++ b/packages/eez-studio-ui/_stylesheets/project-editor.less @@ -293,6 +293,9 @@ margin: 2px; padding: 4px; cursor: grab; + &[draggable="false"] { + cursor: pointer; + } display: flex; flex-direction: row; align-items: center; @@ -523,7 +526,13 @@ } .EezStudio_PropertiesPanel_Body { + height: 100%; overflow: auto; + + .EezStudio_PropertyGrid_NothingSelected { + height: 100%; + background-color: @panelHeaderColor; + } } } @@ -3824,3 +3833,23 @@ white-space: nowrap; border-radius: 4px; } + +.EezStudio_ProjectEditor_SelectComponentDialog { + .modal-content { + width: 530px; + max-height: calc(100vh - 200px); + .modal-body { + padding: 0; + overflow: hidden; + display: flex; + .EezStudio_ComponentsPalette_Enclosure { + height: unset; + } + } + } +} + +.EezStudio_PageStructure_NoPageSelected { + height: 100%; + background-color: @panelHeaderColor; +} diff --git a/packages/eez-studio-ui/dialog.tsx b/packages/eez-studio-ui/dialog.tsx index aeb61d6d6..13a2824e4 100644 --- a/packages/eez-studio-ui/dialog.tsx +++ b/packages/eez-studio-ui/dialog.tsx @@ -317,7 +317,6 @@ export const BootstrapDialog = observer( additionalFooterControl?: React.ReactNode; backdrop?: "static" | boolean; className?: string; - modalContentStyle?: React.CSSProperties; }> { div: HTMLDivElement | null = null; form: HTMLFormElement | null = null; diff --git a/packages/home/settings.tsx b/packages/home/settings.tsx index f23e0259e..4df639212 100644 --- a/packages/home/settings.tsx +++ b/packages/home/settings.tsx @@ -114,6 +114,16 @@ ipcRenderer.on("mru-changed", async (sender: any, mru: IMruItem[]) => { //////////////////////////////////////////////////////////////////////////////// +const getShowComponentsPaletteInProjectEditor = function () { + return ipcRenderer.sendSync("getShowComponentsPaletteInProjectEditor"); +}; + +const setShowComponentsPaletteInProjectEditor = function (value: boolean) { + ipcRenderer.send("setShowComponentsPaletteInProjectEditor", value); +}; + +//////////////////////////////////////////////////////////////////////////////// + class SettingsController { activetLocale = getLocale(); activeDateFormat = getDateFormat(); @@ -130,6 +140,9 @@ class SettingsController { pythonUseCustomPath: boolean = false; pythonCustomPath: string = ""; + _showComponentsPaletteInProjectEditor: boolean = + getShowComponentsPaletteInProjectEditor(); + constructor() { this.pythonUseCustomPath = window.localStorage.getItem("pythonUseCustomPath") == "1" @@ -397,6 +410,15 @@ class SettingsController { } showDialog(); }; + + get showComponentsPaletteInProjectEditor() { + return this._showComponentsPaletteInProjectEditor; + } + + set showComponentsPaletteInProjectEditor(value: boolean) { + this._showComponentsPaletteInProjectEditor = value; + setShowComponentsPaletteInProjectEditor(value); + } } export const settingsController = new SettingsController(); diff --git a/packages/home/tabs-store.tsx b/packages/home/tabs-store.tsx index a1edb5f64..2d90ea9d9 100644 --- a/packages/home/tabs-store.tsx +++ b/packages/home/tabs-store.tsx @@ -622,6 +622,9 @@ export class ProjectEditorTab implements IHomeTab { projectStore.navigationStore.selectedPanel.selectAll(); } }; + const onToggleComponentsPalette = () => { + projectStore.layoutModels.toggleComponentsPalette(); + }; const onResetLayoutModels = () => { if (!this.runMode) { projectStore.layoutModels.reset(); @@ -674,6 +677,7 @@ export class ProjectEditorTab implements IHomeTab { ipcRenderer.on("delete", deleteSelection); ipcRenderer.on("select-all", selectAll); + ipcRenderer.on("toggleComponentsPalette", onToggleComponentsPalette); ipcRenderer.on("resetLayoutModels", onResetLayoutModels); ipcRenderer.on("reload-project", onReloadProject); @@ -698,6 +702,10 @@ export class ProjectEditorTab implements IHomeTab { ipcRenderer.removeListener("delete", deleteSelection); ipcRenderer.removeListener("select-all", selectAll); + ipcRenderer.removeListener( + "toggleComponentsPalette", + onToggleComponentsPalette + ); ipcRenderer.removeListener( "resetLayoutModels", onResetLayoutModels diff --git a/packages/main/menu.ts b/packages/main/menu.ts index 5ba808d29..c9978aec0 100644 --- a/packages/main/menu.ts +++ b/packages/main/menu.ts @@ -728,6 +728,19 @@ function buildViewMenu(win: IWindow | undefined) { type: "separator" }); + viewSubmenu.push({ + label: settings.showComponentsPaletteInProjectEditor + ? "Hide Components Palette" + : "Show Components Palette", + click: function (item) { + if (win) { + win.browserWindow.webContents.send( + "toggleComponentsPalette" + ); + } + } + }); + viewSubmenu.push({ label: "Reset Layout", click: function (item) { diff --git a/packages/main/settings.ts b/packages/main/settings.ts index 96a6631fb..919e7bb1b 100644 --- a/packages/main/settings.ts +++ b/packages/main/settings.ts @@ -59,6 +59,8 @@ class Settings { _loaded: boolean = false; + showComponentsPaletteInProjectEditor: boolean = true; + async loadSettings() { if (this._loaded) { return; @@ -89,7 +91,8 @@ class Settings { locale: observable, dateFormat: observable, timeFormat: observable, - isDarkTheme: observable + isDarkTheme: observable, + showComponentsPaletteInProjectEditor: observable }); reaction( @@ -182,6 +185,11 @@ class Settings { if (settingsJs.isDarkTheme != undefined) { this.isDarkTheme = settingsJs.isDarkTheme; } + + if (settingsJs.showComponentsPaletteInProjectEditor != undefined) { + this.showComponentsPaletteInProjectEditor = + settingsJs.showComponentsPaletteInProjectEditor; + } } } @@ -467,3 +475,26 @@ ipcMain.on("setIsDarkTheme", function (event: any, value: boolean) { }); //////////////////////////////////////////////////////////////////////////////// + +function getShowComponentsPaletteInProjectEditor() { + return settings.showComponentsPaletteInProjectEditor; +} + +function setShowComponentsPaletteInProjectEditor(value: boolean) { + runInAction(() => { + settings.showComponentsPaletteInProjectEditor = value; + }); +} + +ipcMain.on("getShowComponentsPaletteInProjectEditor", function (event: any) { + event.returnValue = getShowComponentsPaletteInProjectEditor(); +}); + +ipcMain.on( + "setShowComponentsPaletteInProjectEditor", + function (event: any, value: boolean) { + setShowComponentsPaletteInProjectEditor(value); + } +); + +//////////////////////////////////////////////////////////////////////////////// diff --git a/packages/project-editor/core/objectAdapter.ts b/packages/project-editor/core/objectAdapter.ts index 97acd983d..239690984 100644 --- a/packages/project-editor/core/objectAdapter.ts +++ b/packages/project-editor/core/objectAdapter.ts @@ -12,7 +12,7 @@ import { createTransformer } from "mobx-utils"; import { map, find, each, pickBy } from "lodash"; import { stringCompare } from "eez-studio-shared/string"; -import { Rect } from "eez-studio-shared/geometry"; +import { Point, Rect } from "eez-studio-shared/geometry"; import { getProperty, @@ -24,7 +24,8 @@ import { getParent, getId, EezObject, - setKey + setKey, + getKey } from "project-editor/core/object"; import { @@ -54,7 +55,9 @@ import { addItem, isObjectReferencable, canContain, - getProjectEditorDataFromClipboard + getProjectEditorDataFromClipboard, + getAncestorOfType, + getAddItemName } from "project-editor/store"; import { DragAndDropManager } from "project-editor/core/dd"; @@ -555,6 +558,7 @@ export class TreeObjectAdapter { add?: boolean; pasteSelection?: () => void; duplicateSelection?: () => void; + atPoint?: Point; }, editable?: boolean, additionalMenuItems?: Electron.MenuItem[] @@ -594,12 +598,13 @@ export class TreeObjectAdapter { if ( editable && parentObject && + !(parentObject instanceof ProjectEditor.FlowClass) && canAdd(parentObject) && !(actions?.add === false) ) { menuItems.push( new MenuItem({ - label: "Add", + label: `Add ${getAddItemName(parentObject)}...`, click: async () => { const aNewObject = await addItem(parentObject!); if (aNewObject) { @@ -757,7 +762,29 @@ export class TreeObjectAdapter { menuItems = menuItems.concat(additionalMenuItems); } + console.log("selectedObject", selectedObject); + console.log("parentObject", parentObject); + if ( + editable && + getAncestorOfType( + selectedObject || parentObject, + ProjectEditor.FlowClass.classInfo + ) && + getKey(parentObject) != "localVariables" + ) { + ProjectEditor.newComponentMenuItem( + selectedObject || parentObject, + menuItems, + actions?.atPoint + ); + } + if (menuItems.length > 0) { + // remove separator at the end + if (menuItems[menuItems.length - 1].type == "separator") { + menuItems.splice(menuItems.length - 1, 1); + } + const menu = new Menu(); menuItems.forEach(menuItem => menu.append(menuItem)); return menu; diff --git a/packages/project-editor/features/changes/flow-viewer.tsx b/packages/project-editor/features/changes/flow-viewer.tsx index 6506fef06..32dc04283 100644 --- a/packages/project-editor/features/changes/flow-viewer.tsx +++ b/packages/project-editor/features/changes/flow-viewer.tsx @@ -773,6 +773,17 @@ class FlowDocument implements IDocument { targetObjectId: string, connectionInput: string ) {} + + connectToNewTarget( + sourceObjectId: string, + connectionOutput: string, + atPoint: Point + ) {} + connectToNewSource( + targetObjectId: string, + connectionInput: string, + atPoint: Point + ) {} } //////////////////////////////////////////////////////////////////////////////// diff --git a/packages/project-editor/features/page/PagesNavigation.tsx b/packages/project-editor/features/page/PagesNavigation.tsx index 9858d7f58..80703e4c4 100644 --- a/packages/project-editor/features/page/PagesNavigation.tsx +++ b/packages/project-editor/features/page/PagesNavigation.tsx @@ -428,7 +428,12 @@ export const PageStructure = observer( /> - ) : null; + ) : ( +
e.preventDefault()} + >
+ ); } } ); diff --git a/packages/project-editor/features/style/StylesTreeNavigation.tsx b/packages/project-editor/features/style/StylesTreeNavigation.tsx index 5f0cd3022..4152bbbf9 100644 --- a/packages/project-editor/features/style/StylesTreeNavigation.tsx +++ b/packages/project-editor/features/style/StylesTreeNavigation.tsx @@ -71,7 +71,7 @@ const AddButton = observer( render() { return ( (resolve => { + const onOk = (value: Component) => { + resolve(value); + }; + + const onCancel = () => { + resolve(null); + }; + + showDialog( + + ); + }); +} + +//////////////////////////////////////////////////////////////////////////////// + +const SelectComponentDialog = observer( + class SelectComponentDialog extends React.Component<{ + projectStore: ProjectStore; + type: "actions" | "widgets"; + onOk: (value: Component) => void; + onCancel: () => void; + }> { + open: boolean = true; + + constructor(props: any) { + super(props); + + makeObservable(this, { + open: observable + }); + } + + render() { + return ( + + + { + runInAction(() => { + this.open = false; + }); + this.props.onOk(component); + }} + /> + + + ); + } + } +); + +//////////////////////////////////////////////////////////////////////////////// + +export function newComponentMenuItem( + object: IEezObject, + menuItems: Electron.MenuItem[], + atPoint?: Point +) { + const flow = getAncestorOfType(object, ProjectEditor.FlowClass.classInfo); + if (flow) { + const isPage = flow instanceof ProjectEditor.PageClass; + const isAction = flow instanceof ProjectEditor.ActionClass; + + if (isPage || isAction) { + let type: "actions" | "widgets" = "actions"; + + if ( + (isPage && + (!atPoint || + (atPoint.x >= 0 && + atPoint.x < flow.width && + atPoint.y >= 0 && + atPoint.y < flow.height))) || + object instanceof ProjectEditor.WidgetClass + ) { + type = "widgets"; + } + + menuItems.unshift(new MenuItem({ type: "separator" })); + + menuItems.unshift( + new MenuItem({ + label: `Add ${type == "actions" ? "Action" : "Widget"}...`, + click: async () => { + const projectStore = + ProjectEditor.getProjectStore(object); + + const component = await selectComponentDialog( + projectStore, + type + ); + + if (component) { + if (atPoint) { + component.left = Math.round(atPoint.x); + component.top = Math.round(atPoint.y); + } + + let parent; + + let selectedWidget = object; + if ( + selectedWidget instanceof + ProjectEditor.WidgetClass + ) { + const pastePlace = findPastePlaceInside( + selectedWidget, + getClassInfo(component), + true + ); + if (isArray(pastePlace)) { + parent = pastePlace; + if (atPoint) { + component.left -= selectedWidget.left; + component.top -= selectedWidget.top; + } + } + } + + let newObject = projectStore.addObject( + parent || flow.components, + component + ); + + projectStore.navigationStore.showObjects( + [newObject], + true, + true, + true + ); + } + } + }) + ); + } + } +} + +//////////////////////////////////////////////////////////////////////////////// export const ComponentsPalette = observer( class ComponentsPalette extends React.Component { @@ -85,6 +259,7 @@ export const ComponentsPalette = observer( export const ComponentsPalette1 = observer( class ComponentsPalette1 extends React.Component<{ type: "widgets" | "actions"; + onSelectComponent?: (value: Component) => void; }> { static contextType = ProjectContext; declare context: React.ContextType; @@ -110,24 +285,28 @@ export const ComponentsPalette1 = observer( readFromLocalStorage: action }); - this.dispose = reaction( - () => ({ - searchText: this.searchText - }), - arg => { - localStorage.setItem( - "ComponentsPaletteSearchText_" + this.props.type, - arg.searchText - ); - } - ); + if (!this.props.onSelectComponent) { + this.dispose = reaction( + () => ({ + searchText: this.searchText + }), + arg => { + localStorage.setItem( + "ComponentsPaletteSearchText_" + this.props.type, + arg.searchText + ); + } + ); + } } readFromLocalStorage() { - this.searchText = - localStorage.getItem( - "ComponentsPaletteSearchText_" + this.props.type - ) || ""; + if (!this.props.onSelectComponent) { + this.searchText = + localStorage.getItem( + "ComponentsPaletteSearchText_" + this.props.type + ) || ""; + } } componentDidUpdate() { @@ -135,7 +314,9 @@ export const ComponentsPalette1 = observer( } componentWillUnmount() { - this.dispose(); + if (this.dispose) { + this.dispose(); + } } onSelect(widgetClass: IObjectClassInfo | undefined) { @@ -191,6 +372,7 @@ export const ComponentsPalette1 = observer( this.selectedComponentClass } onSelect={this.onSelect} + onSelectComponent={this.props.onSelectComponent} > ))} @@ -207,6 +389,7 @@ class PaletteGroup extends React.Component<{ componentClasses: IObjectClassInfo[]; selectedComponentClass: IObjectClassInfo | undefined; onSelect: (componentClass: IObjectClassInfo | undefined) => void; + onSelectComponent?: (value: Component) => void; }> { render() { let name = getComponentGroupDisplayName(this.props.name); @@ -230,6 +413,9 @@ class PaletteGroup extends React.Component<{ componentClass === this.props.selectedComponentClass } + onSelectComponent={ + this.props.onSelectComponent + } /> ); })} @@ -247,15 +433,12 @@ const PaletteItem = observer( componentClass: IObjectClassInfo; selected: boolean; onSelect: (componentClass: IObjectClassInfo | undefined) => void; + onSelectComponent?: (value: Component) => void; }> { static contextType = ProjectContext; declare context: React.ContextType; - constructor(props: { - componentClass: IObjectClassInfo; - selected: boolean; - onSelect: (componentClass: IObjectClassInfo | undefined) => void; - }) { + constructor(props: any) { super(props); makeObservable(this, { @@ -264,9 +447,7 @@ const PaletteItem = observer( }); } - onDragStart(event: React.DragEvent) { - event.stopPropagation(); - + get component() { let protoObject = new this.props.componentClass.objectClass(); const componentClass = getClass(protoObject); @@ -315,6 +496,14 @@ const PaletteItem = observer( object.height = 0; } + return object; + } + + onDragStart(event: React.DragEvent) { + event.stopPropagation(); + + const object = this.component; + setClipboardData( event, objectToClipboardData(this.context, object) @@ -373,6 +562,7 @@ const PaletteItem = observer( let className = classNames("eez-component-palette-item", { selected: this.props.selected, + "no-drag": this.props.onSelectComponent != undefined, dragging }); @@ -384,12 +574,24 @@ const PaletteItem = observer( return (
- this.props.onSelect(this.props.componentClass) + onClick={() => { + if (this.props.onSelectComponent) { + this.props.onSelectComponent(this.component); + } else { + this.props.onSelect(this.props.componentClass); + } + }} + draggable={!this.props.onSelectComponent} + onDragStart={ + this.props.onSelectComponent + ? undefined + : this.onDragStart + } + onDragEnd={ + this.props.onSelectComponent + ? undefined + : this.onDragEnd } - draggable={true} - onDragStart={this.onDragStart} - onDragEnd={this.onDragEnd} style={titleStyle} > {typeof icon === "string" ? : icon} diff --git a/packages/project-editor/flow/editor/editor.tsx b/packages/project-editor/flow/editor/editor.tsx index d404ad2f0..01acc3750 100644 --- a/packages/project-editor/flow/editor/editor.tsx +++ b/packages/project-editor/flow/editor/editor.tsx @@ -774,7 +774,8 @@ export const Canvas = observer( setTimeout(() => { const menu = context.document.createContextMenu( - context.viewState.selectedObjects + context.viewState.selectedObjects, + { atPoint: point } ); if (menu) { if (this.mouseHandler) { diff --git a/packages/project-editor/flow/editor/flow-document.tsx b/packages/project-editor/flow/editor/flow-document.tsx index d8c041277..1097d0836 100644 --- a/packages/project-editor/flow/editor/flow-document.tsx +++ b/packages/project-editor/flow/editor/flow-document.tsx @@ -10,7 +10,7 @@ import { getObjectIdsInsideRect, getSelectedObjectsBoundingRect } from "project-editor/flow/editor/bounding-rects"; -import { IEezObject, getParent } from "project-editor/core/object"; +import { IEezObject, getId, getParent } from "project-editor/core/object"; import { createObject, getAncestorOfType, @@ -24,6 +24,11 @@ import { ProjectEditor } from "project-editor/project-editor-interface"; import type { Page } from "project-editor/features/page/page"; import { canPasteWithDependencies } from "project-editor/store/paste-with-dependencies"; import type { PageTabState } from "project-editor/features/page/PageEditor"; +import { selectComponentDialog } from "project-editor/flow/editor/ComponentsPalette"; +import { + IsTrueActionComponent, + LogActionComponent +} from "../components/actions"; export class FlowDocument implements IDocument { constructor( @@ -154,121 +159,133 @@ export class FlowDocument implements IDocument { return maxLengthGroup ? maxLengthGroup : []; } - createContextMenu(objects: TreeObjectAdapter[]) { + createContextMenu( + objects: TreeObjectAdapter[], + options?: { + atPoint?: Point; + } + ) { + const flow = this.flow.object; + const isPage = flow instanceof ProjectEditor.PageClass; + + let additionalMenuItems: Electron.MenuItem[] = []; + + if (isPage && objects.length == 0) { + additionalMenuItems.push( + new MenuItem({ + label: "Center View", + click: async () => { + this.flowContext.viewState.centerView(); + } + }) + ); + + additionalMenuItems.push( + new MenuItem({ + label: "Center View on All Pages", + click: async () => { + this.flowContext.viewState.centerView(); + + for (const page of this.projectStore.project.pages) { + if (page != this.flow.object) { + const editor = + this.projectStore.editorsStore.getEditorByObject( + page + ); + if (editor?.state) { + const pageTabState = + editor.state as PageTabState; + + pageTabState.centerView(); + } else { + let uiState = + this.projectStore.uiStateStore.getObjectUIState( + page, + "flow-state" + ); + + if (!uiState) { + uiState = {}; + } + + uiState.transform = { + translate: { + x: this.flowContext.viewState + .transform.translate.x, + y: this.flowContext.viewState + .transform.translate.y + }, + scale: + uiState.transform?.scale ?? + this.flowContext.viewState.transform + .scale + }; + + runInAction(() => { + this.projectStore.uiStateStore.updateObjectUIState( + page, + "flow-state", + uiState + ); + }); + } + } + } + } + }) + ); + + if (!this.projectStore.uiStateStore.globalFlowZoom) { + additionalMenuItems.push( + new MenuItem({ + label: "Set the Same Zoom for All Pages", + click: async () => { + for (const page of this.projectStore.project + .pages) { + if (page != this.flow.object) { + let uiState = + this.projectStore.uiStateStore.getObjectUIState( + page, + "flow-state" + ); + + if (!uiState) { + uiState = {}; + } + + uiState.transform = { + translate: { + x: this.flowContext.viewState + .transform.translate.x, + y: this.flowContext.viewState + .transform.translate.y + }, + scale: this.flowContext.viewState + .transform.scale + }; + + runInAction(() => { + this.projectStore.uiStateStore.updateObjectUIState( + page, + "flow-state", + uiState + ); + }); + } + } + } + }) + ); + } + } + return this.flow.createSelectionContextMenu( { - add: false + add: false, + atPoint: options?.atPoint }, undefined, - this.flow.object instanceof ProjectEditor.PageClass && - objects.length == 0 - ? [ - new MenuItem({ - label: "Center View", - click: async () => { - this.flowContext.viewState.centerView(); - } - }), - new MenuItem({ - label: "Center View on All Pages", - click: async () => { - this.flowContext.viewState.centerView(); - - for (const page of this.projectStore.project - .pages) { - if (page != this.flow.object) { - const editor = - this.projectStore.editorsStore.getEditorByObject( - page - ); - if (editor?.state) { - const pageTabState = - editor.state as PageTabState; - - pageTabState.centerView(); - } else { - let uiState = - this.projectStore.uiStateStore.getObjectUIState( - page, - "flow-state" - ); - - if (!uiState) { - uiState = {}; - } - - uiState.transform = { - translate: { - x: this.flowContext.viewState - .transform.translate.x, - y: this.flowContext.viewState - .transform.translate.y - }, - scale: - uiState.transform?.scale ?? - this.flowContext.viewState - .transform.scale - }; - - runInAction(() => { - this.projectStore.uiStateStore.updateObjectUIState( - page, - "flow-state", - uiState - ); - }); - } - } - } - } - }), - ...(this.projectStore.uiStateStore.globalFlowZoom - ? [] - : [ - new MenuItem({ - label: "Set the Same Zoom for All Pages", - click: async () => { - for (const page of this.projectStore - .project.pages) { - if (page != this.flow.object) { - let uiState = - this.projectStore.uiStateStore.getObjectUIState( - page, - "flow-state" - ); - - if (!uiState) { - uiState = {}; - } - - uiState.transform = { - translate: { - x: this.flowContext - .viewState.transform - .translate.x, - y: this.flowContext - .viewState.transform - .translate.y - }, - scale: this.flowContext - .viewState.transform - .scale - }; - - runInAction(() => { - this.projectStore.uiStateStore.updateObjectUIState( - page, - "flow-state", - uiState - ); - }); - } - } - } - }) - ]) - ] - : undefined + additionalMenuItems ); } @@ -401,4 +418,217 @@ export class FlowDocument implements IDocument { this.projectStore.addObject(flow.connectionLines, connectionLine); } + + async connectToNewTarget( + sourceObjectId: string, + connectionOutputName: string, + atPoint: Point + ) { + const component = await selectComponentDialog( + this.flowContext.projectStore, + "actions" + ); + + if (component) { + component.left = Math.round(atPoint.x); + component.top = Math.round(atPoint.y); + + this.flowContext.projectStore.undoManager.setCombineCommands(true); + + const flow = this.flow.object as Flow; + let targetObject = this.flowContext.projectStore.addObject( + flow.components, + component + ) as Component; + + const sourceObject = this.projectStore.getObjectFromObjectId( + sourceObjectId + ) as Component; + + let updatePositionInterval: NodeJS.Timer | undefined; + + const connectionOutput = sourceObject + .getOutputs() + .find(output => output.name == connectionOutputName); + if (connectionOutput) { + let connectionInput = targetObject + .getInputs() + .find( + input => + input.isSequenceInput == + connectionOutput.isSequenceOutput + ); + + if (!connectionInput) { + connectionInput = targetObject.getInputs()[0]; + } + + if (connectionInput) { + if ( + targetObject instanceof IsTrueActionComponent || + targetObject instanceof LogActionComponent + ) { + if (connectionInput.isSequenceInput) { + this.projectStore.updateObject(targetObject, { + value: "", + customInputs: [] + }); + } + } + + const connectionLine = createObject( + this.flowContext.projectStore, + { + source: sourceObject.objID, + output: connectionOutputName, + target: targetObject.objID, + input: connectionInput.name + }, + ConnectionLine + ); + + this.projectStore.addObject( + flow.connectionLines, + connectionLine + ); + + updatePositionInterval = setInterval(() => { + if (!targetObject._geometry) { + return; + } + clearInterval(updatePositionInterval); + + const xOffset = + Math.round(atPoint.x) - + connectionLine._targetPosition!.x; + const yOffset = + Math.round(atPoint.y) - + connectionLine._targetPosition!.y; + this.projectStore.updateObject(targetObject, { + left: targetObject.left + xOffset, + top: targetObject.top + yOffset + }); + + this.flowContext.projectStore.undoManager.setCombineCommands( + false + ); + }, 0); + } + } + + if (!updatePositionInterval) { + this.flowContext.projectStore.undoManager.setCombineCommands( + false + ); + } + // + const objectAdapter = this.flowContext.document.findObjectById( + getId(targetObject) + ); + if (objectAdapter) { + const viewState = this.flowContext.viewState; + viewState.selectObjects([objectAdapter]); + } + } + } + + async connectToNewSource( + targetObjectId: string, + connectionInputName: string, + atPoint: Point + ) { + const component = await selectComponentDialog( + this.flowContext.projectStore, + "actions" + ); + + if (component) { + component.left = Math.round(atPoint.x); + component.top = Math.round(atPoint.y); + + this.flowContext.projectStore.undoManager.setCombineCommands(true); + + const flow = this.flow.object as Flow; + let sourceObject = this.flowContext.projectStore.addObject( + flow.components, + component + ) as Component; + + const targetObject = this.projectStore.getObjectFromObjectId( + targetObjectId + ) as Component; + + let updatePositionInterval: NodeJS.Timer | undefined; + + const connectionInput = targetObject + .getInputs() + .find(input => input.name == connectionInputName); + if (connectionInput) { + let connectionOutput = sourceObject + .getOutputs() + .find( + output => + output.isSequenceOutput == + connectionInput.isSequenceInput + ); + + if (!connectionOutput) { + connectionOutput = sourceObject.getOutputs()[0]; + } + + if (connectionOutput) { + const connectionLine = createObject( + this.flowContext.projectStore, + { + source: sourceObject.objID, + output: connectionOutput.name, + target: targetObject.objID, + input: connectionInputName + }, + ConnectionLine + ); + + this.projectStore.addObject( + flow.connectionLines, + connectionLine + ); + + updatePositionInterval = setInterval(() => { + if (!sourceObject._geometry) { + return; + } + clearInterval(updatePositionInterval); + + const xOffset = + Math.round(atPoint.x) - + connectionLine._sourcePosition!.x; + const yOffset = + Math.round(atPoint.y) - + connectionLine._sourcePosition!.y; + this.projectStore.updateObject(sourceObject, { + left: sourceObject.left + xOffset, + top: sourceObject.top + yOffset + }); + + this.flowContext.projectStore.undoManager.setCombineCommands( + false + ); + }, 0); + } + } + if (!updatePositionInterval) { + this.flowContext.projectStore.undoManager.setCombineCommands( + false + ); + } + // + const objectAdapter = this.flowContext.document.findObjectById( + getId(sourceObject) + ); + if (objectAdapter) { + const viewState = this.flowContext.viewState; + viewState.selectObjects([objectAdapter]); + } + } + } } diff --git a/packages/project-editor/flow/editor/mouse-handler.tsx b/packages/project-editor/flow/editor/mouse-handler.tsx index 7ed7c490f..9207a4f20 100644 --- a/packages/project-editor/flow/editor/mouse-handler.tsx +++ b/packages/project-editor/flow/editor/mouse-handler.tsx @@ -825,6 +825,12 @@ export class NewConnectionLineFromOutputMouseHandler extends MouseHandler { this.target.objectId, this.target.connectionInput ); + } else if (this.distance >= 20) { + context.document.connectToNewTarget( + this.sourceObject.id, + this.connectionOutput, + context.viewState.transform.offsetToPagePoint(this.endPoint) + ); } context.document.onDragEnd(); @@ -1012,6 +1018,12 @@ export class NewConnectionLineFromInputMouseHandler extends MouseHandler { this.targetObject.id, this.connectionInput ); + } else if (this.distance >= 20) { + context.document.connectToNewSource( + this.targetObject.id, + this.connectionInput, + context.viewState.transform.offsetToPagePoint(this.startPoint) + ); } context.document.onDragEnd(); diff --git a/packages/project-editor/flow/flow-interfaces.ts b/packages/project-editor/flow/flow-interfaces.ts index e90bd9b8a..3a458303d 100644 --- a/packages/project-editor/flow/flow-interfaces.ts +++ b/packages/project-editor/flow/flow-interfaces.ts @@ -164,6 +164,16 @@ export interface IDocument { targetObjectId: string, connectionInput: string ): void; + connectToNewTarget( + sourceObjectId: string, + connectionOutput: string, + atPoint: Point + ): void; + connectToNewSource( + targetObjectId: string, + connectionInput: string, + atPoint: Point + ): void; } export interface ObjectIdUnderPointer { diff --git a/packages/project-editor/flow/runtime-viewer/flow-document.tsx b/packages/project-editor/flow/runtime-viewer/flow-document.tsx index d6e230d4d..ce9fcac2c 100644 --- a/packages/project-editor/flow/runtime-viewer/flow-document.tsx +++ b/packages/project-editor/flow/runtime-viewer/flow-document.tsx @@ -121,4 +121,15 @@ export class FlowDocument implements IDocument { targetObjectId: string, connectionInput: string ) {} + + connectToNewTarget( + sourceObjectId: string, + connectionOutput: string, + atPoint: Point + ) {} + connectToNewSource( + targetObjectId: string, + connectionInput: string, + atPoint: Point + ) {} } diff --git a/packages/project-editor/lvgl/LVGLStylesTreeNavigation.tsx b/packages/project-editor/lvgl/LVGLStylesTreeNavigation.tsx index 3b69dea5e..23d3ec944 100644 --- a/packages/project-editor/lvgl/LVGLStylesTreeNavigation.tsx +++ b/packages/project-editor/lvgl/LVGLStylesTreeNavigation.tsx @@ -68,7 +68,7 @@ const AddButton = observer( render() { return ( + this.props.objectAdapter.selectedObject && + canAdd(this.props.objectAdapter.selectedObject) && ( + + ) ); } } diff --git a/packages/project-editor/store/editor.ts b/packages/project-editor/store/editor.ts index 9da8689db..3d9371a00 100644 --- a/packages/project-editor/store/editor.ts +++ b/packages/project-editor/store/editor.ts @@ -332,7 +332,7 @@ export class EditorsStore { } } - if (!activeEditor) { + if (!activeEditor && this.tabs.length) { activeEditor = this.activeEditor; } diff --git a/packages/project-editor/store/helper.ts b/packages/project-editor/store/helper.ts index 3f60d7477..a8659b0e4 100644 --- a/packages/project-editor/store/helper.ts +++ b/packages/project-editor/store/helper.ts @@ -667,13 +667,6 @@ export function extendContextMenu( } } -export function canAdd(object: IEezObject) { - return ( - (isArrayElement(object) || isArray(object)) && - getClassInfo(object).newItem != undefined - ); -} - export function canDuplicate(object: IEezObject) { return isArrayElement(object); } @@ -732,6 +725,34 @@ export function canPaste(projectStore: ProjectStore, object: IEezObject) { //////////////////////////////////////////////////////////////////////////////// +export function canAdd(object: IEezObject) { + return ( + (isArrayElement(object) || isArray(object)) && + getClassInfo(object).newItem != undefined + ); +} + +export function getAddItemName(object: IEezObject) { + const parent = isArray(object) ? object : getParent(object); + if (!parent) { + return null; + } + + const project = getProject(parent); + if (parent == project.userWidgets) { + return "User Widget"; + } + if (getParent(parent) == project.lvglStyles) { + return "Style"; + } + + if (getParent(parent) == project.lvglGroups) { + return "Group"; + } + + return humanize(getClass(parent).name); +} + export async function addItem(object: IEezObject) { const parent = isArray(object) ? object : getParent(object); if (!parent) { diff --git a/packages/project-editor/store/layout-models.tsx b/packages/project-editor/store/layout-models.tsx index 0b7e556c9..dcddc5727 100644 --- a/packages/project-editor/store/layout-models.tsx +++ b/packages/project-editor/store/layout-models.tsx @@ -11,6 +11,7 @@ import { } from "eez-studio-ui/layout-models"; import type { ProjectStore } from "project-editor/store"; +import { settingsController } from "home/settings"; //////////////////////////////////////////////////////////////////////////////// @@ -184,6 +185,15 @@ export class LayoutModels extends AbstractLayoutModels { icon: "material:view_compact" }; + static COMPONENTS_PALETTE_TAB: FlexLayout.IJsonTabNode = { + type: "tab", + enableClose: false, + name: "Components Palette", + id: LayoutModels.COMPONENTS_PALETTE_TAB_ID, + component: "componentsPalette", + icon: "svg:components" + }; + static iconFactory = (node: FlexLayout.TabNode) => { let icon = node.getIcon(); if (!icon || typeof icon != "string") { @@ -452,14 +462,7 @@ export class LayoutModels extends AbstractLayoutModels { type: "tabset", weight: 1, children: [ - { - type: "tab", - enableClose: false, - name: "Components Palette", - id: LayoutModels.COMPONENTS_PALETTE_TAB_ID, - component: "componentsPalette", - icon: "svg:components" - } + LayoutModels.COMPONENTS_PALETTE_TAB ] } ] @@ -1074,6 +1077,12 @@ export class LayoutModels extends AbstractLayoutModels { } } + toggleComponentsPalette() { + settingsController.showComponentsPaletteInProjectEditor = + !settingsController.showComponentsPaletteInProjectEditor; + this.projectStore.project.enableTabs(); + } + reset() { for (const model of this.models) { model.set(FlexLayout.Model.fromJson(model.json)); diff --git a/packages/project-editor/ui-components/ListNavigation.tsx b/packages/project-editor/ui-components/ListNavigation.tsx index 515355aa8..3dffe5342 100644 --- a/packages/project-editor/ui-components/ListNavigation.tsx +++ b/packages/project-editor/ui-components/ListNavigation.tsx @@ -22,6 +22,7 @@ import { import { addItem, canAdd, + getAddItemName, IPanel, isPartOfNavigation } from "project-editor/store"; @@ -103,7 +104,11 @@ const AddButton = observer( render() { return ( e.preventDefault()} + >
+ ); } //