package nodebox.node; import nodebox.node.event.*; import nodebox.util.FileUtils; import org.xml.sax.SAXException; import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.SAXParser; import javax.xml.parsers.SAXParserFactory; import java.io.*; import java.util.*; /** * A Node library stores a set of (possibly hierarchical) nodes. *

* Node libraries are both documents and libraries. By mentioning a node on the library search path, you can get * all the nodes. *

* Node libraries can be backed by files, and saved in that same file, or they can be stored in memory only. *

* This implementation of the node library only stores a root node. * Calling get(String) on the library actually forwards the call to getChild() on the root node. */ public class NodeLibrary { public static enum ExternalEvent {FRAME, CANVAS} public static final NodeLibrary BUILTINS = new NodeLibrary(); public static final String CANVAS_X = "canvasX"; public static final String CANVAS_Y = "canvasY"; public static final String CANVAS_WIDTH = "canvasWidth"; public static final String CANVAS_HEIGHT = "canvasHeight"; public static final String CANVAS_BACKGROUND = "canvasBackground"; public static final float DEFAULT_CANVAS_WIDTH = 1000f; public static final float DEFAULT_CANVAS_HEIGHT = 1000f; // TODO: If the background color can be changed somewhere, all libraries are changed. public static final nodebox.graphics.Color DEFAULT_CANVAS_BACKGROUND = new nodebox.graphics.Color(1); private String name; private File file; private Node rootNode; private float frame = 1F; private HashMap variables; private NodeCode code; private NodeEventBus eventBus = new NodeEventBus(); private CanvasListener canvasListener = new CanvasListener(); private DependencyGraph parameterGraph = new DependencyGraph(); private Map> externalDependencies = new HashMap>(); /** * Load a library from the given XML. *

* This library is not added to the manager. The manager is used only to look up prototypes. * You can add the library to the manager yourself using manager.add(), or by calling * manager.load(). * * @param libraryName the name of the new library * @param xml the xml data of the library * @param manager the manager used to look up node prototypes. * @return a new node library * @throws RuntimeException When the string could not be parsed. * @see nodebox.node.NodeLibraryManager#add(NodeLibrary) * @see nodebox.node.NodeLibraryManager#load(String, String) */ public static NodeLibrary load(String libraryName, String xml, NodeLibraryManager manager) throws RuntimeException { try { NodeLibrary library = new NodeLibrary(libraryName); load(library, new ByteArrayInputStream(xml.getBytes("UTF8")), manager); return library; } catch (ParserConfigurationException e) { throw new RuntimeException("Error in the XML parser configuration", e); } catch (SAXException e) { throw new RuntimeException("Error while parsing " + libraryName + ": " + e.getMessage(), e); } catch (IOException e) { throw new RuntimeException("I/O error while parsing.", e); } } /** * Load a library from the given file. *

* This library is not added to the manager. The manager is used only to look up prototypes. * You can add the library to the manager yourself using manager.add(), or by calling * manager.load(). * * @param f the file to load * @param manager the manager used to look up node prototypes. * @return a new node library * @throws RuntimeException When the file could not be found, or parsing failed. * @see nodebox.node.NodeLibraryManager#add(NodeLibrary) * @see nodebox.node.NodeLibraryManager#load(File) */ public static NodeLibrary load(File f, NodeLibraryManager manager) throws RuntimeException { try { // The library name is the file name without the ".ndbx" extension. // Chop off the .ndbx String libraryName = FileUtils.stripExtension(f); NodeLibrary library = new NodeLibrary(libraryName, f); load(library, new FileInputStream(f), manager); return library; } catch (ParserConfigurationException e) { throw new RuntimeException("Error in the XML parser configuration", e); } catch (SAXException e) { throw new RuntimeException("Error while parsing " + f + ": " + e.getMessage(), e); } catch (FileNotFoundException e) { throw new RuntimeException("File not found " + f, e); } catch (IOException e) { throw new RuntimeException("I/O error while parsing " + f, e); } } /** * This method gets called from the public load method and does the actual parsing. *

* The method requires a newly created (empty) library. Nodes are added to this library. * * @param library the newly created library * @param is the input stream data * @param manager the manager used for looking up prototypes. * @throws IOException when the data could not be loaded * @throws ParserConfigurationException when the parser is incorrectly configured * @throws SAXException when the data could not be parsed */ private static void load(NodeLibrary library, InputStream is, NodeLibraryManager manager) throws IOException, ParserConfigurationException, SAXException { SAXParserFactory spf = SAXParserFactory.newInstance(); SAXParser parser = spf.newSAXParser(); NDBXHandler handler = new NDBXHandler(library, manager); parser.parse(is, handler); setCanvasParameter(library, CANVAS_X); setCanvasParameter(library, CANVAS_Y); setCanvasParameter(library, CANVAS_WIDTH); setCanvasParameter(library, CANVAS_HEIGHT); setCanvasParameter(library, CANVAS_BACKGROUND); } private static void setCanvasParameter(NodeLibrary library, String name) { String valueAsString = library.getVariable(name); Parameter param = library.getRootNode().getParameter(name); if (param != null && valueAsString != null) param.set(param.parseValue(valueAsString)); } private NodeLibrary() { this.name = "builtins"; this.file = null; this.rootNode = null; this.variables = null; } public NodeLibrary(String name) { this(name, null); } public NodeLibrary(String name, File file) { this.name = name; this.file = file; this.rootNode = Node.ROOT_NODE.newInstance(this, "root"); this.variables = new LinkedHashMap(); Parameter pCanvasBackground = rootNode.addParameter(CANVAS_BACKGROUND, Parameter.Type.COLOR, DEFAULT_CANVAS_BACKGROUND); Parameter pCanvasWidth = rootNode.addParameter(CANVAS_WIDTH, Parameter.Type.FLOAT, DEFAULT_CANVAS_WIDTH); Parameter pCanvasHeight = rootNode.addParameter(CANVAS_HEIGHT, Parameter.Type.FLOAT, DEFAULT_CANVAS_HEIGHT); Parameter pCanvasX = rootNode.addParameter(CANVAS_X, Parameter.Type.FLOAT, 0f); Parameter pCanvasY = rootNode.addParameter(CANVAS_Y, Parameter.Type.FLOAT, 0f); pCanvasBackground.setLabel("Background Color"); pCanvasWidth.setLabel("Document Width"); pCanvasHeight.setLabel("Document Height"); pCanvasX.setLabel("Offset X"); pCanvasY.setLabel("Offset Y"); this.rootNode.setValue("_code", new WrapInCanvasCode()); // We listen to our own library for changes to canvas settings. // The listener object needs to be stored in a field, otherwise it will get garbage-collected. // The event bus only stores weak references. addListener(canvasListener); } public String getName() { return name; } public File getFile() { return file; } public void setFile(File file) { if (this.file != null) { throw new AssertionError("File can only be set if no file was set before."); } this.file = file; } //// Node management //// public Node getRootNode() { return rootNode; } public List getExportedNodes() { List allChildren = rootNode.getChildren(); List exportedChildren = new ArrayList(allChildren.size()); for (Node child : allChildren) { if (child.isExported()) { exportedChildren.add(child); } } return exportedChildren; } public void add(Node node) { if (node.getLibrary() != this) throw new AssertionError("This node is already added to another library."); // The root node can be null in only one case: when we're creating the builtins library. // In that case, the rootNode becomes the given node. if (rootNode == null) { rootNode = node; } else { rootNode.add(node); } } /** * Get a node from this library. *

* Only exported nodes are returned. If you want all nodes, use getRootNode().getChild() * * @param name the name of the node * @return the node, or null if a node with this name could not be found. */ public Node get(String name) { if ("root".equals(name)) return rootNode; return rootNode.getExportedChild(name); } /** * Get the node at the given absolute path. * This method does a best effort to get the most specific node. If it fails to find a given segment, * it stops and returns the parent. * * @param path the path to parse * @return a node somewhere within the path, hopefully at the end. * @see nodebox.node.Node#getAbsolutePath() */ public Node getNodeForPath(String path) { Node parent = getRootNode(); if (!path.startsWith("/")) return parent; for (String part : path.substring(1).split("/")) { Node child = parent.getChild(part); if (child == null) { break; } else { parent = child; } } return parent; } public Node remove(String name) { Node node = rootNode.getChild(name); if (node == null) return null; rootNode.remove(node); return node; } public boolean remove(Node node) { return rootNode.remove(node); } public int size() { return rootNode.size(); } public boolean contains(String nodeName) { return rootNode.containsChildNode(nodeName); } //// Variables //// public String[] getVariableNames() { return variables.keySet().toArray(new String[variables.keySet().size()]); } public String getVariable(String name) { return variables.get(name); } public void setVariable(String name, String value) { variables.put(name, value); } //// Code //// public void setCode(NodeCode code) { this.code = code; } public NodeCode getCode() { return code; } //// Animation //// public float getFrame() { return frame; } public void setFrame(float frame) { this.frame = frame; externalDependencyTriggered(ExternalEvent.FRAME); } //// Persistence ///// public void store() throws IOException, IllegalArgumentException { if (file == null) throw new IllegalArgumentException("Library was not loaded from a file and no file given to store."); store(file); } public void store(File f) throws IOException { file = f; NDBXWriter.write(this, f); } /** * Get the full XML data for this library and all of its nodes. * * @return an XML string */ public String toXml() { return NDBXWriter.asString(this); } //// Parameter dependencies //// /** * Add a dependency between two parameters. *

* Whenever the dependent node wants to update, it needs to check if the dependency * is clean. Also, whenever the dependency changes, the dependent gets notified. *

* Do not call this method directly. Instead, let Parameter create the dependencies by using setExpression(). * * @param dependency the parameter that provides the value * @param dependent the parameter that needs the value * @see nodebox.node.Parameter#setExpression(String) */ public void addParameterDependency(Parameter dependency, Parameter dependent) { parameterGraph.addDependency(dependency, dependent); } /** * Remove all dependencies this parameter has. *

* This method gets called when a parameter clears out its expression. *

* Do not call this method directly. Instead, let Parameter remove dependencies by using clearExpression(). * * @param p the parameter * @see nodebox.node.Parameter#clearExpression() */ public void removeParameterDependencies(Parameter p) { parameterGraph.removeDependencies(p); } /** * Remove all dependents this parameter has. *

* This method gets called when the parameter is about to be removed. It signal all of its dependent nodes * that the parameter will no longer be available. *

* Do not call this method directly. Instead, let Parameter remove dependents by using removeParameter(). * * @param p the parameter * @see Node#removeParameter(String) */ public void removeParameterDependents(Parameter p) { parameterGraph.removeDependents(p); } /** * Get all parameters that rely on this parameter. *

* These parameters all have expressions that point to this parameter. Whenever this parameter changes, * they get notified. *

* This list contains all "live" parameters when you call it. Please don't hold on to this list for too long, * since parameters can be added and removed at will. * * @param p the parameter * @return a list of parameters that depend on this parameter. This list can safely be modified. */ public Set getParameterDependents(Parameter p) { return parameterGraph.getDependents(p); } /** * Get all parameters this parameter depends on. *

* This list contains all "live" parameters when you call it. Please don't hold on to this list for too long, * since parameters can be added and removed at will. * * @param p the parameter * @return a list of parameters this parameter depends on. This list can safely be modified. */ public Set getParameterDependencies(Parameter p) { return parameterGraph.getDependencies(p); } //// External event dependencies //// /** * Indicates that Parameter p depends on an external event, such as a change to the frame or canvas size. * This method is called whenever an expression is set that refers to e.g. FRAME. *

* Whenever this external event happens, the parameter will be marked dirty. * * @param p the parameter * @param event the event */ public void addExternalDependency(Parameter p, ExternalEvent event) { HashSet parameters = externalDependencies.get(event); if (parameters == null) { parameters = new HashSet(); externalDependencies.put(event, parameters); } parameters.add(p); } /** * Removes all external dependencies for Parameter p. * This happens when an expression is cleared. * * @param p the parameter. */ public void removeExternalDependencies(Parameter p) { for (HashSet parameters : externalDependencies.values()) { for (Iterator iterator = parameters.iterator(); iterator.hasNext(); ) { Parameter parameter = iterator.next(); if (parameter == p) { iterator.remove(); } } } } /** * This method is called when an external event, such as a frame change, happened. *

* All the parameters that depend on this event will be marked dirty. * * @param event the event that was triggered */ public void externalDependencyTriggered(ExternalEvent event) { HashSet parameters = externalDependencies.get(event); if (parameters != null) { for (Parameter p : parameters) { p.markDirty(); } } } //// Events //// public void addListener(NodeEventListener l) { eventBus.addListener(l); } public boolean removeListener(NodeEventListener l) { return eventBus.removeListener(l); } public List getListeners() { return eventBus.getListeners(); } public void fireNodeDirty(Node source) { eventBus.send(new NodeDirtyEvent(source)); } public void fireNodeUpdated(Node source, ProcessingContext context) { eventBus.send(new NodeUpdatedEvent(source, context)); } public void fireNodeAttributeChanged(Node source, Node.Attribute attribute) { eventBus.send(new NodeAttributeChangedEvent(source, attribute)); } public void fireChildAdded(Node source, Node child) { eventBus.send(new ChildAddedEvent(source, child)); } public void fireChildRemoved(Node source, Node child) { eventBus.send(new ChildRemovedEvent(source, child)); } public void fireConnectionAdded(Node source, Connection c) { eventBus.send(new ConnectionAddedEvent(source, c)); } public void fireConnectionRemoved(Node source, Connection c) { eventBus.send(new ConnectionRemovedEvent(source, c)); } public void fireRenderedChildChanged(Node source, Node child) { eventBus.send(new RenderedChildChangedEvent(source, child)); } public void fireValueChanged(Node source, Parameter parameter) { eventBus.send(new ValueChangedEvent(source, parameter)); } //// Standard overrides //// @Override public String toString() { return getName(); } /** * Custom listener that listens to changes in canvas properties and triggers an external event change. */ private class CanvasListener implements NodeEventListener { public void receive(NodeEvent event) { if (event.getSource() != getRootNode()) return; if (!(event instanceof ValueChangedEvent)) return; ValueChangedEvent vce = (ValueChangedEvent) event; if (!vce.getParameter().getName().startsWith("canvas")) return; externalDependencyTriggered(ExternalEvent.CANVAS); } } }