diff --git a/org.fortiss.tooling.common.ui/src/org/fortiss/tooling/common/ui/javafx/lwfxef/.ratings b/org.fortiss.tooling.common.ui/src/org/fortiss/tooling/common/ui/javafx/lwfxef/.ratings index 79b9ab81603cc8ffd1e7878d8acbb734448cc36f..06fc22528dedfb67b78ed83a82ce341b10743e09 100644 --- a/org.fortiss.tooling.common.ui/src/org/fortiss/tooling/common/ui/javafx/lwfxef/.ratings +++ b/org.fortiss.tooling.common.ui/src/org/fortiss/tooling/common/ui/javafx/lwfxef/.ratings @@ -1,12 +1,12 @@ DiagramCoordinate.java 6b00aec99054d4cd19003a72bd4e5e774ac6a641 GREEN -DiagramLayers.java 155cbb47a5f0aaa0025320ae607e6777f3a2d2e8 GREEN -DiagramViewer.java 920bb0f4ee6dd9ac6b607e44c01f04a413b2e2ed GREEN +DiagramLayers.java aa1f95dbae290c8b00202abe4385b01b8f36e5ab GREEN +DiagramViewer.java e5afa84170823a396c2f80864eb752366d34eb92 GREEN DiagramViewerDefaultTags.java 6230763252409c60009ab8887b4ef582cf883229 GREEN -DiagramViewerFeatures.java 397c9600193df18e865f1ff7c829df577c56d383 GREEN +DiagramViewerFeatures.java 3dd78d9c117fc156924a151c6f8d770c53c103bc GREEN DiagramViewerSelection.java e833f592543bc97077907d980a39b123fc4044e6 GREEN EDragGesture.java 5cfa098d3877db11981c2750e5e103156d62fc5e GREEN FeedbackChange.java b088fa89af648f1674f2f9c1f7f99d585ce801ca GREEN -GridCanvasVisual.java 734027d56af342cd01ff445ba9347b8dbb6c83c2 GREEN -MVCBundleManager.java e4892a571fd26eccc5e4e9b2256432721723f542 GREEN +GridCanvasVisual.java e7c83211e0fce2b0d55aac77d0203950286646b9 GREEN +MVCBundleManager.java 18667b4ed98da124b7c1bc7a103e95232df9ad49 GREEN MouseState.java 3d9993f799d5d74bc74ac03b46e4a1857c4d267e GREEN SVGExporter.java cbbd1eceb2910fd5c1693e05c5303a193127b9db GREEN diff --git a/org.fortiss.tooling.common.ui/src/org/fortiss/tooling/common/ui/javafx/lwfxef/DiagramLayers.java b/org.fortiss.tooling.common.ui/src/org/fortiss/tooling/common/ui/javafx/lwfxef/DiagramLayers.java index 155cbb47a5f0aaa0025320ae607e6777f3a2d2e8..aa1f95dbae290c8b00202abe4385b01b8f36e5ab 100644 --- a/org.fortiss.tooling.common.ui/src/org/fortiss/tooling/common/ui/javafx/lwfxef/DiagramLayers.java +++ b/org.fortiss.tooling.common.ui/src/org/fortiss/tooling/common/ui/javafx/lwfxef/DiagramLayers.java @@ -14,12 +14,9 @@ import org.fortiss.tooling.common.ui.javafx.lwfxef.mvc.IMVCBundle; import javafx.collections.ObservableList; import javafx.scene.Group; import javafx.scene.Node; -import javafx.scene.transform.Scale; /** {@link Group} of layers for the {@link DiagramViewer}. */ public final class DiagramLayers extends Group { - /** The diagram viewer of this layer group. */ - private final DiagramViewer viewer; /** The bottom layer. */ private final Layer bottomLayer = new Layer(); /** The content node layer. */ @@ -42,12 +39,9 @@ public final class DiagramLayers extends Group { private final Layer topLayer = new Layer(); /** The current mouse drag state associated with this bundle's visual. */ private final MouseState mouseState; - /** The scale to be applied for zooming the content of the layers. */ - private Scale scale = new Scale(); /** Constructor. */ public DiagramLayers(DiagramViewer viewer) { - this.viewer = viewer; ObservableList<Node> c = getChildren(); c.add(bottomLayer); c.add(contentLayer); @@ -144,25 +138,6 @@ public final class DiagramLayers extends Group { topLayer.clear(); } - /** Registers the scroll listener and the zoom scale transformation. */ - private void registerScrollListenerAndZoomScale(Node node) { - node.setOnScroll(viewer.getScrollingHandler()); - node.getTransforms().add(scale); - } - - /** Unregisters the scroll listener and the zoom scale transformation. */ - private void unregisterScrollListenerAndZoomScale(Node node) { - node.setOnScroll(null); - node.getTransforms().remove(scale); - } - - /** Scales all nodes in each layer. */ - /* package */ void setScale(double factor) { - scale.setX(factor); - scale.setY(factor); - scale.setZ(factor); - } - /** Interface for layers. */ public static interface ILayer { /** @@ -181,6 +156,7 @@ public final class DiagramLayers extends Group { /** Implementation of layers. */ private class Layer extends Group implements ILayer { + /** {@inheritDoc} */ @Override public void add(Node node, IMVCBundle bundle) { @@ -188,7 +164,6 @@ public final class DiagramLayers extends Group { return; } getChildren().add(node); - registerScrollListenerAndZoomScale(node); mouseState.registerMouseListeners(node, bundle); } @@ -199,7 +174,6 @@ public final class DiagramLayers extends Group { return; } mouseState.unregisterMouseListeners(node); - unregisterScrollListenerAndZoomScale(node); getChildren().remove(node); } @@ -208,7 +182,6 @@ public final class DiagramLayers extends Group { public void clear() { for(Node node : getChildren()) { mouseState.unregisterMouseListeners(node); - unregisterScrollListenerAndZoomScale(node); } getChildren().clear(); } diff --git a/org.fortiss.tooling.common.ui/src/org/fortiss/tooling/common/ui/javafx/lwfxef/DiagramViewer.java b/org.fortiss.tooling.common.ui/src/org/fortiss/tooling/common/ui/javafx/lwfxef/DiagramViewer.java index 920bb0f4ee6dd9ac6b607e44c01f04a413b2e2ed..e5afa84170823a396c2f80864eb752366d34eb92 100644 --- a/org.fortiss.tooling.common.ui/src/org/fortiss/tooling/common/ui/javafx/lwfxef/DiagramViewer.java +++ b/org.fortiss.tooling.common.ui/src/org/fortiss/tooling/common/ui/javafx/lwfxef/DiagramViewer.java @@ -10,10 +10,11 @@ package org.fortiss.tooling.common.ui.javafx.lwfxef; import static java.lang.Math.abs; -import static java.lang.Math.ceil; import static java.lang.Math.max; import static java.lang.Math.min; import static java.util.Objects.requireNonNull; +import static javafx.geometry.Orientation.HORIZONTAL; +import static javafx.geometry.Orientation.VERTICAL; import static javafx.geometry.VPos.TOP; import static javafx.scene.layout.GridPane.setHgrow; import static javafx.scene.layout.GridPane.setVgrow; @@ -49,6 +50,7 @@ import org.fortiss.tooling.common.ui.javafx.lwfxef.mvc.MVCBundleTag; import org.fortiss.tooling.common.ui.javafx.lwfxef.visual.IVisualFactory; import javafx.event.EventHandler; +import javafx.geometry.BoundingBox; import javafx.geometry.Bounds; import javafx.geometry.Orientation; import javafx.geometry.Point2D; @@ -61,6 +63,8 @@ import javafx.scene.control.ScrollBar; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.input.KeyEvent; +import javafx.scene.input.MouseButton; +import javafx.scene.input.MouseEvent; import javafx.scene.input.ScrollEvent; import javafx.scene.layout.GridPane; import javafx.scene.layout.Pane; @@ -69,6 +73,9 @@ import javafx.scene.paint.Paint; import javafx.scene.shape.Line; import javafx.scene.shape.Rectangle; import javafx.scene.text.Text; +import javafx.scene.transform.Affine; +import javafx.scene.transform.Transform; +import javafx.scene.transform.Translate; /** * This class represents the diagram viewer node, which manages the grid @@ -108,6 +115,8 @@ public class DiagramViewer { private final Line linkLineFeedback = new Line(); /** The current context menu. */ private ContextMenu contextMenu = null; + /** All scaling and translation of the diagram content is done through this transform. */ + private Affine transform = new Affine(); /** A debug label. */ private Label debugLabel = new Label(""); @@ -138,53 +147,42 @@ public class DiagramViewer { // viewer pane viewerPane = new Pane(); layers.boundsInLocalProperty().addListener((obs, ov, nv) -> { - updateHorizontalScrollbar(); - updateVerticalScrollbar(); + updateScrollBars(); }); + layers.getTransforms().add(transform); + viewerPane.getChildren().add(layers); viewerPane.widthProperty().addListener(evt -> { updateGridCanvasSize(); - updateHorizontalScrollbar(); + updateScrollBars(); }); viewerPane.heightProperty().addListener(evt -> { updateGridCanvasSize(); - updateVerticalScrollbar(); + updateScrollBars(); }); // focus event handling viewerPane.focusedProperty().addListener((obs, ov, nv) -> { viewerManager.handleFocus(nv, getLayers()); }); + viewerPane.addEventFilter(MouseEvent.MOUSE_CLICKED, evt -> { + if(evt.isAltDown()) { + Point2D p = layers.sceneToLocal(evt.getSceneX(), evt.getSceneY()); + scrollToCenter(new DiagramCoordinate(p.getX(), p.getY())); + } + }); + // grid pane for viewer and scroll bars scrolledPane = new GridPane(); scrolledPane.add(viewerPane, 1, 1); setHgrow(viewerPane, ALWAYS); setVgrow(viewerPane, ALWAYS); - // vertical scrollbar - verticalScrollbar = new ScrollBar(); - verticalScrollbar.setOrientation(Orientation.VERTICAL); + verticalScrollbar = createContentScrollBar(VERTICAL); scrolledPane.add(verticalScrollbar, 2, 1); - verticalScrollbar.valueProperty().addListener((obs, ov, nv) -> { - hideContextMenu(); - double zf = features.getCurrentZoomFactor(); - double vszf = features.getVerticalSpacing() * zf; - double deltaY = nv.doubleValue() * vszf; - layers.setTranslateY(-deltaY); - viewerManager.gridCanvasVisual.setScrollY(nv.doubleValue()); - }); - // horizontal scrollbar - horizontalScrollbar = new ScrollBar(); + horizontalScrollbar = createContentScrollBar(HORIZONTAL); scrolledPane.add(horizontalScrollbar, 1, 2); - horizontalScrollbar.valueProperty().addListener((obs, ov, nv) -> { - hideContextMenu(); - double zf = features.getCurrentZoomFactor(); - double hszf = features.getHorizontalSpacing() * zf; - double deltaX = nv.doubleValue() * hszf; - layers.setTranslateX(-deltaX); - viewerManager.gridCanvasVisual.setScrollX(nv.doubleValue()); - }); viewerPane.getChildren().add(debugLabel); debugLabel.setLayoutX(20); @@ -213,6 +211,31 @@ public class DiagramViewer { // update the viewer content viewerManager.updateContentObjects(); + + // add event filters for scrolling and moving viewerPane + viewerPane.addEventFilter(ScrollEvent.SCROLL, getScrollingFilter()); + ContentDragController contentDragController = new ContentDragController(); + contentDragController.registerEventFilters(viewerPane); + } + + /** Constructs a scrollbar controlling the content */ + private ScrollBar createContentScrollBar(Orientation orientation) { + ScrollBar scrollbar = new ScrollBar(); + scrollbar.setOrientation(orientation); + scrollbar.valueProperty().addListener((obs, ov, nv) -> { + hideContextMenu(); + Point2D diagramTranslation = layers.parentToLocal(0, 0); + switch(orientation) { + case HORIZONTAL: + transform.appendTranslation(-nv.doubleValue() + diagramTranslation.getX(), 0); + break; + case VERTICAL: + transform.appendTranslation(0, -nv.doubleValue() + diagramTranslation.getY()); + break; + } + viewerManager.gridCanvasVisual.updateNodes(layers); + }); + return scrollbar; } /** Returns viewerPane. */ @@ -242,19 +265,7 @@ public class DiagramViewer { contextMenu = new ContextMenu(); contextMenu.getItems().addAll(items); contextMenu.setAutoHide(true); - Point2D locationOnNode = diagramLocation.getLocal(node); - Point2D screenLocation; - double zf = features.getCurrentZoomFactor(); - if(viewerManager.gridCanvasVisual.isGridCanvas(node)) { - double hsd = features.getHorizontalSpacing() * getHorizontalScrollBarValue() * zf; - double vsd = features.getVerticalSpacing() * getVerticalScrollBarValue() * zf; - Point2D correction = locationOnNode.multiply(zf).subtract(hsd, vsd); - screenLocation = node.localToScreen(correction); - } else { - Bounds b = node.getBoundsInLocal(); - screenLocation = node.localToScreen(locationOnNode.getX() + b.getMinX(), - locationOnNode.getY() + b.getMinY()); - } + Point2D screenLocation = layers.localToScreen(diagramLocation); contextMenu.show(node, screenLocation.getX(), screenLocation.getY()); } @@ -278,45 +289,44 @@ public class DiagramViewer { return exporter.export(); } - /** Updates the vertical scrollbar. */ - private void updateVerticalScrollbar() { - Bounds vpb = viewerPane.getLayoutBounds(); - Bounds cb = layers.getLayoutBounds(); - double vs = features.getVerticalSpacing(); - double contentHeight = max(ceil(cb.getMaxY() / vs), 2); - double viewportHeight = ceil(vpb.getHeight() / vs); - double zf = features.getCurrentZoomFactor(); - // this allows scrolling the content nearly out of sight (two grid spaces) - double max = contentHeight - 2 * zf; - verticalScrollbar.setMax(max); - double visi = max * (viewportHeight / (viewportHeight + contentHeight - 2)); - verticalScrollbar.setVisibleAmount(visi); - verticalScrollbar.setUnitIncrement(1); - verticalScrollbar.setBlockIncrement(5); - if(verticalScrollbar.getValue() > max) { - verticalScrollbar.setValue(max); - } - } + /** Updates the scrollbars. */ + private void updateScrollBars() { + // The size of the content (in diagram coordinates) + Bounds contentBounds = layers.getBoundsInLocal(); + double contentHeight = contentBounds.getHeight(); + double contentWidth = contentBounds.getWidth(); + // The size of the visible content (in diagram coordinates) + Bounds visibleBounds = layers.parentToLocal(viewerPane.getLayoutBounds()); + double visibleWidth = visibleBounds.getWidth(); + double visibleHeight = visibleBounds.getHeight(); + + verticalScrollbar.setMin(0); + horizontalScrollbar.setMin(0); + verticalScrollbar.setMax(contentHeight); + horizontalScrollbar.setMax(contentWidth); + + Point2D diagramTopLeft = layers.parentToLocal(0, 0); + verticalScrollbar.setValue(diagramTopLeft.getY()); + horizontalScrollbar.setValue(diagramTopLeft.getX()); + + // Including blank space to the bottom, the height of the content + // is (contentHeight + visibleHeight), of which visibleHeight is + // visible. Since the vertical scrollbar goes from 0 to contentHeight, + // this means we need: + // contentHeight / verticalVisibleAmount + // == (contentHeight + visibleHeight) / visibleHeight + // Hence: + double verticalVisibleAmount = + contentHeight * visibleHeight / (contentHeight + visibleHeight); + double horizontalVisibleAmount = + contentWidth * visibleWidth / (contentWidth + visibleWidth); + verticalScrollbar.setVisibleAmount(verticalVisibleAmount); + horizontalScrollbar.setVisibleAmount(horizontalVisibleAmount); - /** Updates the horizontal scrollbar. */ - private void updateHorizontalScrollbar() { - Bounds vpb = viewerPane.getLayoutBounds(); - Bounds cb = layers.getLayoutBounds(); - double hs = features.getHorizontalSpacing(); - double contentWidth = max(ceil(cb.getMaxX() / hs), 2); - double viewportWidth = ceil(vpb.getWidth() / hs); - double zf = features.getCurrentZoomFactor(); - - // this allows scrolling the content nearly out of sight (two grid spaces) - double max = contentWidth - 2 * zf; - horizontalScrollbar.setMax(max); - double visi = max * (viewportWidth / (viewportWidth + contentWidth - 2)); - horizontalScrollbar.setVisibleAmount(visi); + verticalScrollbar.setUnitIncrement(1); horizontalScrollbar.setUnitIncrement(1); - horizontalScrollbar.setBlockIncrement(5); - if(horizontalScrollbar.getValue() > max) { - horizontalScrollbar.setValue(max); - } + verticalScrollbar.setBlockIncrement(verticalScrollbar.getVisibleAmount()); + horizontalScrollbar.setBlockIncrement(horizontalScrollbar.getVisibleAmount()); } /** Update the size of the grid canvas. */ @@ -429,33 +439,71 @@ public class DiagramViewer { return viewerManager.modelFactory; } + /** Clamp content so that it can't be moved beyond the borders. */ + private void enforceBounds() { + // The size of the content (in diagram coordinates) + Bounds contentBounds = layers.getBoundsInLocal(); + // Crop at the top left corner + BoundingBox enforceContentBounds = + new BoundingBox(0, 0, contentBounds.getMaxX(), contentBounds.getMaxY()); + Bounds contentInParent = layers.localToParent(enforceContentBounds); + + double leftBoundCorrection = max(contentInParent.getMinX(), 0); + double topBoundCorrection = max(contentInParent.getMinY(), 0); + double rightBoundCorrection = min(contentInParent.getMaxX(), 0); + double bottomBoundCorrection = min(contentInParent.getMaxY(), 0); + Point2D clampedInParent = new Point2D(leftBoundCorrection + rightBoundCorrection, + topBoundCorrection + bottomBoundCorrection); + Point2D correctionLocal = layers.parentToLocal(clampedInParent); + Point2D originLocal = layers.parentToLocal(0, 0); + transform.appendTranslation(originLocal.getX() - correctionLocal.getX(), + originLocal.getY() - correctionLocal.getY()); + } + + /** Apply a transformation to the content. */ + void appendContentTransform(Transform t) { + transform.append(t); + enforceBounds(); + updateScrollBars(); + viewerManager.gridCanvasVisual.updateNodes(layers); + } + /** Returns the scroll event handler. */ - /* package */ EventHandler<? super ScrollEvent> getScrollingHandler() { + private EventHandler<? super ScrollEvent> getScrollingFilter() { return evt -> { + evt.consume(); if(evt.isControlDown()) { if(features.getZoomFactorIndex() == -1) { - evt.consume(); return; } if(evt.getDeltaY() > 0) { - features.zoomIn(); - evt.consume(); + Point2D pivot = layers.sceneToLocal(evt.getSceneX(), evt.getSceneY()); + features.zoomIn(pivot.getX(), pivot.getY()); } else if(evt.getDeltaY() < 0) { - features.zoomOut(); - evt.consume(); + Point2D pivot = layers.sceneToLocal(evt.getSceneX(), evt.getSceneY()); + features.zoomOut(pivot.getX(), pivot.getY()); } } else if(evt.isShiftDown()) { + Point2D p0 = layers.screenToLocal(0, 0); + Point2D p1 = layers.screenToLocal(1, 1); double hmax = horizontalScrollbar.getMax(); double hmin = horizontalScrollbar.getMin(); double nval = horizontalScrollbar.getValue() - - evt.getDeltaY() / features.getHorizontalSpacing(); + evt.getDeltaY() * abs(p1.getX() - p0.getX()); horizontalScrollbar.setValue(max(hmin, min(hmax, nval))); } else { + Point2D p0 = layers.screenToLocal(0, 0); + Point2D p1 = layers.screenToLocal(1, 1); double vmax = verticalScrollbar.getMax(); double vmin = verticalScrollbar.getMin(); - double nval = verticalScrollbar.getValue() - - evt.getDeltaY() / features.getVerticalSpacing(); - verticalScrollbar.setValue(max(vmin, min(vmax, nval))); + double vval = + verticalScrollbar.getValue() - evt.getDeltaY() * abs(p1.getY() - p0.getY()); + double hmax = horizontalScrollbar.getMax(); + double hmin = horizontalScrollbar.getMin(); + double hval = horizontalScrollbar.getValue() - + evt.getDeltaX() * abs(p1.getX() - p0.getX()); + verticalScrollbar.setValue(max(vmin, min(vmax, vval))); + horizontalScrollbar.setValue(max(hmin, min(hmax, hval))); } }; } @@ -486,11 +534,7 @@ public class DiagramViewer { * using the underlying model. */ public void updateFromModel() { - double vscroll = verticalScrollbar.getValue(); - double hscroll = horizontalScrollbar.getValue(); viewerManager.updateContentObjects(); - verticalScrollbar.setValue(vscroll); - horizontalScrollbar.setValue(hscroll); } /** @@ -533,9 +577,7 @@ public class DiagramViewer { setSingleSelectedMVCBundle(null); } - /** - * Converts the given absolute diagram coordinates to node-local coordinates. - */ + /** Converts the given absolute diagram coordinates to node-local coordinates. */ /* package */ DiagramCoordinate convertEventToDiagramCoordinates(Point2D absoluteLocation, Node node) { return convertEventToDiagramCoordinates(absoluteLocation.getX(), absoluteLocation.getY(), @@ -545,37 +587,25 @@ public class DiagramViewer { /** Converts the given mouse event coordinates to node-local coordinates. */ /* package */ DiagramCoordinate convertEventToDiagramCoordinates(double evtX, double evtY, Node node) { - if(node == viewerManager.gridCanvasVisual.getGridCanvas()) { - return convertGridCanvasCoordinate(evtX, evtY); - } - return new DiagramCoordinate(evtX, evtY); + Point2D scene = node.localToScene(evtX, evtY); + Point2D diagram = layers.sceneToLocal(scene); + return new DiagramCoordinate(diagram.getX(), diagram.getY()); } - /** Converts the grid canvas coordinate to the diagram coordinate space. */ - public DiagramCoordinate convertGridCanvasCoordinate(double xOnCanvas, double yOnCanvas) { - double zf = features.getCurrentZoomFactor(); - double x = xOnCanvas / zf + features.getHorizontalSpacing() * getHorizontalScrollBarValue(); - double y = yOnCanvas / zf + features.getVerticalSpacing() * getVerticalScrollBarValue(); - return new DiagramCoordinate(x, y); + /** By default, zoom fixes the top left corner */ + /* package */ DiagramCoordinate getDefaultZoomPivot() { + Point2D pivot = layers.parentToLocal(0, 0); + return new DiagramCoordinate(pivot.getX(), pivot.getY()); } /** Scrolls the diagram to center at the given coordinates. */ public void scrollToCenter(DiagramCoordinate center) { - double zf = features.getCurrentZoomFactor(); - Bounds bounds = viewerPane.getLayoutBounds(); - double upperLeftX = center.getX() - bounds.getMinX() / zf; - if(upperLeftX < 0) { - upperLeftX = 0; - } - double newHSBValue = upperLeftX / features.getHorizontalSpacing(); - double upperLeftY = center.getY() - bounds.getMinY() / zf; - if(upperLeftY < 0) { - upperLeftY = 0; - } - double newVSBValue = upperLeftY - features.getVerticalSpacing(); - - horizontalScrollbar.setValue(newHSBValue); - verticalScrollbar.setValue(newVSBValue); + Point2D centerScene = + viewerPane.localToScene(viewerPane.getWidth() / 2, viewerPane.getHeight() / 2); + Point2D currentCenter = layers.sceneToLocal(centerScene); + double dx = currentCenter.getX() - center.getX(); + double dy = currentCenter.getY() - center.getY(); + appendContentTransform(new Translate(dx, dy)); } /** Updates the feedback for selection rectangle. */ @@ -622,10 +652,7 @@ public class DiagramViewer { } } - /** - * Starts the feedback of the link creation line and shows the possible target - * feedbacks. - */ + /** Starts the feedback of the link creation line and shows the possible target feedbacks. */ public void startNewLinkLineFeedback(IMVCBundle linkStartBundle, Node node, Point2D locationInDiagram) { if(node == null || locationInDiagram == null) { @@ -662,7 +689,7 @@ public class DiagramViewer { clearLinkTargetFeedback(); } - /** Clears the link target feedback fronm all possible link targets. */ + /** Clears the link target feedback from all possible link targets. */ private void clearLinkTargetFeedback() { for(IMVCBundle possibleTarget : viewerManager.possibleLinkTargets) { possibleTarget.removeTag(LINK_TARGET_ALLOWED_TAG); @@ -857,4 +884,64 @@ public class DiagramViewer { return null; } } + + /** Mouse controller for moving the content using control-drag. */ + private final class ContentDragController { + /** Last point within a control-drag gesture. */ + private Point2D dragPoint; + /** + * Has the mouse button been released within a control-drag gesture? If so, the next click + * event should be filtered out. + */ + private boolean buttonReleased; + + /** Register the event filters of this controller. */ + /* package */ void registerEventFilters(Node node) { + node.addEventFilter(MouseEvent.MOUSE_DRAGGED, dragDetected); + node.addEventFilter(MouseEvent.ANY, dragged); + node.addEventFilter(MouseEvent.MOUSE_RELEASED, mouseReleased); + node.addEventFilter(MouseEvent.MOUSE_CLICKED, mouseClicked); + } + + /** Control-drag gesture starts when drag is detected. */ + private final EventHandler<MouseEvent> dragDetected = evt -> { + if(dragPoint == null && evt.isControlDown() && evt.getButton() == MouseButton.PRIMARY) { + dragPoint = getLayers().sceneToLocal(evt.getSceneX(), evt.getSceneY()); + buttonReleased = false; + evt.consume(); + } + }; + + /** While dragging, the content is translated appropriately. */ + private final EventHandler<MouseEvent> dragged = evt -> { + if(dragPoint != null) { + evt.consume(); + + Point2D evtLayer = getLayers().sceneToLocal(evt.getSceneX(), evt.getSceneY()); + + Point2D move = evtLayer.subtract(dragPoint); + double dx = move.getX(); + double dy = move.getY(); + appendContentTransform(new Translate(dx, dy)); + dragPoint = getLayers().sceneToLocal(evt.getSceneX(), evt.getSceneY()); + } + }; + + /** If the mouse button is released, the gesture should be stopped. */ + private final EventHandler<MouseEvent> mouseReleased = evt -> { + if(dragPoint != null && evt.getButton() == MouseButton.PRIMARY) { + dragPoint = null; + buttonReleased = true; + evt.consume(); + } + }; + + /** The first click event after the released event should be filtered. */ + private final EventHandler<MouseEvent> mouseClicked = evt -> { + if(buttonReleased && evt.getButton() == MouseButton.PRIMARY) { + evt.consume(); + } + buttonReleased = false; + }; + } } diff --git a/org.fortiss.tooling.common.ui/src/org/fortiss/tooling/common/ui/javafx/lwfxef/DiagramViewerFeatures.java b/org.fortiss.tooling.common.ui/src/org/fortiss/tooling/common/ui/javafx/lwfxef/DiagramViewerFeatures.java index 397c9600193df18e865f1ff7c829df577c56d383..3dd78d9c117fc156924a151c6f8d770c53c103bc 100644 --- a/org.fortiss.tooling.common.ui/src/org/fortiss/tooling/common/ui/javafx/lwfxef/DiagramViewerFeatures.java +++ b/org.fortiss.tooling.common.ui/src/org/fortiss/tooling/common/ui/javafx/lwfxef/DiagramViewerFeatures.java @@ -14,9 +14,12 @@ import static java.lang.Math.max; import static java.util.Objects.requireNonNull; import static javafx.scene.paint.Color.LIGHTGRAY; +import java.util.Objects; + import javafx.geometry.Dimension2D; import javafx.geometry.Point2D; import javafx.scene.paint.Color; +import javafx.scene.transform.Scale; /** * This class handles the state of all features of the {@link DiagramViewer}, which can be toggled @@ -25,10 +28,14 @@ import javafx.scene.paint.Color; public final class DiagramViewerFeatures { /** The diagram viewer. */ private final DiagramViewer viewer; - /** The zoom factors. */ - private double[] zoomFactors = new double[] {0.125, 0.25, 0.5, 0.75, 1, 1.5, 2, 4, 8}; + /** + * The zoom factors. The values are chosen to have a roughly constant factor between them, to + * make interactive scrolling less jumpy. + */ + private double[] zoomFactors = new double[] {0.125, 0.25, 0.3, 0.4, 0.5, 0.65, 0.80, 1, 1.25, + 1.5, 2.0, 2.5, 3.5, 4.5, 6}; /** The current zoom factor array index. */ - private int zoomFactorIndex = 4; // Index of 1 + private int zoomFactorIndex = 7; // Index of 1 /** Flag indicating whether link highlighting is enabled. */ private boolean linkHighlightingEnabled = false; /** Flag if interaction area shading is active. */ @@ -81,23 +88,40 @@ public final class DiagramViewerFeatures { /** Sets the zoom factor index. */ public void setZoomFactorIndex(int zoomFactorIndex) { + Objects.checkIndex(zoomFactorIndex, zoomFactors.length); + double factor = zoomFactors[zoomFactorIndex] / zoomFactors[this.zoomFactorIndex]; this.zoomFactorIndex = zoomFactorIndex; - viewer.updateAllVisuals(); + DiagramCoordinate pivot = viewer.getDefaultZoomPivot(); + viewer.appendContentTransform(new Scale(factor, factor, pivot.getX(), pivot.getY())); } /** Increases zoom factor index by one up to the maximum. */ public void zoomIn() { + DiagramCoordinate pivot = viewer.getDefaultZoomPivot(); + zoomIn(pivot.getX(), pivot.getY()); + } + + /** Increases zoom factor index by one up to the maximum. */ + public void zoomIn(double pivotX, double pivotY) { if(zoomFactorIndex < zoomFactors.length - 1) { zoomFactorIndex++; - viewer.updateAllVisuals(); + double factor = zoomFactors[zoomFactorIndex] / zoomFactors[zoomFactorIndex - 1]; + viewer.appendContentTransform(new Scale(factor, factor, pivotX, pivotY)); } } /** Decreases zoom factor index by one down to the minimum. */ public void zoomOut() { + DiagramCoordinate pivot = viewer.getDefaultZoomPivot(); + zoomOut(pivot.getX(), pivot.getY()); + } + + /** Decreases zoom factor index by one down to the minimum. */ + public void zoomOut(double pivotX, double pivotY) { if(zoomFactorIndex > 0) { zoomFactorIndex--; - viewer.updateAllVisuals(); + double factor = zoomFactors[zoomFactorIndex] / zoomFactors[zoomFactorIndex + 1]; + viewer.appendContentTransform(new Scale(factor, factor, pivotX, pivotY)); } } diff --git a/org.fortiss.tooling.common.ui/src/org/fortiss/tooling/common/ui/javafx/lwfxef/GridCanvasVisual.java b/org.fortiss.tooling.common.ui/src/org/fortiss/tooling/common/ui/javafx/lwfxef/GridCanvasVisual.java index 734027d56af342cd01ff445ba9347b8dbb6c83c2..e7c83211e0fce2b0d55aac77d0203950286646b9 100644 --- a/org.fortiss.tooling.common.ui/src/org/fortiss/tooling/common/ui/javafx/lwfxef/GridCanvasVisual.java +++ b/org.fortiss.tooling.common.ui/src/org/fortiss/tooling/common/ui/javafx/lwfxef/GridCanvasVisual.java @@ -9,6 +9,10 @@ *******************************************************************************/ package org.fortiss.tooling.common.ui.javafx.lwfxef; +import static java.lang.Double.doubleToLongBits; + +import java.util.Objects; + import org.fortiss.tooling.common.ui.javafx.lwfxef.DiagramViewerFeatures.IndicatorType; import org.fortiss.tooling.common.ui.javafx.lwfxef.mvc.IMVCBundle; import org.fortiss.tooling.common.ui.javafx.lwfxef.visual.IVisual; @@ -19,7 +23,9 @@ import javafx.geometry.Rectangle2D; import javafx.scene.Node; import javafx.scene.canvas.Canvas; import javafx.scene.canvas.GraphicsContext; +import javafx.scene.layout.Pane; import javafx.scene.paint.Color; +import javafx.scene.shape.Rectangle; /** * {@link IVisual} for the diagram background. Note that the {@link Canvas} used here is always as @@ -30,27 +36,46 @@ import javafx.scene.paint.Color; final class GridCanvasVisual implements IVisual { /** The diagram bundle. */ private final DiagramViewer viewer; - /** The FX canvas to be drawn on. */ + + /** The pane to contain the whole grid. */ + private final Pane pane; + + /** + * The FX canvas to draw the grid. The canvas is one grid column/row larger than the pane. It + * gets moved on the pane when the position of the grid changes. In this way, the canvas only + * needs to be redrawn when scale or grid configuration change. Moving the final image is much + * faster than redrawing the canvas. + */ private final Canvas gridCanvas; - /** The displacement caused by scroll bars. */ - private DiagramCoordinate displacement = new DiagramCoordinate(0, 0); + + /** + * A rectangle for the left outer border. The border is not painted with the canvas to avoid + * having to redraw the canvas when changing location. + */ + private final Rectangle leftOuterBorder; + + /** A rectangle for the top outer border. */ + private final Rectangle topOuterBorder; + + /** Width of the whole shown grid. */ + double width; + + /** Height of the whole shown grid. */ + double height; /** Constructor. */ public GridCanvasVisual(DiagramViewer viewer) { this.viewer = viewer; - // the background grid canvas - this.gridCanvas = new Canvas(100, 100); - gridCanvas.widthProperty().addListener(evt -> { - paintGrid(); - }); - gridCanvas.heightProperty().addListener(evt -> { - paintGrid(); - }); + this.pane = new Pane(); + this.gridCanvas = new Canvas(); + this.leftOuterBorder = new Rectangle(); + this.topOuterBorder = new Rectangle(); + this.pane.getChildren().addAll(gridCanvas, leftOuterBorder, topOuterBorder); } /** Returns gridCanvas. */ - /* package */Canvas getGridCanvas() { - return gridCanvas; + /* package */Node getGridNode() { + return pane; } /** {@inheritDoc} */ @@ -74,27 +99,25 @@ final class GridCanvasVisual implements IVisual { /** {@inheritDoc} */ @Override public void updateNodes(DiagramLayers layers) { - if(gridCanvas.getParent() == null) { - // grid canvas is added below the diagram layers - viewer.getViewerPane().getChildren().add(0, gridCanvas); - viewer.getLayers().getMouseState().registerMouseListeners(gridCanvas, getMVCBundle()); - gridCanvas.setOnScroll(viewer.getScrollingHandler()); + if(pane.getParent() == null) { + // grid is added below the diagram layers + viewer.getViewerPane().getChildren().add(0, pane); + viewer.getLayers().getMouseState().registerMouseListeners(pane, getMVCBundle()); } - paintGrid(); + updateContent(); } /** {@inheritDoc} */ @Override public void removeAllVisuals(DiagramLayers layers) { - viewer.getLayers().getMouseState().unregisterMouseListeners(gridCanvas); - viewer.getViewerPane().getChildren().remove(gridCanvas); - gridCanvas.setOnScroll(null); + viewer.getLayers().getMouseState().unregisterMouseListeners(pane); + viewer.getViewerPane().getChildren().remove(pane); } /** {@inheritDoc} */ @Override public Rectangle2D getModelBounds() { - Bounds b = gridCanvas.getBoundsInParent(); + Bounds b = pane.getBoundsInParent(); return new Rectangle2D(b.getMinX(), b.getMinY(), b.getWidth(), b.getHeight()); } @@ -115,49 +138,189 @@ final class GridCanvasVisual implements IVisual { /** {@inheritDoc} */ @Override public void requestFocus() { - gridCanvas.requestFocus(); + pane.requestFocus(); } - /** Paints the grid. */ - public void paintGrid() { + /** Returns the zoom factor from current content transformation. */ + private double getZoomFactor() { + Point2D unitVector = new Point2D(1, 1); + Point2D transformedVector = + viewer.getLayers().getLocalToParentTransform().deltaTransform(unitVector); + return transformedVector.getX(); + } + + /** Structure to represent the current location of the grid. */ + private static class GridOffset { + /** Grid x coordinate of upper left corner. */ + int gridX; + + /** Grid y coordinate of upper left corner. */ + int gridY; + + /** Insets in the grid square between (gridX, gridY) and (gridX + 1, gridY +1). */ + Point2D insets; + } + + /** Compute the grid offset current content transformation. */ + private GridOffset getGridOffset() { DiagramViewerFeatures features = viewer.getFeatures(); - double zf = features.getCurrentZoomFactor(); - double width = gridCanvas.getWidth(); - double height = gridCanvas.getHeight(); - GraphicsContext gc = gridCanvas.getGraphicsContext2D(); - // background - Color backgroundColor = features.getBackgroundColor(); - gc.setFill(backgroundColor); - gc.setStroke(backgroundColor); - gc.fillRect(0, 0, width, height); - // outer border and indicators - Color indicatorColor = features.getIndicatorColor(); - gc.setStroke(indicatorColor); - gc.setFill(indicatorColor); - double hSpacing = features.getHorizontalSpacing(); - double vSpacing = features.getVerticalSpacing(); - double dx = displacement.getX() % 1.0; - double dy = displacement.getY() % 1.0; - double zfhs = zf * hSpacing; - double zfvs = zf * vSpacing; - if(features.isDrawOuterBorder()) { - if(displacement.getX() < 1.0) { - gc.fillRect(0, 0, zfhs * (1.0 - dx), height); + Point2D p0 = viewer.getLayers().sceneToLocal(pane.localToScene(0, 0)); + + GridOffset gridOffset = new GridOffset(); + gridOffset.gridX = (int)(p0.getX() / features.getHorizontalSpacing()); + gridOffset.gridY = (int)(p0.getY() / features.getVerticalSpacing()); + gridOffset.insets = new Point2D((p0.getX() / features.getHorizontalSpacing()) % 1.0, + (p0.getY() / features.getVerticalSpacing()) % 1.0); + return gridOffset; + } + + /** + * Structure representing the parameters that go into the drawing of the grid canvas. It is + * useful to determine if the canvas needs repainting. + */ + private static class GridParameters { + + /** Horizontal grid spacing. */ + private double hSpacing; + + /** Vertical grid spacing. */ + private double vSpacing; + + /** Zoom factor. */ + private double zoomFactor; + + /** Grid indicator size. */ + private double indicatorSize; + + /** Grid indicator type. */ + private IndicatorType indicatorType; + + /** Grid background color. */ + private Color backgroundColor; + + /** Grid indicator color. */ + private Color indicatorColor; + + /** {@inheritDoc} */ + @Override + public int hashCode() { + return Objects.hash(backgroundColor, hSpacing, indicatorColor, indicatorSize, + indicatorType, vSpacing, zoomFactor); + } + + /** + * Auto-generated (by Eclipse) deep equality method. + * + * {@inheritDoc} + */ + @Override + public boolean equals(Object obj) { + if(this == obj) { + return true; + } + if(obj == null) { + return false; } - if(displacement.getY() < 1.0) { - gc.fillRect(0, 0, width, zfvs * (1.0 - dy)); + if(getClass() != obj.getClass()) { + return false; } + GridParameters other = (GridParameters)obj; + + // Compare all member variables individually for equality. + // The equality of doubles is tested using {@code doubleToLongBits}, + // as explained in {@link java.lang.Double#equals(Object)}. + return Objects.equals(backgroundColor, other.backgroundColor) && + doubleToLongBits(hSpacing) == doubleToLongBits(other.hSpacing) && + Objects.equals(indicatorColor, other.indicatorColor) && + doubleToLongBits(indicatorSize) == doubleToLongBits(other.indicatorSize) && + indicatorType == other.indicatorType && + doubleToLongBits(vSpacing) == doubleToLongBits(other.vSpacing) && + doubleToLongBits(zoomFactor) == doubleToLongBits(other.zoomFactor); } + } + + /** Update the pane node. */ + private void updateContent() { + DiagramViewerFeatures features = viewer.getFeatures(); + + GridParameters parameters = new GridParameters(); + parameters.hSpacing = features.getHorizontalSpacing(); + parameters.vSpacing = features.getVerticalSpacing(); + parameters.zoomFactor = getZoomFactor(); + parameters.indicatorType = features.getIndicatorType(); + parameters.backgroundColor = features.getBackgroundColor(); + parameters.indicatorColor = features.getIndicatorColor(); + parameters.indicatorSize = features.getIndicatorSize(); + + paintCanvas(parameters); + + GridOffset gridOffset = getGridOffset(); + double zfhs = parameters.zoomFactor * parameters.hSpacing; + double zfvs = parameters.zoomFactor * parameters.vSpacing; + gridCanvas.relocate(-gridOffset.insets.getX() * zfhs, -gridOffset.insets.getY() * zfvs); + + pane.getChildren().removeAll(leftOuterBorder, topOuterBorder); + if(features.isDrawOuterBorder()) { + pane.getChildren().addAll(leftOuterBorder, topOuterBorder); + leftOuterBorder.relocate(-(gridOffset.gridX + gridOffset.insets.getX()) * zfhs, 0); + leftOuterBorder.setWidth(zfhs); + leftOuterBorder.setHeight(height); + leftOuterBorder.setStrokeWidth(0.0); + leftOuterBorder.setFill(features.getIndicatorColor()); + + topOuterBorder.relocate(0, -(gridOffset.gridY + gridOffset.insets.getY()) * zfhs); + topOuterBorder.setWidth(width); + topOuterBorder.setHeight(zfvs); + topOuterBorder.setStrokeWidth(0.0); + topOuterBorder.setFill(features.getIndicatorColor()); + } + } + + /** The parameters of what is currently drawn on the gridCanvas. */ + private GridParameters currentGridCanvasParameters = null; + + /** Draw gridCanvas with the given parameters. */ + private void paintCanvas(GridParameters parameters) { + double targetCanvasWidth = width + parameters.zoomFactor * parameters.hSpacing; + double targetCanvasHeight = height + parameters.zoomFactor * parameters.vSpacing; + + boolean resized = false; + resized |= gridCanvas.getWidth() != targetCanvasWidth; + resized |= gridCanvas.getHeight() != targetCanvasHeight; + + if(resized) { + gridCanvas.setWidth(targetCanvasWidth); + gridCanvas.setHeight(targetCanvasHeight); + } + + if(!resized && parameters.equals(currentGridCanvasParameters)) { + // grid is already drawn on canvas + return; + } + currentGridCanvasParameters = parameters; + + GraphicsContext gc = gridCanvas.getGraphicsContext2D(); + // background + gc.setFill(parameters.backgroundColor); + gc.setStroke(parameters.backgroundColor); + gc.fillRect(0, 0, gridCanvas.getWidth(), gridCanvas.getHeight()); + // abort if no indicators should be drawn - IndicatorType indicatorType = features.getIndicatorType(); + IndicatorType indicatorType = parameters.indicatorType; if(indicatorType == IndicatorType.INVISIBLE) { return; } + // draw grid indicators - double isize = viewer.getFeatures().getIndicatorSize() * zf; - for(double x = zfhs * (1.0 - dx); x < width; x += zfhs) { - for(double y = zfvs * (1.0 - dy); y < height; y += zfvs) { - drawIndicator(gc, x, y, isize, indicatorType); + gc.setStroke(parameters.indicatorColor); + gc.setFill(parameters.indicatorColor); + double zfhs = parameters.zoomFactor * parameters.hSpacing; + double zfvs = parameters.zoomFactor * parameters.vSpacing; + double isize = parameters.indicatorSize * parameters.zoomFactor; + + for(double x = zfhs; x < gridCanvas.getWidth(); x += zfhs) { + for(double y = zfvs; y < gridCanvas.getHeight(); y += zfvs) { + drawIndicator(gc, Math.floor(x) + 0.5, Math.floor(y) + 0.5, isize, indicatorType); } } } @@ -199,41 +362,21 @@ final class GridCanvasVisual implements IVisual { /** {@inheritDoc} */ @Override public EDragGesture getDragGesture(Node node, DiagramCoordinate diagramLocation) { - if(node == gridCanvas) { + if(node == pane) { return EDragGesture.SELECTION; } return EDragGesture.NONE; } - /** Sets horizontal scroll displacement. */ - public void setScrollX(double canvasDeltaX) { - displacement = new DiagramCoordinate(canvasDeltaX, displacement.getY()); - paintGrid(); - } - - /** Sets vertical scroll displacement. */ - public void setScrollY(double canvasDeltaY) { - displacement = new DiagramCoordinate(displacement.getX(), canvasDeltaY); - paintGrid(); - } - /** Sets the width of the grid canvas. */ public void setWidth(double w) { - gridCanvas.setWidth(w); + this.width = w; + updateContent(); } /** Sets the height of the grid canvas. */ public void setHeight(double h) { - gridCanvas.setHeight(h); - } - - /** Relocates the given scene point to local coordinates. */ - public Point2D sceneToLocal(Point2D locationInScene) { - return gridCanvas.sceneToLocal(locationInScene); - } - - /** Returns whether the given node is the grid canvas. */ - public boolean isGridCanvas(Node source) { - return source == gridCanvas; + this.height = h; + updateContent(); } } diff --git a/org.fortiss.tooling.common.ui/src/org/fortiss/tooling/common/ui/javafx/lwfxef/MVCBundleManager.java b/org.fortiss.tooling.common.ui/src/org/fortiss/tooling/common/ui/javafx/lwfxef/MVCBundleManager.java index e4892a571fd26eccc5e4e9b2256432721723f542..18667b4ed98da124b7c1bc7a103e95232df9ad49 100644 --- a/org.fortiss.tooling.common.ui/src/org/fortiss/tooling/common/ui/javafx/lwfxef/MVCBundleManager.java +++ b/org.fortiss.tooling.common.ui/src/org/fortiss/tooling/common/ui/javafx/lwfxef/MVCBundleManager.java @@ -502,7 +502,6 @@ import org.fortiss.tooling.common.ui.javafx.lwfxef.visual.IVisualFactory; /** Updates every visual (and only its appearance, not the structure). */ /* package */ void updateAllVisuals() { - layers.setScale(viewer.getFeatures().getCurrentZoomFactor()); viewer.updateGridCanvasSize(); gridCanvasVisual.updateNodes(layers); for(IContentMVCBundle b : contentBundles.values()) {