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 f2f6b7ab1d Improve Hop handeling in hop web, fixes #6322 (#6326)
f2f6b7ab1d is described below

commit f2f6b7ab1df10494b1edb92fc9551961ce99ae97
Author: Hans Van Akelyen <[email protected]>
AuthorDate: Sat Jan 10 13:50:16 2026 +0100

    Improve Hop handeling in hop web, fixes #6322 (#6326)
---
 docker/build-hop-images.sh                         |  65 +++++
 docker/unified.Dockerfile                          |  72 +++--
 .../org/apache/hop/ui/hopgui/CanvasFacadeImpl.java |   7 +-
 .../org/apache/hop/ui/hopgui/HopWebEntryPoint.java |   1 +
 .../org/apache/hop/ui/core/widget/svg/svg-label.js |   4 -
 .../resources/org/apache/hop/ui/hopgui/canvas.js   | 297 +++++++++++++++------
 .../org/apache/hop/ui/hopgui/dark-mode.css         |   7 +
 .../org/apache/hop/ui/hopgui/light-mode.css        |   7 +
 .../hopgui/file/pipeline/HopGuiPipelineGraph.java  |  85 +++---
 .../hopgui/file/workflow/HopGuiWorkflowGraph.java  |  79 +++---
 10 files changed, 429 insertions(+), 195 deletions(-)

diff --git a/docker/build-hop-images.sh b/docker/build-hop-images.sh
index a46403107a..e9e73f9f86 100755
--- a/docker/build-hop-images.sh
+++ b/docker/build-hop-images.sh
@@ -39,6 +39,7 @@
 #   --maven-threads <threads>     Maven build threads (default: 1C, e.g., 2C, 
4, 8)
 #   --progress <mode>             Docker build output mode: auto (default), 
plain (verbose), tty (compact)
 #   --builder <type>              Builder type: full (Maven, default) or fast 
(pre-built artifacts)
+#   --skip-fat-jar                Skip building the fat jar (auto-detected 
based on images, only needed for dataflow/web-beam)
 #   --no-cache                    Build without using cache
 #   -h, --help                    Show this help message
 #
@@ -55,12 +56,18 @@
 #   # Build web image with beam variant (includes fat jar for Dataflow)
 #   ./build-hop-images.sh --images web-beam
 #
+#   # Build web image only (fat jar automatically skipped)
+#   ./build-hop-images.sh --images web
+#
 #   # Build specific variants
 #   ./build-hop-images.sh --images client-minimal,web-beam
 #
 #   # Build for multiple platforms and push to Docker Hub
 #   ./build-hop-images.sh --platforms linux/amd64,linux/arm64 --push 
--registry apache
 #
+#   # Manually skip fat jar generation (saves time if not building 
dataflow/web-beam)
+#   ./build-hop-images.sh --images web,client --skip-fat-jar
+#
 
################################################################################
 
 set -e
@@ -105,6 +112,7 @@ USE_CACHE="true"
 MAVEN_THREADS="1C"  # Maven thread count (1C, 2C, or specific number like 4)
 BUILD_PROGRESS="auto"  # Docker build progress output (auto, plain, tty)
 BUILDER_TYPE="full"  # Builder type: full (Maven build) or fast (pre-built 
artifacts)
+SKIP_FAT_JAR="auto"  # Whether to skip fat jar generation (auto, true, false)
 SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
 PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
 
@@ -173,6 +181,29 @@ show_help() {
     exit 0
 }
 
+# Function to determine if fat jar is needed based on images being built
+needs_fat_jar() {
+    local images_list="$1"
+    
+    # Fat jar is only needed for dataflow and web-beam images
+    if [[ "$images_list" == "all" ]]; then
+        # Building all images includes dataflow and web-beam
+        return 0
+    fi
+    
+    # Check if any of the requested images needs the fat jar
+    IFS=',' read -ra images_array <<< "$images_list"
+    for img in "${images_array[@]}"; do
+        img=$(echo "$img" | xargs)  # trim whitespace
+        if [[ "$img" == "dataflow"* ]] || [[ "$img" == "web-beam"* ]]; then
+            return 0
+        fi
+    done
+    
+    # No images that need fat jar
+    return 1
+}
+
 # Function to detect version from pom.xml
 detect_version() {
     if [ -f "$PROJECT_ROOT/pom.xml" ]; then
@@ -285,6 +316,10 @@ parse_args() {
                 BUILDER_TYPE="$2"
                 shift 2
                 ;;
+            --skip-fat-jar)
+                SKIP_FAT_JAR="true"
+                shift
+                ;;
             --no-cache)
                 USE_CACHE="false"
                 shift
@@ -358,12 +393,28 @@ build_image() {
         version_tag="${VERSION}-${variant_suffix}"
     fi
     
+    # Determine if we should skip fat jar for this specific image
+    local skip_fat_jar_value="false"
+    if [[ "$SKIP_FAT_JAR" == "true" ]]; then
+        skip_fat_jar_value="true"
+    elif [[ "$SKIP_FAT_JAR" == "auto" ]]; then
+        # Auto-detect: skip if this image doesn't need it
+        if [[ "$stage_name" != "dataflow"* ]] && [[ "$stage_name" != 
"web-beam"* ]]; then
+            skip_fat_jar_value="true"
+        fi
+    fi
+    
+    if [[ "$skip_fat_jar_value" == "true" ]]; then
+        print_info "Skipping fat jar generation (not needed for $stage_name)"
+    fi
+    
     # Build arguments
     local build_args=(
         "--build-arg" "HOP_BUILD_FROM_SOURCE=$SOURCE_TYPE"
         "--build-arg" "TARGET_IMAGE=$stage_name"
         "--build-arg" "MAVEN_THREADS=$maven_threads"
         "--build-arg" "BUILDER_TYPE=$BUILDER_TYPE"
+        "--build-arg" "SKIP_FAT_JAR=$skip_fat_jar_value"
         "--progress" "$BUILD_PROGRESS"
         "--file" "$SCRIPT_DIR/unified.Dockerfile"
         "--target" "$stage_name"
@@ -464,6 +515,20 @@ build_images() {
     if [[ "$BUILDER_TYPE" == "full" ]]; then
         echo "Maven threads: $MAVEN_THREADS"
     fi
+    
+    # Show fat jar status
+    if [[ "$SKIP_FAT_JAR" == "auto" ]]; then
+        if needs_fat_jar "$IMAGES"; then
+            echo "Fat jar:       enabled (auto-detected, needed for 
dataflow/web-beam)"
+        else
+            echo "Fat jar:       disabled (auto-detected, not needed for 
selected images)"
+        fi
+    elif [[ "$SKIP_FAT_JAR" == "true" ]]; then
+        echo "Fat jar:       disabled (manual)"
+    else
+        echo "Fat jar:       enabled"
+    fi
+    
     echo "Push:          $PUSH"
     echo "Cache:         $USE_CACHE"
     echo ""
diff --git a/docker/unified.Dockerfile b/docker/unified.Dockerfile
index d1bf099fac..a53dbc6d5e 100644
--- a/docker/unified.Dockerfile
+++ b/docker/unified.Dockerfile
@@ -172,12 +172,19 @@ RUN echo "=== Extracting assemblies ===" && \
         echo "WARNING: Plugins assembly not found, will use built plugins 
directly"; \
     fi
 
-# Step 2: Generate fat jar for dataflow template
-RUN if [ -f /build/assemblies/client/target/hop/hop-conf.sh ]; then \
-        /build/assemblies/client/target/hop/hop-conf.sh \
-        --generate-fat-jar=/tmp/hop-fatjar.jar; \
+# Step 2: Generate fat jar for dataflow template (only if needed)
+ARG SKIP_FAT_JAR=false
+RUN if [ "${SKIP_FAT_JAR}" = "false" ]; then \
+        echo "=== Generating fat jar ===" && \
+        if [ -f /build/assemblies/client/target/hop/hop-conf.sh ]; then \
+            /build/assemblies/client/target/hop/hop-conf.sh \
+            --generate-fat-jar=/tmp/hop-fatjar.jar; \
+        else \
+            echo "ERROR: hop-conf.sh not found" && exit 1; \
+        fi; \
     else \
-        echo "ERROR: hop-conf.sh not found" && exit 1; \
+        echo "=== Skipping fat jar generation (not needed) ===" && \
+        touch /tmp/hop-fatjar.jar; \
     fi
 
 # Step 3: Prepare optimized directory structures for final images
@@ -189,12 +196,11 @@ RUN mkdir -p /build/hop-web-prepared/webapps/ROOT && \
     cp -r /build/assemblies/client/target/hop/lib/beam/* 
/build/hop-web-prepared/webapps/ROOT/WEB-INF/lib/ && \
     cp -r /build/assemblies/client/target/hop/lib/core/* 
/build/hop-web-prepared/webapps/ROOT/WEB-INF/lib/ && \
     rm /build/hop-web-prepared/webapps/ROOT/WEB-INF/lib/hop-ui-rcp* && \
-    mkdir -p /build/hop-web-prepared/bin && \
-    cp -r /build/docker/resources/run-web.sh 
/build/hop-web-prepared/bin/run-web.sh
+    cp /build/docker/resources/run-web.sh /build/hop-web-prepared/run-web.sh 
&& \
+    chmod +x /build/hop-web-prepared/run-web.sh
 
 # Make scripts executable
-RUN chmod +x /build/hop-web-prepared/webapps/ROOT/*.sh \
-    && chmod +x /build/hop-web-prepared/bin/run-web.sh
+RUN chmod +x /build/hop-web-prepared/webapps/ROOT/*.sh
 
     # Fix hop-config.json
 RUN sed -i 's/config\/projects/${HOP_CONFIG_FOLDER}\/projects/g' 
/build/hop-web-prepared/webapps/ROOT/config/hop-config.json
@@ -221,14 +227,13 @@ RUN mkdir -p /build/hop-client-prepared && \
 RUN mkdir -p /build/hop-rest-prepared/plugins && \
     mkdir -p /build/hop-rest-prepared/webapps && \
     mkdir -p /build/hop-rest-prepared/lib/swt/linux/x86_64 && \
-    mkdir -p /build/hop-rest-prepared/bin && \
     # Copy plugins
     cp -r /build/assemblies/plugins/target/plugins/* 
/build/hop-rest-prepared/plugins/ && \
     # Copy REST war
     cp /build/rest/target/hop-rest*.war 
/build/hop-rest-prepared/webapps/hop.war && \
     # Copy run script
-    cp /build/docker/resources/run-rest.sh 
/build/hop-rest-prepared/bin/run-rest.sh && \
-    chmod +x /build/hop-rest-prepared/bin/run-rest.sh
+    cp /build/docker/resources/run-rest.sh 
/build/hop-rest-prepared/run-rest.sh && \
+    chmod +x /build/hop-rest-prepared/run-rest.sh
 
 
################################################################################
 # Stage 4a: Hop Client/Server Image (Standard)
@@ -299,7 +304,7 @@ USER hop
 ENV PATH=${PATH}:${DEPLOYMENT_PATH}/hop
 WORKDIR /home/hop
 
-CMD bash -c "$DEPLOYMENT_PATH/run.sh"
+ENTRYPOINT ["/bin/bash", "/opt/hop/run.sh"]
 
 
################################################################################
 # Stage 4b: Hop Web Image
@@ -352,11 +357,46 @@ COPY --from=builder --chown=hop /build/hop-web-prepared/ 
"${CATALINA_HOME}"
 
 USER hop
 
-CMD bash -c "$CATALINA_HOME/bin/run-web.sh"
+CMD ["/bin/bash", "/usr/local/tomcat/run-web.sh"]
 
 
 
################################################################################
-# Stage 4c: Hop Dataflow Template Image
+# Stage 4c: Hop REST Image
+################################################################################
+FROM tomcat:10-jdk17 AS rest
+
+# Environment variables
+ENV HOP_CONFIG_FOLDER=""
+ENV HOP_AES_ENCODER_KEY=""
+ENV HOP_AUDIT_FOLDER="${CATALINA_HOME}/webapps/ROOT/audit"
+ENV HOP_CONFIG_FOLDER="${CATALINA_HOME}/webapps/ROOT/config"
+ENV HOP_LOG_LEVEL="Basic"
+ENV HOP_OPTIONS="-Xmx4g"
+ENV HOP_PASSWORD_ENCODER_PLUGIN="Hop"
+ENV HOP_PLUGIN_BASE_FOLDERS="plugins"
+ENV HOP_SHARED_JDBC_FOLDERS=""
+ENV HOP_REST_CONFIG_FOLDER="/config"
+
+# Set TOMCAT start variables
+ENV CATALINA_OPTS='${HOP_OPTIONS} \
+  -DHOP_AES_ENCODER_KEY="${HOP_AES_ENCODER_KEY}" \
+  -DHOP_AUDIT_FOLDER="${HOP_AUDIT_FOLDER}" \
+  -DHOP_CONFIG_FOLDER="${HOP_CONFIG_FOLDER}" \
+  -DHOP_LOG_LEVEL="${HOP_LOG_LEVEL}" \
+  -DHOP_PASSWORD_ENCODER_PLUGIN="${HOP_PASSWORD_ENCODER_PLUGIN}" \
+  -DHOP_PLUGIN_BASE_FOLDERS="${HOP_PLUGIN_BASE_FOLDERS}" \
+  -DHOP_REST_CONFIG_FOLDER="${HOP_REST_CONFIG_FOLDER}" \
+  -DHOP_SHARED_JDBC_FOLDERS="${HOP_SHARED_JDBC_FOLDERS}"\'
+
+# Cleanup and copy resources
+RUN rm -rf webapps/*
+
+COPY --from=builder /build/hop-rest-prepared/ "${CATALINA_HOME}"/
+
+CMD ["/bin/bash", "/usr/local/tomcat/run-rest.sh"]
+
+################################################################################
+# Stage 4d: Hop Dataflow Template Image
 
################################################################################
 FROM gcr.io/dataflow-templates-base/java17-template-launcher-base AS dataflow
 
@@ -380,7 +420,7 @@ ENTRYPOINT ["/opt/google/dataflow/java_template_launcher"]
 
################################################################################
 
 
################################################################################
-# Stage 4e: Hop Web with Beam (includes fat jar for Dataflow)
+# Stage 4f: Hop Web with Beam (includes fat jar for Dataflow)
 
################################################################################
 FROM web AS web-beam
 LABEL variant="beam"
diff --git a/rap/src/main/java/org/apache/hop/ui/hopgui/CanvasFacadeImpl.java 
b/rap/src/main/java/org/apache/hop/ui/hopgui/CanvasFacadeImpl.java
index a75bb9b453..90bd06d7d0 100644
--- a/rap/src/main/java/org/apache/hop/ui/hopgui/CanvasFacadeImpl.java
+++ b/rap/src/main/java/org/apache/hop/ui/hopgui/CanvasFacadeImpl.java
@@ -45,11 +45,8 @@ public class CanvasFacadeImpl extends CanvasFacade {
   private void setDataCommon(Canvas canvas, float magnification, DPoint 
offset, Object meta) {
     JsonObject jsonProps = new JsonObject();
     jsonProps.add("themeId", System.getProperty(HopWeb.HOP_WEB_THEME, 
"light"));
-    jsonProps.add(
-        "gridSize",
-        PropsUi.getInstance().isShowCanvasGridEnabled()
-            ? PropsUi.getInstance().getCanvasGridSize()
-            : 1);
+    jsonProps.add("gridSize", PropsUi.getInstance().getCanvasGridSize());
+    jsonProps.add("showGrid", PropsUi.getInstance().isShowCanvasGridEnabled());
     jsonProps.add("iconSize", PropsUi.getInstance().getIconSize());
     jsonProps.add("magnification", (float) (magnification * 
PropsUi.getNativeZoomFactor()));
     jsonProps.add("offsetX", offset.x);
diff --git a/rap/src/main/java/org/apache/hop/ui/hopgui/HopWebEntryPoint.java 
b/rap/src/main/java/org/apache/hop/ui/hopgui/HopWebEntryPoint.java
index 63188fbfeb..9759739588 100644
--- a/rap/src/main/java/org/apache/hop/ui/hopgui/HopWebEntryPoint.java
+++ b/rap/src/main/java/org/apache/hop/ui/hopgui/HopWebEntryPoint.java
@@ -38,6 +38,7 @@ public class HopWebEntryPoint extends AbstractEntryPoint {
     WidgetUtil.registerDataKeys("nodes");
     WidgetUtil.registerDataKeys("hops");
     WidgetUtil.registerDataKeys("notes");
+    WidgetUtil.registerDataKeys("startHopNode");
     // WidgetUtil.registerDataKeys("svg");
 
     //  The following options are session specific.
diff --git 
a/rap/src/main/resources/org/apache/hop/ui/core/widget/svg/svg-label.js 
b/rap/src/main/resources/org/apache/hop/ui/core/widget/svg/svg-label.js
index cd66df9c03..3bd93abd62 100644
--- a/rap/src/main/resources/org/apache/hop/ui/core/widget/svg/svg-label.js
+++ b/rap/src/main/resources/org/apache/hop/ui/core/widget/svg/svg-label.js
@@ -24,10 +24,6 @@ const handleEvent = function (event) {
 
     let img = document.getElementById(id);
 
-    console.log("svg-label event with id: " + id + ", was found? " + (img !== 
null));
-
-    // img.style.backgroundBlendMode = 'lighten';
-
     if (img===null) {
         return;
     }
diff --git a/rap/src/main/resources/org/apache/hop/ui/hopgui/canvas.js 
b/rap/src/main/resources/org/apache/hop/ui/hopgui/canvas.js
index 5810e4de9c..c0d087976d 100644
--- a/rap/src/main/resources/org/apache/hop/ui/hopgui/canvas.js
+++ b/rap/src/main/resources/org/apache/hop/ui/hopgui/canvas.js
@@ -20,7 +20,9 @@
 
 let x1, x2, y1, y2;
 let clicked = null;
-let offsetX, offsetY, magnification, gridSize;
+let hopStartNode = null;  // Track the start node for hop creation separately
+let iconOffsetX = 0, iconOffsetY = 0;  // Track where in the icon the user 
clicked (like desktop client)
+let offsetX, offsetY, magnification, gridSize, showGrid;
 let fgColor, bgColor, selectedNodeColor, nodeColor;
 
 // These are the colors for the theme. They are picked up by the next event 
handler function.
@@ -35,9 +37,9 @@ function getThemeColors(theme) {
         nodeColor = 'rgb(61, 99, 128)';
     } else {
         // Light theme is the default
-        //
+        // Use the same background as application: colorBackground RGB(240, 
240, 240)
         fgColor = 'rgb(50, 50, 50)';
-        bgColor = 'rgb(210, 210, 210)';
+        bgColor = 'rgb(240, 240, 240)';  // Matches GuiResource.colorBackground
         selectedNodeColor = 'rgb(0, 93, 166)';
         nodeColor = 'rgb(61, 99, 128)';
     }
@@ -51,33 +53,62 @@ const handleEvent = function (event) {
     const hops = event.widget.getData("hops");
     const notes = event.widget.getData("notes");
     const props = event.widget.getData("props");
+    const startHopNodeName = event.widget.getData("startHopNode");
 
     // Global vars to make the coordinate calculation function simpler.
     //
-    offsetX = props.offsetX;
-    offsetY = props.offsetY;
+    // Round offsets to avoid sub-pixel grid misalignment
+    offsetX = Math.round(props.offsetX);
+    offsetY = Math.round(props.offsetY);
     magnification = props.magnification;
-
-    const gridSize = props.gridSize;
-    const iconSize = props.iconSize * magnification;
+    gridSize = props.gridSize;  // Always set for snapping
+    showGrid = props.showGrid;  // Control visibility only
+
+    const iconSize = Math.round(props.iconSize * magnification);
+    
+    // Synchronize hopStartNode with server data
+    if (mode === "hop" && startHopNodeName && nodes[startHopNodeName]) {
+        // Always update hopStartNode when we have server data
+        hopStartNode = nodes[startHopNodeName];
+    } else if (mode !== "hop") {
+        // Clear hopStartNode when not in hop mode
+        hopStartNode = null;
+    }
 
     switch (event.type) {
         case SWT.MouseDown:
+            // Convert screen coordinates to logical coordinates
             x1 = event.x / magnification;
             y1 = event.y / magnification;
+            
+            // Set x2/y2 to current position (needed for hop drawing after 
mode change)
+            x2 = x1;
+            y2 = y1;
 
             // Determine which node is clicked if any
+            const iconLogicalSize = iconSize / magnification;
             for (let key in nodes) {
                 const node = nodes[key];
-                if (node.x <= x1 && x1 < node.x + iconSize
-                    && node.y <= y1 && y1 < node.y + iconSize) {
+                if (node.x <= x1 && x1 < node.x + iconLogicalSize
+                    && node.y <= y1 && y1 < node.y + iconLogicalSize) {
                     clicked = node;
+                    
+                    // Calculate iconOffset: where within the icon did user 
click?
+                    // This matches desktop client: iconOffset = new 
Point(real.x - p.x, real.y - p.y)
+                    iconOffsetX = x1 - node.x;
+                    iconOffsetY = y1 - node.y;
                     break;
                 }
             }
             break;
         case SWT.MouseUp:
-            clicked = null;
+            // Only reset clicked for non-hop modes
+            // In hop mode, keep the start node until mode changes
+            if (mode !== "hop") {
+                clicked = null;
+                iconOffsetX = 0;
+                iconOffsetY = 0;
+            }
             break;
         case SWT.MouseMove:
             x2 = event.x / magnification;
@@ -98,8 +129,17 @@ const handleEvent = function (event) {
             getThemeColors(props.themeId);
 
             const gc = event.gc;
-            const dx = x2 - x1;
-            const dy = y2 - y1;
+            
+            // Calculate delta matching desktop client logic:
+            // Desktop: icon.x = real.x - iconOffset.x  (where icon top-left 
should be)
+            //          dx = icon.x - selectedTransform.getLocation().x
+            // Web equivalent:
+            const iconTargetX = x2 - iconOffsetX;  // Where icon top-left 
should be
+            const iconTargetY = y2 - iconOffsetY;
+            const iconStartX = x1 - iconOffsetX;   // Where icon top-left was 
at start
+            const iconStartY = y1 - iconOffsetY;
+            const dx = iconTargetX - iconStartX;   // Delta for icon top-left
+            const dy = iconTargetY - iconStartY;
 
             // Set the appropriate font size.
             //
@@ -109,38 +149,68 @@ const handleEvent = function (event) {
             let newSize = Math.round(oldSize * magnification);
             gc.font = newSize + fontString.substring(pxIdx);
 
-            // If we're not dragging the cursor with the mouse
-            //
-            // Clear the canvas, regardless of what happens below
-            //
-            gc.rect(0, 0, gc.canvas.width / magnification, gc.canvas.height / 
magnification);
+            // Fill canvas with solid background color matching the application
+            // This creates the overlay effect during drag/hop operations
             gc.fillStyle = bgColor;
-            gc.fill();
+            gc.fillRect(0, 0, gc.canvas.width, gc.canvas.height);
 
-            // Draw grids
+            // Draw grids (only if showGrid is enabled)
             //
-            if (gridSize > 1) {
+            if (showGrid && gridSize > 1) {
                 drawGrid(gc, gridSize);
             }
 
-            // Draw hops
-            drawHops(hops, gc, mode, nodes, dx, iconSize, dy);
+            // Draw existing hops (skip in hop mode for cleaner view)
+            if (mode !== "hop") {
+                drawHops(hops, gc, mode, nodes, dx, iconSize, dy);
+            }
 
-            // The nodes are action or transform icons
-            //
+            // Always draw node outlines so users can see what they're 
targeting
             drawNodes(nodes, mode, dx, dy, gc, iconSize);
 
             // Draw notes
             drawNotes(notes, gc, mode, dx, dy);
 
-            // Draw a new hop
-            if (mode === "hop" && clicked) {
+            // Draw a new hop candidate line (matching desktop client style)
+            if (mode === "hop" && hopStartNode) {
+                // Use current mouse position, or fall back to x2/y2
+                const targetX = x2 !== undefined ? x2 : x1;
+                const targetY = y2 !== undefined ? y2 : y1;
+                
+                const startX = Math.round(fx(hopStartNode.x) + iconSize / 2);
+                const startY = Math.round(fy(hopStartNode.y) + iconSize / 2);
+                const endX = Math.round(fx(targetX));
+                const endY = Math.round(fy(targetY));
+                
+                // Draw solid blue line (matching desktop client)
+                gc.strokeStyle = 'rgb(0, 93, 166)';  // Blue color
+                gc.lineWidth = 2;
                 gc.beginPath();
-                gc.moveTo(
-                    fx(clicked.x) + iconSize / 2,
-                    fy(clicked.y) + iconSize / 2);
-                gc.lineTo(fx(x2), fy(y2));
+                gc.moveTo(startX, startY);
+                gc.lineTo(endX, endY);
                 gc.stroke();
+                
+                // Draw arrow head at the end
+                const angle = Math.atan2(endY - startY, endX - startX);
+                const arrowLength = 10;
+                const arrowWidth = 5;
+                
+                gc.fillStyle = 'rgb(0, 93, 166)';
+                gc.beginPath();
+                gc.moveTo(endX, endY);
+                gc.lineTo(
+                    Math.round(endX - arrowLength * Math.cos(angle - Math.PI / 
6)),
+                    Math.round(endY - arrowLength * Math.sin(angle - Math.PI / 
6))
+                );
+                gc.lineTo(
+                    Math.round(endX - arrowLength * Math.cos(angle + Math.PI / 
6)),
+                    Math.round(endY - arrowLength * Math.sin(angle + Math.PI / 
6))
+                );
+                gc.closePath();
+                gc.fill();
+                
+                // Reset
+                gc.lineWidth = 1;
             }
 
             // Draw a selection rectangle
@@ -172,44 +242,90 @@ const handleEvent = function (event) {
 // This gets called by the RAP code for the Canvas widget.
 function drawGrid(gc, gridSize) {
     gc.fillStyle = fgColor;
-    gc.beginPath();
-    gc.setLineDash([1, gridSize - 1]);
-    // vertical grid
-    for (let i = gridSize; i < gc.canvas.width / magnification; i += gridSize) 
{
-        gc.moveTo(fx(i), fy(0));
-        gc.lineTo(fx(i), fy(gc.canvas.height / magnification));
-    }
-    // horizontal grid
-    for (let j = gridSize; j < gc.canvas.height / magnification; j += 
gridSize) {
-        gc.moveTo(fx(0), fy(j));
-        gc.lineTo(fx(gc.canvas.width / magnification), fy(j));
+    gc.globalAlpha = 0.3;  // Make grid more subtle
+    
+    // Calculate visible area bounds
+    const width = gc.canvas.width;
+    const height = gc.canvas.height;
+    
+    // Calculate the spacing in screen coordinates (rounded for pixel-perfect 
alignment)
+    const spacing = Math.round(gridSize * magnification);
+    
+    // Find where the first snapped logical grid coordinate (0) appears on 
screen
+    // We draw grid dots at screen positions that correspond to snapped 
logical coordinates
+    const originX = Math.round(fx(0));
+    const originY = Math.round(fy(0));
+    
+    // Use proper modulo that handles negatives correctly for finding first 
visible grid dot
+    // JavaScript % can return negative values, so we need ((n % m) + m) % m
+    const firstX = ((originX % spacing) + spacing) % spacing;
+    const firstY = ((originY % spacing) + spacing) % spacing;
+    
+    // Draw dots at grid intersections (matching desktop client)
+    // Desktop uses: gc.drawPoint() which draws using current lineWidth 
(typically 2px)
+    // We'll draw 2x2 pixel dots to match the desktop appearance
+    const dotSize = 2;
+    for (let x = firstX; x <= width; x += spacing) {
+        for (let y = firstY; y <= height; y += spacing) {
+            gc.fillRect(x, y, dotSize, dotSize);
+        }
     }
-    gc.stroke();
-    gc.setLineDash([]);
-    gc.fillStyle = bgColor;
+    
+    gc.globalAlpha = 1.0;  // Reset alpha
 }
 
 function drawHops(hops, gc, mode, nodes, dx, iconSize, dy) {
+    // Set consistent stroke style for all hops
+    gc.strokeStyle = fgColor;
+    gc.lineWidth = 1;
+    
     hops.forEach(function (hop) {
+        // Validate that both nodes exist before attempting to draw
+        const fromNode = nodes[hop.from];
+        const toNode = nodes[hop.to];
+        
+        if (!fromNode || !toNode) {
+            // Skip this hop if either node is missing
+            console.warn("Skipping hop with missing node - from:", hop.from, 
"to:", hop.to);
+            return;
+        }
+        
         gc.beginPath();
-        if (mode === "drag" && nodes[hop.from].selected) {
-            gc.moveTo(
-                fx(nodes[hop.from].x + dx) + iconSize / 2,
-                fy(nodes[hop.from].y + dy) + iconSize / 2);
+        
+        // Calculate from coordinates (with client-side snapping when 
configured)
+        let fromX, fromY;
+        if (mode === "drag" && fromNode.selected) {
+            let x = fromNode.x + dx;
+            let y = fromNode.y + dy;
+            if (gridSize > 1) {
+                x = snapToGrid(x);
+                y = snapToGrid(y);
+            }
+            fromX = Math.round(fx(x) + iconSize / 2);
+            fromY = Math.round(fy(y) + iconSize / 2);
         } else {
-            gc.moveTo(
-                fx(nodes[hop.from].x) + iconSize / 2,
-                fy(nodes[hop.from].y) + iconSize / 2);
+            fromX = Math.round(fx(fromNode.x) + iconSize / 2);
+            fromY = Math.round(fy(fromNode.y) + iconSize / 2);
         }
-        if (mode === "drag" && nodes[hop.to].selected) {
-            gc.lineTo(
-                fx(nodes[hop.to].x + dx) + iconSize / 2,
-                fy(nodes[hop.to].y + dy) + iconSize / 2);
+        
+        // Calculate to coordinates (with client-side snapping when configured)
+        let toX, toY;
+        if (mode === "drag" && toNode.selected) {
+            let x = toNode.x + dx;
+            let y = toNode.y + dy;
+            if (gridSize > 1) {
+                x = snapToGrid(x);
+                y = snapToGrid(y);
+            }
+            toX = Math.round(fx(x) + iconSize / 2);
+            toY = Math.round(fy(y) + iconSize / 2);
         } else {
-            gc.lineTo(
-                fx(nodes[hop.to].x) + iconSize / 2,
-                fy(nodes[hop.to].y) + iconSize / 2);
+            toX = Math.round(fx(toNode.x) + iconSize / 2);
+            toY = Math.round(fy(toNode.y) + iconSize / 2);
         }
+        
+        gc.moveTo(fromX, fromY);
+        gc.lineTo(toX, toY);
         gc.stroke();
     });
 }
@@ -220,27 +336,33 @@ function drawNodes(nodes, mode, dx, dy, gc, iconSize) {
         let x = node.x;
         let y = node.y;
 
-        // Move selected nodes
+        // Move selected nodes with client-side snapping
         //
         if (mode === "drag" && (node.selected || node === clicked)) {
+            const origX = node.x;
+            const origY = node.y;
             x = node.x + dx;
             y = node.y + dy;
+            
+            // Apply snap-to-grid during dragging (when grid size is 
configured)
+            if (gridSize > 1) {
+                x = snapToGrid(x);
+                y = snapToGrid(y);
+            }
         }
-        // Draw the icon background
-        //
-        gc.rect(fx(x), fy(y), iconSize, iconSize);
-        gc.fillStyle = bgColor;
-        gc.fill();
-
-        // Draw a bounding rectangle
+        
+        // Skip drawing backgrounds - keep transparent so SVG icons show 
through
+        // Only draw the bounding rectangle outline
         //
         if (node.selected || node === clicked) {
             gc.lineWidth = 3;
             gc.strokeStyle = selectedNodeColor;
         } else {
-            gc.strokeStyle = nodeColor; //colorCrystalText
+            gc.lineWidth = 1;
+            gc.strokeStyle = nodeColor;
         }
-        drawRoundRectangle(gc, fx(x - 1), fy(y - 1), iconSize + 1, iconSize + 
1, 8, 8, false);
+        // Use rounded screen coordinates for pixel-perfect rendering
+        drawRoundRectangle(gc, Math.round(fx(x - 1)), Math.round(fy(y - 1)), 
iconSize + 1, iconSize + 1, 8, 8, false);
         gc.strokeStyle = fgColor;
         gc.lineWidth = 1;
 
@@ -251,9 +373,8 @@ function drawNodes(nodes, mode, dx, dy, gc, iconSize) {
         // Calculate the font size and magnify it as well.
         //
         gc.fillText(nodeName,
-            fx(x) + iconSize / 2 - gc.measureText(nodeName).width / 2,
-            fy(y) + iconSize + 7);
-        gc.fillStyle = bgColor;
+            Math.round(fx(x) + iconSize / 2 - gc.measureText(nodeName).width / 
2),
+            Math.round(fy(y) + iconSize + 7));
     }
 }
 
@@ -262,13 +383,13 @@ function drawNotes(notes, gc, mode, dx, dy) {
         gc.beginPath();
         if (mode === "drag" && note.selected) {
             gc.rect(
-                fx(note.x + dx),
-                fy(note.y + dy),
+                Math.round(fx(note.x + dx)),
+                Math.round(fy(note.y + dy)),
                 note.width + 10, note.height + 10);
         } else {
             gc.rect(
-                fx(note.x),
-                fy(note.y),
+                Math.round(fx(note.x)),
+                Math.round(fy(note.y)),
                 note.width + 10, note.height + 10);
         }
         gc.stroke();
@@ -276,21 +397,27 @@ function drawNotes(notes, gc, mode, dx, dy) {
 }
 
 function fx(x) {
-    if (x<0) {
-        return 0;
-    }
-    return (x + offsetX) * magnification + offsetX;
+    // Don't clamp negative values - allow drawing outside visible area
+    const result = (x + offsetX) * magnification + offsetX;
+    return result;
 }
 
 function fy(y) {
-    if (y<0) {
-        return 0;
-    }
-    return (y + offsetY) * magnification + offsetY;
+    // Don't clamp negative values - allow drawing outside visible area
+    const result = (y + offsetY) * magnification + offsetY;
+    return result;
 }
 
 function snapToGrid(x) {
-    return gridSize * Math.floor(x / gridSize);
+    // Match Java behavior: integer division first, then multiply
+    // Java: gridSize * (int) Math.round((float) (p.x / gridSize))
+    // where (p.x / gridSize) is integer division in Java
+    
+    // JavaScript equivalent: Math.trunc() truncates towards zero (like Java 
int division)
+    const gridCell = Math.trunc(x / gridSize);
+    const snapped = Math.round(gridSize * gridCell);
+    
+    return snapped;
 }
 
 /*
diff --git a/rap/src/main/resources/org/apache/hop/ui/hopgui/dark-mode.css 
b/rap/src/main/resources/org/apache/hop/ui/hopgui/dark-mode.css
index a9656701e0..ee76dfd58a 100644
--- a/rap/src/main/resources/org/apache/hop/ui/hopgui/dark-mode.css
+++ b/rap/src/main/resources/org/apache/hop/ui/hopgui/dark-mode.css
@@ -2522,3 +2522,10 @@ Shell.jface_contentProposalPopup, 
Shell.jface_infoPopupDialog {
     padding: 0;
     box-shadow: 0 0 4px #ababab;
 }
+
+/* Canvas sizing - ensure canvas fills its container */
+canvas {
+    width: 100% !important;
+    height: 100% !important;
+    display: block !important;
+}
diff --git a/rap/src/main/resources/org/apache/hop/ui/hopgui/light-mode.css 
b/rap/src/main/resources/org/apache/hop/ui/hopgui/light-mode.css
index 0d0179c456..ed604890cf 100644
--- a/rap/src/main/resources/org/apache/hop/ui/hopgui/light-mode.css
+++ b/rap/src/main/resources/org/apache/hop/ui/hopgui/light-mode.css
@@ -2534,3 +2534,10 @@ Shell.jface_contentProposalPopup, 
Shell.jface_infoPopupDialog {
     box-shadow: 0 0 4px #ababab;
 }
 
+/* Canvas sizing - ensure canvas fills its container */
+canvas {
+    width: 100% !important;
+    height: 100% !important;
+    display: block !important;
+}
+
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 4ec638fb22..35d56fa225 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
@@ -277,17 +277,14 @@ public class HopGuiPipelineGraph extends 
HopGuiAbstractGraph
       "pipeline-graph-transform-11000-view-execution-info";
   public static final String CONST_ERROR = "Error";
   public static final String CONST_ERROR_PREVIEWING_PIPELINE = "Error 
previewing pipeline";
+  public static final String START_HOP_NODE = "startHopNode";
+  public static final String PIPELINE_GRAPH_DIALOG_HOP_CAUSES_LOOP_MESSAGE =
+      "PipelineGraph.Dialog.HopCausesLoop.Message";
 
   private final ILogChannel log;
 
   private static final int HOP_SEL_MARGIN = 9;
 
-  private static final int TOOLTIP_HIDE_DELAY_FLASH = 2000;
-
-  private static final int TOOLTIP_HIDE_DELAY_SHORT = 5000;
-
-  private static final int TOOLTIP_HIDE_DELAY_LONG = 10000;
-
   @Getter private PipelineMeta pipelineMeta;
   @Getter public IPipelineEngine<PipelineMeta> pipeline;
 
@@ -392,7 +389,6 @@ public class HopGuiPipelineGraph extends HopGuiAbstractGraph
   Timer redrawTimer;
 
   @Setter private HopPipelineFileType<PipelineMeta> fileType;
-  private boolean singleClick;
   private boolean doubleClick;
   private boolean mouseMovedSinceClick;
 
@@ -527,7 +523,6 @@ public class HopGuiPipelineGraph extends HopGuiAbstractGraph
     setVisible(true);
     newProps();
 
-    canvas.setBackground(GuiResource.getInstance().getColorBlueCustomGrid());
     canvas.addPaintListener(this::paintControl);
 
     selectedTransforms = null;
@@ -550,9 +545,6 @@ public class HopGuiPipelineGraph extends HopGuiAbstractGraph
     // where the focus should be
     //
     hopGui.replaceKeyboardShortcutListeners(this);
-
-    // Scrolled composite ...
-    //
     canvas.pack();
 
     // Update menu, toolbar, force redraw canvas
@@ -752,6 +744,7 @@ public class HopGuiPipelineGraph extends HopGuiAbstractGraph
             // SHIFT CLICK: start drawing a new hop
             //
             canvas.setData("mode", "hop");
+            canvas.setData(START_HOP_NODE, currentTransform.getName());
             startHopTransform = currentTransform;
           } else {
             canvas.setData("mode", "drag");
@@ -858,6 +851,8 @@ public class HopGuiPipelineGraph extends HopGuiAbstractGraph
       // go away.
       //
       if (startHopTransform != null) {
+        canvas.setData("mode", "null");
+        canvas.setData(START_HOP_NODE, null);
         startHopTransform = null;
         endHopLocation = null;
         candidate = null;
@@ -903,8 +898,12 @@ public class HopGuiPipelineGraph extends 
HopGuiAbstractGraph
     resize = null;
     forbiddenTransform = null;
 
-    // canvas.setData("mode", null); does not work.
-    canvas.setData("mode", "null");
+    // Only clear mode if we're not in the middle of creating a hop
+    // Otherwise shift+click won't work (mode gets cleared before we can 
select the target)
+    if (startHopTransform == null && endHopTransform == null) {
+      canvas.setData("mode", "null");
+      canvas.setData(START_HOP_NODE, null);
+    }
 
     if (EnvironmentUtils.getInstance().isWeb()) {
       // RAP does not support certain mouse events.
@@ -1349,7 +1348,6 @@ public class HopGuiPipelineGraph extends 
HopGuiAbstractGraph
         && (!pipelineMeta.getSelectedTransforms().isEmpty()
             || !pipelineMeta.getSelectedNotes().isEmpty())) {
       pipelineMeta.unselectAll();
-      selectionRegion = null;
       updateGui();
 
       // Show a short tooltip
@@ -1658,7 +1656,7 @@ public class HopGuiPipelineGraph extends 
HopGuiAbstractGraph
                 candidate = null;
                 forbiddenTransform = transformMeta;
                 toolTip.setText(
-                    BaseMessages.getString(PKG, 
"PipelineGraph.Dialog.HopCausesLoop.Message"));
+                    BaseMessages.getString(PKG, 
PIPELINE_GRAPH_DIALOG_HOP_CAUSES_LOOP_MESSAGE));
                 showToolTip(new org.eclipse.swt.graphics.Point(event.x, 
event.y));
               }
             }
@@ -2189,6 +2187,8 @@ public class HopGuiPipelineGraph extends 
HopGuiAbstractGraph
     lastButton = 0;
     iconOffset = null;
     dragSelection = false;
+    canvas.setData("mode", "null");
+    canvas.setData(START_HOP_NODE, null);
     startHopTransform = null;
     endHopTransform = null;
     endHopLocation = null;
@@ -2201,8 +2201,8 @@ public class HopGuiPipelineGraph extends 
HopGuiAbstractGraph
   /**
    * See if location (x,y) is on a line between two transforms: the hop!
    *
-   * @param x
-   * @param y
+   * @param x coordinate
+   * @param y coordinate
    * @return the pipeline hop on the specified location, otherwise: null
    */
   protected PipelineHopMeta findPipelineHop(int x, int y) {
@@ -2212,8 +2212,8 @@ public class HopGuiPipelineGraph extends 
HopGuiAbstractGraph
   /**
    * See if location (x,y) is on a line between two transforms: the hop!
    *
-   * @param x
-   * @param y
+   * @param x coordinate
+   * @param y coordinate
    * @param exclude the transform to exclude from the hops (from or to 
location). Specify null if no
    *     transform is to be excluded.
    * @return the pipeline hop on the specified location, otherwise: null
@@ -2628,9 +2628,10 @@ public class HopGuiPipelineGraph extends 
HopGuiAbstractGraph
             pipeline.getComponentCopies(transformMeta.getName());
         return !Utils.isEmpty(componentCopies);
       }
+      default -> {
+        return true;
+      }
     }
-
-    return true;
   }
 
   @GuiContextAction(
@@ -2735,7 +2736,7 @@ public class HopGuiPipelineGraph extends 
HopGuiAbstractGraph
     if (enabled && hasLoop) {
       modalMessageDialog(
           BaseMessages.getString(PKG, 
"PipelineGraph.Dialog.HopCausesLoop.Title"),
-          BaseMessages.getString(PKG, 
"PipelineGraph.Dialog.HopCausesLoop.Message"),
+          BaseMessages.getString(PKG, 
PIPELINE_GRAPH_DIALOG_HOP_CAUSES_LOOP_MESSAGE),
           SWT.OK | SWT.ICON_ERROR);
     }
 
@@ -2784,7 +2785,7 @@ public class HopGuiPipelineGraph extends 
HopGuiAbstractGraph
     if (checkedTransforms.stream().anyMatch(entry -> 
pipelineMeta.hasLoop(entry))) {
       modalMessageDialog(
           BaseMessages.getString(PKG, 
"PipelineGraph.Dialog.HopCausesLoop.Title"),
-          BaseMessages.getString(PKG, 
"PipelineGraph.Dialog.HopCausesLoop.Message"),
+          BaseMessages.getString(PKG, 
PIPELINE_GRAPH_DIALOG_HOP_CAUSES_LOOP_MESSAGE),
           SWT.OK | SWT.ICON_ERROR);
     }
 
@@ -3099,8 +3100,8 @@ public class HopGuiPipelineGraph extends 
HopGuiAbstractGraph
           }
           break;
         case TRANSFORM_FAILURE_ICON:
-          String log = (String) areaOwner.getParent();
-          tip.append(log);
+          String failureLog = (String) areaOwner.getParent();
+          tip.append(failureLog);
           tipImage = GuiResource.getInstance().getImageFailure();
           break;
         case HOP_COPY_ICON:
@@ -3298,13 +3299,13 @@ public class HopGuiPipelineGraph extends 
HopGuiAbstractGraph
       } else {
         tooltipImage = GuiResource.getInstance().getImageHopUi();
       }
-      showTooltip(newTip, tooltipImage, screenX, screenY);
+      showSpecialTooltip(newTip, screenX, screenY);
     }
 
     return subject;
   }
 
-  public void showTooltip(String label, Image image, int screenX, int screenY) 
{
+  public void showSpecialTooltip(String label, int screenX, int screenY) {
     toolTip.setText(label);
     toolTip.setVisible(false);
     showToolTip(new org.eclipse.swt.graphics.Point(screenX, screenY));
@@ -3586,7 +3587,7 @@ public class HopGuiPipelineGraph extends 
HopGuiAbstractGraph
       ni.setBorderColorGreen(n.getBorderColorGreen());
       ni.setBorderColorBlue(n.getBorderColorBlue());
 
-      NotePadMeta after = (NotePadMeta) ni.clone();
+      NotePadMeta after = ni.clone();
       hopGui.undoDelegate.addUndoChange(
           pipelineMeta,
           new NotePadMeta[] {before},
@@ -3625,6 +3626,8 @@ public class HopGuiPipelineGraph extends 
HopGuiAbstractGraph
   public void newHopCandidate(HopGuiPipelineTransformContext context) {
     startHopTransform = context.getTransformMeta();
     endHopTransform = null;
+    canvas.setData("mode", "hop");
+    canvas.setData(START_HOP_NODE, startHopTransform.getName());
     redraw();
   }
 
@@ -3659,11 +3662,7 @@ public class HopGuiPipelineGraph extends 
HopGuiAbstractGraph
     double anglePoint = Math.atan2(y - y1, x - x1) + Math.PI;
 
     // Same angle, or close enough?
-    if (anglePoint >= angleLine - 0.01 && anglePoint <= angleLine + 0.01) {
-      return true;
-    }
-
-    return false;
+    return anglePoint >= angleLine - 0.01 && anglePoint <= angleLine + 0.01;
   }
 
   public SnapAllignDistribute createSnapAlignDistribute() {
@@ -3702,7 +3701,7 @@ public class HopGuiPipelineGraph extends 
HopGuiAbstractGraph
       image = "ui/images/preview.svg",
       category = 
"i18n::HopGuiPipelineGraph.ContextualAction.Category.Preview.Text",
       categoryOrder = "3")
-  /** Preview a single transform */
+  // Preview a single transform
   public void preview(HopGuiPipelineTransformContext context) {
     try {
       context.getPipelineMeta().unselectAll();
@@ -3747,7 +3746,7 @@ public class HopGuiPipelineGraph extends 
HopGuiAbstractGraph
       image = "ui/images/debug.svg",
       category = 
"i18n::HopGuiPipelineGraph.ContextualAction.Category.Preview.Text",
       categoryOrder = "3")
-  /** Debug a single transform */
+  // Debug a single transform
   public void debug(HopGuiPipelineTransformContext context) {
     try {
       context.getPipelineMeta().unselectAll();
@@ -3994,11 +3993,8 @@ public class HopGuiPipelineGraph extends 
HopGuiAbstractGraph
           }
           return true;
         }
-        if ((answer & SWT.NO) != 0) {
-          // User doesn't want to save but close
-          return true;
-        }
-        return false;
+        // User doesn't want to save but close
+        return (answer & SWT.NO) != 0;
       } else {
         return true;
       }
@@ -4461,6 +4457,8 @@ public class HopGuiPipelineGraph extends 
HopGuiAbstractGraph
                         }
                       }
                       break;
+                    default:
+                      break;
                   }
                 }
               });
@@ -4849,7 +4847,7 @@ public class HopGuiPipelineGraph extends 
HopGuiAbstractGraph
     }
     // Redraw the canvas to show the error icons etc.
     //
-    hopDisplay().asyncExec(() -> redraw());
+    hopDisplay().asyncExec(this::redraw);
   }
 
   public synchronized void showLastPreviewResults() {
@@ -4893,10 +4891,7 @@ public class HopGuiPipelineGraph extends 
HopGuiAbstractGraph
     if (pipeline.isPreparing()) {
       return true;
     }
-    if (pipeline.isRunning()) {
-      return true;
-    }
-    return false;
+    return pipeline.isRunning();
   }
 
   @Override
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 a8883521f2..37378483ec 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
@@ -223,7 +223,6 @@ public class HopGuiWorkflowGraph extends HopGuiAbstractGraph
 
   private static final int HOP_SEL_MARGIN = 9;
 
-  private static final int TOOLTIP_HIDE_DELAY_FLASH = 2000;
   public static final String ACTION_ID_WORKFLOW_GRAPH_HOP_ENABLE =
       "workflow-graph-hop-10010-hop-enable";
   public static final String ACTION_ID_WORKFLOW_GRAPH_HOP_DISABLE =
@@ -241,6 +240,7 @@ public class HopGuiWorkflowGraph extends HopGuiAbstractGraph
       "WorkflowGraph.Dialog.LoopAfterHopEnabled.Message";
   public static final String 
CONST_WORKFLOW_GRAPH_DIALOG_LOOP_AFTER_HOP_ENABLED_TITLE =
       "WorkflowGraph.Dialog.LoopAfterHopEnabled.Title";
+  public static final String START_HOP_NODE = "startHopNode";
 
   @Getter private final ExplorerPerspective perspective;
 
@@ -435,6 +435,11 @@ public class HopGuiWorkflowGraph extends 
HopGuiAbstractGraph
 
     setVisible(true);
 
+    // Set canvas background to match application background for web
+    if (EnvironmentUtils.getInstance().isWeb()) {
+      canvas.setBackground(GuiResource.getInstance().getColorBackground());
+    }
+
     canvas.addPaintListener(this::paintControl);
 
     selectedActions = null;
@@ -595,6 +600,7 @@ public class HopGuiWorkflowGraph extends HopGuiAbstractGraph
             // SHIFT CLICK is start of drag to create a new hop
             //
             canvas.setData("mode", "hop");
+            canvas.setData(START_HOP_NODE, currentAction.getName());
             startHopAction = currentAction;
           } else {
             canvas.setData("mode", "drag");
@@ -727,6 +733,8 @@ public class HopGuiWorkflowGraph extends HopGuiAbstractGraph
       // go away.
       //
       if (startHopAction != null) {
+        canvas.setData("mode", "null");
+        canvas.setData(START_HOP_NODE, null);
         startHopAction = null;
         hopCandidate = null;
         endHopLocation = null;
@@ -771,8 +779,12 @@ public class HopGuiWorkflowGraph extends 
HopGuiAbstractGraph
     dragSelection = false;
     forbiddenAction = null;
 
-    // canvas.setData("mode", null); does not work.
-    canvas.setData("mode", "null");
+    // Only clear mode if we're not in the middle of creating a hop
+    // Otherwise shift+click won't work (mode gets cleared before we can 
select the target)
+    if (startHopAction == null && endHopAction == null) {
+      canvas.setData("mode", "null");
+      canvas.setData(START_HOP_NODE, null);
+    }
     if (EnvironmentUtils.getInstance().isWeb()) {
       // RAP does not support certain mouse events.
       mouseMove(event);
@@ -945,7 +957,7 @@ public class HopGuiWorkflowGraph extends HopGuiAbstractGraph
                     BaseMessages.getString(PKG, 
"HopGuiWorkflowGraph.Dialog.SplitHop.Title"),
                     BaseMessages.getString(PKG, 
"HopGuiWorkflowGraph.Dialog.SplitHop.Message")
                         + Const.CR
-                        + hop.toString(),
+                        + hop,
                     SWT.ICON_QUESTION,
                     new String[] {
                       BaseMessages.getString(PKG, "System.Button.Yes"),
@@ -1113,7 +1125,6 @@ public class HopGuiWorkflowGraph extends 
HopGuiAbstractGraph
         && (!workflowMeta.getSelectedActions().isEmpty()
             || !workflowMeta.getSelectedNotes().isEmpty())) {
       workflowMeta.unselectAll();
-      selectionRegion = null;
       updateGui();
 
       // Show a short tooltip
@@ -1489,12 +1500,8 @@ public class HopGuiWorkflowGraph extends 
HopGuiAbstractGraph
   }
 
   public boolean checkIfHopAlreadyExists(WorkflowMeta workflowMeta, 
WorkflowHopMeta newHop) {
-    boolean ok = true;
-    if (workflowMeta.findWorkflowHop(newHop.getFromAction(), 
newHop.getToAction(), true) != null) {
-      ok = false;
-    }
 
-    return ok;
+    return workflowMeta.findWorkflowHop(newHop.getFromAction(), 
newHop.getToAction(), true) == null;
   }
 
   /**
@@ -1806,6 +1813,8 @@ public class HopGuiWorkflowGraph extends 
HopGuiAbstractGraph
     hopCandidate = null;
     lastHopSplit = null;
     lastButton = 0;
+    canvas.setData("mode", "null");
+    canvas.setData(START_HOP_NODE, null);
     startHopAction = null;
     endHopAction = null;
     iconOffset = null;
@@ -1835,8 +1844,8 @@ public class HopGuiWorkflowGraph extends 
HopGuiAbstractGraph
   /**
    * See if the location (x,y) is on a line between two actions: the hop!
    *
-   * @param x
-   * @param y
+   * @param x coordinate
+   * @param y coordinate
    * @return the workflow hop on the specified location, otherwise: null
    */
   private WorkflowHopMeta findWorkflowHop(int x, int y) {
@@ -1846,8 +1855,8 @@ public class HopGuiWorkflowGraph extends 
HopGuiAbstractGraph
   /**
    * See if the location (x,y) is on a line between two actions: the hop!
    *
-   * @param x
-   * @param y
+   * @param x coordinate
+   * @param y coordinate
    * @param exclude the action to exclude from the hops (from or to location). 
Specify null if no
    *     action is to be excluded.
    * @return the workflow hop on the specified location, otherwise: null
@@ -1942,6 +1951,8 @@ public class HopGuiWorkflowGraph extends 
HopGuiAbstractGraph
   public void newHopCandidate(HopGuiWorkflowActionContext context) {
     startHopAction = context.getActionMeta();
     endHopAction = null;
+    canvas.setData("mode", "hop");
+    canvas.setData(START_HOP_NODE, startHopAction.getName());
     redraw();
   }
 
@@ -2501,9 +2512,9 @@ public class HopGuiWorkflowGraph extends 
HopGuiAbstractGraph
       WorkflowHopMeta hop = workflowMeta.getWorkflowHop(i);
       if (list.contains(hop.getFromAction()) && 
list.contains(hop.getToAction())) {
 
-        WorkflowHopMeta before = (WorkflowHopMeta) hop.clone();
+        WorkflowHopMeta before = hop.clone();
         hop.setEnabled(enabled);
-        WorkflowHopMeta after = (WorkflowHopMeta) hop.clone();
+        WorkflowHopMeta after = hop.clone();
         hopGui.undoDelegate.addUndoChange(
             workflowMeta,
             new WorkflowHopMeta[] {before},
@@ -2619,9 +2630,9 @@ public class HopGuiWorkflowGraph extends 
HopGuiAbstractGraph
     if (hop == null) {
       return;
     }
-    WorkflowHopMeta before = (WorkflowHopMeta) hop.clone();
+    WorkflowHopMeta before = hop.clone();
     hop.setEnabled(enabled);
-    WorkflowHopMeta after = (WorkflowHopMeta) hop.clone();
+    WorkflowHopMeta after = hop.clone();
     hopGui.undoDelegate.addUndoChange(
         workflowMeta,
         new WorkflowHopMeta[] {before},
@@ -2651,9 +2662,9 @@ public class HopGuiWorkflowGraph extends 
HopGuiAbstractGraph
         .forEach(
             hop -> {
               if (hop.isEnabled() != enabled) {
-                WorkflowHopMeta before = (WorkflowHopMeta) hop.clone();
+                WorkflowHopMeta before = hop.clone();
                 hop.setEnabled(enabled);
-                WorkflowHopMeta after = (WorkflowHopMeta) hop.clone();
+                WorkflowHopMeta after = hop.clone();
                 hopGui.undoDelegate.addUndoChange(
                     workflowMeta,
                     new WorkflowHopMeta[] {before},
@@ -2716,6 +2727,8 @@ public class HopGuiWorkflowGraph extends 
HopGuiAbstractGraph
     messageBox.open();
   }
 
+  @SuppressWarnings({"java:S1854", "java:S1481"})
+  // Ignore warning of setting tipImage
   protected void setToolTip(int x, int y, int screenX, int screenY) {
     if (!hopGui.getProps().showToolTips() || openedContextDialog) {
       return;
@@ -3174,8 +3187,6 @@ public class HopGuiWorkflowGraph extends 
HopGuiAbstractGraph
           new NotePadMeta[] {before},
           new NotePadMeta[] {notePadMeta},
           new int[] {workflowMeta.indexOfNote(notePadMeta)});
-      // notePadMeta.width = ConstUi.NOTE_MIN_SIZE;
-      // notePadMeta.height = ConstUi.NOTE_MIN_SIZE;
 
       updateGui();
     }
@@ -3212,11 +3223,7 @@ public class HopGuiWorkflowGraph extends 
HopGuiAbstractGraph
     double anglePoint = Math.atan2(y - y1, x - x1) + Math.PI;
 
     // Same angle, or close enough?
-    if (anglePoint >= angleLine - 0.01 && anglePoint <= angleLine + 0.01) {
-      return true;
-    }
-
-    return false;
+    return anglePoint >= angleLine - 0.01 && anglePoint <= angleLine + 0.01;
   }
 
   @Override
@@ -3342,10 +3349,7 @@ public class HopGuiWorkflowGraph extends 
HopGuiAbstractGraph
     if (workflow.isActive()) {
       return true;
     }
-    if (workflow.isInitialized()) {
-      return true;
-    }
-    return false;
+    return workflow.isInitialized();
   }
 
   /**
@@ -3757,11 +3761,8 @@ public class HopGuiWorkflowGraph extends 
HopGuiAbstractGraph
           }
           return true;
         }
-        if ((answer & SWT.NO) != 0) {
-          // User doesn't want to save but close
-          return true;
-        }
-        return false;
+        // User doesn't want to save but close
+        return (answer & SWT.NO) != 0;
       } else {
         return true;
       }
@@ -4421,10 +4422,8 @@ public class HopGuiWorkflowGraph extends 
HopGuiAbstractGraph
         return false;
       }
       ActionResult actionResult = actionTracker.getActionResult();
-      if (actionResult == null) {
-        // No execution information available yet (not started)
-        return false;
-      }
+      // No execution information available yet (not started)
+      return actionResult != null;
     }
     return true;
   }

Reply via email to