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 61a69d7d8a issue #3119: drag the execution viewport to navigate the
canvas (#7932)
61a69d7d8a is described below
commit 61a69d7d8a08c06b91d30e24769e416bd5309b58
Author: Matt Casters <[email protected]>
AuthorDate: Thu Aug 13 15:35:43 2026 +0200
issue #3119: drag the execution viewport to navigate the canvas (#7932)
The execution viewers painted the minimap but never started a viewport
drag on mouse down, so the blue overlay did nothing on desktop or Hop
Web. Wire setupDragViewPort, allow click-to-jump on the minimap, and
apply the final offset on mouse up in RAP.
---
.../org/apache/hop/ui/hopgui/canvas-svg.js | 38 +++++++-
.../hopgui/file/pipeline/HopGuiPipelineGraph.java | 2 +
.../hopgui/file/workflow/HopGuiWorkflowGraph.java | 2 +
.../perspective/execution/DragViewZoomBase.java | 64 ++++++------
.../execution/PipelineExecutionViewer.java | 9 +-
.../perspective/execution/ViewPortNavigator.java | 107 +++++++++++++++++++++
.../execution/WorkflowExecutionViewer.java | 9 +-
.../hop/ui/hopgui/shared/BaseExecutionViewer.java | 10 +-
.../execution/ViewPortNavigatorTest.java | 81 ++++++++++++++++
9 files changed, 286 insertions(+), 36 deletions(-)
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 de07704935..59376aabe6 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
@@ -1753,6 +1753,16 @@
}
},
+ _isOverNavigationView: function (props, screenX, screenY) {
+ if (!props) {
+ return false;
+ }
+ if (props.graphPort && containsRect(props.graphPort, screenX,
screenY)) {
+ return true;
+ }
+ return !!(props.viewPort && containsRect(props.viewPort, screenX,
screenY));
+ },
+
_beginNavDrag: function (screenX, screenY, viewPort) {
this._navDragActive = true;
this._navDragStartX = screenX;
@@ -1812,9 +1822,27 @@
if (event.button === 0) {
this._pointerHeld = true;
}
- if (event.button === 0 && props.viewPort &&
containsRect(props.viewPort, screenX, screenY)) {
- this._beginNavDrag(screenX, screenY, props.viewPort);
- return;
+ if (event.button === 0 && this._isOverNavigationView(props,
screenX, screenY)) {
+ var startViewPort = props.viewPort;
+ if (startViewPort && !containsRect(startViewPort, screenX,
screenY)) {
+ // Click on the minimap outside the overlay: jump so the
overlay is
+ // centered on the click, then drag from there.
+ var jumped = clampNavPreviewRect(
+ startViewPort,
+ props.graphPort,
+ screenX - startViewPort.width / 2,
+ screenY - startViewPort.height / 2);
+ startViewPort = {
+ x: jumped.x,
+ y: jumped.y,
+ width: startViewPort.width,
+ height: startViewPort.height
+ };
+ }
+ if (startViewPort) {
+ this._beginNavDrag(screenX, screenY, startViewPort);
+ return;
+ }
}
var graph = graphCoords(screenX, screenY, graphProps);
// Always remember mousedown for mode=drag / mode=resize previews
after server arms mode.
@@ -1872,7 +1900,7 @@
}
}
}
- if (!props.viewPort || !containsRect(props.viewPort, screenX,
screenY)) {
+ if (!this._isOverNavigationView(props, screenX, screenY)) {
this._beginSelect(screenX, screenY);
}
return;
@@ -2015,7 +2043,7 @@
if (this._syncServerModePreviews(screenX, screenY, graph.x,
graph.y, buttonsDown)) {
return;
}
- if (props.viewPort && containsRect(props.viewPort, screenX,
screenY)) {
+ if (this._isOverNavigationView(props, screenX, screenY)) {
this._canvas.style.cursor = "grab";
this._clearNoteResizeHandles();
return;
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/HopGuiPipelineGraph.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/HopGuiPipelineGraph.java
index b7e1d85029..c09fc4e589 100644
---
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/HopGuiPipelineGraph.java
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/HopGuiPipelineGraph.java
@@ -2405,6 +2405,8 @@ public class HopGuiPipelineGraph extends
HopGuiAbstractGraph
// Change the cursor when the mouse is on the resize edge of a note
if (resizeOver != null) {
setCursor(getDisplay().getSystemCursor(resizeOver.getCursor()));
+ } else if (isOverNavigationView(new Point(event.x, event.y))) {
+ setCursor(getDisplay().getSystemCursor(SWT.CURSOR_SIZEALL));
}
// Change cursor when the mouse is on a hop, note link, or an area that
support hovering
else if (mouseOverNoteLink != null
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/workflow/HopGuiWorkflowGraph.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/workflow/HopGuiWorkflowGraph.java
index 713ce64efa..c9e8b51b01 100644
---
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/workflow/HopGuiWorkflowGraph.java
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/workflow/HopGuiWorkflowGraph.java
@@ -2104,6 +2104,8 @@ public class HopGuiWorkflowGraph extends
HopGuiAbstractGraph
// Change the cursor when the mouse is on the resize edge of a note
if (resizeOver != null) {
setCursor(getDisplay().getSystemCursor(resizeOver.getCursor()));
+ } else if (isOverNavigationView(new Point(event.x, event.y))) {
+ setCursor(getDisplay().getSystemCursor(SWT.CURSOR_SIZEALL));
}
// Change cursor when the mouse is on a hop, note link, or an area that
support hovering
else if (mouseOverNoteLink != null
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/execution/DragViewZoomBase.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/execution/DragViewZoomBase.java
index 633bc9665c..926fa54611 100644
---
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/execution/DragViewZoomBase.java
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/execution/DragViewZoomBase.java
@@ -401,44 +401,54 @@ public abstract class DragViewZoomBase extends Composite {
}
/**
- * See if this is a click on the navigation view inner rectangle with the
goal of dragging it
- * around a bit.
+ * See if this is a click on the navigation minimap. A click on the blue
visible-area overlay
+ * starts a drag. A click on the rest of the minimap first jumps the view so
the overlay is
+ * centered on the click, then starts a drag.
*/
protected boolean setupDragViewPort(Point screenClick) {
- if (viewPort != null && viewPort.contains(screenClick)) {
- viewPortNavigation = true;
- viewPortStart = new Point(screenClick);
- viewDragBaseOffset = new DPoint(offset);
- // Change cursor when dragging view port
- setCursor(getDisplay().getSystemCursor(SWT.CURSOR_SIZEALL));
- return true;
+ if (!ViewPortNavigator.hitMinimap(graphPort, viewPort, screenClick)) {
+ return false;
}
- return false;
+ if (viewPort != null && !viewPort.contains(screenClick)) {
+ Point viewCenter = ViewPortNavigator.viewPortCenter(viewPort);
+ if (viewCenter != null) {
+ offset = computeViewPortDragOffset(new DPoint(offset), viewCenter,
screenClick);
+ validateOffset();
+ redraw();
+ }
+ }
+ viewPortNavigation = true;
+ viewPortStart = new Point(screenClick);
+ viewDragBaseOffset = new DPoint(offset);
+ setCursor(getDisplay().getSystemCursor(SWT.CURSOR_SIZEALL));
+ return true;
}
protected void dragViewPort(Point clickLocation) {
- double deltaX = clickLocation.x - viewPortStart.x;
- double deltaY = clickLocation.y - viewPortStart.y;
+ if (viewPortStart == null || viewDragBaseOffset == null) {
+ return;
+ }
+ offset = computeViewPortDragOffset(viewDragBaseOffset, viewPortStart,
clickLocation);
+ validateOffset();
+ redraw();
+ }
- // Convert pixel delta (in minimap/canvas space) to graph coordinates
using the same
- // scale as the minimap: overlay size in pixels = visible size in graph *
scale.
- //
- double mag = Math.max(0.01, magnification);
+ /**
+ * Convert a pixel drag of the visible-area overlay into a graph offset.
Uses the same
+ * magnification as drawing so the overlay tracks the pointer 1:1.
+ */
+ private DPoint computeViewPortDragOffset(DPoint baseOffset, Point start,
Point current) {
+ double mag = Math.max(0.01, calculateCorrectedMagnification());
Point area = getArea();
double visibleWidthGraph = area.x / mag;
double visibleHeightGraph = area.y / mag;
- if (viewPort.width <= 0 || viewPort.height <= 0) {
- return;
- }
- double scaleX = (double) viewPort.width / visibleWidthGraph;
- double scaleY = (double) viewPort.height / visibleHeightGraph;
- double deltaGraphX = deltaX / scaleX;
- double deltaGraphY = deltaY / scaleY;
-
- offset = new DPoint(viewDragBaseOffset.x - deltaGraphX,
viewDragBaseOffset.y - deltaGraphY);
+ return ViewPortNavigator.dragOffset(
+ baseOffset, viewPort, start, current, visibleWidthGraph,
visibleHeightGraph);
+ }
- validateOffset();
- redraw();
+ /** True when the pointer is over the navigation minimap (frame or
visible-area overlay). */
+ protected boolean isOverNavigationView(Point screenClick) {
+ return ViewPortNavigator.hitMinimap(graphPort, viewPort, screenClick);
}
public void validateOffset() {
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/execution/PipelineExecutionViewer.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/execution/PipelineExecutionViewer.java
index 75378d7530..e50060547d 100644
---
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/execution/PipelineExecutionViewer.java
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/execution/PipelineExecutionViewer.java
@@ -793,6 +793,8 @@ public class PipelineExecutionViewer extends
BaseExecutionViewer
viewPort = pipelinePainter.getViewPort();
graphPort = pipelinePainter.getGraphPort();
+ canvas.setData("viewPort", viewPort);
+ canvas.setData("graphPort", graphPort);
} catch (Exception e) {
new ErrorDialog(hopGui.getActiveShell(), CONST_ERROR, "Error drawing
pipeline image", e);
}
@@ -927,7 +929,12 @@ public class PipelineExecutionViewer extends
BaseExecutionViewer
lastClick = new Point(real.x, real.y);
boolean control = (event.stateMask & SWT.MOD1) != 0;
- if (setupDragView(event.button, control, new Point(event.x, event.y))) {
+ Point clickScreen = new Point(event.x, event.y);
+ if (setupDragViewPort(clickScreen)) {
+ return;
+ }
+
+ if (setupDragView(event.button, control, clickScreen)) {
return;
}
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/execution/ViewPortNavigator.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/execution/ViewPortNavigator.java
new file mode 100644
index 0000000000..edf225ec70
--- /dev/null
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/execution/ViewPortNavigator.java
@@ -0,0 +1,107 @@
+/*
+ * 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.perspective.execution;
+
+import org.apache.hop.core.gui.DPoint;
+import org.apache.hop.core.gui.Point;
+import org.apache.hop.core.gui.Rectangle;
+
+/**
+ * Hit-testing and offset math for the canvas navigation minimap (viewport).
Used by pipeline,
+ * workflow, and execution viewers so dragging the blue overlay (or clicking
the minimap) moves the
+ * visible graph area.
+ */
+public final class ViewPortNavigator {
+
+ private ViewPortNavigator() {}
+
+ /**
+ * True when {@code click} is on the minimap frame or the visible-area
overlay.
+ *
+ * @param graphPort minimap rectangle in canvas pixels, may be null
+ * @param viewPort visible-area overlay in canvas pixels, may be null
+ * @param click canvas pixel location
+ * @return true when the click is on the navigation view
+ */
+ public static boolean hitMinimap(Rectangle graphPort, Rectangle viewPort,
Point click) {
+ if (click == null) {
+ return false;
+ }
+ if (graphPort != null && graphPort.contains(click)) {
+ return true;
+ }
+ return viewPort != null && viewPort.contains(click);
+ }
+
+ /**
+ * Center of the visible-area overlay, used as the drag origin when jumping
the view to a minimap
+ * click outside the overlay.
+ *
+ * @param viewPort visible-area overlay in canvas pixels
+ * @return center point, or null when the overlay is missing
+ */
+ public static Point viewPortCenter(Rectangle viewPort) {
+ if (viewPort == null) {
+ return null;
+ }
+ return new Point(viewPort.x + viewPort.width / 2, viewPort.y +
viewPort.height / 2);
+ }
+
+ /**
+ * Convert a pixel drag of the visible-area overlay into a new graph offset.
+ *
+ * <p>The overlay size in pixels is {@code visibleSizeGraph * scale}. Moving
the overlay right
+ * reveals content further right, which decreases {@code offset.x}.
+ *
+ * @param baseOffset graph offset when the drag started
+ * @param viewPort visible-area overlay in canvas pixels
+ * @param start drag start in canvas pixels
+ * @param current current pointer in canvas pixels
+ * @param visibleWidthGraph visible canvas width in graph coordinates
+ * @param visibleHeightGraph visible canvas height in graph coordinates
+ * @return new graph offset, or a copy of {@code baseOffset} when the drag
cannot be applied
+ */
+ public static DPoint dragOffset(
+ DPoint baseOffset,
+ Rectangle viewPort,
+ Point start,
+ Point current,
+ double visibleWidthGraph,
+ double visibleHeightGraph) {
+ if (baseOffset == null) {
+ return null;
+ }
+ if (viewPort == null
+ || start == null
+ || current == null
+ || viewPort.width <= 0
+ || viewPort.height <= 0
+ || visibleWidthGraph <= 0
+ || visibleHeightGraph <= 0) {
+ return new DPoint(baseOffset);
+ }
+ double scaleX = viewPort.width / visibleWidthGraph;
+ double scaleY = viewPort.height / visibleHeightGraph;
+ if (scaleX == 0 || scaleY == 0) {
+ return new DPoint(baseOffset);
+ }
+ double deltaGraphX = (current.x - start.x) / scaleX;
+ double deltaGraphY = (current.y - start.y) / scaleY;
+ return new DPoint(baseOffset.x - deltaGraphX, baseOffset.y - deltaGraphY);
+ }
+}
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/execution/WorkflowExecutionViewer.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/execution/WorkflowExecutionViewer.java
index 387cca3a3a..f65ef47aa1 100644
---
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/execution/WorkflowExecutionViewer.java
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/execution/WorkflowExecutionViewer.java
@@ -685,6 +685,8 @@ public class WorkflowExecutionViewer extends
BaseExecutionViewer
viewPort = workflowPainter.getViewPort();
graphPort = workflowPainter.getGraphPort();
+ canvas.setData("viewPort", viewPort);
+ canvas.setData("graphPort", graphPort);
} catch (Exception e) {
new ErrorDialog(hopGui.getActiveShell(), CONST_ERROR, "Error drawing
workflow image", e);
}
@@ -823,7 +825,12 @@ public class WorkflowExecutionViewer extends
BaseExecutionViewer
lastClick = new Point(real.x, real.y);
boolean control = (event.stateMask & SWT.MOD1) != 0;
- if (setupDragView(event.button, control, new Point(event.x, event.y))) {
+ Point clickScreen = new Point(event.x, event.y);
+ if (setupDragViewPort(clickScreen)) {
+ return;
+ }
+
+ if (setupDragView(event.button, control, clickScreen)) {
return;
}
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/shared/BaseExecutionViewer.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/shared/BaseExecutionViewer.java
index 73f6834c5c..a8a726be21 100644
--- a/ui/src/main/java/org/apache/hop/ui/hopgui/shared/BaseExecutionViewer.java
+++ b/ui/src/main/java/org/apache/hop/ui/hopgui/shared/BaseExecutionViewer.java
@@ -197,8 +197,8 @@ public abstract class BaseExecutionViewer extends
DragViewZoomBase
AreaOwner areaOwner = getVisibleAreaOwner(real.x, real.y);
Cursor cursor = null;
- // Change cursor when dragging view or view port
- if (viewDrag || viewPortNavigation) {
+ // Change cursor when dragging view or view port, or hovering the minimap
+ if (viewDrag || viewPortNavigation || isOverNavigationView(new
Point(event.x, event.y))) {
cursor = getDisplay().getSystemCursor(SWT.CURSOR_SIZEALL);
}
// Change cursor when hover an action or transform icon
@@ -217,6 +217,12 @@ public abstract class BaseExecutionViewer extends
DragViewZoomBase
@Override
public void mouseUp(MouseEvent event) {
+ // RAP does not send mouse-move events to the server. Apply the final
viewport or
+ // pan position from the mouse-up coordinates, matching
HopGuiPipelineGraph.
+ if (EnvironmentUtils.getInstance().isWeb() && (viewPortNavigation ||
viewDrag)) {
+ mouseMove(event);
+ }
+
if (viewPortNavigation || viewDrag) {
viewDrag = false;
viewPortNavigation = false;
diff --git
a/ui/src/test/java/org/apache/hop/ui/hopgui/perspective/execution/ViewPortNavigatorTest.java
b/ui/src/test/java/org/apache/hop/ui/hopgui/perspective/execution/ViewPortNavigatorTest.java
new file mode 100644
index 0000000000..b9c2e22fba
--- /dev/null
+++
b/ui/src/test/java/org/apache/hop/ui/hopgui/perspective/execution/ViewPortNavigatorTest.java
@@ -0,0 +1,81 @@
+/*
+ * 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.perspective.execution;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.apache.hop.core.gui.DPoint;
+import org.apache.hop.core.gui.Point;
+import org.apache.hop.core.gui.Rectangle;
+import org.junit.jupiter.api.Test;
+
+class ViewPortNavigatorTest {
+
+ @Test
+ void hitMinimapDetectsGraphPortAndViewPort() {
+ Rectangle graphPort = new Rectangle(800, 400, 200, 150);
+ Rectangle viewPort = new Rectangle(820, 420, 40, 30);
+
+ assertTrue(ViewPortNavigator.hitMinimap(graphPort, viewPort, new
Point(810, 410)));
+ assertTrue(ViewPortNavigator.hitMinimap(graphPort, viewPort, new
Point(830, 425)));
+ assertFalse(ViewPortNavigator.hitMinimap(graphPort, viewPort, new
Point(10, 10)));
+ assertFalse(ViewPortNavigator.hitMinimap(graphPort, viewPort, null));
+ assertTrue(ViewPortNavigator.hitMinimap(null, viewPort, new Point(830,
425)));
+ assertFalse(ViewPortNavigator.hitMinimap(null, null, new Point(830, 425)));
+ }
+
+ @Test
+ void viewPortCenterIsMidpoint() {
+ Rectangle viewPort = new Rectangle(100, 200, 40, 20);
+ Point center = ViewPortNavigator.viewPortCenter(viewPort);
+ assertEquals(120, center.x);
+ assertEquals(210, center.y);
+ assertNull(ViewPortNavigator.viewPortCenter(null));
+ }
+
+ @Test
+ void dragOffsetMovesGraphOppositeTheOverlay() {
+ // Overlay is 40px wide and represents 400 graph units (scale 0.1).
+ Rectangle viewPort = new Rectangle(820, 420, 40, 20);
+ DPoint base = new DPoint(-50.0, -80.0);
+
+ // Drag the overlay 10px right and 5px down → graph offset decreases.
+ DPoint moved =
+ ViewPortNavigator.dragOffset(
+ base, viewPort, new Point(830, 425), new Point(840, 430), 400.0,
200.0);
+
+ assertEquals(-150.0, moved.x, 1e-9);
+ assertEquals(-130.0, moved.y, 1e-9);
+ }
+
+ @Test
+ void dragOffsetIgnoresInvalidInputs() {
+ DPoint base = new DPoint(-10.0, -20.0);
+ Rectangle viewPort = new Rectangle(0, 0, 10, 10);
+
+ DPoint same =
+ ViewPortNavigator.dragOffset(base, viewPort, new Point(0, 0), new
Point(5, 5), 0.0, 10.0);
+ assertEquals(-10.0, same.x, 1e-9);
+ assertEquals(-20.0, same.y, 1e-9);
+ assertNull(
+ ViewPortNavigator.dragOffset(null, viewPort, new Point(0, 0), new
Point(1, 1), 10, 10));
+ }
+}