diff --git a/frontend/src/framework/GuiMessageBroker.ts b/frontend/src/framework/GuiMessageBroker.ts index 2926ad23f..1ba5a743e 100644 --- a/frontend/src/framework/GuiMessageBroker.ts +++ b/frontend/src/framework/GuiMessageBroker.ts @@ -27,6 +27,7 @@ export enum GuiEvent { HideDataChannelConnectionsRequest = "hideDataChannelConnectionsRequest", HighlightDataChannelConnectionRequest = "highlightDataChannelConnectionRequest", UnhighlightDataChannelConnectionRequest = "unhighlightDataChannelConnectionRequest", + DataChannelPointerUp = "dataChannelPointerUp", DataChannelOriginPointerDown = "dataChannelOriginPointerDown", DataChannelConnectionsChange = "dataChannelConnectionsChange", DataChannelNodeHover = "dataChannelNodeHover", @@ -62,6 +63,7 @@ export type GuiEventPayloads = { moduleInstanceId: string; originElement: HTMLElement; }; + [GuiEvent.DataChannelPointerUp]: {}; [GuiEvent.DataChannelNodeHover]: { connectionAllowed: boolean; }; diff --git a/frontend/src/framework/internal/GlobalCursor.tsx b/frontend/src/framework/internal/GlobalCursor.tsx index a437471bc..056310505 100644 --- a/frontend/src/framework/internal/GlobalCursor.tsx +++ b/frontend/src/framework/internal/GlobalCursor.tsx @@ -26,9 +26,50 @@ export class GlobalCursor { this._cursorStack = []; } + private getCursorClassName(cursorType: GlobalCursorType) { + switch (cursorType) { + case GlobalCursorType.Default: + return "cursor-default"; + case GlobalCursorType.Copy: + return "cursor-copy"; + case GlobalCursorType.Pointer: + return "cursor-pointer"; + case GlobalCursorType.Grab: + return "cursor-grab"; + case GlobalCursorType.Grabbing: + return "cursor-grabbing"; + case GlobalCursorType.Crosshair: + return "cursor-crosshair"; + case GlobalCursorType.Move: + return "cursor-move"; + case GlobalCursorType.Text: + return "cursor-text"; + case GlobalCursorType.Wait: + return "cursor-wait"; + case GlobalCursorType.Help: + return "cursor-help"; + case GlobalCursorType.Progress: + return "cursor-progress"; + case GlobalCursorType.Cell: + return "cursor-cell"; + case GlobalCursorType.VerticalText: + return "cursor-vertical-text"; + case GlobalCursorType.Alias: + return "cursor-alias"; + case GlobalCursorType.ContextMenu: + return "cursor-context-menu"; + case GlobalCursorType.NoDrop: + return "cursor-no-drop"; + case GlobalCursorType.NotAllowed: + return "cursor-not-allowed"; + default: + return "cursor-default"; + } + } + private updateBodyCursor() { if (this._lastCursor !== GlobalCursorType.Default) { - const oldCursorClass = `cursor-${this._lastCursor}`; + const oldCursorClass = this.getCursorClassName(this._lastCursor); document.body.classList.remove(oldCursorClass); } @@ -38,7 +79,7 @@ export class GlobalCursor { } const newCursor = this._cursorStack[this._cursorStack.length - 1]; - const newCursorClass = `cursor-${newCursor}`; + const newCursorClass = this.getCursorClassName(newCursor); document.body.classList.add(newCursorClass); this._lastCursor = newCursor; } diff --git a/frontend/src/framework/internal/components/Content/private-components/DataChannelVisualization/dataChannelVisualization.tsx b/frontend/src/framework/internal/components/Content/private-components/DataChannelVisualization/dataChannelVisualization.tsx index 6213d856d..1959e156f 100644 --- a/frontend/src/framework/internal/components/Content/private-components/DataChannelVisualization/dataChannelVisualization.tsx +++ b/frontend/src/framework/internal/components/Content/private-components/DataChannelVisualization/dataChannelVisualization.tsx @@ -46,6 +46,11 @@ export const DataChannelVisualization: React.FC = const guiMessageBroker = props.workbench.getGuiMessageBroker(); + const [dataChannelConnectionsLayerVisible, setDataChannelConnectionsLayerVisible] = useGuiState( + guiMessageBroker, + GuiState.DataChannelConnectionLayerVisible + ); + React.useEffect(() => { forceRerender(); }, [boundingRect]); @@ -66,6 +71,7 @@ export const DataChannelVisualization: React.FC = mousePressed = true; setCurrentPointerPosition(currentOriginPoint); setCurrentChannelName(null); + setDataChannelConnectionsLayerVisible(true); const moduleInstance = props.workbench.getModuleInstance(payload.moduleInstanceId); if (!moduleInstance) { @@ -84,6 +90,7 @@ export const DataChannelVisualization: React.FC = setEditDataChannelConnectionsForModuleInstanceId(null); setShowDataChannelConnections(false); props.workbench.getGlobalCursor().restoreOverrideCursor(); + setDataChannelConnectionsLayerVisible(false); } function handlePointerUp() { @@ -121,9 +128,9 @@ export const DataChannelVisualization: React.FC = function handleNodeHover(payload: GuiEventPayloads[GuiEvent.DataChannelNodeHover]) { if (payload.connectionAllowed) { - props.workbench.getGlobalCursor().setOverrideCursor(GlobalCursorType.Copy); + props.workbench.getGlobalCursor().changeOverrideCursor(GlobalCursorType.Copy); } else { - props.workbench.getGlobalCursor().setOverrideCursor(GlobalCursorType.NotAllowed); + props.workbench.getGlobalCursor().changeOverrideCursor(GlobalCursorType.NotAllowed); } } @@ -170,6 +177,10 @@ export const DataChannelVisualization: React.FC = GuiEvent.DataChannelOriginPointerDown, handleDataChannelOriginPointerDown ); + const removeDataChannelPointerUpHandler = guiMessageBroker.subscribeToEvent( + GuiEvent.DataChannelPointerUp, + handlePointerUp + ); const removeDataChannelDoneHandler = guiMessageBroker.subscribeToEvent( GuiEvent.HideDataChannelConnectionsRequest, handleDataChannelDone @@ -196,6 +207,7 @@ export const DataChannelVisualization: React.FC = removeHighlightDataChannelConnectionRequestHandler(); removeUnhighlightDataChannelConnectionRequestHandler(); removeDataChannelOriginPointerDownHandler(); + removeDataChannelPointerUpHandler(); removeDataChannelDoneHandler(); removeConnectionChangeHandler(); removeNodeHoverHandler(); @@ -209,7 +221,7 @@ export const DataChannelVisualization: React.FC = React.useEffect(() => { function handlePointerUp() { - if (!editDataChannelConnectionsForModuleInstanceId) { + if (!editDataChannelConnectionsForModuleInstanceId || dataChannelConnectionsLayerVisible) { return; } setShowDataChannelConnections(false); @@ -222,16 +234,24 @@ export const DataChannelVisualization: React.FC = return () => { document.removeEventListener("pointerup", handlePointerUp); }; - }, [editDataChannelConnectionsForModuleInstanceId]); + }, [editDataChannelConnectionsForModuleInstanceId, dataChannelConnectionsLayerVisible]); + + let midPointY = (originPoint.y + currentPointerPosition.y) / 2; + + if (currentPointerPosition.y < originPoint.y + 40 && currentPointerPosition.y > originPoint.y) { + midPointY = originPoint.y - 20; + } else if (currentPointerPosition.y > originPoint.y - 40 && currentPointerPosition.y < originPoint.y) { + midPointY = originPoint.y + 20; + } const midPoint1: Point = { x: originPoint.x, - y: (originPoint.y + currentPointerPosition.y) / 2, + y: midPointY, }; const midPoint2: Point = { x: currentPointerPosition.x, - y: (originPoint.y + currentPointerPosition.y) / 2, + y: midPointY, }; function makeDataChannelPaths() { @@ -344,7 +364,7 @@ export const DataChannelVisualization: React.FC = = = @@ -425,7 +445,7 @@ export const DataChannelVisualization: React.FC = diff --git a/frontend/src/framework/internal/components/Content/private-components/ViewWrapper/private-components/channelSelector.tsx b/frontend/src/framework/internal/components/Content/private-components/ViewWrapper/private-components/channelSelector.tsx index 84991cd54..2aeeb50bf 100644 --- a/frontend/src/framework/internal/components/Content/private-components/ViewWrapper/private-components/channelSelector.tsx +++ b/frontend/src/framework/internal/components/Content/private-components/ViewWrapper/private-components/channelSelector.tsx @@ -1,8 +1,9 @@ import React from "react"; import { createPortal } from "react-dom"; -import { XMarkIcon } from "@heroicons/react/20/solid"; +import { Overlay } from "@lib/components/Overlay"; import { Point } from "@lib/utils/geometry"; +import { Close } from "@mui/icons-material"; export type ChannelSelectorProps = { channelNames: string[]; @@ -35,34 +36,38 @@ export const ChannelSelector: React.FC = (props) => { }, [props.onCancel]); return createPortal( -
window.innerWidth / 2 ? window.innerWidth - props.position.x : undefined, - bottom: props.position.y > window.innerHeight / 2 ? window.innerHeight - props.position.y : undefined, - }} - > -
-
Select a channel
-
- + <> + +
window.innerWidth / 2 ? window.innerWidth - props.position.x : undefined, + bottom: + props.position.y > window.innerHeight / 2 ? window.innerHeight - props.position.y : undefined, + }} + > +
+
Select a channel
+
+ +
+ {props.channelNames.map((channelName) => { + return ( +
props.onSelectChannel(channelName)} + > + {channelName} +
+ ); + })}
- {props.channelNames.map((channelName) => { - return ( -
props.onSelectChannel(channelName)} - > - {channelName} -
- ); - })} -
, + , document.body ); }; diff --git a/frontend/src/framework/internal/components/Content/private-components/ViewWrapper/private-components/inputChannelNode.tsx b/frontend/src/framework/internal/components/Content/private-components/ViewWrapper/private-components/inputChannelNode.tsx index ee9bd3102..1b0d79f23 100644 --- a/frontend/src/framework/internal/components/Content/private-components/ViewWrapper/private-components/inputChannelNode.tsx +++ b/frontend/src/framework/internal/components/Content/private-components/ViewWrapper/private-components/inputChannelNode.tsx @@ -7,7 +7,7 @@ import { IconButton } from "@lib/components/IconButton"; import { useElementBoundingRect } from "@lib/hooks/useElementBoundingRect"; import { Point, pointerEventToPoint, rectContainsPoint } from "@lib/utils/geometry"; import { resolveClassNames } from "@lib/utils/resolveClassNames"; -import { Close } from "@mui/icons-material"; +import { Remove } from "@mui/icons-material"; export type InputChannelNodeProps = { inputName: string; @@ -21,6 +21,7 @@ export type InputChannelNodeProps = { export const InputChannelNode: React.FC = (props) => { const ref = React.useRef(null); + const removeButtonRef = React.useRef(null); const [connectable, setConnectable] = React.useState(false); const [hovered, setHovered] = React.useState(false); const [hasConnection, setHasConnection] = React.useState(false); @@ -112,16 +113,19 @@ export const InputChannelNode: React.FC = (props) => { } function handlePointerUp(e: PointerEvent) { - console.debug("pointer up", isHovered, isConnectable); if (isHovered) { - if (isConnectable) { + if (removeButtonRef.current && removeButtonRef.current.contains(e.target as Node)) { + props.onChannelConnectionDisconnect(props.inputName); + setHovered(false); + isHovered = false; + } else if (isConnectable) { props.onChannelConnect(props.inputName, moduleInstanceId, pointerEventToPoint(e)); - } else { - guiMessageBroker.publishEvent(GuiEvent.HideDataChannelConnectionsRequest, {}); + setHovered(false); + isHovered = false; } - setHovered(false); - isHovered = false; } + guiMessageBroker.publishEvent(GuiEvent.DataChannelPointerUp, {}); + e.stopPropagation(); } function handleEditDataChannelConnectionsRequest() { @@ -129,10 +133,11 @@ export const InputChannelNode: React.FC = (props) => { } function handleDataChannelDone() { - setConnectable(false); isConnectable = false; + setConnectable(false); + setHovered(false); + isHovered = false; setEditDataChannelConnections(false); - console.debug("done"); } function handlePointerMove(e: PointerEvent) { @@ -140,13 +145,11 @@ export const InputChannelNode: React.FC = (props) => { if (boundingRect && rectContainsPoint(boundingRect, pointerEventToPoint(e))) { setHovered(true); isHovered = true; - console.debug("hovered"); return; } if (isHovered) { setHovered(false); isHovered = false; - console.debug("unhovered"); } } @@ -164,7 +167,7 @@ export const InputChannelNode: React.FC = (props) => { handleEditDataChannelConnectionsRequest ); - document.addEventListener("pointerup", handlePointerUp); + ref.current?.addEventListener("pointerup", handlePointerUp, true); document.addEventListener("pointermove", handlePointerMove); return () => { @@ -172,16 +175,11 @@ export const InputChannelNode: React.FC = (props) => { removeDataChannelOriginPointerDownHandler(); removeShowDataChannelConnectionsRequestHandler(); - document.removeEventListener("pointerup", handlePointerUp); + ref.current?.removeEventListener("pointerup", handlePointerUp); document.removeEventListener("pointermove", handlePointerMove); }; }, [props.onChannelConnect, props.workbench, props.moduleInstanceId, props.inputName, props.channelKeyCategories]); - function handleChannelConnectionRemoveClick(e: React.PointerEvent) { - e.stopPropagation(); - props.onChannelConnectionDisconnect(props.inputName); - } - function handlePointerEnter() { guiMessageBroker.publishEvent(GuiEvent.HighlightDataChannelConnectionRequest, { moduleInstanceId: props.moduleInstanceId, @@ -195,6 +193,7 @@ export const InputChannelNode: React.FC = (props) => { function handlePointerLeave() { guiMessageBroker.publishEvent(GuiEvent.UnhighlightDataChannelConnectionRequest, {}); + guiMessageBroker.publishEvent(GuiEvent.DataChannelNodeUnhover, {}); } return ( @@ -204,36 +203,39 @@ export const InputChannelNode: React.FC = (props) => { data-channelconnector className={resolveClassNames( "flex", + "flex-col", "items-center", "justify-center", "rounded", "border", "p-4", + "h-20", "m-2", - "h-16", + "gap-2", "text-sm", { - "hover:border-red-600": !connectable, - "hover:border-green-600": connectable, - "bg-green-600": hovered && connectable, - "bg-red-600": hovered && !connectable, + "bg-green-600 border-green-600": hovered && connectable, + "bg-blue-600 border-blue-600": hovered && !connectable, "bg-slate-100": !hovered, "text-white": hovered, + "shadow-md": hasConnection, } )} onPointerEnter={handlePointerEnter} onPointerLeave={handlePointerLeave} > {props.displayName} - {editDataChannelConnections && hasConnection && ( - - - - )} + + +
); }; diff --git a/frontend/src/framework/internal/components/Content/private-components/ViewWrapper/private-components/inputChannelNodeWrapper.tsx b/frontend/src/framework/internal/components/Content/private-components/ViewWrapper/private-components/inputChannelNodeWrapper.tsx index 48dc02d5b..31059589d 100644 --- a/frontend/src/framework/internal/components/Content/private-components/ViewWrapper/private-components/inputChannelNodeWrapper.tsx +++ b/frontend/src/framework/internal/components/Content/private-components/ViewWrapper/private-components/inputChannelNodeWrapper.tsx @@ -39,6 +39,7 @@ export const InputChannelNodeWrapper: React.FC = ( props.guiMessageBroker.publishEvent(GuiEvent.HideDataChannelConnectionsRequest, {}); setVisible(false); } + e.stopPropagation(); } function handleEditDataChannelConnectionsRequest( @@ -78,7 +79,7 @@ export const InputChannelNodeWrapper: React.FC = ( return createPortal(
= (props) => { ); function handleModuleClick() { + if (dataChannelConnectionsLayerVisible) { + return; + } if (settingsPanelWidth <= 5) { setSettingsPanelWidth(20); } @@ -91,9 +94,6 @@ export const ViewWrapper: React.FC = (props) => { } function handlePointerUp() { - if (dataChannelConnectionsLayerVisible) { - return; - } if (drawerContent === DrawerContent.ModulesList) { if (!timeRef.current || Date.now() - timeRef.current < 800) { handleModuleClick(); @@ -110,52 +110,58 @@ export const ViewWrapper: React.FC = (props) => { e.stopPropagation(); } - function handleChannelConnect(inputName: string, moduleInstanceId: string, destinationPoint: Point) { - const originModuleInstance = props.workbench.getModuleInstance(moduleInstanceId); + const handleChannelConnect = React.useCallback( + function handleChannelConnect(inputName: string, moduleInstanceId: string, destinationPoint: Point) { + const originModuleInstance = props.workbench.getModuleInstance(moduleInstanceId); - if (!originModuleInstance) { - guiMessageBroker.publishEvent(GuiEvent.HideDataChannelConnectionsRequest, {}); - return; - } - - const acceptedKeys = props.moduleInstance - .getInputChannelDefs() - .find((channelDef) => channelDef.name === inputName)?.keyCategories; - - const channels = Object.values(originModuleInstance.getBroadcastChannels()).filter((channel) => { - if (!acceptedKeys || acceptedKeys.some((key) => channel.getDataDef().key === key)) { - return Object.values(props.moduleInstance.getInputChannels()).every((inputChannel) => { - if (inputChannel.getDataDef().key === channel.getDataDef().key) { - return true; - } - return false; - }); + if (!originModuleInstance) { + guiMessageBroker.publishEvent(GuiEvent.HideDataChannelConnectionsRequest, {}); + return; } - return false; - }); - if (channels.length === 0) { - guiMessageBroker.publishEvent(GuiEvent.HideDataChannelConnectionsRequest, {}); - return; - } + const acceptedKeys = props.moduleInstance + .getInputChannelDefs() + .find((channelDef) => channelDef.name === inputName)?.keyCategories; - if (channels.length > 1) { - setChannelSelectorCenterPoint(destinationPoint); - setSelectableChannels(Object.values(channels).map((channel) => channel.getName())); - setCurrentInputName(inputName); - return; - } + const channels = Object.values(originModuleInstance.getBroadcastChannels()).filter((channel) => { + if (!acceptedKeys || acceptedKeys.some((key) => channel.getDataDef().key === key)) { + return Object.values(props.moduleInstance.getInputChannels()).every((inputChannel) => { + if (inputChannel.getDataDef().key === channel.getDataDef().key) { + return true; + } + return false; + }); + } + return false; + }); - const channelName = Object.values(channels)[0].getName(); + if (channels.length === 0) { + guiMessageBroker.publishEvent(GuiEvent.HideDataChannelConnectionsRequest, {}); + return; + } - props.moduleInstance.setInputChannel(inputName, channelName); - guiMessageBroker.publishEvent(GuiEvent.HideDataChannelConnectionsRequest, {}); - } + if (channels.length > 1) { + setChannelSelectorCenterPoint(destinationPoint); + setSelectableChannels(Object.values(channels).map((channel) => channel.getName())); + setCurrentInputName(inputName); + return; + } - function handleChannelDisconnect(inputName: string) { - props.moduleInstance.removeInputChannel(inputName); - guiMessageBroker.publishEvent(GuiEvent.DataChannelConnectionsChange, {}); - } + const channelName = Object.values(channels)[0].getName(); + + props.moduleInstance.setInputChannel(inputName, channelName); + guiMessageBroker.publishEvent(GuiEvent.HideDataChannelConnectionsRequest, {}); + }, + [props.moduleInstance, props.workbench] + ); + + const handleChannelDisconnect = React.useCallback( + function handleChannelDisconnect(inputName: string) { + props.moduleInstance.removeInputChannel(inputName); + guiMessageBroker.publishEvent(GuiEvent.DataChannelConnectionsChange, {}); + }, + [props.moduleInstance] + ); function handleCancelChannelSelection() { setChannelSelectorCenterPoint(null); @@ -164,6 +170,8 @@ export const ViewWrapper: React.FC = (props) => { } function handleChannelSelection(channelName: string) { + guiMessageBroker.publishEvent(GuiEvent.HideDataChannelConnectionsRequest, {}); + if (!currentInputName) { return; } @@ -171,7 +179,6 @@ export const ViewWrapper: React.FC = (props) => { setSelectableChannels([]); props.moduleInstance.setInputChannel(currentInputName, channelName); - guiMessageBroker.publishEvent(GuiEvent.HideDataChannelConnectionsRequest, {}); } const showAsActive = diff --git a/frontend/src/lib/components/Slider/slider.tsx b/frontend/src/lib/components/Slider/slider.tsx index 6e9d48b46..51ad7cb2c 100644 --- a/frontend/src/lib/components/Slider/slider.tsx +++ b/frontend/src/lib/components/Slider/slider.tsx @@ -221,7 +221,7 @@ export const Slider = React.forwardRef((props: SliderProps, ref: React.Forwarded "h-5", "block", "bg-blue-600", - "z-50", + "z-30", "shadow-sm", "rounded-full", "transform", diff --git a/frontend/src/modules/DistributionPlot/view.tsx b/frontend/src/modules/DistributionPlot/view.tsx index e4d98785a..ea1f6fce1 100644 --- a/frontend/src/modules/DistributionPlot/view.tsx +++ b/frontend/src/modules/DistributionPlot/view.tsx @@ -107,6 +107,10 @@ export const view = ({ return; } + if (plotType !== PlotType.ScatterWithColorMapping) { + setPlotType(PlotType.ScatterWithColorMapping); + } + const handleChannelColorChanged = (data: any | null, metaData: BroadcastChannelMeta | null) => { setDataColor(data); setMetaDataColor(metaData); diff --git a/frontend/src/modules/SimulationTimeSeries/channelDefs.ts b/frontend/src/modules/SimulationTimeSeries/channelDefs.ts index 951ac662d..5b165af6c 100644 --- a/frontend/src/modules/SimulationTimeSeries/channelDefs.ts +++ b/frontend/src/modules/SimulationTimeSeries/channelDefs.ts @@ -10,8 +10,4 @@ export const broadcastChannelsDef = { key: BroadcastChannelKeyCategory.Realization, value: BroadcastChannelValueType.Numeric, }, - [BroadcastChannelNames.Realization_Value_TEST]: { - key: BroadcastChannelKeyCategory.Realization, - value: BroadcastChannelValueType.Numeric, - }, };