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 bc502f0363 Minimap and infinite navigation, fixes #6094 (#6533)
bc502f0363 is described below
commit bc502f036307406cb381cb894a8447c49bd326ee
Author: Hans Van Akelyen <[email protected]>
AuthorDate: Tue Feb 10 11:49:05 2026 +0100
Minimap and infinite navigation, fixes #6094 (#6533)
---
.../java/org/apache/hop/core/gui/BasePainter.java | 580 +++++++++------------
.../org/apache/hop/pipeline/PipelinePainter.java | 44 ++
.../org/apache/hop/workflow/WorkflowPainter.java | 44 ++
.../core/gui/messages/messages_en_US.properties | 18 +
.../main/java/org/apache/hop/ui/core/PropsUi.java | 9 +
.../hopgui/file/pipeline/HopGuiPipelineGraph.java | 19 +-
.../hopgui/file/workflow/HopGuiWorkflowGraph.java | 20 +-
.../configuration/tabs/ConfigGuiOptionsTab.java | 14 +
.../perspective/execution/DragViewZoomBase.java | 112 ++--
.../org/apache/hop/ui/hopgui/shared/SwtGc.java | 1 -
.../core/dialog/messages/messages_en_US.properties | 2 +
11 files changed, 470 insertions(+), 393 deletions(-)
diff --git a/engine/src/main/java/org/apache/hop/core/gui/BasePainter.java
b/engine/src/main/java/org/apache/hop/core/gui/BasePainter.java
index ef312afca3..012b3bd1cc 100644
--- a/engine/src/main/java/org/apache/hop/core/gui/BasePainter.java
+++ b/engine/src/main/java/org/apache/hop/core/gui/BasePainter.java
@@ -18,6 +18,8 @@
package org.apache.hop.core.gui;
import java.util.List;
+import lombok.Getter;
+import lombok.Setter;
import org.apache.hop.base.BaseHopMeta;
import org.apache.hop.base.IBaseMeta;
import org.apache.hop.core.Const;
@@ -25,27 +27,24 @@ import org.apache.hop.core.NotePadMeta;
import org.apache.hop.core.exception.HopException;
import org.apache.hop.core.gui.AreaOwner.AreaType;
import org.apache.hop.core.gui.IGc.EColor;
+import org.apache.hop.core.gui.IGc.EFont;
import org.apache.hop.core.gui.IGc.EImage;
import org.apache.hop.core.gui.IGc.ELineStyle;
import org.apache.hop.core.util.Utils;
import org.apache.hop.core.variables.IVariables;
+import org.apache.hop.i18n.BaseMessages;
import org.apache.hop.pipeline.transform.stream.StreamIcon;
+@Getter
+@Setter
public abstract class BasePainter<Hop extends BaseHopMeta<?>, Part extends
IBaseMeta> {
+ private static final Class<?> PKG = BasePainter.class;
+
public final double theta = Math.toRadians(11); // arrowhead sharpness
public static final int MINI_ICON_MARGIN = 5;
- public static final int MINI_ICON_TRIANGLE_BASE = 20;
- public static final int MINI_ICON_DISTANCE = 4;
- public static final int MINI_ICON_SKEW = 0;
-
- public static final int CONTENT_MENU_INDENT = 4;
-
public static final int CORNER_RADIUS_5 = 10;
- public static final int CORNER_RADIUS_4 = 8;
- public static final int CORNER_RADIUS_3 = 6;
- public static final int CORNER_RADIUS_2 = 4;
protected boolean drawingBorderAroundName;
@@ -81,6 +80,12 @@ public abstract class BasePainter<Hop extends
BaseHopMeta<?>, Part extends IBase
protected Rectangle viewPort;
protected String mouseOverName;
+ /**
+ * When true, draw the origin boundary (dashed lines, hatching, and label).
Should be set from the
+ * UI based on "Enable infinite move" (only show when infinite move is
enabled).
+ */
+ protected boolean showOriginBoundary;
+
public BasePainter(
IGc gc,
IVariables variables,
@@ -152,7 +157,7 @@ public abstract class BasePainter<Hop extends
BaseHopMeta<?>, Part extends IBase
}
gc.setFont(
Const.NVL(note.getFontName(), noteFontName),
- (int) ((double) fontHeight / zoomFactor),
+ (int) (fontHeight / zoomFactor),
note.isFontBold(),
note.isFontItalic());
@@ -241,7 +246,7 @@ public abstract class BasePainter<Hop extends
BaseHopMeta<?>, Part extends IBase
}
gc.setLineStyle(ELineStyle.DASHDOT);
gc.setLineWidth(lineWidth);
- gc.setForeground(EColor.GRAY);
+ gc.setForeground(EColor.BLACK);
// SWT on Windows doesn't cater for negative rect.width/height so handle
here.
Point s = real2screen(rect.x, rect.y);
if (rect.width < 0) {
@@ -254,135 +259,186 @@ public abstract class BasePainter<Hop extends
BaseHopMeta<?>, Part extends IBase
gc.setLineStyle(ELineStyle.SOLID);
}
+ /**
+ * Maximum grid points to draw; avoids severe slowdown on large/zoomed-out
canvases (e.g.
+ * Windows).
+ */
+ private static final int MAX_GRID_POINTS = 50_000;
+
protected void drawGrid() {
- Point bounds = gc.getDeviceBounds();
- for (int x = 0; x < bounds.x; x += gridSize) {
- for (int y = 0; y < bounds.y; y += gridSize) {
- gc.drawPoint((int) (x + (offset.x % gridSize)), (int) (y + (offset.y %
gridSize)));
- }
+ if (area == null || area.x <= 0 || area.y <= 0) {
+ return;
+ }
+ // Grid is drawn in the same coordinate system as drawOriginBoundary: the
origin (0,0) of the
+ // pipeline is at (offset.x, offset.y) here. The hatched "outside
workable" area is x < offset.x
+ // or y < offset.y. So we only draw grid where x >= offset.x and y >=
offset.y.
+ float mag = Math.max(0.1f, magnification);
+ int originX = (int) Math.round(offset.x);
+ int originY = (int) Math.round(offset.y);
+ int workableMinX = Math.max(0, originX);
+ int workableMinY = Math.max(0, originY);
+ // Visible extent in this coordinate system is (0,0) to (area.x/mag,
area.y/mag); workable part
+ // is from (originX, originY) to that right/bottom edge.
+ int workableMaxX = (int) Math.ceil(area.x / mag);
+ int workableMaxY = (int) Math.ceil(area.y / mag);
+ if (workableMaxX <= workableMinX || workableMaxY <= workableMinY) {
+ return;
}
- }
-
- protected int round(double value) {
- return (int) Math.round(value);
- }
-
- protected int calcArrowLength() {
- return 19 + (lineWidth - 1) * 5; // arrowhead length
- }
- /**
- * @return the magnification
- */
- public float getMagnification() {
- return magnification;
+ int baseStep = (mag < 1.0f) ? Math.max(gridSize, (int) (gridSize / mag)) :
gridSize;
+ int rangeX = workableMaxX - workableMinX;
+ int rangeY = workableMaxY - workableMinY;
+ long totalPoints = (long) (rangeX / baseStep + 1) * (rangeY / baseStep +
1);
+ int step = baseStep;
+ if (totalPoints > MAX_GRID_POINTS) {
+ int minStep =
+ Math.max(
+ gridSize,
+ (int) Math.ceil(Math.sqrt((double) rangeX * rangeY / (double)
MAX_GRID_POINTS)));
+ step = Math.max(baseStep, minStep);
+ }
+ int offsetX = (int) (offset.x % step);
+ int offsetY = (int) (offset.y % step);
+ if (offsetX < 0) {
+ offsetX += step;
+ }
+ if (offsetY < 0) {
+ offsetY += step;
+ }
+ // First grid position at or after workable visible origin (never in
hatched area)
+ int startX =
+ Math.max(
+ workableMinX,
+ offsetX + step * (int) Math.ceil((double) (workableMinX - offsetX)
/ step));
+ int startY =
+ Math.max(
+ workableMinY,
+ offsetY + step * (int) Math.ceil((double) (workableMinY - offsetY)
/ step));
+ for (int x = startX; x < workableMaxX; x += step) {
+ for (int y = startY; y < workableMaxY; y += step) {
+ if (x >= 0 && y >= 0) {
+ gc.drawPoint(x, y);
+ }
+ }
+ }
}
/**
- * @param magnification the magnification to set
+ * Draws a subtle boundary at the origin (0,0) and hatches the negative
coordinate space to
+ * indicate that transforms/actions cannot be created there. Only drawn when
showOriginBoundary is
+ * true (e.g. when "Enable infinite move" is on). Same coordinate system as
drawGrid (real = graph
+ * + offset).
*/
- public void setMagnification(float magnification) {
- this.magnification = magnification;
- }
-
- public Point getArea() {
- return area;
- }
-
- public void setArea(Point area) {
- this.area = area;
- }
-
- public List<AreaOwner> getAreaOwners() {
- return areaOwners;
- }
-
- public void setAreaOwners(List<AreaOwner> areaOwners) {
- this.areaOwners = areaOwners;
- }
-
- public DPoint getOffset() {
- return offset;
- }
-
- public void setOffset(DPoint offset) {
- this.offset = offset;
- }
-
- public int getIconSize() {
- return iconSize;
- }
-
- public void setIconSize(int iconSize) {
- this.iconSize = iconSize;
- }
-
- public int getGridSize() {
- return gridSize;
- }
-
- public void setGridSize(int gridSize) {
- this.gridSize = gridSize;
- }
-
- public Rectangle getSelectionRectangle() {
- return selectionRectangle;
- }
-
- public void setSelectionRectangle(Rectangle selectionRectangle) {
- this.selectionRectangle = selectionRectangle;
- }
-
- public int getLineWidth() {
- return lineWidth;
- }
-
- public void setLineWidth(int lineWidth) {
- this.lineWidth = lineWidth;
- }
-
- public Object getSubject() {
- return subject;
- }
+ protected void drawOriginBoundary() {
+ if (!showOriginBoundary) {
+ return;
+ }
+ double visW = area.x / Math.max(0.01, magnification);
+ double visH = area.y / Math.max(0.01, magnification);
+ int ox = (int) Math.round(offset.x);
+ int oy = (int) Math.round(offset.y);
- public void setSubject(Object subject) {
- this.subject = subject;
- }
+ int alpha = gc.getAlpha();
+ gc.setAlpha(160);
+
+ // Hatch the negative region as an L-shape so the corner is not
double-hatched:
+ // 1) Full strip left of origin: [0, ox] x [0, visH]
+ // 2) Top strip above origin only to the right of the left strip: [ox,
visW] x [0, oy]
+ if (ox > 0 && oy > 0) {
+ drawHatching(0, 0, ox, (int) visH); // left strip (full height)
+ int topW = (int) visW - ox;
+ if (topW > 0 && oy > 0) {
+ drawHatching(ox, 0, topW, oy); // top strip (no overlap)
+ }
+ } else if (ox > 0 && ox < visW) {
+ drawHatching(0, 0, ox, (int) visH);
+ } else if (oy > 0 && oy < visH) {
+ drawHatching(0, 0, (int) visW, oy);
+ }
- public IGc getGc() {
- return gc;
- }
+ gc.setAlpha(alpha);
- public void setGc(IGc gc) {
- this.gc = gc;
- }
+ // Dashed border on top of the hatching: only the positive side of the
axes (actionable area),
+ // so the line does not extend into the negative/hatched region
+ gc.setForeground(EColor.BLACK);
+ gc.setLineWidth(1);
+ gc.setLineStyle(ELineStyle.DASH);
- public String getNoteFontName() {
- return noteFontName;
- }
+ if (ox >= 0 && ox <= visW && oy <= visH) {
+ gc.drawLine(ox, oy, ox, (int) visH); // vertical segment from origin
downward
+ }
+ if (oy >= 0 && oy <= visH && ox <= visW) {
+ gc.drawLine(ox, oy, (int) visW, oy); // horizontal segment from origin
rightward
+ }
- public void setNoteFontName(String noteFontName) {
- this.noteFontName = noteFontName;
- }
+ gc.setLineStyle(ELineStyle.SOLID);
- public int getNoteFontHeight() {
- return noteFontHeight;
+ // "Outside workable area" label on the x-axis and y-axis (both
normal/horizontal)
+ String originBoundaryLabel = BaseMessages.getString(PKG,
"BasePainter.OutsideWorkableArea");
+ if (originBoundaryLabel != null && !originBoundaryLabel.isEmpty()) {
+ gc.setForeground(EColor.DARKGRAY);
+ gc.setFont(EFont.SMALL);
+ Point textSize = gc.textExtent(originBoundaryLabel);
+ int tw = textSize.x;
+ int th = textSize.y;
+ if (ox > tw && ox < visW && (int) visH > th) {
+ int tx = Math.max(4, (ox - tw) / 2);
+ int ty = ((int) visH - th) / 2;
+ gc.drawText(originBoundaryLabel, tx, ty);
+ }
+ if (oy > th && oy < visH && (int) visW - ox > tw) {
+ int topW = (int) visW - ox;
+ int tx = ox + (topW - tw) / 2;
+ int ty = Math.max(4, (oy - th) / 2);
+ gc.drawText(originBoundaryLabel, tx, ty);
+ }
+ }
}
- public void setNoteFontHeight(int noteFontHeight) {
- this.noteFontHeight = noteFontHeight;
- }
+ /**
+ * Base spacing between hatching lines at 100% zoom; scaled by zoom so we
draw fewer when zoomed
+ * out.
+ */
+ private static final int HATCH_STEP_BASE = 24;
- public double getTheta() {
- return theta;
+ /**
+ * Draws subtle diagonal hatching in the given rectangle. Uses a global
diagonal grid (lines x+y =
+ * base + n*step) so that when multiple regions are hatched (e.g. left and
top of origin), the
+ * lines align seamlessly across the boundary.
+ */
+ private void drawHatching(int x, int y, int w, int h) {
+ if (w <= 0 || h <= 0) {
+ return;
+ }
+ gc.setForeground(EColor.DARKGRAY);
+ gc.setLineWidth(1);
+ gc.setLineStyle(ELineStyle.SOLID);
+ float mag = Math.max(0.1f, magnification);
+ int step = Math.max(HATCH_STEP_BASE, (int) (HATCH_STEP_BASE / mag));
+ // Global base so all hatched regions share the same grid (aligns at
boundaries).
+ int base = (int) (((offset.x + offset.y) % step + step) % step);
+ // Diagonals are lines where x+y = K; they intersect the rect when K is in
[x+y, x+w+y+h].
+ int kMin = x + y;
+ int kMax = x + w + y + h;
+ int nStart = (int) Math.ceil((double) (kMin - base) / step);
+ int nEnd = (int) Math.floor((double) (kMax - base) / step);
+ for (int n = nStart; n <= nEnd; n++) {
+ int k = base + n * step;
+ // Clip line x+y=k to rect [x,x+w] x [y,y+h]: x in [max(x, k-(y+h)),
min(x+w, k-y)].
+ int x1 = Math.max(x, k - (y + h));
+ int x2 = Math.min(x + w, k - y);
+ if (x1 <= x2) {
+ gc.drawLine(x1, k - x1, x2, k - x2);
+ }
+ }
}
- public Hop getCandidate() {
- return candidate;
+ protected int round(double value) {
+ return (int) Math.round(value);
}
- public void setCandidate(Hop candidate) {
- this.candidate = candidate;
+ protected int calcArrowLength() {
+ return 19 + (lineWidth - 1) * 5; // arrowhead length
}
protected int[] getLine(Part fs, Part ts) {
@@ -435,252 +491,108 @@ public abstract class BasePainter<Hop extends
BaseHopMeta<?>, Part extends IBase
Object endObject)
throws HopException;
+ /** Fixed width of the navigation viewport (minimap) in pixels. */
+ private static final int VIEWPORT_WIDTH = 200;
+
+ /** Maximum height of the minimap; height follows content aspect ratio up to
this cap. */
+ private static final int VIEWPORT_HEIGHT_MAX = 200;
+
/**
* Draw a small rectangle at the bottom right of the screen which depicts
the viewport as a part
- * of the total size of the metadata graph. If the graph fits completely on
the screen, the
- * navigation view is not shown.
+ * of the total size of the metadata graph. Width is fixed; height follows
content aspect ratio
+ * (capped) so the minimap represents the graph without spurious empty
space. Content is top-left
+ * aligned so "at the top" of the canvas matches the top of the minimap.
*/
protected void drawNavigationView() {
if (!showingNavigationView || maximum == null) {
- // Disabled or no maximum size available
return;
}
+ double contentMaxX = Math.max(1, maximum.x);
+ double contentMaxY = Math.max(1, maximum.y);
- // Compensate the screen size for the current magnification
+ // 1) Visible rectangle in graph coordinates (unclamped so we can show
panned-outside view).
//
- int areaWidth = (int) (area.x / magnification);
- int areaHeight = (int) (area.y / magnification);
-
- // We want to show a rectangle depicting the total area of the
pipeline/workflow graph.
- // This area must be adjusted to a maximum of 200 if it is too large.
+ double mag = Math.max(0.01, magnification);
+ double visibleLeft = -offset.x;
+ double visibleTop = -offset.y;
+ double visibleWidthGraph = area.x / mag;
+ double visibleHeightGraph = area.y / mag;
+ double visibleRightGraph = visibleLeft + visibleWidthGraph;
+ double visibleBottomGraph = visibleTop + visibleHeightGraph;
+
+ // 2) Minimap bounds = union of content and visible view so the overlay
always fits inside.
//
- double graphWidth = maximum.x;
- double graphHeight = maximum.y;
- if (graphWidth > 200 || graphHeight > 200) {
- double coefficient = 200 / Math.max(graphWidth, graphHeight);
- graphWidth *= coefficient;
- graphHeight *= coefficient;
- }
-
- // Position it in the bottom right corner of the screen
+ double minGraphX = Math.min(0, visibleLeft);
+ double minGraphY = Math.min(0, visibleTop);
+ double maxGraphX = Math.max(contentMaxX, visibleRightGraph);
+ double maxGraphY = Math.max(contentMaxY, visibleBottomGraph);
+ double graphRangeX = Math.max(1, maxGraphX - minGraphX);
+ double graphRangeY = Math.max(1, maxGraphY - minGraphY);
+
+ // 3) Fixed width; height follows content aspect ratio (capped) so the
minimap fits the graph.
//
- double graphX = area.x - graphWidth - 10.0;
- double graphY = area.y - graphHeight - 10.0;
+ int viewportHeight =
+ Math.min(
+ VIEWPORT_HEIGHT_MAX,
+ Math.max(20, (int) Math.round(VIEWPORT_WIDTH * graphRangeY /
graphRangeX)));
+ double scale = Math.min(VIEWPORT_WIDTH / graphRangeX, viewportHeight /
graphRangeY);
+ double contentWidth = graphRangeX * scale;
+ double contentHeight = graphRangeY * scale;
+ double contentLeft = area.x - VIEWPORT_WIDTH - 10.0;
+ double contentTop = area.y - viewportHeight - 10.0;
+ // Top-left align so "at the top" of the canvas matches the top of the
minimap
int alpha = gc.getAlpha();
gc.setAlpha(75);
gc.setForeground(EColor.DARKGRAY);
gc.setBackground(EColor.LIGHTBLUE);
- gc.drawRectangle((int) graphX, (int) graphY, (int) graphWidth, (int)
graphHeight);
- gc.fillRectangle((int) graphX, (int) graphY, (int) graphWidth, (int)
graphHeight);
-
- // Now draw a darker area inside showing the size of the view-screen in
relation to the graph
- // surface. The view size is a fraction of the total graph area outlined
above.
- //
- double viewWidth = (graphWidth * areaWidth) / Math.max(areaWidth,
maximum.x);
- double viewHeight = (graphHeight * areaHeight) / Math.max(areaHeight,
maximum.y);
-
- // The offset is a part of the screen size. The maximum offset is the
graph size minus the area
- // size.
- // The offset horizontally is [0, -maximum.x+areaWidth]
- // The idea is that if the right side of the pipeline or workflow is shown
you don't need to
- // scroll further.
- // The offset fractions calculated below are in the range 0-1 (there about)
- //
- double offsetXFraction = (double) (-offset.x) / ((double) maximum.x);
- double offsetYFraction = (double) (-offset.y) / ((double) maximum.y);
+ gc.drawRectangle((int) contentLeft, (int) contentTop, VIEWPORT_WIDTH,
viewportHeight);
+ gc.fillRectangle((int) contentLeft, (int) contentTop, VIEWPORT_WIDTH,
viewportHeight);
- // We shift the view rectangle to the right or down based on the offset
fraction and the wiggle
- // room of the inner rectangle.
+ // 3) Origin for graph (0,0) in minimap pixels; content and overlay use
same scale.
//
- double viewX = graphX + (graphWidth) * offsetXFraction;
- double viewY = graphY + (graphHeight) * offsetYFraction;
+ double graphOriginX = contentLeft - minGraphX * scale;
+ double graphOriginY = contentTop - minGraphY * scale;
+ drawNavigationViewContent(graphOriginX, graphOriginY, scale, scale);
+
+ double viewX = graphOriginX + visibleLeft * scale;
+ double viewY = graphOriginY + visibleTop * scale;
+ double viewWidth = visibleWidthGraph * scale;
+ double viewHeight = visibleHeightGraph * scale;
+
+ // Clamp overlay to the drawn content area (avoid drawing outside the
light blue)
+ viewX = Math.max(contentLeft, Math.min(contentLeft + contentWidth - 1,
viewX));
+ viewY = Math.max(contentTop, Math.min(contentTop + contentHeight - 1,
viewY));
+ viewWidth = Math.min(viewWidth, contentLeft + contentWidth - viewX);
+ viewHeight = Math.min(viewHeight, contentTop + contentHeight - viewY);
+ viewWidth = Math.max(0, viewWidth);
+ viewHeight = Math.max(0, viewHeight);
gc.setForeground(EColor.BLACK);
gc.setBackground(EColor.BLUE);
gc.drawRectangle((int) viewX, (int) viewY, (int) viewWidth, (int)
viewHeight);
gc.fillRectangle((int) viewX, (int) viewY, (int) viewWidth, (int)
viewHeight);
- // We remember the rectangles so that we can navigate in it when the user
drags it around.
- //
- graphPort = new Rectangle((int) graphX, (int) graphY, (int) graphWidth,
(int) graphHeight);
+ graphPort = new Rectangle((int) contentLeft, (int) contentTop,
VIEWPORT_WIDTH, viewportHeight);
viewPort = new Rectangle((int) viewX, (int) viewY, (int) viewWidth, (int)
viewHeight);
gc.setAlpha(alpha);
}
/**
- * Gets zoomFactor
- *
- * @return value of zoomFactor
- */
- public double getZoomFactor() {
- return zoomFactor;
- }
-
- /**
- * @param zoomFactor The zoomFactor to set
- */
- public void setZoomFactor(double zoomFactor) {
- this.zoomFactor = zoomFactor;
- }
-
- /**
- * Gets drawingEditIcons
- *
- * @return value of drawingBorderAroundName
- */
- public boolean isDrawingBorderAroundName() {
- return drawingBorderAroundName;
- }
-
- /**
- * @param drawingBorderAroundName The option to set
- */
- public void setDrawingBorderAroundName(boolean drawingBorderAroundName) {
- this.drawingBorderAroundName = drawingBorderAroundName;
- }
-
- /**
- * Gets miniIconSize
- *
- * @return value of miniIconSize
- */
- public int getMiniIconSize() {
- return miniIconSize;
- }
-
- /**
- * @param miniIconSize The miniIconSize to set
- */
- public void setMiniIconSize(int miniIconSize) {
- this.miniIconSize = miniIconSize;
- }
-
- /**
- * Gets showingNavigationView
- *
- * @return value of showingNavigationView
- */
- public boolean isShowingNavigationView() {
- return showingNavigationView;
- }
-
- /**
- * Sets showingNavigationView
- *
- * @param showingNavigationView value of showingNavigationView
- */
- public void setShowingNavigationView(boolean showingNavigationView) {
- this.showingNavigationView = showingNavigationView;
- }
-
- /**
- * Gets maximum
- *
- * @return value of maximum
- */
- public Point getMaximum() {
- return maximum;
- }
-
- /**
- * Sets maximum
+ * Override to draw rectangles and lines inside the navigation viewport
representing the graph
+ * elements (e.g. transforms/actions and hops). Coordinates are in graph
space; convert to
+ * viewport pixels with: screenX = graphX + graphCoordX * scaleX, screenY =
graphY + graphCoordY *
+ * scaleY.
*
- * @param maximum value of maximum
+ * @param graphX left of the viewport rectangle in screen pixels
+ * @param graphY top of the viewport rectangle in screen pixels
+ * @param scaleX scale from graph X to viewport width
+ * @param scaleY scale from graph Y to viewport height
*/
- public void setMaximum(Point maximum) {
- this.maximum = maximum;
- }
-
- /**
- * Gets variables
- *
- * @return value of variables
- */
- public IVariables getVariables() {
- return variables;
- }
-
- /**
- * Sets variables
- *
- * @param variables value of variables
- */
- public void setVariables(IVariables variables) {
- this.variables = variables;
- }
-
- /**
- * Gets screenMagnification
- *
- * @return value of screenMagnification
- */
- public float getScreenMagnification() {
- return screenMagnification;
- }
-
- /**
- * Sets screenMagnification
- *
- * @param screenMagnification value of screenMagnification
- */
- public void setScreenMagnification(float screenMagnification) {
- this.screenMagnification = screenMagnification;
- }
-
- /**
- * Gets graphPort
- *
- * @return value of graphPort
- */
- public Rectangle getGraphPort() {
- return graphPort;
- }
-
- /**
- * Sets graphPort
- *
- * @param graphPort value of graphPort
- */
- public void setGraphPort(Rectangle graphPort) {
- this.graphPort = graphPort;
- }
-
- /**
- * Gets viewPort
- *
- * @return value of viewPort
- */
- public Rectangle getViewPort() {
- return viewPort;
- }
-
- /**
- * Sets viewPort
- *
- * @param viewPort value of viewPort
- */
- public void setViewPort(Rectangle viewPort) {
- this.viewPort = viewPort;
- }
-
- /**
- * Gets mouseOverName
- *
- * @return value of mouseOverName
- */
- public String getMouseOverName() {
- return mouseOverName;
- }
-
- /**
- * Sets mouseOverName
- *
- * @param mouseOverName value of mouseOverName
- */
- public void setMouseOverName(String mouseOverName) {
- this.mouseOverName = mouseOverName;
+ protected void drawNavigationViewContent(
+ double graphX, double graphY, double scaleX, double scaleY) {
+ // Default: nothing. PipelinePainter and WorkflowPainter draw
transforms/actions and hops.
}
}
diff --git a/engine/src/main/java/org/apache/hop/pipeline/PipelinePainter.java
b/engine/src/main/java/org/apache/hop/pipeline/PipelinePainter.java
index 80dd852873..1854417411 100644
--- a/engine/src/main/java/org/apache/hop/pipeline/PipelinePainter.java
+++ b/engine/src/main/java/org/apache/hop/pipeline/PipelinePainter.java
@@ -211,10 +211,54 @@ public class PipelinePainter extends
BasePainter<PipelineHopMeta, TransformMeta>
gc.dispose();
}
+ @Override
+ protected void drawNavigationViewContent(
+ double graphX, double graphY, double scaleX, double scaleY) {
+ if (pipelineMeta == null || maximum == null) {
+ return;
+ }
+ // Minimum size in viewport pixels so transforms remain visible
+ int minSize = 2;
+ // Draw hops as lines first (behind transforms)
+ gc.setForeground(EColor.DARKGRAY);
+ gc.setLineWidth(1);
+ for (PipelineHopMeta hop : pipelineMeta.getPipelineHops()) {
+ if (hop.getFromTransform() == null || hop.getToTransform() == null) {
+ continue;
+ }
+ Point fromLoc = hop.getFromTransform().getLocation();
+ Point toLoc = hop.getToTransform().getLocation();
+ if (fromLoc == null || toLoc == null) {
+ continue;
+ }
+ int fromCenterX = (int) (graphX + (fromLoc.x + iconSize / 2) * scaleX);
+ int fromCenterY = (int) (graphY + (fromLoc.y + iconSize / 2) * scaleY);
+ int toCenterX = (int) (graphX + (toLoc.x + iconSize / 2) * scaleX);
+ int toCenterY = (int) (graphY + (toLoc.y + iconSize / 2) * scaleY);
+ gc.drawLine(fromCenterX, fromCenterY, toCenterX, toCenterY);
+ }
+ // Draw transforms as small rectangles
+ gc.setForeground(EColor.BLACK);
+ gc.setBackground(EColor.WHITE);
+ for (TransformMeta transform : pipelineMeta.getTransforms()) {
+ Point loc = transform.getLocation();
+ if (loc == null) {
+ continue;
+ }
+ int w = Math.max(minSize, (int) Math.ceil(iconSize * scaleX));
+ int h = Math.max(minSize, (int) Math.ceil(iconSize * scaleY));
+ int x = (int) (graphX + loc.x * scaleX);
+ int y = (int) (graphY + loc.y * scaleY);
+ gc.fillRectangle(x, y, w, h);
+ gc.drawRectangle(x, y, w, h);
+ }
+ }
+
private void drawPipeline() throws HopException {
if (gridSize > 1) {
drawGrid();
}
+ drawOriginBoundary();
try {
ExtensionPointHandler.callExtensionPoint(
diff --git a/engine/src/main/java/org/apache/hop/workflow/WorkflowPainter.java
b/engine/src/main/java/org/apache/hop/workflow/WorkflowPainter.java
index 3cfce6792d..bbc04c8aea 100644
--- a/engine/src/main/java/org/apache/hop/workflow/WorkflowPainter.java
+++ b/engine/src/main/java/org/apache/hop/workflow/WorkflowPainter.java
@@ -111,10 +111,54 @@ public class WorkflowPainter extends
BasePainter<WorkflowHopMeta, ActionMeta> {
gc.dispose();
}
+ @Override
+ protected void drawNavigationViewContent(
+ double graphX, double graphY, double scaleX, double scaleY) {
+ if (workflowMeta == null || maximum == null) {
+ return;
+ }
+ // Minimum size in viewport pixels so actions remain visible
+ int minSize = 2;
+ // Draw hops as lines first (behind actions)
+ gc.setForeground(EColor.DARKGRAY);
+ gc.setLineWidth(1);
+ for (WorkflowHopMeta hop : workflowMeta.getWorkflowHops()) {
+ if (hop.getFromAction() == null || hop.getToAction() == null) {
+ continue;
+ }
+ Point fromLoc = hop.getFromAction().getLocation();
+ Point toLoc = hop.getToAction().getLocation();
+ if (fromLoc == null || toLoc == null) {
+ continue;
+ }
+ int fromCenterX = (int) (graphX + (fromLoc.x + iconSize / 2) * scaleX);
+ int fromCenterY = (int) (graphY + (fromLoc.y + iconSize / 2) * scaleY);
+ int toCenterX = (int) (graphX + (toLoc.x + iconSize / 2) * scaleX);
+ int toCenterY = (int) (graphY + (toLoc.y + iconSize / 2) * scaleY);
+ gc.drawLine(fromCenterX, fromCenterY, toCenterX, toCenterY);
+ }
+ // Draw actions as small rectangles
+ gc.setForeground(EColor.BLACK);
+ gc.setBackground(EColor.WHITE);
+ for (ActionMeta action : workflowMeta.getActions()) {
+ Point loc = action.getLocation();
+ if (loc == null) {
+ continue;
+ }
+ int w = Math.max(minSize, (int) Math.ceil(iconSize * scaleX));
+ int h = Math.max(minSize, (int) Math.ceil(iconSize * scaleY));
+ int x = (int) (graphX + loc.x * scaleX);
+ int y = (int) (graphY + loc.y * scaleY);
+ gc.fillRectangle(x, y, w, h);
+ gc.drawRectangle(x, y, w, h);
+ }
+ }
+
private void drawActions() throws HopException {
if (gridSize > 1) {
drawGrid();
}
+ drawOriginBoundary();
try {
ExtensionPointHandler.callExtensionPoint(
diff --git
a/engine/src/main/resources/org/apache/hop/core/gui/messages/messages_en_US.properties
b/engine/src/main/resources/org/apache/hop/core/gui/messages/messages_en_US.properties
new file mode 100644
index 0000000000..591601afe9
--- /dev/null
+++
b/engine/src/main/resources/org/apache/hop/core/gui/messages/messages_en_US.properties
@@ -0,0 +1,18 @@
+#
+# 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.
+#
+
+BasePainter.OutsideWorkableArea=Outside workable area
diff --git a/ui/src/main/java/org/apache/hop/ui/core/PropsUi.java
b/ui/src/main/java/org/apache/hop/ui/core/PropsUi.java
index ffb34f25d7..0a352e0726 100644
--- a/ui/src/main/java/org/apache/hop/ui/core/PropsUi.java
+++ b/ui/src/main/java/org/apache/hop/ui/core/PropsUi.java
@@ -83,6 +83,7 @@ public class PropsUi extends Props {
private static final String GRAPH_EXTRA_VIEW_VERTICAL_ORIENTATION =
"GraphExtraViewVerticalOrientation";
private static final String DISABLE_ZOOM_SCROLLING = "DisableZoomScrolling";
+ private static final String ENABLE_INFINITE_CANVAS_MOVE =
"EnableInfiniteCanvasMove";
public static final int DEFAULT_MAX_EXECUTION_LOGGING_TEXT_SIZE = 2000000;
private Map<RGB, RGB> contrastingColors;
@@ -1173,4 +1174,12 @@ public class PropsUi extends Props {
public void setZoomScrollingDisabled(boolean disabled) {
setProperty(DISABLE_ZOOM_SCROLLING, disabled ? YES : NO);
}
+
+ public boolean isInfiniteCanvasMoveEnabled() {
+ return YES.equalsIgnoreCase(getProperty(ENABLE_INFINITE_CANVAS_MOVE, NO));
+ }
+
+ public void setInfiniteCanvasMoveEnabled(boolean enabled) {
+ setProperty(ENABLE_INFINITE_CANVAS_MOVE, enabled ? YES : NO);
+ }
}
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 c4554a0185..7819fa8b51 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
@@ -1423,9 +1423,13 @@ public class HopGuiPipelineGraph extends
HopGuiAbstractGraph
String message = null;
switch (fSingleClickType) {
case Pipeline:
- message =
- BaseMessages.getString(PKG,
"PipelineGraph.ContextualActionDialog.Pipeline.Header");
- contextHandler = new HopGuiPipelineContext(pipelineMeta, this, real);
+ // Do not show context menu in negative coordinate space (transforms
cannot be created
+ // there)
+ if (real.x >= 0 && real.y >= 0) {
+ message =
+ BaseMessages.getString(PKG,
"PipelineGraph.ContextualActionDialog.Pipeline.Header");
+ contextHandler = new HopGuiPipelineContext(pipelineMeta, this,
real);
+ }
break;
case Transform:
message =
@@ -3584,6 +3588,7 @@ public class HopGuiPipelineGraph extends
HopGuiAbstractGraph
pipelinePainter.setShowingNavigationView(true);
pipelinePainter.setScreenMagnification(magnification);
pipelinePainter.setShowingNavigationView(!PropsUi.getInstance().isHideViewportEnabled());
+
pipelinePainter.setShowOriginBoundary(PropsUi.getInstance().isInfiniteCanvasMoveEnabled());
try {
pipelinePainter.drawPipelineImage();
@@ -3599,11 +3604,13 @@ public class HopGuiPipelineGraph extends
HopGuiAbstractGraph
BasePropertyHandler.getProperty("PipelineCanvas_image"),
getClass().getClassLoader());
gc.setTransform(0.0f, 0.0f, (float) (magnification *
PropsUi.getNativeZoomFactor()));
- gc.drawImage(svgFile, 150, 150, 32, 40, gc.getMagnification(), 0);
+ int iconX = 150 + (int) Math.round(offset.x);
+ int iconY = 150 + (int) Math.round(offset.y);
+ gc.drawImage(svgFile, iconX, iconY, 32, 40, gc.getMagnification(),
0);
gc.drawText(
BaseMessages.getString(PKG,
"PipelineGraph.NewPipelineBackgroundMessage"),
- 155,
- 125,
+ 155 + (int) Math.round(offset.x),
+ 125 + (int) Math.round(offset.y),
true);
}
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 5d7b66b7b0..2ec438a9ac 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
@@ -1197,10 +1197,13 @@ public class HopGuiWorkflowGraph extends
HopGuiAbstractGraph
String message = null;
switch (fSingleClickType) {
case Workflow:
- message =
- BaseMessages.getString(
- PKG,
"HopGuiWorkflowGraph.ContextualActionDialog.Workflow.Header");
- contextHandler = new HopGuiWorkflowContext(workflowMeta, this, real);
+ // Do not show context menu in negative coordinate space (actions
cannot be created there)
+ if (real.x >= 0 && real.y >= 0) {
+ message =
+ BaseMessages.getString(
+ PKG,
"HopGuiWorkflowGraph.ContextualActionDialog.Workflow.Header");
+ contextHandler = new HopGuiWorkflowContext(workflowMeta, this,
real);
+ }
break;
case Action:
message =
@@ -3142,6 +3145,7 @@ public class HopGuiWorkflowGraph extends
HopGuiAbstractGraph
workflowPainter.setShowingNavigationView(true);
workflowPainter.setScreenMagnification(magnification);
workflowPainter.setShowingNavigationView(!PropsUi.getInstance().isHideViewportEnabled());
+
workflowPainter.setShowOriginBoundary(PropsUi.getInstance().isInfiniteCanvasMoveEnabled());
try {
workflowPainter.drawWorkflow();
@@ -3160,11 +3164,13 @@ public class HopGuiWorkflowGraph extends
HopGuiAbstractGraph
BasePropertyHandler.getProperty("WorkflowCanvas_image"),
getClass().getClassLoader());
gc.setTransform(0.0f, 0.0f, (float) (magnification *
PropsUi.getNativeZoomFactor()));
- gc.drawImage(svgFile, 150, 150, 32, 40, gc.getMagnification(), 0);
+ int iconX = 150 + (int) Math.round(offset.x);
+ int iconY = 150 + (int) Math.round(offset.y);
+ gc.drawImage(svgFile, iconX, iconY, 32, 40, gc.getMagnification(),
0);
gc.drawText(
BaseMessages.getString(PKG,
"HopGuiWorkflowGraph.NewWorkflowBackgroundMessage"),
- 155,
- 125,
+ 155 + (int) Math.round(offset.x),
+ 125 + (int) Math.round(offset.y),
true);
}
} catch (Exception e) {
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/configuration/tabs/ConfigGuiOptionsTab.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/configuration/tabs/ConfigGuiOptionsTab.java
index d87104f259..70b587183e 100644
---
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/configuration/tabs/ConfigGuiOptionsTab.java
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/configuration/tabs/ConfigGuiOptionsTab.java
@@ -90,6 +90,7 @@ public class ConfigGuiOptionsTab {
private Button wHideViewport;
private Button wUseDoubleClick;
private Button wDrawBorderAroundCanvasNames;
+ private Button wEnableInfiniteMove;
private Button wDisableZoomScrolling;
private Button wHideMenuBar;
private Button wShowTableViewToolbar;
@@ -160,6 +161,7 @@ public class ConfigGuiOptionsTab {
wHideViewport.setSelection(!props.isHideViewportEnabled()); // Inverted
logic
wUseDoubleClick.setSelection(props.useDoubleClick());
wDrawBorderAroundCanvasNames.setSelection(props.isBorderDrawnAroundCanvasNames());
+ wEnableInfiniteMove.setSelection(props.isInfiniteCanvasMoveEnabled());
wHideMenuBar.setSelection(props.isHidingMenuBar());
wShowTableViewToolbar.setSelection(props.isShowTableViewToolbar());
wDarkMode.setSelection(props.isDarkMode());
@@ -598,6 +600,17 @@ public class ConfigGuiOptionsTab {
margin);
lastCanvasControl = wDrawBorderAroundCanvasNames;
+ // Enable infinite move
+ wEnableInfiniteMove =
+ createCheckbox(
+ canvasContent,
+ "EnterOptionsDialog.EnableInfiniteMove.Label",
+ "EnterOptionsDialog.EnableInfiniteMove.ToolTip",
+ props.isInfiniteCanvasMoveEnabled(),
+ lastCanvasControl,
+ margin);
+ lastCanvasControl = wEnableInfiniteMove;
+
// Disable zoom scrolling
wDisableZoomScrolling =
createCheckbox(
@@ -919,6 +932,7 @@ public class ConfigGuiOptionsTab {
!wHideViewport.getSelection()); // Inverted: checkbox is "show",
property is "hide"
props.setUseDoubleClickOnCanvas(wUseDoubleClick.getSelection());
props.setDrawBorderAroundCanvasNames(wDrawBorderAroundCanvasNames.getSelection());
+ props.setInfiniteCanvasMoveEnabled(wEnableInfiniteMove.getSelection());
props.setZoomScrollingDisabled(wDisableZoomScrolling.getSelection());
props.setDarkMode(wDarkMode.getSelection());
props.setHidingMenuBar(wHideMenuBar.getSelection());
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 d69d6eee4a..0a65d4898d 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
@@ -49,12 +49,16 @@ public abstract class DragViewZoomBase extends Composite {
@Override
public abstract void redraw();
+ /**
+ * Convert canvas/screen coordinates to graph coordinates. Must match the
drawing transform:
+ * canvas = magnification * (graph + offset), so graph = canvas /
magnification - offset.
+ */
public Point screen2real(int x, int y) {
float correctedMagnification = calculateCorrectedMagnification();
DPoint real =
new DPoint(
- ((double) x - offset.x) / correctedMagnification - offset.x,
- ((double) y - offset.y) / correctedMagnification - offset.y);
+ (double) x / correctedMagnification - offset.x,
+ (double) y / correctedMagnification - offset.y);
return real.toPoint();
}
@@ -197,8 +201,10 @@ public abstract class DragViewZoomBase extends Composite {
offset.x = offset.x + ((double) mouseEvent.x / area.x) * (viewWidth -
oldViewWidth);
offset.y = offset.y + ((double) mouseEvent.y / area.y) * (viewHeight -
oldViewHeight);
- offset.x = offset.x > 0 ? 0 : offset.x;
- offset.y = offset.y > 0 ? 0 : offset.y;
+ if (!PropsUi.getInstance().isInfiniteCanvasMoveEnabled()) {
+ offset.x = offset.x > 0 ? 0 : offset.x;
+ offset.y = offset.y > 0 ? 0 : offset.y;
+ }
validateOffset();
setZoomLabel();
@@ -281,21 +287,20 @@ public abstract class DragViewZoomBase extends Composite {
canvas.setData("panStartOffset", new Point((int) offset.x, (int)
offset.y));
canvas.setData("panCurrentOffset", new Point((int) offset.x, (int)
offset.y));
- // Pass boundary information to client for constraint validation
+ // Pass boundary information to client; when infinite move is off, do
not allow panning past
+ // origin (0,0)
double zoomFactor = PropsUi.getNativeZoomFactor() * Math.max(0.1,
magnification);
Point area = getArea();
double viewWidth = area.x / zoomFactor;
double viewHeight = area.y / zoomFactor;
-
- // Calculate min/max offset boundaries (same as validateOffset())
- double minX = -maximum.x + viewWidth;
- double minY = -maximum.y + viewHeight;
- double maxX = 0;
- double maxY = 0;
-
+ Point effective = getEffectiveMaximum();
+ double minX = -effective.x + viewWidth;
+ double minY = -effective.y + viewHeight;
+ int maxX = PropsUi.getInstance().isInfiniteCanvasMoveEnabled() ?
Integer.MAX_VALUE : 0;
+ int maxY = PropsUi.getInstance().isInfiniteCanvasMoveEnabled() ?
Integer.MAX_VALUE : 0;
canvas.setData(
"panBoundaries",
- new org.apache.hop.core.gui.Rectangle((int) minX, (int) minY,
(int) maxX, (int) maxY));
+ new org.apache.hop.core.gui.Rectangle((int) minX, (int) minY,
maxX, maxY));
// Force immediate redraw to sync pan data to client BEFORE mouse move
events
// This ensures the client has the pan data when the first MouseMove
arrives
@@ -324,48 +329,46 @@ public abstract class DragViewZoomBase extends Composite {
}
protected void dragViewPort(Point clickLocation) {
- // The delta is calculated
- //
double deltaX = clickLocation.x - viewPortStart.x;
double deltaY = clickLocation.y - viewPortStart.y;
- // What's the wiggle room for the little rectangle in the bigger one?
- //
- int wiggleX = viewPort.width;
- int wiggleY = viewPort.height;
-
- // What's that in percentages? We draw the little rectangle at 25% size.
+ // 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 deltaXPct = wiggleX == 0 ? 0 : deltaX / (wiggleX / 0.25);
- double deltaYPct = wiggleY == 0 ? 0 : deltaY / (wiggleY / 0.25);
-
- // The offset is then a matter of setting a percentage of the graph size
- //
- double deltaOffSetX = deltaXPct * maximum.x;
- double deltaOffSetY = deltaYPct * maximum.y;
+ double mag = Math.max(0.01, magnification);
+ 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 - deltaOffSetX,
viewDragBaseOffset.y - deltaOffSetY);
+ offset = new DPoint(viewDragBaseOffset.x - deltaGraphX,
viewDragBaseOffset.y - deltaGraphY);
- // Make sure we don't catapult the view somewhere we can't find the graph
anymore.
- //
validateOffset();
redraw();
}
public void validateOffset() {
+ if (maximum == null) {
+ return;
+ }
double zoomFactor = PropsUi.getNativeZoomFactor() * Math.max(0.1,
magnification);
- // What's the size of the graph when painted on screen?
- //
- double graphWidth = maximum.x;
- double graphHeight = maximum.y;
-
- // We need to know the size of the screen.
- //
Point area = getArea();
double viewWidth = area.x / zoomFactor;
double viewHeight = area.y / zoomFactor;
+ // Use effective graph size: when panning right/down we allow the canvas
to expand
+ // so the user can drag into empty space (same as transforming against the
side).
+ //
+ double graphWidth = getEffectiveMaximum().x;
+ double graphHeight = getEffectiveMaximum().y;
+
// Let's not move the graph off the screen to the top/left
//
double minX = -graphWidth + viewWidth;
@@ -377,16 +380,35 @@ public abstract class DragViewZoomBase extends Composite {
offset.y = minY;
}
- // Are we moving the graph too far down/right?
+ // Do not allow panning past the origin (0,0) unless "infinite move" is
enabled.
+ // Panning right/down (negative offset) can extend the visible view past
the content.
//
- double maxX = 0;
- if (offset.x > maxX) {
- offset.x = maxX;
+ if (!PropsUi.getInstance().isInfiniteCanvasMoveEnabled()) {
+ if (offset.x > 0) {
+ offset.x = 0;
+ }
+ if (offset.y > 0) {
+ offset.y = 0;
+ }
}
- double maxY = 0;
- if (offset.y > maxY) {
- offset.y = maxY;
+ }
+
+ /**
+ * Returns the effective canvas size used for pan validation and painting.
When the user pans
+ * right or down, the effective size grows so that empty space can be
revealed (similar to
+ * resizing a note against the edge).
+ */
+ public Point getEffectiveMaximum() {
+ if (maximum == null) {
+ return new Point(0, 0);
}
+ double zoomFactor = PropsUi.getNativeZoomFactor() * Math.max(0.1,
magnification);
+ Point area = getArea();
+ double viewWidth = area.x / zoomFactor;
+ double viewHeight = area.y / zoomFactor;
+ double effectiveX = Math.max(maximum.x, viewWidth - offset.x);
+ double effectiveY = Math.max(maximum.y, viewHeight - offset.y);
+ return new Point((int) Math.ceil(effectiveX), (int) Math.ceil(effectiveY));
}
protected Point getArea() {
diff --git a/ui/src/main/java/org/apache/hop/ui/hopgui/shared/SwtGc.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/shared/SwtGc.java
index fed8bd8545..51b5317bc5 100644
--- a/ui/src/main/java/org/apache/hop/ui/hopgui/shared/SwtGc.java
+++ b/ui/src/main/java/org/apache/hop/ui/hopgui/shared/SwtGc.java
@@ -389,7 +389,6 @@ public class SwtGc implements IGc {
transform.dispose();
}
transform = new Transform(gc.getDevice());
- transform.translate(translationX, translationY);
transform.scale(magnification, magnification);
gc.setTransform(transform);
currentMagnification = magnification;
diff --git
a/ui/src/main/resources/org/apache/hop/ui/core/dialog/messages/messages_en_US.properties
b/ui/src/main/resources/org/apache/hop/ui/core/dialog/messages/messages_en_US.properties
index 68b90b923b..d8cdfb2a91 100644
---
a/ui/src/main/resources/org/apache/hop/ui/core/dialog/messages/messages_en_US.properties
+++
b/ui/src/main/resources/org/apache/hop/ui/core/dialog/messages/messages_en_US.properties
@@ -116,6 +116,8 @@
EnterOptionsDialog.DrawBorderAroundCanvasNamesOnCanvas.Label=Draw border around
EnterOptionsDialog.DisableZoomScrolling.Label=Disable zoom scrolling
EnterOptionsDialog.DisableZoomScrolling.ToolTip=When enabled, the mouse scroll
wheel will not zoom in/out on the canvas
EnterOptionsDialog.EnableAutoCollapseCoreObjectTree.Label=Auto collapse
palette tree
+EnterOptionsDialog.EnableInfiniteMove.Label=Enable infinite move
+EnterOptionsDialog.EnableInfiniteMove.ToolTip=When enabled, you can pan the
pipeline and workflow canvas in all directions without being limited by the
origin.
EnterOptionsDialog.FixedWidthFont.Label=Fixed width font:
EnterOptionsDialog.General.Label=General
EnterOptionsDialog.GlobalZoom.Label=UI zoom level: