package nodebox.client; import com.google.common.base.Splitter; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; import com.google.common.collect.Maps; import nodebox.node.*; import nodebox.ui.PaneView; import nodebox.ui.Platform; import nodebox.ui.Theme; import nodebox.ui.Zoom; import org.python.google.common.base.Joiner; import javax.imageio.ImageIO; import javax.swing.*; import java.awt.*; import java.awt.event.*; import java.awt.geom.AffineTransform; import java.awt.geom.GeneralPath; import java.awt.geom.Point2D; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.concurrent.ExecutionException; import static com.google.common.base.Preconditions.checkNotNull; public class NetworkView extends ZoomableView implements PaneView, Zoom { public static final int GRID_CELL_SIZE = 48; public static final int NODE_MARGIN = 6; public static final int NODE_PADDING = 5; public static final int NODE_WIDTH = GRID_CELL_SIZE * 3 - NODE_MARGIN * 2; public static final int NODE_HEIGHT = GRID_CELL_SIZE - NODE_MARGIN * 2; public static final int NODE_ICON_SIZE = 26; public static final int GRID_OFFSET = 6; public static final int PORT_WIDTH = 10; public static final int PORT_HEIGHT = 3; public static final int PORT_SPACING = 10; public static final Dimension NODE_DIMENSION = new Dimension(NODE_WIDTH, NODE_HEIGHT); public static final String SELECT_PROPERTY = "NetworkView.select"; public static final int COMMENT_BOX_MARGIN_HORIZONTAL = 5; private static Map<String, BufferedImage> fileImageCache = new HashMap<String, BufferedImage>(); private static BufferedImage nodeGeneric, commentIcon, commentBox; public static final float MIN_ZOOM = 0.05f; public static final float MAX_ZOOM = 1.0f; public static final Map<String, Color> PORT_COLORS = Maps.newHashMap(); public static final Color DEFAULT_PORT_COLOR = new Color(52, 85, 52); public static final Color PORT_HOVER_COLOR = Color.YELLOW; public static final Color TOOLTIP_BACKGROUND_COLOR = new Color(254, 255, 215); public static final Color TOOLTIP_STROKE_COLOR = Color.DARK_GRAY; public static final Color TOOLTIP_TEXT_COLOR = Color.DARK_GRAY; public static final Color DRAG_SELECTION_COLOR = new Color(255, 255, 255, 100); public static final BasicStroke DRAG_SELECTION_STROKE = new BasicStroke(1f); public static final BasicStroke CONNECTION_STROKE = new BasicStroke(2); private final NodeBoxDocument document; private JPopupMenu networkMenu; private Point networkMenuLocation; private Point nodeMenuLocation; private LoadingCache<Node, BufferedImage> nodeImageCache; private Set<String> selectedNodes = new HashSet<String>(); // Interaction state private boolean isDraggingNodes = false; private boolean isShiftPressed = false; private boolean isAltPressed = false; private boolean isDragSelecting = false; private ImmutableMap<String, nodebox.graphics.Point> dragPositions = ImmutableMap.of(); private NodePort overInput; private Node overOutput; private Node overComment; private Node connectionOutput; private NodePort connectionInput; private Point2D connectionPoint; private boolean startDragging; private Point2D dragStartPoint; private Point2D dragCurrentPoint; static { try { nodeGeneric = ImageIO.read(NetworkView.class.getResourceAsStream("/node-generic.png")); commentIcon = ImageIO.read(NetworkView.class.getResourceAsStream("/comment-icon.png")); commentBox = ImageIO.read(NetworkView.class.getResourceAsStream("/notes-background.png")); } catch (IOException e) { throw new RuntimeException(e); } PORT_COLORS.put(Port.TYPE_INT, new Color(116, 119, 121)); PORT_COLORS.put(Port.TYPE_FLOAT, new Color(116, 119, 121)); PORT_COLORS.put(Port.TYPE_STRING, new Color(92, 90, 91)); PORT_COLORS.put(Port.TYPE_BOOLEAN, new Color(92, 90, 91)); PORT_COLORS.put(Port.TYPE_POINT, new Color(119, 154, 173)); PORT_COLORS.put(Port.TYPE_COLOR, new Color(94, 85, 112)); PORT_COLORS.put("geometry", new Color(20, 20, 20)); PORT_COLORS.put("list", new Color(76, 137, 174)); PORT_COLORS.put("data", new Color(52, 85, 129)); } /** * 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. * <p/> * If this node has no image, the prototype is searched to find its image. If no image could be found, * a generic image is returned. * * @param node the node * @param nodeRepository the list of nodes to look for the icon * @return an Image object. */ public static BufferedImage getImageForNode(Node node, NodeRepository nodeRepository) { for (NodeLibrary library : nodeRepository.getLibraries()) { BufferedImage img = findNodeImage(library, node); if (img != null) { return img; } } if (node.getPrototype() != null) { return getImageForNode(node.getPrototype(), nodeRepository); } else { return nodeGeneric; } } public static BufferedImage findNodeImage(NodeLibrary library, Node node) { if (node == null || node.getImage() == null || node.getImage().isEmpty()) return null; if (!library.getRoot().hasChild(node)) return null; File libraryDirectory = null; if (library.getFile() != null) libraryDirectory = library.getFile().getParentFile(); else if (library.equals(NodeLibrary.coreLibrary)) libraryDirectory = new File("libraries/core"); if (libraryDirectory != null) { File nodeImageFile = new File(libraryDirectory, node.getImage()); if (nodeImageFile.exists()) { return readNodeImage(nodeImageFile); } } return null; } public static BufferedImage readNodeImage(File nodeImageFile) { String imagePath = nodeImageFile.getAbsolutePath(); if (fileImageCache.containsKey(imagePath)) { return fileImageCache.get(imagePath); } else { try { BufferedImage image = ImageIO.read(nodeImageFile); fileImageCache.put(imagePath, image); return image; } catch (IOException e) { return null; } } } public NetworkView(NodeBoxDocument document) { super(MIN_ZOOM, MAX_ZOOM); this.document = document; setBackground(Theme.NETWORK_BACKGROUND_COLOR); initEventHandlers(); initMenus(); nodeImageCache = CacheBuilder.newBuilder() .maximumSize(1000) .build(new NodeImageCacheLoader(document.getNodeRepository())); } private void initEventHandlers() { setFocusable(true); // This is disabled so we can detect the tab key. setFocusTraversalKeysEnabled(false); addKeyListener(new KeyHandler()); MouseHandler mh = new MouseHandler(); addMouseListener(mh); addMouseMotionListener(mh); addFocusListener(new FocusHandler()); } private void initMenus() { networkMenu = new JPopupMenu(); networkMenu.add(new NewNodeAction()); networkMenu.add(new ResetViewAction()); networkMenu.add(new GoUpAction()); } private JPopupMenu createNodeMenu(Node node) { JPopupMenu menu = new JPopupMenu(); menu.add(new SetRenderedAction()); menu.add(new RenameAction()); menu.add(new DeleteAction()); menu.add(new GroupIntoNetworkAction(null)); if (node.isNetwork()) { menu.add(new GoInAction()); } if (!node.hasComment()) { menu.add(new AddCommentAction()); } else { menu.add(new EditCommentAction()); menu.add(new RemoveCommentAction()); } menu.add(new HelpAction()); return menu; } public NodeBoxDocument getDocument() { return document; } public Node getActiveNetwork() { return document.getActiveNetwork(); } //// Events //// /** * Refresh the nodes and connections cache. */ public void updateAll() { updateNodes(); updateConnections(); } public void updateNodes() { repaint(); } public void updateConnections() { repaint(); } public void updatePosition(Node node) { updateConnections(); } public void checkErrorAndRepaint() { // TODO Check for errors in an efficient way. } public void codeChanged(Node node, boolean changed) { repaint(); } //// Model queries //// private ImmutableList<Node> getNodes() { return getDocument().getActiveNetwork().getChildren(); } private ImmutableList<Node> getNodesReversed() { return getNodes().reverse(); } private Iterable<Connection> getConnections() { return getDocument().getActiveNetwork().getConnections(); } public static boolean isPublished(Node network, Node childNode, Port childPort) { return network.hasPublishedInput(childNode.getName(), childPort.getName()); } //// Painting the nodes //// @Override protected void paintComponent(Graphics g) { 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(Theme.NETWORK_BACKGROUND_COLOR); g2.fill(g.getClipBounds()); // Paint the grid // (The grid is not really affected by the view transform) paintGrid(g2); // Set the view transform AffineTransform originalTransform = g2.getTransform(); g2.transform(getViewTransform()); paintNodes(g2); paintConnections(g2); paintCurrentConnection(g2); paintPortTooltip(g2); paintDragSelection(g2); paintCommentBox(g2); // Restore original transform g2.setTransform(originalTransform); } private void paintGrid(Graphics2D g) { g.setColor(Theme.NETWORK_GRID_COLOR); int gridCellSize = (int) Math.round(GRID_CELL_SIZE * getViewScale()); int gridOffset = (int) Math.round(GRID_OFFSET * getViewScale()); if (gridCellSize < 10) return; int transformOffsetX = (int) (getViewX() % gridCellSize); int transformOffsetY = (int) (getViewY() % gridCellSize); for (int y = -gridCellSize; y < getHeight() + gridCellSize; y += gridCellSize) { g.drawLine(0, y - gridOffset + transformOffsetY, getWidth(), y - gridOffset + transformOffsetY); } for (int x = -gridCellSize; x < getWidth() + gridCellSize; x += gridCellSize) { g.drawLine(x - gridOffset + transformOffsetX, 0, x - gridOffset + transformOffsetX, getHeight()); } } private void paintConnections(Graphics2D g) { g.setColor(Theme.CONNECTION_DEFAULT_COLOR); g.setStroke(CONNECTION_STROKE); for (Connection connection : getConnections()) { paintConnection(g, connection); } } private void paintConnection(Graphics2D g, Connection connection) { Node outputNode = findNodeWithName(connection.getOutputNode()); Node inputNode = findNodeWithName(connection.getInputNode()); Port inputPort = inputNode.getInput(connection.getInputPort()); g.setColor(portTypeColor(outputNode.getOutputType())); Rectangle outputRect = nodeRect(outputNode); Rectangle inputRect = nodeRect(inputNode); paintConnectionLine(g, outputRect.x + 4, outputRect.y + outputRect.height + 1, inputRect.x + portOffset(inputNode, inputPort) + 4, inputRect.y - 4); } private void paintCurrentConnection(Graphics2D g) { g.setColor(Theme.CONNECTION_DEFAULT_COLOR); if (connectionOutput != null) { Rectangle outputRect = nodeRect(connectionOutput); g.setColor(portTypeColor(connectionOutput.getOutputType())); paintConnectionLine(g, outputRect.x + 4, outputRect.y + outputRect.height + 1, (int) connectionPoint.getX(), (int) connectionPoint.getY()); } } private static void paintConnectionLine(Graphics2D g, int x0, int y0, int x1, int y1) { double dy = Math.abs(y1 - y0); if (dy < GRID_CELL_SIZE) { g.drawLine(x0, y0, x1, y1); } else { double halfDx = Math.abs(x1 - x0) / 2.0; GeneralPath p = new GeneralPath(); p.moveTo(x0, y0); p.curveTo(x0, y0 + halfDx, x1, y1 - halfDx, x1, y1); g.draw(p); } } private void paintNodes(Graphics2D g) { g.setColor(Theme.NETWORK_NODE_NAME_COLOR); Node renderedNode = getActiveNetwork().getRenderedChild(); for (Node node : getNodes()) { Port hoverInputPort = overInput != null && overInput.node.equals(node.getName()) ? findNodeWithName(overInput.node).getInput(overInput.port) : null; BufferedImage icon = getCachedImageForNode(node); paintNode(g, getActiveNetwork(), node, icon, commentIcon, isSelected(node), renderedNode == node, connectionOutput, hoverInputPort, overOutput == node); } } private BufferedImage getCachedImageForNode(Node node) { try { return nodeImageCache.get(node); } catch (ExecutionException e) { throw new RuntimeException(e); } } public static Color portTypeColor(String type) { Color portColor = PORT_COLORS.get(type); return portColor == null ? DEFAULT_PORT_COLOR : portColor; } private static String getShortenedName(String name, int startChars) { nodebox.graphics.Text text = new nodebox.graphics.Text(name, nodebox.graphics.Point.ZERO); text.setFontName(Theme.NETWORK_FONT.getFontName()); text.setFontSize(Theme.NETWORK_FONT.getSize()); int cells = Math.min(Math.max(3, 1 + (int) Math.ceil(text.getMetrics().getWidth() / (GRID_CELL_SIZE - 6))), 6); if (cells > 3) return getShortenedName(name.substring(0, startChars) + "\u2026" + name.substring(name.length() - 3, name.length()), startChars - 1); return name; } private void paintNode(Graphics2D g, Node network, Node node, BufferedImage icon, BufferedImage commentIcon, boolean selected, boolean rendered, Node connectionOutput, Port hoverInputPort, boolean hoverOutput) { Rectangle r = nodeRect(node); String outputType = node.getOutputType(); // Draw selection ring if (selected) { g.setColor(Color.WHITE); g.fillRect(r.x, r.y, NODE_WIDTH, NODE_HEIGHT); } // Draw node g.setColor(portTypeColor(outputType)); if (selected) { g.fillRect(r.x + 2, r.y + 2, NODE_WIDTH - 4, NODE_HEIGHT - 4); } else { g.fillRect(r.x, r.y, NODE_WIDTH, NODE_HEIGHT); } // Draw render flag if (rendered) { g.setColor(Color.WHITE); GeneralPath gp = new GeneralPath(); gp.moveTo(r.x + NODE_WIDTH - 2, r.y + NODE_HEIGHT - 20); gp.lineTo(r.x + NODE_WIDTH - 2, r.y + NODE_HEIGHT - 2); gp.lineTo(r.x + NODE_WIDTH - 20, r.y + NODE_HEIGHT - 2); g.fill(gp); } // Draw input ports g.setColor(Color.WHITE); int portX = 0; for (Port input : node.getInputs()) { if (isHiddenPort(input)) { continue; } if (hoverInputPort == input) { g.setColor(PORT_HOVER_COLOR); } else { g.setColor(portTypeColor(input.getType())); } // Highlight ports that match the dragged connection type int portHeight = PORT_HEIGHT; if (connectionOutput != null) { String connectionOutputType = connectionOutput.getOutputType(); String inputType = input.getType(); if (connectionOutputType.equals(inputType) || inputType.equals(Port.TYPE_LIST)) { portHeight = PORT_HEIGHT * 2; } else if (TypeConversions.canBeConverted(connectionOutputType, inputType)) { portHeight = PORT_HEIGHT - 1; } else { portHeight = 1; } } if (isPublished(network, node, input)) { Point2D topLeft = inverseViewTransformPoint(new Point(4, 0)); g.setColor(portTypeColor(input.getType())); g.setStroke(CONNECTION_STROKE); paintConnectionLine(g, (int) topLeft.getX(), (int) topLeft.getY(), r.x + portX + 4, r.y - 2); } g.fillRect(r.x + portX, r.y - portHeight, PORT_WIDTH, portHeight); portX += PORT_WIDTH + PORT_SPACING; } // Draw output port if (hoverOutput && connectionOutput == null) { g.setColor(PORT_HOVER_COLOR); } else { g.setColor(portTypeColor(outputType)); } g.fillRect(r.x, r.y + NODE_HEIGHT, PORT_WIDTH, PORT_HEIGHT); // Draw icon g.drawImage(icon, r.x + NODE_PADDING, r.y + NODE_PADDING, NODE_ICON_SIZE, NODE_ICON_SIZE, null); g.setColor(Color.WHITE); g.setFont(Theme.NETWORK_FONT); g.drawString(getShortenedName(node.getName(), 7), r.x + NODE_ICON_SIZE + NODE_PADDING * 2 + 2, r.y + 22); // Draw comment icon if (node.hasComment()) { g.drawImage(commentIcon, r.x + NODE_WIDTH - 13, r.y + 5, null); } } private void paintPortTooltip(Graphics2D g) { if (overInput != null) { Node overInputNode = findNodeWithName(overInput.node); Port overInputPort = overInputNode.getInput(overInput.port); Rectangle r = inputPortRect(overInputNode, overInputPort); Point2D pt = new Point2D.Double(r.getX(), r.getY() + 11); String text = String.format("%s (%s)", overInput.port, overInputPort.getType()); paintTooltip(g, pt, text); } else if (overOutput != null && connectionOutput == null) { Rectangle r = outputPortRect(overOutput); Point2D pt = new Point2D.Double(r.getX(), r.getY() + 11); String text = String.format("output (%s)", overOutput.getOutputType()); paintTooltip(g, pt, text); } } private static void paintTooltip(Graphics2D g, Point2D point, String text) { FontMetrics fontMetrics = g.getFontMetrics(); int textWidth = fontMetrics.stringWidth(text); int verticalOffset = 10; Rectangle r = new Rectangle((int) point.getX(), (int) point.getY() + verticalOffset, textWidth, fontMetrics.getHeight()); r.grow(4, 3); g.setColor(TOOLTIP_STROKE_COLOR); g.drawRoundRect(r.x, r.y, r.width, r.height, 8, 8); g.setColor(TOOLTIP_BACKGROUND_COLOR); g.fillRoundRect(r.x, r.y, r.width, r.height, 8, 8); g.setColor(TOOLTIP_TEXT_COLOR); g.drawString(text, (float) point.getX(), (float) point.getY() + fontMetrics.getAscent() + verticalOffset); } private void paintCommentBox(Graphics2D g) { if (overComment != null) { Rectangle r = nodeRect(overComment); FontMetrics fontMetrics = g.getFontMetrics(); int commentWidth = fontMetrics.stringWidth(overComment.getComment()); int x = r.x + 16; int y = r.y + GRID_CELL_SIZE - 5; g.setColor(Color.DARK_GRAY); g.fillRect(x + 1, y + 1, commentWidth + COMMENT_BOX_MARGIN_HORIZONTAL * 2, commentBox.getHeight()); g.drawImage(commentBox, x, y, commentWidth + COMMENT_BOX_MARGIN_HORIZONTAL * 2, commentBox.getHeight(), null); g.setColor(Color.DARK_GRAY); g.drawString(overComment.getComment(), x + COMMENT_BOX_MARGIN_HORIZONTAL, y + 14); } } private void paintDragSelection(Graphics2D g) { if (isDragSelecting) { Rectangle r = dragSelectRect(); g.setColor(DRAG_SELECTION_COLOR); g.setStroke(DRAG_SELECTION_STROKE); g.fill(r); // To get a smooth line we need to subtract one from the width and height. g.drawRect((int) r.getX(), (int) r.getY(), (int) r.getWidth() - 1, (int) r.getHeight() - 1); } } private Rectangle dragSelectRect() { if (dragStartPoint == null || dragCurrentPoint == null) return new Rectangle(); int x0 = (int) dragStartPoint.getX(); int y0 = (int) dragStartPoint.getY(); int x1 = (int) dragCurrentPoint.getX(); int y1 = (int) dragCurrentPoint.getY(); int x = Math.min(x0, x1); int y = Math.min(y0, y1); int w = (int) Math.abs(dragCurrentPoint.getX() - dragStartPoint.getX()); int h = (int) Math.abs(dragCurrentPoint.getY() - dragStartPoint.getY()); return new Rectangle(x, y, w, h); } private static Rectangle nodeRect(Node node) { return new Rectangle(nodePoint(node), NODE_DIMENSION); } private static Rectangle inputPortRect(Node node, Port port) { if (isHiddenPort(port)) return new Rectangle(); Point pt = nodePoint(node); Rectangle portRect = new Rectangle(pt.x + portOffset(node, port), pt.y - PORT_HEIGHT, PORT_WIDTH, PORT_HEIGHT); growHitRectangle(portRect); return portRect; } private static Rectangle outputPortRect(Node node) { Point pt = nodePoint(node); Rectangle portRect = new Rectangle(pt.x, pt.y + NODE_HEIGHT, PORT_WIDTH, PORT_HEIGHT); growHitRectangle(portRect); return portRect; } private static void growHitRectangle(Rectangle r) { r.grow(2, 2); } private static Point nodePoint(Node node) { int nodeX = ((int) node.getPosition().getX()) * GRID_CELL_SIZE; int nodeY = ((int) node.getPosition().getY()) * GRID_CELL_SIZE; return new Point(nodeX, nodeY); } private Point pointToGridPoint(Point e) { Point2D pt = getInverseViewTransform().transform(e, null); return new Point( (int) Math.floor(pt.getX() / GRID_CELL_SIZE), (int) Math.floor(pt.getY() / GRID_CELL_SIZE)); } public Point centerGridPoint() { Point pt = pointToGridPoint(new Point((int) (getBounds().getWidth() / 2), (int) (getBounds().getHeight() / 2))); return new Point((int) pt.getX() - 1, (int) pt.getY()); } private static int portOffset(Node node, Port port) { int portIndex = node.getInputs().indexOf(port); return (PORT_WIDTH + PORT_SPACING) * portIndex; } //// View queries //// private Node findNodeWithName(String name) { return getActiveNetwork().getChild(name); } public Node getNodeAt(Point2D point) { for (Node node : getNodesReversed()) { Rectangle r = nodeRect(node); if (r.contains(point)) { return node; } } return null; } public Node getNodeWithOutputPortAt(Point2D point) { for (Node node : getNodesReversed()) { Rectangle r = outputPortRect(node); if (r.contains(point)) { return node; } } return null; } public NodePort getInputPortAt(Point2D point) { for (Node node : getNodesReversed()) { for (Port port : node.getInputs()) { Rectangle r = inputPortRect(node, port); if (r.contains(point)) { return NodePort.of(node.getName(), port.getName()); } } } return null; } /** * Check if there is a commented node at a given point * * @param point The point that the mouse produces a MouseEvent * @return the Node if it exist at the given point */ public Node getNodeWithCommentAt(Point2D point) { for (Node node : getNodesReversed()) { if (node.hasComment()) { Rectangle r = nodeRect(node); if (r.contains(point)) { return node; } } } return null; } private static boolean isHiddenPort(Port port) { return port.getType().equals(Port.TYPE_STATE) || port.getType().equals(Port.TYPE_CONTEXT); } @Override protected void onViewTransformChanged(double viewX, double viewY, double viewScale) { document.setActiveNetworkPanZoom(viewX, viewY, viewScale); } //// Selections //// public boolean isSelected(Node node) { return (selectedNodes.contains(node.getName())); } public void select(Node node) { selectedNodes.add(node.getName()); } /** * Select this node, and only this node. * <p/> * All other selected nodes will be deselected. * * @param node The node to select. If node is null, everything is deselected. */ public void singleSelect(Node node) { if (selectedNodes.size() == 1 && selectedNodes.contains(node.getName())) return; selectedNodes.clear(); if (node != null && getActiveNetwork().hasChild(node)) { selectedNodes.add(node.getName()); firePropertyChange(SELECT_PROPERTY, null, selectedNodes); document.setActiveNode(node); } repaint(); } public void select(Iterable<Node> nodes) { selectedNodes.clear(); for (Node node : nodes) { selectedNodes.add(node.getName()); } } public void toggleSelection(Node node) { checkNotNull(node); if (selectedNodes.isEmpty()) { singleSelect(node); } else { if (selectedNodes.contains(node.getName())) { selectedNodes.remove(node.getName()); } else { selectedNodes.add(node.getName()); } firePropertyChange(SELECT_PROPERTY, null, selectedNodes); repaint(); } } public void deselectAll() { if (selectedNodes.isEmpty()) return; selectedNodes.clear(); firePropertyChange(SELECT_PROPERTY, null, selectedNodes); document.setActiveNode((Node) null); repaint(); } public Iterable<String> getSelectedNodeNames() { return selectedNodes; } public Iterable<Node> getSelectedNodes() { if (selectedNodes.isEmpty()) return ImmutableList.of(); ImmutableList.Builder<Node> b = new ImmutableList.Builder<nodebox.node.Node>(); for (String name : getSelectedNodeNames()) { b.add(findNodeWithName(name)); } return b.build(); } public void deleteSelection() { // Delete the nodes from the document document.removeNodes(getSelectedNodes()); // Remove the deleted nodes from the current selection selectedNodes.clear(); } private void moveSelectedNodes(int dx, int dy) { for (Node node : getSelectedNodes()) { getDocument().setNodePosition(node, node.getPosition().moved(dx, dy)); } } private void renameNode(Node node) { String s = JOptionPane.showInputDialog(this, "New name (no spaces, don't start with a digit):", node.getName()); if (s == null || s.length() == 0) return; try { getDocument().setNodeName(node, s); } catch (InvalidNameException ex) { JOptionPane.showMessageDialog(this, "The given name is not valid.\n" + ex.getMessage(), Application.NAME, JOptionPane.ERROR_MESSAGE); } catch (Exception ex) { JOptionPane.showMessageDialog(this, "An error occurred:\n" + ex.getMessage(), Application.NAME, JOptionPane.ERROR_MESSAGE); } } /** * Show an input dialog to insert a new comment. */ private void addComment(Node node) { String comment = JOptionPane.showInputDialog(this, "New comment:"); if (comment != null && !comment.trim().isEmpty()) { getDocument().setNodeComment(node, comment); } } private void editComment(Node node) { String comment = JOptionPane.showInputDialog(this, "Edit comment:", node.getComment()); getDocument().setNodeComment(node, comment); } //// Network navigation //// private void goUp() { if (getDocument().getActiveNetworkPath().equals("/")) return; Iterable<String> it = Splitter.on("/").split(getDocument().getActiveNetworkPath()); int parts = Iterables.size(it); String path = parts - 1 > 1 ? Joiner.on("/").join(Iterables.limit(it, parts - 1)) : "/"; getDocument().setActiveNetwork(path); } //// Input Events //// private class KeyHandler extends KeyAdapter { public void keyTyped(KeyEvent e) { switch (e.getKeyChar()) { case KeyEvent.VK_BACK_SPACE: getDocument().deleteSelection(); break; } } public void keyPressed(KeyEvent e) { int keyCode = e.getKeyCode(); if (keyCode == KeyEvent.VK_SHIFT) { isShiftPressed = true; } else if (keyCode == KeyEvent.VK_ALT) { isAltPressed = true; } else if (keyCode == KeyEvent.VK_UP) { moveSelectedNodes(0, -1); } else if (keyCode == KeyEvent.VK_RIGHT) { moveSelectedNodes(1, 0); } else if (keyCode == KeyEvent.VK_DOWN) { moveSelectedNodes(0, 1); } else if (keyCode == KeyEvent.VK_LEFT) { moveSelectedNodes(-1, 0); } } public void keyReleased(KeyEvent e) { if (e.getKeyCode() == KeyEvent.VK_SHIFT) { isShiftPressed = false; } else if (e.getKeyCode() == KeyEvent.VK_ALT) { isAltPressed = false; } } } private class MouseHandler implements MouseListener, MouseMotionListener { public void mouseClicked(MouseEvent e) { Point2D pt = inverseViewTransformPoint(e.getPoint()); if (e.getButton() == MouseEvent.BUTTON1) { if (e.getClickCount() == 1) { Node clickedNode = getNodeAt(pt); if (clickedNode == null) { deselectAll(); } else { if (isShiftPressed) { toggleSelection(clickedNode); } else { singleSelect(clickedNode); } } } else if (e.getClickCount() == 2) { Node clickedNode = getNodeAt(pt); if (clickedNode == null) { Point gridPoint = pointToGridPoint(e.getPoint()); getDocument().showNodeSelectionDialog(gridPoint); } else { document.setRenderedNode(clickedNode); } } } } public void mousePressed(MouseEvent e) { if (e.isPopupTrigger()) { showPopup(e); } else if (isDragTrigger(e)) { } else { Point2D pt = inverseViewTransformPoint(e.getPoint()); // Check if we're over an output port. connectionOutput = getNodeWithOutputPortAt(pt); if (connectionOutput != null) return; // Check if we're over a connected input port. connectionInput = getInputPortAt(pt); if (connectionInput != null) { // We're over a port, but is it connected? Connection c = getActiveNetwork().getConnection(connectionInput.node, connectionInput.port); // Disconnect it, but start a new connection on the same node immediately. if (c != null) { getDocument().disconnect(c); connectionOutput = getActiveNetwork().getChild(c.getOutputNode()); connectionPoint = pt; } return; } // Check if we're pressing a node. Node pressedNode = getNodeAt(pt); if (pressedNode != null) { // Don't immediately set "isDragging." // We wait until we actually drag the first time to do the work. startDragging = true; return; } // We're creating a drag selection. isDragSelecting = true; dragStartPoint = pt; } } public void mouseReleased(MouseEvent e) { if (e.isPopupTrigger()) { showPopup(e); } else { isDraggingNodes = false; isDragSelecting = false; if (isAltPressed) getDocument().stopEditing(); if (connectionOutput != null && connectionInput != null) { getDocument().connect(connectionOutput.getName(), connectionInput.node, connectionInput.port); } connectionOutput = null; repaint(); } } public void mouseEntered(MouseEvent e) { grabFocus(); } public void mouseExited(MouseEvent e) { } public void mouseDragged(MouseEvent e) { Point2D pt = inverseViewTransformPoint(e.getPoint()); // Panning the view has the first priority. if (isPanning()) return; if (connectionOutput != null) { repaint(); connectionInput = getInputPortAt(pt); connectionPoint = pt; overOutput = getNodeWithOutputPortAt(pt); overInput = getInputPortAt(pt); } if (startDragging) { startDragging = false; Node pressedNode = getNodeAt(pt); if (pressedNode != null) { if (selectedNodes == null || selectedNodes.isEmpty() || !selectedNodes.contains(pressedNode.getName())) { singleSelect(pressedNode); } if (isAltPressed) { getDocument().startEdits("Copy Node"); getDocument().dragCopy(); } isDraggingNodes = true; dragPositions = selectedNodePositions(); dragStartPoint = pt; } else { isDraggingNodes = false; } } if (isDraggingNodes) { Point2D offset = minPoint(pt, dragStartPoint); int gridX = (int) Math.round(offset.getX() / GRID_CELL_SIZE); int gridY = (int) Math.round(offset.getY() / (float) GRID_CELL_SIZE); for (String name : selectedNodes) { nodebox.graphics.Point originalPosition = dragPositions.get(name); nodebox.graphics.Point newPosition = originalPosition.moved(gridX, gridY); getDocument().setNodePosition(findNodeWithName(name), newPosition); } } if (isDragSelecting) { dragCurrentPoint = pt; Rectangle r = dragSelectRect(); selectedNodes.clear(); for (Node node : getNodes()) { if (r.intersects(nodeRect(node))) { selectedNodes.add(node.getName()); } } repaint(); } } public void mouseMoved(MouseEvent e) { Point2D pt = inverseViewTransformPoint(e.getPoint()); overOutput = getNodeWithOutputPortAt(pt); overInput = getInputPortAt(pt); overComment = getNodeWithCommentAt(pt); // It is probably very inefficient to repaint the view every time the mouse moves. repaint(); } } public void zoom(double scaleDelta) { // todo: implement } public boolean containsPoint(Point point) { return isVisible() && getBounds().contains(point); } private void showPopup(MouseEvent e) { Point pt = e.getPoint(); NodePort nodePort = getInputPortAt(inverseViewTransformPoint(pt)); if (nodePort != null) { JPopupMenu pMenu = new JPopupMenu(); pMenu.add(new PublishAction(nodePort)); if (findNodeWithName(nodePort.getNode()).hasPublishedInput(nodePort.getPort())) pMenu.add(new GoToPortAction(nodePort)); pMenu.show(this, e.getX(), e.getY()); } else { Node pressedNode = getNodeAt(inverseViewTransformPoint(pt)); if (pressedNode != null) { JPopupMenu nodeMenu = createNodeMenu(pressedNode); nodeMenuLocation = pt; nodeMenu.show(this, e.getX(), e.getY()); } else { networkMenuLocation = pt; networkMenu.show(this, e.getX(), e.getY()); } } } private ImmutableMap<String, nodebox.graphics.Point> selectedNodePositions() { ImmutableMap.Builder<String, nodebox.graphics.Point> b = ImmutableMap.builder(); for (String nodeName : selectedNodes) { b.put(nodeName, findNodeWithName(nodeName).getPosition()); } return b.build(); } private Point2D minPoint(Point2D a, Point2D b) { return new Point2D.Double(a.getX() - b.getX(), a.getY() - b.getY()); } private class FocusHandler extends FocusAdapter { @Override public void focusLost(FocusEvent focusEvent) { isShiftPressed = false; isAltPressed = false; } } private static class NodeImageCacheLoader extends CacheLoader<Node, BufferedImage> { private NodeRepository nodeRepository; private NodeImageCacheLoader(NodeRepository nodeRepository) { this.nodeRepository = nodeRepository; } @Override public BufferedImage load(Node node) throws Exception { for (NodeLibrary library : nodeRepository.getLibraries()) { BufferedImage img = findNodeImage(library, node); if (img != null) { return img; } } if (node.getPrototype() != null) { return load(node.getPrototype()); } else { return nodeGeneric; } } } private class NewNodeAction extends AbstractAction { private NewNodeAction() { super("New Node"); } public void actionPerformed(ActionEvent e) { Point gridPoint = pointToGridPoint(networkMenuLocation); getDocument().showNodeSelectionDialog(gridPoint); } } private class ResetViewAction extends AbstractAction { private ResetViewAction() { super("Reset View"); } public void actionPerformed(ActionEvent e) { resetViewTransform(); } } private class GoUpAction extends AbstractAction { private GoUpAction() { super("Go Up"); } public void actionPerformed(ActionEvent e) { goUp(); } } private class PublishAction extends AbstractAction { private NodePort nodePort; private PublishAction(NodePort nodePort) { super(getActiveNetwork().hasPublishedInput(nodePort.getNode(), nodePort.getPort()) ? "Unpublish" : "Publish"); this.nodePort = nodePort; } public void actionPerformed(ActionEvent e) { if (getActiveNetwork().hasPublishedInput(nodePort.getNode(), nodePort.getPort())) { unpublish(); } else { publish(); } } private void unpublish() { Port port = getActiveNetwork().getPortByChildReference(nodePort.getNode(), nodePort.getPort()); getDocument().unpublish(port.getName()); } private void publish() { String s = JOptionPane.showInputDialog(NetworkView.this, "Publish as:", nodePort.getPort()); if (s == null || s.length() == 0) return; getDocument().publish(nodePort.getNode(), nodePort.getPort(), s); } } private class GoToPortAction extends AbstractAction { private NodePort nodePort; private GoToPortAction(NodePort nodePort) { super("Go to Port"); this.nodePort = nodePort; } public void actionPerformed(ActionEvent e) { getDocument().setActiveNetwork(Node.path(getDocument().getActiveNetworkPath(), nodePort.getNode())); // todo: visually indicate the origin port. // Node node = findNodeWithName(nodePort.getNode()); // Port publishedPort = node.getInput(nodePort.getPort()); // publishedPort.getChildNodeName() // publishedPort.getChildPortName() } } private class SetRenderedAction extends AbstractAction { private SetRenderedAction() { super("Set Rendered"); } public void actionPerformed(ActionEvent e) { Node node = getNodeAt(inverseViewTransformPoint(nodeMenuLocation)); document.setRenderedNode(node); } } private class RenameAction extends AbstractAction { private RenameAction() { super("Rename"); } public void actionPerformed(ActionEvent e) { Node node = getNodeAt(inverseViewTransformPoint(nodeMenuLocation)); if (node != null) { renameNode(node); } } } private class DeleteAction extends AbstractAction { private DeleteAction() { super("Delete"); putValue(ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_BACK_SPACE, 0)); } public void actionPerformed(ActionEvent e) { deleteSelection(); } } private class GoInAction extends AbstractAction { private GoInAction() { super("Edit Children"); } public void actionPerformed(ActionEvent e) { Node node = getNodeAt(inverseViewTransformPoint(nodeMenuLocation)); String childPath = Node.path(getDocument().getActiveNetworkPath(), node.getName()); getDocument().setActiveNetwork(childPath); } } private class GroupIntoNetworkAction extends AbstractAction { private Point gridPoint; private GroupIntoNetworkAction(Point gridPoint) { super("Group into Network"); this.gridPoint = gridPoint; } public void actionPerformed(ActionEvent e) { nodebox.graphics.Point position; if (gridPoint == null) position = getNodeAt(inverseViewTransformPoint(nodeMenuLocation)).getPosition(); else position = new nodebox.graphics.Point(gridPoint); getDocument().groupIntoNetwork(position); } } private class RemoveCommentAction extends AbstractAction { private RemoveCommentAction() { super("Remove Comment"); } public void actionPerformed(ActionEvent e) { Node node = getNodeAt(inverseViewTransformPoint(nodeMenuLocation)); if (node != null) { getDocument().setNodeComment(node, ""); // Since this node no longer has a comment, we're no longer over a comment node. overComment = null; repaint(); } } } private class EditCommentAction extends AbstractAction { private EditCommentAction() { super("Edit Comment"); } public void actionPerformed(ActionEvent e) { Node node = getNodeAt(inverseViewTransformPoint(nodeMenuLocation)); if (node != null) { editComment(node); } } } private class AddCommentAction extends AbstractAction { private AddCommentAction() { super("Add Comment"); } public void actionPerformed(ActionEvent e) { Node node = getNodeAt(inverseViewTransformPoint(nodeMenuLocation)); if (node != null) { addComment(node); repaint(); } } } private class HelpAction extends AbstractAction { private HelpAction() { super("Help"); } public void actionPerformed(ActionEvent e) { Node node = getNodeAt(inverseViewTransformPoint(nodeMenuLocation)); Node prototype = node.getPrototype(); for (NodeLibrary library : document.getNodeRepository().getLibraries()) { if (library.getRoot().hasChild(prototype)) { String libraryName = library.getName(); String nodeName = prototype.getName(); String nodeRef = String.format("http://nodebox.net/node/reference/%s/%s", libraryName, nodeName); Platform.openURL(nodeRef); return; } } JOptionPane.showMessageDialog(NetworkView.this, "There is no reference documentation for node " + prototype, Application.NAME, JOptionPane.WARNING_MESSAGE); } } }