package net.sf.openrocket.gui.scalefigure;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.util.EventObject;
import javax.swing.BorderFactory;
import javax.swing.JComponent;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.ScrollPaneConstants;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import net.sf.openrocket.gui.adaptors.DoubleModel;
import net.sf.openrocket.gui.components.UnitSelector;
import net.sf.openrocket.unit.Tick;
import net.sf.openrocket.unit.Unit;
import net.sf.openrocket.unit.UnitGroup;
import net.sf.openrocket.util.BugException;
import net.sf.openrocket.util.StateChangeListener;
/**
* A scroll pane that holds a {@link ScaleFigure} and includes rulers that show
* natural units. The figure can be moved by dragging on the figure.
* <p>
* This class implements both <code>MouseListener</code> and
* <code>MouseMotionListener</code>. If subclasses require extra functionality
* (e.g. checking for clicks) then these methods may be overridden, and only unhandled
* events passed to this class.
*
* @author Sampo Niskanen <sampo.niskanen@iki.fi>
*/
public class ScaleScrollPane extends JScrollPane
implements MouseListener, MouseMotionListener {
public static final int RULER_SIZE = 20;
public static final int MINOR_TICKS = 3;
public static final int MAJOR_TICKS = 30;
private JComponent component;
private ScaleFigure figure;
private DoubleModel rulerUnit;
private Ruler horizontalRuler;
private Ruler verticalRuler;
private final boolean allowFit;
private boolean fit = false;
/**
* Create a scale scroll pane that allows fitting.
*
* @param component the component to contain (must implement ScaleFigure)
*/
public ScaleScrollPane(JComponent component) {
this(component, true);
}
/**
* Create a scale scroll pane.
*
* @param component the component to contain (must implement ScaleFigure)
* @param allowFit whether automatic fitting of the figure is allowed
*/
public ScaleScrollPane(JComponent component, boolean allowFit) {
super(component);
if (!(component instanceof ScaleFigure)) {
throw new IllegalArgumentException("component must implement ScaleFigure");
}
this.component = component;
this.figure = (ScaleFigure) component;
this.allowFit = allowFit;
rulerUnit = new DoubleModel(0.0, UnitGroup.UNITS_LENGTH);
rulerUnit.addChangeListener(new ChangeListener() {
@Override
public void stateChanged(ChangeEvent e) {
ScaleScrollPane.this.component.repaint();
}
});
horizontalRuler = new Ruler(Ruler.HORIZONTAL);
verticalRuler = new Ruler(Ruler.VERTICAL);
this.setColumnHeaderView(horizontalRuler);
this.setRowHeaderView(verticalRuler);
UnitSelector selector = new UnitSelector(rulerUnit);
selector.setFont(new Font("SansSerif", Font.PLAIN, 8));
this.setCorner(JScrollPane.UPPER_LEFT_CORNER, selector);
this.setCorner(JScrollPane.UPPER_RIGHT_CORNER, new JPanel());
this.setCorner(JScrollPane.LOWER_LEFT_CORNER, new JPanel());
this.setCorner(JScrollPane.LOWER_RIGHT_CORNER, new JPanel());
this.setBorder(BorderFactory.createLineBorder(Color.LIGHT_GRAY));
viewport.addMouseListener(this);
viewport.addMouseMotionListener(this);
figure.addChangeListener(new StateChangeListener() {
@Override
public void stateChanged(EventObject e) {
horizontalRuler.updateSize();
verticalRuler.updateSize();
if (fit) {
setFitting(true);
}
}
});
viewport.addComponentListener(new ComponentAdapter() {
@Override
public void componentResized(ComponentEvent e) {
if (fit) {
setFitting(true);
}
}
});
}
public ScaleFigure getFigure() {
return figure;
}
/**
* Return whether automatic fitting of the figure is allowed.
*/
public boolean isFittingAllowed() {
return allowFit;
}
/**
* Return whether the figure is currently automatically fitted within the component bounds.
*/
public boolean isFitting() {
return fit;
}
/**
* Set whether the figure is automatically fitted within the component bounds.
*
* @throws BugException if automatic fitting is disallowed and <code>fit</code> is <code>true</code>
*/
public void setFitting(boolean fit) {
if (fit && !allowFit) {
throw new BugException("Attempting to fit figure not allowing fit.");
}
this.fit = fit;
if (fit) {
setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_NEVER);
validate();
Dimension view = viewport.getExtentSize();
figure.setScaling(view);
} else {
setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED);
setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED);
}
}
public double getScaling() {
return figure.getScaling();
}
public double getScale() {
return figure.getAbsoluteScale();
}
public void setScaling(double scale) {
if (fit) {
setFitting(false);
}
figure.setScaling(scale);
horizontalRuler.repaint();
verticalRuler.repaint();
}
public Unit getCurrentUnit() {
return rulerUnit.getCurrentUnit();
}
//////////////// Mouse handlers ////////////////
private int dragStartX = 0;
private int dragStartY = 0;
private Rectangle dragRectangle = null;
@Override
public void mousePressed(MouseEvent e) {
dragStartX = e.getX();
dragStartY = e.getY();
dragRectangle = viewport.getViewRect();
}
@Override
public void mouseReleased(MouseEvent e) {
dragRectangle = null;
}
@Override
public void mouseDragged(MouseEvent e) {
if (dragRectangle == null) {
return;
}
dragRectangle.setLocation(dragStartX - e.getX(), dragStartY - e.getY());
dragStartX = e.getX();
dragStartY = e.getY();
viewport.scrollRectToVisible(dragRectangle);
}
@Override
public void mouseClicked(MouseEvent e) {
}
@Override
public void mouseEntered(MouseEvent e) {
}
@Override
public void mouseExited(MouseEvent e) {
}
@Override
public void mouseMoved(MouseEvent e) {
}
//////////////// The view port rulers ////////////////
private class Ruler extends JComponent {
public static final int HORIZONTAL = 0;
public static final int VERTICAL = 1;
private final int orientation;
public Ruler(int orientation) {
this.orientation = orientation;
updateSize();
rulerUnit.addChangeListener(new ChangeListener() {
@Override
public void stateChanged(ChangeEvent e) {
Ruler.this.repaint();
}
});
}
public void updateSize() {
Dimension d = component.getPreferredSize();
if (orientation == HORIZONTAL) {
setPreferredSize(new Dimension(d.width + 10, RULER_SIZE));
} else {
setPreferredSize(new Dimension(RULER_SIZE, d.height + 10));
}
revalidate();
repaint();
}
private double fromPx(int px) {
Dimension origin = figure.getOrigin();
if (orientation == HORIZONTAL) {
px -= origin.width;
} else {
// px = -(px - origin.height);
px -= origin.height;
}
return px / figure.getAbsoluteScale();
}
private int toPx(double l) {
Dimension origin = figure.getOrigin();
int px = (int) (l * figure.getAbsoluteScale() + 0.5);
if (orientation == HORIZONTAL) {
px += origin.width;
} else {
px = px + origin.height;
// px += origin.height;
}
return px;
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2 = (Graphics2D) g;
Rectangle area = g2.getClipBounds();
// Fill area with background color
g2.setColor(getBackground());
g2.fillRect(area.x, area.y, area.width, area.height + 100);
int startpx, endpx;
if (orientation == HORIZONTAL) {
startpx = area.x;
endpx = area.x + area.width;
} else {
startpx = area.y;
endpx = area.y + area.height;
}
Unit unit = rulerUnit.getCurrentUnit();
double start, end, minor, major;
start = fromPx(startpx);
end = fromPx(endpx);
minor = MINOR_TICKS / figure.getAbsoluteScale();
major = MAJOR_TICKS / figure.getAbsoluteScale();
Tick[] ticks = unit.getTicks(start, end, minor, major);
// Set color & hints
g2.setColor(Color.BLACK);
g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL,
RenderingHints.VALUE_STROKE_NORMALIZE);
g2.setRenderingHint(RenderingHints.KEY_RENDERING,
RenderingHints.VALUE_RENDER_QUALITY);
g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
for (Tick t : ticks) {
int position = toPx(t.value);
drawTick(g2, position, t);
}
}
private void drawTick(Graphics g, int position, Tick t) {
int length;
String str = null;
if (t.major) {
length = RULER_SIZE / 2;
} else {
if (t.notable)
length = RULER_SIZE / 3;
else
length = RULER_SIZE / 6;
}
// Set font
if (t.major) {
str = rulerUnit.getCurrentUnit().toString(t.value);
if (t.notable)
g.setFont(new Font("SansSerif", Font.BOLD, 9));
else
g.setFont(new Font("SansSerif", Font.PLAIN, 9));
}
// Draw tick & text
if (orientation == HORIZONTAL) {
g.drawLine(position, RULER_SIZE - length, position, RULER_SIZE);
if (str != null)
g.drawString(str, position, RULER_SIZE - length - 1);
} else {
g.drawLine(RULER_SIZE - length, position, RULER_SIZE, position);
if (str != null)
g.drawString(str, 1, position - 1);
}
}
}
}