package nodebox.client; import edu.umd.cs.piccolo.PNode; import edu.umd.cs.piccolo.event.PBasicInputEventHandler; import edu.umd.cs.piccolo.event.PInputEvent; import edu.umd.cs.piccolo.util.PPaintContext; import nodebox.node.ConnectionError; import nodebox.node.InvalidNameException; import nodebox.node.Node; import nodebox.node.Port; import javax.imageio.ImageIO; import javax.swing.*; import javax.swing.border.Border; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.KeyEvent; import java.awt.event.MouseEvent; import java.awt.geom.Point2D; import java.awt.image.BufferedImage; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.io.File; import java.io.IOException; import static nodebox.base.Preconditions.checkNotNull; public class NodeView extends PNode implements Selectable, PropertyChangeListener { public static final int NODE_FULL_SIZE = 70; public static final int NODE_IMAGE_SIZE = 50; public static final int TEXT_HEIGHT = 14; public static final int NODE_OUTPUT_DRAG_ZONE = 15; public static final Rectangle OUTPUT_BOUNDS = new Rectangle(NODE_FULL_SIZE - NODE_OUTPUT_DRAG_ZONE, (NODE_FULL_SIZE - NODE_OUTPUT_DRAG_ZONE - 6) / 2, NODE_OUTPUT_DRAG_ZONE, NODE_OUTPUT_DRAG_ZONE + 6); public static final int NODE_PORT_HEIGHT = 10; private static final int NODE_PORT_MARGIN = 5; public static final int GRID_SIZE = 10; private static BufferedImage nodeMask, nodeGlow, nodeConnectionGlow, nodeInPort, nodeOutPort, nodeGeneric, nodeError, nodeRendered, nodeCodeChanged, nodeRim; static { try { nodeMask = ImageIO.read(new File("res/node-mask.png")); nodeGlow = ImageIO.read(new File("res/node-glow.png")); nodeConnectionGlow = ImageIO.read(new File("res/node-connection-glow.png")); nodeInPort = ImageIO.read(new File("res/node-in-port.png")); nodeOutPort = ImageIO.read(new File("res/node-out-port.png")); nodeGeneric = ImageIO.read(new File("res/node-generic.png")); nodeError = ImageIO.read(new File("res/node-error.png")); nodeRendered = ImageIO.read(new File("res/node-rendered.png")); nodeCodeChanged = ImageIO.read(new File("res/node-codechanged.png")); nodeRim = ImageIO.read(new File("res/node-rim.png")); } catch (IOException e) { e.printStackTrace(); } } private NetworkView networkView; private Node node; private BufferedImage fullIcon; private Border border; private boolean selected; private transient boolean codeChanged; private transient double fakeX, fakeY; public NodeView(NetworkView networkView, Node node) { this.networkView = networkView; this.node = node; this.selected = false; this.codeChanged = false; setTransparency(1.0F); addInputEventListener(new NodeHandler()); setOffset(node.getX(), node.getY()); setBounds(0, 0, NODE_FULL_SIZE, NODE_FULL_SIZE + TEXT_HEIGHT); addPropertyChangeListener(PROPERTY_TRANSFORM, this); addPropertyChangeListener(PROPERTY_BOUNDS, this); addInputEventListener(new PopupHandler()); updateIcon(); } public NodeBoxDocument getDocument() { return networkView.getDocument(); } /** * Tries to find an image representation for the node. * The image should be located near the library, and have the same name as the library. *

* If this node has no image, the prototype is searched to find its image. If no image could be found, * a generic image is retured. * * @param node the node * @return an Image object. */ public static BufferedImage getImageForNode(Node node) { if (node == null || node.getImage() == null || node.getImage().equals(Node.IMAGE_GENERIC)) return nodeGeneric; File libraryFile = node.getLibrary().getFile(); if (libraryFile != null) { File libraryDirectory = libraryFile.getParentFile(); if (libraryDirectory != null) { File nodeImageFile = new File(libraryDirectory, node.getImage()); if (nodeImageFile.exists()) { try { return ImageIO.read(nodeImageFile); } catch (IOException ignored) { // Pass through } } } } // Look for the prototype return getImageForNode(node.getPrototype()); } /** * Calculate the vertical offset for the port. This value starts from the full node size. * * @param port the port. The index of the port is used to calculate the offset. * @return the vertical offset */ public static int getVerticalOffsetForPort(Port port) { Node node = port.getNode(); java.util.List ports = node.getPorts(); int portIndex = node.getPorts().indexOf(port); int portCount = ports.size(); int totalPortsHeight = (NODE_PORT_HEIGHT + NODE_PORT_MARGIN) * (portCount - 1) + NODE_PORT_HEIGHT; int offsetPerPort = NODE_PORT_HEIGHT + NODE_PORT_MARGIN; int portStartY = (NODE_FULL_SIZE - totalPortsHeight) / 2 - 1; return portStartY + portIndex * offsetPerPort; } /** * Create an icon with the node's image and the rounded embellishments. * * @param node the node * @return an Image object. */ public static BufferedImage getFullImageForNode(Node node, boolean drawPorts) { Image icon = getImageForNode(node); // Create the icon. // We include only the parts that are not changed by state. // This means leaving off the error and rendered image. // Also, we draw the rim at the very end, above the error and rendered, // so we can't draw it here yet. BufferedImage fullIcon = new BufferedImage(NODE_FULL_SIZE, NODE_FULL_SIZE, BufferedImage.TYPE_INT_ARGB); Graphics2D fg = fullIcon.createGraphics(); if (drawPorts) { // Count the input ports and draw them. java.util.List inputs = node.getPorts(); for (Port p : inputs) { int portY = getVerticalOffsetForPort(p); fg.drawImage(nodeInPort, 0, portY, null); } fg.drawImage(nodeOutPort, 0, 0, null); } // Draw the other layers. fg.drawImage(nodeMask, 0, 0, null); fg.setComposite(AlphaComposite.SrcIn); fg.drawImage(icon, 10, 10, NODE_IMAGE_SIZE, NODE_IMAGE_SIZE, null); fg.setComposite(AlphaComposite.SrcOver); //fg.drawImage(nodeReflection, 0, 0, null); fg.dispose(); return fullIcon; } public void updateIcon() { fullIcon = getFullImageForNode(node, true); } public void propertyChange(PropertyChangeEvent evt) { if (evt.getPropertyName().equals(PROPERTY_TRANSFORM)) { getDocument().setNodePosition(node, new nodebox.graphics.Point(super.getOffset())); } } public NetworkView getNetworkView() { return networkView; } public Node getNode() { return node; } public Border getBorder() { return border; } public void setBorder(Border border) { this.border = border; } protected void paint(PPaintContext ctx) { Graphics2D g = ctx.getGraphics(); Shape clip = g.getClip(); g.clip(getBounds()); g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY); // Draw the selection/connection border if (selected && networkView.getConnectionTarget() != this) g.drawImage(nodeGlow, 0, 0, null); if (networkView.getConnectionTarget() == this) g.drawImage(nodeConnectionGlow, 0, 0, null); g.drawImage(fullIcon, 0, 0, null); if (codeChanged) g.drawImage(nodeCodeChanged, 0, 0, null); if (node.hasError()) g.drawImage(nodeError, 0, 0, null); if (node.isRendered()) g.drawImage(nodeRendered, 0, 0, null); g.drawImage(nodeRim, 0, 0, null); // Draw the node name. g.setFont(Theme.SMALL_BOLD_FONT); g.setColor(Theme.NETWORK_NODE_NAME_COLOR); int textWidth = g.getFontMetrics().stringWidth(node.getName()); int x = (int) ((NODE_FULL_SIZE - textWidth) / 2f); SwingUtils.drawShadowText(g, node.getName(), x, NODE_FULL_SIZE + 5, Theme.NETWORK_NODE_NAME_SHADOW_COLOR, -1); // Reset the clipping. g.setClip(clip); } public boolean isSelected() { return selected; } public void setSelected(boolean s) { if (selected == s) return; selected = s; repaint(); } public boolean hasCodeChanged() { return codeChanged; } public void setCodeChanged(boolean changed) { if (codeChanged == changed) return; codeChanged = changed; repaint(); } private void doRename() { String s = JOptionPane.showInputDialog(networkView, "New name:", node.getName()); if (s == null || s.length() == 0) return; try { getDocument().setNodeName(node, s); } catch (InvalidNameException ex) { JOptionPane.showMessageDialog(networkView, "The given name is not valid.\n" + ex.getMessage(), Application.NAME, JOptionPane.ERROR_MESSAGE); } } @Override public Point2D getOffset() { return new Point2D.Double(fakeX, fakeY); } @Override public void setOffset(Point2D pt) { setOffset(pt.getX(), pt.getY()); } @Override public void setOffset(double x, double y) { fakeX = x; fakeY = y; super.setOffset(snap(fakeX), snap(fakeY)); } private static double snap(double value) { value = Math.round(value); return Math.round(value / GRID_SIZE) * GRID_SIZE; } private class NodeHandler extends PBasicInputEventHandler { protected Point2D dragPoint; private boolean isDragging; public void mouseClicked(PInputEvent e) { if (e.getClickCount() == 1) { e.getInputManager().setKeyboardFocus(this); networkView.singleSelect(NodeView.this); } else if (e.getClickCount() == 2 && e.isLeftMouseButton()) { Point2D pt = NodeView.this.getOffset(); double y = e.getPosition().getY() - pt.getY(); // Check if we're clicking on the label, which is below the node. // We give the user some space below to compensate for the glow. if (y > NODE_FULL_SIZE - 4) { doRename(); } else { getDocument().setRenderedNode(node); } } e.setHandled(true); } public void mousePressed(PInputEvent e) { if (isPanningEvent(e)) return; if (e.getButton() == MouseEvent.BUTTON1) { Point2D pt = NodeView.this.getOffset(); double x = e.getPosition().getX() - pt.getX(); double y = e.getPosition().getY() - pt.getY(); // Find the area where the mouse is pressed // Possible areas are the output connector and the node itself. if (OUTPUT_BOUNDS.contains(x, y)) { isDragging = false; networkView.startConnection(NodeView.this); } else { isDragging = true; dragPoint = e.getPosition(); // Make sure that this node is also selected. if (!isSelected()) { // If other nodes are selected, deselect them so they // don't get dragged along. networkView.singleSelect(NodeView.this); } } } } public void mouseEntered(PInputEvent e) { if (networkView.isConnecting() && networkView.getConnectionSource() != NodeView.this) { networkView.setTemporaryConnectionTarget(NodeView.this); } } public void mouseExited(PInputEvent e) { if (networkView.isConnecting()) { networkView.setTemporaryConnectionTarget(null); } } public void mouseDragged(PInputEvent e) { if (isPanningEvent(e)) return; if (isDragging) { checkNotNull(dragPoint, "dragPoint cannot be null."); Point2D pt = e.getPosition(); double dx = pt.getX() - dragPoint.getX(); double dy = pt.getY() - dragPoint.getY(); getNetworkView().dragSelection(dx, dy); dragPoint = pt; } else if (networkView.isConnecting()) { Point2D p = e.getPosition(); networkView.dragConnectionPoint(p); } e.setHandled(true); } public void mouseReleased(PInputEvent event) { if (networkView.isConnecting()) { // Check if both source and target are set. if (networkView.getConnectionSource() != null && networkView.getConnectionTarget() != null) { Node source = networkView.getConnectionSource().getNode(); Node target = networkView.getConnectionTarget().getNode(); // Look for compatible ports. java.util.List compatiblePorts = target.getCompatibleInputs(source); if (compatiblePorts.isEmpty()) { // There are no compatible parameters. } else if (compatiblePorts.size() == 1) { // Only one possible connection, make it now. Port inputPort = compatiblePorts.get(0); try { getDocument().connect(source.getOutputPort(), inputPort); } catch (ConnectionError e) { JOptionPane.showMessageDialog(networkView, e.getMessage(), "Connection error", JOptionPane.ERROR_MESSAGE); } } else { JPopupMenu menu = new JPopupMenu("Select input"); for (Port p : compatiblePorts) { Action a = new SelectCompatiblePortAction(source, p); menu.add(a); } Point pt = getNetworkView().getMousePosition(); menu.show(getNetworkView(), pt.x, pt.y); } } networkView.endConnection(); } } private boolean isPanningEvent(PInputEvent event) { return (event.getModifiers() & MouseEvent.ALT_MASK) != 0; } } class SelectCompatiblePortAction extends AbstractAction { private Node outputNode; private Port inputPort; SelectCompatiblePortAction(Node outputNode, Port inputPort) { super(inputPort.getName()); this.outputNode = outputNode; this.inputPort = inputPort; } public void actionPerformed(ActionEvent e) { try { getDocument().connect(outputNode.getOutputPort(), inputPort); } catch (ConnectionError ce) { JOptionPane.showMessageDialog(networkView, ce.getMessage(), "Connection error", JOptionPane.ERROR_MESSAGE); } } } private class PopupHandler extends PBasicInputEventHandler { public void processEvent(PInputEvent e, int i) { if (!e.isPopupTrigger()) return; JPopupMenu menu = new JPopupMenu(); menu.add(new SetRenderedAction()); menu.add(new RenameAction()); menu.add(new DeleteAction()); menu.add(new GoInAction()); Point2D p = e.getCanvasPosition(); menu.show(NodeView.this.networkView, (int) p.getX(), (int) p.getY()); e.setHandled(true); } } private class SetRenderedAction extends AbstractAction { private SetRenderedAction() { super("Set Rendered"); } public void actionPerformed(ActionEvent e) { getDocument().setRenderedNode(node); networkView.repaint(); } } private class RenameAction extends AbstractAction { private RenameAction() { super("Rename"); } public void actionPerformed(ActionEvent e) { doRename(); } } private class DeleteAction extends AbstractAction { public DeleteAction() { super("Delete"); putValue(ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_BACK_SPACE, 0)); } public void actionPerformed(ActionEvent e) { getDocument().removeNode(node); } } private class GoInAction extends AbstractAction { private GoInAction() { super("Edit Children"); putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0)); } public void actionPerformed(ActionEvent e) { getDocument().setActiveNetwork(node); } } }