package nodebox.client;
import nodebox.ui.Platform;
import javax.imageio.ImageIO;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.awt.geom.AffineTransform;
import java.awt.geom.NoninvertibleTransformException;
import java.awt.geom.Point2D;
import java.io.IOException;
public class ZoomableView extends JComponent {
private static Cursor defaultCursor, panCursor;
private final double minZoom, maxZoom;
// View state
private double viewX, viewY, viewScale = 1;
private transient AffineTransform viewTransform = null;
private transient AffineTransform inverseViewTransform = null;
// Interaction state
private boolean isSpacePressed = false;
private boolean isPanning = false;
private Point2D dragStartPoint;
private boolean isZooming = false;
private Point2D zoomStartPoint;
private Point2D zoomEndPoint;
static {
Image panCursorImage;
try {
if (Platform.onWindows())
panCursorImage = ImageIO.read(NetworkView.class.getResourceAsStream("/view-cursor-pan-32.png"));
else
panCursorImage = ImageIO.read(NetworkView.class.getResourceAsStream("/view-cursor-pan.png"));
Toolkit toolkit = Toolkit.getDefaultToolkit();
panCursor = toolkit.createCustomCursor(panCursorImage, new Point(0, 0), "PanCursor");
defaultCursor = Cursor.getDefaultCursor();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public ZoomableView(double minZoom, double maxZoom) {
this.minZoom = minZoom;
this.maxZoom = maxZoom;
setFocusable(true);
addKeyListener(new KeyHandler());
addMouseListener(new MouseHandler());
addMouseMotionListener(new MouseMotionHandler());
addMouseWheelListener(new MouseWheelHandler());
final FocusHandler fh = new FocusHandler();
addFocusListener(fh);
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
SwingUtilities.getWindowAncestor(ZoomableView.this).addWindowFocusListener(fh);
}
});
}
public double getViewX() {
return viewX;
}
public void setViewPosition(double x, double y) {
viewX = x;
viewY = y;
repaint();
}
public double getViewY() {
return viewY;
}
public double getViewScale() {
return viewScale;
}
public boolean isSpacePressed() {
return isSpacePressed;
}
public boolean isDragTrigger(MouseEvent e) {
return isSpacePressed();
}
public boolean isPanning() {
return isPanning;
}
public boolean isZooming() {
return isZooming;
}
private Point2D minPoint(Point2D a, Point2D b) {
return new Point2D.Double(a.getX() - b.getX(), a.getY() - b.getY());
}
public void setViewTransform(double viewX, double viewY, double viewScale) {
this.viewX = viewX;
this.viewY = viewY;
this.viewScale = viewScale;
onViewTransformChanged(viewX, viewY, viewScale);
this.viewTransform = null;
this.inverseViewTransform = null;
repaint();
}
protected void onViewTransformChanged(double viewX, double viewY, double viewScale) {
}
public AffineTransform getViewTransform() {
if (viewTransform == null) {
viewTransform = new AffineTransform();
viewTransform.translate(viewX, viewY);
viewTransform.scale(viewScale, viewScale);
}
return viewTransform;
}
//// View Transform ////
public AffineTransform getInverseViewTransform() {
if (inverseViewTransform == null) {
try {
inverseViewTransform = getViewTransform().createInverse();
} catch (NoninvertibleTransformException e) {
inverseViewTransform = new AffineTransform();
}
}
return inverseViewTransform;
}
public void resetViewTransform() {
setViewTransform(0, 0, 1);
}
public Point2D inverseViewTransformPoint(Point p) {
Point2D pt = new Point2D.Double(p.getX(), p.getY());
return getInverseViewTransform().transform(pt, null);
}
public void zoom(double scaleDelta, double x, double y) {
if (!isVisible()) return;
double currentScale = getViewScale();
double newScale = currentScale * scaleDelta;
if (newScale < minZoom) {
scaleDelta = minZoom / viewScale;
} else if (newScale > maxZoom) {
scaleDelta = maxZoom / viewScale;
}
double vx = viewX - (x - viewX) * (scaleDelta - 1);
double vy = viewY - (y - viewY) * (scaleDelta - 1);
setViewTransform(vx, vy, viewScale * scaleDelta);
}
private class KeyHandler extends KeyAdapter {
@Override
public void keyPressed(KeyEvent e) {
int keyCode = e.getKeyCode();
if (keyCode == KeyEvent.VK_SPACE) {
isSpacePressed = true;
setCursor(panCursor);
}
}
@Override
public void keyReleased(KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_SPACE) {
isSpacePressed = false;
setCursor(defaultCursor);
}
}
}
private class MouseHandler extends MouseAdapter {
@Override
public void mousePressed(MouseEvent e) {
if (e.isPopupTrigger()) return;
// Pan using the middle mouse button.
if (e.getButton() == MouseEvent.BUTTON2) { //middle mouse pan, does same as below
dragStartPoint = e.getPoint();
isPanning = true;
return;
}
// Zoom by pressing middle mouse button, then left mouse button.
if (isPanning && e.getButton() == MouseEvent.BUTTON1) {
// Center point of the zoom, doesn't change.
zoomStartPoint = e.getPoint();
// The distance the mouse will be dragged when zooming.
zoomEndPoint = e.getPoint();
isZooming = true;
return;
}
// If the space bar and mouse is pressed, we're getting ready to pan the view.
if (isSpacePressed) {
// When panning the view use the original mouse point, not the one affected by the view transform.
dragStartPoint = e.getPoint();
isPanning = true;
}
}
@Override
public void mouseReleased(MouseEvent e) {
if (isZooming && e.getButton() == MouseEvent.BUTTON1) {
isZooming = false;
// Reset pan start point when zoom is finished to avoid a jump.
dragStartPoint = e.getPoint();
return;
}
isPanning = false;
}
@Override
public void mouseEntered(MouseEvent e) {
requestFocusInWindow();
}
}
private class MouseMotionHandler extends MouseMotionAdapter {
@Override
public void mouseDragged(MouseEvent e) {
if (isZooming()) {
// How far have we moved?
Point2D offset = minPoint(e.getPoint(), zoomEndPoint);
// Drag mouse left to zoom out, right to zoom in.
zoom(1 + offset.getX() / 200.0, zoomStartPoint.getX(), zoomStartPoint.getY());
// Reset the end point because zoom() takes incremental (delta) values
zoomEndPoint = e.getPoint();
return;
}
if (isPanning()) {
// When panning the view use the original mouse point, not the one affected by the view transform.
Point2D offset = minPoint(e.getPoint(), dragStartPoint);
setViewTransform(viewX + offset.getX(), viewY + offset.getY(), viewScale);
dragStartPoint = e.getPoint();
}
}
}
private class MouseWheelHandler implements MouseWheelListener {
@Override
public void mouseWheelMoved(MouseWheelEvent e) {
double scaleDelta = 1.0 - (e.getWheelRotation() / 10.0);
zoom(scaleDelta, e.getX(), e.getY());
}
}
private class FocusHandler implements WindowFocusListener, FocusListener {
@Override
public void windowLostFocus(WindowEvent e) {
isSpacePressed = false;
isPanning = false;
setCursor(defaultCursor);
}
@Override
public void windowGainedFocus(WindowEvent e) {
}
@Override
public void focusGained(FocusEvent e) {
}
@Override
public void focusLost(FocusEvent e) {
isSpacePressed = false;
setCursor(defaultCursor);
}
}
}