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));
+  }
+}

Reply via email to