diff --git a/src/component_info_sidebar/ComponentPreviewWidget.tsx b/src/component_info_sidebar/ComponentPreviewWidget.tsx index a2e406b7..d4aee0bd 100644 --- a/src/component_info_sidebar/ComponentPreviewWidget.tsx +++ b/src/component_info_sidebar/ComponentPreviewWidget.tsx @@ -14,6 +14,7 @@ import { collectParamIO } from './portPreview'; import { IONodeTree } from './IONodeTree'; import type { IONode } from './portPreview'; import { commandIDs } from "../commands/CommandIDs"; +import { canvasUpdatedSignal } from '../components/XircuitsBodyWidget'; export interface IComponentInfo { name: string; @@ -156,7 +157,7 @@ class OverviewSection extends ReactWidget { } setModel(m: IComponentInfo | null) { this._model = m; - this.update(); + this.update(); } render(): JSX.Element { if (!this._model) { @@ -321,6 +322,7 @@ export class ComponentPreviewWidget extends SidePanel { const shell = this._app.shell as ILabShell; shell.expandRight(); shell.activateById(this.id); + this._bindCanvasListener(); } private _computeToolbarState(): ToolbarState { @@ -357,9 +359,47 @@ export class ComponentPreviewWidget extends SidePanel { canOpenScript: !!m.node && !isStartFinish, canCenter: !!(m.node && m.engine), canOpenWorkflow: nodeType === 'xircuits_workflow', - canCollapse: !isStartFinish + canCollapse: !isStartFinish }; } + + private _isListening = false; + + private _bindCanvasListener(): void { + if (this._isListening || this.isDisposed) return; + + const onCanvasUpdate = () => { + const engine = this._model?.engine; + const currentNode = this._model?.node; + if (!engine || !currentNode) return; + + // Refresh node reference in case the model recreated it after a change + const id = currentNode.getID?.(); + const latestNode = engine.getModel?.().getNodes?.().find(n => n.getID?.() === id); + if (latestNode && latestNode !== currentNode) { + this._model!.node = latestNode; + } + + try { + const { inputs = [], outputs = [] } = collectParamIO(this._model!.node as any); + this._inputs.setData(inputs); + this._outputs.setData(outputs); + } catch (err) { + console.warn('[Sidebar] Failed to collect I/O, keeping previous state:', err); + } + + + this._topbar?.update(); + }; + + canvasUpdatedSignal.connect(onCanvasUpdate, this); + this._isListening = true; + + this.disposed.connect(() => { + canvasUpdatedSignal.disconnect(onCanvasUpdate, this); + this._isListening = false; + }); + } private _navigate(step: -1 | 1) { const node = this._model?.node; diff --git a/src/components/XircuitsBodyWidget.tsx b/src/components/XircuitsBodyWidget.tsx index c33f7e72..5fecb6be 100644 --- a/src/components/XircuitsBodyWidget.tsx +++ b/src/components/XircuitsBodyWidget.tsx @@ -206,6 +206,10 @@ const ZoomControls = styled.div<{visible: boolean}>` } `; +export type CanvasUpdatedPayload = { reason: 'content'; }; + +export const canvasUpdatedSignal = new Signal(window); + export const BodyWidget: FC = ({ context, xircuitsApp, @@ -436,12 +440,37 @@ export const BodyWidget: FC = ({ setSaved(false); } }, []); + + // Schedule a single canvas update per frame and ignore incomplete link drags + const scheduleCanvasEmit = React.useMemo(() => { + let scheduled = false; + return () => { + if (scheduled) return; + scheduled = true; + requestAnimationFrame(() => { + scheduled = false; + + const model = xircuitsApp.getDiagramEngine().getModel(); + + // skip if a link is still being dragged (no target port yet) + const draggingUnfinished = + !!model && + Object.values(model.getLinks?.() ?? {}).some( + (l: any) => !(l?.getTargetPort?.()) + ); + if (!draggingUnfinished) { + canvasUpdatedSignal.emit({ reason: 'content' }); + } + }); + }; + }, [xircuitsApp]); const onChange = useCallback((): void => { if (skipSerializationRef.current) { return; } serializeModel(); + scheduleCanvasEmit(); }, [serializeModel]); @@ -471,6 +500,7 @@ export const BodyWidget: FC = ({ return () => clearTimeout(timeout); }, linksUpdated: (event) => { + scheduleCanvasEmit(); const timeout = setTimeout(() => { event.link.registerListener({ sourcePortChanged: () => { @@ -494,6 +524,7 @@ export const BodyWidget: FC = ({ xircuitsApp.getDiagramEngine().setModel(deserializedModel); clearSearchFlags(); + // On the first load, clear undo history and register global engine listeners if (initialRender.current) { currentContext.model.sharedModel.clearUndoHistory();