/*
* @(#)ZoomDrawingView.java
*
* Project: JHotdraw - a GUI framework for technical drawings
* http://www.jhotdraw.org
* http://jhotdraw.sourceforge.net
* Copyright: (c) by the original author(s) and all contributors
* License: Lesser GNU Public License (LGPL)
* http://www.opensource.org/licenses/lgpl-license.html
*/
package org.jhotdraw.contrib.zoom;
import org.jhotdraw.framework.Drawing;
import org.jhotdraw.framework.DrawingChangeEvent;
import org.jhotdraw.framework.DrawingEditor;
import org.jhotdraw.framework.FigureEnumeration;
import org.jhotdraw.framework.JHotDrawRuntimeException;
import org.jhotdraw.standard.StandardDrawing;
import org.jhotdraw.standard.StandardDrawingView;
import org.jhotdraw.util.Geom;
import javax.swing.JViewport;
import java.awt.*;
import java.awt.event.*;
import java.awt.geom.AffineTransform;
/**
* A view that can display drawings at an arbitrary scale.
*
* @author Andre Spiegel <spiegel@gnu.org>
* @version <$CURRENT_VERSION$>
*/
public class ZoomDrawingView extends StandardDrawingView {
/**
* The current scaling factor
*/
private double scale = 1.0;
/**
* The zooming speed, meaning the scaling factor change speed when doing
* zoomIn/zoomOut operations.<br>
* Default of 2.0 is increase 100% or decrease 50% the current factor.<br>
* Can be changed dynamically by clients using the setter method
*/
private double zoomSpeed = 2.0;
public ZoomDrawingView(DrawingEditor editor) {
this(editor, MINIMUM_WIDTH, MINIMUM_HEIGHT);
}
public ZoomDrawingView(DrawingEditor editor, int width, int height) {
super(editor, width, height);
}
/**
* @return The current zoom scale of this view. The dimensions of
* figures are multiplied by this number before display.
*/
public final double getScale() {
return scale;
}
/**
* Sets a new zoom scale for this view. The dimensions of figures
* are multiplied by this number before display.
*/
private void setScale(double newScale) {
// "de"-scale with old scale
Dimension oldSize = getUserSize();
scale = newScale;
// re-scale with new scale
setUserSize(oldSize.width, oldSize.height);
centralize(drawing());
forceRedraw();
}
private void forceRedraw() {
drawingInvalidated(new DrawingChangeEvent
(drawing(), new Rectangle(getSize())));
repairDamage();
}
/**
* Sets the size of this view in user coordinates. The size of the view
* on the screen will be this size, multiplied by the current scale.
*/
public void setUserSize(int width, int height) {
setSize((int) (width * getScale()),
(int) (height * getScale()));
}
/**
* Sets the size of this view in user coordinates. The size of the view
* on the screen will be this size, multiplied by the current scale.
*/
public void setUserSize(Dimension d) {
setUserSize(d.width, d.height);
}
/**
* @return the size of this view, in screen coordinates
*/
public Dimension getSize() {
return super.getSize();
}
public Dimension getViewportSize() {
return getParent().getSize();
}
protected boolean hasZoomSupport() {
return getParent() instanceof JViewport;
}
/**
* Sets the coordinates of the left top corner displayed by the view.<br>
*/
public void setOriginPosition(Point newOrigin) {
setViewPosition(newOrigin);
forceRedraw();
}
protected void setViewPosition(Point newPosition) {
((JViewport)getParent()).setViewPosition(newPosition);
}
/**
* @return The size of this view, in user coordinates. The size
* on the screen is this size, multiplied by the current scale.
*/
public Dimension getUserSize() {
Dimension screenSize = getSize();
return new Dimension((int) (screenSize.width / getScale()),
(int) (screenSize.height / getScale()));
}
/**
* Readjusts this view and its containing ScrollPane to display the
* given rectangle, which is given in user coordinates. This method
* only works if this view is contained in a JViewport. It throws
* a RuntimeException otherwise.
*/
public void zoom(int x, int y, int width, int height) {
if (hasZoomSupport()) {
Dimension viewportSize = getViewportSize();
double xScale = (double) viewportSize.width / (double) width;
double yScale = (double) viewportSize.height / (double) height;
double newScale = Math.min(xScale, yScale);
// "de"-scale with old scale
Dimension userSize = getUserSize();
this.scale = newScale;
// re-scale with new scale
setUserSize(userSize);
revalidate();
setViewPosition(
new Point((int) (x * getScale()), (int) (y * getScale())));
forceRedraw();
}
else {
throw new JHotDrawRuntimeException
("zooming only works if this view is contained in a ScrollPane");
}
}
/**
* Set this view's scale factor
*/
public void zoom(float newScale) {
if (hasZoomSupport()) {
JViewport viewport = (JViewport) getParent();
Dimension viewportSize = viewport.getSize();
Dimension userSize = getUserSize();
scale = newScale;
Point viewOrg = viewport.getViewPosition();
viewOrg.x = viewOrg.x + (viewportSize.width / 2);
viewOrg.y = viewOrg.y + (viewportSize.height / 2);
int xScreen = (int) (viewOrg.x * scale);
int yScreen = (int) (viewOrg.y * scale);
int xOrigin = xScreen - viewportSize.width / 2;
int yOrigin = yScreen - viewportSize.height / 2;
if (xOrigin < 0) xOrigin = 0;
if (yOrigin < 0) yOrigin = 0;
setUserSize(userSize);
revalidate();
viewport.setViewPosition(new Point(xOrigin, yOrigin));
forceRedraw();
}
else {
throw new JHotDrawRuntimeException
("zooming only works if this view is contained in a ScrollPane");
}
}
/**
* Zooms out by a factor of two, keeping point (x,y), which is given
* in user coordinates, in the center.
*/
public void zoomOut(int x, int y) {
if (hasZoomSupport()) {
Dimension viewportSize = getViewportSize();
// "de"-scale with old scale
Dimension userSize = getUserSize();
this.scale = getScale() / getZoomSpeed();
int xScreen = (int) (x * getScale());
int yScreen = (int) (y * getScale());
int xOrigin = xScreen - viewportSize.width / 2;
int yOrigin = yScreen - viewportSize.height / 2;
if (xOrigin < 0) xOrigin = 0;
if (yOrigin < 0) yOrigin = 0;
// re-scale with new scale
setUserSize(userSize);
revalidate();
setViewPosition(new Point(xOrigin, yOrigin));
forceRedraw();
}
else {
throw new JHotDrawRuntimeException
("zooming only works if this view is contained in a ScrollPane");
}
}
/**
* InContext
* Zooms in by a factor of the current scale, keeping point (x,y), which is given
* in user coordinates, in the center.
*/
public void zoomIn(int x, int y) {
if (hasZoomSupport()) {
JViewport viewport = (JViewport) getParent();
Dimension viewportSize = viewport.getSize();
Dimension userSize = getUserSize();
this.scale = getScale() * getZoomSpeed();
int xScreen = (int) (x * getScale());
int yScreen = (int) (y * getScale());
int xOrigin = xScreen - viewportSize.width / 2;
int yOrigin = yScreen - viewportSize.height / 2;
if (xOrigin < 0) xOrigin = 0;
if (yOrigin < 0) yOrigin = 0;
setUserSize(userSize);
revalidate();
viewport.setViewPosition(new Point(xOrigin, yOrigin));
forceRedraw();
}
else {
throw new JHotDrawRuntimeException
("zooming only works if this view is contained in a ScrollPane");
}
}
/**
* Sets the zoom scale to 1.0 and adjusts the scroll pane
* so that point (x, y) is in the center.
*/
public void deZoom(int x, int y) {
if (hasZoomSupport()) {
Dimension viewportSize = getViewportSize();
Dimension userSize = getUserSize();
int xOrigin = x - viewportSize.width / 2;
int yOrigin = y - viewportSize.height / 2;
if (xOrigin < 0) xOrigin = 0;
if (yOrigin < 0) yOrigin = 0;
this.scale = 1.0;
setUserSize(userSize);
revalidate();
setViewPosition(new Point((int) (xOrigin),
(int) (yOrigin)));
forceRedraw();
}
else {
throw new JHotDrawRuntimeException
("zooming only works if this view is contained in a ScrollPane");
}
}
public void paint(Graphics g) {
super.paint(transformGraphics(g, getScale()));
}
public Graphics getGraphics() {
return transformGraphics(super.getGraphics(), getScale());
}
private final Graphics transformGraphics(Graphics g, double currentScale) {
if (currentScale != 1.0) {
Graphics2D g2 = (Graphics2D) g;
// Don't use setTransform() here because that would destroy
// any transformation that Swing sets for partial redrawing.
// Simply add our own transformation to any existing one.
g2.transform(AffineTransform.getScaleInstance(currentScale, currentScale));
}
return g;
}
/**
* Constrain to user coordinates, not screen coordinates.
*/
protected Point constrainPoint(Point p) {
Dimension size = getSize();
p.x = Geom.range(1, (int) (size.width / getScale()), p.x);
p.y = Geom.range(1, (int) (size.height / getScale()), p.y);
if (getConstrainer() != null) {
return getConstrainer().constrainPoint(p);
}
return p;
}
public void drawBackground(Graphics g) {
g.setColor(getBackground());
g.fillRect(0, 0,
(int) (getBounds().width / getScale()),
(int) (getBounds().height / getScale()));
}
private void centralize(Drawing d, Dimension bounds) {
Point boundsCenter = new Point(bounds.width / 2, bounds.height / 2);
Rectangle r = ((StandardDrawing) d).displayBox();
Point drawingCenter = new Point(r.x + r.width / 2, r.y + r.height / 2);
int diffX = boundsCenter.x - drawingCenter.x;
int diffY = boundsCenter.y - drawingCenter.y;
if (diffX != 0 || diffY != 0) {
for (FigureEnumeration fe = d.figures(); fe.hasNextFigure();) {
fe.nextFigure().moveBy(diffX, diffY);
}
}
}
private void centralize(Drawing d) {
centralize(d, getUserSize());
}
public void setDrawing(Drawing d) {
super.setDrawing(d);
Rectangle r = ((StandardDrawing) d).displayBox();
//Dimension drawingSize = new Dimension(r.width, r.height);
Dimension viewportSize = new Dimension(r.width, r.height);
if (getParent() != null) {
viewportSize = getViewportSize();
}
/*
Dimension userSize = new Dimension(viewportSize);
this.scale = 1.0;
while (drawingSize.width > userSize.width ||
drawingSize.height > userSize.height) {
this.scale = getScale() / 2.0;
userSize.width = userSize.width * 2;
userSize.height = userSize.height * 2;
}
centralize(d, userSize);
*/
super.setPreferredSize(viewportSize);
super.setSize(viewportSize);
revalidate();
}
public Dimension getMinimumSize() {
return super.getSize();
}
public Dimension getPreferredSize() {
return getMinimumSize();
}
/**
* Overridden to scale damage to screen coordinates.
*/
public void repairDamage() {
Rectangle damagedArea = getDamage();
if (damagedArea != null) {
repaint((int) (damagedArea.x * getScale()),
(int) (damagedArea.y * getScale()),
(int) (damagedArea.width * getScale()),
(int) (damagedArea.height * getScale()));
setDamage(null);
}
}
/**
* Overridden to accumulate damage in an instance variable of this class.
*/
public void drawingInvalidated(DrawingChangeEvent e) {
Rectangle r = e.getInvalidatedRectangle();
if (getDamage() == null) {
setDamage(r);
}
else {
Rectangle damagedArea = getDamage();
damagedArea.add(r);
// the returned rectange may be a clone so we better set it again
setDamage(damagedArea);
}
}
/**
* @return a new MouseEvent, the coordinates of which are transformed
* to compensate for the current zoom factor
*/
private MouseEvent createScaledEvent(MouseEvent e) {
return new MouseEvent(e.getComponent(),
e.getID(),
e.getWhen(),
e.getModifiers(),
(int) (e.getX() / getScale()),
(int) (e.getY() / getScale()),
e.getClickCount(),
e.isPopupTrigger());
}
protected MouseListener createMouseListener() {
return new StandardDrawingView.DrawingViewMouseListener() {
public void mousePressed(MouseEvent e) {
super.mousePressed(createScaledEvent(e));
}
public void mouseReleased(MouseEvent e) {
super.mouseReleased(createScaledEvent(e));
}
};
}
protected MouseMotionListener createMouseMotionListener() {
return new StandardDrawingView.DrawingViewMouseMotionListener() {
public void mouseDragged(MouseEvent e) {
super.mouseDragged(createScaledEvent(e));
}
public void mouseMoved(MouseEvent e) {
super.mouseMoved(createScaledEvent(e));
}
};
}
protected KeyListener createKeyListener() {
return new StandardDrawingView.DrawingViewKeyListener() {
public void keyPressed(KeyEvent e) {
super.keyPressed(e);
if (e.getKeyChar() == ' ') {
forceRedraw();
}
else if (e.getKeyChar() == 'o') {
setScale(getScale() / getZoomSpeed());
}
else if (e.getKeyChar() == 'i') {
setScale(getScale() * getZoomSpeed());
}
else if (e.getKeyChar() == 'c') {
centralize(drawing());
}
else {
super.keyPressed(e);
}
}
};
}
/**
* Returns the current zoom speed
*/
public double getZoomSpeed()
{
return zoomSpeed;
}
/**
* Set the zoom speed. Will be greater than 1.
*/
public void setZoomSpeed(double newZoomSpeed)
{
// check greater than 1. A smaller value would reverse the zooming
// operation, and a zero value would provoque divide by zero exceptions
zoomSpeed = Math.max(1.1, newZoomSpeed);
}
}