import React from "react"; import { action, computed, observable, runInAction, makeObservable } from "mobx"; import { observer } from "mobx-react"; import { Point, pointDistance, Rect, rectContains, rectExpand } from "eez-studio-shared/geometry"; import { Draggable } from "eez-studio-ui/draggable"; import { IPanel, isLVGLCreateInProgress } from "project-editor/store"; import { ProjectContext } from "project-editor/project/context"; import type { Flow } from "project-editor/flow/flow"; import type { FlowTabState } from "project-editor/flow/flow-tab-state"; import type { IFlowContext } from "project-editor/flow/flow-interfaces"; import { RuntimeFlowContext } from "project-editor/flow/runtime-viewer/context"; import { Svg } from "project-editor/flow/editor/render"; import { ConnectionLineDebugValues, ConnectionLines } from "project-editor/flow/connection-line/ConnectionLineComponent"; import { getObjectBoundingRect } from "project-editor/flow/editor/bounding-rects"; import { IMouseHandler, PanMouseHandler } from "project-editor/flow/editor/mouse-handler"; import { Selection } from "project-editor/flow/runtime-viewer/selection"; import classNames from "classnames"; import { ProjectEditor } from "project-editor/project-editor-interface"; const CONF_DOUBLE_CLICK_TIME = 350; // ms const CONF_DOUBLE_CLICK_DISTANCE = 5; // px const AllConnectionLines = observer( ({ flowContext }: { flowContext: IFlowContext }) => { return ( ); } ); const AllConnectionLineDebugValues = observer( ({ flowContext }: { flowContext: IFlowContext }) => { return ( ); } ); //////////////////////////////////////////////////////////////////////////////// export const Canvas = observer( class Canvas extends React.Component<{ children?: React.ReactNode; flowContext: IFlowContext; pageRect?: Rect; }> { div: HTMLDivElement; updateClientRectRequestAnimationFrameId: any; setOverflowTimeout: any; deltaY = 0; buttonsAtDown: number; lastMouseUpPosition: Point; lastMouseUpTime: number | undefined; draggable = new Draggable(this); constructor(props: any) { super(props); makeObservable(this, { _mouseHandler: observable, onDragStart: action.bound, onDragEnd: action.bound }); } _mouseHandler: IMouseHandler | undefined; get mouseHandler() { return this._mouseHandler; } set mouseHandler(value: IMouseHandler | undefined) { runInAction(() => { this._mouseHandler = value; }); } updateClientRect = () => { if ($(this.div).is(":visible")) { const transform = this.props.flowContext.viewState.transform; let clientRect = this.div.getBoundingClientRect(); if ( clientRect.left !== transform.clientRect.left || clientRect.top !== transform.clientRect.top || (clientRect.width && clientRect.width !== transform.clientRect.width) || (clientRect.height && clientRect.height !== transform.clientRect.height) ) { if ( this.props.flowContext.projectStore.projectTypeTraits .isDashboard && this.props.flowContext.projectStore.runtime && !this.props.flowContext.projectStore.runtime .isDebuggerActive ) { // set overflow to hidden and back to auto after timeout if (this.setOverflowTimeout) { clearTimeout(this.setOverflowTimeout); this.setOverflowTimeout = undefined; } this.div.style.overflow = "hidden"; this.setOverflowTimeout = setTimeout(() => { this.setOverflowTimeout = undefined; this.div.style.overflow = "auto"; }, 100); } runInAction(() => { transform.clientRect = clientRect; }); } } this.updateClientRectRequestAnimationFrameId = requestAnimationFrame(this.updateClientRect); }; componentDidMount() { if ( this.props.flowContext.projectStore.runtime && this.props.flowContext.projectStore.runtime.isDebuggerActive ) { this.draggable.attach(this.div); } this.div.addEventListener("wheel", this.onWheel, { passive: false }); this.updateClientRect(); } componentWillUnmount() { this.draggable.attach(null); this.div.removeEventListener("wheel", this.onWheel); cancelAnimationFrame(this.updateClientRectRequestAnimationFrameId); if (this.setOverflowTimeout) { clearTimeout(this.setOverflowTimeout); this.setOverflowTimeout = undefined; } } onWheel = (event: WheelEvent) => { if (event.buttons === 4) { // do nothing if mouse wheel is pressed, i.e. pan will be activated in onMouseDown return; } const transform = this.props.flowContext.viewState.transform.clone(); if (event.ctrlKey) { this.deltaY += event.deltaY; if (Math.abs(this.deltaY) > 10) { let scale: number; if (this.deltaY < 0) { scale = transform.nextScale; } else { scale = transform.previousScale; } this.deltaY = 0; var point = transform.clientToOffsetPoint({ x: event.clientX, y: event.clientY }); let x = point.x - transform.clientRect.width / 2; let y = point.y - transform.clientRect.height / 2; let tx = x - ((x - transform.translate.x) * scale) / transform.scale; let ty = y - ((y - transform.translate.y) * scale) / transform.scale; transform.scale = scale; if (!this.props.flowContext.frontFace) { transform.translate = { x: tx, y: ty }; } runInAction(() => { this.props.flowContext.viewState.transform = transform; }); } } else { if (this.props.flowContext.frontFace) { return; } transform.translate = { x: transform.translate.x - (event.shiftKey ? event.deltaY : event.deltaX), y: transform.translate.y - (event.shiftKey ? event.deltaX : event.deltaY) }; runInAction(() => { this.props.flowContext.viewState.transform = transform; }); } event.preventDefault(); event.stopPropagation(); }; onContextMenu = (event: React.MouseEvent) => { event.preventDefault(); }; createMouseHandler(event: MouseEvent) { const flowContext = this.props.flowContext; if (!event.altKey) { let point = flowContext.viewState.transform.pointerEventToPagePoint( event ); const result = flowContext.document.objectFromPoint(point); if (result) { const object = flowContext.document.findObjectById( result.id ); if (object) { flowContext.viewState.deselectAllObjects(); flowContext.viewState.selectObject(object); event.preventDefault(); } } else { flowContext.viewState.deselectAllObjects(); } } return undefined; } onDragStart(event: PointerEvent) { this.props.flowContext.projectStore.editorsStore.selectEditorTabForObject( this.props.flowContext.document.flow.object ); this.buttonsAtDown = event.buttons; if (this.mouseHandler) { this.mouseHandler.up(this.props.flowContext, true); this.mouseHandler = undefined; } if (event.buttons && event.buttons !== 1) { this.mouseHandler = new PanMouseHandler(); } else { this.mouseHandler = this.createMouseHandler(event); } if (this.mouseHandler) { this.mouseHandler.lastPointerEvent = { clientX: event.clientX, clientY: event.clientY, movementX: event.movementX ?? 0, movementY: event.movementY ?? 0, ctrlKey: event.ctrlKey, shiftKey: event.shiftKey, timeStamp: event.timeStamp }; this.mouseHandler.down(this.props.flowContext, event); } } onDragMove = (event: PointerEvent) => { if (this.mouseHandler) { this.mouseHandler.lastPointerEvent = { clientX: event.clientX, clientY: event.clientY, movementX: event.movementX ? event.movementX : this.mouseHandler.lastPointerEvent ? this.mouseHandler.lastPointerEvent.movementX : 0, movementY: event.movementY ? event.movementY : this.mouseHandler.lastPointerEvent ? this.mouseHandler.lastPointerEvent.movementY : 0, ctrlKey: event.ctrlKey, shiftKey: event.shiftKey, timeStamp: event.timeStamp }; this.mouseHandler.move(this.props.flowContext, event); } }; onDragEnd(event: PointerEvent, cancel: boolean) { let preventContextMenu = false; if (this.mouseHandler) { this.mouseHandler.up(this.props.flowContext, cancel); if (this.mouseHandler instanceof PanMouseHandler) { if (pointDistance(this.mouseHandler.totalMovement) > 10) { preventContextMenu = true; } } this.mouseHandler = undefined; } let time = new Date().getTime(); if (this.buttonsAtDown === 1) { let distance = pointDistance( { x: event.clientX, y: event.clientY }, { x: this.draggable.xDragStart, y: this.draggable.yDragStart } ); if (distance <= CONF_DOUBLE_CLICK_DISTANCE) { if (this.lastMouseUpTime !== undefined) { let distance = pointDistance( { x: event.clientX, y: event.clientY }, this.lastMouseUpPosition ); if ( time - this.lastMouseUpTime <= CONF_DOUBLE_CLICK_TIME && distance <= CONF_DOUBLE_CLICK_DISTANCE ) { // double click if ( this.props.flowContext.viewState.selectedObjects .length === 1 ) { const object = this.props.flowContext.viewState .selectedObjects[0]; object.open(); } else if ( this.props.flowContext.viewState.selectedObjects .length === 0 ) { this.props.flowContext.viewState.resetTransform(); } } } this.lastMouseUpTime = time; this.lastMouseUpPosition = { x: event.clientX, y: event.clientY }; } else { this.lastMouseUpTime = undefined; } } else { this.lastMouseUpTime = undefined; if (!preventContextMenu && this.buttonsAtDown === 2) { // show context menu const context = this.props.flowContext; const point = context.viewState.transform.pointerEventToPagePoint( event ); context.viewState.deselectAllObjects(); let result = context.document.objectFromPoint(point); if (result) { const object = context.document.findObjectById( result.id ); if (object) { context.viewState.selectObject(object); } } setTimeout(() => { const menu = context.document.createContextMenu( context.viewState.selectedObjects ); if (menu) { if (this.mouseHandler) { this.mouseHandler.up( this.props.flowContext, true ); this.mouseHandler = undefined; } menu.popup({}); } }, 0); } } } render() { let style: React.CSSProperties = {}; const runtime = this.props.flowContext.projectStore.runtime!; const runMode = runtime && !runtime.isDebuggerActive; const transform = this.props.flowContext.viewState.transform; let xt: number; let yt: number; let scale: number; if ( runMode && this.props.flowContext.projectStore.projectTypeTraits .isDashboard && this.props.flowContext.document.flow.object instanceof ProjectEditor.PageClass && this.props.flowContext.document.flow.object.scaleToFit ) { xt = 0; yt = 0; scale = 1; } else if ( runMode && this.props.flowContext.projectStore.projectTypeTraits .isFirmware && this.props.flowContext.projectStore.projectTypeTraits .hasFlowSupport && this.props.flowContext.projectStore.runtime instanceof ProjectEditor.WasmRuntimeClass ) { xt = Math.round( (transform.clientRect.width - this.props.flowContext.projectStore.runtime .displayWidth) / 2 ); yt = Math.round( (transform.clientRect.height - this.props.flowContext.projectStore.runtime .displayHeight) / 2 ); if (yt < 0) { yt = 0; } scale = 1; } else { xt = Math.round( transform.translate.x + transform.clientRect.width / 2 ); yt = Math.round( transform.translate.y + transform.clientRect.height / 2 ); if (yt < 0 && runMode) { yt = 0; } scale = transform.scale; } if ( transform.clientRect.width <= 1 || transform.clientRect.height <= 1 ) { style.visibility = "hidden"; } const lvglCreateInProgress = !runtime.isStopped && this.props.flowContext.flowState && this.props.flowContext.flowState.flow instanceof ProjectEditor.PageClass && isLVGLCreateInProgress(this.props.flowContext.flowState.flow); return (