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

bamaer 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 c7fc11cf0d Issue #8595 : Don't show data when moving transforms (#8616)
c7fc11cf0d is described below

commit c7fc11cf0d1f3e68744fcbaedb386960328310da
Author: Matt Casters <[email protected]>
AuthorDate: Fri Sep 25 19:44:57 2026 +0200

    Issue #8595 : Don't show data when moving transforms (#8616)
    
    Releasing the mouse after a move no longer opens the output rows when the
    pointer comes up on a data badge. A click on the badge still opens them.
---
 .../HopGuiPipelineGraphClickContractTest.java      |   3 +
 .../HopGuiPipelineGraphOutputDataTest.java         | 359 +++++++++++++++++++++
 .../hopgui/file/pipeline/HopGuiPipelineGraph.java  | 329 ++++++++++++++-----
 3 files changed, 602 insertions(+), 89 deletions(-)

diff --git 
a/rcp/src/test/java/org/apache/hop/ui/hopgui/file/pipeline/HopGuiPipelineGraphClickContractTest.java
 
b/rcp/src/test/java/org/apache/hop/ui/hopgui/file/pipeline/HopGuiPipelineGraphClickContractTest.java
index 10f695f538..0053dc29f7 100644
--- 
a/rcp/src/test/java/org/apache/hop/ui/hopgui/file/pipeline/HopGuiPipelineGraphClickContractTest.java
+++ 
b/rcp/src/test/java/org/apache/hop/ui/hopgui/file/pipeline/HopGuiPipelineGraphClickContractTest.java
@@ -369,6 +369,9 @@ class HopGuiPipelineGraphClickContractTest extends 
GraphCanvasTestBase {
     idle.put("selectionRegion", null);
     idle.put("dragSelection", false);
     idle.put("iconDragStartScreen", null);
+    idle.put("iconDragCommitted", false);
+    idle.put("outputDataPressed", false);
+    idle.put("showingOutputRows", false);
     idle.put("avoidContextDialog", false);
     idle.put("lastButton", 0);
     assertGraphState(graph, idle);
diff --git 
a/rcp/src/test/java/org/apache/hop/ui/hopgui/file/pipeline/HopGuiPipelineGraphOutputDataTest.java
 
b/rcp/src/test/java/org/apache/hop/ui/hopgui/file/pipeline/HopGuiPipelineGraphOutputDataTest.java
new file mode 100644
index 0000000000..876068b023
--- /dev/null
+++ 
b/rcp/src/test/java/org/apache/hop/ui/hopgui/file/pipeline/HopGuiPipelineGraphOutputDataTest.java
@@ -0,0 +1,359 @@
+/*
+ * 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.file.pipeline;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Consumer;
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.gui.AreaOwner;
+import org.apache.hop.core.gui.Point;
+import org.apache.hop.core.row.RowBuffer;
+import org.apache.hop.core.row.RowMeta;
+import org.apache.hop.core.row.value.ValueMetaString;
+import org.apache.hop.i18n.BaseMessages;
+import org.apache.hop.pipeline.PipelineHopMeta;
+import org.apache.hop.pipeline.PipelineMeta;
+import org.apache.hop.pipeline.engines.local.LocalPipelineEngine;
+import org.apache.hop.pipeline.transform.TransformMeta;
+import org.apache.hop.pipeline.transforms.dummy.DummyMeta;
+import org.apache.hop.ui.core.PropsUi;
+import org.apache.hop.ui.hopgui.HopGui;
+import org.apache.hop.ui.hopgui.HopGuiEnvironment;
+import org.apache.hop.ui.hopgui.file.GraphCanvasTestBase;
+import org.apache.hop.ui.hopgui.perspective.explorer.ExplorerPerspective;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.layout.FormLayout;
+import org.eclipse.swt.widgets.Canvas;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swtbot.swt.finder.SWTBot;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Issue #8595: the output-rows badge sits on the corner of a transform and 
part way along a hop, so
+ * the mouse-up of a move often lands on it. That release ends the move. A 
click on the badge still
+ * opens the rows, once, and leaves the canvas idle so the next click is not 
another move.
+ */
+@Tag("uitest")
+class HopGuiPipelineGraphOutputDataTest extends GraphCanvasTestBase {
+
+  private static final String SOURCE_TRANSFORM = "Read";
+  private static final String TARGET_TRANSFORM = "Write";
+  private static final Point SOURCE_LOCATION = new Point(80, 80);
+  private static final Point TARGET_LOCATION = new Point(320, 80);
+  private static final int DRAG_Y = 160;
+
+  @BeforeAll
+  static void registerGuiPlugins() throws HopException {
+    HopGuiEnvironment.init();
+  }
+
+  @Test
+  void clickingTheBadgeOpensTheRowsOnce() {
+    onCanvas(
+        scene -> {
+          Point badge = scene.outputBadge(scene.source);
+          Point before = scene.locationOf(scene.source);
+
+          Set<Shell> openBefore = openShells();
+          fireAsync(scene.canvas, SWT.MouseDown, scene.scale, badge, 1, 
SWT.NONE);
+          fireAsync(scene.canvas, SWT.MouseUp, scene.scale, badge, 1, 
SWT.BUTTON1);
+          Shell rows = awaitNewShell(scene.bot, openBefore);
+          assertNotNull(rows, "a click on the output-data badge opens the 
rows");
+          assertEquals(scene.transformRowsTitle, titleOf(rows));
+
+          // The rows dialog runs its own event loop, so this second click is 
delivered while the
+          // first dialog is still up. It must not open another one, or stack 
them until Escape.
+          fireAsync(scene.canvas, SWT.MouseDown, scene.scale, badge, 1, 
SWT.NONE);
+          fireAsync(scene.canvas, SWT.MouseUp, scene.scale, badge, 1, 
SWT.BUTTON1);
+
+          assertEquals(List.of(scene.transformRowsTitle), 
catchDialogs(scene.bot, openBefore));
+          assertEquals(
+              before, scene.locationOf(scene.source), "a click must not move 
the transform");
+          assertTrue(scene.source.isSelected(), "viewing the rows keeps the 
selection");
+          assertIdle(scene.graph);
+        });
+  }
+
+  @Test
+  void releasingADragOnTheBadgeDoesNotOpenTheRows() {
+    onCanvas(
+        scene -> {
+          Point icon = scene.icon(scene.source);
+          Point beforeSource = scene.locationOf(scene.source);
+          Point beforeTarget = scene.locationOf(scene.target);
+
+          fire(scene.canvas, SWT.MouseDown, scene.scale, icon, 1, SWT.NONE);
+          Point drop = new Point(icon.x, icon.y + DRAG_Y);
+          fire(scene.canvas, SWT.MouseMove, scene.scale, drop, 0, SWT.BUTTON1);
+
+          Point badge = scene.outputBadge(scene.source);
+          List<String> dialogs = release(scene, badge);
+
+          Point afterSource = scene.locationOf(scene.source);
+          Point afterTarget = scene.locationOf(scene.target);
+          assertTrue(dialogs.isEmpty(), "releasing a drag on the output-data 
badge opens no rows");
+          assertTrue(
+              afterSource.y - beforeSource.y > 100, "the dragged transform 
should have moved down");
+          assertEquals(beforeSource.x, afterSource.x);
+          assertEquals(
+              afterSource.y - beforeSource.y,
+              afterTarget.y - beforeTarget.y,
+              "every selected transform moves together");
+          assertIdle(scene.graph);
+
+          // The next click is a click again: one rows dialog, and the 
transform stays where the
+          // drag left it.
+          List<String> after = click(scene, scene.outputBadge(scene.source));
+          assertEquals(List.of(scene.transformRowsTitle), after);
+          assertEquals(afterSource, scene.locationOf(scene.source));
+          assertIdle(scene.graph);
+        });
+  }
+
+  @Test
+  void draggingFromTheBadgeMovesTheTransform() {
+    onCanvas(
+        scene -> {
+          Point badge = scene.outputBadge(scene.source);
+          Point before = scene.locationOf(scene.source);
+          Point drop = new Point(badge.x, badge.y + DRAG_Y);
+
+          fire(scene.canvas, SWT.MouseDown, scene.scale, badge, 1, SWT.NONE);
+          fire(scene.canvas, SWT.MouseMove, scene.scale, drop, 0, SWT.BUTTON1);
+          // The badge was the grab point, so it is still under the pointer 
when the button comes
+          // up.
+          List<String> dialogs = release(scene, drop);
+
+          assertTrue(
+              dialogs.isEmpty(),
+              "a drag that starts on the output-data badge does not open the 
rows");
+          assertTrue(scene.locationOf(scene.source).y - before.y > 100);
+          assertIdle(scene.graph);
+        });
+  }
+
+  @Test
+  void hopBadgeClickOpensRowsAndADragReleaseDoesNot() {
+    onCanvas(
+        scene -> {
+          List<String> clicked = click(scene, scene.hopBadge());
+          assertEquals(List.of(scene.hopRowsTitle), clicked);
+          assertIdle(scene.graph);
+
+          Point icon = scene.icon(scene.source);
+          Point before = scene.locationOf(scene.source);
+          fire(scene.canvas, SWT.MouseDown, scene.scale, icon, 1, SWT.NONE);
+          fire(
+              scene.canvas,
+              SWT.MouseMove,
+              scene.scale,
+              new Point(icon.x, icon.y + DRAG_Y),
+              0,
+              SWT.BUTTON1);
+          List<String> dragged = release(scene, scene.hopBadge());
+
+          assertTrue(
+              dragged.isEmpty(), "releasing a drag on the hop output-data 
badge opens no rows");
+          assertTrue(scene.locationOf(scene.source).y - before.y > 100);
+          assertIdle(scene.graph);
+        });
+  }
+
+  private List<String> click(Scene scene, Point at) {
+    return clickAndCatchDialogs(scene.bot, scene.canvas, scene.scale, at, 1, 
SWT.NONE);
+  }
+
+  /** Mouse-up posted without waiting, so a rows dialog cannot deadlock the 
test. */
+  private List<String> release(Scene scene, Point at) {
+    Set<Shell> before = openShells();
+    fireAsync(scene.canvas, SWT.MouseUp, scene.scale, at, 1, SWT.BUTTON1);
+    return catchDialogs(scene.bot, before);
+  }
+
+  private static void assertIdle(HopGuiPipelineGraph graph) {
+    Map<String, Object> idle = new LinkedHashMap<>();
+    idle.put("startHopTransform", null);
+    idle.put("endHopTransform", null);
+    idle.put("endHopLocation", null);
+    idle.put("candidate", null);
+    idle.put("selectedTransform", null);
+    idle.put("selectedTransforms", null);
+    idle.put("selectionRegion", null);
+    idle.put("dragSelection", false);
+    idle.put("iconDragStartScreen", null);
+    idle.put("iconDragCommitted", false);
+    idle.put("outputDataPressed", false);
+    idle.put("showingOutputRows", false);
+    idle.put("avoidContextDialog", false);
+    idle.put("lastButton", 0);
+    idle.put("clickedPipelineHop", null);
+    assertGraphState(graph, idle);
+  }
+
+  private void onCanvas(Consumer<Scene> test) {
+    AtomicReference<HopGuiPipelineGraph> graphRef = new AtomicReference<>();
+    AtomicReference<PipelineMeta> metaRef = new AtomicReference<>();
+    int[] previousGrid = new int[1];
+
+    withScene(
+        shell -> {
+          shell.setSize(1000, 700);
+          shell.setLayout(new FormLayout());
+          PropsUi props = PropsUi.getInstance();
+          previousGrid[0] = props.getCanvasGridSize();
+          props.setCanvasGridSize(1);
+          props.setUseDoubleClickOnCanvas(false);
+          props.setUseRightClickForContextDialog(false);
+
+          PipelineMeta pipelineMeta = buildPipeline();
+          metaRef.set(pipelineMeta);
+          HopGuiPipelineGraph graph =
+              new HopGuiPipelineGraph(
+                  shell,
+                  hopGui(),
+                  new ExplorerPerspective(),
+                  pipelineMeta,
+                  new HopPipelineFileType<>());
+          graph.pipeline = new LocalPipelineEngine(pipelineMeta);
+          graph.setOutputRowsMap(outputRows());
+          graph.setOutputHopRowsMap(hopRows(pipelineMeta));
+          pipelineMeta.findTransform(SOURCE_TRANSFORM).setSelected(true);
+          pipelineMeta.findTransform(TARGET_TRANSFORM).setSelected(true);
+          graphRef.set(graph);
+        },
+        bot -> {
+          try {
+            test.accept(new Scene(bot, graphRef.get(), metaRef.get()));
+          } finally {
+            onUi(() -> 
PropsUi.getInstance().setCanvasGridSize(previousGrid[0]));
+          }
+        });
+  }
+
+  private static PipelineMeta buildPipeline() {
+    PipelineMeta pipelineMeta = new PipelineMeta();
+    pipelineMeta.setName("output-data");
+    TransformMeta source = transform(SOURCE_TRANSFORM, SOURCE_LOCATION);
+    TransformMeta target = transform(TARGET_TRANSFORM, TARGET_LOCATION);
+    pipelineMeta.addTransform(source);
+    pipelineMeta.addTransform(target);
+    pipelineMeta.addPipelineHop(new PipelineHopMeta(source, target));
+    return pipelineMeta;
+  }
+
+  private static TransformMeta transform(String name, Point location) {
+    TransformMeta transformMeta = new TransformMeta("Dummy", name, new 
DummyMeta());
+    transformMeta.setLocation(location.x, location.y);
+    return transformMeta;
+  }
+
+  private static Map<String, RowBuffer> outputRows() {
+    Map<String, RowBuffer> rows = new HashMap<>();
+    rows.put(SOURCE_TRANSFORM, sample());
+    rows.put(TARGET_TRANSFORM, sample());
+    return rows;
+  }
+
+  private static Map<String, RowBuffer> hopRows(PipelineMeta pipelineMeta) {
+    TransformMeta source = pipelineMeta.findTransform(SOURCE_TRANSFORM);
+    TransformMeta target = pipelineMeta.findTransform(TARGET_TRANSFORM);
+    Map<String, RowBuffer> rows = new HashMap<>();
+    rows.put(source.getName() + "\t" + target.getName(), sample());
+    return rows;
+  }
+
+  private static RowBuffer sample() {
+    RowMeta rowMeta = new RowMeta();
+    rowMeta.addValueMeta(new ValueMetaString("id"));
+    RowBuffer buffer = new RowBuffer(rowMeta);
+    buffer.addRow("1");
+    return buffer;
+  }
+
+  private static String rowsTitle(String name) {
+    return BaseMessages.getString(
+        HopGui.class, "PipelineGraph.ViewOutput.OutputDialog.Header", name);
+  }
+
+  private final class Scene {
+    final SWTBot bot;
+    final HopGuiPipelineGraph graph;
+    final PipelineMeta pipelineMeta;
+    final Canvas canvas;
+    final double scale;
+    final TransformMeta source;
+    final TransformMeta target;
+    final String transformRowsTitle;
+    final String hopRowsTitle;
+
+    Scene(SWTBot bot, HopGuiPipelineGraph graph, PipelineMeta pipelineMeta) {
+      this.bot = bot;
+      this.graph = graph;
+      this.pipelineMeta = pipelineMeta;
+      this.canvas = onUi(graph::getCanvas);
+      this.scale = canvasToGraphScale(graph);
+      this.source = pipelineMeta.findTransform(SOURCE_TRANSFORM);
+      this.target = pipelineMeta.findTransform(TARGET_TRANSFORM);
+      this.transformRowsTitle = rowsTitle(SOURCE_TRANSFORM);
+      this.hopRowsTitle = rowsTitle(SOURCE_TRANSFORM + " → " + 
TARGET_TRANSFORM);
+    }
+
+    Point icon(TransformMeta transform) {
+      return awaitIcon(
+          bot,
+          graph::getVisibleAreaOwner,
+          AreaOwner.AreaType.TRANSFORM_ICON,
+          transform,
+          transform == source ? SOURCE_LOCATION : TARGET_LOCATION);
+    }
+
+    Point outputBadge(TransformMeta transform) {
+      return awaitArea(
+          bot,
+          graph,
+          graph::getVisibleAreaOwner,
+          AreaOwner.AreaType.TRANSFORM_OUTPUT_DATA,
+          area -> area.getParent() == transform);
+    }
+
+    Point hopBadge() {
+      PipelineHopMeta hop = pipelineMeta.findPipelineHop(source, target);
+      return awaitArea(
+          bot,
+          graph,
+          graph::getVisibleAreaOwner,
+          AreaOwner.AreaType.HOP_OUTPUT_DATA,
+          area -> area.getParent() == hop);
+    }
+
+    Point locationOf(TransformMeta transform) {
+      Point location = onUi(transform::getLocation);
+      return new Point(location.x, location.y);
+    }
+  }
+}
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 f743301ad2..81f5436ec3 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
@@ -393,6 +393,21 @@ public class HopGuiPipelineGraph extends 
HopGuiAbstractGraph
   /** True once pointer has moved past {@link #ICON_DRAG_THRESHOLD_PX} and 
drag has started. */
   private boolean iconDragCommitted;
 
+  /** Screen position of the current press, used to tell a click from a drag 
on mouse-up. */
+  private Point mouseDownScreen;
+
+  /**
+   * The press landed on a transform or hop output-rows badge. The rows open 
on release only for
+   * that press, and only when the pointer did not travel into a drag (issue 
#8595).
+   */
+  private boolean outputDataPressed;
+
+  /**
+   * The output-rows dialog is on screen. It runs its own event loop, which 
delivers any click made
+   * while it is up; that click must not move transforms or open a second copy 
of the dialog.
+   */
+  private boolean showingOutputRows;
+
   /**
    * Display filters used while placing a transform dragged from the context 
dialog (issue #3111).
    * Create happens on mouse-up (drop), not on drag-start.
@@ -772,6 +787,9 @@ public class HopGuiPipelineGraph extends HopGuiAbstractGraph
 
   @Override
   public void mouseDoubleClick(MouseEvent event) {
+    if (showingOutputRows) {
+      return;
+    }
 
     if (!PropsUi.getInstance().useDoubleClick()) {
       return;
@@ -837,6 +855,9 @@ public class HopGuiPipelineGraph extends HopGuiAbstractGraph
 
   @Override
   public void mouseDown(MouseEvent event) {
+    if (showingOutputRows) {
+      return;
+    }
     if (EnvironmentUtils.getInstance().isWeb()) {
       // RAP does not support certain mouse events.
       mouseHover(event);
@@ -854,9 +875,11 @@ public class HopGuiPipelineGraph extends 
HopGuiAbstractGraph
 
     Point real = screen2real(event.x, event.y);
     lastClick = new Point(real.x, real.y);
+    mouseDownScreen = new Point(event.x, event.y);
     lastButton = event.button;
     dragSelection = false;
     iconDragStartScreen = null;
+    outputDataPressed = false;
 
     // Hide the tooltip!
     hideToolTips();
@@ -955,10 +978,28 @@ public class HopGuiPipelineGraph extends 
HopGuiAbstractGraph
           break;
 
         case TRANSFORM_OUTPUT_DATA:
+          // The rows open on mouse-up, and only if this press does not become 
a drag. The badge
+          // covers the corner of the icon, so a press here also arms a move: 
otherwise grabbing
+          // that corner cannot drag the transform, and the release still 
opens the rows.
+          //
+          outputDataPressed = true;
+          if (canEditGraph() && event.button == 1 && !shift && !control) {
+            armIconDrag((TransformMeta) areaOwner.getParent(), event, real);
+          }
+          redraw();
           done = true;
           break;
 
         case HOP_OUTPUT_DATA:
+          // A hop badge is a button, not a drag handle. Drop any transform 
press so the release
+          // cannot finish a move and open the rows.
+          //
+          outputDataPressed = true;
+          currentTransform = null;
+          selectedTransform = null;
+          iconDragStartScreen = null;
+          iconDragCommitted = false;
+          dragSelection = false;
           done = true;
           break;
 
@@ -1026,29 +1067,7 @@ public class HopGuiPipelineGraph extends 
HopGuiAbstractGraph
           } else if (canEditGraph()) {
             // Defer entering drag mode until pointer moves past threshold 
(avoids drag when
             // clicking on name or making a small movement). Read-only 
sessions never arm drag.
-            iconDragStartScreen = new Point(event.x, event.y);
-            iconDragCommitted = false;
-            previousTransformLocations = 
pipelineMeta.getSelectedTransformLocations();
-
-            Point p = currentTransform.getLocation();
-            iconOffset = new Point(real.x - p.x, real.y - p.y);
-
-            // The RAP/web client does not deliver mouse-move events while a 
button is held, so the
-            // movement threshold in mouseMove() can never fire during a 
press. Arm the drag right
-            // away on mouse-down so the transform follows the cursor and is 
dropped on mouse-up;
-            // native SWT keeps the threshold behaviour to distinguish a click 
from a drag.
-            if (EnvironmentUtils.getInstance().isWeb() && event.button == 1 && 
!shift && !control) {
-              iconDragCommitted = true;
-              markPositionUndoPoint();
-              dragSelection = true;
-              canvas.setData("mode", "drag");
-              selectedTransforms = pipelineMeta.getSelectedTransforms();
-              selectedTransform = currentTransform;
-              pipelineGridDelegate.onPipelineSelectionChanged();
-              for (ITransformSelectionListener listener : 
currentTransformListeners) {
-                listener.onUpdateSelection(currentTransform);
-              }
-            }
+            armIconDrag(currentTransform, event, real);
           }
           redraw();
           done = true;
@@ -1205,6 +1224,14 @@ public class HopGuiPipelineGraph extends 
HopGuiAbstractGraph
 
   @Override
   public void mouseUp(MouseEvent e) {
+    if (showingOutputRows) {
+      return;
+    }
+    // A preview badge opens its rows only for the press that landed on it. 
Cleared before any
+    // return below, including the ones that finish a drag.
+    boolean previewPress = outputDataPressed && e.button == 1;
+    outputDataPressed = false;
+
     // Track if we just completed a resize operation
     boolean wasResizing = false;
 
@@ -1335,14 +1362,13 @@ public class HopGuiPipelineGraph extends 
HopGuiAbstractGraph
     if (areaOwner != null && areaOwner.getAreaType() != null) {
       switch (areaOwner.getAreaType()) {
         case TRANSFORM_OUTPUT_DATA:
-          if (showTransformOutputData(areaOwner)) {
-            lastButton = 0;
-            return;
-          }
-          break;
         case HOP_OUTPUT_DATA:
-          if (showHopOutputData(areaOwner)) {
-            lastButton = 0;
+          // The badge sits on the corner of the icon and part way along the 
hop, so the release of
+          // a move often lands on it. That release ends the move. A click, 
the pointer having
+          // stayed within the drag threshold, is what opens the rows (issue 
#8595).
+          //
+          if (previewPress && opensPreviewRows(e)) {
+            showPreviewRows(areaOwner);
             return;
           }
           break;
@@ -1398,7 +1424,9 @@ public class HopGuiPipelineGraph extends 
HopGuiAbstractGraph
     if (selectedTransform != null && startHopTransform == null) {
       if (e.button == 1) {
         Point realClick = screen2real(e.x, e.y);
-        if (lastClick.x == realClick.x && lastClick.y == realClick.y) {
+        // A drag that ends where it started is still a drag. Hop Web arms 
that flag on mouse-down,
+        // so there the coordinates alone distinguish a click.
+        if (lastClick.x == realClick.x && lastClick.y == realClick.y && 
!dragWasCommitted()) {
           // Flip selection when control is pressed!
           if (control) {
             selectedTransform.flipSelected();
@@ -1553,6 +1581,115 @@ public class HopGuiPipelineGraph extends 
HopGuiAbstractGraph
     lastButton = 0;
   }
 
+  /**
+   * Arms a transform move from a press on its icon or on its output-rows 
badge. The drag itself
+   * starts only after the pointer passes {@link #ICON_DRAG_THRESHOLD_PX}, 
except on Hop Web, which
+   * never delivers mouse-move events while a button is held.
+   */
+  private void armIconDrag(TransformMeta transform, MouseEvent event, Point 
real) {
+    currentTransform = transform;
+    iconDragStartScreen = new Point(event.x, event.y);
+    iconDragCommitted = false;
+    previousTransformLocations = pipelineMeta.getSelectedTransformLocations();
+
+    Point p = currentTransform.getLocation();
+    iconOffset = new Point(real.x - p.x, real.y - p.y);
+
+    // The RAP/web client does not deliver mouse-move events while a button is 
held, so the
+    // movement threshold in mouseMove() can never fire during a press. Arm 
the drag right away on
+    // mouse-down so the transform follows the cursor and is dropped on 
mouse-up; native SWT keeps
+    // the threshold behaviour to distinguish a click from a drag.
+    boolean shift = (event.stateMask & SWT.SHIFT) != 0;
+    boolean control = (event.stateMask & SWT.MOD1) != 0;
+    if (EnvironmentUtils.getInstance().isWeb() && event.button == 1 && !shift 
&& !control) {
+      iconDragCommitted = true;
+      markPositionUndoPoint();
+      dragSelection = true;
+      canvas.setData("mode", "drag");
+      selectedTransforms = pipelineMeta.getSelectedTransforms();
+      selectedTransform = currentTransform;
+      pipelineGridDelegate.onPipelineSelectionChanged();
+      for (ITransformSelectionListener listener : currentTransformListeners) {
+        listener.onUpdateSelection(currentTransform);
+      }
+    }
+  }
+
+  /**
+   * A stationary release on the badge that was pressed. A drag, a hop or a 
lasso that ends on the
+   * badge does not qualify: releasing the mouse after one of those ends the 
gesture and nothing
+   * more.
+   */
+  private boolean opensPreviewRows(MouseEvent event) {
+    if (startHopTransform != null || selectionRegion != null || 
movedPastDragThreshold(event)) {
+      return false;
+    }
+    // On the desktop a committed drag is a move even when the pointer is back 
where it started.
+    // Hop Web arms the drag on mouse-down, so there the distance above is the 
whole check.
+    return !dragWasCommitted();
+  }
+
+  /** The press moved far enough to be a drag. Hop Web sets the flag before 
any movement. */
+  private boolean dragWasCommitted() {
+    return iconDragCommitted && !EnvironmentUtils.getInstance().isWeb();
+  }
+
+  private boolean movedPastDragThreshold(MouseEvent event) {
+    if (mouseDownScreen == null) {
+      return false;
+    }
+    int dx = event.x - mouseDownScreen.x;
+    int dy = event.y - mouseDownScreen.y;
+    return dx * dx + dy * dy > (long) ICON_DRAG_THRESHOLD_PX * 
ICON_DRAG_THRESHOLD_PX;
+  }
+
+  /**
+   * Opens the rows for the badge under the pointer. The dialog runs its own 
event loop, so the drag
+   * is dropped first: a click that loop dispatches would otherwise still be 
the move that just
+   * ended, and would open the rows again.
+   */
+  private void showPreviewRows(AreaOwner areaOwner) {
+    endPreviewPress();
+    if (areaOwner.getAreaType() == AreaType.HOP_OUTPUT_DATA) {
+      showHopOutputData(areaOwner);
+    } else {
+      showTransformOutputData(areaOwner);
+    }
+  }
+
+  /**
+   * Drops the press that opened the output rows, without changing what is 
selected on the graph.
+   */
+  private void endPreviewPress() {
+    selectedTransform = null;
+    currentTransform = null;
+    selectedNote = null;
+    selectedTransforms = null;
+    selectedNotes = null;
+    dragSelection = false;
+    iconDragStartScreen = null;
+    iconDragCommitted = false;
+    iconOffset = null;
+    splitHop = false;
+    if (lastHopSplit != null) {
+      lastHopSplit.setSplit(false);
+      lastHopSplit = null;
+    }
+    startHopTransform = null;
+    endHopTransform = null;
+    endHopLocation = null;
+    candidate = null;
+    clickedPipelineHop = null;
+    clickedHopBadge = false;
+    selectionRegion = null;
+    lastButton = 0;
+    avoidContextDialog = false;
+    canvas.setData("mode", "null");
+    canvas.setData(START_HOP_NODE, null);
+    canvas.setData("resizeDirection", null);
+    resetPositionUndoMark();
+  }
+
   @GuiContextAction(
       id = "pipeline-graph-transform-1000-view-output",
       parentId = HopGuiPipelineTransformContext.CONTEXT_ID,
@@ -1599,72 +1736,79 @@ public class HopGuiPipelineGraph extends 
HopGuiAbstractGraph
   }
 
   private boolean showOutputDataDialog(String titleName, String messageName, 
RowBuffer rowBuffer) {
-    if (rowBuffer != null) {
-      synchronized (rowBuffer.getBuffer()) {
-        if (!rowBuffer.isEmpty()) {
-          try {
-            String title =
-                BaseMessages.getString(
-                    PKG, "PipelineGraph.ViewOutput.OutputDialog.Header", 
titleName);
-            String message =
-                BaseMessages.getString(
-                    PKG, 
"PipelineGraph.ViewOutput.OutputDialog.OutputRows.Text", messageName);
-            String prefix = "";
-
-            if (pipeline != null && pipeline.getPipelineRunConfiguration() != 
null) {
-              PipelineRunConfiguration pipelineRunConfiguration =
-                  pipeline.getPipelineRunConfiguration();
-              if (pipelineRunConfiguration.getEngineRunConfiguration()
-                  instanceof LocalPipelineRunConfiguration 
localPipelineRunConfiguration) {
-                String sampleTypeInGui = 
localPipelineRunConfiguration.getSampleTypeInGui();
-                if (StringUtils.isNotEmpty(sampleTypeInGui)) {
-                  try {
-                    SampleType sampleType = 
SampleType.valueOf(sampleTypeInGui);
-                    switch (sampleType) {
-                      case None:
-                        break;
-                      case First:
-                        prefix =
-                            BaseMessages.getString(
-                                PKG, 
"PipelineGraph.ViewOutput.OutputDialog.First.Text");
-                        break;
-                      case Last:
-                        prefix =
-                            BaseMessages.getString(
-                                PKG, 
"PipelineGraph.ViewOutput.OutputDialog.Last.Text");
-                        break;
-                      case Random:
-                        prefix +=
-                            BaseMessages.getString(
-                                PKG, 
"PipelineGraph.ViewOutput.OutputDialog.Random.Text");
-                        break;
-                      default:
-                        break;
-                    }
-                  } catch (Exception ex) {
-                    LogChannel.UI.logError("Unknown sample type: " + 
sampleTypeInGui);
+    if (rowBuffer == null) {
+      return false;
+    }
+    // Already inside this dialog's event loop: the click that got us here is 
not another preview.
+    if (showingOutputRows) {
+      return true;
+    }
+    synchronized (rowBuffer.getBuffer()) {
+      if (!rowBuffer.isEmpty()) {
+        showingOutputRows = true;
+        try {
+          String title =
+              BaseMessages.getString(
+                  PKG, "PipelineGraph.ViewOutput.OutputDialog.Header", 
titleName);
+          String message =
+              BaseMessages.getString(
+                  PKG, 
"PipelineGraph.ViewOutput.OutputDialog.OutputRows.Text", messageName);
+          String prefix = "";
+
+          if (pipeline != null && pipeline.getPipelineRunConfiguration() != 
null) {
+            PipelineRunConfiguration pipelineRunConfiguration =
+                pipeline.getPipelineRunConfiguration();
+            if (pipelineRunConfiguration.getEngineRunConfiguration()
+                instanceof LocalPipelineRunConfiguration 
localPipelineRunConfiguration) {
+              String sampleTypeInGui = 
localPipelineRunConfiguration.getSampleTypeInGui();
+              if (StringUtils.isNotEmpty(sampleTypeInGui)) {
+                try {
+                  SampleType sampleType = SampleType.valueOf(sampleTypeInGui);
+                  switch (sampleType) {
+                    case None:
+                      break;
+                    case First:
+                      prefix =
+                          BaseMessages.getString(
+                              PKG, 
"PipelineGraph.ViewOutput.OutputDialog.First.Text");
+                      break;
+                    case Last:
+                      prefix =
+                          BaseMessages.getString(
+                              PKG, 
"PipelineGraph.ViewOutput.OutputDialog.Last.Text");
+                      break;
+                    case Random:
+                      prefix +=
+                          BaseMessages.getString(
+                              PKG, 
"PipelineGraph.ViewOutput.OutputDialog.Random.Text");
+                      break;
+                    default:
+                      break;
                   }
+                } catch (Exception ex) {
+                  LogChannel.UI.logError("Unknown sample type: " + 
sampleTypeInGui);
                 }
               }
             }
-
-            new ShowRowsDialog(
-                    hopGui.getActiveShell(),
-                    variables,
-                    title,
-                    prefix + message,
-                    rowBuffer.getRowMeta(),
-                    rowBuffer.getBuffer())
-                .open();
-          } catch (Exception ex) {
-            new ErrorDialog(
-                hopGui.getActiveShell(), CONST_ERROR, "Error showing output 
rows dialog", ex);
           }
+
+          new ShowRowsDialog(
+                  hopGui.getActiveShell(),
+                  variables,
+                  title,
+                  prefix + message,
+                  rowBuffer.getRowMeta(),
+                  rowBuffer.getBuffer())
+              .open();
+        } catch (Exception ex) {
+          new ErrorDialog(
+              hopGui.getActiveShell(), CONST_ERROR, "Error showing output rows 
dialog", ex);
+        } finally {
+          showingOutputRows = false;
         }
       }
-      return true;
     }
-    return false;
+    return true;
   }
 
   /**
@@ -1797,6 +1941,9 @@ public class HopGuiPipelineGraph extends 
HopGuiAbstractGraph
   private void menuDetect(Event event) {
     // No SWT menu hangs off the canvas, and in Hop Web the browser's own menu 
is unwanted.
     event.doit = false;
+    if (showingOutputRows) {
+      return;
+    }
     if (!PropsUi.getInstance().useRightClickForContextDialog()) {
       return;
     }
@@ -2459,6 +2606,9 @@ public class HopGuiPipelineGraph extends 
HopGuiAbstractGraph
 
   @Override
   public void mouseMove(MouseEvent event) {
+    if (showingOutputRows) {
+      return;
+    }
     boolean shift = (event.stateMask & SWT.SHIFT) != 0;
     boolean doRedraw = false;
 
@@ -3434,6 +3584,7 @@ public class HopGuiPipelineGraph extends 
HopGuiAbstractGraph
     dragSelection = false;
     iconDragStartScreen = null;
     iconDragCommitted = false;
+    outputDataPressed = false;
     canvas.setData("mode", "null");
     canvas.setData(START_HOP_NODE, null);
     startHopTransform = null;

Reply via email to