package nodebox.client;
import com.google.common.collect.ImmutableList;
import nodebox.client.visualizer.*;
import nodebox.graphics.CanvasContext;
import nodebox.graphics.IGeometry;
import nodebox.handle.Handle;
import nodebox.ui.Theme;
import nodebox.ui.Zoom;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.LinkedList;
import static com.google.common.base.Preconditions.checkNotNull;
import static nodebox.util.ListUtils.listClass;
public class Viewer extends ZoomableView implements OutputView, Zoom, MouseListener, MouseMotionListener, KeyListener {
public static final double MIN_ZOOM = 0.01;
public static final double MAX_ZOOM = 64.0;
private final JPopupMenu viewerMenu;
private nodebox.graphics.Point lastMousePosition = nodebox.graphics.Point.ZERO;
private Handle handle;
private boolean showHandle = true;
private boolean showPoints = false;
private boolean showPointNumbers = false;
private boolean showOrigin = false;
private boolean showBounds = false;
private boolean viewPositioned = false;
private java.util.List<?> outputValues;
private Rectangle2D canvasBounds = new Rectangle2D.Double(-500, -500, 1000, 1000);
private Class valuesClass;
private Visualizer currentVisualizer = VisualizerFactory.getDefaultVisualizer();
public Viewer() {
super(MIN_ZOOM, MAX_ZOOM);
addMouseListener(this);
addMouseMotionListener(this);
setFocusable(true);
addKeyListener(this);
setBackground(Theme.VIEWER_BACKGROUND_COLOR);
viewerMenu = new JPopupMenu();
viewerMenu.add(new ResetViewAction());
PopupHandler popupHandler = new PopupHandler();
addMouseListener(popupHandler);
}
public void zoom(double scaleDelta) {
super.zoom(scaleDelta, getWidth() / 2.0, getHeight() / 2.0);
}
public boolean containsPoint(Point point) {
return isVisible() && getBounds().contains(point);
}
public void setShowHandle(boolean showHandle) {
this.showHandle = showHandle;
repaint();
}
public void setShowPoints(boolean showPoints) {
this.showPoints = showPoints;
repaint();
}
public void setShowPointNumbers(boolean showPointNumbers) {
this.showPointNumbers = showPointNumbers;
repaint();
}
public void setShowOrigin(boolean showOrigin) {
this.showOrigin = showOrigin;
repaint();
}
public void setShowBounds(boolean showBounds) {
this.showBounds = showBounds;
repaint();
}
//// Handle support ////
public Handle getHandle() {
return handle;
}
public void setHandle(Handle handle) {
this.handle = handle;
repaint();
}
public void updateHandle() {
if (handle == null) return;
handle.update();
}
public boolean hasVisibleHandle() {
if (handle == null) return false;
if (!showHandle) return false;
// Don't show handles for LastResortVisualizer and ColorVisualizer.
if (currentVisualizer instanceof LastResortVisualizer) return false;
if (currentVisualizer instanceof ColorVisualizer) return false;
return handle.isVisible();
}
//// Network data events ////
public void setOutputValues(java.util.List<?> outputValues) {
this.outputValues = outputValues;
valuesClass = listClass(outputValues);
Visualizer visualizer = VisualizerFactory.getVisualizer(outputValues, valuesClass);
if (visualizer instanceof LastResortVisualizer && outputValues.size() == 0) {
// This scenario means likely that we're in a node that normally outputs
// some visual type but currently outputs null (or None)
// If we'd reset the visualizer the screen offset would change, and this would
// lead to strange (and wrong) interactions with handles (big leaps in
// current mouse locations).
repaint();
return;
}
if (currentVisualizer != visualizer) {
currentVisualizer = visualizer;
resetViewTransform();
}
checkNotNull(currentVisualizer);
repaint();
}
public void setCanvasBounds(Rectangle2D bounds) {
this.canvasBounds = bounds;
repaint();
}
@Override
public void resetViewTransform() {
Point2D position = currentVisualizer.getOffset(outputValues, getSize());
setViewTransform(position.getX(), position.getY(), 1);
}
//// Mouse events ////
private nodebox.graphics.Point pointForEvent(MouseEvent e) {
Point2D pt = inverseViewTransformPoint(e.getPoint());
return new nodebox.graphics.Point(pt);
}
public nodebox.graphics.Point getLastMousePosition() {
return lastMousePosition;
}
public void mouseClicked(MouseEvent e) {
// We register the mouse click as an edit since it can trigger a change to the node.
if (e.isPopupTrigger()) return;
if (hasVisibleHandle()) {
//getDocument().addEdit(HANDLE_UNDO_TEXT, HANDLE_UNDO_TYPE, activeNode);
handle.mouseClicked(pointForEvent(e));
}
}
public void mousePressed(MouseEvent e) {
// We register the mouse press as an edit since it can trigger a change to the node.
if (e.isPopupTrigger()) return;
if (hasVisibleHandle()) {
//getDocument().addEdit(HANDLE_UNDO_TEXT, HANDLE_UNDO_TYPE, activeNode);
handle.mousePressed(pointForEvent(e));
}
}
public void mouseReleased(MouseEvent e) {
// We register the mouse release as an edit since it can trigger a change to the node.
if (e.isPopupTrigger()) return;
if (hasVisibleHandle()) {
//getDocument().addEdit(HANDLE_UNDO_TEXT, HANDLE_UNDO_TYPE, activeNode);
handle.mouseReleased(pointForEvent(e));
}
}
public void mouseEntered(MouseEvent e) {
// Entering the viewer with your mouse should not change the node, so we do not register an edit.
if (e.isPopupTrigger()) return;
if (hasVisibleHandle()) {
handle.mouseEntered(pointForEvent(e));
}
}
public void mouseExited(MouseEvent e) {
// Exiting the viewer with your mouse should not change the node, so we do not register an edit.
if (e.isPopupTrigger()) return;
if (hasVisibleHandle()) {
handle.mouseExited(pointForEvent(e));
}
}
public void mouseDragged(MouseEvent e) {
// We register the mouse drag as an edit since it can trigger a change to the node.
if (e.isPopupTrigger()) return;
if (isPanning()) return;
if (hasVisibleHandle()) {
//getDocument().addEdit(HANDLE_UNDO_TEXT, HANDLE_UNDO_TYPE, activeNode);
handle.mouseDragged(pointForEvent(e));
}
lastMousePosition = pointForEvent(e);
}
public void mouseMoved(MouseEvent e) {
// Moving the mouse in the viewer area should not change the node, so we do not register an edit.
if (e.isPopupTrigger()) return;
if (hasVisibleHandle()) {
handle.mouseMoved(pointForEvent(e));
}
lastMousePosition = pointForEvent(e);
}
public void keyTyped(KeyEvent e) {
if (hasVisibleHandle())
handle.keyTyped(e.getKeyCode(), e.getModifiersEx());
}
public void keyPressed(KeyEvent e) {
if (hasVisibleHandle())
handle.keyPressed(e.getKeyCode(), e.getModifiersEx());
}
public void keyReleased(KeyEvent e) {
Component c = SwingUtilities.getWindowAncestor(Viewer.this);
if (c instanceof FullScreenFrame) {
FullScreenFrame frame = (FullScreenFrame) c;
if (e.getKeyCode() == KeyEvent.VK_ESCAPE) {
frame.close();
} else if (e.getKeyCode() == KeyEvent.VK_P) {
int metaMask = KeyEvent.META_MASK;
int metaShiftMask = KeyEvent.META_MASK | KeyEvent.SHIFT_MASK;
if (e.getModifiers() == metaMask)
frame.toggleAnimation();
else if (e.getModifiers() == metaShiftMask)
frame.rewindAnimation();
}
}
if (hasVisibleHandle())
handle.keyReleased(e.getKeyCode(), e.getModifiersEx());
}
@Override
public boolean isFocusable() {
return true;
}
@Override
public void paintComponent(Graphics g) {
if (!viewPositioned) {
setViewPosition(getWidth() / 2.0, getHeight() / 2.0);
viewPositioned = true;
}
Graphics2D g2 = (Graphics2D) g;
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR);
// Draw background
g2.setColor(getBackground());
g2.fill(g.getClipBounds());
// Set the view transform
AffineTransform originalTransform = g2.getTransform();
g2.transform(getViewTransform());
paintBounds(g2);
paintObjects(g2);
paintHandle(g2);
paintPoints(g2);
paintPointNumbers(g2);
// Restore original transform
g2.setClip(null);
g2.setTransform(originalTransform);
g2.setStroke(new BasicStroke(1));
paintOrigin(g2);
}
public void paintObjects(Graphics2D g) {
if (currentVisualizer != null)
currentVisualizer.draw(g, outputValues);
}
private void paintPoints(Graphics2D g) {
if (showPoints && IGeometry.class.isAssignableFrom(valuesClass)) {
// TODO Create a dynamic iterator that combines all output values into one flat sequence.
LinkedList<nodebox.graphics.Point> points = new LinkedList<nodebox.graphics.Point>();
for (Object o : outputValues) {
IGeometry geo = (IGeometry) o;
points.addAll(geo.getPoints());
}
PointVisualizer.drawPoints(g, points);
}
}
private void paintPointNumbers(Graphics2D g) {
if (!showPointNumbers) return;
g.setFont(Theme.SMALL_MONO_FONT);
g.setColor(Color.BLUE);
int index = 0;
if (IGeometry.class.isAssignableFrom(valuesClass)) {
for (Object o : outputValues) {
IGeometry geo = (IGeometry) o;
for (nodebox.graphics.Point pt : geo.getPoints())
paintPointNumber(g, pt, index++);
}
} else if (nodebox.graphics.Point.class.isAssignableFrom(valuesClass)) {
for (Object o : outputValues)
paintPointNumber(g, (nodebox.graphics.Point) o, index++);
}
}
private void paintPointNumber(Graphics2D g, nodebox.graphics.Point pt, int number) {
if (pt.isOnCurve()) {
g.setColor(Color.BLUE);
} else {
g.setColor(Color.RED);
}
g.drawString(number + "", (int) (pt.x + 3), (int) (pt.y - 2));
}
public void paintOrigin(Graphics2D g) {
if (showOrigin) {
int x = (int) Math.round(getViewX());
int y = (int) Math.round(getViewY());
g.setColor(Color.DARK_GRAY);
g.drawLine(x, 0, x, getHeight());
g.drawLine(0, y, getWidth(), y);
}
}
public void paintBounds(Graphics2D g) {
if (showBounds) {
g.setColor(Color.DARK_GRAY);
int x = (int) Math.round(canvasBounds.getX());
int y = (int) Math.round(canvasBounds.getY());
int width = (int) Math.round(canvasBounds.getWidth());
int height = (int) Math.round(canvasBounds.getHeight());
g.drawRect(x, y, width, height);
g.drawLine(x + width + 1, y + 1, x + width + 1, y + height + 1);
g.drawLine(x + 1, y + height + 1, x + width + 1, y + height + 1);
}
}
public void paintHandle(Graphics2D g) {
if (hasVisibleHandle()) {
// Create a canvas with a transparent background.
nodebox.graphics.Canvas canvas = new nodebox.graphics.Canvas();
canvas.setBackground(new nodebox.graphics.Color(0, 0, 0, 0));
CanvasContext ctx = new CanvasContext(canvas);
try {
handle.draw(ctx);
} catch (Exception e) {
e.printStackTrace();
}
ctx.getCanvas().draw(g);
}
}
private class PopupHandler extends MouseAdapter {
@Override
public void mousePressed(MouseEvent e) {
if (e.isPopupTrigger()) {
showPopup(e);
}
}
@Override
public void mouseReleased(MouseEvent e) {
if (e.isPopupTrigger()) {
showPopup(e);
}
}
public void showPopup(MouseEvent e) {
if (!e.isPopupTrigger()) return;
viewerMenu.show(Viewer.this, e.getX(), e.getY());
}
}
private class ResetViewAction extends AbstractAction {
private ResetViewAction() {
super("Reset View");
}
public void actionPerformed(ActionEvent e) {
resetViewTransform();
}
}
}