/* * Copyright (c) 2014, Danno Ferrin * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ package com.shemnon.btc.view; import javafx.beans.property.DoubleProperty; import javafx.beans.property.SimpleDoubleProperty; import javafx.geometry.Bounds; import javafx.scene.Group; import javafx.scene.Node; import javafx.scene.input.MouseEvent; import javafx.scene.input.ScrollEvent; import javafx.scene.input.ZoomEvent; import javafx.scene.layout.Pane; import javafx.scene.transform.Affine; /** * * Created by shemnon on 3 Mar 2014. */ public class ZoomPane extends Pane { double scrollScaleFactor = 1.1; double minZoom = 0.01; // 1:100 double maxZoom = 10.0; // 10:1 Affine transform; Affine workTransform; Node zoomNode; DoubleProperty scale = new SimpleDoubleProperty(1.0); DoubleProperty tx = new SimpleDoubleProperty(0.0); DoubleProperty ty = new SimpleDoubleProperty(0.0); boolean set = false; double lastScale = 1.0; double lastTX = 0; double lastTY = 0; double lastX = 0; double lastY = 0; final Group zoomGroup; public ZoomPane(Node... nodes) { super(); zoomGroup = new Group(nodes); getChildren().addAll(zoomGroup); transform = new Affine(); workTransform = new Affine(); getChildren().get(0).getTransforms().setAll(transform); setOnZoomStarted(e -> start(e.getX(), e.getY())); setOnScrollStarted(e -> start(e.getX(), e.getY())); setOnMousePressed(e -> start(e.getX(), e.getY())); setOnZoomFinished(e -> finish()); setOnMouseReleased(e -> finish()); setOnScrollFinished(e -> finish()); setOnZoom(this::zooming); setOnScroll(this::scrolling); setOnMouseDragged(this::dragging); } void start(double x, double y) { lastScale = scale.get(); lastTX = tx.get(); lastTY = ty.get(); lastX = x; lastY = y; set = true; } void finish() { set = false; } protected void writeToTransform() { transform.setToTransform( scale.get(), 0, tx.get(), 0, scale.get(), ty.get()); } public void zooming(ZoomEvent ze) { zoom(ze.getTotalZoomFactor(), ze.getX(), ze.getY()); } private void zoom(double zoomFactor, double x, double y) { if (!set) start(x, y); double cs = scale.get(); double netZoom = zoomFactor * cs; if (netZoom < minZoom) { zoomFactor = minZoom / cs; } else if (netZoom > maxZoom) { zoomFactor = maxZoom / cs; } workTransform.setMxx(lastScale); workTransform.setMyy(lastScale); workTransform.setTx(lastTX); workTransform.setTy(lastTY); workTransform.prependScale(zoomFactor, zoomFactor, x, y); scale.set(workTransform.getMxx()); tx.set(workTransform.getTx()); ty.set(workTransform.getTy()); writeToTransform(); } public void scrolling(ScrollEvent se) { double clickCount; if (se.getTouchCount() == 0) { finish(); clickCount = Math.signum(se.getDeltaY())*4; } else { if (!set) { start(se.getX(), se.getY()); } if (se.isInertia()) return; // inertia tends to cause crazy zooms clickCount = se.getTotalDeltaY() / se.getMultiplierY(); } zoom(Math.pow(scrollScaleFactor, clickCount), se.getX(), se.getY()); } public void dragging(MouseEvent me) { if (!set) start(me.getX(), me.getY()); if (!Double.isNaN(lastX) && !Double.isNaN(lastY)) { double dx = me.getX() - lastX; double dy = me.getY() - lastY; tx.set(tx.get() + dx); ty.set(ty.get() + dy); } lastX = me.getX(); lastY = me.getY(); writeToTransform(); } public void center() { Bounds gb = zoomGroup.getLayoutBounds(); Bounds tb = getLayoutBounds(); double scale = this.scale.get(); tx.set((tb.getWidth() - gb.getWidth()*scale) / 2); ty.set((tb.getHeight() - gb.getHeight()*scale) / 2 ); writeToTransform(); } public void fit() { Bounds gb = zoomGroup.getLayoutBounds(); Bounds tb = getLayoutBounds(); double scalex = tb.getWidth() / (gb.getWidth() + 10.0/scale.get()); double scaley = tb.getHeight() / (gb.getHeight() + 70.0/scale.get()); double scale = Math.min(1.0, Math.min(scalex, scaley)); tx.set((tb.getWidth() - gb.getWidth() * scale) / 2); ty.set((tb.getHeight() - gb.getHeight()*scale) / 2 + 25); this.scale.set(scale); writeToTransform(); } public void zoomOneToOne() { Bounds tb = getLayoutBounds(); double scale = 1 / this.scale.get(); zoom(scale, tb.getWidth() / 2, tb.getHeight() / 2); } public void centerOnNode(Node n) { if (n == null) return; Bounds b = n.getLayoutBounds(); while (n != null && n != zoomGroup) { b = n.localToParent(b); n = n.getParent(); } if (n == zoomGroup) { Bounds tb = getLayoutBounds(); tx.set((tb.getWidth() - (b.getMinX() + b.getMaxX())*scale.get()) / 2); ty.set((tb.getHeight() - (b.getMinY() + b.getMaxY())*scale.get()) / 2); writeToTransform(); } } }