This is an automated email from the ASF dual-hosted git repository.

hansva pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hop.git


The following commit(s) were added to refs/heads/main by this push:
     new a8c5a9ac56 Issue #8432 : Give each Hop Web graph canvas its own SVG 
renderer remote (#8439)
a8c5a9ac56 is described below

commit a8c5a9ac56759ecc07f8cf23ea725a4ed18188bf
Author: Matt Casters <[email protected]>
AuthorDate: Thu Sep 17 13:21:21 2026 +0200

    Issue #8432 : Give each Hop Web graph canvas its own SVG renderer remote 
(#8439)
---
 .../apache/hop/ui/hopgui/CanvasSvgFacadeImpl.java  | 13 +---
 .../apache/hop/ui/hopgui/CanvasZoomHandler.java    | 53 +++++++------
 .../hop/ui/hopgui/canvas/CanvasGraphRegistry.java  | 82 +++++++++++++-------
 .../ui/hopgui/canvas/CanvasInteractionHandler.java | 25 +++---
 .../ui/hopgui/canvas/CanvasSvgRendererHandler.java | 58 ++++++++++----
 .../org/apache/hop/ui/hopgui/canvas-svg.js         | 89 ++++++++++++++++------
 .../org/apache/hop/ui/hopgui/canvas-zoom.js        | 10 ++-
 .../hop/ui/hopgui/CanvasOverlayAttachTest.java     | 18 ++++-
 .../ui/hopgui/canvas/CanvasGraphRegistryTest.java  | 44 +++++++++++
 .../perspective/explorer/ExplorerPerspective.java  |  2 +-
 10 files changed, 271 insertions(+), 123 deletions(-)

diff --git 
a/rap/src/main/java/org/apache/hop/ui/hopgui/CanvasSvgFacadeImpl.java 
b/rap/src/main/java/org/apache/hop/ui/hopgui/CanvasSvgFacadeImpl.java
index 43e014c00a..622824c2ac 100644
--- a/rap/src/main/java/org/apache/hop/ui/hopgui/CanvasSvgFacadeImpl.java
+++ b/rap/src/main/java/org/apache/hop/ui/hopgui/CanvasSvgFacadeImpl.java
@@ -48,13 +48,12 @@ public class CanvasSvgFacadeImpl extends CanvasSvgFacade {
     CanvasGraphRegistry.getInstance().register(canvasId, canvas, graph);
     canvas.setData("canvasId", canvasId);
     canvas.setData("sessionUuid", getSessionUuidInternal());
-    new CanvasSvgRendererHandler((Composite) canvas.getParent());
+    CanvasSvgRendererHandler.ensureRemote(canvas);
   }
 
   @Override
   void unregisterCanvasInternal(Canvas canvas) {
-    String canvasId = WidgetUtil.getId(canvas);
-    CanvasGraphRegistry.getInstance().unregister(canvasId);
+    CanvasSvgRendererHandler.unregister(canvas);
   }
 
   @Override
@@ -125,13 +124,7 @@ public class CanvasSvgFacadeImpl extends CanvasSvgFacade {
     Object graph = registry.getGraph(canvasId);
     syncAreaOwnersToGraph(graph, result.getAreaOwners());
     setCanvasWidgetDataInternal(canvas, revision);
-    // Single shared client renderer for the session: only rebind when this 
canvas is the active
-    // tab. Otherwise a background paint (e.g. mouse-up on the graph we just 
navigated away from
-    // after following a BV/DV link) steals the overlay back to the previous 
model.
-    Canvas active = registry.getActiveCanvas();
-    if (active == null || active.isDisposed() || active == canvas) {
-      CanvasSvgRendererHandler.notifyCanvasReady(canvas, revision);
-    }
+    CanvasSvgRendererHandler.notifyCanvasReady(canvas, revision);
   }
 
   @Override
diff --git a/rap/src/main/java/org/apache/hop/ui/hopgui/CanvasZoomHandler.java 
b/rap/src/main/java/org/apache/hop/ui/hopgui/CanvasZoomHandler.java
index ac459b2e91..4f0d66d357 100644
--- a/rap/src/main/java/org/apache/hop/ui/hopgui/CanvasZoomHandler.java
+++ b/rap/src/main/java/org/apache/hop/ui/hopgui/CanvasZoomHandler.java
@@ -53,44 +53,41 @@ public class CanvasZoomHandler extends Widget {
     }
 
     CanvasGraphRegistry registry = CanvasGraphRegistry.getInstance();
-    registry.setActiveCanvas(canvas);
-    registry.setActiveZoomable(zoomable);
-
-    RemoteObject remoteObject = registry.getZoomRemote();
+    String canvasId = WidgetUtil.getId(canvas);
+    RemoteObject remoteObject = registry.getZoomRemote(canvasId);
     if (remoteObject == null) {
-      createRemoteObject(registry);
+      createRemoteObject(registry, canvasId);
     } else {
-      updateCanvas(remoteObject);
+      remoteObject.call("attachListener", null);
     }
   }
 
-  private void createRemoteObject(CanvasGraphRegistry registry) {
+  private void createRemoteObject(CanvasGraphRegistry registry, String 
canvasId) {
     try {
       Connection connection = RWT.getUISession().getConnection();
-      Canvas currentCanvas = registry.getActiveCanvas();
 
       RemoteObject remoteObject = 
connection.createRemoteObject("hop.CanvasZoom");
       remoteObject.set("self", remoteObject.getId());
-      remoteObject.set("canvas", WidgetUtil.getId(currentCanvas));
+      remoteObject.set("canvas", canvasId);
 
       remoteObject.setHandler(
           new AbstractOperationHandler() {
             @Override
             public void handleNotify(String event, JsonObject properties) {
               if ("zoom".equals(event)) {
-                handleZoom(registry, properties);
+                handleZoom(properties);
               }
             }
           });
 
       remoteObject.listen("zoom", true);
-      registry.setZoomRemote(remoteObject);
+      registry.putZoomRemote(canvasId, remoteObject);
 
       getDisplay()
           .timerExec(
               50,
               () -> {
-                RemoteObject ro = registry.getZoomRemote();
+                RemoteObject ro = registry.getZoomRemote(canvasId);
                 if (ro != null) {
                   ro.call("attachListener", null);
                 }
@@ -101,13 +98,8 @@ public class CanvasZoomHandler extends Widget {
     }
   }
 
-  private static void handleZoom(CanvasGraphRegistry registry, JsonObject 
properties) {
-    Canvas currentCanvas = registry.getActiveCanvas();
-    Object zoomable = registry.getActiveZoomable();
-    if (currentCanvas == null || zoomable == null) {
-      return;
-    }
-    if (!(zoomable instanceof IZoomable activeZoomable)) {
+  private void handleZoom(JsonObject properties) {
+    if (canvas == null || canvas.isDisposed() || zoomable == null) {
       return;
     }
 
@@ -116,26 +108,33 @@ public class CanvasZoomHandler extends Widget {
     int y = properties.get("y").asInt();
 
     org.eclipse.swt.widgets.Event swtEvent = new 
org.eclipse.swt.widgets.Event();
-    swtEvent.widget = currentCanvas;
+    swtEvent.widget = canvas;
     swtEvent.x = x;
     swtEvent.y = y;
     swtEvent.count = count;
     MouseEvent mouseEvent = new MouseEvent(swtEvent);
 
     if (count > 0) {
-      activeZoomable.zoomIn(mouseEvent);
+      zoomable.zoomIn(mouseEvent);
     } else {
-      activeZoomable.zoomOut(mouseEvent);
+      zoomable.zoomOut(mouseEvent);
     }
   }
 
-  private void updateCanvas(RemoteObject remoteObject) {
-    remoteObject.set("canvas", WidgetUtil.getId(canvas));
-    remoteObject.call("attachListener", null);
-  }
-
   @Override
   public void dispose() {
+    try {
+      if (canvas != null && !canvas.isDisposed()) {
+        RemoteObject remoteObject =
+            
CanvasGraphRegistry.getInstance().removeZoomRemote(WidgetUtil.getId(canvas));
+        if (remoteObject != null) {
+          remoteObject.destroy();
+        }
+      }
+    } catch (Exception e) {
+      LogChannel.UI.logDebug(
+          "Failed to destroy CanvasZoomHandler remote object: " + 
e.getMessage());
+    }
     CanvasGraphRegistry registry = CanvasGraphRegistry.getInstance();
     if (registry.getActiveCanvas() == this.canvas) {
       registry.setActiveCanvas(null);
diff --git 
a/rap/src/main/java/org/apache/hop/ui/hopgui/canvas/CanvasGraphRegistry.java 
b/rap/src/main/java/org/apache/hop/ui/hopgui/canvas/CanvasGraphRegistry.java
index 2c3ccf08b2..2d130b5145 100644
--- a/rap/src/main/java/org/apache/hop/ui/hopgui/canvas/CanvasGraphRegistry.java
+++ b/rap/src/main/java/org/apache/hop/ui/hopgui/canvas/CanvasGraphRegistry.java
@@ -20,13 +20,15 @@ package org.apache.hop.ui.hopgui.canvas;
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.atomic.AtomicLong;
+import org.apache.hop.core.logging.LogChannel;
 import org.eclipse.rap.rwt.SingletonUtil;
 import org.eclipse.rap.rwt.remote.RemoteObject;
 import org.eclipse.swt.widgets.Canvas;
 
 /**
  * Per UISession registry of pipeline/workflow canvas renders. Stores the 
latest SVG snapshot and
- * click-map for each canvas widget.
+ * click-map for each canvas widget, plus one RAP remote object per canvas for 
the SVG overlay,
+ * hover bridge, and wheel zoom.
  */
 public class CanvasGraphRegistry {
 
@@ -35,20 +37,14 @@ public class CanvasGraphRegistry {
   private final Map<String, CanvasRenderSnapshot> snapshots = new 
ConcurrentHashMap<>();
   private final Map<String, Object> graphsByCanvasId = new 
ConcurrentHashMap<>();
   private final Map<String, Canvas> canvasById = new ConcurrentHashMap<>();
+  private final Map<String, RemoteObject> svgRendererRemotes = new 
ConcurrentHashMap<>();
+  private final Map<String, RemoteObject> interactionRemotes = new 
ConcurrentHashMap<>();
+  private final Map<String, RemoteObject> zoomRemotes = new 
ConcurrentHashMap<>();
   private final AtomicLong revisionCounter = new AtomicLong(1);
 
-  /** Per-session remote object for the SVG canvas client (must not be static 
across sessions). */
-  private RemoteObject svgRendererRemote;
-
-  /** Per-session remote object for canvas hover notifications. */
-  private RemoteObject interactionRemote;
-
-  /** Active canvas for hover routing within this UI session. */
+  /** Active canvas for hover/zoom routing within this UI session. */
   private Canvas activeCanvas;
 
-  /** Per-session remote object for mouse-wheel zoom. */
-  private RemoteObject zoomRemote;
-
   /** Active zoom target (HopGuiPipelineGraph / HopGuiWorkflowGraph). */
   private Object activeZoomable;
 
@@ -62,9 +58,16 @@ public class CanvasGraphRegistry {
   }
 
   public void unregister(String canvasId) {
-    canvasById.remove(canvasId);
+    Canvas canvas = canvasById.remove(canvasId);
     graphsByCanvasId.remove(canvasId);
     snapshots.remove(canvasId);
+    destroyRemote(svgRendererRemotes.remove(canvasId));
+    destroyRemote(interactionRemotes.remove(canvasId));
+    destroyRemote(zoomRemotes.remove(canvasId));
+    if (activeCanvas != null && (activeCanvas == canvas || 
isDisposed(activeCanvas))) {
+      activeCanvas = null;
+      activeZoomable = null;
+    }
   }
 
   public void updateSnapshot(String canvasId, CanvasRenderSnapshot snapshot) {
@@ -92,36 +95,40 @@ public class CanvasGraphRegistry {
     return snapshot == null ? 0 : snapshot.getRevision();
   }
 
-  public RemoteObject getSvgRendererRemote() {
-    return svgRendererRemote;
+  public RemoteObject getSvgRendererRemote(String canvasId) {
+    return canvasId == null ? null : svgRendererRemotes.get(canvasId);
   }
 
-  public void setSvgRendererRemote(RemoteObject svgRendererRemote) {
-    this.svgRendererRemote = svgRendererRemote;
+  public void putSvgRendererRemote(String canvasId, RemoteObject remoteObject) 
{
+    svgRendererRemotes.put(canvasId, remoteObject);
   }
 
-  public RemoteObject getInteractionRemote() {
-    return interactionRemote;
+  public RemoteObject getInteractionRemote(String canvasId) {
+    return canvasId == null ? null : interactionRemotes.get(canvasId);
   }
 
-  public void setInteractionRemote(RemoteObject interactionRemote) {
-    this.interactionRemote = interactionRemote;
+  public void putInteractionRemote(String canvasId, RemoteObject remoteObject) 
{
+    interactionRemotes.put(canvasId, remoteObject);
   }
 
-  public Canvas getActiveCanvas() {
-    return activeCanvas;
+  public RemoteObject getZoomRemote(String canvasId) {
+    return canvasId == null ? null : zoomRemotes.get(canvasId);
   }
 
-  public void setActiveCanvas(Canvas activeCanvas) {
-    this.activeCanvas = activeCanvas;
+  public void putZoomRemote(String canvasId, RemoteObject remoteObject) {
+    zoomRemotes.put(canvasId, remoteObject);
   }
 
-  public RemoteObject getZoomRemote() {
-    return zoomRemote;
+  public RemoteObject removeZoomRemote(String canvasId) {
+    return canvasId == null ? null : zoomRemotes.remove(canvasId);
   }
 
-  public void setZoomRemote(RemoteObject zoomRemote) {
-    this.zoomRemote = zoomRemote;
+  public Canvas getActiveCanvas() {
+    return activeCanvas;
+  }
+
+  public void setActiveCanvas(Canvas activeCanvas) {
+    this.activeCanvas = activeCanvas;
   }
 
   public Object getActiveZoomable() {
@@ -131,4 +138,23 @@ public class CanvasGraphRegistry {
   public void setActiveZoomable(Object activeZoomable) {
     this.activeZoomable = activeZoomable;
   }
+
+  private static boolean isDisposed(Canvas canvas) {
+    try {
+      return canvas.isDisposed();
+    } catch (Exception e) {
+      return true;
+    }
+  }
+
+  private static void destroyRemote(RemoteObject remoteObject) {
+    if (remoteObject == null) {
+      return;
+    }
+    try {
+      remoteObject.destroy();
+    } catch (Exception e) {
+      LogChannel.UI.logDebug("Failed to destroy canvas remote object: " + 
e.getMessage());
+    }
+  }
 }
diff --git 
a/rap/src/main/java/org/apache/hop/ui/hopgui/canvas/CanvasInteractionHandler.java
 
b/rap/src/main/java/org/apache/hop/ui/hopgui/canvas/CanvasInteractionHandler.java
index 81b3e08035..4b36d18591 100644
--- 
a/rap/src/main/java/org/apache/hop/ui/hopgui/canvas/CanvasInteractionHandler.java
+++ 
b/rap/src/main/java/org/apache/hop/ui/hopgui/canvas/CanvasInteractionHandler.java
@@ -25,6 +25,7 @@ import org.eclipse.rap.rwt.RWT;
 import org.eclipse.rap.rwt.remote.AbstractOperationHandler;
 import org.eclipse.rap.rwt.remote.Connection;
 import org.eclipse.rap.rwt.remote.RemoteObject;
+import org.eclipse.rap.rwt.widgets.WidgetUtil;
 import org.eclipse.swt.widgets.Canvas;
 import org.eclipse.swt.widgets.Composite;
 import org.eclipse.swt.widgets.Widget;
@@ -44,21 +45,21 @@ public class CanvasInteractionHandler extends Widget {
       return;
     }
     CanvasGraphRegistry registry = CanvasGraphRegistry.getInstance();
-    registry.setActiveCanvas(canvas);
-    RemoteObject remoteObject = registry.getInteractionRemote();
+    String canvasId = WidgetUtil.getId(canvas);
+    RemoteObject remoteObject = registry.getInteractionRemote(canvasId);
     if (remoteObject == null) {
-      createRemoteObject(registry, canvas);
+      createRemoteObject(registry, canvasId);
     } else {
-      updateCanvas(remoteObject, canvas);
+      remoteObject.call("attachListener", null);
     }
   }
 
-  private static void createRemoteObject(CanvasGraphRegistry registry, Canvas 
canvas) {
+  private static void createRemoteObject(CanvasGraphRegistry registry, String 
canvasId) {
     try {
       Connection connection = RWT.getUISession().getConnection();
       RemoteObject remoteObject = 
connection.createRemoteObject("hop.CanvasInteraction");
       remoteObject.set("self", remoteObject.getId());
-      remoteObject.set("canvas", 
org.eclipse.rap.rwt.widgets.WidgetUtil.getId(canvas));
+      remoteObject.set("canvas", canvasId);
       remoteObject.setHandler(
           new AbstractOperationHandler() {
             @Override
@@ -69,25 +70,19 @@ public class CanvasInteractionHandler extends Widget {
             }
           });
       remoteObject.listen("hover", true);
-      registry.setInteractionRemote(remoteObject);
+      registry.putInteractionRemote(canvasId, remoteObject);
       remoteObject.call("attachListener", null);
     } catch (Exception e) {
       LogChannel.UI.logError("Failed to create CanvasInteractionHandler remote 
object", e);
     }
   }
 
-  private static void updateCanvas(RemoteObject remoteObject, Canvas canvas) {
-    remoteObject.set("canvas", 
org.eclipse.rap.rwt.widgets.WidgetUtil.getId(canvas));
-    remoteObject.call("attachListener", null);
-  }
-
   private static void handleHover(JsonObject properties) {
-    CanvasGraphRegistry registry = CanvasGraphRegistry.getInstance();
-    if (registry.getActiveCanvas() == null) {
+    if (properties.get("canvasId") == null) {
       return;
     }
     String canvasId = properties.get("canvasId").asString();
-    Object graph = registry.getGraph(canvasId);
+    Object graph = CanvasGraphRegistry.getInstance().getGraph(canvasId);
     if (graph == null) {
       return;
     }
diff --git 
a/rap/src/main/java/org/apache/hop/ui/hopgui/canvas/CanvasSvgRendererHandler.java
 
b/rap/src/main/java/org/apache/hop/ui/hopgui/canvas/CanvasSvgRendererHandler.java
index 002675bee0..5edca97115 100644
--- 
a/rap/src/main/java/org/apache/hop/ui/hopgui/canvas/CanvasSvgRendererHandler.java
+++ 
b/rap/src/main/java/org/apache/hop/ui/hopgui/canvas/CanvasSvgRendererHandler.java
@@ -4,7 +4,7 @@
  * this work for additional information regarding copyright ownership.
  * The ASF licenses this file to You under the Apache License, Version 2.0
  * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
+ * the License. You may obtain a copy of the License at
  *
  *      http://www.apache.org/licenses/LICENSE-2.0
  *
@@ -24,14 +24,24 @@ import org.eclipse.rap.rwt.remote.Connection;
 import org.eclipse.rap.rwt.remote.RemoteObject;
 import org.eclipse.rap.rwt.widgets.WidgetUtil;
 import org.eclipse.swt.widgets.Canvas;
-import org.eclipse.swt.widgets.Composite;
-import org.eclipse.swt.widgets.Widget;
 
-/** Pushes SVG render metadata to the Hop Web canvas client. */
-public class CanvasSvgRendererHandler extends Widget {
+/**
+ * Pushes SVG render metadata to the Hop Web canvas client. One {@code 
hop.CanvasSvgRenderer} remote
+ * object is created per canvas so dialogs and editor tabs can keep 
independent overlays.
+ */
+public final class CanvasSvgRendererHandler {
+
+  private CanvasSvgRendererHandler() {}
 
-  public CanvasSvgRendererHandler(Composite parent) {
-    super(parent, 0);
+  public static void ensureRemote(Canvas canvas) {
+    if (canvas == null || canvas.isDisposed()) {
+      return;
+    }
+    String canvasId = WidgetUtil.getId(canvas);
+    CanvasGraphRegistry registry = CanvasGraphRegistry.getInstance();
+    if (registry.getSvgRendererRemote(canvasId) == null) {
+      createRemoteObject(registry, canvasId);
+    }
   }
 
   public static void notifyCanvasReady(Canvas canvas, long revision) {
@@ -39,23 +49,40 @@ public class CanvasSvgRendererHandler extends Widget {
       return;
     }
     CanvasGraphRegistry registry = CanvasGraphRegistry.getInstance();
-    registry.setActiveCanvas(canvas);
-    RemoteObject remoteObject = registry.getSvgRendererRemote();
+    String canvasId = WidgetUtil.getId(canvas);
+    RemoteObject remoteObject = registry.getSvgRendererRemote(canvasId);
     if (remoteObject == null) {
-      remoteObject = createRemoteObject(registry, canvas);
+      remoteObject = createRemoteObject(registry, canvasId);
     }
     if (remoteObject != null) {
-      updateRemoteObject(remoteObject, canvas, revision);
+      updateRemoteObject(remoteObject, revision);
+    }
+  }
+
+  public static void unregister(Canvas canvas) {
+    if (canvas == null) {
+      return;
+    }
+    try {
+      CanvasGraphRegistry.getInstance().unregister(WidgetUtil.getId(canvas));
+    } catch (Exception e) {
+      LogChannel.UI.logDebug(
+          "Failed to unregister Canvas SVG renderer RemoteObject: " + 
e.getMessage());
     }
   }
 
-  private static RemoteObject createRemoteObject(CanvasGraphRegistry registry, 
Canvas canvas) {
+  private static RemoteObject createRemoteObject(CanvasGraphRegistry registry, 
String canvasId) {
     try {
       Connection connection = RWT.getUISession().getConnection();
       RemoteObject remoteObject = 
connection.createRemoteObject("hop.CanvasSvgRenderer");
       remoteObject.set("self", remoteObject.getId());
-      registry.setSvgRendererRemote(remoteObject);
-      updateRemoteObject(remoteObject, canvas, 0);
+      remoteObject.set("sessionUuid", CanvasSvgFacade.getSessionUuid());
+      remoteObject.set("canvasId", canvasId);
+      remoteObject.set("renderRevision", 0L);
+      remoteObject.set(
+          "serviceHandlerUrl",
+          
RWT.getServiceManager().getServiceHandlerUrl(CanvasRenderServiceHandler.SERVICE_ID));
+      registry.putSvgRendererRemote(canvasId, remoteObject);
       remoteObject.call("attachListener", null);
       return remoteObject;
     } catch (Exception e) {
@@ -64,9 +91,8 @@ public class CanvasSvgRendererHandler extends Widget {
     }
   }
 
-  private static void updateRemoteObject(RemoteObject remoteObject, Canvas 
canvas, long revision) {
+  private static void updateRemoteObject(RemoteObject remoteObject, long 
revision) {
     remoteObject.set("sessionUuid", CanvasSvgFacade.getSessionUuid());
-    remoteObject.set("canvasId", WidgetUtil.getId(canvas));
     remoteObject.set("renderRevision", revision);
     remoteObject.set(
         "serviceHandlerUrl",
diff --git a/rap/src/main/resources/org/apache/hop/ui/hopgui/canvas-svg.js 
b/rap/src/main/resources/org/apache/hop/ui/hopgui/canvas-svg.js
index d245a2af71..f6033d4a38 100644
--- a/rap/src/main/resources/org/apache/hop/ui/hopgui/canvas-svg.js
+++ b/rap/src/main/resources/org/apache/hop/ui/hopgui/canvas-svg.js
@@ -22,6 +22,9 @@
     if (!window.hop) {
         window.hop = {};
     }
+    if (!hop._canvasInteractions) {
+        hop._canvasInteractions = {};
+    }
 
     var serviceBasePath = (function () {
         var path = window.location.pathname;
@@ -398,6 +401,25 @@
             if (this._overlay && this._overlay.parentNode) {
                 this._overlay.parentNode.removeChild(this._overlay);
             }
+            this._resetOverlayDom();
+        },
+
+        _resetOverlayDom: function () {
+            this._overlay = null;
+            this._svgHost = null;
+            this._effectsLayer = null;
+            this._panBoundsOutline = null;
+            this._navViewportPreview = null;
+            this._selectLasso = null;
+            this._ghostSvg = null;
+            this._ghostRectPool = null;
+            this._hopLineEl = null;
+            this._dragPreviewRects = null;
+            this._noteHandleRects = null;
+        },
+
+        _overlayIsLive: function () {
+            return !!(this._overlay && this._overlay.isConnected);
         },
 
         attachListener: function () {
@@ -409,6 +431,9 @@
         },
 
         setCanvasId: function (properties) {
+            if (this._canvasId && this._canvasId !== properties.value) {
+                return;
+            }
             this._canvasId = properties.value;
         },
 
@@ -445,13 +470,11 @@
                     return;
                 }
                 self._findTimer = null;
-                if (self._canvas === canvas) {
-                    var parent = canvas.parentElement;
-                    if (parent && self._overlay && self._overlay.parentNode 
!== parent) {
-                        self._attachToCanvas(canvas);
-                    } else if (self._overlay) {
-                        self._syncOverlayLayout(canvas);
-                    }
+                if (!self._overlayIsLive()) {
+                    self._resetOverlayDom();
+                }
+                if (self._canvas === canvas && self._overlayIsLive()) {
+                    self._syncOverlayLayout(canvas);
                     return;
                 }
                 self._attachToCanvas(canvas);
@@ -473,6 +496,10 @@
             this._detachInteractionListeners();
             this._canvas = canvas;
 
+            if (this._overlay && !this._overlayIsLive()) {
+                this._resetOverlayDom();
+            }
+
             if (!this._overlay) {
                 this._overlay = document.createElement("div");
                 this._overlay.style.position = "absolute";
@@ -533,7 +560,7 @@
             }
 
             var parent = canvas.parentElement;
-            if (parent && this._overlay.parentNode !== parent) {
+            if (parent && !this._overlay.parentNode) {
                 if (window.getComputedStyle(parent).position === "static") {
                     parent.style.position = "relative";
                 }
@@ -2194,7 +2221,7 @@
                 return;
             }
             this._lastHoverKey = hoverKey;
-            var interaction = hop._canvasInteractionInstance;
+            var interaction = hop._canvasInteractions && 
hop._canvasInteractions[this._canvasId];
             if (area && area.hover && interaction && 
interaction._remoteObject) {
                 interaction._remoteObject.notify("hover", {
                     canvasId: this._canvasId,
@@ -2220,19 +2247,13 @@
                 widget._sessionUuid = value;
             },
             canvasId: function (widget, value) {
-                var changed = widget._canvasId !== value;
-                widget._canvasId = value;
-                // Tab switch: re-bind overlay to the newly active canvas and 
force SVG fetch.
-                if (changed) {
-                    widget._revision = 0;
-                    if (widget._svgHost) {
-                        widget._svgHost.innerHTML = "";
-                    }
-                    widget._findAndAttachCanvas();
-                    widget._fetchAndRender(0);
-                } else {
-                    widget._findAndAttachCanvas();
+                // canvasId is instance identity. A second canvas must get its 
own remote;
+                // never steal this overlay by rewriting the id (issue #8432).
+                if (widget._canvasId && widget._canvasId !== value) {
+                    return;
                 }
+                widget._canvasId = value;
+                widget._findAndAttachCanvas();
             },
             renderRevision: function (widget, value) {
                 if (value !== widget._revision) {
@@ -2249,17 +2270,28 @@
     hop.CanvasInteraction = function (properties) {
         this._canvasId = properties.canvas;
         this._remoteObject = null;
-        hop._canvasInteractionInstance = this;
+        if (!hop._canvasInteractions) {
+            hop._canvasInteractions = {};
+        }
+        if (this._canvasId) {
+            hop._canvasInteractions[this._canvasId] = this;
+        }
     };
 
     hop.CanvasInteraction.prototype = {
         destroy: function () {
-            if (hop._canvasInteractionInstance === this) {
-                hop._canvasInteractionInstance = null;
+            if (this._canvasId && hop._canvasInteractions && 
hop._canvasInteractions[this._canvasId] === this) {
+                delete hop._canvasInteractions[this._canvasId];
             }
         },
         attachListener: function () {
             this._remoteObject = rap.getRemoteObject(this);
+            if (this._canvasId) {
+                if (!hop._canvasInteractions) {
+                    hop._canvasInteractions = {};
+                }
+                hop._canvasInteractions[this._canvasId] = this;
+            }
         }
     };
 
@@ -2273,7 +2305,16 @@
         events: ["hover"],
         propertyHandler: {
             canvas: function (widget, value) {
+                if (widget._canvasId && hop._canvasInteractions && 
hop._canvasInteractions[widget._canvasId] === widget) {
+                    delete hop._canvasInteractions[widget._canvasId];
+                }
                 widget._canvasId = value;
+                if (!hop._canvasInteractions) {
+                    hop._canvasInteractions = {};
+                }
+                if (value) {
+                    hop._canvasInteractions[value] = widget;
+                }
             }
         }
     });
diff --git a/rap/src/main/resources/org/apache/hop/ui/hopgui/canvas-zoom.js 
b/rap/src/main/resources/org/apache/hop/ui/hopgui/canvas-zoom.js
index 8c964e3d2f..1e3f47a28b 100644
--- a/rap/src/main/resources/org/apache/hop/ui/hopgui/canvas-zoom.js
+++ b/rap/src/main/resources/org/apache/hop/ui/hopgui/canvas-zoom.js
@@ -147,7 +147,11 @@
         
         // Method called when the canvas property is updated from Java
         setCanvas: function(properties) {
-            this._canvasId = properties.canvasId;
+            var canvasId = properties.canvasId;
+            if (this._canvasId && this._canvasId !== canvasId) {
+                return;
+            }
+            this._canvasId = canvasId;
             this._findAndAttachCanvas();
         },
 
@@ -256,6 +260,10 @@
         events: ["zoom"],
         propertyHandler: {
             canvas: function(widget, value) {
+                // canvas is instance identity. Do not steal another graph's 
wheel listener.
+                if (widget._canvasId && widget._canvasId !== value) {
+                    return;
+                }
                 // When canvas property is updated from Java, re-attach wheel 
to that canvas.
                 widget._canvasId = value;
                 widget._findAndAttachCanvas();
diff --git 
a/rap/src/test/java/org/apache/hop/ui/hopgui/CanvasOverlayAttachTest.java 
b/rap/src/test/java/org/apache/hop/ui/hopgui/CanvasOverlayAttachTest.java
index c78ae9c836..701f07516f 100644
--- a/rap/src/test/java/org/apache/hop/ui/hopgui/CanvasOverlayAttachTest.java
+++ b/rap/src/test/java/org/apache/hop/ui/hopgui/CanvasOverlayAttachTest.java
@@ -27,11 +27,14 @@ import java.nio.charset.StandardCharsets;
 import org.junit.jupiter.api.Test;
 
 /**
- * Guards the Hop Web canvas overlay against issue #8286.
+ * Guards the Hop Web canvas overlay against issues #8286 and #8432.
  *
  * <p>RAP does not put widget ids on DOM elements unless enableUITests is on, 
so looking up {@code
  * document.getElementById(canvasId)} and then guessing "the first canvas 
larger than 500x500" left
  * a blank graph in a small viewport and drew the graph inside a dialog when 
one was open.
+ *
+ * <p>A session-wide {@code hop.CanvasSvgRenderer} remote then moved that 
overlay into a second
+ * canvas (dialog over editor tab). Each canvas must keep its own overlay for 
its lifetime.
  */
 class CanvasOverlayAttachTest {
 
@@ -45,6 +48,18 @@ class CanvasOverlayAttachTest {
     assertTrue(js.contains("this._canvasId = properties.canvasId"), js);
   }
 
+  @Test
+  void svgOverlayIsPinnedToOneCanvasAndRecreatedIfDestroyed() throws 
IOException {
+    String js = readResource("org/apache/hop/ui/hopgui/canvas-svg.js");
+
+    assertFalse(js.contains("_overlay.parentNode !== parent"), js);
+    assertFalse(js.contains("widget._svgHost.innerHTML = \"\""), js);
+    assertTrue(js.contains("_overlayIsLive"), js);
+    assertTrue(js.contains("isConnected"), js);
+    assertTrue(js.contains("hop._canvasInteractions"), js);
+    assertFalse(js.contains("hop._canvasInteractionInstance"), js);
+  }
+
   @Test
   void zoomResolvesTheRapWidgetNotALargeCanvas() throws IOException {
     String js = readResource("org/apache/hop/ui/hopgui/canvas-zoom.js");
@@ -52,6 +67,7 @@ class CanvasOverlayAttachTest {
     assertFalse(js.contains("rect.width > 500"), js);
     assertTrue(js.contains("ObjectRegistry"), js);
     assertTrue(js.contains("this._canvasId = properties.canvas"), js);
+    assertTrue(js.contains("widget._canvasId && widget._canvasId !== value"), 
js);
   }
 
   @Test
diff --git 
a/rap/src/test/java/org/apache/hop/ui/hopgui/canvas/CanvasGraphRegistryTest.java
 
b/rap/src/test/java/org/apache/hop/ui/hopgui/canvas/CanvasGraphRegistryTest.java
new file mode 100644
index 0000000000..4352f80468
--- /dev/null
+++ 
b/rap/src/test/java/org/apache/hop/ui/hopgui/canvas/CanvasGraphRegistryTest.java
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.ui.hopgui.canvas;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import java.util.List;
+import org.eclipse.rap.json.JsonObject;
+import org.junit.jupiter.api.Test;
+
+class CanvasGraphRegistryTest {
+
+  @Test
+  void unregisterDropsOnlyThatCanvasSnapshot() {
+    CanvasGraphRegistry registry = new CanvasGraphRegistry();
+    registry.updateSnapshot(
+        "a", new CanvasRenderSnapshot(1, "<svg a/>", List.of(), new 
JsonObject()));
+    registry.updateSnapshot(
+        "b", new CanvasRenderSnapshot(2, "<svg b/>", List.of(), new 
JsonObject()));
+
+    registry.unregister("a");
+
+    assertNull(registry.getSnapshot("a"));
+    assertEquals(0, registry.getCurrentRevision("a"));
+    assertEquals(2, registry.getCurrentRevision("b"));
+    assertEquals("<svg b/>", registry.getSnapshot("b").getSvg());
+  }
+}
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/ExplorerPerspective.java
 
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/ExplorerPerspective.java
index 270a1406e1..f8a537f101 100644
--- 
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/ExplorerPerspective.java
+++ 
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/ExplorerPerspective.java
@@ -5097,7 +5097,7 @@ public class ExplorerPerspective implements 
IHopPerspective, TabClosable, IFileD
       CanvasSvgHelper.notifyCanvasReady(workflowGraph.getCanvas());
     } else if (activeHandler instanceof HopGuiAbstractGraph abstractGraph) {
       // Plugin model graphs (Data Vault, Business Vault, dimensional, source 
model, …)
-      // share the single Hop Web SVG renderer and zoom remote; re-bind on tab 
switch.
+      // each have their own Hop Web SVG renderer and zoom remotes; refresh on 
tab switch.
       Object zoomHandler = getModelGraphZoomHandler(abstractGraph);
       if (zoomHandler != null) {
         CanvasZoomHelper.notifyCanvasReady(zoomHandler);

Reply via email to