From 2554523f90cd72b8c9a669abf9e141ac74eac16a Mon Sep 17 00:00:00 2001 From: rabea-Al Date: Sat, 4 Oct 2025 21:00:23 +0800 Subject: [PATCH 1/2] Add automatic sidebar sync with canvas updates --- .../ComponentPreviewWidget.tsx | 50 ++++++++++++++++++- src/components/XircuitsBodyWidget.tsx | 7 +++ 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/src/component_info_sidebar/ComponentPreviewWidget.tsx b/src/component_info_sidebar/ComponentPreviewWidget.tsx index a2e406b7..dd8c3f1c 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,8 +359,52 @@ 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; + + // Skip updating sidebar if a link is still being dragged (incomplete connection) + const hasUnfinishedLink = Object.values(engine.getModel()?.getLinks?.() ?? {}).some( + (link: any) => !link.getTargetPort?.() + ); + if (hasUnfinishedLink) 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) { diff --git a/src/components/XircuitsBodyWidget.tsx b/src/components/XircuitsBodyWidget.tsx index c33f7e72..b6e1e5d1 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, @@ -471,6 +475,7 @@ export const BodyWidget: FC = ({ return () => clearTimeout(timeout); }, linksUpdated: (event) => { + canvasUpdatedSignal.emit({ reason: 'content' }); const timeout = setTimeout(() => { event.link.registerListener({ sourcePortChanged: () => { @@ -494,6 +499,8 @@ export const BodyWidget: FC = ({ xircuitsApp.getDiagramEngine().setModel(deserializedModel); clearSearchFlags(); + canvasUpdatedSignal.emit({ reason: 'content' }); + // On the first load, clear undo history and register global engine listeners if (initialRender.current) { currentContext.model.sharedModel.clearUndoHistory(); From 136a9d4b242362a65d49649eee07acc46349fb4c Mon Sep 17 00:00:00 2001 From: rabea-Al Date: Tue, 14 Oct 2025 01:48:34 +0800 Subject: [PATCH 2/2] Make canvas updates fire only after real link changes, not every small drag --- .../ComponentPreviewWidget.tsx | 6 ---- src/components/XircuitsBodyWidget.tsx | 28 +++++++++++++++++-- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/src/component_info_sidebar/ComponentPreviewWidget.tsx b/src/component_info_sidebar/ComponentPreviewWidget.tsx index dd8c3f1c..d4aee0bd 100644 --- a/src/component_info_sidebar/ComponentPreviewWidget.tsx +++ b/src/component_info_sidebar/ComponentPreviewWidget.tsx @@ -373,12 +373,6 @@ export class ComponentPreviewWidget extends SidePanel { const currentNode = this._model?.node; if (!engine || !currentNode) return; - // Skip updating sidebar if a link is still being dragged (incomplete connection) - const hasUnfinishedLink = Object.values(engine.getModel()?.getLinks?.() ?? {}).some( - (link: any) => !link.getTargetPort?.() - ); - if (hasUnfinishedLink) 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); diff --git a/src/components/XircuitsBodyWidget.tsx b/src/components/XircuitsBodyWidget.tsx index b6e1e5d1..5fecb6be 100644 --- a/src/components/XircuitsBodyWidget.tsx +++ b/src/components/XircuitsBodyWidget.tsx @@ -440,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]); @@ -475,7 +500,7 @@ export const BodyWidget: FC = ({ return () => clearTimeout(timeout); }, linksUpdated: (event) => { - canvasUpdatedSignal.emit({ reason: 'content' }); + scheduleCanvasEmit(); const timeout = setTimeout(() => { event.link.registerListener({ sourcePortChanged: () => { @@ -499,7 +524,6 @@ export const BodyWidget: FC = ({ xircuitsApp.getDiagramEngine().setModel(deserializedModel); clearSearchFlags(); - canvasUpdatedSignal.emit({ reason: 'content' }); // On the first load, clear undo history and register global engine listeners if (initialRender.current) {